Skip to content

fix: make ExitWaiter.doExit idempotent to avoid double-resume crash#1861

Open
radheradhe01 wants to merge 2 commits into
apple:mainfrom
radheradhe01:fix/exitwaiter-idempotent-doexit
Open

fix: make ExitWaiter.doExit idempotent to avoid double-resume crash#1861
radheradhe01 wants to merge 2 commits into
apple:mainfrom
radheradhe01:fix/exitwaiter-idempotent-doexit

Conversation

@radheradhe01

@radheradhe01 radheradhe01 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

ExitWaiter.doExit resumes every stored continuation but never guards against being called twice and never clears the array. doExit can be reached more than once for the same process: in the exec path the registered onExit calls releaseWaiters (→ doExit) and then performs further work that can throw (process.delete()); ExitMonitor.track catches that error and invokes the same onExit again, calling doExit a second time. Resuming a CheckedContinuation twice is a fatal trap.

This change makes doExit idempotent: it returns early if the exit has already been recorded, sets exitStatus first, and clears the continuations before resuming them so they cannot be resumed again. All access is serialized on the owning actor; the guard makes the "exit fires once" invariant explicit and crash-safe.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Verified by static / code-level review; not built locally (no macOS 26 toolchain available here) — CI build will validate.

### Problem
`ExitWaiter.doExit` resumes every stored continuation but never guards against being called twice and never clears the array:

```swift
public func doExit(exitStatus: ExitStatus) {
    for cc in continuations {
        cc.resume(returning: exitStatus)
    }
    self.exitStatus = exitStatus
}
```

`doExit` can be reached more than once for the same process. In the exec path, the registered `onExit` callback calls `releaseWaiters` (→ `doExit`) and then performs further work that can throw (`process.delete()`); `ExitMonitor.track` catches that error and invokes the **same** `onExit` again, calling `doExit` a second time. Resuming a `CheckedContinuation` twice is a fatal trap.

### Fix
Make `doExit` idempotent: return early if the exit has already been recorded, set `exitStatus` first, and clear the continuations before resuming them so they cannot be resumed again.

### Notes
All access is serialized on the owning actor; the guard simply makes the "exit fires once" invariant explicit and crash-safe.
@radheradhe01 radheradhe01 changed the title Make ExitWaiter.doExit idempotent to avoid double-resuming continuations fix: make ExitWaiter.doExit idempotent to avoid double-resume crash Jun 30, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code Coverage

Tier Line Coverage
Unit 33.21%
Integration 0.64%
Combined 33.85%

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