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

97 lines
3.2 KiB
Python

"""Regression tests for the combinatorial-blowup DoS in nltk.sem model
evaluation (CWE-770; CVE-2026-12840).
A formula with k nested quantifiers/lambdas makes ``Model.satisfy`` do
O(|domain| ** k) work, and k comes entirely from the (untrusted) formula. The
cost is now bounded by ``Model.MAX_SATISFY_OPERATIONS`` before the recursion can
blow up. These tests confirm the bound refuses deeply-nested formulas while
legitimate shallow ones still evaluate.
The "must not hang" test runs in a spawned process with a hard timeout so a
regression (running the unbounded O(|domain| ** k) loop) cannot hang the suite.
"""
import multiprocessing
import queue
from nltk.sem import Assignment, Model, Valuation
from nltk.sem.evaluate import Error, _max_binder_depth
from nltk.sem.logic import Expression
def _model():
dom = {"a", "b"}
val = Valuation([("P", {("a",)}), ("R", {("a", "b")})])
return Model(dom, val), Assignment(dom)
def test_max_binder_depth():
p = Expression.fromstring
assert _max_binder_depth(p("P(x)")) == 0
assert _max_binder_depth(p("all x.P(x)")) == 1
assert _max_binder_depth(p("all x.all y.R(x,y)")) == 2
assert _max_binder_depth(p("all x.(P(x) & exists y.R(x,y))")) == 2
assert _max_binder_depth(p("exists x.-all y.R(x,y)")) == 2
def test_legitimate_formulas_still_evaluate():
m, g = _model()
assert m.evaluate("all x.(x = x)", g) is True
assert m.evaluate("exists x.P(x)", g) is True
assert m.evaluate("all x.exists y.R(x,y)", g) is False
def test_shallow_formula_within_bound_allowed():
# depth 5 over a domain of 10 -> 1e5 < MAX_SATISFY_OPERATIONS (1e6): allowed.
dom = set("abcdefghij")
m = Model(dom, Valuation([("P", {("a",)})]))
g = Assignment(dom)
f = "all x0.all x1.all x2.all x3.all x4.(x0 = x0)"
assert m.evaluate(f, g) is True
_TIMEOUT = 15
def _eval_worker(result_q):
try:
dom = set("abcdefghij") # |domain| = 10
m = Model(dom, Valuation([("P", {("a",)})]))
g = Assignment(dom)
# 9 nested quantifiers -> 10**9 satisfaction checks if unbounded.
f = "".join("all x%d." % i for i in range(9)) + "(x0 = x0)"
try:
m.evaluate(f, g)
result_q.put(("ok", "evaluated"))
except Error:
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_deeply_nested_formula_is_refused_not_evaluated():
"""A deeply-nested formula must be refused, not run as O(|domain| ** k)."""
finished, status, value = _run_in_process(_eval_worker)
assert (
finished
), "Model.evaluate ran an unbounded O(|domain| ** k) computation (DoS)"
assert status == "ok", f"worker raised: {value}"
assert value == "refused"