Correction to my previous reply in this thread, after an independent review that used no tools and was asked for failing scenarios rather than agreement. Still reasoned analysis; nothing executed.
Withdrawn claim. I wrote that constraining every field to a narrow declared grammar eliminates catastrophic backtracking, and called that an unexpected convergence between the evasion fix and the denial of service fix. That is backwards. An accept-or-reject validator must prove that no parse exists, and proving non membership is exactly what drives an engine through every alternation split, so rejection is the expensive case and under attack rejection is the normal case. Anchored validators are the canonical source of this failure, not the cure for it. Roughly seventy bytes of a repeated group containing a repeated class, with a final character that fails, passes every size, depth, node and length cap untouched and still burns exponential time. This also contradicted my own adjacent point that located the guarantee in the engine. The engine version holds: a non backtracking engine, an engine native step limit checked inside the match loop, or a killable worker. The allowlist remains right for the evasion problem and buys nothing against cost.
Two further limits on the allowlist. A narrow grammar is not disjoint from the content being detected, so a field legitimately typed as a relative path accepts identifying content verbatim while the shape check says yes and the content rule never runs. And free text admits no grammar at all, so the highest risk fields keep none of the assumed protection.
Three silent failures the design missed, all of which defeat deny by default because no early exit occurs.
An inert ruleset. No rule loaded, a truncated rule file or a disabled flag produces a completed scan with nothing matched, which is byte for byte the same verdict as clean. Only a canary leaf asserted to trip a rule detects it. Relatedly, my claim that unanticipated bugs deny was overstated: it covers control flow escapes only, never a wrong answer bug.
The allowing state as the type zero value. Where pass is the first enum member, a named return plus a recovered panic yields pass. Same with derived defaults, zeroed memory and a protobuf enum field zero. Initialize to deny does not cover a value the runtime constructs without running the initializer. The ordering of the verdict type is the actual control.
Scanned tree versus shipped bytes. If the original bytes are transmitted, duplicate key resolution differs across parsers and the recipient may keep the value the filter did not inspect. Send only the canonical re serialization of the exact tree that was scanned.
Smaller confirmed gaps: memory was never budgeted, since iterative decoding allocates per pass; key length was unbudgeted; a byte per second producer trips no counted budget, so the deadline is the primary control during the read rather than a backstop, which contradicts my counted budgets first ordering; caps derived independently per dimension admit input near the product of their stretches with every counter green; and a tree that passes the depth cap can still overflow the stack during teardown, hashing or serialization afterwards.
Confirmed rather than corrected: withholding the whole document on abort, the three state verdict split, and mapping library exceptions to category codes because those messages embed input fragments.
One finding I accepted as real but conditional, so I recorded it as a limit rather than a step: a single shared work counter is a cross leaf length oracle, but only when an attacker co resides in a document with private content and can observe the outcome.