Skip to content

[Fork/ABI] Make fresh-instance replay activation-state safe - #1096

Draft
brandonpayton wants to merge 15 commits into
fix/fork-catch-activation-ownership-qk044from
fix/fork-activation-state-safe-abi43-qk044
Draft

[Fork/ABI] Make fresh-instance replay activation-state safe#1096
brandonpayton wants to merge 15 commits into
fix/fork-catch-activation-ownership-qk044from
fix/fork-activation-state-safe-abi43-qk044

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 25, 2026

Copy link
Copy Markdown
Member

Why

Kandelo promises that software can call fork() at an arbitrary point in its
execution. A child process copies linear memory into a fresh WebAssembly
(Wasm) instance, but references, exception objects, mutable globals, tables,
and dynamically loaded module instances do not come with that byte copy.

The old instrumenter stored reference values in module-wide scratch tables and
put only a table slot number in each continuation frame. That was unsafe for
recursion and reentrancy, and it failed in a real child because the fresh
module's scratch tables were empty.

The first draft of this pull request replaced that failure with broad
instrumentation rejection. That was also the wrong contract: ordinary
compiler-generated Wasm references, exceptions, globals, tables, and dynamic
link state need preservation mechanisms, not source-shape exclusions. This
revision gives every valid reference shape an explicit reconstruction owner.

What changed

  • Live activation references use one versioned process reference graph.
    Function references are named through fresh-instance catalogs; external
    references use process-owned handles; exception and Wasm garbage-collection
    values use generated typed codecs that preserve aliases and cycles.
  • Catch, CatchRef, CatchAll, CatchAllRef, legacy exception handling,
    scalar/vector/reference payloads, multiple arms, distinct targets, recursion,
    and compiler cleanup state replay by reconstructing and throwing inside the
    child instance. No exception reference crosses JavaScript.
  • Mutable scalar/reference globals, imported globals and tables, table
    set/fill/copy/init/grow, and data/element segment lifetime have
    explicit module-state owners. Pthread replicas synchronize through a
    published generation.
  • Main and side modules replay one ordered transaction. The dynamic loader
    records exact memory/table bases, dependencies, handles, symbol scope,
    thread-local storage, initialization state, and table mutations.
  • Legacy synchronous loader imports are lowered to a staged, non-reentrant
    protocol. Host imports compile and instantiate only; libc invokes bootstrap,
    relocation, and constructors as ordinary Wasm after the import returns.
    Instrumented artifacts have no native Wasm start section.
  • Temporary exception, reference, transit-table, loader, and transaction roots
    are released after success, abort, process replacement, or worker teardown.
    The historical module-static reference stashes are absent.
  • The linked continuation remains arbitrarily chained. Reference recipes add
    no ordinary source-function local and no bytes beyond the existing 16-byte
    frame header. Catch payload storage overlays the maximum simultaneously live
    tuple rather than growing with arm count.
  • This is application binary interface (ABI) 43. Artifacts must claim
    FORK_CAP_ACTIVATION_STATE_SAFE; package/index/host guards also require the
    exact role metadata, staged-loader inventory, and zero native start sections.
    Unsafe ABI 42 output cannot be relabeled as ABI 43.
  • Fork-instrumented package identities hash Cargo's target-unfiltered non-dev
    dependency union. The shared cache has no build-host dimension, so identical
    source now receives the same key on macOS and Linux.
  • Source-only staging and prepare-merge jobs build the fork inventory tool
    before artifact-guard integration tests; a workflow contract guards that
    prerequisite.
  • Artifact guards no longer ask WABT to disassemble modern exception-handling
    and Wasm garbage-collection modules. The instrumenter now exposes a bounded
    structural identity query that validates the ABI export thunk, optional
    delegate, memory model, relocatable role, fork imports, and fork exports
    without decoding unrelated function bodies. ABI extraction and fork-import
    predicates consume the same record, including for large transformed
    programs that exceed WABT's decoder. A source-only WABT fallback is retained
    for environments where the Rust tool has not yet been built.

Validation

