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