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

114 lines
4.2 KiB
Python

"""Regression tests for the unbounded-enumeration DoS in
``nltk.parse.generate.generate`` (CWE-400).
A recursive grammar can derive an exponential (doubly-exponential for a
self-embedding rule) number of sentences. With the default ``depth`` and no
``n`` limit, ``generate`` over a 15-byte grammar such as ``S -> S S | 'a'``
never terminates (it hangs producing even the first sentence) or exhausts
memory. The number of derivation-expansion steps is now bounded by
``MAX_GENERATE_OPERATIONS``; once exceeded, generation raises ``ValueError``.
The "must not hang" test runs in a spawned process with a hard timeout so a
regression (running the unbounded enumeration) cannot hang/OOM the suite.
"""
import multiprocessing
import queue
import pytest
from nltk import CFG
from nltk.parse import generate as generate_mod
from nltk.parse.generate import MAX_GENERATE_OPERATIONS, demo_grammar, generate
def test_max_generate_operations_is_a_finite_positive_int():
assert isinstance(MAX_GENERATE_OPERATIONS, int)
assert MAX_GENERATE_OPERATIONS > 0
def test_behaviour_preserved_for_finite_grammars():
grammar = CFG.fromstring(demo_grammar)
# The values asserted in generate.doctest must be unchanged.
assert len(list(generate(grammar, n=10))) == 10
assert len(list(generate(grammar, depth=3))) == 0
assert len(list(generate(grammar, depth=4))) == 6
assert len(list(generate(grammar, depth=5))) == 42
assert len(list(generate(grammar, depth=6))) == 114
# Default depth on a (non-recursive) grammar still enumerates everything.
assert len(list(generate(grammar))) == 114
# Empty strings / empty productions (grammar.doctest) still work.
g2 = CFG.fromstring("S -> A B\nA -> 'a'\nB -> 'b' | ''")
assert list(generate(g2)) == [["a", "b"], ["a", ""]]
def test_n_zero_returns_no_sentences():
# n is the maximum number of sentences; n=0 must return none, not be
# treated as "no limit". Holds even for a recursive grammar (nothing is
# enumerated, so no budget error).
grammar = CFG.fromstring(demo_grammar)
assert list(generate(grammar, n=0)) == []
recursive = CFG.fromstring("S -> S S | 'a'")
assert list(generate(recursive, n=0)) == []
def test_bounded_recursive_depth_still_terminates():
# An explicit, small depth on a recursive grammar terminates as before.
g = CFG.fromstring("S -> S S | 'a'")
assert len(list(generate(g, depth=5))) == 26
assert len(list(generate(g, depth=6))) == 677
def test_generation_refused_when_budget_exceeded():
# With a tiny budget the explosive grammar is refused quickly and safely
# (in-process: the low limit trips long before memory is a concern).
g = CFG.fromstring("S -> S S | 'a'")
original = generate_mod.MAX_GENERATE_OPERATIONS
generate_mod.MAX_GENERATE_OPERATIONS = 10_000
try:
with pytest.raises(ValueError):
list(generate(g, depth=8))
finally:
generate_mod.MAX_GENERATE_OPERATIONS = original
_TIMEOUT = 30
def _generate_worker(result_q):
try:
# The exact reported DoS: default depth, no n, 15-byte recursive grammar.
g = CFG.fromstring("S -> S S | 'a'")
try:
list(generate(g))
result_q.put(("ok", "enumerated"))
except ValueError:
result_q.put(("ok", "refused"))
except BaseException as exc: # surface to the parent process
result_q.put(("error", repr(exc)))
def _run_in_process(target):
ctx = multiprocessing.get_context("spawn")
result_q = ctx.Queue()
proc = ctx.Process(target=target, args=(result_q,))
proc.start()
proc.join(_TIMEOUT)
if proc.is_alive():
proc.terminate()
proc.join()
return False, None, None
try:
status, payload = result_q.get_nowait()
except queue.Empty:
return True, "error", "worker produced no result"
return True, status, payload
def test_default_depth_recursive_grammar_is_refused_not_unbounded():
"""generate(recursive_grammar) must be refused, not run unbounded."""
finished, status, value = _run_in_process(_generate_worker)
assert finished, "generate() enumerated an unbounded recursive grammar (DoS)"
assert status == "ok", f"worker raised: {value}"
assert value == "refused"