Run through scripts/dev-shell.sh against this implementation:

  • bash scripts/dev-shell.sh bash scripts/ci-run-test-suite.sh fork-instrument
    — 331 passed, 0 failed.
  • bash scripts/dev-shell.sh bash scripts/ci-run-test-suite.sh vitest
    — 2,552 passed, 2 expected ucontext failures, 130 skipped, 3 failures.
    Two are the exact PR [Rehearsal/Fork/POSIX] Validate catch-state and spawn fixes before main-first activation #1094-base lazy-archive assertions: that base calls
    fetch(url, undefined), while current main owns Brandon's one-argument
    correction in 7d8929e87. The third was an aggregate-suite output race:
    fork-dlopen-replay-e2e observed host/dist/worker-entry.js while another
    test was rebuilding that directory; the focused file passed 4/4 immediately
    afterward. An earlier aggregate run also exposed stale local less and
    tar mirror generations; rebuilding/reinstalling them through their normal
    package scripts corrected their final keys, and the affected focused
    resolver/credentials files passed 12/12.
  • Corrected compiler exception/fork matrix — 41 runnable cases passed,
    2 expected ucontext failures, and 8 source-less cases delegated to named WAT
    suites.
  • Fresh Node process Worker CatchRef coverage — 2/2 passed, including a
    non-null function-reference payload that the child calls.
  • Chromium continuation coverage — 5/5 passed: deep chained continuation,
    root/later allocation rollback, scalar CatchRef, reference-bearing CatchRef,
    and aliased Wasm-GC state.
  • Chromium, Firefox, and WebKit GC identity/transit/cleanup coverage — 3/3
    passed.
  • Real main/pthread/side-module/constructor/dependency/TLS loader group —
    23/23 passed; loader host-import misuse coverage — 7/7 passed.
  • Focused final host parser/runtime/parity set — 59 passed, 2 capability skips.
  • Sortix — 5,037 passed; libc — 303 passed; POSIX — 174 passed.
  • bash scripts/dev-shell.sh bash scripts/check-abi-version.sh update — passed.
  • bash scripts/dev-shell.sh bash scripts/check-abi-version.sh — passed.
  • bash scripts/dev-shell.sh bash scripts/test-wasm-artifact-guards.sh
    passed, including structural present/missing/invalid ABI state, fork-import
    truth, malformed-decoder fail-closed behavior, and the source-only
    WABT/Binaryen fallback.
  • Structural artifact-identity tests cover modern GC modules, helper/delegated
    ABI thunks, Wasm64, missing/invalid ABI state, and the command-line output.
    The previous remote preflight and toolchain-cache failures were WABT
    decode failures on valid ABI 43 modules, not reference-shape rejection.
    On exact head e61c1a1a8, preflight and toolchain-cache both pass with
    the structural decoder. The same run's change-scope, package/test-gate
    validation, fork-instrument, and cargo-kernel jobs pass. Its ABI 43 library
    rebuild phase completed without a failure. Its program phase reached 28
    successful jobs before CPython exposed two remaining WABT predicates:
    instrumentation and the complete ABI 43 fork contract succeeded, then ABI
    extraction returned status 2 before packaging.
  • Reproduced CPython 3.13.3 from source in an isolated work root. The exact
    13 MiB transformed artifact passes structural ABI, fork-import, and complete
    fork-contract validation; after routing those final predicates through the
    structural decoder, the full recipe packages and installs python.wasm and
    its 14 MiB runtime ZIP. homebrew-validate-wasm-executable.sh ... 43 wasm32
    reports required.
  • Sealed local installation, local-generation handling, and Homebrew bottle
    inspection pass with the updated guard. Regenerating
    program-packages.json produced no diff, so the fix does not change package
    cache identities.
  • Exact-head staging run 30193794024 proves the CPython guard fix on
    6babf5e2f: all 14 foundational library builds and 56 of 62 attempted
    program builds passed. CPython 3.13.3 built a 13 MiB python.wasm and
    14 MiB runtime ZIP, installed both through the package resolver, uploaded
    the staged ABI 43 archive, and recorded a successful index entry; job
    89775163413 passed. The independent rootfs job 89775165583 also passed,
    building and staging the 336-path, 16,787,687-byte ABI 43 rootfs. The six
    attempted-program failures were exactly shell and its five direct
    consumers (lamp, nginx-php-vfs, nginx-vfs, node-vfs, and
    wordpress). Every terminal log reaches the same fail-closed ABI 41
    Homebrew metadata guard; none reaches a fork-instrument or package-specific
    transform failure.
  • Final PR head 320e2bc1b contains only two documentation commits after that
    tested implementation SHA. Browser smoke run 30199271143 passed, and
    staging run 30199271156 passed its fork-instrument, cargo-kernel,
    artifact/package validation, toolchain-cache, and complete preflight jobs.
    Preflight computed the same 14-library/62-program matrix because the
    incomplete PR ledger cannot safely be reused as a complete fetch-only
    snapshot. The remaining staging rebuild and exact-shell run 30199271149
    were then intentionally cancelled to avoid spending hours rebuilding
    unchanged cache keys and reproducing the already classified missing
    Homebrew input; no validation failure triggered either cancellation.
  • Exact-shell run 30193794103 started before that PR generation was
    complete, selected the canonical/source fallback, and resolved 40 of 47
    browser inputs. Five composite images truthfully rejected the existing ABI
    41 Homebrew shell metadata; MariaDB wasm64 and its VFS lacked sysroot64 in
    that fallback. The run failed before shell composition, Node boot, Chromium
    boot, or fork replay. The staging matrix's own shell job 89775165119 then
    materialized its ABI 43 package dependencies and independently stopped at
    the same ABI 41 Homebrew metadata guard. A complete verified PR generation
    therefore cannot exist until the coordinated ABI 43 Homebrew bottle and
    metadata rebuild is performed; rerunning alone cannot make this boundary
    green.
  • Fresh exact-head browser smoke passes, including host typechecking and the
    Playwright cross-origin-isolation smoke.
  • A full local ./run.sh browser --host 127.0.0.1 materialization pass
    resolved 56 of 71 requested package entries, rebuilt the 336-path rootfs,
    and validated the kernel and core gallery inputs. Redis server, GNU tar,
    Tcl, Vim, and wget passed fork instrumentation during this pass, alongside
    the already rebuilt Git, git-remote-http, and nginx artifacts. The command
    then stopped before Vite because the required Homebrew shell metadata
    declares ABI 41 while the candidate requires ABI 43.
  • Program-package projection check, standalone resolver regeneration check,
    git diff --check, and changed-shell bash -n checks — passed.
  • Full xTask validation — 534/534 unit tests and 1/1 integration test passed.
  • Package-system Vitest — 18 files and 125/125 tests passed on the
    host-independent cache head; package publication-flow and package
    build-root/projection contracts passed. After the workflow-only tool
    preparation edit, 124/125 passed in one aggregate rerun and
    program-resolver-literals hit its known five-second parallel-load timeout;
    that file immediately passed 2/2 alone in 1.48 seconds. The explicitly
    prepared artifact-guard file passed 16/16.
  • Cross-host projection proof — generation with
    CARGO_BUILD_TARGET=x86_64-unknown-linux-gnu was byte-identical to ordinary
    macOS generation; the clean remote Linux package projection check also
    passed.
  • bash scripts/dev-shell.sh bash build.sh — passed and locally rebuilt the
    kernel, instrumenter, fixtures, host, ABI 43 package closure, and 336-path
    root filesystem image from the corrected final keys. The resulting
    rootfs.vfs is 16,787,687 bytes. Nothing was published.

