`5667063` moved test_output.py's ReDoS bounds off the wall clock, because a loaded machine steals wall seconds without adding any cycles and two rows failed at 2.24s / 3.66s against a 2.0s bound while census had the CPU. The remaining ten bounds in five other files still ran on `time.monotonic()` and carried the same defect. They now share ONE clock. The clock is IMPORTED, not copied: `tests/redos_clock.py`. Five private copies would leave four of them unpinned -- the instrument test (test_the_redos_clock_ignores_time_this_process_did_not_spend) can only pin the implementation it calls, and the suite already holds that rule for the code it measures. Every ported row was verified the only way a time bound can be: the vulnerable form patched back in, red demanded, `git checkout --` after. Measured against the 2.0s bound (3.0s for the url arm): active_content long-attr `{0,63}` -> `*` RED neutralize long-attr same patch RED output gate long-attr same patch 12.41s okf link graph `[^\]\[]` -> `[^\]]` 6.91s sanitize comment str.find -> `<!--.*?-->` 17.56s lexicon md-link-anchor-text 319.14s lexicon md-link-anchor-url 8.55s lexicon md-link-ref-comment 37.82s Two rows did not go red, for two different reasons. test_sanitize.py::test_legitimate_comment_heavy_document is the legitimate SIDE of a separation, not a second pin on the defect: closed comments never withhold the required literal, so the lazy form runs it in 0.016s. Recorded in place. test_lexicon.py::test_redos_pathological_subagent_input_returns_fast is DEAD -- the same zero-signal shape the `<a ` carrier had, found by the same method. The seed form is `(?:.*?\s+)?` (llm-security 7.8.0, injection-patterns.mjs:84) and this repo has never carried it: the bounded `{0,12}?` port is in the pattern table's first commit. Patched in by hand at the row's own size: shipped 0.135s vs seed 0.113s, separation 1.2x. Not the keyword gate either -- a variant that reaches the inner branch stays linear over four doublings (exponent ~1.0), because the nesting is one lazy run inside an OPTIONAL group, never a repeated one. Left standing with the measurement written into it; picking a new carrier is an operator call, like the wall-clock row above it. The dead sibling row named in STATE is fixed: test_active_content.py's long-attribute row swaps carrier `<a ` -> `<script `, for the reason `5667063` established on its composed-gate twin -- 0.7.0's own no-URL narrowing put `<a>` in `_URL_AFFORDANCE_TAGS`, so the tag returns inert BEFORE its body reaches the arm the row guards. Re-measured here, not inherited: `<a ` 0.041s and NO findings against the vulnerable form; `<script ` 19.349s against 0.052s shipped, 373x apart. `test_pathological_input_returns_within_a_bound` deliberately keeps its wall clock (operator decision): it claims to catch a hang, and only a wall clock catches one. 792 tests, 129/129, 6/6.
35 lines
1.7 KiB
Python
35 lines
1.7 KiB
Python
"""The one clock every ReDoS bound in this suite is measured against.
|
|
|
|
Process CPU time, not wall clock: a ReDoS blowup is spent cycles, and a loaded
|
|
machine steals wall clock without adding any. In 0.7.0 these bounds ran on
|
|
``time.monotonic()`` and two of them failed at 2.24s / 3.66s against a 2.0s
|
|
bound while two census processes had the CPU; the same rows passed 3/3 on an
|
|
idle machine. The scans had not slowed down — they were descheduled.
|
|
|
|
This lives in its own module, imported by all six test files, rather than being
|
|
copied into each. The suite already holds that rule for the code it measures
|
|
("never re-implement a predicate you measure — import it"), and it binds harder
|
|
here: ``test_output.py::test_the_redos_clock_ignores_time_this_process_did_not_spend``
|
|
pins ONE implementation. Five copies would leave four of them unpinned and free
|
|
to drift back to a wall clock without a single test going red.
|
|
|
|
What this clock gives up: a scan that BLOCKS forever burns no CPU, so it would
|
|
hang the suite instead of failing it. Acceptable for every caller here — these
|
|
scanners are pure regex over an in-memory string, with no I/O and no locks, so
|
|
the only way they can be slow is by spending cycles. It is also why
|
|
``test_output.py::test_pathological_input_returns_within_a_bound`` deliberately
|
|
keeps a wall clock: that row claims to catch "a hang or a blowup", and only a
|
|
wall clock catches the first.
|
|
"""
|
|
import time
|
|
|
|
|
|
def scan_seconds(scanner, payload) -> float:
|
|
"""CPU seconds ``scanner(payload)`` cost.
|
|
|
|
Pinned by ``test_the_redos_clock_ignores_time_this_process_did_not_spend``
|
|
in ``test_output.py``, which carries the measurements behind the choice.
|
|
"""
|
|
start = time.process_time()
|
|
scanner(payload)
|
|
return time.process_time() - start
|