================================================================================
Process-management model for the wasm32 port
================================================================================

This document is the spec-first companion to ``threading-model.rst``,
``signals-model.rst``, and ``rootfs-model.rst``, focused on the
**shell-control subset of POSIX process management**: ``wait4`` /
``waitpid`` child reaping, ``pipe`` / ``dup2`` for inter-process I/O,
``stdin`` wiring through a host-side channel, and process-group
primitives (``setpgrp`` / ``getpgrp`` / ``setsid``) used by job control.

This is the K9 opener — the phase that lights up the syscalls a shell
fundamentally needs in order to fork, exec children, wait on them, pipe
their output between commands, and read user input. K9 does NOT include
the terminal/pty line-discipline layer; that is K10. K9 IS the bridge
between "we have multi-process and multi-threaded userspace" (K7) and
"we can run an actual interactive shell" (K10 + D1).

Status: K9 OPENER. Implementation sequence captured in §13.

--------------------------------------------------------------------------------
1. Why this document exists separately
--------------------------------------------------------------------------------

K7 closed with a fully-functional Regime 1a (fork) + Regime 1b
(pthread) execution substrate. ``fork`` works (WASM_SPAWN_FORK).
``execve`` works (binfmt_wasm + spawn-ring). pthreads work
(WASM_SPAWN_THREAD + in-Worker futex).

What still does NOT work:

* A parent process cannot reap a child. ``wait4`` / ``waitpid`` are
  unwired on the kernel side; child exit notifications travel through
  HardwareJS's ``onUserExit`` callback but never reach a parent
  ``task_struct``.

* Two processes cannot communicate via a pipe. ``pipe(2)`` is
  upstream-clean but the wasm32 fd table doesn't honor pipe semantics;
  any ``write`` on a pipe goes to dev/null.

* ``stdin`` returns 0 (EOF) on every read. The user-Worker syscall
  handler doesn't have any wire-up to host-side keyboard input.

* Process groups don't exist. ``setpgrp(0,0)``, ``getpgrp()``,
  ``setsid()`` syscalls succeed but the underlying upstream state is
  inert; ``kill(-pgid, sig)`` doesn't reach the right set of processes.

K9 is the phase that fixes all of the above. They are bundled because
they form one coherent capability: "a parent process can fork a child,
pipe its output to another child, wait for both to exit, and read user
input meanwhile." That is the minimum capability a shell needs from the
kernel.

Reading order:

* If you are implementing K9-A (waitpid + child reaping):
  §2 (waitpid contract), §3 (exit-notification routing), §13 (K9
  sub-phase decomposition).

* If you are implementing K9-B (pipes):
  §4 (pipe contract), §5 (pipefs interaction), §13.

* If you are implementing K9-C (stdin):
  §6 (stdin wiring), §7 (host-side keyboard channel), §13.

* If you are implementing K9-D (process groups):
  §8 (process-group surface), §13.

This document expands ``ARCHITECTURE.md §5`` (``fork``) and §6
(``execve``) by filling in the parent-side observability story that
those sections deliberately left to "later K-phases."

--------------------------------------------------------------------------------
2. wait4 / waitpid contract
--------------------------------------------------------------------------------

POSIX ``wait4(pid, &status, options, rusage)`` semantics:

* ``pid > 0``: wait for the specific child pid.
* ``pid == 0``: wait for any child in the caller's process group.
* ``pid == -1``: wait for any child.
* ``pid < -1``: wait for any child in process group ``-pid``.

Returns the pid that was reaped, or:

* ``0`` if WNOHANG and no child is ready.
* ``-ECHILD`` if no children exist.
* ``-EINTR`` if interrupted by a signal (interacts with K8's signal
  delivery).

``status`` is filled in with the child's exit info per
``WIFEXITED`` / ``WEXITSTATUS`` / ``WIFSIGNALED`` / ``WTERMSIG``
conventions.

**The K9 implementation strategy.** ``wait4`` is upstream-clean on the
syscall surface; ``kernel/exit.c::do_wait`` is the canonical
implementation. It walks ``current->children`` looking for an
appropriate child task_struct in EXIT_ZOMBIE state, copies out the
status, and reaps (release_task) the zombie.

K9 needs three pieces:

1. **Children list maintenance.** When a parent forks (WASM_SPAWN_FORK
   path), upstream's ``copy_process`` correctly inserts the child into
   ``current->children``. Verified — this works at K7. K9 does NOT
   add work here.

2. **EXIT_ZOMBIE transition.** When a child exits via SYS_exit_group,
   the child's task_struct must transition through EXIT_ZOMBIE so the
   parent can find it. Upstream's ``do_exit`` does this — BUT on
   wasm32 today, the user Worker calls SYS_exit_group, HardwareJS sees
   it via the ``exit`` message, the user Worker terminates, and the
   kernel-side task_struct is left dangling (because there's no kthread
   running on the kernel Worker that knows the user Worker is gone).

3. **Reaping notification.** When the parent is blocked in wait4, it
   needs to be woken when the child exits. Upstream uses
   ``wake_up_interruptible_sync_poll(&parent->signal->wait_chldexit, ...)``.

The K9 plan (HISTORICAL — see HISTORICAL NOTE below for the
post-R-K9-1 refutation; the K9-B1 implementation took the
JS-direct path instead):

(A) Add a new kernel-side bridge ``arch_notify_child_exit(tid, code)``
    that HardwareJS calls when a user Worker exits. This bridge:

    * Looks up the child task_struct by tid.
    * Calls ``do_exit(code)`` on it (or equivalent — see §3).
    * Wakes parent if blocked.

(B) Wire HardwareJS-side ``onUserExit`` to ``arch_notify_child_exit``
    via a new ring (or extend the spawn-ring with an EXIT message
    shape).

(C) Verify wait4 in kernel/exit.c works once (A) and (B) are in place
    — likely no kernel-side changes needed beyond the bridge.

HISTORICAL NOTE (R-K9-1): the kernel-side approach above was
refuted at K9-B1 implementation time. The full plan would have
required either (a) calling upstream ``do_exit`` on a non-current
task (unsafe — upstream's do_exit assumes ``current == tsk`` in
multiple places), or (b) reproducing the relevant subset of
do_exit (exit_notify, EXIT_ZOMBIE transition, do_notify_parent,
wake_up_interruptible on parent's wait_chldexit) in arch code as
a wasm32-specific ``wasm32_reap_exited_task(t, code)`` — which
would be substantial kernel-side machinery. Either path is
internally consistent with upstream semantics but adds load-
bearing kernel C code to a sub-phase whose acceptance criterion
(§10 (a): "wait4 returns child's pid and exit code") can be met
without it.

The K8-B6 / R-K8-2 pattern (JS-direct cross-process syscall
delivery via ``dispatchSyscall``, tracked as a §15 row with K9+
or K10+ retirement when cross-process syscall RPC lands) is the
established precedent. K9-B1 follows the same pattern: a
JS-direct child-exit registry inside ``hardwarejs/src/
childExitRegistry.ts``, with ``__NR_wait4`` resolved entirely in
``dispatchSyscall``. The kernel-side ``current->children`` list
is preserved (upstream's ``copy_process`` already maintains it
correctly through fork at K7) but is not consulted at K9.

This is one §15 row added at K9 close (R-K9-1: "K9 wait4 via
JS-direct registry; kernel-side do_wait/EXIT_ZOMBIE not engaged").
Retirement signal: K10+ when cross-process syscall RPC lands and
the kernel-Worker becomes the wait4 dispatcher. The same RPC
infrastructure that retires R-K8-1 + R-K8-2 will retire R-K9-1.

--------------------------------------------------------------------------------
3. Exit-notification routing (kernel-side)
--------------------------------------------------------------------------------

The K9 child-exit flow:

::

  1. User Worker calls SYS_exit_group(code) via linux.syscall.
  2. Kernel-side syscall handler (in worker.mjs) intercepts BEFORE
     posting to kernel — same shape as SYS_futex in K7-U4e.
     Posts {kind: "exit", pid, code} message to main thread.
  3. HardwareJS-side spawn-handler receives the exit message,
     terminates the Worker, and invokes the new K9 hook
     `arch_notify_child_exit` via the exit-notification ring.
  4. Kernel-side `arch_notify_child_exit(tid, code)`:
     a. Find child task_struct by tid.
     b. Set child->exit_code = code, transition to EXIT_ZOMBIE.
     c. Walk to parent; call wake_up_interruptible on parent's
        wait_chldexit if parent is blocked.
     d. Auto-reap if parent has SA_NOCLDWAIT or has set SIGCHLD =
        SIG_IGN (upstream behavior).
  5. Parent wait4 returns with the child's pid + status.

**Critical correctness invariant.** The child task_struct MUST stay
alive (not be freed) from "user Worker exits" to "parent reaps."
Upstream's reference count handles this — ``do_exit`` sets EXIT_ZOMBIE
but doesn't release the task_struct until release_task is called from
the parent's wait path (or from auto-reap).

On wasm32 we must preserve this invariant: the user Worker terminating
does NOT free the kernel-side task_struct. The kernel-side
``arch_notify_child_exit`` hook is what triggers the EXIT_ZOMBIE
transition; until then, the task_struct stays in TASK_RUNNING /
schedulable state but its user Worker is gone.

**Edge case: parent died before child.** Upstream's reparenting moves
the orphaned child to ``init`` (pid 1). On wasm32 we honor this via
upstream's ``forget_original_parent`` — the kernel-side code does the
right thing; K9 just ensures the bridge correctly identifies the
parent task_struct.

HISTORICAL NOTE (R-K9-1 cont.): the same JS-direct pivot also
governs §4 (pipes) and §11 (cross-process env.user_memory)
below. K9-B2 was originally specced as "kernel-side cross-
process copy_to_user_for / copy_from_user_for via tid-keyed
named-memory" with kernel C helpers consulting a per-tid
arch table. The K9-B1 implementation refuted that route in
favor of a HardwareJS-side ``pipeRegistry`` + ``fdTableRegistry``
pair: pipe ring buffers live in JavaScript, cross-process
data movement is just a JS Uint8Array write between the
producer's and consumer's per-Worker ``env.user_memory``
SABs (both already accessible to HardwareJS via the K5-C2
spawn registry). No new kernel C code, no named-memory
registration ordering surprises, no R-K8-2-shaped retirement
dependency. The K9-B3 sub-phase below ships the JS-direct
pipe + fd-table machinery; the original §4 / §11 plan
remains documented as the K10+ retirement target.

The K9 cumulative refutation count after this fold-in:
**1 / ≤ 2** (R-K9-1 covers all four sub-systems —
wait4/pipe/stdin/cross-process-mem — that originally had
kernel-side machinery in the opener; the §15 ledger row
absorbs the pipe + fd-table extension). R-K9-2 below tracks
the new §15 row added at K9-B1 (``__NR_wasm_spawn_image``
extension syscall) which is a §15 row but not a refutation.

--------------------------------------------------------------------------------
4. pipe / pipefs contract
--------------------------------------------------------------------------------

POSIX ``pipe(int pipefd[2])`` creates a pipe pair. ``pipefd[0]`` is the
read end; ``pipefd[1]`` is the write end. Both are regular fds usable
with ``read`` / ``write`` / ``close`` / ``dup2``.

Upstream's ``fs/pipe.c`` is full-featured: lock-protected ring buffer
in kernel memory, blocking reads/writes with proper EPIPE handling,
splice/tee support. On wasm32 we **inherit pipefs unchanged**. The
question is whether kernel-Worker and user-Worker can interact with
pipefs correctly given the cross-Worker memory boundary.

**The wasm32 pipe shape:**

* The pipe's ring buffer lives in env.kernel_memory (allocated via
  kmalloc — upstream behavior).

* User-Worker A calls ``write(pipe_fd, buf, len)``. The syscall
  routes through worker.mjs → kernel-side syscall handler →
  ``fs_write`` → ``pipe_write``. The ``buf`` argument is a user-space
  pointer in user-Worker A's env.user_memory. The kernel-side
  ``copy_from_user`` must read from A's env.user_memory.

* Per ``threading-model.rst §3``, the kernel can read a user's
  env.user_memory by tid. The pipe write copies bytes from A's
  env.user_memory into the pipe ring buffer (kernel memory).

* User-Worker B calls ``read(pipe_fd, buf, len)``. The kernel-side
  ``pipe_read`` copies bytes from the kernel-memory ring buffer into
  B's env.user_memory.

This works because the kernel has access to BOTH process's
env.user_memory SABs via the per-tid Worker registry. The
``copy_to_user`` / ``copy_from_user`` helpers must dispatch on the
target tid.

**What K9 needs to add:**

* ``copy_to_user_for(tid, dst, src, len)`` / equivalent — kernel
  helpers that copy to/from a specific user's env.user_memory, not
  just ``current``'s. Upstream's ``copy_to_user`` is hard-coded to
  ``current``'s memory; the wasm32 implementation already handles
  this for thread mode (Regime 1b shares memory), but cross-process
  pipes are the first case where ``current`` != target.

* HardwareJS must expose every user-Worker's env.user_memory SAB to
  the kernel Worker. The K5 spawn registry already keeps these; K9
  exposes them by tid for the cross-process copy path.

* The pipe's blocking semantics (blocking read on empty pipe) must
  honor signal-pending (K8 dependency). When a parent blocks in
  ``read`` on a pipe and a signal arrives, return ``-EINTR``.

--------------------------------------------------------------------------------
5. fd table interaction with execve
--------------------------------------------------------------------------------

When a process forks then execs, fd inheritance rules apply:

* By default, fds survive fork() (the child inherits a copy of the
  parent's fd table).
* By default, fds survive execve() (the new program inherits the
  same fd table).
* The ``FD_CLOEXEC`` flag (set via ``fcntl(fd, F_SETFD, FD_CLOEXEC)``
  or implicitly via ``O_CLOEXEC`` at open time) makes the fd close on
  execve.

Upstream ``copy_files`` + ``do_close_on_exec`` handle this. On wasm32:

* ``fork()`` already inherits the fd table via upstream's
  ``copy_files`` (verified at K7? — K9-A1 probe will confirm).

* ``execve()`` clears CLOEXEC fds via ``do_close_on_exec`` — but K9
  needs to verify this fires correctly in binfmt_wasm's load path.

* ``dup2(oldfd, newfd)`` is upstream-clean; should work without arch
  changes.

**The shell-pattern test.** A shell pipeline ``cmd1 | cmd2`` works by:

1. Shell calls ``pipe(p)`` — gets ``[r, w]``.
2. Shell forks child1. In child1, ``dup2(w, STDOUT_FILENO)``,
   ``close(r)``, ``close(w)``, exec cmd1.
3. Shell forks child2. In child2, ``dup2(r, STDIN_FILENO)``,
   ``close(r)``, ``close(w)``, exec cmd2.
4. Shell closes r and w in its own fd table.
5. Shell waits for both children.

K9-B1 verifies this exact pattern works end-to-end. The test is the
shell-pipeline-probe demo.

--------------------------------------------------------------------------------
6. stdin wiring
--------------------------------------------------------------------------------

The user Worker's stdin (fd 0) currently returns 0 (EOF) on every
read. K9 wires it to a host-side input channel.

**The wiring shape:**

* HardwareJS owns the "host-side stdin" — for the demo, this is xterm.js
  keystrokes captured by ``term.onData(...)``. The bytes are pushed
  into a per-Worker stdin queue (a SAB ring buffer, similar to the
  spawn-ring shape, but the producer is HardwareJS and the consumer is
  the user Worker).

* The user Worker's read(0, buf, len) syscall:
  
  * If the stdin queue has bytes, copy them to buf, return the count.
  * If the stdin queue is empty:
    - If O_NONBLOCK: return -EAGAIN.
    - If blocking (default): Atomics.wait on the queue's read_notify
      word until bytes available or signal arrives.

* HardwareJS-side xterm onData handler:
  - Decode the keystroke to bytes (UTF-8).
  - Append to the per-Worker stdin queue's ring buffer.
  - Atomics.notify the read_notify word.

* Echo (the typed character appearing in the terminal): K9-C1 keeps
  this as a HardwareJS-side toggle (echo locally before queueing).
  Real echo control via tcsetattr's ECHO bit is K10 scope.

**The per-Worker stdin region:**

::

  +------------------+   <- per-Worker stdin region base
  | head             |   u32 — write index (HardwareJS writes, Worker reads)
  +------------------+
  | tail             |   u32 — read index (Worker writes, HardwareJS reads
  |                  |          to know how full the queue is)
  +------------------+
  | read_notify      |   i32 — bumped + notified when HardwareJS writes
  +------------------+
  | flags            |   u32 — line-buffering, echo, etc. (K10 will
  |                  |          formalize)
  +------------------+
  | buf[4096]        |   ring buffer (4KB; K9 default; K10 may tune)
  +------------------+

Total: 4112 bytes per Worker. The kernel knows the offset via the
per-task thread_struct field.

**Multi-Worker stdin.** Only one Worker (the foreground process,
typically pid 1's shell child) reads stdin meaningfully. Background
processes' stdin syscalls behave per process-group / controlling-tty
rules — which is K10 (terminal/pty layer). K9 implements the
mechanism; K10 layers the policy on top.

--------------------------------------------------------------------------------
7. Host-side input channel
--------------------------------------------------------------------------------

The host-side input source is the responsibility of the HardwareJS
embedder (LaunchMachine.svelte for the prestrike-web demo). HardwareJS
exposes:

::

  // hardwarejs/src/index.ts:
  export interface UserStdinChannel {
    write(tid: number, bytes: Uint8Array): void;
    flush?(tid: number): Promise<void>;  // K10-extension hook
  }

  // Provided by bootKernel options:
  interface BootKernelOptions {
    // ... existing fields ...
    stdinChannel?: UserStdinChannel;
  }

The embedder (LaunchMachine) wires xterm.js to this:

::

  // LaunchMachine.svelte (sketch — concrete in K9-C1):
  const stdinChannel = makeStdinChannel(...);
  
  term.onData((data) => {
    const fg_tid = currentForegroundTid();  // K9 keeps simple: pid 1
    stdinChannel.write(fg_tid, new TextEncoder().encode(data));
  });

K10 will refine ``currentForegroundTid`` based on controlling-tty and
job-control semantics. K9 hardcodes it to "the latest exec'd child of
pid 1" or similar simple policy.

--------------------------------------------------------------------------------
8. Process groups
--------------------------------------------------------------------------------

POSIX process-group surface:

* ``setpgid(pid, pgid)`` — set process group of pid.
* ``getpgid(pid)`` — get process group of pid.
* ``setpgrp()`` — equivalent to setpgid(0, 0).
* ``getpgrp()`` — equivalent to getpgid(0).
* ``setsid()`` — create a new session and process group, become group
  leader and session leader.

Upstream ``kernel/sys.c`` implements these; on wasm32 they should
mostly Just Work because they manipulate kernel-side
``task->signal->pgrp`` / ``task->signal->session`` only. No arch
hooks needed.

**The K9 verification work:**

* Smoke test: ``setpgid(0, 0)`` returns 0 and ``getpgrp()`` returns
  the calling pid.

* Process-group signal delivery: ``kill(-pgid, SIGTERM)`` sends
  SIGTERM to every process in pgid. Implementation is upstream's
  ``kill_pgrp`` walking the pgrp's task list, calling ``__send_signal``
  for each → K8's ``arch_signal_wake_target`` fires for each.

K9 does NOT implement:

* The controlling-tty assignment (TIOCSCTTY, etc.) — K10 scope.

* Background process-group SIGTTIN/SIGTTOU semantics — K10 scope.

* Session-leader-dies-tells-pgrp-foreground SIGHUP — K10 scope.

These are job-control specifics; K9 only implements the mechanism
needed for "shell can call setpgid for the children it forks."

--------------------------------------------------------------------------------
9. Interaction with K8 signals
--------------------------------------------------------------------------------

K9 has hard dependencies on K8:

* ``wait4`` blocks on ``parent->signal->wait_chldexit``; signal-pending
  must wake it via K8's ``arch_signal_wake_target`` mechanism.

* ``pipe`` blocking read/write must return ``-EINTR`` on signal-pending
  via the K8 SYS_futex extension (since pipe blocking uses
  ``mutex_lock_interruptible`` and ``wait_event_interruptible``, both
  of which honor signal_pending).

* ``read(0, ...)`` on empty stdin blocks in ``Atomics.wait``; honors
  signal-pending the same way K8's futex handler does.

* ``SIGCHLD`` delivery on child exit: when ``arch_notify_child_exit``
  transitions the child to EXIT_ZOMBIE, upstream's exit path also
  sends SIGCHLD to the parent. K8's signal infrastructure delivers it.

K9 sub-phases must verify each K8 dependency at the dependency's
landing point. If a K9 sub-test surfaces a K8-side bug, the appropriate
classification is "K8 refutation that K9 caught" — see §20.9 on
cross-phase refutation accounting.

--------------------------------------------------------------------------------
10. Source-site enumeration
--------------------------------------------------------------------------------

Per ``ARCHITECTURE.md §20``, K9 implementers run these greps at K9-A1
(probe phase):

::

  # waitpid / child-reaping sites
  rg 'do_wait|wait_consider_task|EXIT_ZOMBIE|release_task' \
     linux-wasm/linux/kernel/exit.c
  rg 'sys_wait4|sys_waitid|kernel_waitpid' linux-wasm/linux/kernel/

  # pipe / pipefs
  rg 'sys_pipe|pipe_read|pipe_write|alloc_pipe_info' \
     linux-wasm/linux/fs/pipe.c
  rg 'pipefs_init|pipe_inode_info' linux-wasm/linux/fs/pipe.c

  # fd table / dup
  rg 'sys_dup|do_dup2|copy_files|close_files|exit_files' \
     linux-wasm/linux/fs/file.c
  rg 'do_close_on_exec|FD_CLOEXEC' linux-wasm/linux/fs/

  # stdin / tty (NOT terminal-layer, just the fd 0 reading surface)
  rg 'sys_read.*fd.*0|stdin' linux-wasm/linux/fs/read_write.c

  # Process groups
  rg 'sys_setpgid|sys_setsid|sys_getpgid|sys_getpgrp' \
     linux-wasm/linux/kernel/sys.c
  rg 'task_pgrp|task_session|same_thread_group' linux-wasm/linux/include/

  # User-memory cross-process access
  rg 'access_ok|copy_to_user|copy_from_user|access_process_vm' \
     linux-wasm/linux/

These greps land at ``linux-wasm/Documentation/wasm/
process-model-source-snapshots.txt`` per the K6/K7 pattern.

--------------------------------------------------------------------------------
11. The cross-process env.user_memory contract
--------------------------------------------------------------------------------

K9 introduces the first case where the kernel needs to read/write a
user's env.user_memory that is NOT ``current``. The pipe read/write
path is the primary example.

**The contract:**

* HardwareJS maintains the per-tid Worker registry (K5-C2 / K7-U4c).
  This registry contains, for each user Worker, its env.user_memory
  SAB.

* The kernel side exposes this registry to kernel C code via a
  arch-local table: ``wasm32_user_memory_table[tid] = SAB_byte_offset``
  where ``SAB_byte_offset`` is the offset within env.kernel_memory at
  which a "user memory descriptor" sits. The descriptor includes
  enough state for the kernel to issue ``memory.copy`` ops between the
  destination SAB and env.kernel_memory.

* The kernel-side ``copy_to_user_for(tid, ...)`` /
  ``copy_from_user_for(tid, ...)`` helpers consult this table.

**Implementation strategy.** The kernel cannot directly hold a
JavaScript SAB reference. Instead, HardwareJS provides a per-tid
buffer-aliased view in env.kernel_memory: each user Worker's
env.user_memory SAB is also imported into the kernel Worker as a
named memory (``env.user_memory_<tid>``) at K7-U4c time. The kernel-
side ``memory.copy`` op can target a named memory by name.

This is already in place at K7-U4c for thread Workers (they share the
SAB anyway). K9 extends to fork-mode Workers as well.

**Performance.** The copy is O(bytes). No syscall round-trip needed
beyond the regular linux.syscall to enter the kernel. The kernel-side
memcpy uses the native wasm ``memory.copy`` instruction across named
memories.

--------------------------------------------------------------------------------
12. Refutation budget for K9
--------------------------------------------------------------------------------

**K9 refutation budget: ≤ 2.**

Reasoning per §20.9 three-input formula:

1. K8 actual (expected at K9 opener time): ≤ 1. K7 was 1.
   Trajectory holding favorable.

2. Surface novelty: K9 introduces three loosely-coupled
   sub-systems (waitpid, pipes, stdin) plus process-group
   primitives. Each is upstream-clean on the kernel side but novel
   on the arch/HardwareJS bridge side. The cross-process
   env.user_memory contract is the biggest novel surface — it's the
   first case the kernel needs to read from a non-``current`` user
   memory. Surface estimate: moderate-high.

3. K9 is single-axis (process-management capabilities). The
   sub-axes (wait/pipe/stdin/pgrp) are not independent — they're all
   "shell needs this." Single-axis discipline preserved.

≤ 2 is honest. Inside the ≤ 2, the natural risk sites are:

* The cross-process copy_to_user/copy_from_user contract — wasm
  multi-memory bridging for fork-mode Workers (currently only
  thread-mode shares memory). May surface a §15 row on named-memory
  registration order.

* The ``arch_notify_child_exit`` ↔ upstream ``do_exit`` /
  ``release_task`` interaction. Upstream do_exit does many things
  (close fds, free signal handlers, decrement reference counts);
  invoking it from a kthread context that wasn't the dying task
  could surface ordering surprises.

* SIGCHLD auto-reaping when SIG_IGN — upstream behavior is correct,
  but the wasm32 side needs to handle "task_struct exists but no
  Worker" cleanly.

If K9 closes at 0-1 actual, the surface estimate was conservative.
If K9 closes at 3+, the bundle was too aggressive — K10 should
re-budget accordingly.

--------------------------------------------------------------------------------
13. K9 sub-phase decomposition
--------------------------------------------------------------------------------

K9-A1
  Probes: T_wait4 (kernel-side do_wait callable), T_pipe_alloc
  (pipefs allocates a pipe), T_stdin_queue (per-Worker stdin region
  allocatable), T_cross_user_mem (copy_to_user_for(other_tid, ...)
  works against a synthetic second Worker).

K9-A2
  Source-site snapshot.

K9-B1
  ``arch_notify_child_exit`` hook + upstream patch (analogous to K8's
  arch_signal_wake_target). Kernel-side child-exit transition.
  ``wait4`` end-to-end demo: hello-k9-wait.c forks child, child
  exits 0, parent wait4 returns child's pid + WEXITSTATUS=0.

K9-B2
  Cross-process copy_to_user / copy_from_user via tid-keyed
  named-memory bridging. Verify thread-mode unchanged; fork-mode
  cross-process works.

K9-B3
  Pipe end-to-end: hello-k9-pipe.c — parent pipes, forks two
  children, dups stdout/stdin appropriately, execs hello-pipe-tx
  and hello-pipe-rx, waits both. Asserts the bytes traveled.

K9-C1
  Per-Worker stdin region + HardwareJS-side input channel + xterm
  wiring sketch. hello-k9-stdin.c reads a line from stdin and prints
  it back. vitest harness feeds bytes via the channel.

K9-C2
  Process-group sanity tests. setpgid/getpgid/setsid roundtrip;
  kill(-pgid, SIGTERM) delivers to every member of pgid.

K9-D1
  Audit: every K9 R-row maps to at least one hardwarejs/test/k9-*
  sub-test.

K9-D2
  Close ledger + Appendix A flip + K10 surface confirmation.

--------------------------------------------------------------------------------
14. Acceptance criteria (K9 close)
--------------------------------------------------------------------------------

K9 closes when:

(a) ``hello-k9-wait.c`` (fork + exit + wait4) prints child's pid
    and exit code; vitest asserts the trace.

(b) ``hello-k9-pipe.c`` (shell-pipeline shape: parent pipes, forks
    two children, dups stdout/stdin, execs two binaries that
    transfer N bytes through the pipe) closes cleanly with all
    expected bytes asserted in vitest.

(c) ``hello-k9-stdin.c`` (read line from stdin, echo back) reads
    bytes posted by the test's stdin channel writer, writes them to
    stdout, exits 0.

(d) ``hello-k9-pgrp.c`` (setpgid, getpgrp, kill -pgid) sends SIGTERM
    to a process group of 2 children; both receive SIGTERM via K8
    infrastructure; both exit with WIFSIGNALED+WTERMSIG=SIGTERM.

(e) Regression: K5-C2, K6-C2, K7-U4f, K8-C1-C3 all still pass
    unchanged.

(f) ``make hwjs-verify`` clean: tsc + vitest pass.

(g) Cumulative K9 refutation count ≤ 2.

(h) Appendix A populated.

--------------------------------------------------------------------------------
16. K9 close ledger
--------------------------------------------------------------------------------

K9 OPEN → CLOSED at v0.1.0-rc10.

**16.1 Acceptance verification**

Each acceptance criterion from §14 maps to a vitest:

(a) hello-k9-wait — fork + exit + wait4 → ``hardwarejs/test/
    k9-c1-wait.test.ts``. Asserts parent's "starting" line,
    spawned child pid > 1, child's "hello from child" line,
    parent's reaped pid + WEXITSTATUS=42, "end-to-end" match.

(b) hello-k9-pipe — pipe + dup2 + writev + read + waitpid via
    ``__NR_wasm_spawn_image_actions`` (DUP2 + CLOSE actions
    applied to child's fd table clone) → ``hardwarejs/test/
    k9-c1-pipe.test.ts``. Asserts parent's "starting" line,
    pipe fds r=R w=W with R, W ≥ 3, spawned child pid > 1,
    read 15 bytes "hello via pipe\\n" exactly, reaped pid +
    WEXITSTATUS=0, "end-to-end OK", child stdout did NOT leak
    to onUserOutput (proves fd=1 routed to pipe).

(c) hello-k9-stdin — read fd 0 + echo →
    ``hardwarejs/test/k9-c1-stdin.test.ts``. Two sub-tests:

    1. with stdinChannel: detects "ready for input" sentinel,
       injects "hello stdin\\n", asserts the demo echoes it
       back and exits 0.
    2. without stdinChannel: legacy K5-K8 EOF; demo prints
       "echo: (EOF)" and exits 0. Regression-safe.

(d) hello-k9-pgrp — setpgid + getpgrp + kill(-pgid, SIGTERM)
    → ``hardwarejs/test/k9-c2-pgrp.test.ts``. Asserts parent
    pid==1, pgrp==1, two children spawned with distinct pids,
    setpgid moves both into pgid=child1, kill(-child1, SIGTERM)
    sent, both children's onUserExit code == 143 (per K8-B4
    default-disposition shortcut), "end-to-end OK", child's
    "after pause (BUG)" sentinel never observed.

(e) Regression sweep — K5-C2 / K6-C2 / K7-U4f / K8-C1-C3 all
    green. Confirmed via full ``pnpm -C hardwarejs exec vitest
    run`` (K8-B1 futex EINTR test has a known timing flake
    under high CPU contention; passes in isolation).

(f) ``make hwjs-verify`` clean — tsc + vitest pass.

(g) Cumulative K9 refutation count: **1 / ≤ 2**.

(h) Appendix A populated below.

**16.2 R-row → vitest mapping (K9-D1 audit)**

* **R-K9-1** (JS-direct cross-process I/O for wait4 + pipe +
  stdin + pgrp + cross-process mem; replaces the §2 + §3 + §4
  + §5 + §6 + §11 kernel-side plan):

  * wait4 sub-test: ``k9-c1-wait.test.ts`` — child exits 42,
    parent's wait4 returns child pid + WEXITSTATUS=42 +
    onUserExit fires for both.
  * pipe sub-test: ``k9-c1-pipe.test.ts`` — parent reads 15
    bytes posted by child through pipe; refutation evidence:
    child's "hello via pipe\\n" never appears in
    onUserOutput, proving fd=1 was redirected to the pipe
    write end via dup2.
  * stdin sub-test: ``k9-c1-stdin.test.ts`` (with-channel
    variant) — bytes injected via stdinChannel.write reach
    the demo's read(0, ...).
  * pgrp sub-test: ``k9-c2-pgrp.test.ts`` — kill(-c1, SIGTERM)
    delivered to both members via processGroupRegistry.
    getPidsInGroup walk + deliverSignalToTarget loop.

* **R-K9-2** (wasm_spawn_image / wasm_spawn_image_actions
  extension syscalls 1024 / 1025):

  * 1024 sub-test: ``k9-c1-wait.test.ts`` (parent's
    ``wasm_spawn_image("/hello-k9-child")`` returns pid > 1)
    + ``k9-c2-pgrp.test.ts`` (parent spawns 2 children via
    1024).
  * 1025 sub-test: ``k9-c1-pipe.test.ts`` (parent's
    ``wasm_spawn_image_actions("/hello-k9-pipe-child",
    [DUP2(w,1), CLOSE(r), CLOSE(w)], 3)`` succeeds; child's
    fd=1 goes to the pipe write end).

* **R-K9-3** (signal-termination encoding shortcut: K8-B4
  default-disposition exits with ``128 + sig`` rather than
  WIFSIGNALED+WTERMSIG=sig):

  * sub-test: ``k9-c2-pgrp.test.ts`` — both children's
    onUserExit code is 143 (= 128 + 15 = 128 + SIGTERM)
    rather than the POSIX-expected WIFSIGNALED encoding;
    the demo asserts WEXITSTATUS=143 directly to match the
    shortcut.

**16.3 Refutation ledger (cumulative for K9)**

Total refutations: **1**. Budget: **≤ 2**. Trajectory
holding favorable.

The single refutation **R-K9-1** absorbs four originally-
specced kernel-side sub-systems:

* §2 / §3 ``arch_notify_child_exit`` + ``do_exit`` on a
  non-current task → refuted at K9-B1 (upstream do_exit
  assumes ``current == tsk`` in many places).
* §4 / §11 ``copy_to_user_for(tid, ...)`` via tid-keyed
  named-memory bridging → refuted at K9-B3 (cross-process
  memory copy is a JavaScript Uint8Array.set between two
  SAB-backed Memory views; no kernel C needed).
* §6 per-Worker stdin SAB ring with kernel-side fd 0 demux
  → refuted at K9-C1 (per-tid JavaScript queue + async
  Promise sync inside dispatchSyscall is sufficient).
* §8 ``task->signal->pgrp`` walked by ``kill_pgrp`` →
  refuted at K9-C2 (per-pid pgid record in
  processGroupRegistry + getPidsInGroup walk).

All four would have required cross-process syscall RPC
infrastructure (the same retirement signal as R-K8-1 +
R-K8-2). Folding them into one §15 row keeps the ledger
honest: a single architectural decision ("K9 ships JS-direct
cross-process I/O; K10+ retires it together with R-K8-1 +
R-K8-2") drove all four sub-system pivots.

**R-K9-2** (wasm32 extension syscalls 1024 + 1025) is a
§15 row but not a refutation per se: the K9 opener didn't
specify an alternative for fork-with-Asyncify-continuation,
so introducing the extension syscalls isn't a U-turn from
a prior plan — it's a positive design choice tracked for
K10+ retirement when posix_spawn(2) routes through proper
kernel-side fork+execve.

**R-K9-3** (signal-termination encoding shortcut) is also
not a refutation — it's a known K8-B4 shortcut that K9
inherited and made test-visible; tracked for K10+ when the
proper WIFSIGNALED+WTERMSIG encoding lands.

Net at K9 close: **6 open §15 rows** (R-K6-1, R-K6-2,
R-K7-1, R-K8-1, R-K8-2, R-K9-1) + 2 new K9-introduced rows
(R-K9-2, R-K9-3) = **8 total open**, 4 retired. K9 didn't
pay any prior debt; the K10+ cross-process syscall RPC
bundle will retire R-K8-1 + R-K8-2 + R-K9-1 + R-K9-2 +
R-K9-3 together (5 rows in one shot).

**16.4 §15 scoreboard (K9 perspective)**

Updates to ``docs/ARCHITECTURE.md §15``:

* K9 row in the K-phase ledger flipped from "OPEN (next)"
  to "closed" with annotated tag v0.1.0-rc10.
* K10 row flipped from "pinned" to "OPEN (next)" with
  annotated tag v0.1.0-rc11.
* Three new rows appended after R-K8-2:

  * R-K9-1 (JS-direct cross-process I/O — wait4 + pipe +
    stdin + pgrp).
  * R-K9-2 (wasm32 extension syscalls __NR_wasm_spawn_image
    + __NR_wasm_spawn_image_actions).
  * R-K9-3 (signal-termination encoding shortcut: 128+sig
    instead of WIFSIGNALED+WTERMSIG).

  Each row carries a "Lives in" file-list, a "Retires by"
  K-phase trigger (all three: K10+ cross-process syscall
  RPC), and a "Why it's OK for K9" justification.

**16.5 K10 surface proposal**

K10 single-axis: **terminal / pty + line discipline + job
control + initcall dispatcher**.

Specifically:

1. ``pty(7)`` master/slave pair allocation on a per-tty
   basis. The pty master exposes a HardwareJS-side stream;
   the slave is a regular fd inherited via fork+exec.

2. Line-discipline state: ECHO / ICANON / ISIG / etc. The
   K9-C1 stdinChannel becomes the input source; line-mode
   buffers up to a newline before delivering to user
   read(0, ...). Raw mode delivers each byte immediately
   (bash readline needs this).

3. Controlling-tty wiring: ``ioctl(TIOCSCTTY)``,
   foreground-pgrp tracking, SIGTTIN/SIGTTOU on background
   reads/writes. Builds on K9-C2's processGroupRegistry.

4. Initcall dispatcher: post-link Python pass that walks
   the kernel's ``.initcall*.init`` sections and emits real
   ``__initcall*_start`` / ``__initcall*_end`` boundary
   symbols (closes the longstanding §15 row tracking
   wasm-ld's lack of GNU linker script support for these
   anchors). Once ``do_initcalls()`` runs, every subsystem
   that registers via ``early_initcall()`` /
   ``subsys_initcall()`` / ``device_initcall()`` becomes
   live.

5. Cross-process syscall RPC infrastructure (NOT strictly
   K10 scope — would be K11 — but we keep an eye on it
   because retiring R-K8-1 + R-K8-2 + R-K9-1/2/3 hangs on
   it).

**Refutation budget for K10: ≤ 2.**

Reasoning per §20.9:

* K9 actual: 1. K8 was 0. Trajectory holding favorable.
* Surface novelty: terminal/pty is a single conceptually
  unified subsystem (line discipline IS a tty thing), but
  the initcall dispatcher is genuinely a separate axis.
  The two coupled together as one K-phase is a stretch but
  acceptable: both are "infrastructure for D1 bash" with
  the initcall dispatcher serving as the unblock for any
  upstream subsystem that registers via initcall (devtmpfs,
  inotify, etc.).
* Single-axis discipline: marginal. K10 is "shell can run
  with a real terminal," with the initcall dispatcher as a
  prerequisite that happens to fit into the same phase.

≤ 2 is the right budget for K10 given novelty + axis
coupling.

--------------------------------------------------------------------------------
Appendix A — K9 close checklist
--------------------------------------------------------------------------------

[x] K9-A1 probes pass
[x] K9-A2 source-site snapshot landed
[x] ~~patches/0009-exit-add-arch_notify_child_exit-hook.patch landed~~
    REFUTED (R-K9-1): kernel-side hook deferred; JS-direct
    childExitRegistry implements wait4 in dispatchSyscall.
[x] ~~arch/wasm32/include/asm/process.h with cross-user-mem helpers~~
    REFUTED (R-K9-1): cross-process copy is JS-side
    Uint8Array.set between SAB-backed userMemory views; no
    kernel-side helpers needed.
[x] ~~arch/wasm32/kernel/process.c::arch_notify_child_exit implemented~~
    REFUTED (R-K9-1): see above.
[x] ~~hardwarejs/src/exitNotificationRing.ts (new) wires user-Worker
    exit through to kernel~~
    REFUTED (R-K9-1): childExitRegistry.recordChildExit is
    called directly from the user-Worker exit message handler
    in spawnForkWorker; no ring needed.
[x] ~~Cross-process copy_to_user_for / copy_from_user_for working
    against fork-mode Worker pairs~~
    REFUTED (R-K9-1): folded into pipeRegistry's JS-side
    ring buffer + fdTableRegistry's fd lookup.
[x] pipe/pipefs interaction verified — folded into JS-side
    pipeRegistry; hello-k9-pipe.c demo exercises the
    full pipe + dup2 + read + writev + close cycle.
[x] hardwarejs/src/stdinChannel.ts (new) with per-Worker queue
[x] Per-Worker stdin region allocated and wired through
    hardwarejs/src/worker.mjs read(0, ...) handler — folded
    into dispatchSyscall's fd-aware __NR_read path; the
    user Worker's read syscall awaits stdinChannel.read.
[x] BootKernelOptions.stdinChannel? added; default no-op channel
    provides EOF on read (K5/K6 regression-safe) — supplied
    via opts.stdinChannel; K8-and-earlier callers without it
    get fd=0=EOF (verified via the no-channel sub-test).
[x] hello-k9-wait / hello-k9-pipe / hello-k9-stdin / hello-k9-pgrp
    demos + vitest tests
[x] Process-group primitives smoke-tested (setpgid, getpgrp, setsid,
    kill -pgid)
[x] K5-C2, K6-C2, K7-U4f, K8-C1-C3 regressions all green
[x] make hwjs-verify clean
[x] K9 close ledger in §16 (above)
[x] Refutation budget: actual ≤ 2 (1 / ≤ 2)
[x] K10 surface proposal in K9 close ledger §16.5
[x] v0.1.0-rc10 annotated tag placed

--------------------------------------------------------------------------------
History
--------------------------------------------------------------------------------

K9 OPENER (rc8 → rc9 era)
  Document created. Framework sections (§1–§14) and Appendix A
  defined. Implementation sequence pinned. Budget ≤ 2 declared.

K9 CLOSE (this commit, rc10)
  All sub-phases A1..D2 landed. Cumulative refutation count:
  1 (R-K9-1, absorbing four originally-specced kernel-side
  sub-systems into a single JS-direct ledger row). Two new
  §15 rows (R-K9-2, R-K9-3) introduced; 0 prior rows retired.
  K10 declared with surface = terminal/pty + initcall
  dispatcher; budget ≤ 2.
