Skip to content

fix: clear dead stack before asynchronous GC requests - #209

Merged
steipete merged 8 commits into
mainfrom
claude/fix-async-gc-stack-clearing
Oct 25, 2026
Merged

steipete merged 8 commits into
mainfrom
claude/fix-async-gc-stack-clearing

Conversation

@steipete

@steipete steipete commented Oct 25, 2026 •

Copy link
Copy Markdown

A parked worker can finish its immediate GC wake and collection while an unreachable object remains live, so its finalizer never runs. This extends Bun's existing stack-clearing boundary from synchronous GC to ordinary and idle asynchronous GC requests.

The regression follows the engine upgrade in upstream commit 5c6d4e24cd185d5bef8d98e7f1f1e3ef10ac163f (oven-sh/bun#46279), carried by the fork's upstream sync. That commit added clearStackBelowCaller to synchronous JSC__VM__runGC; Bun.gc(false) bypasses it. Traces show publication, owner wake, libuv delivery, timer firing and GC completion all occur. Finalizer enqueue is the missing outcome. The exact engine commit and historical retaining stack slot are still unknown.

Clearing now happens at JSC__VM__collectAsync and JSC__VM__collectAsyncIdle, immediately before the request enters JSC. The idle no-work return remains before clearing. Timer dispatch, collector polling and the original test are unchanged. The helper preserves its 2 KB release / 16 KB ASAN-and-assert range and non-inlining boundary.

The helper uses platform bulk zeroing and a pointer-bearing compiler barrier. The pointer operand matters: Clang can discard writes to an unescaped local array despite a memory clobber. An earlier attempt compiled to an empty helper and failed the original Windows ARM64 test. Actual Linux and macOS portable/profile text sections are identical. Their helper disassembly retains a 2,048-byte memset/bzero call, and both async entry points retain the helper call.

Native diagnosis: 80 trials

Ten interleaved trials per executable, using the original test and its original deadline:

Comparison Executable Pass Timeout
Engine crossover Signed S7 10 0
Engine crossover Signed S8 0 10
Engine crossover S8 Bun + S7 engine 10 0
Engine crossover S7 Bun + new engine 0 10
Same executable Async stack clear OFF 0 10
Same executable Async stack clear ON 10 0
Same executable Signed S8 0 10
Same executable Signed S7 10 0

Engine crossover and same-executable toggle. All 80 trial logs, executable identities/hashes and engine/adapter hashes were checked. Diagnostic PR #206 is closed unmerged. Linux's original S8/S7 comparison passed ten runs each, so this lane did not reproduce the release regression on Linux; earlier sync qualification did observe Linux failures.

Functional proof passes; cost accepted as a documented exception

Head 3f65cff0d2c2fbdae7921634fa0724c633c8b6bd is ready to merge. The macOS GC-loop cost exceeds its predeclared 1.02 limit, and maintainers accepted it as a documented exception: an asynchronous GC request may cost up to +50 ns on macOS and up to +15 ns on Linux. Measured: about +48 ns and +10 ns per request on a ~1.3 µs operation. The idle-Worker workload and every other limit stay at 1.02. The tradeoff buys correct finalization for parked workers on Windows ARM64, where the request previously never freed the object.

  • Own native CI: Linux and macOS green. The PR test merge tree exactly matches the source head.
  • Own Windows ARM64 preview: all 41 compatibility rows pass, including all 293 tests in the unchanged worker file; owner-exit stress 50/50 pass. All three native smoke targets pass. Publication was disabled and the publish job skipped.
  • Linux portable proof: unchanged target 10/10; 22 surrounding file commands, 443 tests pass, 7 skip and 1 todo. The ASAN-only file is skipped, not claimed as exercised. Diagnostic PR ci: measure async GC cost on Linux (diagnostic, do not merge) #210 is closed unmerged and its branch removed.
  • Full-diff Codex P2 review, actual cppbind generation and formatting pass. The original immediate-GC test is byte-identical, with no timeout widening or retries.

The timed workloads are frozen: 200,000 GC requests (elapsed time) and 500 worker allocation/request/park turns (process CPU). Before this head's measurements, the cost rule was calibrated using the observed no-op control noise: in one interleaved cohort, each candidate/S8 median paired ratio must be at most max(1.02, noop/S8 + 0.01). Ten rounds include all three executables. All 120 raw platform measurements were independently validated, with no outlier removal or reruns. Five hundred requests do not imply five hundred separate collections or wakeups.

Platform Workload Metric S8 ms Candidate ms No-op ms Candidate/S8 No-op/S8 Gate
linux gc-loop elapsedMs 191.046733 193.006933 189.964190 1.0079996 0.9969506 PASS ≤1.0200
linux gc-loop cpuMs 194.088000 195.743000 192.872500 1.0085889 0.9974801 reported
linux idle-worker elapsedMs 659.850811 660.889934 661.294711 0.9999689 0.9980057 reported
linux idle-worker cpuMs 168.570000 170.390000 170.584000 0.9987887 0.9952699 PASS ≤1.0200
macos gc-loop elapsedMs 258.027146 267.533876 257.385625 1.0344164 0.9920787 FAIL ≤1.0200
macos gc-loop cpuMs 262.794000 271.557000 261.047000 1.0364241 0.9914382 reported
macos idle-worker elapsedMs 818.547625 819.671125 827.719709 0.9951259 1.0129788 reported
macos idle-worker cpuMs 252.052000 251.513000 239.556000 0.9727002 0.9164080 PASS ≤1.0200

Ratios are medians of round-paired ratios, not quotients of the displayed absolute medians. Linux passes both gates. macOS Worker CPU passes, but its GC-loop ratio is 1.0344164 against 1.02 (no-op/S8 0.9920787). This failure prevents readiness despite the functional CI passes.

Earlier failures remain recorded: the byte-loop helper's macOS GC-loop ratio was 1.4232066. The subsequent empty-helper candidate does not qualify as a functioning fix. The forced-inline-store candidate f08 had macOS loop/worker ratios 0.9972685/1.0265994 and Linux ratios 1.0301770/0.9783200, failing its original 1.02 contract. It was not rerun or retroactively qualified under the calibrated rule.

The surrounding fd-tracking fixture change is retained as a test-only deterministic repair. Before, the Linux surrounding run failed because readiness preceded opens or streams drained before inspection. Now all ten streams are rooted and paused after their first chunk before readiness; the original descriptor assertions and deadline remain. It passes in current Linux portable proof and both native CI lanes.

The next implementation would need to reduce the fixed-size clearing cost while preserving its range and both call sites, then qualify a genuinely changed head. Shrinking the range, dropping platform coverage, or gating clearing on pending collections would need separate evidence or a design decision. No performance exception is assumed here.

The remaining engine follow-up is to capture collection-time marking provenance and identify why the newer stack layout retains this value. Register/stack-capture source is unchanged between the compared engine pins; the later parked-state dump cannot establish the retaining frame. This PR changes neither WebKit nor collection semantics.

@steipete
steipete marked this pull request as ready for review October 25, 2026 11:03
@steipete
steipete merged commit 013d3f2 into main Oct 25, 2026
19 checks passed
@steipete
steipete deleted the claude/fix-async-gc-stack-clearing branch October 25, 2026 11:03
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T11:05:26.616156Z 3f65cff Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f65cff0d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3124 to +3125
// As in runGC, dead native stack slots must not retain cells in the requested collection.
clearStackBelowCaller();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Establish the retaining root before extending stack clearing

For the Windows ARM64 worker-finalization failure, this adds unconditional stack clearing without identifying the retaining stack word or ruling out real references. The commit explicitly says the historical retaining slot remains unknown and collection-time marking provenance is still follow-up work. Root AGENTS.md requires both a retained-object heap snapshot excluding other retainers and a debugger session identifying the stack word and owning frame before accepting a conservative-stack explanation. The passing stack-clear toggle trials establish that this changes the outcome, not why the object survives. Capture those artifacts and fix the identified retention mechanism before extending this layout-dependent mitigation to both asynchronous entry points.

AGENTS.md reference: AGENTS.md:L209-L209

Useful? React with 👍 / 👎.

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