memory: deep-promote values escaping arena scopes; gate JIT off inside arena windows (#873) - #884
Merged
Merged
Conversation
…e arena windows (#873) Closes #873. A list escaping an arena_mark…arena_reset scope became a dangling reference into memory the next arena_mark handed back out — silent wrong values, type confusion, and (append into a heap list) a free(): invalid pointer abort, all reachable from pure EigenScript and invisible to ASan. promote_if_arena's container arm was a comment. - promote_if_arena (eigenscript.c): recursive VAL_LIST deep-promote — fresh heap list, arena children promoted recursively (acyclic at promotion time: building a cycle requires mutating through a binding, and binding stores promote), heap children shared. Lists are the only arena-capable container (dict/fn/buffer/text-builder constructors are heap-only). - Store seams promoted: list_append (arena item into heap list — the abort repro), OP_INDEX_SET interpreter case + jit_helper_index_set (general arm promote; num fast paths heap-force into heap targets under an open window), OP_SET_LOCAL (module slots are immortal), iterator-state make_num, set_at 1D/2D, list_insert_at, copy_into. Arg-pack wrappers switched to make_list_heap (bound as param slots). - JIT gated off while an arena window is open: both fresh-entry sites, the OSR trigger, jit_helper_call's nested-thunk gate, plus a deep bail (return 2) when a builtin opens a window mid-thunk — emitted stores don't arena-promote, so arena scopes run interpreted. - Escaping a stored value is now documented safe behavior (README): the arena reclaims only unstored intermediates. - tests/test_arena_escape.eigs (18 checks, suite section after Arena Ownership): both issue repros, nested lists, every store seam, a stomp loop overwriting the reclaimed region, and an OSR-threshold hot variant. Green under release, ASan, poison, JIT on and off. Validated: release suite 3783/3783; ASan detect_leaks=1 3781/3781 with leak tally 0; poison (0xAA + MALLOC_PERTURB_=170) arena tests clean both JIT modes; jit-smoke green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical arena-allocation escape bug (#873) in the EigenScript runtime by ensuring values that are stored out of an arena_mark…arena_reset window are safely copied to the heap, eliminating dangling references that previously caused silent wrong values, type confusion, and allocator aborts.
Changes:
- Implement deep promotion for arena-backed lists in
promote_if_arena, and promote arena values when storing into heap-owned containers (append/indexed store/local/env/builtin seams). - Gate JIT execution/OSR entry while an arena window is open, including a “deep bail” when a builtin opens an arena window mid-thunk.
- Add a dedicated regression test suite covering the escape scenarios and wire it into the test runner; document the now-guaranteed semantics.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_arena_escape.eigs | New regression suite for #873 covering escape/store seams and OSR/JIT-threshold behavior. |
| tests/run_all_tests.sh | Adds the new arena escape containment suite to the main test runner. |
| src/vm.c | Promotes/heap-forces arena values at additional VM/JIT store seams and gates JIT/OSR while an arena window is open. |
| src/eigenscript.h | Exposes make_num_permanent for heap-only numeric allocation used by store/promotion paths. |
| src/eigenscript.c | Deep-promotes arena lists in promote_if_arena and promotes arena items appended into heap lists. |
| src/builtins.c | Promotes arena values written into heap containers in copy_into, set_at, and list_insert_at. |
| README.md | Documents the new guarantee: stored escaping values are safe via heap promotion. |
| CHANGELOG.md | Records the fix and its behavioral guarantee, plus the new regression coverage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #873.
promote_if_arenacopied only numbers and strings to the heap on store; its container arm was a comment ("Callers should avoid storing arena-allocated complex types"). A list escaping the scope became a dangling reference — reproduced all three severities from pure EigenScript: silent wrong values (the issue's documented-loop repro read iteration 39's data), type confusion (the tag overwritten underneath a binding), and afree(): invalid pointerabort (arena item appended into a heap list, then decref'd through the stale pointer). ASan sees none of it — the arena hands the same region back out.Design
Deep-promote on store. A loud error would break the documented idiom (
tmp is [...]inside the scope is a store); read-side generation checks tax every access. Deep promotion preserves exactly what num/str promotion already promises: unstored intermediates stay arena-cheap and die at reset; anything stored survives by copy. Lists are the only arena-capable container (make_dict/make_fn/buffers/text builders are heap-only constructors), and arena lists are acyclic at promotion time (building a cycle requires mutating through a binding, and binding stores promote), so the recursion terminates.Every store seam promotes — named binding,
OP_SET_LOCAL(module slots are immortal), dict fields (already promoted once lists promote),list_appendinto a heap list,OP_INDEX_SET(interpreter case and the JIT helper, including the num fast paths, which now heap-force into heap targets while a window is open), iterator-statemake_num,set_at1D/2D,list_insert_at,copy_into. Arg-pack wrappers switch tomake_list_heap(they're bound as param slots — heap avoids both the dangle and the new deep-promote cost).The JIT is gated off while an arena window is open — both fresh-entry sites, the OSR trigger,
jit_helper_call's nested-thunk gate (all the same pattern as the existing MT/task-scheduler gates), plus a deep bail when a builtin opens a window mid-thunk. Emitted stores don't arena-promote; arena scopes are explicit scratch windows and run interpreted.Proof
tests/test_arena_escape.eigs(18 checks, wired after the Arena Ownership section): both issue repros, nested lists, every seam above — each under a stomp loop that overwrites the reclaimed region so a dangling reference reads wrong, not lucky — plus a 12,000-iteration variant proving correctness across OSR thresholds. All 18 were red before the fix (the append seam aborted the process).Validation
make asan+ASAN_OPTIONS=detect_leaks=1: 3781/3781, leak tally 0 — the deep-promote's refcount flows are clean.0xAA+MALLOC_PERTURB_=170): arena tests clean, JIT on and off.make jit-smokegreen; README documents the now-guaranteed escape semantics.Closes #873.
🤖 Generated with Claude Code