Skip to content

[Rehearsal/Fork/POSIX] Validate catch-state and spawn fixes before main-first activation - #1094

Closed
brandonpayton wants to merge 17 commits into
integration/homebrew-post-frozen-product-batch-qk044from
fix/fork-catch-activation-ownership-qk044
Closed

[Rehearsal/Fork/POSIX] Validate catch-state and spawn fixes before main-first activation#1094
brandonpayton wants to merge 17 commits into
integration/homebrew-post-frozen-product-batch-qk044from
fix/fork-catch-activation-ownership-qk044

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 25, 2026

Copy link
Copy Markdown
Member

Rehearsal only — do not merge or publish from this PR. Its artifacts are pre-main validation evidence. The replacement rollout lands the coherent source changes through a normal PR to main, then rebuilds every canonical artifact from the exact resulting main SHA.

Why

The ABI 42 Bash bottle crashes during the in-guest Homebrew lifecycle because code generated by wasm-fork-instrument lets ordinary exception handling overwrite unrelated memory.

For every supported plain WebAssembly catch, the instrumenter injects code that remembers the selected catch arm and its scalar payload. That generated code used the module-wide _wpk_fork_buf pointer even during normal catch execution, not just during fork(). The pointer is zero before the first fork and may name released, reusable continuation storage after replay. Recursive calls also shared the same catch tuple. A normal catch could therefore corrupt low memory, stale continuation storage, or another activation's state.

This is a Kandelo instrumenter bug, not behavior written by Bash and not an unavoidable property of WebAssembly exceptions. Clearing the pointer alone is insufficient because address zero is valid linear memory; the remembered values must belong to the function call that caught them.

Validation also exposed a separate posix_spawn ownership bug. The host copied every spawn blob into a 65,608-byte allocation but checked only whether it fit in the kernel's entire WebAssembly memory. The current development environment produces an 83,772-byte blob, so it overwrote the adjacent Rust heap. The resulting corruption could make a child complete and then disappear from waitpid.

What changed

Activation-owned catch replay

  • Give every supported plain-catch region activation-local arm and scalar-payload locals.
  • Serialize those synthetic locals in that invocation's linked continuation frame, so recursion and nested calls cannot alias one another.
  • Replay exact nonnegative plain-catch arm IDs and use -1 for the mixed catch_ref path instead of consulting stale table contents.
  • Remove plain-catch scratch from the module prefix.
  • Clear _wpk_fork_buf before returning to normal execution as a secondary lifetime defense.

Bounded spawn transport

  • Validate the caller's path, blob, and pid-output ranges before reading them.
  • Apply the same 4 MiB argv/environment contract used by execve.
  • Keep ordinary blobs in the existing channel-sized scratch allocation.
  • Lazily allocate one bounded kernel-owned buffer for larger complete spawn blobs and reuse it synchronously on the kernel worker.
  • Return a real error instead of writing outside either allocation.
  • Refresh exactly the three VFS identities whose build process executes the changed host runtime while prewarming: lamp, nginx-php-vfs, and wordpress. The package input-closure test enforces this dependency.

Focused prepublication staging

  • Add a fail-closed stage-rootfs-closure-only mode that derives the exact current 15-package wasm32 rootfs closure and records every deferred package.
  • Preserve the derivation helper's no-clobber contract by giving it a new file inside a private temporary directory.
  • Keep this mode from authorizing a merge: merge-gate remains pending and prepare-merge rejects the limited label.

ABI decision

This change does not alter exported functions, descriptor fields, state values, imports, host parsing rules, or syscall wire formats.

Each instrumented artifact already declares its own module-specific prefix and frame sizes. Existing artifacts retain their existing generated code; rebuilt artifacts describe the smaller prefix and larger activation frames. Fork-using packages must be rebuilt to receive the correction.

The spawn fix uses the existing kernel_alloc_scratch export and existing spawn wire format. It corrects ownership and bounds in the shared Node/browser host path.

