Skip to content

memory: deep-promote values escaping arena scopes; gate JIT off inside arena windows (#873) - #884

Merged
InauguralPhysicist merged 1 commit into
mainfrom
arena-escape-873
Aug 5, 2026
Merged

memory: deep-promote values escaping arena scopes; gate JIT off inside arena windows (#873)#884
InauguralPhysicist merged 1 commit into
mainfrom
arena-escape-873

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #873.

promote_if_arena copied 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 a free(): invalid pointer abort (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_append into 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-state make_num, set_at 1D/2D, list_insert_at, copy_into. Arg-pack wrappers switch to make_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

  • Release suite 3783/3783 (new section included).
  • make asan + ASAN_OPTIONS=detect_leaks=1: 3781/3781, leak tally 0 — the deep-promote's refcount flows are clean.
  • Poison build (0xAA + MALLOC_PERTURB_=170): arena tests clean, JIT on and off.
  • make jit-smoke green; README documents the now-guaranteed escape semantics.

Closes #873.

🤖 Generated with Claude Code

…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>
Copilot AI lite review requested due to automatic review settings August 5, 2026 12:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@InauguralPhysicist
InauguralPhysicist merged commit 5721918 into main Aug 5, 2026
19 checks passed
@InauguralPhysicist
InauguralPhysicist deleted the arena-escape-873 branch August 5, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants