Skip to content

rc: a local read for an argument is held when a later argument assigns it - #657

Merged
ASDAlexander77 merged 3 commits into
mainfrom
rc-argument-slot-assigned
Oct 23, 2026
Merged

ASDAlexander77 merged 3 commits into
mainfrom
rc-argument-slot-assigned

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Follow-up 7 from the own-raytrace rulings, and the rc half of 8 (fuzzer A1, local form).

Bug

g(n, (n = mk(2), 0)): arguments are read in order, so the second gave n a new value before the call took the first, and under rc that assignment released the block the callee then read (exit 139 or garbage; probes review8r3 L5, L6, L6b, L7, L7h, h.ts, fuzzer A1-local). Also:

  • a field or an element of a local (g(h.v, (h.v = mk(), 0)), also of a folded const h = new H());
  • a conditional argument (g(n, c ? (n = mk(), 0) : 1)) - the call lands in a later block;
  • a call in a try body; an assigned parameter; a closure called in the argument list that assigns a captured n.

Fix

holdGlobalArguments now also holds a read of a local, as #647 holds a read of a global: retained right after the read, released after the call (and in the handler of a try that takes what the call throws). It holds when a store into the local, a place in it, or its release - or any call, for a captured local - lies on a path from the read to the call: new own_facts::slotOf / slotWritten / findBetween (OwnershipFacts.h), which follow the blocks a conditional argument makes. The retain also goes right after the read when the call is in a later block of the same region and no loop leads back (before, only same-block reads; this also helps a global read in a conditional argument list).

own still accepts the local forms here; its half is the next PR (stacked on this one).

Tests

  • tests/00argument_slot_assigned.ts (gc, rc, none; AOT + JIT): each form above, with allocations of the same size before each read, plus a call that throws.
  • Tooth: without the fix rc exits 139.

Verified

  • Windows Release: 5278/5278.
  • Linux (WSL): 5260/5260; the new test again under MALLOC_PERTURB_=165 MALLOC_CHECK_=3: 6/6.

🤖 Generated with Claude Code

Update

  • 2nd commit: a Shared<T> local reassigned in a later argument (g(s, (s = new Shared(..), 0))) is held under own too (handles are counted there; own read freed memory). Test own/own_shared_argument_assigned.ts in the shared-handle loops (gc, none, rc, own + verify-ownership).
  • Merged main: rc: hold an argument whose place the call overwrites #653 added overwrittenBeforeCall (same-block slot/place overwrite). Both are kept: rc: hold an argument whose place the call overwrites #653's runs first; localAssignedBefore still covers a conditional argument, a closure assigning a captured local, a field of a folded const, and a handle under own.
  • After the merge: Windows 5312/5312; Linux 5294/5294, the new tests 16/16 under MALLOC_PERTURB_=165 MALLOC_CHECK_=3.
  • mm_fuzz.py run --count 500 --seed 1 on Linux (this + -mm=own: a local lent to a call cannot be assigned before the call takes it #660): 924 programs, 0 rc failures, 1 own failure = fuzzer bug O4 (a Shared parameter reassigned in try/finally), pre-existing and unrelated.

…s it

`g(n, (n = mk(2), 0))`: the arguments are read in order, so the first
read `n` and the second gave `n` a new value before the call took the
first - and under rc that assignment released the block the callee then
read (exit 139, or garbage). The same with a field or an element of a
local (`g(h.v, (h.v = mk(), 0))`, also of a folded `const h = new H()`),
a conditional argument (its own blocks), a call in a try body, an
assigned parameter, and a closure called there that assigns a captured
local. Follow-ups 7 and the rc half of 8 (fuzzer A1, local form).

holdGlobalArguments now also holds such an argument, as it holds one
read out of a global (#647): retained right after the read, released
after the call (and in the handler of a try that takes what the call
throws). It holds a read of a local when a store into that local, a
place in it, or its release - or any call, for a captured local - lies
on a path from the read to the call (own_facts::findBetween). The retain
goes right after the read also when a conditional argument put the call
in a later block of the same region, unless a loop leads back.

own still accepts the local forms; that is the own half of follow-up 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 and others added 2 commits October 23, 2026 10:14
…signs it

`g(s, (s = new Shared(new V(2)), 0))` with `s` a local handle: the
assignment gave back the count the old handle held, the last one, and
`g` read the freed block. Handles are counted under own too, and the
previous commit held such a read under rc only, while own's new check
leaves handles to the counts - so own read freed memory. The hold now
applies to a handle under own as well, as holdGlobalArguments already
does for a handle read out of a global.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#653 added a same-block check for an argument whose slot or place a later argument overwrites (overwrittenBeforeCall); it runs before localAssignedBefore, which still covers a conditional argument, a closure assigning a captured local, a field of a folded const, and a handle under own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 7283f6b into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the rc-argument-slot-assigned branch October 23, 2026 09:56
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.

1 participant