Skip to content

fix(codegen): release the boxed value an associative-array local owns when rebound in a loop - #603

Merged
nahime0 merged 6 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/595-assoc-rebind-release
Jul 25, 2026
Merged

fix(codegen): release the boxed value an associative-array local owns when rebound in a loop#603
nahime0 merged 6 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/595-assoc-rebind-release

Conversation

@mirchaemanuel

Copy link
Copy Markdown
Contributor

Summary

Reassigning an associative-array local to a fresh literal inside a loop leaked the previous hash's boxed value on every iteration. Stdout was correct; the heap grew linearly with the iteration count. The indexed-array sibling was already clean.

$count = 0;
for ($i = 0; $i < 20; $i++) {
    $m = ["k" => $i + 1, "j" => "x"];
    $count = $count + $m["k"];
}
echo $count, "\n"; // 210, but --heap-debug: live_blocks=20

Root cause

The rebind release itself was correct — the previous hash was released and __rt_hash_free_deep decrefs its boxed values. The leak was one level deeper. __rt_hash_set takes ownership of the inserted value (it does not incref), so the hash-value materializer only re-retains values it classifies as borrowed through value_can_own_mixed_box_source. That allow-list of fresh owned boxed-Mixed producers omitted the checked-integer arithmetic ops (ichecked_add/sub/mul) and mixed_numeric_binop. A boxed arithmetic result such as $i + 1 was therefore treated as borrowed and re-incref'd with no matching release — one leaked block per rebind. The indexed sibling stayed clean because array_push retains and the lowering releases the owning temp.

Fix

Recognize those four ops as owned boxed-Mixed producers in value_can_own_mixed_box_source. They always return a freshly allocated, solely-owned boxed Mixed cell (never an operand alias), so a mixed-valued container (hash, indexed array, or object property) steals the sole reference on insert instead of re-retaining it. The box is then released exactly once when the container is torn down. The predicate has always been an over-approximation paired with the source type by its callers (it already includes call/runtime_call/method_call, which can produce non-refcounted results), so the addition follows the existing contract.

Test plan

New heap-debug regression suite tests/codegen/runtime_gc/assoc_rebind_release.rs (12 tests, all assert leak summary: clean + correct stdout): the exact #595 repro; ichecked_mul/sub and mixed_numeric_binop value variants; empty-literal rebind; a dynamic-string value (guards the already-clean string path); a mixed object property (fixed by the same classification); the indexed control (regression guard); ownership guards where the hash escapes into an outer array, is passed to a function, or is returned (double-free guards); and the #528 loop-grown-rebind shape.

Red→green: 8 of the 12 leaked before the fix, all 12 pass after. Focused slices green: runtime_gc arrays::nested_mixed_write hash assoc (585) and objects ownership reassign invoker (315, covering the predicate's other call sites — object properties and invoker arguments). cargo build is warning-free.

Note on #528

#528 (indexed rebind leak after pushing into an outer array) was re-verified on current main and is already heap-clean — fixed structurally by the EIR ownership work that has landed since it was filed. It is a different code path from this fix; this PR adds its shape as a permanent regression guard, and the issue can be closed as already resolved.

Fixes #595

https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L

Reassigning an associative-array local to a fresh literal inside a loop
leaked the previous hash's boxed value on every iteration (issue illegalstudio#595).

A mixed-valued container steals its inserted value's sole reference
(`__rt_hash_set` does not incref), so the hash value materializer only
re-increfs values it classifies as borrowed via
`value_can_own_mixed_box_source`. That allow-list did not include the
checked-integer arithmetic ops (`ichecked_add`/`sub`/`mul`) or
`mixed_numeric_binop`, so a freshly boxed arithmetic result such as
`$i + 1` was treated as borrowed and re-incref'd without a matching
release, leaking one block per rebind. The indexed sibling was clean
because `array_push` retains and the owning temp is released.

Recognizing those ops as owned boxed-Mixed producers makes the container
steal the sole reference, so the box is released exactly once when the
container is torn down. The same classification keeps mixed-valued
indexed arrays and object properties heap-clean, while values that escape
(pushed into an outer array, passed to a function, or returned) stay
balanced.

Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. size:s Small pull request. type:fix Corrects broken or incompatible behavior. labels Jul 23, 2026
@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

While bisecting this I found an adjacent pre-existing refcount corruption, independent of this fix (fails identically with the classification change reverted): #604 — a directly-passed owned boxed-mixed call argument whose return value is also consumed trips heap debug detected bad refcount in a loop. Routing the value through a local first is clean.

@nahime0

nahime0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Maintainer integration update on 650e3855e:

  • Merged current main (0fd01c162) into the existing contributor branch without rewriting its history.
  • Replaced the duplicated non-string codegen producer allow-list with the EIR ownership contract: after final local-slot widening and provisional-release pruning, lowering publishes proven owning temporaries as Ownership::Owned, updating both Value and Instruction metadata; Mixed boxing/container stores now consume that metadata.
  • Kept strings on the dedicated classifier because Owned alone cannot distinguish heap-owned strings from concat scratch storage.
  • Added EIR regression coverage for checked arithmetic and MixedBox ownership, borrowed user-call results, metadata coherence, and the string-scratch boundary.
  • Focused local validation on the published candidate: cargo build; 12 ownership-lowering tests; 231 runtime_gc tests; all 12 Reassigning an associative-array local to a fresh literal in a loop leaks the previous hash every iteration #595 tests with normal optimization and with ELEPHC_IR_OPT=off; assembly-comment and git diff --check hygiene.

The adjacent #604 call-argument corruption remains intentionally separate and is being handled in #618.

CI is now running on the exact published head 650e3855eada3a7e9b3a34d611388b815db3e3a3.

@github-actions github-actions Bot added the area:eir Touches EIR definitions, lowering, validation, or passes. label Jul 24, 2026

nahime0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

CI repair on 06fa48c23:

  • Reproduced the deterministic linux-x86_64 failure in test_fiber_invokable_object_literal_uses_descriptor_receiver locally (both CI retries produced a dangling receiver).
  • Root cause: the new final ownership metadata correctly marks the inline ObjectNew as Ownership::Owned, but descriptor capture treated Owned alone as permission to steal the reference. Lowering also emits an explicit Release for the Fiber constructor argument after the constructor retains/copies it, so the captured receiver was freed and later overwritten by Fiber::start() arguments.
  • Fixed the ownership-transfer boundary without restoring the producer allow-list: retaining consumers may steal only when the value is Owned and EIR has no explicit Release use for that value. Descriptor/closure captures and implicit Mixed/global boxing now share this predicate. An owned value with scheduled cleanup is retained; a genuinely transferred value is still stolen.
  • Updated the changelog wording to document the distinction between ownership and consumability.

Focused validation:

CI should restart on exact head 06fa48c235ca8a62cd527d7aa67330f63fac6ed7.

@nahime0
nahime0 merged commit d0b3c37 into illegalstudio:main Jul 25, 2026
113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. size:s Small pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reassigning an associative-array local to a fresh literal in a loop leaks the previous hash every iteration

2 participants