971 lines · plain
1================================================================================2Process-management model for the wasm32 port3================================================================================4 5This document is the spec-first companion to ``threading-model.rst``,6``signals-model.rst``, and ``rootfs-model.rst``, focused on the7**shell-control subset of POSIX process management**: ``wait4`` /8``waitpid`` child reaping, ``pipe`` / ``dup2`` for inter-process I/O,9``stdin`` wiring through a host-side channel, and process-group10primitives (``setpgrp`` / ``getpgrp`` / ``setsid``) used by job control.11 12This is the K9 opener — the phase that lights up the syscalls a shell13fundamentally needs in order to fork, exec children, wait on them, pipe14their output between commands, and read user input. K9 does NOT include15the terminal/pty line-discipline layer; that is K10. K9 IS the bridge16between "we have multi-process and multi-threaded userspace" (K7) and17"we can run an actual interactive shell" (K10 + D1).18 19Status: K9 OPENER. Implementation sequence captured in §13.20 21--------------------------------------------------------------------------------221. Why this document exists separately23--------------------------------------------------------------------------------24 25K7 closed with a fully-functional Regime 1a (fork) + Regime 1b26(pthread) execution substrate. ``fork`` works (WASM_SPAWN_FORK).27``execve`` works (binfmt_wasm + spawn-ring). pthreads work28(WASM_SPAWN_THREAD + in-Worker futex).29 30What still does NOT work:31 32* A parent process cannot reap a child. ``wait4`` / ``waitpid`` are33 unwired on the kernel side; child exit notifications travel through34 HardwareJS's ``onUserExit`` callback but never reach a parent35 ``task_struct``.36 37* Two processes cannot communicate via a pipe. ``pipe(2)`` is38 upstream-clean but the wasm32 fd table doesn't honor pipe semantics;39 any ``write`` on a pipe goes to dev/null.40 41* ``stdin`` returns 0 (EOF) on every read. The user-Worker syscall42 handler doesn't have any wire-up to host-side keyboard input.43 44* Process groups don't exist. ``setpgrp(0,0)``, ``getpgrp()``,45 ``setsid()`` syscalls succeed but the underlying upstream state is46 inert; ``kill(-pgid, sig)`` doesn't reach the right set of processes.47 48K9 is the phase that fixes all of the above. They are bundled because49they form one coherent capability: "a parent process can fork a child,50pipe its output to another child, wait for both to exit, and read user51input meanwhile." That is the minimum capability a shell needs from the52kernel.53 54Reading order:55 56* If you are implementing K9-A (waitpid + child reaping):57 §2 (waitpid contract), §3 (exit-notification routing), §13 (K958 sub-phase decomposition).59 60* If you are implementing K9-B (pipes):61 §4 (pipe contract), §5 (pipefs interaction), §13.62 63* If you are implementing K9-C (stdin):64 §6 (stdin wiring), §7 (host-side keyboard channel), §13.65 66* If you are implementing K9-D (process groups):67 §8 (process-group surface), §13.68 69This document expands ``ARCHITECTURE.md §5`` (``fork``) and §670(``execve``) by filling in the parent-side observability story that71those sections deliberately left to "later K-phases."72 73--------------------------------------------------------------------------------742. wait4 / waitpid contract75--------------------------------------------------------------------------------76 77POSIX ``wait4(pid, &status, options, rusage)`` semantics:78 79* ``pid > 0``: wait for the specific child pid.80* ``pid == 0``: wait for any child in the caller's process group.81* ``pid == -1``: wait for any child.82* ``pid < -1``: wait for any child in process group ``-pid``.83 84Returns the pid that was reaped, or:85 86* ``0`` if WNOHANG and no child is ready.87* ``-ECHILD`` if no children exist.88* ``-EINTR`` if interrupted by a signal (interacts with K8's signal89 delivery).90 91``status`` is filled in with the child's exit info per92``WIFEXITED`` / ``WEXITSTATUS`` / ``WIFSIGNALED`` / ``WTERMSIG``93conventions.94 95**The K9 implementation strategy.** ``wait4`` is upstream-clean on the96syscall surface; ``kernel/exit.c::do_wait`` is the canonical97implementation. It walks ``current->children`` looking for an98appropriate child task_struct in EXIT_ZOMBIE state, copies out the99status, and reaps (release_task) the zombie.100 101K9 needs three pieces:102 1031. **Children list maintenance.** When a parent forks (WASM_SPAWN_FORK104 path), upstream's ``copy_process`` correctly inserts the child into105 ``current->children``. Verified — this works at K7. K9 does NOT106 add work here.107 1082. **EXIT_ZOMBIE transition.** When a child exits via SYS_exit_group,109 the child's task_struct must transition through EXIT_ZOMBIE so the110 parent can find it. Upstream's ``do_exit`` does this — BUT on111 wasm32 today, the user Worker calls SYS_exit_group, HardwareJS sees112 it via the ``exit`` message, the user Worker terminates, and the113 kernel-side task_struct is left dangling (because there's no kthread114 running on the kernel Worker that knows the user Worker is gone).115 1163. **Reaping notification.** When the parent is blocked in wait4, it117 needs to be woken when the child exits. Upstream uses118 ``wake_up_interruptible_sync_poll(&parent->signal->wait_chldexit, ...)``.119 120The K9 plan (HISTORICAL — see HISTORICAL NOTE below for the121post-R-K9-1 refutation; the K9-B1 implementation took the122JS-direct path instead):123 124(A) Add a new kernel-side bridge ``arch_notify_child_exit(tid, code)``125 that HardwareJS calls when a user Worker exits. This bridge:126 127 * Looks up the child task_struct by tid.128 * Calls ``do_exit(code)`` on it (or equivalent — see §3).129 * Wakes parent if blocked.130 131(B) Wire HardwareJS-side ``onUserExit`` to ``arch_notify_child_exit``132 via a new ring (or extend the spawn-ring with an EXIT message133 shape).134 135(C) Verify wait4 in kernel/exit.c works once (A) and (B) are in place136 — likely no kernel-side changes needed beyond the bridge.137 138HISTORICAL NOTE (R-K9-1): the kernel-side approach above was139refuted at K9-B1 implementation time. The full plan would have140required either (a) calling upstream ``do_exit`` on a non-current141task (unsafe — upstream's do_exit assumes ``current == tsk`` in142multiple places), or (b) reproducing the relevant subset of143do_exit (exit_notify, EXIT_ZOMBIE transition, do_notify_parent,144wake_up_interruptible on parent's wait_chldexit) in arch code as145a wasm32-specific ``wasm32_reap_exited_task(t, code)`` — which146would be substantial kernel-side machinery. Either path is147internally consistent with upstream semantics but adds load-148bearing kernel C code to a sub-phase whose acceptance criterion149(§10 (a): "wait4 returns child's pid and exit code") can be met150without it.151 152The K8-B6 / R-K8-2 pattern (JS-direct cross-process syscall153delivery via ``dispatchSyscall``, tracked as a §15 row with K9+154or K10+ retirement when cross-process syscall RPC lands) is the155established precedent. K9-B1 follows the same pattern: a156JS-direct child-exit registry inside ``hardwarejs/src/157childExitRegistry.ts``, with ``__NR_wait4`` resolved entirely in158``dispatchSyscall``. The kernel-side ``current->children`` list159is preserved (upstream's ``copy_process`` already maintains it160correctly through fork at K7) but is not consulted at K9.161 162This is one §15 row added at K9 close (R-K9-1: "K9 wait4 via163JS-direct registry; kernel-side do_wait/EXIT_ZOMBIE not engaged").164Retirement signal: K10+ when cross-process syscall RPC lands and165the kernel-Worker becomes the wait4 dispatcher. The same RPC166infrastructure that retires R-K8-1 + R-K8-2 will retire R-K9-1.167 168--------------------------------------------------------------------------------1693. Exit-notification routing (kernel-side)170--------------------------------------------------------------------------------171 172The K9 child-exit flow:173 174::175 176 1. User Worker calls SYS_exit_group(code) via linux.syscall.177 2. Kernel-side syscall handler (in worker.mjs) intercepts BEFORE178 posting to kernel — same shape as SYS_futex in K7-U4e.179 Posts {kind: "exit", pid, code} message to main thread.180 3. HardwareJS-side spawn-handler receives the exit message,181 terminates the Worker, and invokes the new K9 hook182 `arch_notify_child_exit` via the exit-notification ring.183 4. Kernel-side `arch_notify_child_exit(tid, code)`:184 a. Find child task_struct by tid.185 b. Set child->exit_code = code, transition to EXIT_ZOMBIE.186 c. Walk to parent; call wake_up_interruptible on parent's187 wait_chldexit if parent is blocked.188 d. Auto-reap if parent has SA_NOCLDWAIT or has set SIGCHLD =189 SIG_IGN (upstream behavior).190 5. Parent wait4 returns with the child's pid + status.191 192**Critical correctness invariant.** The child task_struct MUST stay193alive (not be freed) from "user Worker exits" to "parent reaps."194Upstream's reference count handles this — ``do_exit`` sets EXIT_ZOMBIE195but doesn't release the task_struct until release_task is called from196the parent's wait path (or from auto-reap).197 198On wasm32 we must preserve this invariant: the user Worker terminating199does NOT free the kernel-side task_struct. The kernel-side200``arch_notify_child_exit`` hook is what triggers the EXIT_ZOMBIE201transition; until then, the task_struct stays in TASK_RUNNING /202schedulable state but its user Worker is gone.203 204**Edge case: parent died before child.** Upstream's reparenting moves205the orphaned child to ``init`` (pid 1). On wasm32 we honor this via206upstream's ``forget_original_parent`` — the kernel-side code does the207right thing; K9 just ensures the bridge correctly identifies the208parent task_struct.209 210HISTORICAL NOTE (R-K9-1 cont.): the same JS-direct pivot also211governs §4 (pipes) and §11 (cross-process env.user_memory)212below. K9-B2 was originally specced as "kernel-side cross-213process copy_to_user_for / copy_from_user_for via tid-keyed214named-memory" with kernel C helpers consulting a per-tid215arch table. The K9-B1 implementation refuted that route in216favor of a HardwareJS-side ``pipeRegistry`` + ``fdTableRegistry``217pair: pipe ring buffers live in JavaScript, cross-process218data movement is just a JS Uint8Array write between the219producer's and consumer's per-Worker ``env.user_memory``220SABs (both already accessible to HardwareJS via the K5-C2221spawn registry). No new kernel C code, no named-memory222registration ordering surprises, no R-K8-2-shaped retirement223dependency. The K9-B3 sub-phase below ships the JS-direct224pipe + fd-table machinery; the original §4 / §11 plan225remains documented as the K10+ retirement target.226 227The K9 cumulative refutation count after this fold-in:228**1 / ≤ 2** (R-K9-1 covers all four sub-systems —229wait4/pipe/stdin/cross-process-mem — that originally had230kernel-side machinery in the opener; the §15 ledger row231absorbs the pipe + fd-table extension). R-K9-2 below tracks232the new §15 row added at K9-B1 (``__NR_wasm_spawn_image``233extension syscall) which is a §15 row but not a refutation.234 235--------------------------------------------------------------------------------2364. pipe / pipefs contract237--------------------------------------------------------------------------------238 239POSIX ``pipe(int pipefd[2])`` creates a pipe pair. ``pipefd[0]`` is the240read end; ``pipefd[1]`` is the write end. Both are regular fds usable241with ``read`` / ``write`` / ``close`` / ``dup2``.242 243Upstream's ``fs/pipe.c`` is full-featured: lock-protected ring buffer244in kernel memory, blocking reads/writes with proper EPIPE handling,245splice/tee support. On wasm32 we **inherit pipefs unchanged**. The246question is whether kernel-Worker and user-Worker can interact with247pipefs correctly given the cross-Worker memory boundary.248 249**The wasm32 pipe shape:**250 251* The pipe's ring buffer lives in env.kernel_memory (allocated via252 kmalloc — upstream behavior).253 254* User-Worker A calls ``write(pipe_fd, buf, len)``. The syscall255 routes through worker.mjs → kernel-side syscall handler →256 ``fs_write`` → ``pipe_write``. The ``buf`` argument is a user-space257 pointer in user-Worker A's env.user_memory. The kernel-side258 ``copy_from_user`` must read from A's env.user_memory.259 260* Per ``threading-model.rst §3``, the kernel can read a user's261 env.user_memory by tid. The pipe write copies bytes from A's262 env.user_memory into the pipe ring buffer (kernel memory).263 264* User-Worker B calls ``read(pipe_fd, buf, len)``. The kernel-side265 ``pipe_read`` copies bytes from the kernel-memory ring buffer into266 B's env.user_memory.267 268This works because the kernel has access to BOTH process's269env.user_memory SABs via the per-tid Worker registry. The270``copy_to_user`` / ``copy_from_user`` helpers must dispatch on the271target tid.272 273**What K9 needs to add:**274 275* ``copy_to_user_for(tid, dst, src, len)`` / equivalent — kernel276 helpers that copy to/from a specific user's env.user_memory, not277 just ``current``'s. Upstream's ``copy_to_user`` is hard-coded to278 ``current``'s memory; the wasm32 implementation already handles279 this for thread mode (Regime 1b shares memory), but cross-process280 pipes are the first case where ``current`` != target.281 282* HardwareJS must expose every user-Worker's env.user_memory SAB to283 the kernel Worker. The K5 spawn registry already keeps these; K9284 exposes them by tid for the cross-process copy path.285 286* The pipe's blocking semantics (blocking read on empty pipe) must287 honor signal-pending (K8 dependency). When a parent blocks in288 ``read`` on a pipe and a signal arrives, return ``-EINTR``.289 290--------------------------------------------------------------------------------2915. fd table interaction with execve292--------------------------------------------------------------------------------293 294When a process forks then execs, fd inheritance rules apply:295 296* By default, fds survive fork() (the child inherits a copy of the297 parent's fd table).298* By default, fds survive execve() (the new program inherits the299 same fd table).300* The ``FD_CLOEXEC`` flag (set via ``fcntl(fd, F_SETFD, FD_CLOEXEC)``301 or implicitly via ``O_CLOEXEC`` at open time) makes the fd close on302 execve.303 304Upstream ``copy_files`` + ``do_close_on_exec`` handle this. On wasm32:305 306* ``fork()`` already inherits the fd table via upstream's307 ``copy_files`` (verified at K7? — K9-A1 probe will confirm).308 309* ``execve()`` clears CLOEXEC fds via ``do_close_on_exec`` — but K9310 needs to verify this fires correctly in binfmt_wasm's load path.311 312* ``dup2(oldfd, newfd)`` is upstream-clean; should work without arch313 changes.314 315**The shell-pattern test.** A shell pipeline ``cmd1 | cmd2`` works by:316 3171. Shell calls ``pipe(p)`` — gets ``[r, w]``.3182. Shell forks child1. In child1, ``dup2(w, STDOUT_FILENO)``,319 ``close(r)``, ``close(w)``, exec cmd1.3203. Shell forks child2. In child2, ``dup2(r, STDIN_FILENO)``,321 ``close(r)``, ``close(w)``, exec cmd2.3224. Shell closes r and w in its own fd table.3235. Shell waits for both children.324 325K9-B1 verifies this exact pattern works end-to-end. The test is the326shell-pipeline-probe demo.327 328--------------------------------------------------------------------------------3296. stdin wiring330--------------------------------------------------------------------------------331 332The user Worker's stdin (fd 0) currently returns 0 (EOF) on every333read. K9 wires it to a host-side input channel.334 335**The wiring shape:**336 337* HardwareJS owns the "host-side stdin" — for the demo, this is xterm.js338 keystrokes captured by ``term.onData(...)``. The bytes are pushed339 into a per-Worker stdin queue (a SAB ring buffer, similar to the340 spawn-ring shape, but the producer is HardwareJS and the consumer is341 the user Worker).342 343* The user Worker's read(0, buf, len) syscall:344 345 * If the stdin queue has bytes, copy them to buf, return the count.346 * If the stdin queue is empty:347 - If O_NONBLOCK: return -EAGAIN.348 - If blocking (default): Atomics.wait on the queue's read_notify349 word until bytes available or signal arrives.350 351* HardwareJS-side xterm onData handler:352 - Decode the keystroke to bytes (UTF-8).353 - Append to the per-Worker stdin queue's ring buffer.354 - Atomics.notify the read_notify word.355 356* Echo (the typed character appearing in the terminal): K9-C1 keeps357 this as a HardwareJS-side toggle (echo locally before queueing).358 Real echo control via tcsetattr's ECHO bit is K10 scope.359 360**The per-Worker stdin region:**361 362::363 364 +------------------+ <- per-Worker stdin region base365 | head | u32 — write index (HardwareJS writes, Worker reads)366 +------------------+367 | tail | u32 — read index (Worker writes, HardwareJS reads368 | | to know how full the queue is)369 +------------------+370 | read_notify | i32 — bumped + notified when HardwareJS writes371 +------------------+372 | flags | u32 — line-buffering, echo, etc. (K10 will373 | | formalize)374 +------------------+375 | buf[4096] | ring buffer (4KB; K9 default; K10 may tune)376 +------------------+377 378Total: 4112 bytes per Worker. The kernel knows the offset via the379per-task thread_struct field.380 381**Multi-Worker stdin.** Only one Worker (the foreground process,382typically pid 1's shell child) reads stdin meaningfully. Background383processes' stdin syscalls behave per process-group / controlling-tty384rules — which is K10 (terminal/pty layer). K9 implements the385mechanism; K10 layers the policy on top.386 387--------------------------------------------------------------------------------3887. Host-side input channel389--------------------------------------------------------------------------------390 391The host-side input source is the responsibility of the HardwareJS392embedder (LaunchMachine.svelte for the prestrike-web demo). HardwareJS393exposes:394 395::396 397 // hardwarejs/src/index.ts:398 export interface UserStdinChannel {399 write(tid: number, bytes: Uint8Array): void;400 flush?(tid: number): Promise<void>; // K10-extension hook401 }402 403 // Provided by bootKernel options:404 interface BootKernelOptions {405 // ... existing fields ...406 stdinChannel?: UserStdinChannel;407 }408 409The embedder (LaunchMachine) wires xterm.js to this:410 411::412 413 // LaunchMachine.svelte (sketch — concrete in K9-C1):414 const stdinChannel = makeStdinChannel(...);415 416 term.onData((data) => {417 const fg_tid = currentForegroundTid(); // K9 keeps simple: pid 1418 stdinChannel.write(fg_tid, new TextEncoder().encode(data));419 });420 421K10 will refine ``currentForegroundTid`` based on controlling-tty and422job-control semantics. K9 hardcodes it to "the latest exec'd child of423pid 1" or similar simple policy.424 425--------------------------------------------------------------------------------4268. Process groups427--------------------------------------------------------------------------------428 429POSIX process-group surface:430 431* ``setpgid(pid, pgid)`` — set process group of pid.432* ``getpgid(pid)`` — get process group of pid.433* ``setpgrp()`` — equivalent to setpgid(0, 0).434* ``getpgrp()`` — equivalent to getpgid(0).435* ``setsid()`` — create a new session and process group, become group436 leader and session leader.437 438Upstream ``kernel/sys.c`` implements these; on wasm32 they should439mostly Just Work because they manipulate kernel-side440``task->signal->pgrp`` / ``task->signal->session`` only. No arch441hooks needed.442 443**The K9 verification work:**444 445* Smoke test: ``setpgid(0, 0)`` returns 0 and ``getpgrp()`` returns446 the calling pid.447 448* Process-group signal delivery: ``kill(-pgid, SIGTERM)`` sends449 SIGTERM to every process in pgid. Implementation is upstream's450 ``kill_pgrp`` walking the pgrp's task list, calling ``__send_signal``451 for each → K8's ``arch_signal_wake_target`` fires for each.452 453K9 does NOT implement:454 455* The controlling-tty assignment (TIOCSCTTY, etc.) — K10 scope.456 457* Background process-group SIGTTIN/SIGTTOU semantics — K10 scope.458 459* Session-leader-dies-tells-pgrp-foreground SIGHUP — K10 scope.460 461These are job-control specifics; K9 only implements the mechanism462needed for "shell can call setpgid for the children it forks."463 464--------------------------------------------------------------------------------4659. Interaction with K8 signals466--------------------------------------------------------------------------------467 468K9 has hard dependencies on K8:469 470* ``wait4`` blocks on ``parent->signal->wait_chldexit``; signal-pending471 must wake it via K8's ``arch_signal_wake_target`` mechanism.472 473* ``pipe`` blocking read/write must return ``-EINTR`` on signal-pending474 via the K8 SYS_futex extension (since pipe blocking uses475 ``mutex_lock_interruptible`` and ``wait_event_interruptible``, both476 of which honor signal_pending).477 478* ``read(0, ...)`` on empty stdin blocks in ``Atomics.wait``; honors479 signal-pending the same way K8's futex handler does.480 481* ``SIGCHLD`` delivery on child exit: when ``arch_notify_child_exit``482 transitions the child to EXIT_ZOMBIE, upstream's exit path also483 sends SIGCHLD to the parent. K8's signal infrastructure delivers it.484 485K9 sub-phases must verify each K8 dependency at the dependency's486landing point. If a K9 sub-test surfaces a K8-side bug, the appropriate487classification is "K8 refutation that K9 caught" — see §20.9 on488cross-phase refutation accounting.489 490--------------------------------------------------------------------------------49110. Source-site enumeration492--------------------------------------------------------------------------------493 494Per ``ARCHITECTURE.md §20``, K9 implementers run these greps at K9-A1495(probe phase):496 497::498 499 # waitpid / child-reaping sites500 rg 'do_wait|wait_consider_task|EXIT_ZOMBIE|release_task' \501 linux-wasm/linux/kernel/exit.c502 rg 'sys_wait4|sys_waitid|kernel_waitpid' linux-wasm/linux/kernel/503 504 # pipe / pipefs505 rg 'sys_pipe|pipe_read|pipe_write|alloc_pipe_info' \506 linux-wasm/linux/fs/pipe.c507 rg 'pipefs_init|pipe_inode_info' linux-wasm/linux/fs/pipe.c508 509 # fd table / dup510 rg 'sys_dup|do_dup2|copy_files|close_files|exit_files' \511 linux-wasm/linux/fs/file.c512 rg 'do_close_on_exec|FD_CLOEXEC' linux-wasm/linux/fs/513 514 # stdin / tty (NOT terminal-layer, just the fd 0 reading surface)515 rg 'sys_read.*fd.*0|stdin' linux-wasm/linux/fs/read_write.c516 517 # Process groups518 rg 'sys_setpgid|sys_setsid|sys_getpgid|sys_getpgrp' \519 linux-wasm/linux/kernel/sys.c520 rg 'task_pgrp|task_session|same_thread_group' linux-wasm/linux/include/521 522 # User-memory cross-process access523 rg 'access_ok|copy_to_user|copy_from_user|access_process_vm' \524 linux-wasm/linux/525 526These greps land at ``linux-wasm/Documentation/wasm/527process-model-source-snapshots.txt`` per the K6/K7 pattern.528 529--------------------------------------------------------------------------------53011. The cross-process env.user_memory contract531--------------------------------------------------------------------------------532 533K9 introduces the first case where the kernel needs to read/write a534user's env.user_memory that is NOT ``current``. The pipe read/write535path is the primary example.536 537**The contract:**538 539* HardwareJS maintains the per-tid Worker registry (K5-C2 / K7-U4c).540 This registry contains, for each user Worker, its env.user_memory541 SAB.542 543* The kernel side exposes this registry to kernel C code via a544 arch-local table: ``wasm32_user_memory_table[tid] = SAB_byte_offset``545 where ``SAB_byte_offset`` is the offset within env.kernel_memory at546 which a "user memory descriptor" sits. The descriptor includes547 enough state for the kernel to issue ``memory.copy`` ops between the548 destination SAB and env.kernel_memory.549 550* The kernel-side ``copy_to_user_for(tid, ...)`` /551 ``copy_from_user_for(tid, ...)`` helpers consult this table.552 553**Implementation strategy.** The kernel cannot directly hold a554JavaScript SAB reference. Instead, HardwareJS provides a per-tid555buffer-aliased view in env.kernel_memory: each user Worker's556env.user_memory SAB is also imported into the kernel Worker as a557named memory (``env.user_memory_<tid>``) at K7-U4c time. The kernel-558side ``memory.copy`` op can target a named memory by name.559 560This is already in place at K7-U4c for thread Workers (they share the561SAB anyway). K9 extends to fork-mode Workers as well.562 563**Performance.** The copy is O(bytes). No syscall round-trip needed564beyond the regular linux.syscall to enter the kernel. The kernel-side565memcpy uses the native wasm ``memory.copy`` instruction across named566memories.567 568--------------------------------------------------------------------------------56912. Refutation budget for K9570--------------------------------------------------------------------------------571 572**K9 refutation budget: ≤ 2.**573 574Reasoning per §20.9 three-input formula:575 5761. K8 actual (expected at K9 opener time): ≤ 1. K7 was 1.577 Trajectory holding favorable.578 5792. Surface novelty: K9 introduces three loosely-coupled580 sub-systems (waitpid, pipes, stdin) plus process-group581 primitives. Each is upstream-clean on the kernel side but novel582 on the arch/HardwareJS bridge side. The cross-process583 env.user_memory contract is the biggest novel surface — it's the584 first case the kernel needs to read from a non-``current`` user585 memory. Surface estimate: moderate-high.586 5873. K9 is single-axis (process-management capabilities). The588 sub-axes (wait/pipe/stdin/pgrp) are not independent — they're all589 "shell needs this." Single-axis discipline preserved.590 591≤ 2 is honest. Inside the ≤ 2, the natural risk sites are:592 593* The cross-process copy_to_user/copy_from_user contract — wasm594 multi-memory bridging for fork-mode Workers (currently only595 thread-mode shares memory). May surface a §15 row on named-memory596 registration order.597 598* The ``arch_notify_child_exit`` ↔ upstream ``do_exit`` /599 ``release_task`` interaction. Upstream do_exit does many things600 (close fds, free signal handlers, decrement reference counts);601 invoking it from a kthread context that wasn't the dying task602 could surface ordering surprises.603 604* SIGCHLD auto-reaping when SIG_IGN — upstream behavior is correct,605 but the wasm32 side needs to handle "task_struct exists but no606 Worker" cleanly.607 608If K9 closes at 0-1 actual, the surface estimate was conservative.609If K9 closes at 3+, the bundle was too aggressive — K10 should610re-budget accordingly.611 612--------------------------------------------------------------------------------61313. K9 sub-phase decomposition614--------------------------------------------------------------------------------615 616K9-A1617 Probes: T_wait4 (kernel-side do_wait callable), T_pipe_alloc618 (pipefs allocates a pipe), T_stdin_queue (per-Worker stdin region619 allocatable), T_cross_user_mem (copy_to_user_for(other_tid, ...)620 works against a synthetic second Worker).621 622K9-A2623 Source-site snapshot.624 625K9-B1626 ``arch_notify_child_exit`` hook + upstream patch (analogous to K8's627 arch_signal_wake_target). Kernel-side child-exit transition.628 ``wait4`` end-to-end demo: hello-k9-wait.c forks child, child629 exits 0, parent wait4 returns child's pid + WEXITSTATUS=0.630 631K9-B2632 Cross-process copy_to_user / copy_from_user via tid-keyed633 named-memory bridging. Verify thread-mode unchanged; fork-mode634 cross-process works.635 636K9-B3637 Pipe end-to-end: hello-k9-pipe.c — parent pipes, forks two638 children, dups stdout/stdin appropriately, execs hello-pipe-tx639 and hello-pipe-rx, waits both. Asserts the bytes traveled.640 641K9-C1642 Per-Worker stdin region + HardwareJS-side input channel + xterm643 wiring sketch. hello-k9-stdin.c reads a line from stdin and prints644 it back. vitest harness feeds bytes via the channel.645 646K9-C2647 Process-group sanity tests. setpgid/getpgid/setsid roundtrip;648 kill(-pgid, SIGTERM) delivers to every member of pgid.649 650K9-D1651 Audit: every K9 R-row maps to at least one hardwarejs/test/k9-*652 sub-test.653 654K9-D2655 Close ledger + Appendix A flip + K10 surface confirmation.656 657--------------------------------------------------------------------------------65814. Acceptance criteria (K9 close)659--------------------------------------------------------------------------------660 661K9 closes when:662 663(a) ``hello-k9-wait.c`` (fork + exit + wait4) prints child's pid664 and exit code; vitest asserts the trace.665 666(b) ``hello-k9-pipe.c`` (shell-pipeline shape: parent pipes, forks667 two children, dups stdout/stdin, execs two binaries that668 transfer N bytes through the pipe) closes cleanly with all669 expected bytes asserted in vitest.670 671(c) ``hello-k9-stdin.c`` (read line from stdin, echo back) reads672 bytes posted by the test's stdin channel writer, writes them to673 stdout, exits 0.674 675(d) ``hello-k9-pgrp.c`` (setpgid, getpgrp, kill -pgid) sends SIGTERM676 to a process group of 2 children; both receive SIGTERM via K8677 infrastructure; both exit with WIFSIGNALED+WTERMSIG=SIGTERM.678 679(e) Regression: K5-C2, K6-C2, K7-U4f, K8-C1-C3 all still pass680 unchanged.681 682(f) ``make hwjs-verify`` clean: tsc + vitest pass.683 684(g) Cumulative K9 refutation count ≤ 2.685 686(h) Appendix A populated.687 688--------------------------------------------------------------------------------68916. K9 close ledger690--------------------------------------------------------------------------------691 692K9 OPEN → CLOSED at v0.1.0-rc10.693 694**16.1 Acceptance verification**695 696Each acceptance criterion from §14 maps to a vitest:697 698(a) hello-k9-wait — fork + exit + wait4 → ``hardwarejs/test/699 k9-c1-wait.test.ts``. Asserts parent's "starting" line,700 spawned child pid > 1, child's "hello from child" line,701 parent's reaped pid + WEXITSTATUS=42, "end-to-end" match.702 703(b) hello-k9-pipe — pipe + dup2 + writev + read + waitpid via704 ``__NR_wasm_spawn_image_actions`` (DUP2 + CLOSE actions705 applied to child's fd table clone) → ``hardwarejs/test/706 k9-c1-pipe.test.ts``. Asserts parent's "starting" line,707 pipe fds r=R w=W with R, W ≥ 3, spawned child pid > 1,708 read 15 bytes "hello via pipe\\n" exactly, reaped pid +709 WEXITSTATUS=0, "end-to-end OK", child stdout did NOT leak710 to onUserOutput (proves fd=1 routed to pipe).711 712(c) hello-k9-stdin — read fd 0 + echo →713 ``hardwarejs/test/k9-c1-stdin.test.ts``. Two sub-tests:714 715 1. with stdinChannel: detects "ready for input" sentinel,716 injects "hello stdin\\n", asserts the demo echoes it717 back and exits 0.718 2. without stdinChannel: legacy K5-K8 EOF; demo prints719 "echo: (EOF)" and exits 0. Regression-safe.720 721(d) hello-k9-pgrp — setpgid + getpgrp + kill(-pgid, SIGTERM)722 → ``hardwarejs/test/k9-c2-pgrp.test.ts``. Asserts parent723 pid==1, pgrp==1, two children spawned with distinct pids,724 setpgid moves both into pgid=child1, kill(-child1, SIGTERM)725 sent, both children's onUserExit code == 143 (per K8-B4726 default-disposition shortcut), "end-to-end OK", child's727 "after pause (BUG)" sentinel never observed.728 729(e) Regression sweep — K5-C2 / K6-C2 / K7-U4f / K8-C1-C3 all730 green. Confirmed via full ``pnpm -C hardwarejs exec vitest731 run`` (K8-B1 futex EINTR test has a known timing flake732 under high CPU contention; passes in isolation).733 734(f) ``make hwjs-verify`` clean — tsc + vitest pass.735 736(g) Cumulative K9 refutation count: **1 / ≤ 2**.737 738(h) Appendix A populated below.739 740**16.2 R-row → vitest mapping (K9-D1 audit)**741 742* **R-K9-1** (JS-direct cross-process I/O for wait4 + pipe +743 stdin + pgrp + cross-process mem; replaces the §2 + §3 + §4744 + §5 + §6 + §11 kernel-side plan):745 746 * wait4 sub-test: ``k9-c1-wait.test.ts`` — child exits 42,747 parent's wait4 returns child pid + WEXITSTATUS=42 +748 onUserExit fires for both.749 * pipe sub-test: ``k9-c1-pipe.test.ts`` — parent reads 15750 bytes posted by child through pipe; refutation evidence:751 child's "hello via pipe\\n" never appears in752 onUserOutput, proving fd=1 was redirected to the pipe753 write end via dup2.754 * stdin sub-test: ``k9-c1-stdin.test.ts`` (with-channel755 variant) — bytes injected via stdinChannel.write reach756 the demo's read(0, ...).757 * pgrp sub-test: ``k9-c2-pgrp.test.ts`` — kill(-c1, SIGTERM)758 delivered to both members via processGroupRegistry.759 getPidsInGroup walk + deliverSignalToTarget loop.760 761* **R-K9-2** (wasm_spawn_image / wasm_spawn_image_actions762 extension syscalls 1024 / 1025):763 764 * 1024 sub-test: ``k9-c1-wait.test.ts`` (parent's765 ``wasm_spawn_image("/hello-k9-child")`` returns pid > 1)766 + ``k9-c2-pgrp.test.ts`` (parent spawns 2 children via767 1024).768 * 1025 sub-test: ``k9-c1-pipe.test.ts`` (parent's769 ``wasm_spawn_image_actions("/hello-k9-pipe-child",770 [DUP2(w,1), CLOSE(r), CLOSE(w)], 3)`` succeeds; child's771 fd=1 goes to the pipe write end).772 773* **R-K9-3** (signal-termination encoding shortcut: K8-B4774 default-disposition exits with ``128 + sig`` rather than775 WIFSIGNALED+WTERMSIG=sig):776 777 * sub-test: ``k9-c2-pgrp.test.ts`` — both children's778 onUserExit code is 143 (= 128 + 15 = 128 + SIGTERM)779 rather than the POSIX-expected WIFSIGNALED encoding;780 the demo asserts WEXITSTATUS=143 directly to match the781 shortcut.782 783**16.3 Refutation ledger (cumulative for K9)**784 785Total refutations: **1**. Budget: **≤ 2**. Trajectory786holding favorable.787 788The single refutation **R-K9-1** absorbs four originally-789specced kernel-side sub-systems:790 791* §2 / §3 ``arch_notify_child_exit`` + ``do_exit`` on a792 non-current task → refuted at K9-B1 (upstream do_exit793 assumes ``current == tsk`` in many places).794* §4 / §11 ``copy_to_user_for(tid, ...)`` via tid-keyed795 named-memory bridging → refuted at K9-B3 (cross-process796 memory copy is a JavaScript Uint8Array.set between two797 SAB-backed Memory views; no kernel C needed).798* §6 per-Worker stdin SAB ring with kernel-side fd 0 demux799 → refuted at K9-C1 (per-tid JavaScript queue + async800 Promise sync inside dispatchSyscall is sufficient).801* §8 ``task->signal->pgrp`` walked by ``kill_pgrp`` →802 refuted at K9-C2 (per-pid pgid record in803 processGroupRegistry + getPidsInGroup walk).804 805All four would have required cross-process syscall RPC806infrastructure (the same retirement signal as R-K8-1 +807R-K8-2). Folding them into one §15 row keeps the ledger808honest: a single architectural decision ("K9 ships JS-direct809cross-process I/O; K10+ retires it together with R-K8-1 +810R-K8-2") drove all four sub-system pivots.811 812**R-K9-2** (wasm32 extension syscalls 1024 + 1025) is a813§15 row but not a refutation per se: the K9 opener didn't814specify an alternative for fork-with-Asyncify-continuation,815so introducing the extension syscalls isn't a U-turn from816a prior plan — it's a positive design choice tracked for817K10+ retirement when posix_spawn(2) routes through proper818kernel-side fork+execve.819 820**R-K9-3** (signal-termination encoding shortcut) is also821not a refutation — it's a known K8-B4 shortcut that K9822inherited and made test-visible; tracked for K10+ when the823proper WIFSIGNALED+WTERMSIG encoding lands.824 825Net at K9 close: **6 open §15 rows** (R-K6-1, R-K6-2,826R-K7-1, R-K8-1, R-K8-2, R-K9-1) + 2 new K9-introduced rows827(R-K9-2, R-K9-3) = **8 total open**, 4 retired. K9 didn't828pay any prior debt; the K10+ cross-process syscall RPC829bundle will retire R-K8-1 + R-K8-2 + R-K9-1 + R-K9-2 +830R-K9-3 together (5 rows in one shot).831 832**16.4 §15 scoreboard (K9 perspective)**833 834Updates to ``docs/ARCHITECTURE.md §15``:835 836* K9 row in the K-phase ledger flipped from "OPEN (next)"837 to "closed" with annotated tag v0.1.0-rc10.838* K10 row flipped from "pinned" to "OPEN (next)" with839 annotated tag v0.1.0-rc11.840* Three new rows appended after R-K8-2:841 842 * R-K9-1 (JS-direct cross-process I/O — wait4 + pipe +843 stdin + pgrp).844 * R-K9-2 (wasm32 extension syscalls __NR_wasm_spawn_image845 + __NR_wasm_spawn_image_actions).846 * R-K9-3 (signal-termination encoding shortcut: 128+sig847 instead of WIFSIGNALED+WTERMSIG).848 849 Each row carries a "Lives in" file-list, a "Retires by"850 K-phase trigger (all three: K10+ cross-process syscall851 RPC), and a "Why it's OK for K9" justification.852 853**16.5 K10 surface proposal**854 855K10 single-axis: **terminal / pty + line discipline + job856control + initcall dispatcher**.857 858Specifically:859 8601. ``pty(7)`` master/slave pair allocation on a per-tty861 basis. The pty master exposes a HardwareJS-side stream;862 the slave is a regular fd inherited via fork+exec.863 8642. Line-discipline state: ECHO / ICANON / ISIG / etc. The865 K9-C1 stdinChannel becomes the input source; line-mode866 buffers up to a newline before delivering to user867 read(0, ...). Raw mode delivers each byte immediately868 (bash readline needs this).869 8703. Controlling-tty wiring: ``ioctl(TIOCSCTTY)``,871 foreground-pgrp tracking, SIGTTIN/SIGTTOU on background872 reads/writes. Builds on K9-C2's processGroupRegistry.873 8744. Initcall dispatcher: post-link Python pass that walks875 the kernel's ``.initcall*.init`` sections and emits real876 ``__initcall*_start`` / ``__initcall*_end`` boundary877 symbols (closes the longstanding §15 row tracking878 wasm-ld's lack of GNU linker script support for these879 anchors). Once ``do_initcalls()`` runs, every subsystem880 that registers via ``early_initcall()`` /881 ``subsys_initcall()`` / ``device_initcall()`` becomes882 live.883 8845. Cross-process syscall RPC infrastructure (NOT strictly885 K10 scope — would be K11 — but we keep an eye on it886 because retiring R-K8-1 + R-K8-2 + R-K9-1/2/3 hangs on887 it).888 889**Refutation budget for K10: ≤ 2.**890 891Reasoning per §20.9:892 893* K9 actual: 1. K8 was 0. Trajectory holding favorable.894* Surface novelty: terminal/pty is a single conceptually895 unified subsystem (line discipline IS a tty thing), but896 the initcall dispatcher is genuinely a separate axis.897 The two coupled together as one K-phase is a stretch but898 acceptable: both are "infrastructure for D1 bash" with899 the initcall dispatcher serving as the unblock for any900 upstream subsystem that registers via initcall (devtmpfs,901 inotify, etc.).902* Single-axis discipline: marginal. K10 is "shell can run903 with a real terminal," with the initcall dispatcher as a904 prerequisite that happens to fit into the same phase.905 906≤ 2 is the right budget for K10 given novelty + axis907coupling.908 909--------------------------------------------------------------------------------910Appendix A — K9 close checklist911--------------------------------------------------------------------------------912 913[x] K9-A1 probes pass914[x] K9-A2 source-site snapshot landed915[x] ~~patches/0009-exit-add-arch_notify_child_exit-hook.patch landed~~916 REFUTED (R-K9-1): kernel-side hook deferred; JS-direct917 childExitRegistry implements wait4 in dispatchSyscall.918[x] ~~arch/wasm32/include/asm/process.h with cross-user-mem helpers~~919 REFUTED (R-K9-1): cross-process copy is JS-side920 Uint8Array.set between SAB-backed userMemory views; no921 kernel-side helpers needed.922[x] ~~arch/wasm32/kernel/process.c::arch_notify_child_exit implemented~~923 REFUTED (R-K9-1): see above.924[x] ~~hardwarejs/src/exitNotificationRing.ts (new) wires user-Worker925 exit through to kernel~~926 REFUTED (R-K9-1): childExitRegistry.recordChildExit is927 called directly from the user-Worker exit message handler928 in spawnForkWorker; no ring needed.929[x] ~~Cross-process copy_to_user_for / copy_from_user_for working930 against fork-mode Worker pairs~~931 REFUTED (R-K9-1): folded into pipeRegistry's JS-side932 ring buffer + fdTableRegistry's fd lookup.933[x] pipe/pipefs interaction verified — folded into JS-side934 pipeRegistry; hello-k9-pipe.c demo exercises the935 full pipe + dup2 + read + writev + close cycle.936[x] hardwarejs/src/stdinChannel.ts (new) with per-Worker queue937[x] Per-Worker stdin region allocated and wired through938 hardwarejs/src/worker.mjs read(0, ...) handler — folded939 into dispatchSyscall's fd-aware __NR_read path; the940 user Worker's read syscall awaits stdinChannel.read.941[x] BootKernelOptions.stdinChannel? added; default no-op channel942 provides EOF on read (K5/K6 regression-safe) — supplied943 via opts.stdinChannel; K8-and-earlier callers without it944 get fd=0=EOF (verified via the no-channel sub-test).945[x] hello-k9-wait / hello-k9-pipe / hello-k9-stdin / hello-k9-pgrp946 demos + vitest tests947[x] Process-group primitives smoke-tested (setpgid, getpgrp, setsid,948 kill -pgid)949[x] K5-C2, K6-C2, K7-U4f, K8-C1-C3 regressions all green950[x] make hwjs-verify clean951[x] K9 close ledger in §16 (above)952[x] Refutation budget: actual ≤ 2 (1 / ≤ 2)953[x] K10 surface proposal in K9 close ledger §16.5954[x] v0.1.0-rc10 annotated tag placed955 956--------------------------------------------------------------------------------957History958--------------------------------------------------------------------------------959 960K9 OPENER (rc8 → rc9 era)961 Document created. Framework sections (§1–§14) and Appendix A962 defined. Implementation sequence pinned. Budget ≤ 2 declared.963 964K9 CLOSE (this commit, rc10)965 All sub-phases A1..D2 landed. Cumulative refutation count:966 1 (R-K9-1, absorbing four originally-specced kernel-side967 sub-systems into a single JS-direct ledger row). Two new968 §15 rows (R-K9-2, R-K9-3) introduced; 0 prior rows retired.969 K10 declared with surface = terminal/pty + initcall970 dispatcher; budget ≤ 2.971