Commit 6698cd2c0 for clamav.net
commit 6698cd2c05e53a064f7a5450926cbadb171ddeb0
Author: Val S. <valsnyde@cisco.com>
Date: Wed Aug 19 10:39:44 2026 -0400
Stabilize Valgrind and scanner integration tests (#1805)
* Adapt Valgrind file descriptor checks
Valgrind 3.23 changed --track-fds=yes so descriptors left open at process exit count as real errors and trigger --error-exitcode. This caused the clamd and freshclam Valgrind tests to fail on intentional process-lifetime descriptors rather than memory defects.
Select the fd tracking mode from the installed Valgrind version. Preserve the original yes mode before 3.23, disable fd tracking on 3.23 through 3.25, and use the new bad mode on 3.26 and later so invalid descriptor use is still detected without reporting descriptors closed by process exit.
* Make clean scan assertions path-safe
The clamscan and clamd tests used the unanchored regular expression OK to decide whether a clean result appeared. Randomized temporary paths containing those letters caused virus-detection tests to fail even though the scanner output was correct.
Match the complete ': OK' result marker through the line ending and use the shared expression for every bare clean-result assertion. Also activate the LHA test's existing negative assertion so it checks the behavior it documents.
* Wait for clamd test readiness
The shared clamd startup helper returned immediately after creating the process. Tests then used inconsistent short ping windows, so slow startup under Valgrind could fail the first tests even while the daemon was still loading its databases.
Wait for clamdscan to receive PONG from the daemon using the selected configuration, allowing up to 60 attempts. Report early daemon exits and readiness timeouts at the startup boundary, and honor custom configurations in the non-Valgrind path.
diff --git a/unit_tests/clamd_test.py b/unit_tests/clamd_test.py
index fa90afdc0..31d3dbdb6 100644
--- a/unit_tests/clamd_test.py
+++ b/unit_tests/clamd_test.py
@@ -154,7 +154,7 @@ class TC(testcase.TestCase):
)
else:
command = '{clamd} --config-file={clamd_config}'.format(
- clamd=TC.clamd, clamd_config=TC.clamd_config
+ clamd=TC.clamd, clamd_config=clamd_config
)
self.log.info('Starting clamd: {}'.format(command))
self.proc = subprocess.Popen(
@@ -164,6 +164,19 @@ class TC(testcase.TestCase):
stderr=sys.stdout.buffer,
)
+ startup = self.execute_command(
+ '{clamdscan} --ping 60 -c {clamd_config}'.format(
+ clamdscan=TC.clamdscan,
+ clamd_config=clamd_config,
+ )
+ )
+ poll = self.proc.poll()
+ assert poll == None, (
+ 'clamd exited with status {} before becoming ready'.format(poll)
+ )
+ assert startup.ec == 0, 'clamd did not become ready:\n{}'.format(startup.err)
+ self.verify_output(startup.out, expected=['PONG'])
+
def run_clamdscan(self,
scan_args,
expected_ec=0,
@@ -846,7 +859,11 @@ class TC(testcase.TestCase):
output = self.execute_command('{clamdscan} -c {clamd_config} --wait --ping 10 {test_exe}'.format(
clamdscan=TC.clamdscan, clamd_config=clamd_config, test_exe=big_file))
expected_results = ['MaxFileSize FOUND']
- unexpected_results = ['OK', 'MaxScanSize FOUND', 'Can\'t allocate memory ERROR']
+ unexpected_results = [
+ testcase.CLEAN_SCAN_RESULT,
+ 'MaxScanSize FOUND',
+ 'Can\'t allocate memory ERROR',
+ ]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
assert output.ec == 1
@@ -854,7 +871,11 @@ class TC(testcase.TestCase):
output = self.execute_command('{clamdscan} -c {clamd_config} {test_exe}'.format(
clamdscan=TC.clamdscan, clamd_config=clamd_config, test_exe=big_zip))
expected_results = ['MaxScanSize FOUND']
- unexpected_results = ['OK', 'MaxFileSize FOUND', 'Can\'t allocate memory ERROR']
+ unexpected_results = [
+ testcase.CLEAN_SCAN_RESULT,
+ 'MaxFileSize FOUND',
+ 'Can\'t allocate memory ERROR',
+ ]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
assert output.ec == 1
diff --git a/unit_tests/clamscan/assorted_test.py b/unit_tests/clamscan/assorted_test.py
index ff55fa21b..9813f27e4 100644
--- a/unit_tests/clamscan/assorted_test.py
+++ b/unit_tests/clamscan/assorted_test.py
@@ -110,7 +110,7 @@ class TC(testcase.TestCase):
assert output.ec == 0
- expected_results = ['OK']
+ expected_results = [testcase.CLEAN_SCAN_RESULT]
# The alert sig files are all given the signature name, so we can verify that the correct sigs were found.
# We need only to trim off the extension and say "FOUND" for the alerting sigs.
@@ -211,7 +211,7 @@ class TC(testcase.TestCase):
'trust_plus_mal.zip: ClamAV-Test-File.UNOFFICIAL FOUND',
'trust_plus_mal2.zip: ClamAV-Test-File.UNOFFICIAL FOUND',
]
- unexpected_results = ['OK']
+ unexpected_results = [testcase.CLEAN_SCAN_RESULT]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
@@ -237,7 +237,7 @@ class TC(testcase.TestCase):
'iso_normal.logo.iso: logo.png.UNOFFICIAL FOUND',
'iso_no_joliet.logo.iso: logo.png.UNOFFICIAL FOUND',
]
- unexpected_results = ['OK']
+ unexpected_results = [testcase.CLEAN_SCAN_RESULT]
self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
diff --git a/unit_tests/clamscan/embedded_files_test.py b/unit_tests/clamscan/embedded_files_test.py
index 38e1672ce..b153a1f79 100644
--- a/unit_tests/clamscan/embedded_files_test.py
+++ b/unit_tests/clamscan/embedded_files_test.py
@@ -49,7 +49,7 @@ class TC(testcase.TestCase):
'test.png.emb-zips: test-file-2-2.UNOFFICIAL FOUND',
]
unexpected_stdout = [
- 'OK',
+ testcase.CLEAN_SCAN_RESULT,
]
self.verify_output(output.out, expected=expected_stdout, unexpected=unexpected_stdout)
@@ -75,7 +75,7 @@ class TC(testcase.TestCase):
'test.png.emb-arjs: test-file-2-2.UNOFFICIAL FOUND',
]
unexpected_stdout = [
- 'OK',
+ testcase.CLEAN_SCAN_RESULT,
]
self.verify_output(output.out, expected=expected_stdout, unexpected=unexpected_stdout)
@@ -101,7 +101,7 @@ class TC(testcase.TestCase):
'test.png.emb-cabs: test-file-2-2.UNOFFICIAL FOUND',
]
unexpected_stdout = [
- 'OK',
+ testcase.CLEAN_SCAN_RESULT,
]
self.verify_output(output.out, expected=expected_stdout, unexpected=unexpected_stdout)
@@ -125,6 +125,6 @@ class TC(testcase.TestCase):
'clam.exe.emb-exes: Win.Test.SmolEXE.UNOFFICIAL FOUND',
]
unexpected_stdout = [
- 'OK',
+ testcase.CLEAN_SCAN_RESULT,
]
self.verify_output(output.out, expected=expected_stdout, unexpected=unexpected_stdout)
diff --git a/unit_tests/clamscan/fp_check_test.py b/unit_tests/clamscan/fp_check_test.py
index c65266d24..306a1595b 100644
--- a/unit_tests/clamscan/fp_check_test.py
+++ b/unit_tests/clamscan/fp_check_test.py
@@ -140,7 +140,7 @@ rename()
db2=TC.normalized_hash_fp,
)
)
- self.verify_output(output.out, expected=["OK"], unexpected=[])
+ self.verify_output(output.out, expected=[testcase.CLEAN_SCAN_RESULT], unexpected=[])
def test_fp_for_normalized_fips_md5(self):
"""
@@ -198,7 +198,7 @@ rename()
db2=TC.normalized_hash_wild_fp,
)
)
- self.verify_output(output.out, expected=["OK"], unexpected=[])
+ self.verify_output(output.out, expected=[testcase.CLEAN_SCAN_RESULT], unexpected=[])
def test_fp_for_nonnormalized(self):
"""
@@ -215,7 +215,7 @@ rename()
db2=TC.original_hash_fp,
)
)
- self.verify_output(output.out, expected=["OK"], unexpected=[])
+ self.verify_output(output.out, expected=[testcase.CLEAN_SCAN_RESULT], unexpected=[])
def test_fp_for_nonnormalized_wild(self):
"""
@@ -232,7 +232,7 @@ rename()
db2=TC.original_hash_wild_fp,
)
)
- self.verify_output(output.out, expected=["OK"], unexpected=[])
+ self.verify_output(output.out, expected=[testcase.CLEAN_SCAN_RESULT], unexpected=[])
def test_fp_for_zipped_file(self):
"""
@@ -248,7 +248,7 @@ rename()
db2=TC.test_file_zipped_hash_fp,
)
)
- self.verify_output(output.out, expected=["OK"], unexpected=[])
+ self.verify_output(output.out, expected=[testcase.CLEAN_SCAN_RESULT], unexpected=[])
def test_fp_for_zipped_file_wild(self):
"""
@@ -264,4 +264,4 @@ rename()
db2=TC.test_file_zipped_hash_wild_fp,
)
)
- self.verify_output(output.out, expected=["OK"], unexpected=[])
+ self.verify_output(output.out, expected=[testcase.CLEAN_SCAN_RESULT], unexpected=[])
diff --git a/unit_tests/clamscan/lzh_lha_archive_test.py b/unit_tests/clamscan/lzh_lha_archive_test.py
index 8f317f18c..9650af480 100644
--- a/unit_tests/clamscan/lzh_lha_archive_test.py
+++ b/unit_tests/clamscan/lzh_lha_archive_test.py
@@ -54,6 +54,6 @@ class TC(testcase.TestCase):
'logo.png.UNOFFICIAL FOUND',
]
unexpected_stdout = [
- 'OK',
+ testcase.CLEAN_SCAN_RESULT,
]
- self.verify_output(output.out, expected=expected_stdout)
+ self.verify_output(output.out, expected=expected_stdout, unexpected=unexpected_stdout)
diff --git a/unit_tests/testcase.py b/unit_tests/testcase.py
index 8fc3a79ad..15e516a15 100644
--- a/unit_tests/testcase.py
+++ b/unit_tests/testcase.py
@@ -27,9 +27,37 @@ TIMEOUT_EXIT_CODE = 111
STRICT_ORDER = 0
ANY_ORDER = 1
CHUNK_SIZE = 100
+CLEAN_SCAN_RESULT = r": OK(?:\r?\n|$)"
loggers = {}
+_VALGRIND_TRACK_FDS_MODE = None
+
+
+def _get_valgrind_track_fds_mode(valgrind):
+ """Select the best fd tracking mode supported by Valgrind."""
+ global _VALGRIND_TRACK_FDS_MODE
+
+ if _VALGRIND_TRACK_FDS_MODE is None:
+ version_output = subprocess.check_output(
+ [str(valgrind), "--version"], universal_newlines=True
+ )
+ version = re.search(r"(\d+)\.(\d+)", version_output)
+
+ # Valgrind 3.23 made descriptors left open at process exit errors.
+ # The "bad" mode, added in 3.26, reports invalid descriptor use
+ # without treating descriptors inherited across exec or intentionally
+ # closed by process exit as failures.
+ version_tuple = tuple(map(int, version.groups())) if version else (0, 0)
+ if version_tuple >= (3, 26):
+ _VALGRIND_TRACK_FDS_MODE = "bad"
+ elif version_tuple >= (3, 23):
+ _VALGRIND_TRACK_FDS_MODE = "no"
+ else:
+ _VALGRIND_TRACK_FDS_MODE = "yes"
+
+ return _VALGRIND_TRACK_FDS_MODE
+
#TODO: replace w/ this when Python 3.5 support is dropped.
# class CmdResult(NamedTuple):
# ec: int
@@ -133,7 +161,8 @@ class TestCase(unittest.TestCase):
if os.getenv('VALGRIND') != None:
cls.log_suffix = '.valgrind.log'
cls.valgrind = Path(os.getenv("VALGRIND"))
- cls.valgrind_args = '-v --trace-children=yes --track-fds=yes --leak-check=full --show-possibly-lost=no ' + \
+ track_fds_mode = _get_valgrind_track_fds_mode(cls.valgrind)
+ cls.valgrind_args = '-v --trace-children=yes --track-fds={} --leak-check=full --show-possibly-lost=no '.format(track_fds_mode) + \
'--show-leak-kinds=definite --errors-for-leak-kinds=definite --main-stacksize=16777216 --gen-suppressions=all ' + \
'--suppressions={} '.format(cls.path_source / "unit_tests" / "valgrind.supp") + \
'--log-file={} '.format(cls.path_tmp / "valgrind.log") + \