Not run or not claimed:

  • shellcheck is not installed in the declared dev shell. It was attempted
    and is not claimed; bash -n and the executable shell tests above pass.
  • A successful manual browser page boot is not claimed. The complete
    dev-shell ./run.sh browser preparation now reaches the shell resolver, but
    correctly rejects the existing Homebrew closure's ABI 41 metadata before
    Vite starts. Broad Playwright gallery coverage therefore remains unrun until
    the coordinated ABI 43 bottle/shell rebuild exists. The focused tests above
    did execute real browser process Workers, and exact-head browser smoke is
    green.
  • Staging test-gate-prepare and its split downstream suites did not run.
    The inherited fetch-only workflow requires a complete program matrix before
    materializing the prepared workspace, and there is no ABI 43 last-green
    shell artifact to substitute for this new epoch. Loosening that condition
    would only move the same missing-artifact failure into preparation.
  • exec-brk-base skips because the local MariaDB artifact is still ABI 18.
  • A complete ABI 43 registry/browser generation does not yet exist: staging
    produced all 14 library archives and 56 program archives, but the shell and
    five shell-derived images remain unavailable until the coordinated
    Homebrew rebuild. Seven further ABI-bound program generations were not
    scheduled by the inherited staging policy: erlang-vfs, perl,
    perl-vfs, python-vfs, redis, sqlite-cli, and texlive. The local
    proof is the 15-generation rootfs closure, not a publication candidate.
  • CI uploaded the 70 successful current-key archives and failure records to
    the isolated pr-1096-staging prerelease ledger. No Homebrew bottle,
    canonical ABI 43 binary index, canonical shell/VFS artifact, or release
    namespace was promoted or activated.

