fix: harden against confirmed review findings - #2
Conversation
Three verified findings from an automated multi-agent review, plus a
bundled build-prerequisite pin fix.
1. Parse-time recursion segfault (security/crash). The 256 depth cap
existed only in the renderer; the recursive-descent parser had no
bound, so a ~5000-deep nested {% if %} or a ~50k-long `not` chain
overflowed the native stack and crashed the process (SIGSEGV). Thread
a depth counter through both the block parser (parse_body / _parse_if
/ _parse_for) and the expression parser (_p_or..._p_primary), raising
cleanly at 256 (matching the renderer's _MAX_DEPTH). Recursive _eval
is transitively protected because the parser now caps expression
nesting.
2. safe/escape divergence from Jinja (parity). Safety was a transient
flag on the evaluation result, so `x|escape|escape` double-escaped and
`x|safe` was lost through `{% set %}` and subsequent filters. Carry a
`safe` flag on TemplateValue itself: `safe` marks the value, `escape`
is idempotent on already-safe values, string-transform filters
(upper/lower/title/trim/replace/truncate) preserve it, and `{% set %}`
persists it. Now matches Jinja's Markup semantics.
3. {% set %} inside {% for %} leaked across iterations (parity). Loop
bodies mutated the enclosing scope, so a counter rendered 3 where Jinja
renders 0. Snapshot the scope at loop entry, reset to it before every
iteration (no cross-iteration accumulation) and restore after the loop
(no leak out), matching Jinja's loop scoping.
Build prerequisite: pixi.toml pinned `mojo>=1.0.0b3`, which excludes all
`1.0.0b3.devN` nightlies under PEP 440 ordering, so `pixi install` failed
to solve. Bumped to `>=1.0.0b3.dev0,<2` (same fix as mojo-redis).
Adds 10 regression tests (deep-nesting guards, Markup semantics, loop
scoping). Full suite: 69 passed, 0 failed.
Co-Authored-By: Claude <noreply@anthropic.com>
|
🤖 Independent Claude review: Verdict: Needs work — one blocking robustness gap in fix #1. BLOCKING — the parse-depth cap does not close the stack-overflow class it claims to. The PR body states "Recursive
These are the same crash class as the fixed Verified clean:
Note: mojo toolchain isn't installed in my environment, so the overflow is established by static call-path analysis, not a runtime crash repro. |
Follow-up to the parse-depth hardening. That fix capped parser recursion
and claimed `_eval` was "transitively protected because the parser now
caps expression nesting" — but that only holds for parser-recursive
constructs. Left-associative operator chains (`_p_add`, `_p_and`, `_p_or`)
and postfix chains (`_p_postfix`: `.attr`, `[idx]`, `| filter`) are parsed
iteratively in while-loops, so they parse at constant depth no matter how
long they are. The parser guard never trips, but `_eval`/`_apply_filter`
recurse down `e.a` once per link at render time with no depth bound, so a
template like `{{ 1 + 1 + ...(50k) }}` or `{{ x | upper | ...(50k) }}`
parses fine and then overflows the native stack and SIGSEGVs the process
(a reachable render-time DoS).
Thread a depth counter through `_eval` and `_apply_filter`, raising
cleanly once it exceeds `_MAX_DEPTH` (256, matching the parser and the
sibling libraries) instead of overflowing. Statement-level calls in
`_render_body` seed depth at 0; every recursive expression call increments.
Adds 8 regression tests covering the render-time vectors the previous 10
tests missed: deep `+`, `and`, and `or` operator chains, and deep `.attr`,
`[idx]`, and `| filter` postfix chains all raise instead of crashing, plus
two legal-depth chains (200 links) that must still render. Verified the new
tests crash the runner on pre-fix source and pass after. Full suite: 77
passed, 0 failed.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Code review (opus, static): CHANGES-NEEDED The safe-flag autoescape fix (#2) and loop-scoping fix are correct and well-tested. But the headline recursion-segfault fix is incomplete — the "transitively protected" claim is false:
Fix: cap the postfix spine in |
…ime) The render-time `_eval` guard already stops a deep `.attr` / `[idx]` / `| filter` chain from overflowing the native stack, but `_p_postfix` accumulated the spine with no cap, so a pathological chain still built the full expression arena in memory before `_eval` could raise. Count the postfix spine and reject it past `_MAX_PARSE_DEPTH` (the same cap the prefix/structural guards and the renderer use), so a deep chain raises up front rather than materializing a giant arena. Add a test that a `.attr` spine just over the cap raises at parse time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From an automated multi-agent review (personal-context#62); implemented + verified by Claude Code. (PR auto-recovered — the agent completed and pushed this branch but a transient API error interrupted it before it could open the PR.)
fix: harden against confirmed review findings
Three verified findings from an automated multi-agent review, plus a
bundled build-prerequisite pin fix.
Parse-time recursion segfault (security/crash). The 256 depth cap
existed only in the renderer; the recursive-descent parser had no
bound, so a ~5000-deep nested {% if %} or a ~50k-long
notchainoverflowed the native stack and crashed the process (SIGSEGV). Thread
a depth counter through both the block parser (parse_body / _parse_if
/ _parse_for) and the expression parser (_p_or..._p_primary), raising
cleanly at 256 (matching the renderer's _MAX_DEPTH). Recursive _eval
is transitively protected because the parser now caps expression
nesting.
safe/escape divergence from Jinja (parity). Safety was a transient
flag on the evaluation result, so
x|escape|escapedouble-escaped andx|safewas lost through{% set %}and subsequent filters. Carry asafeflag on TemplateValue itself:safemarks the value,escapeis idempotent on already-safe values, string-transform filters
(upper/lower/title/trim/replace/truncate) preserve it, and
{% set %}persists it. Now matches Jinja's Markup semantics.
{% set %} inside {% for %} leaked across iterations (parity). Loop
bodies mutated the enclosing scope, so a counter rendered 3 where Jinja
renders 0. Snapshot the scope at loop entry, reset to it before every
iteration (no cross-iteration accumulation) and restore after the loop
(no leak out), matching Jinja's loop scoping.
Build prerequisite: pixi.toml pinned
mojo>=1.0.0b3, which excludes all1.0.0b3.devNnightlies under PEP 440 ordering, sopixi installfailedto solve. Bumped to
>=1.0.0b3.dev0,<2(same fix as mojo-redis).Adds 10 regression tests (deep-nesting guards, Markup semantics, loop
scoping). Full suite: 69 passed, 0 failed.
Co-Authored-By: Claude noreply@anthropic.com