Reference-typed locals, caught references, mutable reference globals, and arbitrary mutable table state remain a separate correctness boundary. A fresh fork child has a fresh Wasm instance, so raw auxiliary-table references cannot be treated as preserved. That broader work is being handed to a separate ABI-epoch workstream that will reconstruct supported scalar exception state and reject non-serializable reference state before execution. This PR does not claim that work is complete.

An independent audit after this head was assembled also found pre-existing allocation-capacity hazards in PTY input, initial-cwd, and vectored/socket I/O scratch transfers. They are not caused or changed by this PR and are being handled in a separate systematic scratch-ownership workstream. This PR claims the complete spawn transport fix, not repository-wide hostile-input scratch safety.

Regression coverage

Activation ownership:

  • catch before the first fork cannot modify low memory;
  • catch after replay cannot modify released continuation storage;
  • three recursive calls retain three distinct caught payloads;
  • outer-frame allocation failure and abort replay preserve low and retired storage;
  • mixed plain and reference catches select the correct replay path; and
  • wasm32 and wasm64 end functions clear the buffer before publishing normal state.

Spawn transport:

  • exact channel-sized and first-byte-over boundaries;
  • exact whole-blob maximum and over-limit rejection;
  • invalid path/blob/pid-output ranges and metadata limits;
  • allocation failure and invalid returned allocation;
  • two consecutive real spawns with more than 64 KiB of environment data, complete value verification in each child, and successful waitpid reaping.

Validated on the integrated product tree:

  • fork-instrument suite: 210 passed;
  • focused host continuation suite: 38 passed;
  • host spawn/process slice: 108 passed;
  • complete Sortix spawn surface: 24 passed;
  • package build-input import closure: 18 passed;
  • host typecheck: passed;
  • kernel unit suite: 1,248 passed;
  • libc conformance: 303 passed, 20 expected failures, 1 accepted flaky result;
  • Open POSIX suite: 174 passed, 3 expected failures, 2 skipped;
  • package publication contract suite: passed;
  • authoritative program-package projection: current;
  • exact local rootfs projection: 15 wasm32 generations and no unrelated package;
  • Bash wasm32 cache key: unchanged at 1d813d7f9db4979fc5c0eef6b37e213cb73a42395757cc92ef8ebeee2e62913d; and
  • ABI snapshot, C header, TypeScript bindings, and artifact guards: consistent.

For the real Bash module, activation ownership reduces the instrumented file from 3,360,883 to 3,348,482 bytes (12,401 bytes, or 0.37%). The gzipped difference is 769 bytes. Performance was not measured.

GitHub staging run 30144350443 completed successfully on this exact head: all 15 intended wasm32 rootfs generations passed, the library matrix stayed skipped, no unrelated package entered the matrix, and final release materialization succeeded.

Exact Node.js/Chromium shell run 30144350446 did not reach either boot proof. It resolved or built 40 of 47 selected generations, then failed for two coherent-tree reasons:

  • five VFS/image consumers attempted to rebuild the ABI 42 shell source while the product metadata still selected the ABI 41 shell plan; and
  • the workflow selected the declared wasm64 MariaDB generations without preparing the required wasm64 sysroot.

This is useful pre-merge evidence rather than final release evidence. It confirms that the focused 15-entry rootfs staging path is green and that the earlier disk-clobber failure is gone, while also showing that this pre-main integration tree is not a coherent final package-production source. The corrected Bash bottle, exact browser-input closure, Node.js/Chromium boot proof, and live first-/third-party Homebrew lifecycle remain required.

Integration note

This PR is intentionally based on integration/homebrew-post-frozen-product-batch-qk044, but Git reachability is not sufficient package provenance. Neither an ancestry-only merge nor a protected pre-main source tag would make bottles built from this head bottles built from main.

Accordingly:

  • PR [Homebrew/Provenance] Keep the corrected Bash source in main history #1095 is not part of the publication path;
  • the mutable pr-1094-staging artifacts are validation evidence only;
  • no final bottle or durable browser closure may be published from this pre-main head; and
  • the coherent ABI 42 platform and packaging activation must first merge normally into main, without prematurely switching the product shell.

