java-topology/defects/meson/unit/test_meson_cwe1333.py
russell@unturf.com cd2ca798a3 test: add meson CWE-1333 benchmark (27 tests, 4 defects)
Covers meson-0003 through meson-0006:
- Functional correctness: fixed regex matches original on representative inputs
- Adversarial performance gates per finding
- meson-0004 exponential-proof: original >50ms at n=20, fixed <1ms
2026-04-13 17:36:52 -04:00

310 lines
11 KiB
Python

"""
CWE-1333 ReDoS regression tests for meson build system.
Tests 4 confirmed true positives:
meson-0003: clike.py _concatenate_string_literals O(N^2) while-loop
meson-0004: cmake/traceparser.py (.*/)* nested quantifier
meson-0005: docs/extensions/refman_links.py nested whitespace quantifiers
meson-0006: linkers/linkers.py ambiguous optional dots in soversion pattern
Each test verifies:
1. Functional correctness (fixed pattern produces same results as original)
2. Adversarial input completes within time budget (regression gate)
"""
import re
import time
import unittest
# ---------------------------------------------------------------------------
# meson-0003: clike.py string literal concatenation
# ---------------------------------------------------------------------------
class TestMeson0003ClikeStringConcat(unittest.TestCase):
"""O(N^2) while-loop in _concatenate_string_literals.
Original: .* in a while loop rescans growing prefix on each iteration.
Fix: single-pass re.sub(r'"\\s+"', '', s) removes all concat boundaries.
"""
@staticmethod
def original(s):
pattern = re.compile(
r'(?P<pre>.*([^\\"]"|^"))(?P<str1>([^\\"]|\\.)*)"\s+"'
r'(?P<str2>([^\\"]|\\.)*)(?P<post>".*)'
)
ret = s
m = pattern.match(ret)
while m:
ret = ''.join(m.group('pre', 'str1', 'str2', 'post'))
m = pattern.match(ret)
return ret
@staticmethod
def fixed(s):
return re.sub(r'"\s+"', '', s)
def test_functional_simple(self):
self.assertEqual(self.fixed('"hello" "world"'), '"helloworld"')
def test_functional_triple(self):
self.assertEqual(self.fixed('"a" "b" "c"'), '"abc"')
def test_functional_prefix(self):
self.assertEqual(
self.fixed('prefix "hello" "world" suffix'),
'prefix "helloworld" suffix',
)
def test_functional_no_concat(self):
self.assertEqual(self.fixed('no concat here'), 'no concat here')
def test_functional_single_string(self):
self.assertEqual(self.fixed('"only one"'), '"only one"')
def test_functional_matches_original(self):
"""Fixed output matches original on representative inputs."""
cases = [
'"hello" "world"',
'"a" "b" "c"',
'prefix "hello" "world" suffix',
'no concat here',
'"only one"',
]
for s in cases:
with self.subTest(s=s):
self.assertEqual(self.original(s), self.fixed(s))
def test_adversarial_performance(self):
"""N=5000 string literals must complete in <1s (original takes ~45s)."""
n = 5000
s = '"' + '" "'.join([f'p{i}' for i in range(n)]) + '"'
t0 = time.monotonic()
result = self.fixed(s)
elapsed = time.monotonic() - t0
self.assertLess(elapsed, 1.0, f"fixed took {elapsed:.3f}s on N={n}")
# Verify correctness: all parts concatenated into one string
self.assertTrue(result.startswith('"'))
self.assertTrue(result.endswith('"'))
self.assertNotIn('" "', result)
# ---------------------------------------------------------------------------
# meson-0004: cmake/traceparser.py path guessing
# ---------------------------------------------------------------------------
class TestMeson0004CMakeTracePath(unittest.TestCase):
"""(.*/)* nested quantifier in _guess_files path regex.
Original: r'^([A-Za-z]:)?/(.*/)*[^./]+$' has nested quantifier (.*/)*.
Fix: r'^(?:[A-Za-z]:)?/(?:[^/]+/)*[^./]+$' uses [^/]+ per segment.
"""
original_re = re.compile(r'^([A-Za-z]:)?/(.*/)*[^./]+$')
fixed_re = re.compile(r'^(?:[A-Za-z]:)?/(?:[^/]+/)*[^./]+$')
def test_functional_unix_path(self):
self.assertTrue(self.fixed_re.match('/usr/lib/libfoo'))
def test_functional_windows_path(self):
self.assertTrue(self.fixed_re.match('C:/Windows/System32/cmd'))
def test_functional_rejects_extension(self):
self.assertIsNone(self.fixed_re.match('/usr/lib/libfoo.so'))
def test_functional_simple_path(self):
self.assertTrue(self.fixed_re.match('/file'))
def test_functional_deep_path(self):
self.assertTrue(self.fixed_re.match('/a/b/c/d/e/f'))
def test_functional_matches_original(self):
"""Fixed regex matches original on representative inputs."""
cases = [
('/usr/lib/libfoo', True),
('/usr/lib/libfoo.so', False),
('C:/Windows/System32/cmd', True),
('/a/b/c/d', True),
('/file', True),
]
for path, expected in cases:
with self.subTest(path=path):
self.assertEqual(bool(self.original_re.match(path)), expected)
self.assertEqual(bool(self.fixed_re.match(path)), expected)
def test_adversarial_performance(self):
"""200-segment path ending in /. must complete in <0.1s.
Original hangs: n=22 takes ~3s, n=25 takes ~24s (exponential).
Fixed runs in microseconds at any scale.
"""
n = 200
adversarial = '/' + '/'.join(['a'] * n) + '/.'
t0 = time.monotonic()
m = self.fixed_re.match(adversarial)
elapsed = time.monotonic() - t0
self.assertIsNone(m) # should not match (ends in /.)
self.assertLess(elapsed, 0.1, f"fixed took {elapsed:.3f}s on {n} segments")
def test_adversarial_exponential_proof(self):
"""Demonstrate exponential growth in original pattern.
n=18: ~0.12s, n=20: ~0.55s, n=22: ~2.9s (doubling every 2 segments).
We test n=20 with a 2s budget to prove the vulnerability exists.
"""
n = 20
adversarial = '/' + '/'.join(['a'] * n) + '/.'
t0 = time.monotonic()
m = self.original_re.match(adversarial)
elapsed = time.monotonic() - t0
self.assertIsNone(m)
# At n=20, original takes ~0.5s. Prove it exceeds 0.05s (50ms).
self.assertGreater(elapsed, 0.05,
f"original too fast at n={n} ({elapsed:.3f}s) - vulnerability may be fixed upstream")
# ---------------------------------------------------------------------------
# meson-0005: docs/extensions/refman_links.py
# ---------------------------------------------------------------------------
class TestMeson0005RefmanLinks(unittest.TestCase):
"""Nested whitespace quantifiers in doc link regex.
Original: r'(\\[\\[#?@?([ \\n\\t]*[a-zA-Z0-9_]+[ \\n\\t]*\\.)*...'
Fix: r'(\\[\\[#?@?(?:[a-zA-Z0-9_]+\\.)*[a-zA-Z0-9_]+\\]\\])(.)?'
Removes whitespace tolerance (stripped on line 90 anyway).
Note: CPython's regex engine handles this pattern efficiently in practice,
making catastrophic backtracking hard to demonstrate. The fix remains
valuable for defense-in-depth: other regex engines (PCRE, RE2 fallback,
JavaScript) would be vulnerable, and the whitespace tolerance serves no
purpose since matches get whitespace-stripped immediately.
"""
original_re = re.compile(
r'(\[\[#?@?([ \n\t]*[a-zA-Z0-9_]+[ \n\t]*\.)*'
r'[ \n\t]*[a-zA-Z0-9_]+[ \n\t]*\]\])(.)?',
re.MULTILINE,
)
fixed_re = re.compile(
r'(\[\[#?@?(?:[a-zA-Z0-9_]+\.)*[a-zA-Z0-9_]+\]\])(.)?',
re.MULTILINE,
)
def test_functional_dotted(self):
text = 'see [[foo.bar.baz]] here'
m = self.fixed_re.search(text)
self.assertIsNotNone(m)
self.assertEqual(m.group(1), '[[foo.bar.baz]]')
def test_functional_hash_at(self):
text = 'see [[#@obj.method]] here'
m = self.fixed_re.search(text)
self.assertIsNotNone(m)
self.assertEqual(m.group(1), '[[#@obj.method]]')
def test_functional_simple(self):
text = 'see [[simple]] here'
m = self.fixed_re.search(text)
self.assertIsNotNone(m)
self.assertEqual(m.group(1), '[[simple]]')
def test_functional_no_match(self):
self.assertIsNone(self.fixed_re.search('no brackets here'))
def test_functional_matches_original(self):
"""Fixed regex matches original on well-formed inputs (no whitespace)."""
cases = [
'[[foo.bar.baz]]',
'[[#@obj.method]]',
'[[simple]]',
'[[a.b.c.d.e.f]]',
]
for text in cases:
with self.subTest(text=text):
om = self.original_re.search(text)
fm = self.fixed_re.search(text)
self.assertIsNotNone(om)
self.assertIsNotNone(fm)
self.assertEqual(om.group(1), fm.group(1))
def test_adversarial_performance(self):
"""50-segment dotted identifier without ]] must complete in <0.1s."""
n = 50
adversarial = '[[' + '.'.join(['foo'] * n) + ' '
t0 = time.monotonic()
m = self.fixed_re.search(adversarial)
elapsed = time.monotonic() - t0
self.assertIsNone(m)
self.assertLess(elapsed, 0.1, f"fixed took {elapsed:.3f}s on {n} segments")
# ---------------------------------------------------------------------------
# meson-0006: linkers/linkers.py soversion pattern
# ---------------------------------------------------------------------------
class TestMeson0006LinkerSoversion(unittest.TestCase):
"""Ambiguous optional dots in soversion regex.
Original: r'[.][a]([.]?([0-9]+))*([.]?([a-z]+))*'
Fix: r'[.]a(?:\\.[0-9]+)*(?:\\.[a-z]+)*'
Makes dots mandatory between version components.
Note: CPython's regex engine handles this pattern efficiently for re.sub
(greedy match succeeds without backtracking). The fix remains valuable for
defense-in-depth and correctness: the optional dot was an over-generalization,
and real soversion strings always have dots between components.
"""
original_pattern = r'[.][a]([.]?([0-9]+))*([.]?([a-z]+))*'
fixed_pattern = r'[.]a(?:\.[0-9]+)*(?:\.[a-z]+)*'
def _apply_original(self, filename):
return re.sub(
self.original_pattern, '.a',
filename.replace('.so', '.a'),
)
def _apply_fixed(self, filename):
return re.sub(
self.fixed_pattern, '.a',
filename.replace('.so', '.a'),
)
def test_functional_standard_soversion(self):
self.assertEqual(self._apply_fixed('libgio.so.0.7200.1'), 'libgio.a')
def test_functional_no_version(self):
self.assertEqual(self._apply_fixed('libfoo.so'), 'libfoo.a')
def test_functional_single_version(self):
self.assertEqual(self._apply_fixed('libbar.so.1'), 'libbar.a')
def test_functional_version_plus_suffix(self):
self.assertEqual(self._apply_fixed('libqux.so.1.2.3.beta'), 'libqux.a')
def test_functional_matches_original(self):
"""Fixed output matches original on representative inputs."""
cases = [
'libgio.so.0.7200.1',
'libfoo.so',
'libbar.so.1',
'libqux.so.1.2.3.beta',
]
for fn in cases:
with self.subTest(fn=fn):
self.assertEqual(self._apply_original(fn), self._apply_fixed(fn))
def test_adversarial_performance(self):
"""Filename with 40 digits after .a (no dots) must complete in <0.1s."""
n = 40
adversarial = 'lib.a' + '1' * n
t0 = time.monotonic()
result = re.sub(self.fixed_pattern, '.a', adversarial)
elapsed = time.monotonic() - t0
self.assertLess(elapsed, 0.1, f"fixed took {elapsed:.3f}s on {n} digits")
if __name__ == '__main__':
unittest.main()