Skip to content

Null-fill every unsent parameter at every call entry (heap use-after-free, #1661) - #1664

Merged
InauguralPhysicist merged 3 commits into
mainfrom
fix/1661-underarity-uaf
Oct 7, 2026
Merged

InauguralPhysicist merged 3 commits into
mainfrom
fix/1661-underarity-uaf

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Fixes the heap use-after-free reported in #1661. It is reachable from ordinary code and was also present in v0.44.0.

Root cause

  • In CASE(CALL) and jit_helper_call, a callee with 2+ params called with fewer args bound only min(argc, param_count) of its params. The null fill for the rest ran only if (can_default).
  • With local_count == param_count, the env did not reserve the unsent slots. A recycled freelist env therefore kept a stale pointer in them.
  • The interpreter's GET_LOCAL bounds-checks and returns null, so it never saw the stale value. The JIT's GET_LOCAL reads values[slot] unchecked, so it dereferenced the stale pointer, a value the cycle collector had already freed.
  • With EIGS_JIT_OFF=1 there was no crash, but the bug was still visible: a closure over the unsent param resolved an outer variable of the same name instead of null.

Fix

Every unsent param is null-filled by name, whether or not the callee has defaults. Call-path audit:

entry verdict
CASE(CALL) fresh env broken, fixed
jit_helper_call broken, fixed
CASE(DISPATCH) broken, fixed
dispatch C fallback name binding missing, fixed
task_start name binding missing, fixed
recycled env, loop-env reuse, and the other entries OK; see the commit message

Tests

Suite section [0fd] holds 18 rows: the reproducer, depth 1000 and 3000, the try/catch double-free variant, JIT-to-JIT, dispatch and task_spawn, each across the default, interpreter and forced-JIT tiers.

  • Unfixed: 4/18 pass on both the release and ASan builds. The remaining 14 fail with rc=134 aborts, failed asserts, or ASan use-after-free reports.
  • Fixed: 18/18 on both builds.
  • make test-changed: 1959/1959. make precheck: 18 passed, 1 skipped.
  • Full suite: 4308/4310. The 2 failures are section [99s], because the builder's container has no libSDL2_mixer; they fail identically on unfixed main.

This goes out as patch release v0.45.1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KC99CmwatKssQYggkBCgwF

claude added 3 commits October 7, 2026 11:33
Root cause. On the fresh-env call path (CASE(CALL), jit_helper_call),
a 2+-param callee called under-arity bound only min(argc, param_count)
params; the null fill of [argc, param_count) ran only `if (can_default)`.
For a defaults-free callee the env kept count < param_count, and when
local_count == param_count no env_reserve_slots ran either. A printf at
the bind site on the issue's reproducer showed, on every call:

    call bind: argc=1 param_count=2 local_count=2 count=1 cap=16 v1=0
    call bind: ... count=1 cap=16 v1=fffb55ac1ff6ea80   (stale heap ptr)

The interpreter's GET_LOCAL bounds-checks and reads null, but the JIT's
GET_LOCAL reads values[slot] unchecked, trusting count >= local_count.
env_new recycles freelist envs without clearing values[] past count, so
the JIT read a previous occupant's slot: under ASan a heap-use-after-free
(read in slot_as_double from OP_EQ; the list freed by env_decref in
CASE(RETURN) -> slot_decref -> cycle collector). EIGS_JIT_OFF=1 is clean.
Without the JIT the unbound name still leaked: a closure over an unsent
param resolved it in an outer scope instead of reading null.

Fix: the fill now runs for every unsent param, defaults or not, at
CASE(CALL), jit_helper_call, CASE(DISPATCH), the C fallback of the
`dispatch` builtin, and task_start. spawn (thread entry) and
call_eigs_fn (sort_by and the other callbacks) already null-filled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six programs, each at the default, interpreter and forced-JIT tiers:
the issue's reproducer, depth 1000 and 3000 with and without defaults,
the try/catch variant, a closure-over-unsent-param check across direct
call / spawn / task_spawn / dispatch / sort_by plus a JIT stale-slot
witness, a JIT-to-JIT (jit_helper_call) witness, and the eval-forced
`dispatch` C fallback. Unfixed: 4/18 pass under release and ASan (only
the interpreter rows of the JIT-only cases); fixed: 18/18 under both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T12:28:34.185450Z f4e655f PR opened
🔒 Security Review ✅ Completed 2026-10-07T12:37:18.902917Z f4e655f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codspeed

codspeed Bot commented Oct 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 13 untouched benchmarks


Comparing fix/1661-underarity-uaf (f4e655f) with main (15c364a)

Open in CodSpeed

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator Author

The Opus 4.8 blind critic PASSED this PR. It built fixed and base binaries itself and checked every entry across the default, interpreter and forced-JIT tiers:

  • the fixed build is clean on all of them;
  • the probes are real witnesses (ASan reports a heap-use-after-free on base);
  • base scores 4/18 on [0fd], fixed scores 18/18;
  • the semantics match SPEC.md:918.

The residual hazard class is filed as #1666: a mechanical guard, not a JIT runtime check, which it measured as too costly on the hottest op. CI: 26 green, 6 skipped.

@InauguralPhysicist
InauguralPhysicist added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit c17dff1 Oct 7, 2026
32 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the fix/1661-underarity-uaf branch October 7, 2026 13:11
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.

2 participants