ABI and rebuild scope

ABI 43 changes the fork capability contract, host channel request flags,
reference transaction/archive formats, generated Rust/C/TypeScript constants,
module bootstrap requirements, and artifact inventory.

Every fork-instrumented main program and side module must be rebuilt from raw
linker output. Affected package bottles, package receipts, binary indexes,
shell closure, and VFS images must then be regenerated in that order. The
final-key local proof rebuilt Dash, Bash, ncurses, coreutils, gawk, grep, sed,
bc, file, m4, make, findutils, diffutils, and posix-utils-lite from source.
The coordinated run must re-enable the six expensive packages excluded from
ordinary staging and must give sqlite-cli a reviewed source-build owner or
retire that generation; no ABI 42 binary or missing ledger entry can fill
those seven slots.
The complete coordinated publication sequence is recorded in
docs/plans/2026-07-25-abi-43-activation-state-safe-rebuild-plan.md.

Stack and merge

This draft remains stacked on PR #1094's exact rehearsal head
6d923c6454dd7174082f25c3d3991d03f86f5ddb. PR #1094 was not modified or
pushed. PR #1097 is the active ABI-42 main-first replacement-base work; this
draft will not be restacked until that base is agreed and lands. Before merge,
the restack must preserve Brandon's already-main VFS correction and
attribution.

Do not merge this pull request without Brandon's explicit approval.

Serialize supported tagged catch state per activation, reject non-reconstructible reference and table state, and require the ABI 43 safety capability across build and host launch boundaries.
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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

ABI v43. 70 built, 6 failed, 76 total.

