fix(codegen): release the boxed value an associative-array local owns when rebound in a loop - #603
Merged
nahime0 merged 6 commits intoJul 25, 2026
Conversation
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
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 |
Member
|
Maintainer integration update on
The adjacent #604 call-argument corruption remains intentionally separate and is being handled in #618. CI is now running on the exact published head |
Member
|
CI repair on
Focused validation:
CI should restart on exact head |
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.
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.
Root cause
The rebind release itself was correct — the previous hash was released and
__rt_hash_free_deepdecrefs its boxed values. The leak was one level deeper.__rt_hash_settakes ownership of the inserted value (it does not incref), so the hash-value materializer only re-retains values it classifies as borrowed throughvalue_can_own_mixed_box_source. That allow-list of fresh owned boxed-Mixed producers omitted the checked-integer arithmetic ops (ichecked_add/sub/mul) andmixed_numeric_binop. A boxed arithmetic result such as$i + 1was therefore treated as borrowed and re-incref'd with no matching release — one leaked block per rebind. The indexed sibling stayed clean becausearray_pushretains 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 amixed-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 includescall/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 assertleak summary: clean+ correct stdout): the exact #595 repro;ichecked_mul/subandmixed_numeric_binopvalue variants; empty-literal rebind; a dynamic-string value (guards the already-clean string path); amixedobject 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) andobjects ownership reassign invoker(315, covering the predicate's other call sites — object properties and invoker arguments).cargo buildis warning-free.Note on #528
#528 (indexed rebind leak after pushing into an outer array) was re-verified on current
mainand 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