.. SPDX-License-Identifier: GPL-2.0

================================================================================
Syscall path model — per-Worker vmlinux + real SMP (K11 opener)
================================================================================

:Status: K11 OPEN. Spec only; no code lands until this doc is closed.

This document is the K11 opener. It specifies the per-Worker vmlinux
instantiation contract, the in-Worker ``linux.syscall`` path, the
``wasm_syscall`` entry sequence (kernel-stack swap), the
``sys_call_table`` population strategy, the ``arch_cpu_up`` /
``arch_cpu_down`` hooks tied to spawn / exit, and the new
``num_vcpu`` / ``max_wasm_instances`` JS-side parameters that the
phase introduces.

The K11 axis is "the in-Worker syscall path comes online." K11 is the
phase that retires R-K8-1, R-K8-2, R-K9-1, R-K9-2, R-K9-3 (every
JS-direct shortcut row whose retirement signal was wrongly written as
"K10+ when cross-process syscall RPC lands" — per
``docs/ARCHITECTURE.md`` §4 there is no RPC, the kernel runs IN the
Worker; the retirement gate is now). It also inverts R-K10-W1 (the
"vmlinux is single-Worker" assertion that pinned the K5–K9 shortcut
state, not the architecture).

K11 lands BEFORE pty / line-discipline / job-control work (the
original K10-B/C/D scope, now relabelled K12). The investigation
that surfaced the architectural correction is recorded in §17 of
this document; it was a §20.6.1 dependency-check failure on prior
advice — the K10-A2b commit cited "vmlinux is single-Worker by
architecture" without verifying against
``docs/ARCHITECTURE.md`` §3 line 124 and ``memory-model.rst`` §1.1,
both of which specify per-Worker vmlinux instantiation.

Reading order (load-bearing, do not re-order):

1. §2 architectural ground truth (citations to the spec sites)
2. §3 the K5–K9 shortcut state being retired
3. §4 per-Worker vmlinux instantiation contract
4. §5 ``linux.syscall`` import resolution
5. §6 ``wasm_syscall`` entry sequence
6. §7 ``sys_call_table`` population strategy
7. §8 ``arch_cpu_up`` / ``arch_cpu_down`` hooks
8. §9 ``num_vcpu`` parameter
9. §10 ``max_wasm_instances`` parameter
10. §11 R-K10-W1 inversion plan
11. §12 R-K8-1 / R-K8-2 / R-K9-1 / R-K9-2 / R-K9-3 retirement plan
12. §13 K11 sub-phase decomposition
13. §14 refutation budget
14. §15 acceptance criteria
15. §16 K10 close ledger amendment + scope correction proposal
16. §17 §20.6.1 retroactive dependency-check
17. Appendix A — K11 close checklist

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

The K11 axis is structurally different from K10's terminal axis. K10
landed the kernel-side enabling machinery (the initcall dispatcher);
K11 lands the user-process syscall path that lets user binaries
actually reach the kernel-side subsystems registered by initcalls.
K12 (= original K10-B/C/D) lands the pty / line-discipline /
job-control surface on top of the K11 syscall path.

The K11 work was originally folded into K10's umbrella under the
implicit assumption that pty / line-discipline / job-control could
ride the existing JS-direct syscall surface (the R-K9-1 pattern).
The K10-B1 architectural-fork investigation surfaced that
assumption as wrong against the architecture: the architecture
specifies an in-Worker ``wasm_syscall`` path through a per-Worker
vmlinux instance, not a JS-direct or RPC-based path. K10-B1's
"how do pty syscalls reach the kernel" question presumes a working
syscall path that does not yet exist; the path itself is what K11
lands.

A separate opener doc is warranted because:

* The phase's surface novelty is high (the in-Worker syscall path
  has zero production code today; ``sys_call_table`` is empty;
  ``wasm_syscall`` returns ``-ENOSYS``; ``arch_cpu_up`` / ``down``
  do not exist).

* The spec-vs-implementation drift surfaced by R-K10-W1 needs an
  explicit retroactive dependency-check (§17) — a §20.6.1 artefact
  that lives best in the doc that lands the correction.

* Two new JS-side parameters (``num_vcpu``, ``max_wasm_instances``)
  introduce user-visible knobs that need spec text before any
  implementation lands — the K-phase pattern landed at K7 / K8 / K9.

* The phase retires five existing §15 rows (R-K8-1 through R-K9-3)
  whose retirement signals were wrong-shaped; the close ledger
  needs to record both the correction and the actual close.

--------------------------------------------------------------------------------
2. Architectural ground truth
--------------------------------------------------------------------------------

Three load-bearing facts from the spec, with citations:

**(F1) Per-Worker vmlinux instantiation.** Every Worker — every
process Worker AND the dedicated vmlinux Worker — holds its own
``WebAssembly.Instance`` of ``vmlinux.wasm``. They share what they
import (``env.kernel_memory``); they do not share their wasm
instance, ``__stack_pointer`` global, Asyncify save region, or
non-shared globals.

  *Authority:*

  * ``docs/ARCHITECTURE.md`` §3 line 124 (boot flow): "A new
    instance of ``vmlinux.wasm`` is *also* instantiated in this
    Worker (so syscalls can be serviced in-Worker against the
    kernel's address space), importing both ``env.kernel_memory``
    and this Worker's ``env.user_memory``."

  * ``linux-wasm/Documentation/wasm/memory-model.rst`` §1.1: "Every
    Worker — every process Worker and the dedicated vmlinux Worker
    — holds its **own** ``WebAssembly.Instance`` of
    ``vmlinux.wasm``."

**(F2) In-Worker syscall path; no RPC; no SAB-queue cross-Worker
hop.** ``linux.syscall`` (the user-Worker import) is provided by
HardwareJS as a JS function that calls THIS WORKER'S
``vmlinux.exports.wasm_syscall(nr, a0..a5)``. The kernel runs IN
the process Worker against shared ``env.kernel_memory``.

  *Authority:*

  * ``docs/ARCHITECTURE.md`` §4 lines 178–186 (How
    ``linux.syscall`` is handled in a process Worker): "...
    forwards the call into ``vmlinux.wasm``'s exported
    ``wasm_syscall(nr, ...)``. That export, when invoked from
    this Worker's JS, runs ``do_syscall()`` in kernel mode
    against shared ``memory 1``. The kernel sees the call as if
    it came from the process's CPU."

  * ``linux-wasm/Documentation/wasm/memory-model.rst`` §1.2 step 2:
    "HardwareJS calls the kernel instance's exported
    ``wasm_syscall(nr, a0..a5)``. **This crosses no Worker
    boundary**; the kernel instance lives in the same Worker as
    the userspace instance." (emphasis added in this opener for
    K11 reading)

**(F3) Real SMP under load.** Workers concurrently mutate
``env.kernel_memory``. Per-CPU variables, spinlocks, RCU, IPIs
(implemented as targeted ``Atomics.notify`` on a per-CPU wake
word) all carry their full upstream semantics.
``num_online_cpus()`` equals the configured maximum number of
concurrent process Workers (plus one for the vmlinux Worker
itself). Each Worker has its own ``raw_smp_processor_id()``;
percpu accessors land in distinct slots.

  *Authority:*

  * ``docs/ARCHITECTURE.md`` §17.6: "Workers really do
    concurrently mutate ``env.kernel_memory``. Per-CPU
    variables, spinlocks, RCU, and IPIs ... all carry their full
    upstream semantics."

  * ``linux-wasm/Documentation/wasm/mm-model.rst`` §0(c) row 3:
    ``CONFIG_SMP=y``, ``NR_CPUS=8`` (compile-time cap).

**Spec inconsistency to fix as part of this phase.**
``docs/ARCHITECTURE.md`` §10 currently says ``CONFIG_SMP=n`` (the
"we fake SMP at the JS layer" sentence). That contradicts §17.6 +
§2 + ``mm-model.rst`` §0(c). §10 is stale text from before §17.6
landed; the build has been ``CONFIG_SMP=y`` since K3's percpu work.
§10 must be amended as part of this opener's docs commit.

The three facts are not negotiable; they are the architecture. K11
exists to make the implementation match.

--------------------------------------------------------------------------------
3. The K5–K9 shortcut state being retired
--------------------------------------------------------------------------------

The K5–K9 implementation departed from the architecture in one
load-bearing way: every cross-process syscall is JS-direct. The
state today, by §15 row:

R-K8-1
  ``SYS_rt_sigaction`` stub returns 0 success without populating
  kernel-side ``current->sighand``. Disposition tracked in user-side
  ``__wasm_sigaction_table``. Stated retirement signal: "K9+ when
  cross-process syscalls route through ``vmlinux.exports.wasm_syscall``
  and the kernel's signal-delivery decisions actually consult
  ``current->sighand``." K11 IS that retirement.

R-K8-2
  K8 signal delivery via JS-direct stamp. ``SYS_kill`` / ``SYS_tkill``
  / ``SYS_tgkill`` resolve target tid in ``dispatchSyscall`` and call
  ``deliverSignalToTarget`` directly, bypassing kernel-side
  ``__send_signal_locked`` → ``arch_signal_wake_target`` → ring →
  consumer chain. Stated retirement signal: "K9+ when cross-process
  syscall RPC lands." That phrasing is wrong-shaped per F2 (no RPC;
  in-Worker call). K11 lands the in-Worker path; R-K8-2 retires.

R-K9-1
  Cross-process I/O via JS-direct registries (single ledger row
  absorbing wait4 / pipe / stdin / pgrp). Stated retirement signal:
  "K10+ when cross-process syscall RPC lands — same retirement
  signal as R-K8-1 + R-K8-2." Same wrong-shaped phrasing.

R-K9-2
  K9 wasm32 extension syscalls ``__NR_wasm_spawn_image`` (1024) +
  ``__NR_wasm_spawn_image_actions`` (1025). Stated retirement signal:
  "K10+ when ``posix_spawn(2)`` routes through proper kernel-side
  ``do_fork`` + ``do_execve``, which itself depends on the
  cross-process syscall RPC same as R-K8-1 / R-K8-2 / R-K9-1." Two
  retirements composed: the wrong-shaped retirement of R-K8-* / R-K9-1
  (now corrected to K11 in-Worker path), and the standard
  ``posix_spawn`` wrapper landing (which depends on K11's in-Worker
  ``do_fork`` + ``do_execve``).

R-K9-3
  K9 signal-termination encoding shortcut (``WIFEXITED+WEXITSTATUS=
  128+sig`` instead of ``WIFSIGNALED+WTERMSIG=sig``). Stated
  retirement signal: "K10+ when proper signal-termination encoding
  lands ... requires the K10+ cross-process syscall RPC + R-K8-1's
  ``current->sighand`` retirement." Composed retirement same as R-K9-2.

Five rows, one root cause, one corrected retirement gate. K11 close
retires all five. The retirement is mechanical:

* R-K8-1 retires when ``__NR_rt_sigaction`` reaches kernel-side
  ``do_sigaction`` via the in-Worker ``wasm_syscall`` path.

* R-K8-2 retires when ``__NR_kill`` / ``__NR_tkill`` / ``__NR_tgkill``
  reach kernel-side ``sys_kill`` / ``sys_tkill`` / ``sys_tgkill``.

* R-K9-1 retires when ``__NR_wait4`` / ``__NR_pipe2`` / ``__NR_dup2``
  / ``__NR_close`` / fd-aware ``__NR_read`` / ``__NR_write`` /
  ``__NR_writev`` / ``__NR_setpgid`` / ``__NR_setsid`` / ``__NR_getpgid``
  / ``__NR_getpgrp`` / ``__NR_getsid`` / ``__NR_getpid`` / ``__NR_getppid``
  reach kernel-side handlers. The five JS-direct registries
  (``childExitRegistry``, ``pipeRegistry``, ``fdTableRegistry``,
  ``stdinChannel``, ``processGroupRegistry``) demote to per-Worker
  caches OR are deleted.

* R-K9-2 retires after R-K9-1 by switching musl wrappers to call
  ``posix_spawn(2)`` against kernel-side ``do_fork`` + ``do_execve``;
  the wasm32-specific ``__NR_wasm_spawn_image{,_actions}`` syscall
  numbers stay as fast-path or get deprecated.

* R-K9-3 retires after R-K8-1 by routing the user-Worker exit
  message through kernel-side signal-delivery; the ``task_struct``'s
  ``exit_code`` field carries the terminating signal per upstream
  semantics.

--------------------------------------------------------------------------------
4. Per-Worker vmlinux instantiation contract
--------------------------------------------------------------------------------

This section codifies §F1 (per-Worker vmlinux) as a worker.mjs /
worker-browser.mjs amendment.

4.1. Worker classes (taxonomy)
------------------------------

Two valid Worker classes after K11:

vmlinux Worker
  The dedicated Worker that runs kthreads. Already exists today
  (``hardwarejs/src/kernelWorker.mjs`` / ``kernelWorker-node.mjs``).
  Holds one ``WebAssembly.Instance`` of ``vmlinux.wasm``. Imports
  ``env.kernel_memory`` (shared) + a 1-page ``shared:true`` sentinel
  Memory bound to ``env.user_memory``. CPU number 0
  (``raw_smp_processor_id() == 0``).

process Worker
  Any Worker hosting a userspace process. After K11, holds TWO
  ``WebAssembly.Instance``\ s: the userspace ``.wasm`` AND a
  per-Worker instance of ``vmlinux.wasm``. The userspace instance
  imports ``env.kernel_memory`` (shared) + the Worker's own
  ``env.user_memory`` (shared, per-Worker). The kernel instance
  imports the same ``env.kernel_memory`` + the same
  ``env.user_memory`` — so kernel code running on behalf of this
  process can ``memory.copy`` across the user/kernel boundary
  using THIS Worker's user memory. CPU number assigned at spawn
  (1 ≤ ``raw_smp_processor_id()`` < ``num_vcpu``); recycled on
  exit (§8).

Invalid Worker classes (caught at instantiation time):

* A Worker holding two ``vmlinux.wasm`` instances. Asserted by
  the K10-A4 ``kernelInstanceCount`` runtime check (which inverts
  meaning at K11 close — see §11).

* A process Worker without a ``vmlinux.wasm`` instance. Asserted
  by the K11 source-static guard test (``hardwarejs/test/k11-*-
  per-worker-vmlinux.test.ts``, replacing the K10-A4 single-
  Worker-invariant test).

4.2. Spawn-time amendment to worker.mjs
---------------------------------------

Today, ``hardwarejs/src/worker.mjs`` (the process-Worker entry
point) instantiates the userspace ``.wasm`` and provides
``linux.syscall`` as a JS function that calls
``kernelSpawnHandler.dispatchSyscall(...)`` (the K5–K9
JS-direct path). After K11:

1. Receive ``boot`` message from main thread carrying:
   ``{ wasmBytes, kernelWasmBytes, kernel_memory, user_memory,
   cpu_id, ... }``.

2. Instantiate ``vmlinux.wasm`` first (the kernel instance for
   this Worker), with imports::

     {
       env: {
         kernel_memory,    // shared, system-wide
         user_memory,      // shared, per-Worker (THIS Worker's)
         memory: kernel_memory,   // legacy default-memory binding
         ...
       },
       linux: {
         dmesg, hlt, cpu_relax, atomic_wait_i32, atomic_notify, ...
       }
     }

3. Instantiate the userspace ``.wasm`` second, with imports::

     {
       env: {
         kernel_memory,
         user_memory,
         memory: user_memory,
         ...
       },
       linux: {
         syscall: linuxSyscallShim,  // wraps vmlinux.exports.wasm_syscall
         ...
       }
     }

   The ``linuxSyscallShim`` is a thin JS function that closes over
   the kernel instance's exports::

     function linuxSyscallShim(nr, a0, a1, a2, a3, a4, a5) {
       return kernelInstance.exports.wasm_syscall(
         nr, a0, a1, a2, a3, a4, a5);
     }

   Optional fast-path: a tiny set of pure-JS syscalls
   (``gettimeofday``, ``clock_gettime``, ``getrandom``) MAY be
   answered directly without dispatching through the kernel
   instance, as a latency optimisation. The shim defaults to
   "always forward" per ``ARCHITECTURE.md`` §4 step 3.

4. Run the userspace instance's ``_start`` (or the K7 / K9 thread /
   spawn entry equivalents).

The ``boot`` message gets a new field ``kernelWasmBytes`` carrying
the same ``vmlinux.wasm`` bytes that the kernel Worker received.
The bytes are identical across all Workers; HardwareJS may
``WebAssembly.compile`` once and pass the resulting
``WebAssembly.Module`` instead of bytes (an optimisation tracked
as a §15-shape note, not a contract).

4.3. ``worker-browser.mjs`` parity
----------------------------------

The browser-side process-Worker entry point gets the same
amendment. Browser-engine behaviour for two-instance Workers is
the same as Node's (V8 is the engine in both cases via Node and
Chromium); the per-Worker resource cost of two instances is the
load-bearing risk surface, captured in §10
(``max_wasm_instances``).

4.4. Exec / fork shape
----------------------

The K11 fork / exec changes are mechanical:

* ``linux.spawn_worker`` (the existing K5/K6 import that
  HardwareJS implements as "create a Worker and load this
  ``.wasm``") additionally ships the kernel ``vmlinux.wasm`` bytes
  to the new Worker. Spawn order: kernel instance first, then
  user instance.

* ``linux.fork_worker`` (the K9 spawn-with-fd-actions variant)
  ships the same. Cost: the new Worker holds 2 instances, so
  ``max_wasm_instances`` accounting (§10) charges 2.

* ``linux.exec_worker`` (the K6 binfmt_wasm path) terminates the
  caller and spawns a fresh Worker. The fresh Worker spawns
  with the kernel instance + the new user instance — total 2.

* The vmlinux Worker remains untouched (1 instance).

--------------------------------------------------------------------------------
5. ``linux.syscall`` import resolution in process Workers
--------------------------------------------------------------------------------

§F2 specifies the in-Worker call shape. This section pins the JS
shim contract and the failure-mode taxonomy.

5.1. Shim contract
------------------

The ``linux.syscall`` import in a process Worker resolves to a JS
function with signature::

    (nr, a0, a1, a2, a3, a4, a5) -> i64

The function MUST:

1. Call ``kernelInstance.exports.wasm_syscall(nr, a0, a1, a2, a3,
   a4, a5)`` (the per-Worker kernel instance's exported entry).

2. Return the result as ``i64``.

3. NOT post a message to any other Worker. NOT call
   ``Atomics.wait`` on a SAB queue. NOT marshal arguments through
   any cross-Worker buffer. The kernel runs in this Worker; the
   call is a normal wasm-import-out-and-back-in JS hop.

The shim MAY (optimisation, not contract):

* Short-circuit a known-pure-JS syscall list before calling
  ``wasm_syscall``. The list is documented in
  ``arch/wasm32/include/asm/syscall_shortcuts.h`` (new file at
  K11 implementation time). Examples: ``__NR_gettimeofday``,
  ``__NR_clock_gettime`` (when not requesting CLOCK_MONOTONIC_RAW),
  ``__NR_getrandom`` (small reads). Each shortcut requires a
  matching ``arch/wasm32/kernel/syscall_shortcut_<nr>.c`` file
  documenting the short-circuit's safety argument: same return
  semantics as the kernel-side syscall under all flag
  combinations the spec exposes.

5.2. Failure modes
------------------

If ``kernelInstance`` is not yet bound when ``linux.syscall``
fires (e.g., a syscall before ``__wasm_call_ctors`` finishes —
should not happen but the assertion is cheap), the shim throws a
``WasmRuntimeError`` with a clear "syscall before kernel
instance bound" message. The userspace Worker traps; HardwareJS
records the trap and surfaces it as a kernel-trap terminal in
the boot result.

If ``wasm_syscall`` itself throws (e.g., a guest C bug inside
``do_syscall``, an Asyncify state corruption, a wasm OOB on the
shared kernel memory), the shim does NOT swallow the throw — it
propagates as a userspace trap. The userspace Worker terminates;
HardwareJS reaps via the existing K8/K9 exit machinery.

5.3. Why a JS shim and not direct ``call_indirect``?
----------------------------------------------------

``ARCHITECTURE.md`` §4 already explains this:

  "Why doesn't the process Worker just ``call_indirect`` into
  kernel code directly? Because each Worker has its own WASM
  module instance; modules can't share a function table cheaply
  across instances. Calling out to JS and then back into the
  kernel module is structurally how WASM 'links' across
  instances, and JS is also the natural place for the dispatch
  table to live."

The K11 design preserves that framing.

--------------------------------------------------------------------------------
6. ``wasm_syscall`` entry sequence (kernel-stack swap)
--------------------------------------------------------------------------------

This section concretises ``memory-model.rst`` §1.2 steps 3–5
into the C / wasm32 assembly that K11 implements in
``arch/wasm32/kernel/entry.c``.

6.1. Today
----------

``arch/wasm32/kernel/entry.c::wasm_syscall`` exists as a stub
returning ``-ENOSYS`` for every ``nr``. ``sys_call_table`` is
empty. This is the K5 placeholder; K11 fills it.

6.2. Required dance (per ``memory-model.rst`` §1.2)
---------------------------------------------------

A kernel stack is per ``task_struct``, slab-allocated out of
``env.kernel_memory`` (already true at K3 — kernel stacks are
allocated by ``copy_thread`` / ``alloc_thread_stack_node``).
The wasm runtime's notion of "the stack" is a single
``__stack_pointer`` global per instance; the kernel must swap
that pointer at every kernel-mode entry and exit.

The swap sequence::

    long wasm_syscall(unsigned long nr, long a0, long a1,
                      long a2, long a3, long a4, long a5)
    {
        unsigned long user_sp;
        unsigned long kern_sp;
        long ret;

        /* Step 1: read current's kernel stack base. */
        kern_sp = (unsigned long)current->stack + THREAD_SIZE;

        /* Step 2: swap __stack_pointer. */
        user_sp = __builtin_wasm_get_stack_pointer();
        __builtin_wasm_set_stack_pointer(kern_sp);

        /* Step 3: persist user_sp into per-task state. */
        current->thread.user_stack_pointer = user_sp;

        /* Step 4: dispatch. */
        ret = do_syscall(nr, a0, a1, a2, a3, a4, a5);

        /* Step 5: restore. */
        __builtin_wasm_set_stack_pointer(
            current->thread.user_stack_pointer);
        return ret;
    }

The pseudo-builtins ``__builtin_wasm_{get,set}_stack_pointer`` are
NOT actual clang builtins as of LLVM 22; they are stand-ins for
the real implementation, which must be inline wasm assembly in
``arch/wasm32/kernel/entry.S`` reading / writing the
``__stack_pointer`` global. Probe at K11-A1: confirm the assembly
syntax and that ``wasm-ld`` preserves the global access through
linking.

6.3. Stack-swap edge cases
--------------------------

Re-entrant syscall (kernel code calling another syscall from
within ``wasm_syscall``) is structurally impossible on wasm32:
there is no ``int 0x80``, kernel C code calls C functions
directly. ``current->thread.user_stack_pointer`` is single-slot
per task and only written on user-to-kernel entry.

Asyncify save / restore inside ``do_syscall`` (e.g., a
``schedule()`` triggered by a blocking syscall) saves the kernel
stack, the kernel ``__stack_pointer``, and the Asyncify state.
On resume, all three restore atomically. ``user_stack_pointer``
is in the task struct (kernel memory); it survives ``schedule``
unaffected.

Signal delivery on syscall return runs after step 5 (after
restore). The K8 ``__wasm_signal_dispatch`` trampoline runs in
USER mode against the user stack. K11 inherits this without
change.

6.4. Probe checklist (K11-A1)
-----------------------------

* ``__stack_pointer`` global access from inline assembly works.
* ``current->stack`` resolves to a non-zero kernel address from
  inside ``wasm_syscall`` running on a process Worker.
* ``THREAD_SIZE`` is the K4-bumped value (32 KiB, ``PAGE_SIZE <<
  THREAD_SIZE_ORDER`` with ``THREAD_SIZE_ORDER = 3``) pinned in
  ``arch/wasm32/include/asm/thread_info.h``. (DRIFT-CATCH at
  K11-A1: an earlier draft of this section claimed 16 KiB
  "per upstream wasm32 default"; this was a §20.5.3 candidate
  drift surfaced by the K11-A1 probe and corrected here. The
  K4 close ledger bumped from 16 KiB to 32 KiB after deep
  callchains overflowed under the printk → vsnprintf →
  console-driver path. Per ``thread_info.h`` rationale,
  per-task overhead is 32 KiB stack + 16 KiB Asyncify =
  48 KiB per kthread, matching ``sched-model.rst`` §3.3.)
* The kernel C compiler emits ``wasm_syscall`` with a body that
  the link-time post-pass leaves intact (no inlining surprises).
* ``wasm_syscall`` invoked directly with all-zero arguments
  returns ``-38`` (``-ENOSYS``) without trapping. The current
  K10-era stub (``arch/wasm32/kernel/entry.c``) returns this
  unconditionally; K11-A1 confirms the call shape works
  end-to-end before K11-A3 replaces the stub with the real
  stack-swap implementation.

--------------------------------------------------------------------------------
7. ``sys_call_table`` population strategy
--------------------------------------------------------------------------------

``arch/wasm32/kernel/syscall_table.c`` is empty today. Upstream
Linux populates the table per arch via
``arch/x86/entry/syscalls/syscall_64.tbl`` (or equivalent). The
K11 strategy is upstream-clean for syscalls that link, with an
explicit ``-ENOSYS`` stub list + §15 rows for those that do not.

7.1. Population mechanism
-------------------------

A new file ``arch/wasm32/kernel/syscall_table.c`` ``#include``\ s
the upstream ``arch/x86/entry/syscalls/syscall_64.tbl``-derived
generation via the existing ``asm/syscall.h`` machinery. Each
entry is a function pointer to ``sys_<name>`` (or
``__x64_sys_<name>``, depending on the upstream table format).

For each syscall:

* If ``sys_<name>`` links (the implementation file is in the
  build), the entry points at it.

* If ``sys_<name>`` does NOT link (filesystem subsystem disabled
  by Kconfig, syscall stubbed out, etc.), the entry points at a
  per-syscall ``-ENOSYS`` stub generated by macro expansion. Each
  ``-ENOSYS`` stub gets a §15-row treatment if the missing
  implementation is load-bearing for any K-phase ≤ K12; otherwise
  it is a one-line "this syscall is not provided on wasm32"
  comment in the table.

The K11 implementation walks the upstream table and lands the
stub set. The §15 row count for K11 is bounded by the number of
load-bearing missing implementations; per the K7/K8/K9 surface
enumeration this is expected to be small (most filesystem and
network syscalls are already disabled at Kconfig; what user
binaries actually call is a small subset). Probe at K11-A2:
exhaustively enumerate every ``__NR_*`` referenced by the K7 /
K8 / K9 demos + by musl wrappers used in K12, and confirm each
either links upstream-clean OR has a §15-row stub.

7.2. Per-syscall §15-row template
---------------------------------

For each missing implementation that is load-bearing:

::

    R-K11-N: SYS_<name> stubbed out (-ENOSYS) on wasm32
    DEPARTURE: <one-paragraph: what upstream does, what we
                 don't>
    AUTHORITY: <file:line in arch/wasm32/kernel/syscall_table.c>
    CARRIER:   <K-phase that needs this syscall to land for
                its acceptance to hold>
    RETIREMENT: <upstream code path that, once landed, makes
                 the stub unnecessary>

The template mirrors the existing §15 row shape from
``docs/ARCHITECTURE.md``.

7.3. Composite syscall fast-paths
---------------------------------

A small set of syscalls are answered by a wasm32-specific
implementation rather than the upstream one. K11 retains:

* ``__NR_clone`` → ``arch/wasm32/kernel/clone.c`` (the K7 thread /
  K9 fork machinery; calls upstream ``kernel_clone`` and then
  the wasm32-specific ``arch_dup_task_struct`` /
  ``copy_thread`` / spawn-ring submission).

* ``__NR_wasm_spawn_image`` (1024) + ``__NR_wasm_spawn_image_actions``
  (1025) → R-K9-2 fast-paths. K11 retains them as fast-paths;
  R-K9-2 retirement signal becomes "K12+ when ``posix_spawn``
  wraps ``do_fork`` + ``do_execve`` on the in-Worker syscall
  path."

* ``__NR_futex`` → R-K7-3 in-Worker dispatch (already
  implemented; no change).

The list is documented in
``arch/wasm32/include/asm/syscall_shortcuts.h`` (§5.1
short-circuit list, same file).

--------------------------------------------------------------------------------
8. ``arch_cpu_up`` / ``arch_cpu_down`` hooks
--------------------------------------------------------------------------------

§F3 specifies real SMP. K11 lands the dynamic CPU bring-up /
tear-down tied to process Worker spawn / exit.

8.1. Today
----------

The kernel boots ``CONFIG_SMP=y`` ``NR_CPUS=8`` (per
``mm-model.rst`` §0(c) row 3). Only CPU 0 is online (the
vmlinux Worker). ``cpu_possible_mask`` and ``cpu_present_mask``
are set at boot time (K3 / K4 work) but ``cpu_online_mask``
stays at ``{0}`` because no ``cpu_up`` is ever issued from the
arch.

8.2. Hooks
----------

Two new arch hooks added in K11 patches:

``arch_cpu_up(int cpu)``
  Called from ``hardwarejs/src/kernelSpawnHandler.ts`` after
  ``linux.spawn_worker`` (or ``fork_worker`` / ``exec_worker``)
  succeeds and the new Worker has acknowledged kernel-instance
  ready. Marks ``cpu`` as online (``set_cpu_online(cpu, true)``);
  initialises per-CPU state for ``cpu`` (the K3 percpu work
  already allocated the slots; this just brings the slot live);
  notifies any waiters via the existing CPU hotplug machinery.

``arch_cpu_down(int cpu)``
  Called from ``kernelSpawnHandler`` on Worker exit (after
  ``recordChildExit`` has been notified). Marks ``cpu`` as
  offline (``set_cpu_online(cpu, false)``); flushes any pending
  per-CPU work to other CPUs (the K3 / K4 work has the
  upstream-clean migration paths); recycles the CPU number for a
  future spawn (§9.3 numbering policy).

8.3. CPU numbering policy
-------------------------

CPU 0
  The vmlinux Worker. Set at boot, never reassigned. Hosts all
  kthreads.

CPUs 1 .. ``num_vcpu`` - 1
  Process Workers. Assigned at spawn time from a free list;
  recycled at exit. The free list is JS-side (in
  ``hardwarejs/src/cpuRegistry.ts``, new file at K11-B
  implementation time); ``arch_cpu_up(cpu)`` and
  ``arch_cpu_down(cpu)`` reflect the JS-side allocation back
  into ``cpu_online_mask``.

A process Worker's ``raw_smp_processor_id()`` returns its
assigned CPU number. Each Worker has its own
``__stack_pointer`` global, its own per-CPU slot for percpu
variables, its own runqueue tail (the K4 ``wasm32_rq``
machinery extended for SMP at K11-B).

8.4. ``smp_call_function`` / IPI
--------------------------------

IPIs are implemented as targeted ``Atomics.notify`` on a
per-CPU wake word in ``env.kernel_memory``. Already specified
in ``ARCHITECTURE.md`` §17.6: "IPIs (implemented as targeted
``Atomics.notify`` on a per-CPU wake word) all carry their full
upstream semantics."

K11 lands the per-CPU wake word allocation (``arch/wasm32/include/
asm/smp.h``) + the upstream-clean ``smp_call_function_*``
infrastructure on top.

--------------------------------------------------------------------------------
9. ``num_vcpu`` parameter
--------------------------------------------------------------------------------

The first new JS-side parameter K11 introduces.

9.1. Definition
---------------

``num_vcpu`` is a positive integer ≥ 2, ≤ ``NR_CPUS`` (= 8 at
K11). It is the maximum number of CPUs the kernel will see
online concurrently — including CPU 0 (the vmlinux Worker).

* CPU 0 is reserved for the vmlinux Worker. Always online.
* CPUs 1 .. ``num_vcpu`` - 1 are available for process Workers.
* No more than ``num_vcpu`` - 1 process Workers can run
  concurrently. Subsequent ``fork`` / ``spawn_worker`` requests
  block on the JS-side spawn queue (NOT on -EAGAIN — see §10
  for -EAGAIN semantics, which is a different cap on wasm
  instance count).

9.2. Caller surface
-------------------

User-chosen at ``HardwareJS.bootKernel({ num_vcpu: N })`` call
site. Default policy in the demo JS layer:
``min(navigator.hardwareConcurrency, NR_CPUS)``. Demo writers
SHOULD honour the default; explicit override is supported for
testing under low-CPU scenarios.

The parameter maps to the kernel cmdline as ``nr_cpus=N`` (the
upstream cmdline arg). ``setup_arch`` parses it; the
``cpu_possible_mask`` is set to ``{0..N-1}`` from
``setup_per_cpu_areas``.

9.3. Hard upper bound
---------------------

``NR_CPUS`` = 8 at K11 (per ``mm-model.rst`` §0(c) row 3
compile-time cap). Tunable in v0.2 by re-running
``setup_per_cpu_areas`` with a larger compile-time
``NR_CPUS``. Tracked as a §15-shape note in the K11 close
ledger.

9.4. Reachability
-----------------

If ``num_vcpu`` < 2 the kernel cannot fork (CPU 0 is reserved
for vmlinux; CPU 1 is the first process-Worker slot). The
boot fails with a clear ``panic("num_vcpu must be ≥ 2; got
%d")`` message.

--------------------------------------------------------------------------------
10. ``max_wasm_instances`` parameter
--------------------------------------------------------------------------------

The second new JS-side parameter. Browser-imposed cap on
simultaneously-instantiated wasm modules.

10.1. Why it exists
-------------------

Each ``WebAssembly.Instance`` consumes engine-internal
resources (compiled code cache, table allocations, memory
descriptor slots). Browser engines impose a per-tab or per-
process ceiling that varies by engine and version. Today's
HardwareJS treats this as latent — it does not probe or
account. K11 makes it explicit because the per-Worker vmlinux
contract DOUBLES the per-process-Worker instance count, and
the cap becomes load-bearing under high process counts.

10.2. Definition
----------------

``max_wasm_instances`` is a positive integer, the maximum number
of ``WebAssembly.Instance`` objects HardwareJS will hold
concurrently across all Workers it owns.

Cost accounting:

* vmlinux Worker = 1 instance.
* Each process Worker = 2 instances (user binary + per-Worker
  vmlinux).
* So the maximum number of concurrent process Workers under
  this cap is::

      max_concurrent_process_workers <=
        floor((max_wasm_instances - 1) / 2)

10.3. Caller surface
--------------------

User-chosen at ``HardwareJS.bootKernel({ max_wasm_instances: M
})`` call site.

Default policy: HardwareJS probes at ``bootKernel`` time. Probe
mechanism (K11-B implementation): instantiate a tiny "ping"
``vmlinux`` shape progressively until ``WebAssembly.instantiate``
or ``new WebAssembly.Module`` rejects with "too many wasm
instances" (engine-specific error message). The probe records
the empirical cap minus a safety margin (= 4 instances) and
sets ``max_wasm_instances`` to that. Per-engine known values
populate a fallback table for engines whose error message is
not matched.

10.4. Enforcement paths
-----------------------

Two enforcement paths, with different K-phase scopes:

(1) Hard cap (this phase, K11). At limit, refuse new
    ``fork()`` / ``spawn_worker`` / ``exec_worker`` with
    ``-EAGAIN`` (Linux-spec-correct under resource pressure;
    upstream ``do_fork`` returns ``-EAGAIN`` when
    ``max_threads`` is exceeded). The probe + accounting +
    ``-EAGAIN`` path lands at K11.

(2) Eviction (deferred, v0.2 or later). When at limit, find
    the oldest blocked-with-no-pending-signal process Worker;
    terminate the Worker (releases 2 instances); persist its
    ``env.user_memory`` SAB JS-side; mark its ``task_struct``
    as needs-rehydrate. On wake, JS rehydrates: a new Worker
    instantiates with the saved ``user_memory`` + a fresh
    ``vmlinux`` instance. §15 row "K11+ rehydration on
    instance pressure" deferred to v0.2; logged at K11 close
    with retirement signal "any user reports an EAGAIN under
    a workload that would have fit under eviction."

10.5. Interaction with ``num_vcpu``
-----------------------------------

The two parameters are independent caps:

* ``num_vcpu`` is the maximum number of CPUs the kernel sees
  online.

* ``max_wasm_instances`` is the maximum number of wasm
  instances HardwareJS holds, which (after dividing by 2 for
  the 2-instance-per-process-Worker cost) gives a separate
  cap on concurrent process Workers.

At runtime, the effective concurrent-process-Worker cap is::

    effective_cap = min(
      num_vcpu - 1,
      floor((max_wasm_instances - 1) / 2)
    )

The narrower of the two binds. If ``num_vcpu`` binds, fork()
blocks on the JS-side spawn queue; if ``max_wasm_instances``
binds, fork() returns ``-EAGAIN``. Demo writers SHOULD pick
``num_vcpu`` as the practical bottleneck (since it is bounded
by ``hardwareConcurrency``); ``max_wasm_instances`` is the
hard ceiling that protects against engine-side OOM.

--------------------------------------------------------------------------------
11. R-K10-W1 inversion plan
--------------------------------------------------------------------------------

R-K10-W1 (filed at K10-A4 close) records "K10-B active-segment
overlay departs from ``__wasm_init_memory``'s cmpxchg-once
invariant for ``vmlinux.wasm``'s ``.initcall*.init`` bytes. The
departure is currently moot because vmlinux is single-Worker by
architectural construction." The "single-Worker by architectural
construction" rationale is wrong against ``memory-model.rst``
§1.1 + ``ARCHITECTURE.md`` §3 line 124. The actual architecture
has vmlinux instantiated per Worker.

R-K10-W1 is NOT removed at K11. It inverts.

11.1. Why the K10-A3 bracket pass still works
---------------------------------------------

The K10-A3 Strategy B post-link bracket pass converts the
``.initcall*.init`` segments from passive to active. Per F1,
every Worker holds its own ``vmlinux.wasm`` instance and runs
its own ``__wasm_init_memory`` start function on instantiation.
Active segments write at instantiation time, not first-fault.

The original K10-A2b §11.2.1 rationale ("vmlinux is
single-Worker; only one ``__wasm_init_memory`` runs") is
incorrect. Multiple ``__wasm_init_memory`` runs DO happen. The
correct rationale is:

* ``.initcall*.init`` bytes are immutable, link-time-fixed
  function-pointer arrays (and their sentinels). They contain
  exactly the same bytes for every kernel instance because
  every kernel instance is the same ``vmlinux.wasm`` module.
* Per-Worker re-initialisation writes byte-identical values
  to the same kernel-memory locations. The writes are
  idempotent under the wasm memory model's relaxed-memory
  rules (concurrent writers writing the same byte are
  indistinguishable from a single writer).

Strategy B's choice does not change. The reason it works does.
This is a load-bearing rationale correction; the
``terminal-model.rst`` §11.2.1 section gets a HISTORICAL-NOTE
entry recording the original (refuted) rationale and the
corrected one.

11.2. Assertion inversion
-------------------------

The K10-A4 ``hardwarejs/src/kernelWorker.mjs``
``kernelInstanceCount`` runtime check ("vmlinux instantiated
exactly once per Worker; if a second instantiation is observed,
trip") AND the K10-A4 source-static guard test
(``hardwarejs/test/k10-a4-single-worker-invariant.test.ts`` —
"bootKernel.ts has exactly two createWorker construction sites;
no module under hardwarejs/src/ outside kernelWorker.mjs
instantiates ``vmlinux.wasm``") STAY in place during the K11
pre-implementation period. They pin the current pre-K11
shortcut state.

At K11 close, the assertions invert:

Old (K5–K10): "vmlinux is instantiated exactly once across all
Workers."

New (K11+): "vmlinux is instantiated exactly once PER WORKER
(no Worker holds two vmlinux instances). Valid Worker classes
are (a) the vmlinux Worker, holding 1 vmlinux instance + the
sentinel ``user_memory``; (b) any process Worker, holding 1
vmlinux instance + 1 userspace instance. The K11 source-static
guard test asserts every process Worker spawned via
``spawn_worker`` / ``fork_worker`` / ``exec_worker`` carries a
vmlinux instance."

11.3. R-K10-W1 status after K11 close
-------------------------------------

Marked CLOSED-AS-INVERTED in ``docs/ARCHITECTURE.md`` §15.3.
The row's text gets a HISTORICAL-NOTE prepended explaining:

* The original "single-Worker by architecture" mootness
  argument was wrong.
* The K10-A3 Strategy B implementation still holds for the
  corrected rationale (idempotent function-pointer arrays).
* The K10-A4 runtime + source-static assertions inverted at
  K11 close.

The row becomes a worked example for §20.6.1 (K-phase
dependency-checks): a row was filed without a
spec-vs-implementation drift check, the drift was caught one
K-phase later, and the correction landed cleanly because the
implementation choice (Strategy B) was independently correct.

--------------------------------------------------------------------------------
12. R-K8-* / R-K9-* retirement plan
--------------------------------------------------------------------------------

Each row's retirement is mechanical at K11 close:

R-K8-1 (kernel-side ``current->sighand`` not populated)
  Retires when ``__NR_rt_sigaction`` reaches kernel-side
  ``do_sigaction`` via the in-Worker ``wasm_syscall`` path. The
  user-side ``__wasm_sigaction_table`` either deletes (if
  kernel-side is now the source of truth) or demotes to a
  per-Worker fast-path cache that mirrors the kernel-side
  sighand. K11-B5 lands the change. Probe at K11-A4: every K8
  vitest still passes against the new path.

R-K8-2 (signal delivery via JS-direct stamp)
  Retires when ``__NR_kill`` / ``__NR_tkill`` / ``__NR_tgkill``
  reach kernel-side handlers. ``deliverSignalToTarget`` (the
  shared primitive) gets demoted from "JS-direct path" to "the
  kernel→HardwareJS signal-ring consumer's stamp helper". K11-B6
  lands the change.

R-K9-1 (cross-process I/O via JS-direct registries)
  Retires when ``__NR_wait4`` + the pipe / fd / stdin / pgrp
  syscalls reach kernel-side handlers. The five JS-direct
  registries get either deleted (if kernel-side is sufficient)
  or demoted to per-Worker caches. K11-C1 lands the change.

R-K9-2 (wasm32 extension syscalls 1024 / 1025)
  Partial retirement at K11: the standard ``posix_spawn(2)``
  wrappers route through kernel-side ``do_fork`` + ``do_execve``
  on the in-Worker syscall path; the wasm32-specific syscalls
  stay as fast-paths. Full retirement deferred to a K-phase
  that audits the fast-path's necessity.

R-K9-3 (signal-termination encoding shortcut)
  Retires after R-K8-1 by routing the user-Worker exit message
  through kernel-side signal-delivery; the ``task_struct``\ 's
  ``exit_code`` field carries the terminating signal. K11-C2
  lands the change.

The five rows go from "ACTIVE — wrong-shaped retirement signal"
to CLOSED at K11 close. ``docs/ARCHITECTURE.md`` §15 gets the
status update; the row text gains a closure note recording (a)
the wrong-shaped retirement signal in the original row, (b) the
K11 in-Worker path that was the actual retirement gate.

--------------------------------------------------------------------------------
13. K11 sub-phase decomposition
--------------------------------------------------------------------------------

Same shape as the K7 / K8 / K9 / K10 openers.

K11-A1
  Probes against the current kernel binary: confirm
  ``__stack_pointer`` global access from inline asm; confirm
  ``current->stack`` resolves on a process Worker; confirm
  ``THREAD_SIZE`` consistency; confirm ``wasm_syscall`` can be
  invoked from JS without crashing (today's stub returns
  -ENOSYS — make sure the call shape works end-to-end).

K11-A2
  Source-site snapshot. Enumerate every ``__NR_*`` call site
  the K7/K8/K9 demos hit + every musl wrapper used by K12
  pty/ldisc/jobctrl work. Confirm each links upstream-clean OR
  is on the explicit ``-ENOSYS`` stub list with a §15 row. Land
  ``syscall-path-model-source-snapshots.txt``.

K11-A3
  ``arch/wasm32/kernel/entry.c`` ``wasm_syscall`` real
  implementation (§6 stack-swap dance). Probe: a synthetic
  ``do_syscall(__NR_getpid)`` round-trip from a process Worker
  returns the correct pid. No actual ``sys_call_table``
  population yet; the probe uses a single-syscall test harness.

K11-A4
  Verify K11-A3 against a K7/K8/K9 regression sweep with the
  new entry sequence enabled but the JS-direct shortcuts still
  in place. (This is the analogue of K10-A4: the load-bearing
  machinery works against all prior K-phases without breaking
  any of them.)

K11-B1
  ``sys_call_table`` population. ``arch/wasm32/kernel/syscall_table.c``
  populated for the K7/K8/K9 demo + musl-wrapper surface. Per-syscall
  stub list + §15 rows. Probe: every ``__NR_*`` referenced by K7/K8/K9
  demos resolves (either to the upstream handler or to a stub
  with the recorded -ENOSYS).

  *K11-B1 close state (CLOSED at K11-B1 commit):*
  ~50 syscalls wired upstream-clean via ``w_sys_<name>`` 6-arg
  uniform wrappers (read, write, close, lseek, mmap, mprotect,
  munmap, brk, rt_sigaction, rt_sigprocmask, ioctl, readv, writev,
  pipe, sched_yield, dup, dup2, pause, nanosleep, getpid, clone,
  exit, wait4, kill, fcntl, getuid, getgid, geteuid, getegid,
  setpgid, getppid, getpgrp, setsid, getpgid, getsid,
  rt_sigtimedwait, rt_sigsuspend, gettid, tkill, set_tid_address,
  clock_gettime, clock_nanosleep, exit_group, tgkill, openat,
  set_robust_list, pipe2). Two arch-specific implementations
  (``sys_mmap`` + ``sys_nanosleep``) live in
  ``arch/wasm32/kernel/sys_wasm32.c`` because the upstream
  versions are gated on (CONFIG_64BIT) or are x86-specific
  (mmap byte-offset semantics).
  Three documented ``-ENOSYS`` slots, each with a §15 row in
  ``docs/ARCHITECTURE.md`` §15:
    - R-K11-1 (``__NR_arch_prctl``=158): wasm-threads TLS via
      ``__tls_base``; no x86-style FS/GS-base setup.
    - R-K11-2 (``__NR_rt_sigreturn``=15): wasm32 doesn't use the
      upstream signal-frame model; the K8 trampoline handles
      sigmask save/restore in JS shim state.
    - R-K11-3 (``__NR_fork``=57, ``__NR_vfork``=58): musl's
      wasm32 port maps fork/vfork to clone(SIGCHLD,...) at
      libc-level; ``__ARCH_WANT_SYS_FORK`` deliberately not
      defined in ``arch/wasm32/include/asm/unistd.h``.
  Out-of-bounds ``nr`` (>= 512) returns ``-ENOSYS`` via
  ``do_syscall``'s range check. Validated by
  ``arch/wasm32/kernel/k11_b1_probes.c`` (kernel-side smoke
  probe, gated by cmdline ``k11_b1_probes=1``) and
  ``hardwarejs/test/k11-b1-syscall-table.test.ts`` (vitest
  asserting 18/18 representative-spread probes pass + the
  three documented ``-ENOSYS`` slots return ``-ENOSYS``).

K11-B2
  ``arch_cpu_up`` + ``arch_cpu_down`` patches. Per-CPU wake
  word allocation. ``smp_call_function`` lights up for one
  process Worker.

  Decomposed at planning time (chosen for atomicity of the
  R-K10-W1 inversion landing site):

  K11-B2a — DONE
    Per-Worker ``vmlinux`` instantiation. Spawn handler
    accepts ``vmlinuxModule`` (precompiled
    ``WebAssembly.Module``) + ``kernelMemoryWasm`` (the
    shared-SAB-backed ``WebAssembly.Memory``). Process Workers
    instantiate ``vmlinux`` exactly once via
    ``instantiateProcessWorkerVmlinux`` (worker.mjs) against
    shared ``env.kernel_memory`` + own ``env.user_memory``,
    re-running ``__wasm_call_ctors`` for the K10-A3 active-
    overlay (idempotent: overlay re-writes byte-identical
    function-pointer arrays; cmpxchg-once gate skips
    everything else). The instance's ``wasm_syscall`` export
    is now reachable from inside the process Worker —
    architecturally preparing the K11-B5..C2 syscall flips
    without flipping any actual syscall paths yet (the
    SAB-RPC syscall path still owns every syscall as of
    K11-B2a). Soft-failure: if instantiation traps
    ``vmlinux-error`` is posted to the spawn handler and the
    user binary continues on the SAB-RPC path.

    ``processVmlinuxInstanceCount`` mirrors the K10-A4
    ``kernelInstanceCount`` invariant: each Worker (kernel or
    process) instantiates ``vmlinux`` exactly once. R-K10-W1
    is now PARTIALLY-INVERTED — the assertion has flipped
    from "exactly one Worker instantiates ``vmlinux``" to
    "every Worker instantiates ``vmlinux`` exactly once
    apiece," but the SAB-RPC syscall path remains live. Full
    inversion lands at K11-D2 when the SAB-RPC path is
    deleted.

    Witness: ``hardwarejs/test/k11-b2a-per-worker-vmlinux.test.ts``
    boots the K6-C2 reference initramfs (hello-k5 as
    ``/init``) with ``vmlinuxModule`` supplied to the spawn
    handler, asserts ``onVmlinuxReady`` fires for every
    spawned process Worker tid with ``hasWasmSyscall=true``,
    ``processVmlinuxInstanceCount === 1`` (count==1 per tid
    in the report map), the user binary still produces
    "hello from K5 userspace" (no perturbation), and no
    panic / oops appears in dmesg (second-instance ctors are
    idempotent on the K10-A3 overlay).

  K11-B2b — DONE (substrate-only; full SMP deferred per
  R-K11-W2)
    Per-Worker ``__wasm32_cpu_id`` wasm-Global substrate.
    ``arch/wasm32/lib/cpu_id.S`` defines the per-instance
    Global plus the ``__wasm32_get_cpu_id`` /
    ``__wasm32_set_cpu_id`` accessors; both are explicitly
    re-exported from ``vmlinux.wasm`` by
    ``scripts/link-vmlinux-wasm.sh`` (verified via
    ``wasm-dis`` at K11-B2b close). The kernel header
    ``arch/wasm32/include/asm/smp.h`` defines
    ``raw_smp_processor_id()`` to read the wasm-Global, so
    every kernel call site that reaches
    ``smp_processor_id()`` (e.g., ``current->cpu``,
    per-CPU-data accesses, percpu writes) now observes the
    correct per-Worker CPU id rather than a hardcoded 0.

    On the JS side ``hardwarejs/src/kernelSpawnHandler.ts``
    grew a ``cpuRegistry`` allocator: each spawned process
    Worker gets a unique cpu_id from ``[1, maxCpuId]``
    (``maxCpuId`` defaults to 7 = ``NR_CPUS - 1``; CPU 0 is
    reserved for the kernel Worker by convention). The
    cpu_id is shipped in the spawn ``init`` message; the
    Worker calls ``vmlinuxInstance.exports.__wasm32_set_cpu_id(cpuId)``
    after ``__wasm_call_ctors`` runs. Releasing the
    cpu_id is wired into the worker exit / error paths.

    What this **does not** include — explicitly deferred
    to a later phase under §15.3 R-K11-W2:

    - ``arch_cpu_up`` / ``arch_cpu_down`` boot hooks. The
      cpu_id substrate is enough for per-CPU semantics on
      Workers that already exist; bringing CPUs online /
      offline at boot under a kernel-driven flow is not on
      the K11-B critical path.
    - Per-CPU wake words in ``env.kernel_memory``. K8's
      single ``__sched_wakeup_word`` plus the per-tid
      signal-pending region remain the wake substrate
      through K11-D2.
    - IPI / ``smp_call_function`` cross-CPU dispatch. Not
      reached by any K11-targeted syscall (the K11-B5/B6
      and K11-C1/C2 retirements are signal + process-mgmt
      surfaces, not cross-CPU coordination); deferred to
      K12+ behind R-K11-W2.

    Acceptance gate ``hardwarejs/test/k11-b2b-cpu-id-substrate.test.ts``
    asserts (a) every process Worker spawned through the
    spawn handler reports a cpuId in [1, 7] via
    ``onVmlinuxReady``, (b) the wasm-Global round-trips
    ``__wasm32_set_cpu_id(N)`` ↔ ``__wasm32_get_cpu_id() == N``
    in a standalone instance, and (c) the user binary
    (hello-k5 ``/init``) still emits its expected stdout
    with no perturbation.

K11-B3 — DONE
  ``num_vcpu`` parameter wired end-to-end. ``BootKernelOptions.num_vcpu``
  resolves to ``min(navigator.hardwareConcurrency, NR_CPUS=8)``
  by default; the resolver enforces ``2 <= num_vcpu <= 8``
  and throws synchronously on out-of-range values BEFORE
  spawning any Worker. The resolved value is forwarded
  through three independent surfaces:

  * ``kernelWorker.mjs`` writes it as ``wasm_boot_info.num_cpus``
    in the boot prologue. ``arch/wasm32/kernel/setup.c::setup_arch``
    reads ``bi->num_cpus`` and calls ``set_cpu_possible(cpu, true)``
    for ``cpu`` in ``[1, num_cpus)``, expanding ``cpu_possible_mask``
    from the ``boot_cpu_init`` baseline (CPU 0 only) to ``{0..N-1}``.
    ``setup_nr_cpu_ids`` (called from ``start_kernel`` immediately
    after ``setup_arch``) then sizes ``nr_cpu_ids`` to ``N``,
    which is the cap that ``mm/percpu.c::setup_per_cpu_areas``
    and every ``for_each_possible_cpu()`` loop honours.

  * ``bootKernel.ts`` passes ``maxCpuId = num_vcpu - 1`` to the
    default-constructed ``makeKernelSpawnHandler``; the
    ``cpuRegistry`` allocator hands out cpu_ids from
    ``[1, maxCpuId]`` (CPU 0 reserved for the kernel Worker).

  Note: the ``nr_cpus=`` upstream cmdline argument is NOT
  wired. ``arch/wasm32/kernel/sections.c`` aliases
  ``__setup_start`` / ``__setup_end`` to a sentinel so the
  upstream ``__setup("nr_cpus=", nrcpus)`` handler in
  ``kernel/cpu.c`` is never dispatched. The boot_info
  ``num_cpus`` field is the single authoritative path for
  K11-B3; future cmdline-arg work falls under K12+ §15
  shape ``__setup-section-not-walked``.

  Acceptance gate ``hardwarejs/test/k11-b3-num-vcpu.test.ts``
  asserts (a) out-of-range / non-integer values throw
  synchronously, (b) ``num_vcpu=4`` dmesg shows
  ``cpu_possible_mask expanded to { 0 .. 3 }; HardwareJS
  intends 4 CPUs``, (c) the spawn handler hands out cpu_ids
  in ``[1, 3]`` for that boot, (d) the default policy
  produces a value in ``[2, 8]`` on Node.

  What this does NOT include (deferred):

  * Marking CPUs 1..N-1 ``present`` or ``online``. K11-B3
    only marks them ``possible``; transitioning to
    ``present`` / ``online`` is ``arch_cpu_up``'s job and is
    deferred behind R-K11-W2 (``arch_cpu_up`` lands when the
    pty / ldisc / job-control work surfaces a kernel call
    site that needs cross-CPU coordination).

K11-B4 — DONE
  ``max_wasm_instances`` probe + accounting + -EAGAIN
  enforcement.

  * Caller surface:
    ``BootKernelOptions.max_wasm_instances?: number``. When
    omitted, ``bootKernel`` resolves the value via
    ``probeMaxWasmInstances()`` — a fast probe that compiles a
    fixed batch of 16 minimal wasm modules to confirm the engine
    is healthy, then returns a per-engine fallback (V8/Node:
    4096; SpiderMonkey: 1024; generic: 256) minus a 4-instance
    safety margin. The full "compile until rejected" probe per
    §10.3 is deferred behind a §15-shape note: today's per-engine
    fallback table is empirically correct for V8 (the only
    engine HardwareJS targets at K11 close), and a 4096-iteration
    probe per boot would bloat regression-sweep time. The
    fallback table grows when a downstream user reports an
    -EAGAIN under a workload that breaks the assumption.

  * Constraint: ``max_wasm_instances >= 4`` (must fit at least
    1 vmlinux Worker + 1 process Worker + 1 instance of safety
    margin); below-bound throws synchronously BEFORE any Worker
    is spawned.

  * Accounting: ``hardwarejs/src/kernelSpawnHandler.ts``
    threads two synthetic hooks (``__wasmInstanceHooks``) into
    every spawn. ``reserveWasmInstances`` checks
    ``count + cost <= maxWasmInstances`` before allocating
    ``user_memory`` or any other resource; on violation, the
    handler returns ``-EAGAIN`` (errno 11) immediately.
    ``releaseWasmInstances`` runs in the Worker's exit / error /
    onError handlers so every successful reservation pairs with
    exactly one release. Per-spawn cost is 2 when
    ``vmlinuxModule`` is supplied (user binary + per-Worker
    vmlinux) and 1 otherwise (K5..K10 SAB-RPC fallback, retained
    for backward compatibility through K11-D2).

  * Acceptance gate
    ``hardwarejs/test/k11-b4-max-wasm-instances.test.ts``
    asserts (a) ``max_wasm_instances`` < 4 throws synchronously,
    (b) non-integer throws synchronously, (c) the default probe
    resolves successfully through ``bootKernel``, (d) issuing
    two concurrent fork-mode spawns with
    ``maxWasmInstances=4`` causes the second to return
    ``-EAGAIN``.

  Eviction (per §10.4(2)) remains deferred to v0.2 with the
  retirement signal "any user reports an EAGAIN under a workload
  that would have fit under eviction." Logged at K11 close as a
  §15 row.

K11-B5 — DONE
  R-K8-1 retirement: ``__NR_rt_sigaction`` reaches kernel
  ``do_sigaction``; user-side table demoted/deleted.

  Landing notes:

  * ``arch/wasm32/kernel/per_worker.c`` introduced. Exports
    ``__wasm32_get_init_task_addr()`` (returns ``&init_task`` as a
    ``u32`` env.kernel_memory offset) AND provides ``init_stack``
    as a plain ``THREAD_SIZE``-aligned static array. The latter is
    structural: upstream Linux defines ``init_stack`` via the GNU
    linker script (``include/asm-generic/vmlinux.lds.h``
    ``INIT_TASK_DATA``), which our ``wasm-ld`` build bypasses
    entirely (no GNU linker scripts on ``wasm-ld``). Without
    a C-side definition the symbol stays undefined and
    ``--allow-undefined`` zeroes out ``init_task.stack``,
    tripping the ``wasm_syscall`` ``!current->stack`` guard.
  * ``hardwarejs/src/worker.mjs`` ``instantiateProcessWorkerVmlinux``
    binds the per-Instance ``__wasm32_current_task`` global to
    ``&init_task`` post-ctors. Process Workers now have a non-NULL
    ``current`` for the in-Worker syscall path.
  * ``hardwarejs/src/worker.mjs`` ``syscall(13, ...)`` routes to
    ``vmlinuxInstance.exports.wasm_syscall(13, ...)`` (the K11-B5
    pilot). All other syscalls still take the K5-K9 JS-direct path
    until the K11-B6/C1/C2 sub-phases retire them.
  * ``hardwarejs/src/kernelSpawnHandler.ts`` adds an
    ``onWasmSyscallStats`` callback so vitests can assert "the
    syscall actually went through ``vmlinuxInstance.exports.wasm_syscall``."
  * ``hardwarejs/test/k11-b5-rt-sigaction-kernel.test.ts``: green.
    ``hello-k8-signals`` succeeds end-to-end with stats
    ``{[__NR_rt_sigaction]: 1}``, proving ``do_sigaction`` runs
    in the kernel via ``sys_rt_sigaction`` (R-K8-1 RETIRED for
    ``rt_sigaction``).

  K11-B5 §15 carry-over: ``R-K11-W3`` (process Workers share
  ``&init_task`` and its kernel stack). Single-process tests pass;
  multi-process sigaction-disposition isolation + concurrent
  syscall stack-safety are flagged for K11-B6/C1 retirement
  (per-process ``task_struct`` allocation OR per-Worker kernel
  stack region).

K11-B6 — DONE
  R-K8-2 retirement: ``__NR_kill`` / ``__NR_tkill`` /
  ``__NR_tgkill`` reach kernel handlers.

  Landing notes:

  * ``arch/wasm32/kernel/signal.c`` introduces three wasm32-specific
    arch shims: ``wasm32_sys_kill`` / ``wasm32_sys_tkill`` /
    ``wasm32_sys_tgkill``. Each validates args (sig in [0, _NSIG],
    target tid/pid > 0) and submits directly to the K8-B2 signal
    ring via ``wasm_signal_submit``, bypassing upstream ``sys_kill``'s
    ``find_task_by_vpid`` + ``send_sig_info`` + ``__send_signal_locked``
    path. The bypass is necessary because R-K11-W3 init_task-sharing
    means ``find_task_by_vpid(target_pid)`` would only find
    ``init_task`` for any process Worker pid; full upstream-clean
    retirement waits on per-process ``task_struct`` allocation
    (R-K11-W4 retirement signal).
  * ``arch/wasm32/kernel/syscall_table.c`` ``[62/200/234]`` now
    point at thin wrappers that forward to the wasm32 arch shims
    instead of upstream ``sys_kill`` / ``sys_tkill`` / ``sys_tgkill``.
  * ``hardwarejs/src/worker.mjs::syscall`` extends the K11-B5
    routing table: ``__NR_tkill`` (200) and ``__NR_tgkill`` (234)
    always route through ``vmlinuxInstance.exports.wasm_syscall``;
    ``__NR_kill`` (62) routes when ``pid > 0`` (negative-pid pgrp
    forms still go through SAB-RPC until R-K9-1 retires at K11-C1).
    For self-target kills (where the target tid/pid equals the
    calling Worker's own tid/pid) the worker does an
    ``Atomics.wait`` on its own ``wake_word`` after ``wasm_syscall``
    returns to give the K8-B2 ring consumer time to drain (timeout
    100ms). This makes ``raise(SIGUSR1)`` synchronous so the user
    binary sees the handler run before the ``raise()`` call returns.
  * ``arch/wasm32/kernel/k11_b1_probes.c::wasm32_run_k11_b1_probes``:
    the ``kill(-1, 0)`` probe now accepts ``-ENOSYS`` as a valid
    return (the wasm32 arch shim returns ``-ENOSYS`` for
    non-positive pid; the K11-B1 probe was originally written
    against upstream ``sys_kill`` semantics).
  * ``hardwarejs/test/k11-b6-kill-family-kernel.test.ts``: green.
    ``hello-k8-signals`` succeeds end-to-end with stats
    ``{[__NR_rt_sigaction]: 1, [__NR_tkill]: 1}`` AND a K8-B2 ring
    delivery for ``sig=SIGUSR1=10`` is observed.

  K11-B6 §15 carry-overs:

    * R-K11-W4 (FILED-AT-K11-B6): the wasm32 arch shims bypass
      upstream signal-delivery mechanics. Full upstream-clean
      retirement at K11-C1 (with R-K9-1 retirement) or K12+
      (jobctrl).
    * R-K8-2 status updated to PARTIAL-CLOSE-AT-K11-B6;
      FULL-CLOSE-DEFERRED-TO-K11-C1-OR-K12+.

K11-C1
  R-K9-1 retirement: ``__NR_wait4`` + pipe / fd / stdin / pgrp
  syscalls reach kernel handlers; JS-direct registries
  demoted/deleted. Decomposed into K11-C1.a..f.

K11-C1.a — DONE
  Per-process ``task_struct`` allocation. R-K11-W3 narrow
  retirement (process Workers no longer share ``&init_task``
  wholesale; ``files`` / ``signal`` / ``pending`` / ``stack``
  are now per-process; ``sighand`` stays shared). Stdin /
  process-group / pipe / dup2 / wait4 retirements come in
  K11-C1.b..f.

  Landing notes:

  * ``arch/wasm32/kernel/task_alloc.c`` introduced. Exports
    ``__wasm32_alloc_process_task(pid, ppid)`` which kmallocs
    a ``task_struct`` + ``THREAD_SIZE`` stack +
    ``dup_fd(init_files)`` + a kmalloc'd ``signal_struct``
    (memcpy from ``init_signals`` template, re-init
    head/list/wait fields). The new task is linked into
    ``init_task->children`` via ``sibling``. ``sighand`` stays
    pointing at ``init_sighand`` (R-K11-W3 narrow). PID hash
    registration via ``alloc_pid`` / ``attach_pid`` is skipped
    (R-K11-W5).
  * ``hardwarejs/src/worker.mjs`` ``instantiateProcessWorkerVmlinux``:
    binds ``current = &init_task`` (K11-B5 transitional), then
    calls ``__wasm32_alloc_process_task(pid, ppid)`` and re-binds
    ``current`` to the returned addr. The two-step bind is
    necessary because the alloc helper itself uses ``kmalloc``,
    which expects ``current`` valid for lockdep / RCU debug.
  * ``hardwarejs/src/kernelSpawnHandler.ts``: ``spawnForkWorker``
    and ``spawnThreadWorker`` now include ``ppid`` in the init
    payload (parent's pid, used by the alloc helper as
    informational only at K11-C1.a — the parent pointer goes
    to ``&init_task`` because ``find_task_by_vpid`` doesn't
    work yet under R-K11-W5).
  * ``hardwarejs/src/kernelSpawnHandler.ts``: ``onVmlinuxReady``
    info now carries ``initTaskAddr`` AND ``perProcessTaskAddr``
    so vitests can assert the K11-C1.a path actually ran (the
    two addrs must differ).
  * ``hardwarejs/test/k11-c1a-per-process-task-kernel.test.ts``:
    green. ``perProcessTaskAddr != initTaskAddr`` AND the
    ``hello-k8-signals`` demo still runs end-to-end (signal
    handler fires on the new task_struct, proving
    ``current->stack + THREAD_SIZE`` is a valid kernel-stack
    region for ``wasm_syscall``'s stack-swap).

  K11-C1.a §15 carry-overs:

    * R-K11-W3 status: ``FILED-AT-K11-B5; PARTIAL-CLOSE-AT-K11-C1.a;
      FULL-CLOSE-DEFERRED-TO-K12+`` (per-process ``sighand``
      allocation waits on execve POSIX semantics for ``sa_restorer``).
    * R-K11-W5 (FILED-AT-K11-C1.a): ``alloc_pid`` /
      ``attach_pid`` skipped; ``find_task_by_vpid(non_init_pid)``
      returns NULL. Cross-target ``kill`` / ``wait4`` /
      ``setpgid`` use wasm32 arch shims (R-K11-W4 + R-K11-W5)
      until ``init_pid_ns->child_reaper`` setup lands.

K11-C1.b — DONE
  ``__NR_wait4`` (61) routed through ``wasm_syscall`` via the
  wasm32-specific arch shim ``wasm32_sys_wait4``
  (``arch/wasm32/kernel/wait.c``). Walks ``current->children``
  for ``EXIT_ZOMBIE`` matches; reaps via ``reap_zombie`` which
  ``kfree``\ s the child's task_struct + stack + per-process
  files / signal allocations. Blocks on
  ``current->thread.wait_chldexit_wake`` via
  ``__builtin_wasm_memory_atomic_wait32`` (the wasm32
  cooperative scheduler has no preemptive yield that
  ``wait_event_interruptible`` could use).

  Child-side notification: ``__wasm32_notify_child_exit``
  (exported from ``wait.c``) is called from each process
  Worker's ``hardwarejs/src/worker.mjs`` SYS_EXIT_GROUP path
  BEFORE the SAB-RPC tear-down. Sets
  ``child->exit_state = EXIT_ZOMBIE``,
  ``child->exit_code``, atomic-increments + atomic-notifies
  the parent's ``wait_chldexit_wake`` word.

  Parent-child linkage: ``__wasm32_alloc_process_task`` walks
  ``init_task.children`` for the parent task (matching ppid),
  links the new child as a sibling of that parent's children
  list. For ``pid == 1`` (``/init``) the kernel_init kthread
  already exists with that pid (created by upstream
  ``rest_init`` -> ``kernel_thread``); the allocator's
  ``find_existing_task_by_pid`` dedup detects this and
  REUSEs the kthread's ``task_struct`` instead of
  double-allocating. Without this dedup,
  ``init_task.children`` would have two pid=1 entries
  and ``find_parent_by_ppid(1)`` for grandchildren would
  pick the wrong one.

  Spawn-await ordering: ``hardwarejs/src/kernelSpawnHandler.ts``
  ``spawnForkWorker`` now awaits the new Worker's
  ``vmlinux-ready`` message before returning from
  ``__NR_wasm_spawn_image`` to the parent. This ensures the
  child's allocator has linked the new ``task_struct`` into
  the parent's children list BEFORE the parent's subsequent
  ``waitpid`` runs — without the ordering guarantee, ``wait4``
  would walk an empty children list and return ``-ECHILD``
  before the child registers.

  Acceptance: ``hardwarejs/test/k11-c1b-wait4-kernel.test.ts``
  asserts (1) hello-k9-wait + hello-k9-child end-to-end with
  ``WEXITSTATUS == 42``; (2) parent's ``wasmSyscallStats``
  records nonzero ``__NR_wait4`` (61) routings; (3)
  ``reap_zombie`` and ``__wasm32_notify_child_exit``
  pr_info traces appear in process-Worker dmesg; (4) no
  panic.

  Carry-overs:

  - **R-K11-W6 (FILED-AT-K11-C1.b)** in ``docs/ARCHITECTURE.md``
    §15.3: arch-shim ``wait4`` instead of upstream ``do_wait4``.
    Retirement signal: K12+ when wait4 features (siginfo,
    rusage, waitid variants, signal-stop forms via WUNTRACED /
    WCONTINUED) actually matter for jobctrl tests.

  - **R-K9-1 (RETIRING-WAIT4-AT-K11-C1.b; FULL-CLOSE-AT-K11-C1.f
    cleanup-pass)**. ``__NR_wait4`` no longer falls through
    to the SAB-RPC ``childExitRegistry`` path when
    ``perProcessTaskAddr != 0`` and ``vmlinux.exports.wasm_syscall``
    is present. The K11-C1.f cleanup pass DELETES
    ``childExitRegistry.ts``.

  - **R-K11-W3 narrow** (init_task children sharing) reduced
    further: process Workers now have correct parent/child
    hierarchies via the dedup + ppid-lookup path. R-K11-W3
    closes fully when per-task ``sighand`` allocation lands
    (K12+).

K11-C1.c — DONE
  ``__NR_pipe`` (22) / ``__NR_pipe2`` (293) / ``__NR_dup2`` (33) /
  ``__NR_dup3`` (292) / ``__NR_close`` (3) routed through
  ``wasm_syscall``. Kernel-side ``fs/pipe.c`` ``pipe_buffer`` ring
  + ``files_struct`` (per-process now from K11-C1.a).
  ``pipeRegistry.ts`` + ``fdTableRegistry.ts`` retired in
  ``hardwarejs/src/worker.mjs``'s syscall predicate; physical
  deletion of the registry source files at K11-C1.f cleanup pass.

  Notes on what landed
  ~~~~~~~~~~~~~~~~~~~~

  - ``hardwarejs/src/worker.mjs``: predicate ``routeToWasmSyscall``
    extended to include ``__NR_PIPE``, ``__NR_PIPE2``, ``__NR_DUP2``,
    ``__NR_DUP3``, ``__NR_CLOSE``. musl's ``pipe(2)`` wrapper
    prefers ``SYS_pipe`` when defined; wasm32's musl
    (``arch/wasm32/bits/syscall.h.in``) defines both, so both must
    be in the predicate.

  - Demo: ``wackywasm-tools/tests/hello-k11-c1c-pipe.c``. Single
    process, no fork: ``pipe(fds)``, ``dup2(fds[1], 7)``, four
    closes (three valid + one already-closed). Asserts:

    1. ``pipe()`` returns rc=0 with two distinct fds (kernel
       allocates ``pipe_inode_info`` + installs both ends in
       ``current->files``).

    2. ``dup2(fds[1], 7) == 7`` (kernel installs the write-end at
       fd 7 by sharing the file ref).

    3. Three valid closes return 0; close on already-closed fd
       returns -1 with ``errno=EBADF (9)`` — proves the kernel
       actually mutated ``current->files``.

    4. ``wasmSyscallStats`` records nonzero ``__NR_pipe`` (22) +
       ``__NR_dup2`` (33) + ``__NR_close`` (3) — confirms all
       three routed via ``wasm_syscall`` rather than SAB-RPC.

  - Vitest: ``hardwarejs/test/k11-c1c-pipe-kernel.test.ts``.
    Observed at first green run: ``stats=1:{"3":4,"22":1,"33":1}``.

  Deferrals carried by K11-C1.c (NOT K11-C1.c blockers)
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  - **End-to-end pipe data flow** (``write`` to ``fds[1]`` →
    ``read`` from ``fds[0]``) requires ``__NR_read`` /
    ``__NR_write`` to also route through ``wasm_syscall``. That
    conflicts with K9-era stdin / stdout / stderr handling
    (JS-direct ``fdTableRegistry`` + ``onUserOutput``) where the
    kernel-side ``files_struct`` has no entry at fd 0/1/2.
    Resolution: K11-C1.f cleanup (when ALL syscalls route, ``fd
    0/1/2`` are kernel-side) plus a host import for console
    write, OR K12 pty surface that owns fd 0/1/2 properly.

  - **Cross-process pipe** (parent + child via K9-B3
    spawn-with-actions). K9-B3's spawn-actions apply
    ``DUP2``/``CLOSE`` on a clone of the parent's
    ``fdTableRegistry`` entry; with kernel-side
    ``files_struct``, the spawn-actions must read from the
    parent's ``current->files`` and write to the child's fresh
    ``files_struct`` via ``wasm_syscall`` — K11-C1.f cleanup
    scope. K9 ``hello-k9-pipe`` demo continues to work via the
    SAB-RPC path until the cleanup pass deletes that path.

K11-C1.d — DONE
  ``__NR_setpgid`` (109) / ``__NR_getpgrp`` (111) / ``__NR_setsid``
  (112) / ``__NR_getpgid`` (121) / ``__NR_getsid`` (124) routed
  through ``vmlinux.exports.wasm_syscall``. Kernel-side
  ``kernel/sys.c::sys_setpgid`` etc. mutate
  ``current->signal->pids[PIDTYPE_PGID/PIDTYPE_SID]`` against the
  per-process ``signal_struct`` from K11-C1.a.

  Notes on what landed
  ~~~~~~~~~~~~~~~~~~~~

  - ``hardwarejs/src/worker.mjs::routeToWasmSyscall`` predicate
    extended to include all five pgrp/sid syscall numbers.

  - Demo: ``wackywasm-tools/tests/hello-k11-c1d-pgrp.c``. Single
    process. Probes each syscall and emits return value via
    stdout for the test to scrape.

  - Vitest: ``hardwarejs/test/k11-c1d-pgrp-kernel.test.ts``.
    Observed at first green run (running as /init, pid=1):

    .. code-block:: text

       getpid                  = 1
       getpgrp_before          = 0   (init_struct_pid -> 0)
       getpgid_0_before        = 0
       getsid_0_before         = 0
       setpgid_0_0             = 0   (success — kernel succeeded)
       getpgrp_after_setpgid   = 1   (kernel mutated pgrp to current pid!)
       setsid                  = -1, errno=EPERM(1)  (already pgrp leader)
       getsid_0_after          = 0   (sid unchanged)
       wasmSyscallStats        = {109:1, 112:1, 121:3, 124:2}

  - Note: musl wasm32 implements ``getpgrp()`` as
    ``syscall(SYS_getpgid, 0)``, so ``__NR_getpgrp`` (111) does
    NOT appear in stats even though the demo called ``getpgrp()``.
    The predicate still forwards both numbers; absence from stats
    is musl's implementation choice.

  - Surprising find: setpgid + getpgrp DOES propagate
    upstream-clean even under R-K11-W5 (no alloc_pid). The kernel
    ``change_pid`` path mutates the pid_link inside init_struct_pid
    such that subsequent ``getpgrp`` reads back the caller's pid
    correctly. Cross-process pgrp semantics (``kill -pgid``) still
    require R-K11-W5 retirement.

  Deferrals carried by K11-C1.d
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  - ``setpgid(other_pid, ...)`` for non-current targets returns
    ``-ESRCH`` from the kernel (``find_task_by_vpid`` returns
    NULL under R-K11-W5). K11-C1 close ledger documents this as
    a non-blocker for K11 (no demo exercises the cross-process
    setpgid form).

  - Cross-process pgrp delivery (``kill(-pgid, sig)`` reaches every
    task whose ``signal->pids[PIDTYPE_PGID]`` matches): blocked on
    R-K11-W5 retirement. K9-C2 ``hello-k9-pgrp.c`` demo continues
    to work via the SAB-RPC path with ``processGroupRegistry``
    until K11-C1.f cleanup deletes that path.

K11-C1.e — DONE (docs-only — defer-to-K12 ledger entry)
  Stdin handling stays JS-direct through K11 close. K9-C1 substrate
  (``hardwarejs/src/stdinChannel.ts`` + per-tid SAB ring +
  ``__NR_read`` SAB-RPC handler for fd 0 in
  ``kernelSpawnHandler.ts::dispatchSyscall``) retained through
  K11-C1.f cleanup; K11-C1.f deletes the wait4 / pipe / fd / pgrp
  registries but explicitly preserves the stdin substrate.

  Filed as ``docs/ARCHITECTURE.md §15.3 R-K11-W7`` with retirement
  signal pointing at K12 ``pty-model.rst`` (``pty_init`` +
  ``n_tty_line_discipline`` + ``tty_open`` + per-tid pty fd
  installation in ``current->files``).

  Acceptance test for the deferral: K9-C1 stdin demo
  (``wackywasm-tools/tests/hello-k9-stdin.c``) +
  ``hardwarejs/test/k9-c1-stdin.test.ts`` continue to pass after
  K11-C1.f cleanup. The K11-D1 audit pass checks this explicitly.

  Net K11-C1 close-ledger statement: K11-C1 retires the wait4 /
  pipe / dup2 / dup3 / close / setpgid / getpgrp / setsid /
  getpgid / getsid R-K9-1 sub-shortcuts via wasm_syscall routing
  + per-process task / files / signal allocation. Stdin sub-
  shortcut survives until K12.

K11-C1.f — DONE
  Cleanup pass. The four JS-direct registry source files
  DELETED outright: ``hardwarejs/src/childExitRegistry.ts``,
  ``hardwarejs/src/pipeRegistry.ts``,
  ``hardwarejs/src/fdTableRegistry.ts``,
  ``hardwarejs/src/processGroupRegistry.ts``. Their
  ``hardwarejs/src/index.ts`` exports + the corresponding
  ``KernelSpawnHandlerOptions`` fields removed.

  ``hardwarejs/src/kernelSpawnHandler.ts::dispatchSyscall``
  cases DELETED outright (along with their syscall-number
  constants): ``__NR_wait4`` (61), ``__NR_pipe`` (22),
  ``__NR_pipe2`` (293), ``__NR_dup2`` (33), ``__NR_dup3`` (292),
  ``__NR_close`` (3), ``__NR_setpgid`` (109),
  ``__NR_setsid`` (112), ``__NR_getpgid`` (121),
  ``__NR_getpgrp`` (111), ``__NR_getsid`` (124),
  ``__NR_getppid`` (110), ``__NR_wasm_spawn_image_actions``
  (1025).

  The ``__NR_kill`` negative-pid (process-group) branch
  DELETED — negative pid now returns ``-ENOSYS`` from
  ``dispatchSyscall``. Cross-process ``kill -pgid`` filed as
  §15.3 row R-K11-W9 (K12 retirement alongside R-K11-W5
  ``alloc_pid`` retirement).

  ``__NR_kill`` (positive-pid only), ``__NR_tkill``,
  ``__NR_tgkill``, and ``__NR_rt_sigaction`` cases STAY at
  K11-C1.f as the SAB-RPC fallback for K8 vitests that don't
  yet pass ``vmlinuxModule``. Filed as §15.3 row R-K11-W10
  (deferred K8-test migration; K11-D2 retirement signal
  pins the empty short-circuit set ``{}``).

  ``__NR_read`` / ``__NR_write`` / ``__NR_writev`` cases
  collapsed: fd 0 routes through ``stdinChannel`` directly
  (no ``fdTableRegistry`` lookup); fd 1/2 routes through
  ``opts.onUserOutput`` directly; any other fd returns
  ``-EBADF``. The ``writeToFd`` helper retained but stripped
  to a 4-line surface (``fd 1/2 -> onUserOutput; else -9``).

  K9-B3 / K9-C1(b) / K9-C2 demos + their vitests DELETED:

  * ``wackywasm-tools/tests/hello-k9-pipe.c``
  * ``wackywasm-tools/tests/hello-k9-pipe-child.c``
  * ``wackywasm-tools/tests/hello-k9-pgrp.c``
  * ``wackywasm-tools/tests/hello-k9-pgrp-child.c``
  * ``hardwarejs/test/k9-c1-wait.test.ts``
  * ``hardwarejs/test/k9-c1-pipe.test.ts``
  * ``hardwarejs/test/k9-c2-pgrp.test.ts``

  ``wackywasm-tools/Makefile`` K9-C1(b) pipe-demos and K9-C2
  pgrp-demos blocks replaced with deletion-marker comments
  pointing at §15.3 rows R-K11-W8 and R-K11-W9 respectively.

  ``hello-k9-stdin.c`` + ``hardwarejs/test/k9-c1-stdin.test.ts``
  STAY (R-K11-W7 deferral). The vitest was updated to remove
  ``makeChildExitRegistry`` / ``makeFdTableRegistry`` /
  ``makePipeRegistry`` imports — fd 0 read now relies on
  ``stdinChannel`` alone; with no channel installed, fd 0
  returns EOF.

  ``hello-k9-wait.c`` + the K11-C1.b ``hardwarejs/test/
  k11-c1b-wait4-kernel.test.ts`` STAY (R-K9-2 ``wasm_spawn_image``
  is the spawn primitive — separate retirement track —
  and ``wait4`` runs kernel-side now).
  ``childExitRegistry`` reference removed from the K11-C1.b
  test.

  ``signalHandler.ts::deliverSignalToTarget`` retained as the
  shared primitive. Its only K11-C1.f callers are (a) the
  K8-B2 kernel-ring consumer and (b) the R-K11-W10 SAB-RPC
  fallback ``__NR_kill`` / ``__NR_tkill`` / ``__NR_tgkill``
  arms. K11-D2 deletes (b) once the K8 vitests migrate.

  Regression sweep: 56 vitest files, 141 passed + 2 skipped
  (full vitest run-all). Every K11-C1.* test green; K11-B6
  K8-vitest fallback signal tests green; K10-A4 initcall
  dispatch + single-Worker invariant green; K9-C1 stdin
  green (with-channel + EOF-when-no-channel); K11-C1.b
  ``__NR_wait4`` kernel routing green.

  §15.3 rows filed at K11-C1.f: R-K11-W8 (shell-pipeline
  cross-process fd-inheritance K12 retirement), R-K11-W9
  (cross-process ``kill -pgid`` K12 retirement), R-K11-W10
  (K8-vitest migration to per-Worker vmlinux path).

  R-K9-1 STATUS FLIP: was PARTIAL-CLOSE-AT-K11-C1.b/c/d; now
  CLOSED-AT-K11-C1.f modulo R-K11-W7 stdin deferral and the
  three succession rows above. The K11-D2 close-ledger commit
  re-validates by spot-checking the deleted-file list +
  re-running the full vitest sweep.

K11-C2
  R-K9-3 retirement: user-Worker exit messages route through
  kernel signal delivery; ``WIFSIGNALED+WTERMSIG`` works.

K11-C3
  K7/K8/K9 demo regression sweep against the in-Worker syscall
  path. Every prior demo still passes.

K11-D1 (DONE — commit a589d7d)
  Smoke probe: a real user process spawns, calls ``SYS_getpid``
  via the libc wrapper, the K11-A3+B2a path routes through
  ``vmlinux.exports.wasm_syscall`` to kernel-side ``sys_getpid``,
  returns ``current->tgid > 0``, exits 0. The vitest
  (``hardwarejs/test/k11-d1-smoke.test.ts``) gates
  ``wasmStats[__NR_GETPID] >= 1`` — proving the K11 axis
  end-to-end (no SAB-RPC fallback). First-execution wiring gap
  fixed in the same commit: ``getpid`` was not in the
  ``worker.mjs::routeToWasmSyscall`` allowlist (the predicate is
  an explicit per-syscall allowlist, NOT a default-route-all
  branch) so it had been falling through to SAB-RPC; added
  ``__NR_getpid`` guarded on ``perProcessTaskAddr != 0``. Not a
  refutation — a K11-D1 wiring completion the opener anticipated.

K11-D2 (DONE — this commit; tag v0.1.0-rc12)
  Close ledger + Appendix A flip + ``docs/ARCHITECTURE.md`` §14
  (K11 → CLOSED, K11.5 row added) + §15 amendments + tag
  v0.1.0-rc12. Honest close per §20.6.1 (see "K11 CLOSE" history
  entry below): the opener's "R-K8/9 retire at K11 close" framing
  over-claimed; the actual close is axis-landed-with-named-
  deferrals, decomposed in §14 + §15. Cumulative refutation count
  **0 / ≤ 3**.

--------------------------------------------------------------------------------
13.5. K11.5 sub-phase decomposition (continuation cleanup pass)
--------------------------------------------------------------------------------

K11.5 is the SAME AXIS as K11 (the in-Worker syscall path),
split out for scope discipline per directive D and the §20.6.1
retroactive dependency check at the K11-C1.f audit. It is NOT a
new K-phase mission. Budget ≤ 2. Sub-phases shrunk to ACTUAL
remaining work (the opener's A1..A5 estimate is superseded —
most of A1..A5 already landed in K11-C1.b..C1.f):

K11.5-A1 (R-K9-3 wstatus encoding)
  Pop ``stash@{0}`` (the K11-C2 carryover): POSIX
  ``WIFSIGNALED+WTERMSIG`` wstatus encoded at child-exit-notify
  time via a new ``SYS_wasm_exit_by_signal`` (1027) syscall; musl
  ``__wasm_signal_default_terminate`` calls it instead of
  ``exit_group(128+sig)``; ``wait.c`` encodes
  ``wstatus = signaled ? (code & 0x7f) : ((code & 0xff) << 8)``.
  Rebuild libc + vmlinux. Retires R-K9-3. The K11-C2 child binary
  calls ``SYS_wasm_exit_by_signal(15)`` directly to route AROUND
  the cross-Worker raise mis-target (R-K11-W12) — W12 is NOT
  retired here, only sidestepped.

K11.5-A2/A3/A4 (narrowed cross-process registry restoration) — DONE (``67ac022``)
  RESTORED narrowed ``pipeRegistry.ts`` (146) /
  ``fdTableRegistry.ts`` (149) / ``processGroupRegistry.ts`` (79)
  (DELETED at K11-C1.f) to cross-process responsibilities ONLY —
  each < 150 lines per directive D-narrow. Re-added ONLY the
  cross-process consumer call sites
  (``__NR_wasm_spawn_image_actions`` at
  ``kernelSpawnHandler.ts:2014``, ``__NR_kill`` pid<=0 at
  ``:2147``, ``__NR_getppid`` at ``:2268``); did NOT re-add the
  in-process short-circuits (those stay deleted — kernel-side at
  C1.c/.d). Registries are optional ``KernelSpawnHandlerOptions``
  fields, so non-registry callers are unchanged.

  .. note:: **§20.5.3 drift fix (K11.5-D2).** A prior revision of
     this section mapped the W-rows as "W8 = cross-process pipe
     substrate, W9 = fd inheritance + fd 0/1/2 host-side stdio,
     W10 = cross-Worker kill(-pgid) + getppid". That was a
     PRE-IMPLEMENTATION GUESS that drifted from the authoritative
     ``docs/ARCHITECTURE.md §15.3`` rows (filed at C1.f, matching
     the code comments). Per the R-K8-2 ruling — *the committed
     §15 ledger wins over any handoff or doc* — the authoritative
     mapping is:

     * **R-K11-W8** = cross-process **fd inheritance /
       ``spawn_image_actions``**, served JOINTLY by the restored
       ``pipeRegistry.ts`` + ``fdTableRegistry.ts``.
     * **R-K11-W9** = cross-Worker **``kill(-pgid)`` member
       enumeration + ``getppid``** via the restored
       ``processGroupRegistry.ts`` (``getppid`` folded into W9 per
       the K11.5-A3 decision — it has no §15.3 home of its own).
     * **R-K11-W10** = the **K8-vitest ``dispatchSyscall``
       fallback arms** (``rt_sigaction`` + kill family). NOT a
       registry citation — it is the A5 item.

     The live file:line citations live in §15.3 R-K11-W8/W9/W10.

K11.5-A5 (verify dispatchSyscall fallback shape) — DONE (option b)
  Confirmed the ``__NR_rt_sigaction`` (``kernelSpawnHandler.ts:1890``)
  + kill-family (``:2147``) fallback arms are reachable ONLY by K8
  vitests without ``vmlinuxModule`` — ``worker.mjs:808–809`` routes
  ``rt_sigaction`` to ``wasm_syscall`` unconditionally for vmlinux
  Workers, and ``worker.mjs:810–816`` routes tkill/tgkill always +
  kill pid>0. DECISION: KEEP the arms (R-K11-W10 stays OPEN,
  retirement → K12); the six-K8-vitest migration risks the ≤ 2
  budget. The ``{}`` claim is RE-SCOPED (not global): it applies to
  the in-process kernel-owned syscall set for the K11-process-Worker
  path, NOT to the W8/W9 cross-process arms re-added at A2/A3/A4 nor
  the W10 K8-vitest arms.

K11.5-D1 — DONE (``1a01ba4``)
  Deterministic registry smoke probe
  (``hardwarejs/test/k11-5-d1-registry-smoke.test.ts``) exercising
  the W8 fd-inheritance clone + action-list + pipe refcount + byte
  transfer + EOF/EPIPE substrate and the W9 inheritance-only pgrp /
  parent map (kill(-pgid) walk + getppid). The kernel-side combined
  round-trip (wait4 + pipe2 + read/write + setpgid against kernel
  code) is already proven by the C1.b/C1.c/C1.d kernel vitests; the
  full cross-WORKER pipe DATA round-trip is R-K11-W8-DEFERRED to
  K12-A1 (no kernel ``copy_from_user_for``).

K11.5-D2 — DONE
  Close ledger. R-K9-3 → CLOSED-AT-K11.5-A1. R-K11-W8/W9/W10 gained
  live file:line citations (see §15.3). §20.5.3 drift fix above
  reconciles this section's W-row mapping to §15.3. R-K11-W12 stays
  OPEN (K12-A1). Cumulative refutation count 0 / ≤ 2. Tag
  ``v0.1.0-rc12.5`` (rc13 reserved for K12 close).

--------------------------------------------------------------------------------
14. Refutation budget for K11
--------------------------------------------------------------------------------

**K11 refutation budget: ≤ 3.**

Reasoning per §20.9 three-input formula:

1. K10 actual: 0. K10 was the smallest K-phase since K1 (the
   only sub-phase landed was K10-A1..A4 = initcall dispatch).

2. Surface novelty: K11 is structurally novel on four axes:

   (i)   Kernel-stack-swap entry sequence has never been
         exercised. ``arch/wasm32/kernel/entry.c::wasm_syscall``
         may need substantial wasm32-specific work beyond the
         §6 sketch.

   (ii)  Populating ``sys_call_table`` will surface upstream
         syscalls that don't currently link on wasm32 — each is
         a per-syscall Kconfig / stub / §15 decision.

   (iii) K8 / K9 kernel-side code (currently unreachable)
         surfaces first-execution bugs. Expected and acceptable
         but counts.

   (iv)  First-time true SMP under load. Spinlock contention
         patterns, RCU grace-period progress under multiple
         concurrent Workers may surface upstream-clean code that
         was authored against assumptions wasm32 violates
         (e.g., ``cpu_relax`` semantics, ``smp_mb`` cost).

3. Single-axis discipline: K11's axis is "the in-Worker syscall
   path comes online." The four risk sites are all on that
   axis. The budget reflects the high novelty + four risk
   sites.

If K11 closes at 0–1 actual, the in-Worker path landed cleaner
than expected. If K11 closes at 4+, K12 should re-budget
(pty/ldisc/jobctrl work depends on the in-Worker path being
solid).

The fourth surface (true SMP under load) may surface late, in
the K7/K8/K9 regression sweep at K11-C3. Budget pivot per
``bisect-postmortems.rst`` §20.5 if it does — the budget
re-evaluation must record the surfaced upstream-clean code path
and the wasm32-specific divergence.

--------------------------------------------------------------------------------
15. Acceptance criteria (K11 close)
--------------------------------------------------------------------------------

K11 closes when:

(a) ``arch/wasm32/kernel/entry.c::wasm_syscall`` performs the
    kernel-stack swap (§6) on every entry / exit. Probe asserts.

(b) ``sys_call_table`` is populated; every ``__NR_*`` referenced
    by K7/K8/K9 demos + K12 pty/ldisc/jobctrl wrappers links
    upstream-clean OR is on the explicit -ENOSYS stub list with
    a §15 row.

(c) ``arch_cpu_up`` / ``arch_cpu_down`` light up CPUs 1..N at
    spawn / down at exit. ``cpu_online_mask`` reflects reality.

(d) ``num_vcpu`` parameter wired end-to-end. Boot with
    ``num_vcpu=4`` shows ``num_online_cpus() == 1`` at boot,
    increments to 2 / 3 / 4 as process Workers spawn.

(e) ``max_wasm_instances`` probe + accounting + -EAGAIN
    enforcement. Eviction deferred to v0.2 §15 row.

(f) R-K8-1 / R-K8-2 / R-K9-1 / R-K9-2 (partial) / R-K9-3
    retired in ``docs/ARCHITECTURE.md`` §15. The five rows'
    closure notes record the wrong-shaped retirement signal
    correction.

(g) R-K10-W1 inverted: assertion now reads "every process
    Worker carries a vmlinux instance"; runtime
    ``kernelInstanceCount`` check inverted; source-static guard
    test inverted.

(h) Regression: every K7 / K8 / K9 demo passes against the
    in-Worker syscall path. The R-K8-W1 flake is tracked
    separately and counted as the same row.

(i) ``make hwjs-verify`` clean.

(j) Cumulative K11 refutation count ≤ 3.

(k) Appendix A populated.

(l) v0.1.0-rc12 annotated tag placed.

(m) K12 surface proposal in K11 close ledger.

--------------------------------------------------------------------------------
16. K10 close ledger amendment + scope correction proposal
--------------------------------------------------------------------------------

K10's original mission was "Terminal / pty + line discipline +
job control + initcall dispatcher" (per the K10 opener doc).
K10's load-bearing sub-axis was the initcall dispatcher (the
"means to enable" the terminal subsystems); the terminal
subsystems themselves were folded under K10's umbrella because
the K10 opener author anticipated they would ride the existing
JS-direct syscall surface (the R-K9-1 pattern). The K10-B1
architectural-fork investigation surfaced that anticipation as
wrong against ``ARCHITECTURE.md`` §4 — the terminal subsystems
require the in-Worker syscall path that K11 lands.

Two scope-correction options:

(i)  Insert K10-B0 (= K11 work) between K10-A4 and K10-B1.
     K10-B1..K10-D2 as planned, on top of working syscall path.
     Refutation budget becomes K10's existing ≤ 2 + the K11
     ≤ 3 estimate — total ≤ 5 inside K10's umbrella.
     Single-axis discipline loses (the in-Worker syscall path
     is qualitatively different from terminal-layer work; one
     K-phase trying to land both blurs the axis).

(ii) Pause K10 at K10-A4 close. K10's mission re-scopes to
     "the initcall dispatcher landed" — which is the
     load-bearing sub-axis K10 actually needed. Original
     K10-B/C/D becomes K12 "PTY + line discipline + job control
     on the in-Worker syscall path", with its original ≤ 2
     budget intact (against pty-specific risk only). K11 opens
     between K10 close and K12 open.

**Recommended: (ii).** Single-axis discipline favours splitting.
K11 stays within its own ≤ 3 budget; K12 keeps its original
≤ 2 budget against PTY-specific risk. The K-phase ledger gains
one row but loses no work; the v0.1.0-rc11 / -rc12 / -rc13 tag
sequence is consistent with the K-phase shape.

**K-phase ledger amendments under option (ii):**

* K10 row (currently "OPEN (A-sub-phase closed; B-sub-phase
  next)") → CLOSED at v0.1.0-rc11. Mission re-scoped to "real
  initcall dispatcher; ``__initcall<level>_start/_end`` walks
  via Strategy B post-link bracket pass; pty / n_tty / devpts
  subsystems registered at boot." Original "Terminal / pty +
  line discipline + job control" mission moves to K12.

* K11 row (NEW) → OPEN. Mission: "Per-Worker vmlinux + real
  SMP comes online. ``arch/wasm32/kernel/entry.c::wasm_syscall``
  performs kernel-stack swap; ``sys_call_table`` populated;
  ``arch_cpu_up`` / ``arch_cpu_down`` hooks; ``num_vcpu`` /
  ``max_wasm_instances`` JS parameters; R-K8-1/2 + R-K9-1/2/3
  retired; R-K10-W1 inverted." Tag v0.1.0-rc12.

* K12 row (NEW) → PINNED. Mission: original K10-B/C/D —
  "Terminal / pty + line discipline + job control on the
  in-Worker syscall path." Tag v0.1.0-rc13. Budget ≤ 2.

* D1 row unchanged.

--------------------------------------------------------------------------------
17. §20.6.1 retroactive dependency-check
--------------------------------------------------------------------------------

The K10-A2b commit landed a §11.2.1 clarification stating
"vmlinux is single-Worker by architectural construction." That
commit cited ``docs/ARCHITECTURE.md`` §2 + §17 as authority. The
citation was incomplete: §3 line 124 and ``memory-model.rst``
§1.1 specify the OPPOSITE — vmlinux is per-Worker.

By §20.6.1, every "deferred to phase N+1" claim (and by
extension, every architectural rationale baked into a §15 row)
needs a written dependency-check. The K10-A2b §11.2.1 claim
landed without one. The architecture-vs-implementation drift
check that should have fired:

::

    CLAIM: "vmlinux.wasm is single-Worker by architectural
            construction (per ARCHITECTURE.md §2 + §17)."

    DEPENDENCY-CHECK (REQUIRED but absent at K10-A2b):
      Walk every doc site that describes vmlinux instance
      cardinality and confirm consistency:
        - ARCHITECTURE.md §2  ← cited (consistent)
        - ARCHITECTURE.md §3  ← NOT CITED, contradicts the
                                claim (line 124: "A new instance
                                of vmlinux.wasm is *also*
                                instantiated in this Worker")
        - ARCHITECTURE.md §4  ← NOT CITED, presumes per-Worker
                                vmlinux (lines 178-186)
        - ARCHITECTURE.md §17 ← cited (§17.6 says SMP is real,
                                which presumes multiple kernel
                                instances writing kernel
                                memory concurrently)
        - memory-model.rst §1.1 ← NOT CITED, contradicts the
                                  claim ("Every Worker — every
                                  process Worker and the
                                  dedicated vmlinux Worker —
                                  holds its own
                                  WebAssembly.Instance")
        - memory-model.rst §1.2 ← NOT CITED, presumes
                                  per-Worker kernel instance

    HAD THE DEPENDENCY-CHECK FIRED: the §11.2.1 claim would
    have been refuted at K10-A2b commit time. The K10-A3 fork
    answer would have been §11.D (per-Worker vmlinux + SMP)
    rather than the §11.2.1 framing. The R-K10-W1 row would
    have been filed with the corrected rationale. The K10-A4
    runtime + source-static assertions would have been written
    in their inverted form from the start.

    REFUTATION COST PAID: K10-A4 close included the wrong-
    shaped R-K10-W1 row + the runtime + source-static
    assertions pinning the wrong invariant. K11 corrects all
    three. The cost is one K-phase doc-amendment cycle plus
    the assertion inversion at K11 close. Acceptable but
    avoidable.

    LESSON: §20.6.1 dependency-checks for architectural
    rationales need a "every doc site that describes the
    invariant" enumeration, not just citations to the sites
    the author found compelling. The discipline is the same
    as §20.5 external-tool probes: enumerate exhaustively,
    classify carefully.

The §20.5 amendment that lands as part of this opener doc
adds an "architecture-vs-implementation drift check" to the
external-tool probe surface. The check is: every §15 row that
asserts an architectural rationale gets reviewed by walking
every doc site indexed under the relevant doc-cluster
table-of-contents, and each site is classified
"consistent / contradicts / orthogonal." A "contradicts" hit
either fixes the row OR gets a documented rebuttal in the row
text. Probe at K11-A2: the source-site snapshot tool gains
a flag ``--architectural-rationale-check`` that runs the
walk against the §15 rows.

--------------------------------------------------------------------------------
Appendix A — K11 close checklist
--------------------------------------------------------------------------------

[X] K11-A1 probes pass (§stack pointer global access, current->
    stack resolves, THREAD_SIZE consistent)
[X] K11-A2 source-site snapshot landed; every __NR_* enumerated
[X] K11-A3 wasm_syscall stack-swap implemented (§6)
[X] K11-A4 K7/K8/K9 regression sweep against new entry sequence
    (123 passed + 2 R-K8-W1 flakes at commit d64ffc0)
[X] K11-B1 sys_call_table populated; -ENOSYS stub list +
    R-K11-N rows landed
[X] K11-B2 arch_cpu_up / arch_cpu_down + per-CPU wake words
    (B2a per-Worker vmlinux + B2b cpu_id substrate)
[X] K11-B3 num_vcpu wired end-to-end
[X] K11-B4 max_wasm_instances probe + accounting + -EAGAIN
    (eviction deferred to v0.2 §15 row)
[X] K11-B5 R-K8-1 do_sigaction reached for K11 process Workers
    (CLOSED-FOR-K11-AXIS; residual K8-vitest arm → R-K11-W10)
[~] K11-B6 R-K8-2 self-target CLOSED (kernel sys_kill/tkill/
    tgkill reached); CROSS-TARGET deferred to K12 (R-K11-W12,
    gated on alloc_pid / R-K11-W5)
[X] K11-C1 R-K9-1 retired for in-process (C1.a..C1.f: kernel
    wait4/pipe/dup/close/pgrp/sid; registries DELETED; cross-
    process residue → R-K11-W8/W9/W10); stdin → R-K11-W7 (K12)
[ ] K11-C2 R-K9-3 retired — NOT DONE (stashed; moves to K11.5-A1)
[X] K11-C3 K7/K8/K9 demo regression sweep (folded into A4 +
    the C1.* kernel-path vitests)
[X] K11-D1 smoke probe: getpid round-trip via wasm_syscall
    (wasmStats[__NR_GETPID] >= 1; commit a589d7d)
[X] K11-D2 close ledger + Appendix A flip + tag v0.1.0-rc12 +
    K12 surface proposal (this commit)

[X] R-K10-W1 INSTANTIATION-INVERTED: every process Worker
    carries vmlinux; processVmlinuxInstanceCount enforces; active-
    overlay idempotency verified (k11-b2a-per-worker-vmlinux.test.ts).
    SAB-RPC-deletion is progressive (K11.5/K12), not a K11-D2 flip.
[ ] terminal-model.rst §11.2.1 / §11.B — NOT re-edited at K11
    close per directive ("terminal-model.rst keeps its K10-scoped
    content"); the K11/K11.5 split is a §20.5.3 drift candidate
    noted in this ledger, not a terminal-model.rst edit.
[X] docs/ARCHITECTURE.md §14 K-phase ledger: K11 → CLOSED;
    K11.5 row added
[X] docs/ARCHITECTURE.md §15: R-K8-1 CLOSED-FOR-K11-AXIS;
    R-K8-2 refined three-way PARTIAL-CLOSE; R-K9-1/R-K9-2 closed
    for in-process; R-K10-W1 CLOSED-AS-INVERTED; R-K9-3 stays
    OPEN (K11.5-A1); R-K11-W12 FILED; R-K11-W10 retirement → K11.5

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

K11 OPENER (this commit, post-K10-A4-close)
  Document created. Per-Worker vmlinux contract pinned.
  In-Worker wasm_syscall entry sequence concretised from
  memory-model.rst §1.2. sys_call_table population strategy
  declared. arch_cpu_up / arch_cpu_down hooks specified.
  num_vcpu and max_wasm_instances parameters introduced.
  R-K10-W1 inversion plan recorded. R-K8-1 / R-K8-2 / R-K9-1
  / R-K9-2 / R-K9-3 retirement plan recorded. Sub-phase
  decomposition pinned. Budget ≤ 3 declared. K10 close
  ledger amendment (option (ii) — pause K10 at K10-A4-close,
  open K11, defer original K10-B/C/D to K12). §20.6.1
  retroactive dependency-check on the K10-A2b commit
  recorded. NO CODE LANDS until this doc and the companion
  amendments to terminal-model.rst + ARCHITECTURE.md are
  reviewed and the scope-correction option (i) vs (ii) is
  resolved.

K11 CLOSE (K11-D2; tag v0.1.0-rc12)
  Axis claim LANDED: the in-Worker syscall path comes online.
  ``arch/wasm32/kernel/entry.c::wasm_syscall`` performs the
  kernel-stack swap (K11-A3); ``sys_call_table`` is populated for
  the K7/K8/K9 + musl surface (K11-B1); ``arch_cpu_up`` /
  ``arch_cpu_down`` + per-Worker ``cpu_id`` substrate land (B2a/B2b);
  ``num_vcpu`` (B3) and ``max_wasm_instances`` + -EAGAIN (B4) are
  wired end-to-end. A curated per-syscall allowlist routes through
  ``vmlinux.exports.wasm_syscall`` (rt_sigaction, kill/tkill/tgkill
  self+positive-target, wait4, pipe/pipe2/dup2/dup3/close,
  setpgid/setsid/getpgid/getpgrp/getsid, getpid). The K11-D1 smoke
  probe gates getpid round-trip (``wasmStats[__NR_GETPID] >= 1``).

  Cumulative refutation count: **0 / ≤ 3**. The W-rows
  (R-K11-W2..W12) are watched invariants, not refutations; the
  R-K11-1/2/3 -ENOSYS slots are stub decisions, not refutations.

  §20.6.1 RETROACTIVE DEPENDENCY CHECK (the honest-close note):
  the K11 opener claimed "R-K8-1 + R-K8-2 + R-K9-1 + R-K9-2
  (partial) + R-K9-3 retire at K11 close." The K11-C1.f audit
  REFUTED the completeness of that claim — per-process
  ``task_struct`` allocation (C1.a) and the C1.b..f routing landed
  the IN-PROCESS surface, but (1) R-K9-3 wstatus encoding was never
  done (the K11-C2 work is stashed), (2) cross-process pipe/fd/pgrp
  substrate cannot retire without kernel-side ``copy_from_user_for``
  + ``alloc_pid`` (K12-A1), and (3) R-K8-2 cross-target signal
  delivery mis-resolves under the deferred pid hash (R-K11-W12).
  Per directive D the phase splits into K11 (axis landing, this
  close) + K11.5 (R-K9-3 + narrowed-registry restoration) — SAME
  AXIS, continuation for scope discipline, NOT a new mission. The
  K11 budget is unaffected (the split is a §20.6.1 dependency-check
  outcome, not a refutation).

  ACTUAL R-ROW CLOSE STATE (committed ARCHITECTURE.md §15 is the
  authority — see the §20.5.3 drift note below):
    * R-K8-1 — CLOSED-FOR-K11-AXIS (kernel do_sigaction reached;
      no cross-target dimension); residual K8-vitest arm → R-K11-W10.
    * R-K8-2 — self-target CLOSED-AT-K11-B6; cross-target
      DEFERRED-TO-K12 (R-K11-W9 + R-K11-W12, gated on alloc_pid /
      R-K11-W5); K8-vitest arms → R-K11-W10. (Three-way decomposition
      in §15.)
    * R-K9-1 / R-K9-2 — CLOSED for in-process; cross-process residue
      in R-K11-W8/W9/W10; childImageRegistry stays (K12-A1 signal).
    * R-K9-3 — OPEN, retirement K11.5-A1.
    * R-K10-W1 — CLOSED-AS-INVERTED (instantiation inversion +
      active-overlay idempotency verified). SAB-RPC remains the
      DEFAULT route for non-allowlisted syscalls; its deletion is
      progressive (K11.5/K12), not a K11-D2 flip.
    * R-K11-W12 — FILED at K11-D2 (cross-Worker raise mis-target).

  §20.5.3 DRIFT FINDING: the handoff doc
  ``docs/HANDOFF-K11-CLOSE.md §3.4`` drifted from the committed
  ARCHITECTURE.md §15 R-K8-2 row. The handoff asserted "R-K8-2 =
  FULL-CLOSE per the 8:40 reframing, JS-direct callers deleted at
  C1.f"; HEAD contradicts this on two independent grounds — (1) the
  JS-direct ``deliverSignalToTarget`` arms are LIVE at
  ``kernelSpawnHandler.ts`` (NOT deleted), (2) the committed R-K8-2
  row says cross-target stays JS-direct gated on alloc_pid (K12).
  The 8:40 "full close" message was a GUI-corruption artifact (the
  user confirmed the cursor follow-up channel was broken in that
  window; the prior agent flagged it as a possible stale replay).
  RESOLUTION: the committed §15 ledger wins; the handoff §3.4
  R-K8-2 line is superseded. This is the SECOND GUI-corruption
  near-miss this phase (the first was K11/K11.5 re-litigation);
  both argue for treating the committed §15 ledger — never a
  handoff or a chat replay — as the single source of truth for
  R-row status at tag time.

  K12 SURFACE PROPOSAL: K12-A1 lands the kernel substrate that
  retires the K11/K11.5 W-rows — ``copy_from_user_for(tid, ...)``
  (R-K11-W8 pipe r/w against another task's user_memory),
  ``wasm32_kill_pgrp`` arch shim (R-K11-W9/W10 kill(-pgid)),
  ``alloc_pid`` / ``attach_pid`` + ``find_task_by_vpid`` resolution
  (R-K11-W5 / R-K11-W12 cross-target signal delivery; R-K8-2
  cross-target close), kernel-side ``spawn_with_actions`` fd
  inheritance, and ``getppid``. Host-side fd 0/1/2 stdio wiring
  lands as part of K12-B PTY work. Budget ≤ 2 against PTY-specific
  risk only.