Package Arch Status Sha
icu wasm32 built 3785cb4f
libcurl wasm32 built 2e9bfe54
libcxx wasm32 built 0b9e4ae7
libcxx wasm64 built b1e80986
libiconv wasm32 built 2ae7c3ce
libpng wasm32 built 00b34f15
libxml2 wasm32 built 4c2d44bb
libzip wasm32 built 592acc36
openssl wasm32 built d497113a
openssl wasm64 built e622094d
sqlite wasm32 built 35544fa8
sqlite wasm64 built ff0ea759
zlib wasm32 built f372595b
zlib wasm64 built 5717b46e
bc wasm32 built 26c9f859
bzip2 wasm32 built 18344818
coreutils wasm32 built 640ffdd1
cpython wasm32 built 6bb296c9
curl wasm32 built d55e6c3d
dash wasm32 built e6a122a8
diffutils wasm32 built b6423d11
dinit wasm32 built 459ad0cd
erlang wasm32 built 526bc877
fbdoom wasm32 built 78caa3cd
file wasm32 built 039f051d
findutils wasm32 built 1cf00e9e
gawk wasm32 built 57a33bb3
git wasm32 built 86f7980e
grep wasm32 built 6dcbd57a
gzip wasm32 built e9487ab0
homebrew-bootstrap wasm32 built 3ca8b809
kandelo-sdk wasm32 built 7948a22d
kernel wasm32 built 8b523b39
less wasm32 built e6f4f1d9
lsof wasm32 built 9fd429eb
m4 wasm32 built 0b203084
make wasm32 built d30ffaed
mariadb wasm32 built 0ab88ecb
mariadb wasm64 built d3641197
modeset wasm32 built b1d459dd
msmtpd wasm32 built 1676f17c
nano wasm32 built 5fb7f665
ncurses wasm32 built 2a8d1a03
netcat wasm32 built edcf82d4
nginx wasm32 built afde4ab2
php wasm32 built e0bae805
posix-utils-lite wasm32 built 7446b2dc
ruby wasm32 built a7d611f7
sed wasm32 built d5f738b9
spidermonkey wasm32 built 313e60b9
tar wasm32 built d712156a
tcl wasm32 built 3c78c926
unzip wasm32 built 662e6659
userspace wasm32 built 67450243
vim wasm32 built b2d332ff
wget wasm32 built 8947f2fb
xz wasm32 built ab7ba59e
zip wasm32 built af083f1d
zstd wasm32 built 694f316b
bash wasm32 built 1be72f85
mariadb-test wasm32 built 52fddeeb
mariadb-vfs wasm32 built efa6b131
mariadb-vfs wasm64 built e63163f3
nethack wasm32 built 0262e86e
node wasm32 built eefc841b
redis-vfs wasm32 built 4178da00
shell wasm32 failed
spidermonkey-node wasm32 built f9ac0d7d
vim-browser-bundle wasm32 built 4e098ad4
lamp wasm32 failed
nethack-browser-bundle wasm32 built bd33eda7
nginx-php-vfs wasm32 failed
nginx-vfs wasm32 failed
node-vfs wasm32 failed
rootfs wasm32 built 5526de95
wordpress wasm32 failed

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

Replace the ABI 43 source-shape rejection shortcut with typed activation, exception, GC, static-root, global, table, and segment reconstruction recipes. Lower legacy EH and reentrant loader imports before reachability analysis, remove module-static reference stashes, and retain the fixed linked-frame header and arbitrary chained continuation allocator.

Version the activation-safe artifact contract and inventory so stale ABI 42 or native-start artifacts cannot be restamped as safe.
Give Node and browser process Workers the same versioned reference graph, externref authority, imported-state, table-replica, exception, and replay-gate owners. Recreate main and side-module Store-local identities before continuation rewind and release temporary roots on completion, abort, exec, and teardown.

Stage dlopen as host-only prepare/next operations followed by ordinary Wasm initializers, serialize pthread loader ownership, replay exact module layouts, and defer signal delivery until libc regains the guest boundary.
Exercise compiler EH, CatchRef reference payloads, GC cycles and aliases, externref authority, mutable globals and tables, worker-import exceptions, abort recovery, and stale-root cleanup. Run real child process Workers instead of same-instance simulations.

Cover main, pthread, side-module, constructor and DT_NEEDED loader paths, plus deep arbitrarily chained continuations and transactional ENOMEM recovery in Node and Chromium.
Require ABI 43 activation-state-safe capability, exact role metadata, zero native start sections, and staged-loader inventory across package receipts, indexes, sidecars, Homebrew validation, and shell guards. Build valid compiler-EH fixtures normally instead of routing them to an unsupported bucket.

Regenerate the source-bound program projection and standalone resolver, and select the declared LLVM archiver on Darwin so final-key source rebuilds remain reproducible.
Document the activation-byte versus deterministic-recipe invariant, fixed frame footprint, arbitrary chained continuation, complete CatchRef/GC/global/table ownership, non-reentrant staged loader boundary, and truthful host-engine limits.

Record the exact package, bottle, index, shell, and VFS rebuild sequence without claiming broad publication or manual browser coverage before those coordinated artifacts exist.
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