After that merge, every final dependency archive, bottle, index entry, and browser closure must be rebuilt from the exact resulting main commit. The tap controller must pin that same commit. The limited staging mode proves the bounded rootfs build behavior; it does not authorize merge or publication.

Brandon approved the corrected fork/spawn change, but the integration and publication sequence is being replaced by this main-first provenance path. This PR must not be merged, closed, or used as a final artifact source until that split is complete and its staging evidence is preserved.

@brandonpayton brandonpayton added the skip-staging-tests Opt into skipping the staging tests. This does not skip tests before merge. label Jul 25, 2026
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-1094-staging

ABI v42. 15 built, 0 failed, 15 total.

Package Arch Status Sha
bc wasm32 built 2e7bb9ff
coreutils wasm32 built 33ca2b6c
dash wasm32 built 2ec3ac4e
diffutils wasm32 built 5fd58502
file wasm32 built 4c75af58
findutils wasm32 built e9894ce9
gawk wasm32 built 85ef6c0f
grep wasm32 built ccfc4ba2
m4 wasm32 built a7063dcf
make wasm32 built 49a0f581
ncurses wasm32 built 1d43a87f
posix-utils-lite wasm32 built 2308c7f1
sed wasm32 built 7ba13641
bash wasm32 built 1d813d7f
rootfs wasm32 built 4feb895e

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

Add a maintainer-controlled staging scope that validates the current program projection, force-selects every rootfs wasm32 dependency generation into the PR release, and visibly defers unrelated program generations. Keep the limited run from authorizing prepare-merge and cover malformed, stale, missing, duplicate, identity-mismatched, and non-wasm32 projections.
Exercise the fail-closed path explicitly when the validated program index has no rootfs package projection.
Fail the limited prepublication gate if its target release changes after preflight instead of hiding the race with a canonical-release supplement.
@brandonpayton brandonpayton added the stage-rootfs-closure-only Stage only the exact rootfs wasm32 closure; never authorizes merge label Jul 25, 2026
SYS_SPAWN used the channel-sized scratch address for every blob while checking only the remaining WebAssembly memory. Environments above one channel therefore overwrote adjacent Rust heap state and could make a completed child disappear from waitpid.

Validate the complete transport, preserve the 4 MiB ARG_MAX contract, and lazily allocate one bounded kernel-owned buffer for large blobs. Reuse is safe because copy and kernel parsing are synchronous on the kernel worker event loop. Add boundary-focused host coverage, a two-spawn executable regression, and architecture documentation.
LAMP, nginx+PHP, and WordPress image builds boot NodeKernelHost for opcache prewarming or database installation. Their declared host/src input intentionally makes kernel-worker behavior part of the generated image identity.

Regenerate only those three projected cache identities after the large SYS_SPAWN transport correction. The existing import-closure contract verifies that kernel-worker changes affect exactly these image packages.
@brandonpayton brandonpayton changed the title [Fork] Keep exception replay state inside each function activation [Fork/POSIX] Isolate catch replay state and prevent large-spawn corruption Jul 25, 2026
@brandonpayton
brandonpayton marked this pull request as draft July 25, 2026 08:05
@brandonpayton brandonpayton changed the title [Fork/POSIX] Isolate catch replay state and prevent large-spawn corruption [Rehearsal/Fork/POSIX] Validate catch-state and spawn fixes before main-first activation Jul 25, 2026
@brandonpayton

Copy link
Copy Markdown
Member Author

Closing this rehearsal PR so it cannot be mistaken for the activation or publication path. Its commits and CI remain useful test evidence, but graph reachability is not artifact provenance. The replacement will land normally from current main; after GitHub writes that change to refs/heads/main, every canonical archive, durable package generation, bottle, tap index, and VFS image will be rebuilt from that exact commit. No artifact produced by this PR will be reused as a final input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-staging-tests Opt into skipping the staging tests. This does not skip tests before merge. stage-rootfs-closure-only Stage only the exact rootfs wasm32 closure; never authorizes merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant