brintos

brintos / linux-shallow public Read only

0
0
Text · 42.3 KiB · c8e03c5 Raw
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