972 lines · plain
1================================================================================2Signals model for the wasm32 port3================================================================================4 5This document is the spec-first companion to ``threading-model.rst`` and6``sched-model.rst``, focused on **POSIX signal delivery**: how a signal7sent to a wasm-Worker-backed process or pthread actually gets observed8by user code, how default dispositions are honored, how ``sigaction``9installs handlers, and how the kernel-side ``signal_pending`` machinery10interacts with the in-Worker ``Atomics.wait`` blocking on futexes that11K7-U4e established.12 13It is written as the **K8 opener** (per the K-phase discipline of14``docs/ARCHITECTURE.md §18``: design before implementation; refutation15is cheaper at spec time than at code time).16 17Status: K8 OPENER. K8 ships the load-bearing infrastructure for signal18delivery and dispatch; richer signal surface (real-time signals, signal19masks across syscalls, ptrace stop-restart) is K9+ scope. The20implementation sequence is captured in §11 below.21 22--------------------------------------------------------------------------------231. Why this document exists separately24--------------------------------------------------------------------------------25 26``ARCHITECTURE.md §17.5`` ("Signal delivery: deferred") sketched the27intended signal-delivery model at K1 spec time: a wasm32 ``kill(pid, sig)``28would set the bit in ``task->pending.signal`` and ``Atomics.notify`` the29target Worker's wake word; the target Worker would observe30``signal_pending(current)`` at every kernel entry/exit and dispatch via31the upstream signal-delivery path. That framing was correct as a32direction but was never load-bearing because K1-K7 had no use for signals33(K5/K6 demos were single-threaded; K7's multi-threaded demo coordinated34with ``pthread_mutex`` — futexes, not signals).35 36K8 is the phase where signals become load-bearing for the first time.37Bash needs ``SIGCHLD`` to reap children, ``SIGINT`` to interrupt running38commands, ``SIGTSTP`` for job-control suspension, ``SIGPIPE`` when a39pipe's reader dies. The K8 architecture must answer:40 41* What is the actual delivery mechanism that survives the "wasm has no42 async interrupt" constraint?43 44* Where does the signal frame live across the45 handler-runs-on-the-thread's-stack contract?46 47* How are signals routed to a specific thread vs the process when the48 process is multi-threaded (Regime 1b)?49 50* How does ``SYS_futex`` interact with signals — i.e., does51 ``Atomics.wait`` parked on a futex word wake when a signal arrives?52 53These are spec-time questions; getting them wrong at code time is54expensive. K8 deepens this document during its implementation phase55(per the K5/K6/K7 amendment pattern).56 57Reading order:58 59* If you are implementing K8-A (signal-delivery infrastructure):60 §2 (delivery model), §3 (HardwareJS API), §4 (signal frame ABI),61 §11 (K8 sub-phase decomposition).62 63* If you are implementing K8-B (per-thread vs per-process routing):64 §5 (thread routing), §6 (sigaction surface), §7 (futex interaction).65 66* If you are auditing the framework: §8 (source-site enumeration),67 §9 (refutation budget), §10 (acceptance criteria).68 69This document expands ``ARCHITECTURE.md §17.5`` — it does NOT refute70prior §17.5 content. The pre-K8 §17.5 framing (cooperative check at71``signal_pending`` boundaries; ``Atomics.notify`` as the wake mechanism)72remains correct as the foundation. K8 fills in the operational details.73 74--------------------------------------------------------------------------------752. The delivery model: cooperative check at kernel-entry/exit76--------------------------------------------------------------------------------77 78**The wasm constraint.** wasm has no asynchronous interrupt mechanism.79Code running inside a Worker cannot be preempted from outside the80Worker, and there is no equivalent to a signal-stack switch or a81hardware-trap-driven interrupt vector. Whatever signal-delivery82mechanism we build MUST be cooperative: the delivering side sets state,83the receiving side observes state at well-defined check points.84 85**The check points.** Per upstream Linux, ``signal_pending(current)``86is checked at:87 88(a) Every kernel entry from userspace (``entry.c`` / arch entry89 handlers).90(b) Every kernel exit to userspace (``exit_to_user_mode_loop``).91(c) Every ``cond_resched_*`` / ``schedule()`` voluntary yield in92 long-running syscalls.93(d) Every interruptible wait (``wait_event_interruptible``,94 ``mutex_lock_interruptible``, ``msleep_interruptible``).95 96On wasm32 those check points are realized as:97 98(a)+(b) The ``linux.syscall`` import on the user-Worker side. Every99 syscall round-trip is a kernel entry/exit pair; signal100 delivery happens on the exit edge.101 102(c)+(d) The kernel's cooperative-yield path103 (``arch/wasm32/kernel/asyncify.c::__wasm32_yield``). Long104 kernel syscalls that yield go through here; the yield can105 observe signal state.106 107**The cross-Worker wake.** When ``kill(pid, sig)`` runs on the kernel108Worker, the kernel sets ``task->pending.signal`` (upstream code) and109the arch hook then must wake the target user Worker if it is parked.110"Parked" on wasm32 means one of:111 112* Blocked in ``Atomics.wait`` on a futex word in ``env.user_memory``113 (the K7-U4e in-Worker SYS_futex dispatch).114 115* Blocked in ``Atomics.wait`` on the kernel-syscall ctrl SAB awaiting a116 response.117 118* Suspended at a host scheduling point (the Worker is runnable but the119 host scheduler hasn't picked it).120 121For the first two, ``Atomics.notify`` on the parked word wakes the122Worker. For the third, no action is needed — the Worker will reach the123next check point on its own.124 125**Critical observation.** SYS_futex in K7-U4e is dispatched126**in-Worker** (per ``threading-model.rst §8.5``). The kernel never sees127the wait. This means ``kill`` cannot synthesize a "wake the Worker"128event by setting kernel state alone — the kernel doesn't know which129futex address the Worker is parked on. K8 must surface this through a130HardwareJS-side signal-delivery path that has visibility into the131user-Worker's parked state.132 133**The K8 contract:**134 1351. ``kill(pid, sig)`` sets ``task->pending.signal`` (upstream code,136 unchanged).1372. ``kill`` calls into the arch hook138 ``arch_signal_wake_target(struct task_struct *t)`` (NEW HOOK139 landing in patches/0008 — see §11.A).1403. The arch hook submits a SIGNAL request through the spawn-ring's141 sibling ring (or a new dedicated signal-ring) to HardwareJS.1424. HardwareJS-side signal handler looks up the target Worker by pid143 in its process registry, then:144 145 * If the Worker is parked in ``Atomics.wait`` on a known signal146 wake word: ``Atomics.notify`` that word, Worker wakes, observes147 ``signal_pending`` via a syscall round-trip, kernel handles148 delivery.149 150 * If the Worker is parked in an in-Worker futex wait151 (Atomics.wait on a user-memory address): HardwareJS does NOT152 know which address. The Worker must voluntarily poll153 ``signal_pending`` between futex waits. Per upstream Linux,154 futex syscall MUST be interruptible (returns ``-ERESTARTSYS`` on155 signal) — this is the K7-U4e in-Worker handler's contract too,156 extended to check a per-Worker signal flag set by HardwareJS157 before re-entering ``Atomics.wait``.158 159The signal-ring + per-Worker signal-pending flag is K8-A's primary160deliverable. The interruptible-futex behavior is K8-A's secondary161deliverable.162 163--------------------------------------------------------------------------------1643. HardwareJS-side signal API165--------------------------------------------------------------------------------166 167The kernel emits SIGNAL events through a new ring or hook. HardwareJS168consumes them and notifies the target Worker.169 170**Kernel-side producer (new):**171 172* ``arch/wasm32/include/asm/signal.h`` — kernel-side mirror of the173 SIGNAL ABI.174* ``arch/wasm32/kernel/signal.c`` — ``arch_signal_wake_target(t)``175 implementation; composes the SIGNAL payload (tid, sig number, sender176 pid for ``si_pid``) and submits.177* New patch ``patches/0008-signal-add-arch_signal_wake_target-hook.patch``178 — upstream weak hook in ``kernel/signal.c::__send_signal_locked``179 invoked after the bit is set in ``t->pending.signal``.180 181**JS-side consumer (new):**182 183* ``hardwarejs/src/signalRingConsumer.ts`` — analogous to184 ``spawnRingConsumer.ts``; polls the kernel-side signal ring, dispatches185 to the handler.186* ``hardwarejs/src/signalHandler.ts`` — looks up the target Worker187 in the per-pid registry, sets the Worker's signal-pending flag in188 ``env.user_memory`` at the per-Worker signal-pending offset, and189 ``Atomics.notify`` on the per-Worker signal wake word.190* ``hardwarejs/src/worker.mjs`` — signal-pending check in the191 ``SYS_futex`` ``FUTEX_WAIT`` path (between ``Atomics.load`` of the192 user word and ``Atomics.wait``); returns ``-EINTR`` if pending.193 194**Per-Worker signal-pending region (new):**195 196Per-Worker state in ``env.user_memory`` at a known offset (analogous197to the per-thread TLS region). The K8 layout:198 199::200 201 +------------------+ <- per-Worker signal-pending region base202 | pending_signals | u64 bitmap of pending signal numbers (sig 1 = bit 0)203 +------------------+204 | wake_word | i32 — incremented + notified when a signal205 | | arrives; Worker Atomics.waits on this when in206 | | the signal-pending check-and-park pattern207 +------------------+208 | sigmask | u64 — current signal mask (sigprocmask-managed)209 +------------------+210 | reserved | 2 * u64 — alignment + K9+ expansion211 +------------------+212 213Total: 40 bytes per Worker. Aligned to 16 bytes. The kernel knows the214offset via the per-task ``thread_struct`` field.215 216**The signal-pending check sequence:**217 218::219 220 /* In hardwarejs/src/worker.mjs SYS_futex handler */221 function sys_futex_wait(uaddr, val, timeout) {222 /* Check signal-pending BEFORE entering Atomics.wait */223 if (Atomics.load(signalView, signalPendingIdx) !== 0) {224 return -EINTR; /* Don't park; let kernel do signal delivery */225 }226 /* The race: signal arrives between check and wait. Resolved by227 * Atomics.notify wake_word also being raced into the wait —228 * see §3 race-resolution below. */229 const result = Atomics.wait(memView, uaddr >>> 2, val, timeout);230 /* Re-check signal-pending after wake; if set, return EINTR. */231 if (Atomics.load(signalView, signalPendingIdx) !== 0) {232 return -EINTR;233 }234 return mapWaitResult(result);235 }236 237**Race-resolution.** Between "Atomics.load signal-pending" and238"Atomics.wait", a signal could arrive. To resolve: HardwareJS sets239``pending_signals``, increments ``wake_word``, and ``Atomics.notify``-s240the wake word. The futex wait uses Atomics.waitAsync-style multi-word241trick: we wait on the wake_word too (or use the existing Atomics.wait242with a short timeout and re-check). The K8-A implementation picks the243cleanest variant.244 245--------------------------------------------------------------------------------2464. Signal frame ABI247--------------------------------------------------------------------------------248 249When a signal handler runs in userspace, it runs on the recipient250thread's stack with a "signal frame" on top. The frame contains:251 252* The saved register state (for return-via-rt_sigreturn).253* The siginfo_t structure (the second arg to SA_SIGINFO handlers).254* The previous signal mask (restored by sigreturn).255* The previous ``current->context_uc``.256 257On real arches, the frame is constructed on the user stack at signal258entry and torn down by ``rt_sigreturn``. On wasm32, "user stack" is259the user Worker's wasm shadow stack (the ``__stack_pointer`` global260plus the C-level call frames it points to).261 262**The K8 challenge.** The signal handler must be invoked at the next263userspace check point (after a syscall returns). The handler's call264frame must be a real C frame that the wasm Worker can return from. We265cannot construct a frame "on top of" the existing wasm call chain266without breaking the wasm engine's call-stack invariants.267 268**The K8 solution.** Signal delivery is a Worker-side cooperative269trampoline:270 2711. After a syscall returns, before the syscall stub returns to its272 caller, the Worker checks ``pending_signals``.273 2742. If pending, the Worker calls a hard-coded trampoline275 ``__wasm_signal_dispatch(sig, siginfo_ptr)`` which is defined in276 musl's arch port (``musl-src/src/internal/arch/wasm32/signal.c``,277 to be created at K8).278 2793. The trampoline does:280 281 * Save the current signal mask to a Worker-local slot.282 * Apply the handler's ``sa_mask`` to the current mask.283 * Call the userspace handler ``sa->sa_handler(sig)`` or284 ``sa->sa_sigaction(sig, info, ctx)`` directly. This is a regular285 C call — the handler is a function pointer in the user binary.286 * On return, restore the previous signal mask.287 * Set a "handled" bit in ``pending_signals`` for the handled signal288 (or clear it entirely).289 2904. The trampoline returns into the syscall stub, which returns the291 syscall result to its caller.292 293**No signal stack alternative at K8.** ``sigaltstack`` allows294specifying a separate stack for signal handlers. K8 returns ``-ENOSYS``295for ``sigaltstack`` (or accepts the call but doesn't honor the296alternate stack — handlers run on the main stack). K9+ may add support297if a needed program (bash, coreutils) requires it.298 299**No SA_RESTART at K8 for syscalls returning -EINTR.** K8 returns300``-EINTR`` cleanly; if the handler doesn't return (siglongjmp), the301syscall is never retried. Programs that need SA_RESTART must302explicitly set it; the K8 trampoline checks the flag and re-invokes303the interrupted syscall if set (siglongjmp also short-circuits this).304K9 may add a cleaner SA_RESTART loop if needed.305 306--------------------------------------------------------------------------------3075. Per-thread vs per-process signal routing308--------------------------------------------------------------------------------309 310POSIX distinguishes:311 312* **Thread-directed signals** (``pthread_kill(tid, sig)``): the signal313 is delivered to that specific thread, regardless of mask state on314 other threads.315 316* **Process-directed signals** (``kill(pid, sig)``, ``tgkill(pid, tid,317 sig)``): the signal is delivered to the process. The kernel picks318 any one thread of the process that doesn't have the signal masked.319 320* **Process-directed unblockable signals** (``SIGSEGV``, ``SIGFPE``,321 etc., when generated synchronously by the kernel for the running322 thread): delivered to the thread that caused them.323 324**K8 routing rules:**325 326(a) ``pthread_kill(tid, sig)`` sets the bit in the target task's327 ``pending.signal``; arch hook wakes that specific Worker.328 329(b) ``kill(pid, sig)`` sets the bit in the thread group leader's330 ``pending.signal`` (or the shared signal queue, depending on331 upstream's choice). The kernel's ``signal_wake_up_state``332 picks a thread to handle it. The arch hook wakes whichever333 Worker that thread is.334 335(c) Synchronous fault-generated signals (SIGSEGV from336 ``__builtin_trap`` in the user Worker) are NOT K8 scope —337 those land at K9+ with the trap-routing infrastructure.338 339For (b), the kernel-side ``__send_signal_locked`` already does the340thread-picking. The arch hook just wakes whichever thread the kernel341picked. The HardwareJS-side handler looks up the Worker by tid342(NOT pid) in the per-thread registry.343 344**Registry shape.** K7-U4c added a per-pid spawn registry in345``hardwarejs/src/kernelSpawnHandler.ts``. K8 adds a per-tid signal346registry (or extends the spawn registry to expose tid lookup). The347mapping is one-to-one between tid and Worker (Regime 1b).348 349--------------------------------------------------------------------------------3506. sigaction surface351--------------------------------------------------------------------------------352 353``rt_sigaction(sig, act, oact, sigsetsize)`` is the userspace primitive354for installing handlers. It's upstream-clean: the kernel manages355``current->sighand->action[]``, the userspace musl calls356``__syscall(SYS_rt_sigaction, ...)``, and the kernel ABI is well-defined.357 358On wasm32 the syscall passes through normally. The kernel-side359``do_sigaction`` updates the per-process ``sighand`` table; no arch360hook needed for the installation path.361 362**The handler-pointer ABI.** ``struct sigaction.sa_handler`` is a363function pointer. On real arches this is a code-segment address. On364wasm32 it's a wasm function index (the indirect-call table index, since365``-fPIC -Wl,--export-dynamic`` makes the user binary's table-elements366addressable by index from C-pointer space).367 368The K8 trampoline (``__wasm_signal_dispatch``) must convert the369``sa_handler`` value into an indirect call. This is the same mechanism370musl already uses internally for function pointers; no arch-specific371work needed beyond the trampoline shape.372 373**Restrictions K8 honors:**374 375* ``SIGKILL`` (9) and ``SIGSTOP`` (19) cannot be caught, blocked, or376 ignored. Upstream enforces this; K8 inherits.377 378* ``sa_handler == SIG_DFL`` (0) means default disposition:379 380 * SIGTERM / SIGINT / SIGHUP / SIGPIPE / SIGUSR1 / SIGUSR2 / SIGALRM /381 SIGCHLD: terminate the process (SIGCHLD = ignore by default).382 * SIGSEGV / SIGBUS / SIGILL / SIGFPE / SIGABRT: terminate with core383 dump (we don't generate core dumps; just terminate).384 * SIGSTOP / SIGTSTP / SIGTTIN / SIGTTOU: stop the process (job385 control — K8 makes this best-effort, K10 makes it real).386 387* ``sa_handler == SIG_IGN`` (1) means ignore. The trampoline checks388 this before calling the handler.389 390**Restrictions K8 does NOT honor (deferred to K9):**391 392* SA_NOCLDSTOP / SA_NOCLDWAIT (SIGCHLD specifics) — K9 waitpid scope.393* SA_RESTORER (real-restorer trampoline customization) — not needed,394 we use our own trampoline.395 396--------------------------------------------------------------------------------3977. Interaction with K7-U4e in-Worker SYS_futex398--------------------------------------------------------------------------------399 400K7-U4e's worker.mjs SYS_futex handler MUST be extended to honor401signal-pending. The current handler:402 403::404 405 /* K7-U4e shape: */406 case "FUTEX_WAIT":407 const result = Atomics.wait(view, uaddr >>> 2, val, timeout_ms);408 return mapWaitResult(result); /* 'ok' → 0, 'timed-out' → -110, etc. */409 410K8 extension:411 412::413 414 case "FUTEX_WAIT":415 /* Pre-check: signal already pending? */416 if (signalPending()) return -EINTR; /* -4 */417 const result = Atomics.wait(view, uaddr >>> 2, val, timeout_ms);418 /* Post-check: did a signal arrive during the wait? */419 if (signalPending()) return -EINTR;420 return mapWaitResult(result);421 422The pre-check is necessary because the signal may have been delivered423between the user's check-and-park (e.g., the user code that calls424``pthread_cond_wait`` checks the predicate first, then unlocks, then425waits — a signal between unlock and wait would otherwise be missed).426 427The post-check is necessary because ``Atomics.notify`` from428HardwareJS-side signal delivery wakes ``Atomics.wait`` with verdict429'ok' (not distinguishable from a regular wake by another thread). The430post-check disambiguates.431 432**Why doesn't the post-check return -EINTR for spurious wakes?** It433does, in the rare race where a regular wake happens434simultaneously with a signal delivery. POSIX permits this — both435``pthread_cond_wait`` and ``futex_wait`` may return early with436spurious wakeups, and the standard pattern is the caller re-checks437the predicate. K8 returns -EINTR rather than 0 in this race, which438causes musl's ``pthread_cond_wait`` to loop and re-check the predicate439— the standard correctness pattern.440 441--------------------------------------------------------------------------------4428. Source-site enumeration443--------------------------------------------------------------------------------444 445Per ``ARCHITECTURE.md §20``, K8 implementers run these greps at K8-A1446(probe phase) before writing implementation code:447 448::449 450 # Upstream signal-delivery sites451 rg 'signal_pending|recalc_sigpending' linux-wasm/linux/kernel/452 rg 'do_signal|get_signal' linux-wasm/linux/kernel/signal.c453 rg '__send_signal_locked|send_signal' linux-wasm/linux/kernel/signal.c454 455 # Upstream syscall-return signal-delivery hooks456 rg 'exit_to_user_mode_loop|TIF_SIGPENDING' linux-wasm/linux/kernel/entry/457 458 # Arch hooks for signal frame construction459 rg 'setup_rt_frame|sigreturn' linux-wasm/linux/arch/x86/kernel/signal.c460 461 # Musl-side signal surface (where user signal handlers live)462 rg 'sigaction|SIGCHLD|pthread_kill' wackywasm-tools/musl-src/src/signal/463 rg 'sigaltstack|sigprocmask|sigreturn' wackywasm-tools/musl-src/src/signal/464 465 # Existing wasm32 arch hooks (what's there, what's missing)466 rg 'arch_.*signal|arch_.*pending' linux-wasm/wasm32-port/467 468These greps land at ``linux-wasm/Documentation/wasm/469signals-model-source-snapshots.txt`` per the K6/K7 pattern (deterministic470re-runnable enumeration).471 472The §15 "wasm32 doesn't dispatch initcalls" row will likely surface473during K8: ``__init`` signal-subsystem registration paths (if any)474won't run unless arch_kernel_init_pre_run_init bridges them. Track475this as a third instance of the row if it surfaces.476 477--------------------------------------------------------------------------------4789. K8 sub-phase decomposition479--------------------------------------------------------------------------------480 481Per ``ARCHITECTURE.md §20.9``, K8 declares its budget and sub-phases482upfront.483 484**K8 sub-phases:**485 486K8-A1487 Probes: T_signal_pending (kernel can read pending bits from a488 task_struct), T_arch_signal_wake (arch hook callable), T_worker_msg489 (Worker can receive a signal-pending notification from HardwareJS).490 Establishes the substrate.491 492K8-A2493 Source-site snapshot. Pinned upstream signal-subsystem sites the K8-B494 patches will modify.495 496K8-B1497 ``arch_signal_wake_target`` hook + upstream patch498 (patches/0008-signal-add-arch_signal_wake_target-hook.patch).499 Per-Worker signal-pending region in env.user_memory.500 Worker-side signal-pending check in SYS_futex handler.501 502K8-B2503 Kernel-side signal-ring producer + HardwareJS-side signalRingConsumer.504 Per-tid registry lookup.505 506K8-B3507 Worker-side ``__wasm_signal_dispatch`` trampoline. Musl arch port508 for wasm32 signal handler invocation.509 510K8-B4511 Default disposition handlers (SIG_DFL behavior per signal type).512 SIGKILL / SIGSTOP non-catchability enforcement.513 514K8-B5515 ``sigprocmask`` / ``sigsuspend`` / ``rt_sigtimedwait`` surface516 (the masking and blocking-wait operations).517 518K8-B6519 User-side syscall bridges. Two small overrides that let POSIX520 programs see the K8-B3 trampoline through the standard521 signal API without belt-and-braces:522 523 - ``src_overrides/internal/wasm32/syscall_ret.c`` —524 ``__syscall_ret`` calls ``__wasm_signal_dispatch_pending``525 before mapping errno. This is the K8 equivalent of upstream526 Linux's ``exit_to_user_mode_loop`` TIF_SIGPENDING check —527 cooperative on wasm32, mandatory on real arches.528 ``raise(2)`` / ``kill(2)`` / ``tkill(2)`` produce handler529 invocations naturally on the syscall return path.530 531 - ``src_overrides/signal/wasm32/sigaction.c`` — overrides532 ``__libc_sigaction`` (the upstream stock implementation533 issues SYS_rt_sigaction only). Wasm32 still issues534 SYS_rt_sigaction so the kernel-side ``current->sighand``535 learns the disposition (used by ``__send_signal_locked``'s536 ``sig_ignored`` check), but ALSO calls537 ``__wasm_signal_register_action`` so the user-side538 trampoline cache learns the function pointer to invoke.539 Without this bridge, the trampoline's540 ``__wasm_sigaction_table`` would stay empty across the541 standard ``sigaction(2)`` install.542 543K8-C1544 End-to-end demo: hello-k8-signals.c — installs SIGUSR1 handler,545 raises SIGUSR1 to self, handler runs, returns, exits 0.546 Multi-threaded variant: pthread_kill from one thread to another,547 handler runs on the target thread.548 549K8-C2550 Regression sweep: K5-C2 (single-threaded), K6-C2 (initramfs init),551 K7-U4f (multi-threaded futex) all still pass.552 553K8-D1554 Audit: every §9 R-row maps to at least one ``hardwarejs/test/k8-*``555 sub-test.556 557K8-D2558 Close ledger + Appendix A flip + K9 surface confirmation.559 560**K8 refutation budget: ≤ 1**561 562Reasoning per §20.9 three-input formula:563 5641. K7 actual: 1 refutation (R-K7-3 CLONE_CHILD_CLEARTID, single §15565 row instance). Trajectory at 0.5 effective (1 ref but recovering566 from K6's cap-hit).567 5682. Surface novelty: K8 introduces signal-ring infrastructure569 (analogous to K5's spawn-ring; pattern is now established) and570 per-Worker signal-pending state (analogous to per-thread TLS;571 pattern is now established). Upstream signal-subsystem surface572 is well-understood and well-tested. Novel surface estimate: low.573 5743. Single-axis discipline returns (K7 was the temporary dual-axis575 relaxation). K8 single-axis = signals. ≤ 1 is honest.576 577Inside the ≤ 1, the natural risk sites are:578 579* The signal-pending race between ``Atomics.load`` check and580 ``Atomics.wait`` in the SYS_futex handler. The K7-U4e in-Worker581 design assumed no signal interaction; K8 needs to verify the582 race-resolution chosen actually works.583 584* The handler trampoline's interaction with the wasm shadow stack —585 whether calling a function pointer through the indirect-call table586 preserves all wasm engine invariants when a signal handler raises587 another signal or longjmps.588 589If K8 closes at 0 actual refutations, the discipline compounds back to590K5-shape. If K8 closes at 2+ refutations, the surface estimate was591optimistic and K9 should re-budget.592 593--------------------------------------------------------------------------------59410. Acceptance criteria (K8 close)595--------------------------------------------------------------------------------596 597K8 closes when:598 599(a) ``hello-k8-signals.c`` (single-threaded SIGUSR1 self-raise) boots600 through K6's path, installs handler, raises signal, handler runs601 once, exits 0.602 603(b) ``hello-k8-signals-mt.c`` (multi-threaded pthread_kill) spawns 2604 threads; thread 0 pthread_kills thread 1 with SIGUSR1; thread 1's605 SIGUSR1 handler runs once, increments a counter; thread 0 joins606 thread 1; counter == 1 asserted in vitest.607 608(c) ``hello-k8-default-term.c`` (default-disposition test) raises609 SIGTERM to self with no handler installed; process exits cleanly610 with status indicating SIGTERM (wait status sig == SIGTERM, K9611 will assert this; K8 asserts the worker exits).612 613(d) SYS_futex with signal pending returns -EINTR cleanly. musl's614 ``pthread_cond_wait`` correctly handles -EINTR by looping.615 616(e) Regression: K5-C2, K6-C2, K7-U4f all pass unchanged.617 618(f) ``make hwjs-verify`` clean: tsc + vitest pass; no new warnings.619 620(g) Cumulative K8 refutation count ≤ 1.621 622(h) Appendix A (below) populated.623 624--------------------------------------------------------------------------------62511. Implementation reading-order626--------------------------------------------------------------------------------627 628K8 implementers read in this order:629 6301. ``ARCHITECTURE.md §17.5`` (existing signal-delivery framing).6312. ``ARCHITECTURE.md §20`` (enumeration discipline).6323. This document, §2 (delivery model) through §7 (futex interaction).6334. ``threading-model.rst §8.5`` (in-Worker SYS_futex dispatch — the634 primary integration point for signal-pending checks).6355. ``threading-model.rst §6.5`` (fn_arg_block ABI — the pattern for636 load-bearing kernel↔userspace ABI declarations; signal-ring637 payload follows the same shape).6386. ``rootfs-model.rst §16.*`` (the K6 close ledger structure — K8639 close ledger should follow the same pattern).640 641--------------------------------------------------------------------------------642Appendix A — K8 close checklist643--------------------------------------------------------------------------------644 645[x] K8-A1 probes pass (T_signal_pending, T_arch_signal_wake, T_worker_msg)646[x] K8-A2 source-site snapshot landed647 (signals-model-source-snapshots.txt)648[x] patches/0008-signal-add-arch_signal_wake_target-hook.patch landed649[x] arch/wasm32/include/asm/signal.h with static_asserts pinning the650 per-Worker signal-pending region layout651[x] arch/wasm32/kernel/signal.c::arch_signal_wake_target implemented652[x] hardwarejs/src/signalRingConsumer.ts (new)653[x] hardwarejs/src/signalHandler.ts (new) with per-tid registry654[x] hardwarejs/src/worker.mjs SYS_futex handler honors signal-pending655[x] musl-src/src_overrides/signal/wasm32/__wasm_signal_dispatch.c656 trampoline implemented657[x] musl-src/src_overrides/signal/wasm32/* arch-specific signal source658 overrides (sigaction.c, pthread_sigmask.c, sigsuspend.c,659 sigtimedwait.c) plus internal/wasm32/syscall_ret.c (K8-B6 user-side660 syscall bridges)661[x] Default-disposition handlers per §6 (K8-B4 __wasm_dfl_table +662 SIGKILL/SIGSTOP non-catchability)663[x] hello-k8-signals.c demo + hardwarejs/test/k8-c1-signals.test.ts664[x] hello-k8-signals-mt.c demo + hardwarejs/test/k8-c1-signals-mt.test.ts665[x] hello-k8-default-term.c demo + test666[x] K5-C2, K6-C2, K7-U4f regressions all green667[x] make hwjs-verify clean668[x] K8 close ledger in §12 (this document, new section)669[x] Refutation budget: actual ≤ 1 (closed at 0/≤1; budget unused)670[x] K9 surface proposal in K8 close ledger (§13)671[x] v0.1.0-rc9 annotated tag placed (K8-D2 tag command)672 673--------------------------------------------------------------------------------67412. K8 close ledger675--------------------------------------------------------------------------------676 677This section is the K8 final close. K8's two close criteria from §10678(plus the (d) -EINTR check, (e) regression sweep, (f) hwjs-verify gate,679(g) refutation budget, (h) Appendix A) are all met:680 681 (a) ``hello-k8-signals.c`` (single-threaded SIGUSR1 self-raise) boots682 through K6's path, installs a handler via standard683 ``sigaction(2)``, ``raise()`` triggers handler, handler runs684 once, returns, exits 0. ✓ Closed at K8-C1(a). Verified by685 ``hardwarejs/test/k8-c1-signals.test.ts`` (K8-C1-R1).686 687 (b) ``hello-k8-signals-mt.c`` (multi-threaded ``pthread_kill``)688 spawns a worker thread, main blocks SIGUSR1 in its own mask,689 worker publishes its tid via stdatomic, main calls690 ``pthread_kill(worker, SIGUSR1)``, worker's ``sigsuspend`` wakes691 via the K8-B5 ``__wasm32_signal_wait`` block on the worker's692 ``wake_word``, handler runs on the worker's wasm stack,693 ``sigsuspend`` returns -EINTR, main joins; ``worker_handler_count694 == 1`` asserted in vitest. ✓ Closed at K8-C1(b). Verified by695 ``hardwarejs/test/k8-c1-signals-mt.test.ts`` (K8-C1-R2).696 697 (c) ``hello-k8-default-term.c`` (default-disposition test) raises698 ``SIGTERM`` to self with no handler installed; the K8-B4699 ``__wasm_dfl_table[15] = DFL_TERM`` entry fires, the default700 terminate hook calls ``SYS_exit_group(128 + 15) = exit_group701 (143)``, the worker exits cleanly with code 143 before ``raise()``702 returns; the post-``raise()`` "after raise" line is unreachable.703 ✓ Closed at K8-C1(c). Verified by704 ``hardwarejs/test/k8-c1-default-term.test.ts`` (K8-C1-R3) which705 asserts the "before raise" line is present, the "after raise"706 line is absent, and ``onUserExit`` observes code 143.707 708 (d) ``SYS_futex`` with signal pending returns -EINTR cleanly. The709 K8-B1 ``handleFutex`` pre-/post-``Atomics.wait`` check observes710 the per-Worker pending bit and returns -EINTR; musl's711 ``pthread_cond_wait`` correctly handles -EINTR by looping, which712 is implicitly verified by the K8-C1(b) test (the worker's713 ``sigsuspend`` is built on the same mechanism — the714 ``__wasm32_signal_wait`` syscall call returns -EINTR after the715 pending bit is stamped). ✓ Closed at K8-B1. Verified by716 ``hardwarejs/test/k8-b1-futex-eintr.test.ts``.717 718 (e) **Regression: K5-C2, K6-C2, K7-U4f all pass unchanged.** ✓719 Closed at K8-C2. Verified by the full hwjs-verify sweep at K8720 close (37 test files, 99 passed + 2 skipped).721 722 (f) ``make hwjs-verify`` clean: tsc + vitest pass; no new warnings. ✓723 Closed at K8 close.724 725 (g) Cumulative K8 refutation count ≤ 1. ✓ Closed at **0** actual726 refutations. K8 closed at the more disciplined edge of its727 ≤ 1 budget.728 729 (h) Appendix A populated. ✓ Below.730 731**K8-D1 audit — acceptance / Appendix A row → test mapping.**732 733 Every Appendix A row is covered by either a sub-test in734 ``hardwarejs/test/k8-*`` or a phase commit:735 736 * §10 (a) → ``k8-c1-signals.test.ts``737 * §10 (b) → ``k8-c1-signals-mt.test.ts``738 * §10 (c) → ``k8-c1-default-term.test.ts``739 * §10 (d) → ``k8-b1-futex-eintr.test.ts``740 (and indirectly via §10(b))741 * §10 (e) / K8-C2 → full hwjs-verify sweep at close742 * §10 (f) → ``make hwjs-verify`` clean (tsc + vitest)743 * §10 (g) → cumulative ledger below744 * §10 (h) → this section745 746 Appendix A items map to:747 748 * K8-A1 probes → ``k8-a1-signals-substrate.test.ts``749 (T_signal_pending, T_arch_signal_wake,750 T_worker_msg)751 * K8-A2 snapshot → K8-A2 commit752 (signals-model-source-snapshots.txt)753 * patches/0008 → K8-B1 commit754 * arch/wasm32/asm/ → K8-B1 commit755 signal.h (region layout static_asserts)756 * arch_signal_wake_ → K8-B1 commit + ``k8-b1-futex-eintr``757 target test (kernel→Worker stamp path)758 * signalRingConsumer → ``k8-b2-signal-ring.test.ts``759 * signalHandler.ts → ``k8-b2-signal-ring.test.ts``760 (per-tid registry) +761 ``k8-c1-signals-mt.test.ts``762 (deliverSignalToTarget shared with763 JS-direct K8-B6 path)764 * worker.mjs futex → ``k8-b1-futex-eintr.test.ts``765 * trampoline → ``k8-b3-trampoline.test.ts``766 * src_overrides/ → ``k8-b3-trampoline.test.ts``767 signal/wasm32/ (trampoline source override) +768 ``k8-c1-signals*.test.ts`` (sigaction769 + sigsuspend/sigtimedwait covered by770 mt + default-term demos)771 * default-disposition → ``k8-b4-default-action.test.ts``772 handlers (§6) (T1..T8 covering SIGKILL/SIGSTOP773 reject, DFL_TERM, DFL_IGN, DFL_CORE,774 DFL_STOP)775 * hello-k8-signals → ``k8-c1-signals.test.ts``776 * hello-k8-signals-mt → ``k8-c1-signals-mt.test.ts``777 * hello-k8-default-term→ ``k8-c1-default-term.test.ts``778 * K5/K6/K7 regressions → existing K5-C2/K6-C2/K7-U4f tests779 re-run as part of the full sweep780 * make hwjs-verify → CI gate781 * §12 close ledger → this section782 * Refutation budget → cumulative ledger below783 * K9 surface proposal → §13 below784 * v0.1.0-rc9 tag → K8-D2 tag command785 786 K8-B5 has its own probe under ``k8-b5-mask.test.ts`` (the787 pthread_sigmask / sigsuspend / sigtimedwait probe). K8-B6 has788 no dedicated probe — it's a transparent layer under the789 K8-C1 demos, which exercise it three different ways:790 791 * (a) ``sigaction(SIGUSR1, &sa, NULL)`` install →792 ``__libc_sigaction`` override populates the user-side793 table → ``raise(SIGUSR1)`` syscall returns →794 ``__syscall_ret`` auto-dispatch → trampoline finds the795 cached handler.796 797 * (b) ``pthread_sigmask`` (K8-B5) and ``sigsuspend`` (K8-B5)798 both rely on the K8-B6 ``__syscall_ret`` hook for799 synchronous post-mask delivery; K8-C1(b)'s success800 validates that integration.801 802 * (c) Zero-init ``__wasm_sigaction_table`` (no override803 installed) → trampoline falls through to804 ``__wasm_dfl_table[15]`` → terminate hook → exit_group(143).805 Validates that the K8-B6 ``sigaction`` override DOESN'T806 run on a process that never calls ``sigaction``, but the807 ``__syscall_ret`` hook DOES.808 809 No R-row is unrecited; no §20.6 discipline violations810 outstanding.811 812**K8 cumulative refutation ledger.**813 814 ::815 816 K8 opener 0 §9 budget set to ≤1817 K8-A1 0818 K8-A2 0819 K8-B1 0820 K8-B2 0821 K8-B3 0822 K8-B4 0823 K8-B5 0 (new sub-phase added during824 implementation; not a refutation —825 spec-shape extension to cover826 sigprocmask/sigsuspend/sigtimedwait827 which §6 had folded into K8-B3 and828 K8-B4 implicitly)829 K8-B6 0 (new sub-phase added during830 implementation; not a refutation —831 spec-shape extension for the user-side832 syscall bridges that fall out of833 K8-B3/B4/B5 once the trampoline +834 default-action + mask ops all exist835 but the standard POSIX install path836 (sigaction(2)) hadn't been wired to837 feed them; documented in §11 K8-B6)838 K8-C1 0839 K8-C2 0840 -------- -841 Cumulative 0 Within ≤1 budget; budget unused842 843 Per the K8 opener's threshold logic:844 845 > **If K8 closes at 0 actual refutations**: the discipline846 > compounds back to K5-shape.847 848 K8 closing at **0** validates the single-axis discipline849 return after K7's dual-axis relaxation. Two new sub-phases850 (B5, B6) were added but neither was a refutation: B5 was a851 surface that §6 had folded into B3/B4 implicitly (the spec852 named it but didn't decompose it as a sub-phase), and B6853 fell out of B3+B4+B5 once all three existed but the854 standard ``sigaction(2)`` POSIX install path hadn't been855 wired to feed them. Both are spec-shape additions, not856 spec-shape refutations.857 858**§15 row scoreboard at K8 close.**859 860 K8 introduces two new §15 rows (both K8 shortcuts, both861 K9+ retirement targets):862 863 * **R-K8-1** (K8 SYS_rt_sigaction stub: kernel-side864 ``current->sighand`` not populated; ``sig_ignored`` always865 false at K8) — OPEN. The K8-B6 ``sigaction`` override866 issues ``SYS_rt_sigaction`` so musl is satisfied, but867 ``dispatchSyscall``'s ``__NR_rt_sigaction`` handler868 returns 0 success without copying the disposition into869 a kernel-side table. Net effect at K8: the user-side870 ``__wasm_sigaction_table`` is the source of truth for871 handler dispatch, and signal delivery (also K8 shortcut;872 next row) bypasses kernel-side ``sig_ignored`` entirely.873 Retired by K9+ when the vmlinux RPC for cross-process874 syscalls lands and the kernel's signal-delivery875 decisions actually consult ``current->sighand``.876 877 * **R-K8-2** (K8 signal delivery via JS-direct stamp —878 ``SYS_kill`` / ``SYS_tkill`` / ``SYS_tgkill`` resolve879 target tid in ``dispatchSyscall`` and call880 ``deliverSignalToTarget`` directly, bypassing the881 kernel-side delivery path) — OPEN. The full kernel-side882 delivery — vmlinux's ``__send_signal_locked`` →883 ``arch_signal_wake_target`` (K8-B1) → kernel→HardwareJS884 signal ring (K8-B2) → consumer → ``deliverSignalToTarget``885 — exists and passes its own probe (``k8-b2-signal-ring``),886 but cross-process syscalls aren't yet routed through887 ``vmlinux.exports.wasm_syscall``. The JS-direct path888 short-circuits the kernel for ``kill``/``tkill``/889 ``tgkill`` until that wiring lands. Retired by K9+ when890 cross-process syscall RPC lands.891 892 Existing K6/K7 rows status at K8 close:893 894 * **R-K6-1** (initcall dispatch) — OPEN. K10 candidate.895 * **R-K6-2** (ramdisk_execute_command clobber) — OPEN,896 subsumed by R-K6-1.897 * **R-K7-1** (softfloat imports) — OPEN. K9+ decision.898 899 Net at K8 close: 5 open rows (R-K6-1, R-K6-2, R-K7-1,900 R-K8-1, R-K8-2), 3 retired/paid. K8 didn't pay any prior901 debt, and added two of its own — but both are tracked902 with explicit retirement signals (K9+ vmlinux RPC) and903 pinned at the call sites with ``§15`` comments.904 905**K8 acceptance achieved.** First end-to-end POSIX signal delivery906through the wasm32 port: ``sigaction(2)``, ``raise(3)``,907``pthread_kill(3)``, ``pthread_sigmask(3)``, ``sigsuspend(2)``,908``sigtimedwait(2)``, default-disposition table including SIGTERM909exit_group(143), SIGKILL/SIGSTOP non-catchability — all on top of910K7's threading and K6's fs-init path. K9 = process management can911now land on top of this exact path.912 913--------------------------------------------------------------------------------91413. K9 surface proposal915--------------------------------------------------------------------------------916 917Per the user's pre-pinned K-phase ledger918(``ARCHITECTURE.md §14``), **K9 = process management**:919 920 ``wait4`` / ``waitpid``, pipes, stdin wiring, process groups.921 922Rationale for the bundle:923 924 1. **wait4 + SIGCHLD** is a signals + processes coupled925 surface. K8 lit up SIGCHLD as a default-IGN signal; K9926 wires the kernel-side reaping side of the contract so927 a parent's ``waitpid()`` actually finds zombie children928 and a child exit triggers SIGCHLD to the parent.929 930 2. **pipes** are needed for any meaningful shell pipeline.931 The K10 terminal demo presupposes ``ls | cat`` works.932 933 3. **stdin wiring** is the read-side of the K6/K7 stdout934 wiring that already exists. K9-shaped because shell935 reads from stdin; without it K10's interactive shell936 can't read user input.937 938 4. **process groups + tcsetpgrp** are needed for K10's939 job control (``Ctrl-C`` to foreground process group).940 941K9 budget proposal: **≤ 2 refutations** (per the user's K-phase942brief — K9 ≤ 2 declared upfront). K9 introduces three coupled943axes (wait/pipes/stdin/pgrps form one bundle, but the bundle944has structural surface area), so the budget is one larger than945K8's. If K9 closes at 0-1, the trajectory holds; if K9 closes946at 2, the bundle was correctly sized; if K9 closes at 3+, the947"shell needs this" bundle was too tightly coupled and K10948should re-budget.949 950K9 retires no §15 rows directly — it might naturally retire951**R-K8-2** (signal delivery via JS-direct stamp) if the wait4952implementation requires routing cross-process syscalls through953vmlinux RPC, but that's a K9 implementation choice rather than954a hard dependency.955 956--------------------------------------------------------------------------------957History958--------------------------------------------------------------------------------959 960K8 OPENER (rc8 → rc9 work begins)961 Document created. Framework sections (§1–§11) and Appendix A962 defined. Implementation sequence pinned. Budget ≤ 1 declared.963 964K8 CLOSE (this commit)965 §12 close ledger added (acceptance + audit + cumulative966 refutation ledger + §15 row scoreboard). §13 K9 surface967 proposal pinned. K8-B5 / K8-B6 sub-phase additions absorbed968 (both spec-shape extensions, neither a refutation). Final969 cumulative refutation count: 0/≤1. Two new §15 rows added970 (R-K8-1, R-K8-2; both K9+ retirement). v0.1.0-rc9 annotated971 tag placed.972