Commit 1fb0bc6ba for clamav.net

commit 1fb0bc6ba96ee009d6073ad08a28a8ab039bd806
Author: John Humlick <15677335+jhumlick@users.noreply.github.com>
Date:   Fri Jul 31 16:10:26 2026 -0700

    libclamav: Verify scanning of extracted XLM formulas

    Add a valid ptgStr regression case and use a signature matching its
    disassembled output to verify detection through clamscan and scan-map.

    Flush the extracted XLM stream before scanning so buffered formula output
    is visible to the scanner.

    CLAM-3011

diff --git a/libclamav/xlm_extract.c b/libclamav/xlm_extract.c
index 4ecf8e939..1a898f31c 100644
--- a/libclamav/xlm_extract.c
+++ b/libclamav/xlm_extract.c
@@ -4983,6 +4983,12 @@ cl_error_t cli_extract_xlm_macros_and_images(const char *dir, cli_ctx *ctx, char
     }

     /* Scan the extracted content */
+    if (0 != fflush(out_file)) {
+        cli_dbgmsg("cli_extract_xlm_macros_and_images: Failed to flush extracted XLM macro content\n");
+        status = CL_EWRITE;
+        goto done;
+    }
+
     if (lseek(out_fd, 0, SEEK_SET) != 0) {
         cli_dbgmsg("cli_extract_xlm_macros_and_images: Failed to seek to beginning of temporary file\n");
         status = CL_ESEEK;
diff --git a/unit_tests/check_xlm_scanmap.c b/unit_tests/check_xlm_scanmap.c
index 4ebee825e..396074f58 100644
--- a/unit_tests/check_xlm_scanmap.c
+++ b/unit_tests/check_xlm_scanmap.c
@@ -61,19 +61,28 @@ done:
 int main(int argc, char **argv)
 {
     cl_error_t status;
-    struct cl_engine *engine = NULL;
+    struct cl_engine *engine     = NULL;
+    unsigned int signature_count = 0;
+    cl_error_t expected_status;
     int i;

-    if (argc < 2) {
-        fprintf(stderr, "Usage: %s FILE...\n", argv[0]);
+    if (argc < 4 || (0 != strcmp(argv[2], "clean") && 0 != strcmp(argv[2], "virus"))) {
+        fprintf(stderr, "Usage: %s DATABASE {clean|virus} FILE...\n", argv[0]);
         return 2;
     }
+    expected_status = 0 == strcmp(argv[2], "virus") ? CL_VIRUS : CL_SUCCESS;

     status = cl_init(CL_INIT_DEFAULT);
     if (CL_SUCCESS != status || NULL == (engine = cl_engine_new())) {
         fprintf(stderr, "Failed to initialize ClamAV: %s\n", cl_strerror(status));
         return 2;
     }
+    status = cl_load(argv[1], engine, &signature_count, CL_DB_STDOPT);
+    if (CL_SUCCESS != status) {
+        fprintf(stderr, "Failed to load signature database %s: %s\n", argv[1], cl_strerror(status));
+        cl_engine_free(engine);
+        return 2;
+    }
     status = cl_engine_compile(engine);
     if (CL_SUCCESS != status) {
         fprintf(stderr, "Failed to compile ClamAV engine: %s\n", cl_strerror(status));
@@ -81,10 +90,11 @@ int main(int argc, char **argv)
         return 2;
     }

-    for (i = 1; i < argc; i++) {
+    for (i = 3; i < argc; i++) {
         status = scan_file_as_map(argv[i], engine);
-        if (CL_SUCCESS != status) {
-            fprintf(stderr, "Scan-map failed for %s: %s\n", argv[i], cl_strerror(status));
+        if (expected_status != status) {
+            fprintf(stderr, "Scan-map returned %s for %s; expected %s\n",
+                    cl_strerror(status), argv[i], cl_strerror(expected_status));
             cl_engine_free(engine);
             return 1;
         }
diff --git a/unit_tests/clamscan/xlm_test.py b/unit_tests/clamscan/xlm_test.py
index 5e73d1e11..2c59a5ce5 100644
--- a/unit_tests/clamscan/xlm_test.py
+++ b/unit_tests/clamscan/xlm_test.py
@@ -121,6 +121,7 @@ class TC(testcase.TestCase):
     def test_formula_record_boundaries(self):
         self.step_name('Test XLM FORMULA records at the BIFF8 size boundary')

+        valid_string = b'XLM_VALID_FORMULA'
         testfiles = [
             TC.path_tmp / 'formula-8228.xls',
             TC.path_tmp / 'formula-8227.xls',
@@ -132,7 +133,9 @@ class TC(testcase.TestCase):
             TC.path_tmp / 'formula-8228-truncated-extended-function.xls',
             TC.path_tmp / 'formula-22-overdeclared-token-length.xls',
             TC.path_tmp / 'formula-8228-non-macro.xls',
+            TC.path_tmp / 'formula-valid-ptgstr.xls',
         ]
+        valid_token = b'\x17' + bytes([len(valid_string)]) + b'\x00' + valid_string
         _write_xlm_formula_workbook(testfiles[0], 8228)
         _write_xlm_formula_workbook(testfiles[1], 8227)
         _write_xlm_formula_workbook(testfiles[2], 100)
@@ -143,6 +146,7 @@ class TC(testcase.TestCase):
         _write_xlm_formula_workbook(testfiles[7], 8228, token_tail=b'\x22\x00\x6d\x80')
         _write_xlm_formula_workbook(testfiles[8], 22, declared_token_length=1)
         _write_xlm_formula_workbook(testfiles[9], 8228, macro_sheet=False)
+        _write_xlm_formula_workbook(testfiles[-1], 22 + len(valid_token), token_tail=valid_token)

         command = '{valgrind} {valgrind_args} {clamscan} -d {path_db} --debug {testfiles}'.format(
             valgrind=TC.valgrind,
@@ -160,10 +164,42 @@ class TC(testcase.TestCase):
         )
         assert output.err.count('[cli_extract_xlm_macros_and_images] Extracting macros to') == len(testfiles) - 1

-        scanmap_command = '{} {}'.format(
+        scanmap_command = '{} {} clean {}'.format(
             TC.check_xlm_scanmap,
+            TC.path_build / 'unit_tests' / 'input' / 'clamav.hdb',
             ' '.join(str(path) for path in testfiles),
         )
         scanmap_output = self.execute_command(scanmap_command)

         assert scanmap_output.ec == 0
+
+        signature = TC.path_tmp / 'xlm-formula.ndb'
+        signature.write_text(
+            'XLM.Formula.ptgStr:0:*:{}\n'.format(
+                (b' ptgStr' + valid_string).hex(),
+            )
+        )
+
+        detection_command = '{valgrind} {valgrind_args} {clamscan} -d {signature} {testfile}'.format(
+            valgrind=TC.valgrind,
+            valgrind_args=TC.valgrind_args,
+            clamscan=TC.clamscan,
+            signature=signature,
+            testfile=testfiles[-1],
+        )
+        detection_output = self.execute_command(detection_command)
+
+        assert detection_output.ec == 1
+        self.verify_output(
+            detection_output.out,
+            expected=['{}: XLM.Formula.ptgStr.UNOFFICIAL FOUND'.format(testfiles[-1].name)],
+        )
+
+        scanmap_detection_command = '{} {} virus {}'.format(
+            TC.check_xlm_scanmap,
+            signature,
+            testfiles[-1],
+        )
+        scanmap_detection_output = self.execute_command(scanmap_detection_command)
+
+        assert scanmap_detection_output.ec == 0