Files
nltk--nltk/nltk/test/unit/test_featstruct_redos.py
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

81 lines
3.3 KiB
Python

"""Regression tests for the quadratic ReDoS in feature-structure variable
renaming (CWE-1333; CVE-2026-12919).
``nltk.featstruct._rename_variable`` strips a trailing run of digits from a
variable name with ``re.sub(r"\\d+$", "", var.name)``. The greedy ``\\d+`` made a
name with a long run of digits not at the end (e.g. ``x000...0z``) re-scan the
run from every position, which is quadratic in the run length -- so a single
untrusted feature structure renamed (or unified, which renames by default) could
pin a CPU core. A ``(?<!\\d)`` lookbehind now lets the run match only at its
start, making the substitution linear while leaving the result unchanged.
The "must stay linear" test runs in a spawned process with a hard timeout, and
the worker reports via its exit code (no queue/thread, so it is robust on
free-threaded builds), so a regression to the quadratic pattern cannot hang the
suite.
"""
import multiprocessing
import os
from nltk.featstruct import FeatStruct, _rename_variable
from nltk.sem.logic import Variable
def test_rename_variable_strips_trailing_digits():
# The trailing-number stripping (and fresh-suffix) behaviour is unchanged.
assert _rename_variable(Variable("x12"), set()) == Variable("x2")
assert _rename_variable(Variable("foo3"), set()) == Variable("foo2")
assert _rename_variable(Variable("x"), set()) == Variable("x2")
# A digit run that is not at the end is not stripped (ends in 'z').
assert _rename_variable(Variable("x000z"), set()) == Variable("x000z2")
# Only the trailing run is stripped.
assert _rename_variable(Variable("x1y23"), set()) == Variable("x1y2")
# Fresh suffix avoids clashes with used_vars.
assert _rename_variable(Variable("x"), {Variable("x2")}) == Variable("x3")
def test_rename_and_unify_preserved():
assert str(FeatStruct("[a=?x, b=?y]").rename_variables()) == str(
FeatStruct("[a=?x2, b=?y2]")
)
unified = FeatStruct("[a=?p]").unify(FeatStruct("[b=?q]"))
assert unified is not None
assert unified["a"] == Variable("?p")
assert unified["b"] == Variable("?q")
_TIMEOUT = 20
# A variable name with a long run of digits that is not at the end. The pre-fix
# greedy ``\d+`` made renaming this quadratic (~tens of seconds at this size);
# the fix makes it linear (~milliseconds). It is CPU-only (a ~0.2 MB string), so
# there is no OOM risk.
_EVIL = "[a=?x" + "0" * 200_000 + "z]"
def _rename_worker():
# Renaming this (valid) feature structure must actually complete quickly.
# Exit non-zero on any failure so the parent assertion fails -- otherwise a
# swallowed exception (e.g. parsing breaking before the rename runs) would
# let the test pass without exercising the vulnerable rename path.
try:
FeatStruct(_EVIL).rename_variables()
os._exit(0)
except BaseException:
os._exit(3)
def test_long_digit_run_renames_in_linear_time():
"""A long digit-run variable name must rename in linear time (not ReDoS)."""
ctx = multiprocessing.get_context("spawn")
proc = ctx.Process(target=_rename_worker)
proc.start()
proc.join(_TIMEOUT)
if proc.is_alive():
proc.terminate()
proc.join()
raise AssertionError(
"feature-structure renaming did not finish in time -> quadratic ReDoS"
)
assert proc.exitcode == 0, f"worker failed (exit code {proc.exitcode})"