Skip to content

fork: fail truthfully when a fork continuation overruns its save buffer - #845

Closed
brandonpayton wants to merge 1 commit into
mainfrom
fork-save-buffer-truthful-failure
Closed

brandonpayton wants to merge 1 commit into
mainfrom
fork-save-buffer-truthful-failure

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

Detect a fork-continuation save-buffer overrun immediately after unwind and
before SYS_FORK, for both main-process and pthread fork paths. A too-deep or
too-wide call stack now fails with the exact required and reserved byte counts
instead of spawning a child from channel-corrupting state.

The authoritative fork-instrumentation reference now documents that this is a
post-unwind detection boundary, not an in-Wasm prevention mechanism.

ABI 18 changed the frame cursor to an absolute linear-memory address. The
detector therefore compares current_pos with
forkBufAddr + FORK_SAVE_BUFFER_SIZE; comparing the absolute cursor directly
with the byte size would reject every valid ABI 18 unwind.

Root Cause Evidence

The exact ABI 18 Homebrew launcher reproduces this platform limit:

  • fork buffer: 0x2ac000 .. 0x2b0000 (16,384 bytes)
  • unwind cursor: 0x2b0e2c
  • required continuation bytes: 20,012
  • overrun into the adjacent channel: 3,628 bytes

Without this PR, the channel activity zeroes the child's overrun _start
frame and rewind later traps in __deliver_pending_signal at an indirect call.
That symbol is downstream damage, not the cause. With this PR, the same real
brew --version path exits -1 at the fork boundary and reports:

fork() continuation save buffer overflow - the call stack at fork() needed
20012 bytes but only 16384 (FORK_SAVE_BUFFER_SIZE) are reserved

Validation

  • ./scripts/dev-shell.sh bash -c 'npm --prefix host exec vitest run test/fork-save-buffer-overrun.test.ts': 5/5 passed
  • ./scripts/dev-shell.sh bash -c 'npm --prefix host run typecheck': passed
  • exact ABI 18 Homebrew bootstrap image + staged ABI 18 Bash, brew --version: produced the 20,012/16,384 diagnostic and exited -1
  • git diff --check origin/main...HEAD: passed

Fresh hosted Node/browser and conformance gates are required on this rebased
head.

Scope

This is truthful-failure hardening, not the functional capacity increase.
Homebrew remains blocked until the fork-save region is enlarged or made
elastic, which is an ABI layout change requiring a version bump and rebuilding
ABI-bound program artifacts. This PR makes that remaining boundary explicit
without changing the ABI or replacing any artifacts.

The wpk_fork unwind writes the saved call stack into a fixed
FORK_SAVE_BUFFER_SIZE (16 KiB) buffer that sits immediately below the
syscall channel (forkBufAddr = channelOffset - FORK_BUF_SIZE). The
instrumented unwind carries no bounds check of its own —
crates/fork-instrument/src/runtime.rs documents the requirement
`frames_start_offset + Sigma(frame) <= buffer_size` but never enforces it.
So a fork() from a call stack too deep or wide to fit silently overruns
the buffer into the channel, corrupting the syscall channel and later
surfacing as an unexplained trap or a fork child that never makes
progress — never as the real cause.

Detect the overrun host-side, right after the unwind completes and before
SYS_FORK is sent: current_pos (the write cursor the instrumentation keeps
at the buffer base, which frames never clobber) exceeding the buffer size
means the unwind wrote past it into the channel. When it does, fail the
fork with a clear diagnostic naming the byte overage and the platform
limit, instead of forking on a corrupted channel. Applied to both the
main-process and thread fork paths, which Node and browser hosts share.

Validated in a real browser by temporarily shrinking the buffer: a fork
that overruns now reports "fork() continuation save buffer overflow ..."
and dies cleanly, while forks that fit still succeed (no false positives).
Adds a unit test for the detection arithmetic (wasm32 + wasm64); the
existing fork/spawn/thread suites pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brandonpayton
brandonpayton force-pushed the fork-save-buffer-truthful-failure branch from 0c5bd8b to f5ac952 Compare July 12, 2026 13:41
@brandonpayton

Copy link
Copy Markdown
Member Author

Exact-head CI completed with every substantive suite green except the known fixed-delay race in host/test/teardown-reclaim.test.ts:71:

This PR's focused fork-save-buffer-overrun.test.ts ran 5/5 green in the same failed Vitest job. No rerun requested while #889 remains open.

@brandonpayton

Copy link
Copy Markdown
Member Author

Superseded by #907. The truthful fork-save overrun failure is on main through ea489a227, reconstructed with main-process, pthread, and side-module coverage; this head was not merged verbatim. Issue #922 tracks the separate ABI-sensitive capacity design.

@brandonpayton
brandonpayton deleted the fork-save-buffer-truthful-failure branch July 13, 2026 17:01
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