Skip to content

fix(template): only auto-call callables that take zero arguments - #56

Merged
solisoft merged 2 commits into
mainfrom
fix/template-auto-call-arity
Aug 26, 2026
Merged

fix(template): only auto-call callables that take zero arguments#56
solisoft merged 2 commits into
mainfrom
fix/template-auto-call-arity

Conversation

@solisoft

Copy link
Copy Markdown
Owner

The nightly template_parse_render fuzz target aborted on <%= patch%>:

panicked at src/interpreter/builtins/request_helpers.rs:73:
index out of bounds: the len is 0 but the index is 0

Omitting parentheses is documented as a zero-argument form (<%= now.to_iso %>), but the renderer auto-called every callable it evaluated, passing an empty argument list. So naming a helper that needs arguments ran its body with nothing in it. The request helpers are registered variadic — arity: None means "the runtime does not check the count", not "takes no arguments" — so patch read args[0] on an empty slice. A panic aborts without unwinding, out of reach of the per-request catch_unwind.

Two fixes, at the choke point and at the destination:

  • auto_call_if_callable now invokes only callables that genuinely accept zero arguments, using the same predicate as a bare name in code (can_auto_invoke_with_no_args, which try_auto_invoke also calls now so the two surfaces cannot drift). A callable that needs arguments renders as its value instead of being called. This closes the whole class from the template surface, not just patch.
  • The request helpers bounds-check their arguments (arg_string), so calling one directly with too few raises "… is missing a required argument" rather than panicking.

The minimized reproducer is a tracked fuzz seed. Regression tests cover both directions: a helper needing arguments does not panic, and a genuine zero-arg method is still called without parentheses.

Note: ~760 other builtins still index args[N] unchecked, reachable by calling one with too few arguments from Soli code. Not touched here.

Olivier Bonnaure and others added 2 commits August 26, 2026 08:48
The nightly `template_parse_render` fuzz target aborted on `<%= patch%>`:

    panicked at src/interpreter/builtins/request_helpers.rs:73:
    index out of bounds: the len is 0 but the index is 0

Omitting parentheses is documented as a zero-argument form (`<%= now.to_iso
%>`), but the renderer auto-called *every* callable it evaluated, passing an
empty argument list. So naming a helper that needs arguments ran its body with
nothing in it. The request helpers are registered variadic — `arity: None`
means "the runtime does not check the count", not "takes no arguments" — so
`patch` read `args[0]` on an empty slice. A panic aborts without unwinding, out
of reach of the per-request `catch_unwind`.

Two fixes, at the choke point and at the destination:

- `auto_call_if_callable` now invokes only callables that genuinely accept zero
  arguments, using the same predicate as a bare name in code
  (`can_auto_invoke_with_no_args`, which `try_auto_invoke` also calls now so
  the two surfaces cannot drift). A callable that needs arguments renders as
  its value instead of being called. This closes the whole class from the
  template surface, not just `patch`.
- The request helpers bounds-check their arguments (`arg_string`), so calling
  one directly with too few raises "… is missing a required argument" rather
  than panicking.

The minimized reproducer is a tracked fuzz seed. Regression tests cover both
directions: a helper needing arguments does not panic, and a genuine zero-arg
method is still called without parentheses.

Note: ~760 other builtins still index `args[N]` unchecked, reachable by calling
one with too few arguments from Soli code. Not touched here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t 241

Two things that made the fuzz job red for reasons unrelated to the code under
test.

LeakSanitizer. cargo-fuzz builds with AddressSanitizer, which enables LSan too,
and nothing set ASAN_OPTIONS. These targets exist to catch panics, stack
overflows and UB in the surfaces that take untrusted input — not to audit
allocation lifetimes. The interpreter is `Rc<RefCell<…>>`-based, so a closure
capturing its defining environment forms a reference cycle that LSan correctly
calls an unreachable "direct leak"; it is bounded and harmless in a process
that exits. On PR #55 that failed the job *after* a completely clean fuzz —
`Done 72817 runs in 21 second(s)`, then 446 bytes in 36 allocations — so every
PR touching a fuzzed path inherited a red X that had nothing to do with its
diff. Leak detection is now off for the fuzz step.

The backtick guard in `template_parse_render` predates this and was partly
justified by the same LSan behaviour; its other reason — rendering a backtick
tag runs `sh -c <fuzzer bytes>` on the host — stands on its own, so the guard
stays and the comment now says why.

Unwrap ratchet. The regression tests added in the previous commit pushed
`src/template` from 241 to 244 `.unwrap()`/`.expect()` calls and failed
`scripts/lint_unwraps.sh`. The ratchet is meant to move in one direction, so
the tests were rewritten to not need them (`let Ok(…) else`, and comparing
against `Ok("ADA".to_string())`) rather than raising the baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@solisoft
solisoft merged commit fe4b305 into main Aug 26, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant