Skip to content

A global held across a call is given back when the call throws - #645

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-rc-global-argument-unwind
Oct 22, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-rc-global-argument-unwind

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

This closes the gap left open in #643. holdGlobalArguments retains a global read given to a call that may overwrite it, and releases it after the call. When the call threw, the release was skipped: a try around the call caught the exception, and the global's block kept a count nobody gave back.

  • Under rc this applies to any held value.
  • Under rc and own it applies to a Shared<T> handle, where Shared.count showed the extra count.

Fix

This only applies when a try in the same function takes the call's exceptions (unwindRegionOf):

  • The held value also goes into a slot made at the function's entry. The slot is null except while the call runs: it is stored before the call and nulled after it.
  • The handler that runs first starts with a ReleaseSlot of that slot, then nulls it. That handler is chosen in the same order the landing pad uses (TryOpLowering):
    • for a call in the try body: the cleanup, else the catch clause (after the catch variable is set), else the finally;
    • for a call in a catch clause: the try's finally.
  • Normal paths through a finally see a null slot, so the release does nothing there.

An exception that leaves the function with no try around the call still leaks the count, the same as owned locals do today.

Tests

  • own/own_shared_global_argument_throw.ts runs in the shared_models foreach (gc/none/rc/own) and in the rc/own --verify-ownership list. It covers try/catch, try/finally (caught outside), a block with a local in a try body (a cleanup-only try), a call in a catch clause with a finally, a try in a loop, and the normal path. On main it fails under rc and own.
  • Teeth: turning the unwind slot off fails test-compile-rc-… and test-compile-own-own_shared_global_argument_throw (and their jit twins). Without the switch they pass.
  • Windows ctest -C Release: 4958/4958. WSL Linux full run: 4940/4940.

🤖 Generated with Claude Code

holdGlobalArguments (#639, #643) retains a global read given to a call that
may overwrite it and releases it after the call. A throw out of the call
skipped the release: a try around the call caught the exception, and the
global's block kept a count nobody gave back - under rc for any held value,
under own and rc for a Shared<T> handle, where `Shared.count` showed it.

When a try in the same function takes the call's exceptions, the held value
now also goes into a slot made at the function's entry, null outside the
call, and the handler that runs first (the try's cleanup, else its catch
clause, else its finally; the finally for a call in a catch clause - the
landing pad's own order) begins with a release of that slot. An exception
that leaves the function still leaks the count, as it does for locals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 4e1599d into main Oct 22, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-rc-global-argument-unwind branch October 22, 2026 20:44
ASDAlexander77 added a commit that referenced this pull request Oct 23, 2026
…global a count (#647)

Two shapes the memory-model fuzzer (#646) found double-freeing under rc
since #639.

- holdGlobalArguments retained the argument right before the call. A
  later argument of the same call could give the global a new value
  first (`f(G, (G = mk(), x))`, `f(H.n, (H.n = mk(), x))`): its release
  freed the block, and the hold then retained and released a freed
  block. The retain (and the #645 unwind slot) now follow the read.
- `const n = G` was folded into the read, holding no count. Once the
  global got a new value, here or in a call, `n` dangled (fuzzer R1).
  #639 made that a crash: `run(n.v)`, with `run` assigning G, held and
  released a field of the freed block. Under rc a const read out of a
  global, or an interface or bound method made over one, now gets
  storage like a const read out of a field or an element (#635, #641,
  #642, #644). Under own the folded read stays a borrow.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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