docs(redos): the sweep now measures on the clock its own numbers justify
docs/redos-sweep.py timed on time.monotonic() while every ReDoS bound in the suite had moved to process CPU time, so the 1.5 ms sensitivity floor and the "~23 s at the cap" figure in docs/LIMITATIONS.md were produced by a different instrument than the bounds they support. The script imports scan_seconds now. Re-derived on that instrument, the floor came back UNCHANGED at 1.5 ms. Twelve full runs of all 2585 arms: median ratio 1.95-2.03 in every size bucket above 50 us, but two-point excursions past the 2.6 threshold survive at every magnitude (p99 2.9-3.3 even above 1 ms). Flagged arms per run by floor: 6.9 at 0.5 ms, 1.1 at 1.0 ms, 0.33 at 1.5 ms. Four arms flagged across the twelve runs, each in exactly one; six arms that have ever flagged re-measure at exponent 0.97-1.09 over six doublings, at most 1.2 s at the cap. Descheduling was never what made this sweep noisy -- a ratio from two points is. Measured before publishing and it cost a correction: an earlier draft of this change said the sweep "reports 0 candidates". The next run reported 2. Nine of twelve instrumented runs are clean and eight consecutive shipped-script runs under load flagged 0-3, so neither a clean run nor a flagged one is evidence on its own -- the doc says that now. Also corrects the pattern count in the same bullet: 150 -> 152, the script's own printed total. The 0.3.4 CHANGELOG entry keeps its 150 as a snapshot. No exported surface, no detector behaviour, no calibration touched. 802 passed, 129/129 classes, 6/6 gaps, 43 limitations, gitleaks clean.
This commit is contained in:
parent
4472f209a4
commit
74656123f9
3 changed files with 90 additions and 26 deletions
|
|
@ -524,18 +524,22 @@ items; this is the full list, each with the mechanism.
|
|||
missed; on one preset it is held for review instead of halted.
|
||||
|
||||
- **The ReDoS sweep has a measured sensitivity floor, not a clean bill of
|
||||
health.** All 150 compiled patterns across all eleven regex-bearing modules are
|
||||
health.** All 152 compiled patterns across all eleven regex-bearing modules are
|
||||
swept arm by arm — payloads synthesised per run from each pattern's own
|
||||
skeleton, so `[`, `[system]` and `[system](` are each probed separately rather
|
||||
than relying on generic units, and each pattern is timed in the call mode the
|
||||
production code uses (`.sub()`/`.finditer()` visit every start position where
|
||||
`.match()` cannot). Five patterns were quadratic across 0.3.3 and 0.3.4; all
|
||||
are fixed. But the sweep flags on *timing*, and it ignores measurements below a
|
||||
1.5 ms noise floor at N=8000. A quadratic arm sitting just under that floor
|
||||
would still cost **up to ~23 s** at the 1 000 000-char cap. So the claim this
|
||||
sweep supports is "no arm worse than ~23 s at the cap", not "no quadratic arm
|
||||
remains". The method's blind spot is real and has now been demonstrated twice:
|
||||
a generic-payload pass found only one of 0.3.3's two patterns, and 0.3.2's
|
||||
1.5 ms noise floor at N=8000 — process CPU time, re-derived on that instrument
|
||||
(see the clock bullet below) rather than inherited from the wall clock the
|
||||
script used through 1.1.0. A quadratic arm sitting just under that floor would
|
||||
still cost **up to ~23 s** at the 1 000 000-char cap — that figure is arithmetic
|
||||
and not a measurement: a quadratic arm costs the square of the length ratio, and
|
||||
1.5 ms × 125 × 125 is 23.4 s. So the claim this sweep supports is "no arm worse
|
||||
than ~23 s at the cap", not "no quadratic arm remains". The method's blind spot
|
||||
is real and has now been demonstrated twice: a generic-payload pass found only
|
||||
one of 0.3.3's two patterns, and 0.3.2's
|
||||
hand-written rows missed all three of 0.3.4's — including one on `sanitize`,
|
||||
the first thing every ingested document touches. **Two arm shapes the unit-
|
||||
repetition payloads cannot express** are pinned by hand as a result: a tag that
|
||||
|
|
@ -574,8 +578,8 @@ items; this is the full list, each with the mechanism.
|
|||
the suite cannot report its own blind spot, and every bound in it should be read as
|
||||
"verified red under the vulnerable form" only where a comment says it was.
|
||||
|
||||
- **Every ReDoS bound in the suite is measured on process CPU time; the sweep that
|
||||
sets the published sensitivity floor is not.** `tests/redos_clock.py` is the one
|
||||
- **Every ReDoS bound in the suite is measured on process CPU time, and so is the
|
||||
sweep that sets the published sensitivity floor.** `tests/redos_clock.py` is the one
|
||||
clock all six test files import — `time.process_time()` — because a blowup is spent
|
||||
cycles while a loaded machine steals wall clock without adding any. On
|
||||
`time.monotonic()` two 0.7.0 rows failed at **2.24s / 3.66s** against a 2.0s bound
|
||||
|
|
@ -589,12 +593,26 @@ items; this is the full list, each with the mechanism.
|
|||
in-memory string, with no I/O and no locks — the last wall-clock holdout was retired
|
||||
by auditing its path for anything that could block, not by assumption, and a wall
|
||||
clock guarding a mode that cannot occur still charges the false-red premium
|
||||
(measured there at 21.6s against a 10.0s bound, on a scan that spent 7.6s). **The
|
||||
divergence to know about:** `docs/redos-sweep.py` still times on `time.monotonic()`
|
||||
(`redos-sweep.py:139`), so the 1.5 ms floor at N=8000 and the "~23 s at the cap"
|
||||
figure above are wall-clock numbers, produced by a different instrument than the
|
||||
bounds they justify. They have not been re-derived on the CPU clock; treat them as
|
||||
the noisier of the two measurements rather than as directly comparable.
|
||||
(measured there at 21.6s against a 10.0s bound, on a scan that spent 7.6s). **The sweep
|
||||
now runs on the same clock, and closing that divergence bought no sensitivity.**
|
||||
`docs/redos-sweep.py` imports `scan_seconds` instead of timing on
|
||||
`time.monotonic()`, so the 1.5 ms floor at N=8000 and the "~23 s at the cap"
|
||||
figure above are finally in the same currency as the bounds they justify. What
|
||||
the move did *not* do is quiet the sweep, and the floor came back unchanged.
|
||||
Measured over **twelve full runs of all 2585 arms**: the median ratio sits at
|
||||
**1.95–2.03** in every size bucket above 50 µs — the whole surface measures
|
||||
linear — while two-point excursions past the 2.6 flag threshold survive at every
|
||||
magnitude, p99 ratio **2.9–3.3 even above 1 ms**. Flagged arms per run by floor:
|
||||
**6.9 at 0.5 ms, 1.1 at 1.0 ms, 0.33 at 1.5 ms** (0–2 per run), so 1.5 ms is
|
||||
still the knee. Descheduling was never what made this sweep noisy — a ratio
|
||||
computed from two points is. Four distinct arms flagged at the shipped floor
|
||||
across those twelve runs, **each in exactly one of them**, and nine of the twelve
|
||||
runs were clean; eight consecutive runs of the shipped script immediately after a
|
||||
full test run flagged 0–3 arms each, so machine load still moves the count even
|
||||
on a CPU clock. Six flagged arms re-measured over six doublings give exponent
|
||||
**0.97–1.09** and at most 1.2 s at the 1 000 000-char cap. **A single clean run
|
||||
of this sweep is therefore not evidence either** — and neither is a single
|
||||
flagged one.
|
||||
|
||||
- **Every surface now bounds its input, but not all of them the same way.**
|
||||
`sanitize`, `fence` and `neutralize` raise `OversizeInputError` above
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue