Files
wehub-resource-sync 3454a55636
cffconvert / validate (push) Has been cancelled
ci-workflow / pre-commit (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (macos-latest) (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (ubuntu-latest) (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (windows-latest) (push) Has been cancelled
ci-workflow / Python 3.10 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.11 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.12 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.13 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.14 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.14t on macos-latest (push) Has been cancelled
ci-workflow / Python 3.10 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.11 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.12 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.13 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.14 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.14t on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.10 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.11 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.12 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.13 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.14 on windows-latest (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:46:15 +08:00

88 lines
3.1 KiB
Python

"""Regression test for the exponential beta-reduction DoS in
``nltk.sem.logic.ApplicationExpression.simplify`` (CWE-400).
Beta reduction substitutes the argument into the lambda body; when the body
references its bound variable more than once the argument is duplicated, so a
short expression built from nested duplicating lambdas (``(\\Y.(Y & Y))``)
reduces to an exponentially large normal form and exhausts CPU and memory.
``simplify`` now refuses with a ``ValueError`` once a single reduction's result
exceeds ``MAX_SIMPLIFY_SIZE`` subexpressions; ordinary reductions are unchanged.
"""
import multiprocessing
import os
from nltk.sem import Expression
from nltk.sem.logic import MAX_SIMPLIFY_SIZE, _exceeds_size
_DOUBLER = r"(\Y.(Y & Y))"
def _nest(k):
"""k nested doublers applied to P(a) (reduces to 2**k copies of P(a))."""
expr = "P(a)"
for _ in range(k):
expr = f"{_DOUBLER}({expr})"
return expr
def test_simplify_preserves_benign_reductions():
"""Ordinary beta reductions are unchanged."""
assert Expression.fromstring(r"(\x.P(x))(a)").simplify() == Expression.fromstring(
"P(a)"
)
assert Expression.fromstring(
f"{_DOUBLER}(P(a))"
).simplify() == Expression.fromstring("P(a) & P(a)")
# A non-application is returned untouched.
formula = r"\x.(P(x) -> Q(x))"
assert Expression.fromstring(formula).simplify() == Expression.fromstring(formula)
def test_simplify_under_cap_duplicates_as_before():
"""Under the cap, a duplicating reduction still doubles (2**k copies)."""
result = Expression.fromstring(_nest(5)).simplify()
assert str(result).count("P(a)") == 2**5
def test_max_simplify_size_is_configurable():
"""The cap is a positive int and the size check short-circuits correctly."""
assert isinstance(MAX_SIMPLIFY_SIZE, int) and MAX_SIMPLIFY_SIZE > 0
small = Expression.fromstring("P(a) & Q(b)")
assert _exceeds_size(small, 100) is False
assert _exceeds_size(small, 2) is True
def _blowup_worker():
"""Simplify a huge nested-doubler expression; exit 0 if the cap fires."""
try:
Expression.fromstring(_nest(100)).simplify()
os._exit(1) # completed without the cap firing -> blow-up not bounded
except ValueError:
os._exit(0) # expected: the size cap refused the reduction
except BaseException:
os._exit(3)
def test_simplify_bounds_exponential_blowup():
"""A nested-doubler expression must be refused quickly, not blow up.
Run in a spawned process with a hard deadline: with the cap the reduction
raises almost immediately, while the unbounded version needs exponential CPU
and memory at this size, so a regression is terminated instead of OOM-killing
or hanging the suite.
"""
ctx = multiprocessing.get_context("spawn")
proc = ctx.Process(target=_blowup_worker)
proc.start()
proc.join(30)
if proc.is_alive():
proc.terminate()
proc.join()
raise AssertionError(
"simplify() did not terminate: exponential beta-reduction regressed"
)
assert (
proc.exitcode == 0
), f"expected a ValueError from the size cap (exit {proc.exitcode})"