brintos

brintos / linux-shallow public Read only

0
0
Text · 88.2 KiB · b00c8c0 Raw
1969 lines · plain
1.. SPDX-License-Identifier: GPL-2.02 3================================================================================4arch/wasm32 scheduler model (K4 spec)5================================================================================6 7:Status: K4 (cooperative kthread scheduling, ``switch_to`` via Asyncify,8         rest_init → kthreadd → kernel_init to "No working init found").9         Stable through K6; revisions require a new section + §15 row.10 11This document is the source of truth for *how the wasm32 port models12kernel-thread context switching, cooperative scheduling, idle13behaviour, and the kernel-side half of the Two Regimes spec in14``docs/ARCHITECTURE.md`` §17*. It plays the same role for the15scheduler that ``mm-model.rst`` plays for memory and16``boot-protocol.rst`` plays for boot handoff: spec-first,17code-second. Treat any divergence between this document and18``arch/wasm32/kernel/switch_to.c`` / ``arch/wasm32/kernel/asyncify.c``19/ ``arch/wasm32/include/asm/switch_to.h`` /20``arch/wasm32/include/asm/thread_info.h`` as a bug in the code.21 22This doc explicitly does NOT re-derive the Two Regimes framing —23read §17 of ``docs/ARCHITECTURE.md`` first; this doc is the K424implementation contract built on top of it.25 26--------------------------------------------------------------------------------270. K4-phase scope at a glance28--------------------------------------------------------------------------------29 30(a) **New cross-module ABI introduced by K4.** Three surfaces.31 32  - **kernel-self ↔ Asyncify-transformed-self.** Not kernel ↔33    HardwareJS or kernel ↔ userspace; the kernel module's34    own C code, post-Binaryen-``asyncify``-pass, gains a new35    contract with itself. Specifically: any function on the36    Asyncify *whitelist* (§3.1 below) is permitted to suspend37    via a cooperative yield point and resume later with its38    C-stack restored. Any function NOT on the whitelist that39    nonetheless reaches a yield point at runtime triggers40    ``asyncify-asserts`` (``--pass-arg=asyncify-asserts`` to41    wasm-opt), surfacing the whitelist gap as a hard runtime42    trap rather than a silently corrupt stack. The whitelist43    itself is the ABI artifact, generated by44    ``scripts/asyncify-whitelist.py`` (K4 introduces this45    tool, sibling to ``scripts/wasm-add-memory.py`` and the46    upcoming K5/K6 percpu post-link pass).47 48  - **kernel ↔ HardwareJS (scheduler wakeup word).** A new49    fixed-offset, fixed-name word in ``env.kernel_memory``,50    documented as ``__sched_wakeup_word`` and exported as a51    wasm global of i32 type pointing at the offset. Any party52    that makes a kthread runnable — kernel C code's53    ``try_to_wake_up`` path, a HardwareJS-side timer expiry,54    a user Worker's syscall response — writes a nonzero value55    to the word and calls ``Atomics.notify``. The vmlinux56    Worker, when its cooperative runqueue is empty, parks57    via ``Atomics.wait`` on the same word. §6 below specs58    the word's offset, semantics, and the host-side helper59    surface.60 61  - **kernel ↔ wasm runtime (Asyncify state convention).**62    The Asyncify pass exposes two wasm globals:63    ``asyncify_state`` (i32, 0/1/2 = none/unwind/rewind) and64    ``asyncify_data`` (i32 pointing at the per-task save65    buffer). These ARE the contract: the kernel's C code in66    ``arch/wasm32/kernel/switch_to.c`` manipulates these67    globals directly via wasm-builtin extern declarations,68    and the Asyncify-transformed binary observes them at69    function-entry epilogue and prologue. §3.2 below names70    the wasm globals and the C-level extern declarations71    that bind them.72 73(b) **Architectural decisions K4 forces.** Q1–Q13 in §2 below.74    None are reversible without a coupled HardwareJS ↔ kernel75    bump.76 77(c) **§15 debt this phase predictably opens.** Six rows,78    all noted inline below and added to the §15 table at K479    close:80 81  1. **Asyncify whitelist is a maintenance contract.** Adding82     a yielding kernel path that doesn't update83     ``arch/wasm32/asyncify-whitelist.txt`` (generated by84     ``scripts/asyncify-whitelist.py``) produces a runtime85     ``asyncify-asserts`` trap. The K5/K6 commits that add86     new yielding paths (page faulting, signal handling,87     blocking I/O) MUST regenerate the whitelist. Owed:88     each subsequent K-phase.89  2. **``current->mm != NULL`` path stays ``BUG()``.** K490     implements Regime 2 only. Regime 1 (user-task park via91     ``Atomics.wait`` on per-task futex word) is a K592     deliverable. ``wasm_sched_class.enqueue_task`` in93     ``arch/wasm32/kernel/sched.c`` BUG_ONs if it sees a94     non-NULL ``->mm`` (see ``docs/ARCHITECTURE.md §17.7`` for95     the K4 class scope and HISTORICAL NOTE for the original96     escape-hatch design that was retired). Owed: K5.97  3. **Voluntary preemption only.** ``preempt_count`` is98     compiled in but not driving dispatch (§17.4 already99     established). K4 makes this real: the cooperative100     runqueue NEVER preempts a running kthread mid-function;101     yield happens only at explicit ``schedule()`` /102     ``cond_resched()`` / ``wait_event_*`` / ``msleep`` /103     ``schedule_timeout`` call sites. Owed: never (a104     permanent property of the model).105  4. **Asyncify binary-size delta.** Post-Asyncify ``vmlinux.wasm``106     is empirically X% larger than pre-Asyncify (X measured at K4107     close; recorded in commit body). Whitelisted-only108     transformation keeps the delta bounded, but it is not zero.109     Owed: revisit at K7 if size becomes a deploy-cost issue, by110     either tightening the whitelist or migrating to JSPI per Q7.111  5. **Kthread signal handling deferred.** ``signal_pending()``112     returns 0 for kthreads at K4 (Q5 below). KILL, freezer,113     suspend-related signals are out of K4 scope. Owed: K6+114     when the freezer / suspend / kthread-stop machinery115     becomes interesting (it never may, for a browser116     target).117  6. **``wasm_sched_class`` Regime-1 extension.** K4 lands a118     real ``wasm_sched_class`` for Regime 2 kthreads (see §5119     below and ``docs/ARCHITECTURE.md §17.7``). At K4 the class120     BUG_ONs in ``enqueue_task`` if it sees a task with121     ``->mm != NULL`` (the Regime-1 marker). K5 extends the122     same class to handle Regime 1 by recording the per-task123     futex word on enqueue and firing the notify on dequeue;124     the class structure stays single — there is NOT a separate125     ``sched_browser_class`` (the early K3/K4 design name that126     was retired alongside §17.7's escape-hatch design — see127     ``docs/ARCHITECTURE.md §17.7`` HISTORICAL NOTE for the128     original assumption and why it was refuted). Owed: K5.129 130--------------------------------------------------------------------------------1311. Scope inside §17's framing132--------------------------------------------------------------------------------133 134§17 of ``docs/ARCHITECTURE.md`` defines the Two Regimes. K4 implements135exactly half of the spec:136 137============== =========================================================138Regime 2       **K4 implements in full.** All kthreads inside the139(``->mm == 0``) vmlinux Worker. Cooperative FIFO runqueue inside140                ``wasm_sched_class``. Asyncify-backed ``switch_to``.141                Idle parks on the master scheduler wakeup word.142                kthread_create / kthread_run / kthread_stop all work.143                wait_event / wake_up_process / complete /144                wait_for_completion all work for kthread-only callers.145Regime 1       **K4 leaves as ``BUG()``.** No user-process Workers146(``->mm != 0``) exist yet (binfmt_wasm is K5; the userspace toolchain147                isn't producing user binaries yet).148                ``wasm_sched_class.enqueue_task`` BUG_ONs if it sees a149                task with non-NULL ``->mm``. K5 extends the same class150                to record the per-task ``Atomics.wait`` futex word on151                enqueue and fire the notify on dequeue.152============== =========================================================153 154Specifically out of scope for K4:155 156* User-process spawn (no ``do_fork`` walks the ``->mm != NULL`` branch157  because no Worker exists to run a user process yet).158* binfmt_wasm dispatching to a user wasm binary.159* The ``/init`` binary itself; the userspace toolchain hasn't been160  exercised against a real userland.161* Regime 1 dispatch policy (the per-task ``Atomics.wait`` parking is162  K5's ``wasm_sched_class`` extension; see ``docs/ARCHITECTURE.md163  §17.2`` and the §17.7 HISTORICAL NOTE for the original "deferred164  to v1.0 via ``sched_browser_class``" design that was retired165  mid-K4).166 167What this means in the K4 acceptance path: ``rest_init`` runs,168``kernel_init`` is spawned as a kthread, kernel_init attempts to169``run_init_process("/init")``, fails (no rootfs, no binfmt_wasm),170falls through to ``panic("No working init found.")``. That panic171*is* the K5 boundary — exactly the §17 / K5 framing.172 173--------------------------------------------------------------------------------1742. Architectural decisions Q1–Q13 (the K4 opening thirteen)175--------------------------------------------------------------------------------176 177Q1. Asyncify scope (whitelist)178~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~179 180**Decision: whitelisted Asyncify, generated by181``scripts/asyncify-whitelist.py`` from the reachability closure182of the cooperative-yield function set183{``schedule``, ``cond_resched``, ``wait_event_interruptible``,184``wait_event_timeout``, ``msleep``, ``schedule_timeout``,185``wait_for_completion``, ``wait_for_completion_interruptible``,186``__wasm32_switch_to``}. Pass: ``wasm-opt --asyncify187--pass-arg=asyncify-only-list@arch/wasm32/asyncify-whitelist.txt188--pass-arg=asyncify-asserts``.**189 190Considered:191 192* **Full-program Asyncify.** The default. Easy to enable, hard to193  diagnose. Increases binary size 2-3× (Binaryen issues #2755,194  #5040). Slows EVERY function (each non-trivial wasm function195  gains an unwind/rewind switch at the prologue and a state196  check at every call). Acceptable for size-insensitive197  applications; unacceptable for a kernel that boots into a198  browser tab.199 200* **Whitelist + asyncify-asserts (with named topmost-strip201  exceptions).** Binaryen's ``asyncify-only-list`` pass-arg202  restricts the transformation to a named subset. Any function203  NOT on the list (and not in the transitive closure of callers)204  is left as-is — same size, same speed. The companion205  ``asyncify-asserts`` arg makes the runtime trap if a non-listed206  function reaches a yield point at runtime, which is the207  diagnostic that catches whitelist drift. This is the standard208  pattern in production Asyncify deployments (Emscripten's209  documentation explicitly recommends it for performance-critical210  code paths).211 212  **Topmost-runtime caveat (empirically discovered at K4213  opening).** Binaryen 125's ``asyncify-asserts`` injects the214  state-stability check around every call site in non-instrumented215  functions, including functions that ARE the asyncify state-216  machine driver (those that call ``asyncify_start_unwind`` /217  ``asyncify_stop_rewind`` directly — ``__wasm32_switch_to`` and218  any future helpers). For those functions, the legitimate state219  change IS the bug the assertion fires on, and the kernel traps220  on every context switch. Binaryen acknowledges this in source221  at ``src/passes/Asyncify.cpp:1286-1291`` of version_125 with a222  FIXME ("the top-most runtime is actually a place that needs223  neither instrumentation *nor* assertions"). Our resolution is224  a post-pass strip tool,225  ``scripts/asyncify-strip-topmost-asserts.py``, that reads a226  named list at ``arch/wasm32/kernel/asyncify-topmost.txt`` and227  surgically removes the 9-byte assert pattern from those228  functions' bodies — preserving asserts everywhere else. The229  exception list is the maintenance contract: adding a230  topmost-runtime helper without listing it is a build-time-231  detectable bug (the strip tool fails the build if any listed232  name is missing from the binary's name section); calling233  ``asyncify_start_unwind`` from outside the list is a runtime234  trap. The probe at235  ``linux-wasm/tools/k4-asyncify-asserts-probe/`` validates this236  surgery against the pinned toolchain and detects Binaryen237  drift before the kernel does. See §15 row "asyncify-asserts238  enabled with named topmost-strip exceptions" and §18 entry on239  topmost-runtime as the intended exception.240 241  **The host is responsible for ``asyncify_stop_rewind``242  (empirically discovered at K4 mid-phase, R4b).** Binaryen 125243  does NOT auto-inject ``asyncify_stop_rewind`` at the bottom of244  the rewind chain. Verified by direct disassembly inspection of245  the post-pass ``vmlinux.wat``: zero calls to the synthesized246  ``asyncify_stop_rewind`` function exist anywhere in the binary247  unless emitted by hand from C code. The Asyncify pass moves248  the state machine UNWIND→REWIND→ via the rewind chain's249  prologue/epilogue mechanics (each function's prologue250  decrements ``cur`` to load its frame), but the251  REWIND→NORMAL transition is NOT done automatically; it must252  be performed by the topmost-runtime function (in our253  self-call design, ``__wasm32_switch_to``) on rewind re-entry.254 255  The contract: the topmost-runtime function maintains a256  per-task gate (``thread.unwound``); on the first call (state257  is NORMAL, unwound is false) it sets the flag and calls258  ``asyncify_start_unwind``; the host (the rest of259  ``__wasm32_yield``'s loop) eventually calls260  ``asyncify_start_rewind`` on a saved buffer; the rewind chain261  bottoms out by re-executing the call to the topmost helper,262  which observes ``unwound == true``, calls263  ``asyncify_stop_rewind``, clears the flag, and returns. The264  state is now NORMAL and execution resumes in the caller265  past the suspended call site.266 267  The ``asm/asyncify.h`` header in this tree previously268  contained a comment claiming auto-injection — that comment269  was incorrect for Binaryen 125 and has been deleted along270  with the protocol fix. Future Binaryen versions may change271  this behavior; the topmost-strip-asserts probe and the272  asyncify-removelist propagation probe (both at273  ``linux-wasm/tools/k4-binaryen-asyncify-probe/``) detect274  drift in either direction before the kernel does.275 276* **Manual``__attribute__((noreturn))``-marker pattern.** Mark277  yielding functions with a custom attribute; have the build278  system extract the marker list as the whitelist. Cleaner long-279  term but requires per-function code changes across upstream280  Linux source. Out of bounds (§16 PR rejection criterion).281 282* **JSPI (JS Promise Integration).** Q7 below.283 284**Rationale.** Whitelisted Asyncify is the smallest hammer that285fits. The whitelist size is ~1500 functions at K4 (closure of the286nine root functions through cond_resched / schedule callers); the287whitelist regeneration is mechanical (DWARF-walking the K4288``vmlinux.wasm`` via ``llvm-objdump --syms``, transitively289including callers reachable from the yield-root set). The K4290binary-size delta is bounded to ~15% per Binaryen's documented291"selective asyncify" overhead on similar-size kernels; the actual292number is measured at K4 close and recorded in the commit body293alongside the atomic-opcode count.294 295**§15 debt row at K4 close.** "Asyncify whitelist is a per-K-phase296maintenance contract." See §0(c)(1).297 298Q2. switch_to ABI between kthreads (the kernel-self ↔ Asyncify-self contract)299~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~300 301**Decision: per-task Asyncify save buffer is allocated at302``kthread_create`` time, lives in ``task_struct.thread.asyncify_data``,303sized at ``ASYNCIFY_DATA_SIZE = 16 * 1024`` (16 KiB). The kernel304stack is separately allocated, sized at THREAD_SIZE per Q4.305``__wasm32_switch_to(prev, next)`` is the only function in the306kernel module that touches the ``asyncify_state`` / ``asyncify_data``307wasm globals directly. Everything else above it observes a normal308C function call.**309 310Per-task storage (concrete byte layout):311 312::313 314    struct thread_struct {315        /* Asyncify save buffer. asyncify_data points HERE. The316         * buffer layout is opaque (defined by Binaryen's pass);317         * we treat it as a fixed-size void* region. */318        void          *asyncify_data;319        unsigned long  asyncify_data_bytes;     /* = ASYNCIFY_DATA_SIZE */320 321        /* Kernel stack. __stack_pointer for this task starts at322         * stack_base + stack_size. */323        void          *stack_base;324        unsigned long  stack_size;              /* = THREAD_SIZE */325 326        /* Cooperative runqueue linkage (used by the FIFO scheduler327         * in arch/wasm32/kernel/asyncify.c). */328        struct list_head rq_node;329 330        /* R4b (§17.7 FIFTH HISTORICAL NOTE): per-task gate331         * distinguishing "first call to __wasm32_switch_to (start332         * the unwind)" from "rewind re-execution of switch_to333         * (call asyncify_stop_rewind)." Required because334         * Binaryen 125 does NOT auto-inject stop_rewind at the335         * bottom of the rewind chain — the topmost helper is336         * responsible. Cleared on boot via the zero-initialized337         * thread_struct; toggled by switch_to. Bool semantically;338         * stored as u32 for alignment-with-other-fields parity. */339        unsigned int   unwound;340 341        /* R5 (§17.7 SIXTH HISTORICAL NOTE): per-task slot through342         * which the resume-path "actual outgoing task" pointer is343         * passed. __wasm32_yield's resume branch sets344         * next->thread.last_task = <real outgoing prev from the345         * just-completed switch_to> BEFORE asyncify_start_rewind.346         * On the rewind chain's bottom, __wasm32_switch_to reads347         * prev->thread.last_task (where prev == the resuming task348         * on rewind because the rewind chain restored its saved349         * locals), clears it, and returns it as the macro's350         * `last` argument. The upstream context_switch macro351         * `switch_to(prev, next, prev)` then assigns prev to that352         * return value, so finish_task_switch(prev) processes the353         * correct outgoing task. NULL on first-entry (no prior354         * unwind, no rewound switch_to to consume it). See the355         * "dual-path" note below for the separation between this356         * field and __wasm32_pending_prev. */357        struct task_struct *last_task;358    };359 360How the ABI works on a switch (post-R4b/R5):361 362::363 364    /* Returns the actual outgoing task (== `last` in upstream's365     * switch_to macro semantic). On unwind: prev. On rewind:366     * prev->thread.last_task (set by __wasm32_yield before367     * start_rewind). See "dual-path" note below. */368    struct task_struct *369    __wasm32_switch_to(struct task_struct *prev, struct task_struct *next)370    {371        /* On rewind re-entry the wasm trampoline re-executes the372         * call to __wasm32_switch_to with the SAME arguments. We373         * distinguish unwind (first call, unwound=false) from374         * rewind (second call, unwound=true) via the per-task gate375         * on prev->thread.unwound. */376        if (prev->thread.unwound) {377            struct task_struct *last = prev->thread.last_task;378            prev->thread.unwound = false;379            prev->thread.last_task = NULL;380            asyncify_stop_rewind();381            return last;382        }383 384        prev->thread.unwound = true;385        prev->thread.stack_top = __builtin_wasm_get_stack_pointer();386 387        /* Hand off prev to the post-unwind handler. The unwind chain388         * propagates upward to __wasm32_yield, which observes389         * state==UNWIND, picks the next task, and (in the resume390         * branch) stashes the real outgoing prev into391         * next->thread.last_task, then calls392         * asyncify_start_rewind(next->thread.asyncify_data); on the393         * resulting rewind chain, switch_to is re-executed with394         * the resuming task's saved arguments, sees unwound=true,395         * reads last_task, and returns it. */396        __wasm32_pending_prev = prev;397        asyncify_start_unwind(prev->thread.asyncify_data);398        /* If start_unwind returns to C (Binaryen's pass actually399         * threads control to the wasm-level epilogue at the post-400         * call check), this return value is consumed by the401         * switch_to macro as `last = prev`. */402        return prev;403    }404 405    /* The upstream switch_to macro consumes the return value: */406    #define switch_to(prev, next, last) do {                        \407        (last) = __wasm32_switch_to((prev), (next));                \408    } while (0)409 410**Dual-path: pending_prev vs last_task.** ``__wasm32_pending_prev``411(global, set by ``__wasm32_switch_to``) and412``next->thread.last_task`` (per-task, set by ``__wasm32_yield``413on the resume branch) are TWO DISTINCT MECHANISMS serving414two distinct dispatch paths:415 416* **First-entry dispatch** (the dispatched task has never417  run; ``next->thread.first_entry == true``; no saved418  Asyncify buffer to rewind from). ``__wasm32_yield`` reads419  ``__wasm32_pending_prev`` and passes it to420  ``schedule_tail(prev)`` so the new kthread's first action421  matches upstream's ret_from_fork contract. ``last_task``422  is NULL on this path because no rewound ``switch_to`` is423  going to consume it.424 425* **Resume dispatch** (the dispatched task has a saved426  Asyncify buffer; ``next->thread.first_entry == false``).427  ``__wasm32_yield`` BEFORE calling ``asyncify_start_rewind``:428 429  1. ``next->thread.last_task = __wasm32_pending_prev;``430     (stash the real outgoing task into the resuming task's431     slot, where the rewound ``switch_to`` can find it).432  2. ``__wasm32_pending_prev = NULL;`` (clear; resume path433     does NOT call ``schedule_tail`` — the rewound434     ``context_switch`` will call ``finish_task_switch``435     itself).436  3. ``asyncify_start_rewind(next->thread.asyncify_data);``437  4. ``(void)__wasm32_kthread_dispatch(next);`` — triggers438     the rewind chain, whose bottom is ``switch_to`` which439     reads ``prev->thread.last_task`` and returns it as440     the macro's ``last`` arg.441 442The right mechanism for a given dispatch is determined by443``next->thread.first_entry``. Conflating the two (e.g.444"just always read pending_prev from switch_to") fails445because on rewind, ``switch_to`` runs with restored saved446locals — it cannot reach the currently-set value of a447global without going through a memory location bound to a448saved frame, which would be a different bug.449 450This dual-path emerged from R5 (§17.7 SIXTH HISTORICAL451NOTE). The original K4 design used only ``pending_prev``;452the resume path read it and threw it away, on the453assumption "K5's user-process spawn lifecycle is where454last-task tracking matters." That assumption was refuted by455the kernel busy-spinning at 100% CPU after the first456resume because ``finish_task(pid 1)`` was called with the457saved (wrong) prev, leaving the actual outgoing task's458``on_cpu`` stuck at 1 — the next ``try_to_wake_up`` on that459task spun forever on460``smp_cond_load_acquire(&p->on_cpu, !VAL)``.461 462**Companion mechanism: deep_sp (R6 / §17.7 SEVENTH463HISTORICAL NOTE).** ``__wasm32_yield``'s resume branch464must restore TWO pieces of cross-rewind state, not one:465 466* ``next->thread.last_task`` — the actual outgoing task,467  carried so the rewound ``switch_to`` can return it as468  the macro's ``last`` arg (described above).469 470* ``next->thread.deep_sp`` — the471  ``__stack_pointer`` value at the moment of the original472  unwind (saved in ``__wasm32_switch_to`` BEFORE473  ``asyncify_start_unwind``), restored by474  ``__wasm32_yield`` BEFORE ``asyncify_start_rewind``.475 476The second mechanism exists because Binaryen's Asyncify477REWIND path does NOT execute each function's C-compiler478prologue. Function prologues on REWIND restore wasm479locals from the saved buffer and jump DIRECTLY to the480saved call site, SKIPPING the SP-descent that would481normally occur (``__stack_pointer -= frame_size``).482Through the entire rewind chain, no function descends483SP. So if yield set ``__stack_pointer = stack_base +484THREAD_SIZE`` (a fresh top) before485``asyncify_start_rewind``, the first normal call made486from a rewound function (e.g.487``context_switch -> finish_task_switch``) would allocate488its frame at ``stack_top - frame_size``, overwriting the489resumed task's still-live higher frames — empirically490clobbering pid 1's ``DECLARE_COMPLETION_ONSTACK(done)``491at ``stack_top - 0xc0`` in492``__kthread_create_on_node``.493 494The fix is to start the rewind chain at the deep SP495value the original unwind captured, so rewound calls'496frames land BELOW the live region. As each rewound497function returns, its epilogue does498``__stack_pointer += frame_size``, and SP climbs back to499``stack_top`` by the time dispatch returns to yield.500 501Concrete sequence in ``__wasm32_yield`` (resume branch),502showing both pieces of dual-path state being set up503together:504 505::506 507    /* Snapshot yield's own SP so we can restore it after508     * dispatch returns. saved_yield_sp is a scalar with no509     * taken address; clang keeps it in a wasm local across510     * the dispatch call, so it survives the SP swap. */511    saved_yield_sp = __wasm32_get_sp();512 513    if (next->thread.first_entry) {514        /* fresh stack — no prior rewind chain to preserve */515        __wasm32_set_sp(next->thread.stack_base + THREAD_SIZE);516        /* ... schedule_tail + dispatch ... */517    } else {518        /* preserve the deep SP from this task's last unwind */519        __wasm32_set_sp(next->thread.deep_sp);520        next->thread.last_task = __wasm32_pending_prev;521        __wasm32_pending_prev = NULL;522        asyncify_start_rewind(next->thread.asyncify_data);523        (void)__wasm32_kthread_dispatch(next);524    }525 526    /* After dispatch returns via the next yield's unwind527     * chain, each frame's epilogue restored SP, so SP is528     * back at stack_top. Restore yield's own SP. */529    __wasm32_set_sp(saved_yield_sp);530 531The ``__wasm32_get_sp`` / ``__wasm32_set_sp`` accessors532are implemented in ``arch/wasm32/lib/current.S`` as533direct ``global.get $__stack_pointer`` /534``global.set $__stack_pointer`` ops. C-level inline535assembly was attempted first and silently elided by536LLVM 22 even with ``volatile`` and ``"memory"`` clobber.537The ``.S`` boundary is opaque to the optimizer and each538accessor has no shadow-stack prologue, so calling it539does not itself adjust SP beyond the assignment.540 541The two cross-rewind pieces are independent:542 543* ``last_task`` matters for ``finish_task_switch`` /544  ``finish_task`` semantics (correct ``on_cpu``545  clearing). Without it: 100% CPU busy-spin on546  ``smp_cond_load_acquire``.547 548* ``deep_sp`` matters for memory safety of stack-resident549  data in the resumed task's still-live frames. Without550  it: silent stack corruption of higher frames, surfacing551  as data races on completion objects, locks, kernel-local552  variables. The corruption is non-deterministic from the553  test harness's view: which frame gets clobbered depends554  on which task makes the first normal call after rewind555  and what frame size that call requires.556 557Both pieces of state live in ``struct thread_struct``558(``arch/wasm32/include/asm/processor.h``) so they're559naturally per-task and persist across the unwind /560dispatch / rewind cycle. Future K-phase Asyncify users561(K5/K6/U-series) inherit this mechanism: every562cooperative-yield site must either save+restore deep_sp563explicitly OR run on a stack that is private to a single564resumable continuation.565 566The "control transfers to the wasm-level epilogue" comment hides567the actual mechanism, which is the subject of Q11 and is568fundamentally Binaryen-pass-internal: the Asyncify pass569augments switch_to's caller (``__schedule``'s context_switch570block) with a post-call check that observes571``state==UNWIND``, saves the caller's frame into572``asyncify_data``, and returns. The chain propagates up573through every instrumented function until it reaches the574outermost ``__wasm32_yield``.575 576**R4b correction (§17.7 FIFTH HISTORICAL NOTE).** Earlier577drafts of this section had ``switch_to`` setting578``asyncify_state = ASYNCIFY_STATE_NONE`` on rewind re-entry579*without* explicitly calling ``asyncify_stop_rewind``, on the580assumption that Binaryen would inject the state transition581automatically at the rewind chain's bottom. That assumption582was empirically refuted: ``asyncify_stop_rewind`` is never583called from generated code; the host (in our self-call584design, ``__wasm32_switch_to`` itself) must call it585explicitly. The gate flag distinguishes unwind from rewind586re-entry; setting ``asyncify_state`` by hand is NOT a587substitute for ``asyncify_stop_rewind`` because the latter588performs additional bookkeeping inside the synthesized589implementation.590 591**Failure mode if a non-whitelisted function reaches a yield.**592``asyncify-asserts`` makes wasm-opt emit runtime checks at every593function prologue: if ``asyncify_state == UNWIND`` and this function594isn't on the whitelist, trap. The kernel sees this as595``__builtin_trap()`` → Worker exit → HardwareJS observes "kernel596trap." Better than silent stack corruption.597 598Q3. HardwareJS ↔ vmlinux-Worker idle protocol599~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~600 601**Decision: master scheduler wakeup word at fixed offset in602``env.kernel_memory``. The vmlinux Worker parks on603``Atomics.wait(env.kernel_memory, sched_wakeup_offset,604expected_value)`` when its cooperative runqueue is empty.605Any party that makes a kthread runnable bumps the word and606notifies. The kernel exports the offset as a wasm global607``__sched_wakeup_word`` so HardwareJS doesn't have to hardcode it.**608 609Considered:610 611* **(i) ``Atomics.wait`` on a master futex word in env.kernel_memory.**612  Correct, scalable, lets the host OS truly suspend the Worker.613  Requires the contract: every code path that makes a kthread614  runnable must notify. The contract is small (single chokepoint615  is ``try_to_wake_up`` on kthreads + the scheduler tick's616  TIF_NEED_RESCHED set path).617 618* **(ii) Spin loop / setTimeout poll.** Trivially wrong: wastes a619  CPU core, doesn't let the host OS sleep, makes the laptop fan620  spin in an idle browser tab. Rejected.621 622* **(iii) Per-kthread wake words.** Could work but explodes the623  ABI surface (one offset per task, dynamic enrolment with624  HardwareJS at kthread_create). And the actual question the625  vmlinux Worker is asking is "ANY kthread runnable" — a626  single bitmask-like wake word is the right granularity.627 628**Rationale for (i).** Standard Linux UML / pre-KVM model629mechanism, transliterated to the browser. The single chokepoint630makes the contract auditable; the wake-word ABI is one offset631and one global symbol, not a fanout of per-task state.632 633Concrete spec:634 635* **Offset:** within ``env.kernel_memory``, picked by the kernel636  C code at allocation time and exported as wasm global637  ``__sched_wakeup_word`` (i32, immutable, pointing at the638  offset). HardwareJS reads the global at instantiation time and639  records the offset; no hardcoded constant in either party.640  Same pattern as ``__heap_base`` discovery at K2.641* **Type:** ``u32`` in ``env.kernel_memory``. Initial value: 0.642* **Semantics:** any positive value means "kernel C code has643  set TIF_NEED_RESCHED on at least one kthread; vmlinux Worker644  should re-enter the runqueue scan." The vmlinux Worker, when645  it parks, reads the current value and waits on it; any party646  that wakes a kthread increments the word and calls647  ``Atomics.notify(env.kernel_memory, sched_wakeup_offset, 1)``.648  The kernel C side decrements the word back to 0 once it has649  fully processed all wakeups (a single thread-safe ack pattern650  via ``atomic_xchg(&__sched_wakeup_word, 0)`` at the top of651  the cooperative-yield loop).652 653Concrete contract for non-kernel parties (HardwareJS, user654Workers, the JS-side timer fire path):655 656* HardwareJS exposes a host helper:657  ``linux.kick_scheduler()``. The kernel never calls this658  itself — it manipulates the word directly. The helper is for659  HardwareJS-side timers / process Workers / virtio backends /660  anything that wants the kernel to notice their effect on661  shared state.662* ``linux.kick_scheduler`` reads the kernel-exported663  ``__sched_wakeup_word`` global once at boot, then performs664  ``Atomics.add(env.kernel_memory, offset, 1)`` followed by665  ``Atomics.notify(env.kernel_memory, offset, 1)``. No666  per-call indirection.667 668**§15 debt row.** "Master scheduler wakeup word's offset669discovery currently requires HardwareJS to call into the670``__sched_wakeup_word`` global at instantiation time. If671HardwareJS-bookkept process Workers ever need to do this672discovery themselves (rather than going through the parent673HardwareJS), the offset must propagate via the process-Worker674spawn protocol." K7+.675 676Q4. THREAD_SIZE for wasm677~~~~~~~~~~~~~~~~~~~~~~~~678 679**Decision: ``THREAD_SIZE = 32 KiB`` (``THREAD_SIZE_ORDER = 3``,680with ``PAGE_SIZE = 4096``). Asyncify save buffer is a separate68116 KiB allocation. Per-task footprint: 48 KiB + ``task_struct``.**682 683Current state: ``arch/wasm32/include/asm/thread_info.h`` already684declares ``THREAD_SIZE_ORDER = 2`` (16 KiB). K4 bumps this to685``3``. Documented here so the bump is justified, not silent.686 687Considered:688 689* **16 KiB (status quo, Linux default).** Adequate for most690  arches because native code packs locals densely into691  registers + stack slots; ~64 frames @ 256 bytes is typical692  depth. wasm code emits every local as a typed wasm slot693  (i32 = 4 bytes minimum, i64 = 8 bytes, no register coloring694  at the wasm IR level), so per-frame stack consumption is695  ~1.5–2× heavier than native. Real-world: a printk path696  with vsnprintf + console_lock takes ~3 KiB of native stack;697  on wasm it's ~5 KiB. Lockdep on top of that adds ~6–8 KiB698  more. 16 KiB has zero headroom for any double-fault-grade699  diagnostic path.700 701* **32 KiB.** Doubles the headroom. ~7× per-kthread storage702  vs ``task_struct`` alone (32 KiB + 16 KiB asyncify_data + ~4 KiB703  task_struct ≈ 52 KiB), which is fine at K4 where we have704  at most ~6 kthreads at steady state (kthreadd, kworker,705  rcu_*, ksoftirqd, kernel_init). Worst case ~300 KiB of706  per-task storage; comfortable on a 64 MiB heap.707 708* **64 KiB.** Linus would beat us with a stick (this is one709  wasm-page, not a kernel-stack page). Overkill; reserved for710  K6+ if specific paths blow 32 KiB.711 712* **Dynamic per-task sizing via Kconfig.** Possible (NR_CPUS713  is already compile-time per Q4 of K3) but unnecessary at714  K4. Tunable post-v0.1.0 if measurement shows variance.715 716**Rationale for 32 KiB.** Doubles the headroom over native717Linux default to absorb wasm's heavier per-frame cost.718Measured against the K3 closing dmesg path (Mount-cache hash719init → kmalloc-driven slab traversal), the deepest observed720native stack on x86_64 for the equivalent path is ~6 KiB. We721budget 5× headroom (32 KiB) instead of 2.7× (16 KiB) so that722RCU + lockdep + a future trap-from-deep diagnostic don't723overflow. Update ``arch/wasm32/include/asm/thread_info.h``'s724``THREAD_SIZE_ORDER`` from 2 to 3 as part of the K4 implementation725commit (this file currently says "K6 may revisit"; this IS the726revisit, brought forward to K4 because Asyncify-aware kthreads727are the first real users).728 729**§15 debt row.** None — this is a justified configuration730decision, not a workaround. The ``thread_info.h`` comment is731updated in the K4 implementation commit.732 733Q5. Kernel-thread signal handling (KILL / STOP / freezer)734~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~735 736**Decision: OUT of K4 scope. ``signal_pending()`` returns 0 for737kthreads at K4. ``kthread_stop()`` works (it uses738``KTHREAD_SHOULD_STOP`` bit + ``wake_up_process``, both of which739are kthread-mechanism not signal-mechanism). ``KILL``, ``STOP``,740``CONT``, and the freezer/suspend signal paths are stubbed.**741 742Considered:743 744* **In-scope.** Kthread signals are used by the freezer745  (suspend/resume), by deliberate test code, and by746  out-of-process kernel debuggers. None of these are747  K4-acceptance-relevant.748 749* **Out-of-scope, stubbed.** Saves work, doesn't block any K4750  acceptance criterion, defers the decision to whenever a751  consumer becomes interesting (probably K6 if freezer comes752  in, possibly never if the browser target makes "suspend the753  whole virtual machine" unnecessary).754 755**Rationale.** The browser target's "suspend" model is the756host hiding the tab; the wasm runtime stops scheduling time to757the Worker; on resume, the wasm Worker just picks up where it758was. Linux's freezer infrastructure is geared toward managing759suspend on devices with persistent state across cold boot —760NOT our situation. Defer until something concrete needs it.761 762**§15 debt row.** "Kthread signal handling deferred." See763§0(c)(5).764 765Q6. kernel_init failure shape766~~~~~~~~~~~~~~~~~~~~~~~~~~~~~767 768**Decision: ``kernel_init`` runs as a kthread, attempts769``run_init_process(/init)`` and the other usual paths770(``/sbin/init``, ``/etc/init``, ``/bin/init``, ``/bin/sh``),771all fail with -ENOENT, and the kernel panics with the upstream772"No working init found." message. This panic IS the K5 boundary.**773 774Considered:775 776* **Panic at "No working init found."** Upstream-Linux-exact.777  Triggers ``panic()`` → ``machine_restart`` (which currently778  ``__builtin_trap()``-s per §15) → vmlinux Worker exit. The779  K4 vitest observes "kernel-trap" with the panic message in780  dmesg.781 782* **Hang.** Would also satisfy "reaches kernel_init"; less783  diagnostic. Rejected.784 785* **Synthesize a fake successful exec.** Would let K4 boot786  proceed past kernel_init; would be a lie. Rejected.787 788**Rationale.** "No working init found" is the canonical789"kernel is fully booted, userland is missing" Linux signal.790Anyone reading our K4 dmesg recognizes it instantly. K5791satisfies it by providing /init (which is the userland792binfmt_wasm-spawned first user process).793 794**§15 debt row.** None — this is the K5 boundary, not debt.795 796Q7. Asyncify tool selection (Binaryen vs JSPI vs custom)797~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~798 799**Decision: Binaryen's ``wasm-opt --asyncify`` pass, pinned800toolchain version recorded alongside the LLVM 22 / Node 25 pin801already established at K3 (mm-model.rst §2 Q3).**802 803Considered:804 805* **Binaryen asyncify.** Mature (~5 years in production at806  Emscripten / Cheerp). Tool-independent — works on any wasm807  runtime that implements the threads / atomics proposals;808  no engine-level cooperative-suspension support required.809  State stays in linear memory under our control. Generates810  enough hooks for our needs. Pinned version recorded for811  K4 reproducibility (Binaryen ≥ 119; the upcoming K4812  commit records the exact version against the Q3813  ``clang/wasm-ld 60513b8d``-pinned toolchain).814 815* **JSPI (JS Promise Integration).** Engine-level816  cooperative-suspension support, shipping in V8 since v25817  and in SpiderMonkey since 132. Requires marking imports as818  ``suspending`` at JS instantiation time (a §18-flavour819  JS-object-shape ABI constraint); requires the kernel to820  surface yield points as imports rather than internal821  function calls; pulls Promise plumbing into the kernel-822  HardwareJS boundary. Re-evaluable at K7+ once we have823  perf data from Binaryen-asyncify in real workloads. Right824  now JSPI's cost (ABI surface area, host-side promise825  glue, engine-version requirement) outweighs its benefit826  (~25% less binary size than asyncify).827 828* **Custom continuation-passing transformation.** Reinvent829  Binaryen's wheel. Rejected — categorically the kind of830  YAK §16 rejects.831 832**Rationale.** Binaryen's asyncify is the standard,833maintained, runtime-agnostic answer. K4 records the pinned834version in commit body and in ``Documentation/wasm/toolchain.rst``835(or wherever the toolchain pin spec lives at K4 close — if836that doc doesn't exist yet, K4 introduces a one-page version).837 838**§15 debt row.** "Re-evaluate Binaryen asyncify vs JSPI at839K7+ when we have real-workload perf data." Soft debt — not840blocking, but flag for revisit.841 842Q8. cond_resched() maintenance contract843~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~844 845**Decision: every long-running kernel-C loop MUST contain846explicit ``cond_resched()`` calls at intervals; the K4847cooperative model has no other yield mechanism.**848 849Status: this is upstream Linux convention already (RT and850PREEMPT_NONE configs both rely on it). The K4 thing is: on851this port, ``cond_resched()`` is the ONLY yield primitive852short of a full block. Any kernel C path that loops without853``cond_resched()`` will starve other kthreads.854 855Implications:856 857* upstream Linux's already-present ``cond_resched()`` calls858  in ``mm/`` and ``kernel/`` Just Work.859* If a future K-phase exercises a kernel-internal loop that860  is rare on real arches and so was never instrumented with861  ``cond_resched()`` upstream, the K-phase commit MUST add862  ``cond_resched()`` (preferably push the fix upstream).863* User code in arch/wasm32 (currently just K2's setup_arch864  validation loops; nothing K3 added except memblock-walk865  loops which all have ``cond_resched()`` upstream) follows866  the same rule.867 868**§15 debt row.** None — this is a convention, not a debt869item; it's documented here so future K-phase reviewers know870to look for it.871 872Q9. printk-from-asyncify and the Never-Yields blacklist873~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~874 875**Decision: ``printk`` and all functions it calls876(``vsnprintf``, ``console_drivers``' write callbacks,877``__wasm32_dmesg_write``) are explicitly excluded from the878Asyncify whitelist. ``asyncify-asserts`` will trap if any of879them reaches a yield point.**880 881Why: ``printk`` runs from any context — including ones where882the caller has already started an unwind. If printk itself883yielded, the unwind chain would re-enter printk during884rewind, and printk's locks would deadlock.885 886This is the inverse of Q8: cond_resched is mandatory on887allowed-to-yield paths; printk is mandatory on the888NEVER-yields list. Both are convention contracts; both are889enforced at runtime by Binaryen's asyncify-asserts pass.890 891**The blacklist** (initial K4 set, expandable):892 893* ``printk`` and ``vprintk`` family.894* ``vsnprintf``, ``snprintf``, ``sprintf``.895* ``__wasm32_dmesg_write`` (host-import callback target).896* ``console_drivers``' ``write`` and ``flush`` callbacks (just897  the bootconsole at K4).898* ``rcu_read_lock`` / ``rcu_read_unlock`` and the899  rcu_read-side fast paths (yielding inside an RCU read-side900  critical section breaks the grace period contract).901* ``spin_lock`` / ``spin_unlock`` / ``raw_spin_lock`` /902  ``raw_spin_unlock`` (yielding while holding a spinlock903  deadlocks any waiter).904* ``BUG`` / ``BUG_ON`` / ``WARN`` / ``WARN_ON`` (no point905  yielding from a diagnostic-emit path).906 907Generation: ``scripts/asyncify-whitelist.py`` generates908``arch/wasm32/asyncify-whitelist.txt`` from the yield-root909reachability closure; the blacklist is the COMPLEMENT910of that whitelist by construction. Asyncify-asserts only911emits checks against the whitelist, so the blacklist's912"never yields" property follows from list-membership.913 914**§15 debt row.** None — the rule is implementation-level915and lives in the whitelist generator.916 917Q10. workqueue / kworker918~~~~~~~~~~~~~~~~~~~~~~~~919 920**Decision: ``kworker`` per-CPU threads are normal kthreads921under our K4 machinery; no special arch hook needed.922Upstream ``workqueue.c`` calls ``kthread_create`` /923``kthread_create_on_cpu`` for each kworker; K4's924``kthread_create`` implementation is complete enough that925these kworkers spawn, park on their wait queues, and run926work items cooperatively when an item is queued.**927 928Status check: K3 already linked ``workqueue.c`` (and panicked929in ``init_pwq`` until ``CONFIG_SLUB_TINY=y`` made the per-CPU930slab allocations work). K4's contribution is that the kthread931the workqueue infrastructure tries to spawn at the bottom of932the K3 progression — ``kworker/0:0``, ``kworker/0:0H``, etc.933— actually run for the first time.934 935**§15 debt row.** None — this Just Works through composition936of K3 and K4.937 938Q11. Unwind/rewind reentry pattern939~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~940 941**Decision: self-call reentry from inside the kernel C code942(``arch/wasm32/kernel/asyncify.c::__wasm32_yield``). NO host-943side reentry loop. The host (HardwareJS, vmlinux Worker)944sees the kernel as a single ``wasm_start_kernel`` call that945keeps internally yielding to itself.**946 947Considered:948 949* **(i) Self-call from kernel C.** The yield function does:950 951  ::952 953      while (1) {954          /* asyncify_state = UNWIND set by __wasm32_switch_to */955          asyncify_stop_unwind();956          /* prev's stack is now saved in prev->thread.asyncify_data. */957          struct task_struct *next = pick_next_runnable();958          if (!next) {959              /* Park on master wakeup word. */960              __wasm32_park_until_runnable();961              continue;962          }963          /* Set up rewind. */964          asyncify_state = ASYNCIFY_STATE_REWIND;965          asyncify_data  = next->thread.asyncify_data;966          /* Call back into the kernel — Asyncify rewinds into967           * next's saved continuation. */968          __wasm32_kernel_resume();  // a separate function so969                                     // Asyncify treats it as a970                                     // new entry point.971          /* (control never returns here; rewind transfers into972           *  next's frame instead.) */973      }974 975  Self-contained: HardwareJS doesn't have to know that976  switch_to happens. The vmlinux Worker enters977  ``wasm_start_kernel`` once and stays there until panic /978  trap.979 980* **(ii) Host-loop reentry.** The kernel returns to981  HardwareJS at every switch_to, HardwareJS observes the982  state, calls back in. Trivially exposes the983  Asyncify-state ABI to HardwareJS — bad: §18 says JS-side984  shape constraints belong in docs; Asyncify-state shape985  is one. Also slower (one host round-trip per context986  switch). Rejected.987 988**Rationale.** Keep Asyncify state entirely inside the989kernel module; HardwareJS sees no change from K3 except990that ``wasm_start_kernel`` now runs forever (until panic),991parking on the scheduler wakeup futex when idle, instead992of returning when it reached the K3 hang.993 994**§15 debt row.** None — this is the implementation choice995the K4 code implements; nothing later phases need to revisit.996 997Q12. Kthread first-entry shim998~~~~~~~~~~~~~~~~~~~~~~~~~~~~~999 1000**Decision: ``kthread_create`` allocates the per-task1001asyncify_data buffer, kernel stack, and ``thread_struct``;1002then enqueues the kthread in the cooperative runqueue with1003an "initial" Asyncify state that causes the first rewind1004into the kthread's entry function (``kthread`` ->1005``threadfn(data)``) rather than into a previously-saved1006continuation.**1007 1008Mechanism: the initial asyncify_data buffer is hand-1009constructed by ``__wasm32_kthread_init`` to look like a1010saved state at the entry of ``kthread()`` (the kernel's1011``kthread/kthread.c::kthread`` function, the universal1012kthread entry trampoline). Specifically:1013 1014* The save buffer's header indicates "rewind into ``kthread``1015  at function entry."1016* The buffer's locals area holds ``data`` and ``threadfn``1017  as if they had just been pushed for a normal call.1018 1019This is a one-shot hand-construction. Subsequent yields and1020resumes use the normal Asyncify-save mechanism.1021 1022**§15 debt row.** "Hand-constructed initial asyncify_data1023buffer depends on Binaryen's per-function save-buffer1024layout, which is a Binaryen-private implementation detail.1025A Binaryen version bump could break the layout assumption.1026Detection: ``asyncify-asserts`` catches it as a corrupt1027restore at the first kthread run." Owed: pinned-toolchain1028discipline (Q7). Not K-phase-tied.1029 1030Q13. Asyncify state buffer sizing1031~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1032 1033**Decision: ``ASYNCIFY_DATA_SIZE = 16 KiB`` per kthread.1034Empirically validated at K4 close against the deepest1035observed save-state depth from the K4 acceptance run.**1036 1037Considered:1038 1039* **4 KiB.** Adequate for shallow callers (kthreadd's main1040  loop is ~4 frames deep). Insufficient for any path that1041  goes through ``schedule_timeout`` → ``hrtimer`` → RCU.1042 1043* **16 KiB.** Generous enough to handle the deepest1044  observed K4 path (~20 frames, ~9 KiB save state).1045  Matches THREAD_SIZE_ORDER's reasoning of "5× headroom over1046  observed worst case."1047 1048* **THREAD_SIZE.** Tempting to colocate them (one allocation1049  of 32 KiB total). Slightly wasteful — the save buffer1050  doesn't grow with stack depth linearly, it grows with1051  number of active call frames × per-frame locals, which1052  is roughly half the C stack consumption. Separate1053  allocations keep the buffer sizing tunable1054  independently of stack sizing.1055 1056**Rationale.** 16 KiB is the smallest power-of-two that1057provides 5× headroom over the deepest observed save-state1058in the K4 boot path. Recorded at K4 close with the actual1059high-water-mark.1060 1061**§15 debt row.** None — tunable Kconfig constant; revisit1062if K-phase boot paths grow deeper.1063 1064--------------------------------------------------------------------------------10653. Asyncify mechanism1066--------------------------------------------------------------------------------1067 10683.1 Whitelist generation1069~~~~~~~~~~~~~~~~~~~~~~~~1070 1071**K4 STATUS:** The curated-whitelist approach below is the1072sched-model design target. **K4 ships without** a curated1073whitelist or the generator script: Binaryen's default1074instrumentation scope (every function transitively calling1075``__wasm32_switch_to`` gets the save/restore prologue) is used1076instead. This results in a ~2-3× larger ``vmlinux.wasm``1077post-Asyncify than the whitelist would produce. Deferral1078rationale + retirement protocol is recorded as a §15 row in1079``docs/ARCHITECTURE.md`` ("Asyncify pass runs with Binaryen1080DEFAULT instrumentation scope"). K6 lands the whitelist1081generator and reduces the binary; K4 records the no-whitelist1082baseline as the K6-target reduction reference.1083 1084The DESIGN (K6 target):1085 1086``scripts/asyncify-whitelist.py`` (introduced at K4 in spec,1087deferred to K6 in implementation, sibling to1088``wasm-add-memory.py``, ``wasm-inspect.py``, and the upcoming1089K5/K6 percpu post-link pass) emits1090``arch/wasm32/asyncify-whitelist.txt``.1091 1092Algorithm:1093 10941. Walk the pre-Asyncify ``vmlinux.wasm`` symbol table via1095   ``llvm-objdump --syms``.10962. Build a call graph from the wasm ``code`` section's1097   ``call`` and ``call_indirect`` instructions, mapping1098   each function index to its callees.10993. Seed the closure with the YIELD ROOTS set:1100 1101   ::1102 1103       schedule1104       cond_resched1105       wait_event1106       wait_event_interruptible1107       wait_event_timeout1108       wait_event_interruptible_timeout1109       msleep1110       msleep_interruptible1111       schedule_timeout1112       schedule_timeout_interruptible1113       wait_for_completion1114       wait_for_completion_interruptible1115       wait_for_completion_killable1116       __wasm32_switch_to1117 11184. Transitively include all CALLERS of yield roots (because1119   any caller of a yielding function might observe an1120   unwind through its own frame).11215. Apply the BLACKLIST exclusion (Q9), removing the printk /1122   spinlock / RCU-read-side functions from the whitelist even1123   if they appear transitively. ``asyncify-asserts`` will1124   catch any indirect-call escape at runtime.11256. Emit the whitelist as one function-name-per-line,1126   alphabetized, in ``arch/wasm32/asyncify-whitelist.txt``.1127 1128The build system invokes ``wasm-opt`` at K4 with::1129 1130    wasm-opt -g vmlinux.wasm \1131      --asyncify \1132      --pass-arg=asyncify-asserts \1133      --pass-arg=asyncify-removelist@__wasm32_yield \1134      --pass-arg=asyncify-removelist@wasm_start_kernel \1135      -o vmlinux.wasm.asyncify-with-asserts1136 1137(One ``--pass-arg=asyncify-removelist@NAME`` per function: Binaryen1138125 does not accept ``@file`` syntax for the removelist, as verified1139by the K4 asserts-probe and per the build script's read of1140``arch/wasm32/kernel/asyncify-removelist.txt``.)1141 1142The K6 target invocation will add::1143 1144      --pass-arg=asyncify-only-list@arch/wasm32/asyncify-whitelist.txt1145 1146The ``-g`` flag preserves the function-name section through the1147pass; the post-pass strip tool requires it to resolve names from1148``asyncify-topmost.txt``.1149 1150Then the topmost-runtime asserts get surgically stripped::1151 1152    scripts/asyncify-strip-topmost-asserts.py \1153      vmlinux.wasm.asyncify-with-asserts \1154      --topmost-list arch/wasm32/kernel/asyncify-topmost.txt \1155      -o vmlinux.wasm.asyncify1156 1157The Asyncify pass runs AFTER the K3 percpu-anchor-bracket1158final link and BEFORE the K3 user_memory splice (so1159``wasm-add-memory.py`` operates on the strip-tool's output;1160the splice has no opinion about Asyncify).1161 1162Toolchain drift in either the asyncify pass or the strip tool1163is caught at CI by the two sibling probes,1164``tools/k4-binaryen-asyncify-probe/`` (asyncify_data layout) and1165``tools/k4-asyncify-asserts-probe/`` (strip-tool surgical1166behavior). Both run on every build, so a Binaryen bump that1167changes either the assert encoding or the state machine fails1168the probe before vmlinux mysteriously traps in1169``__wasm32_switch_to``.1170 11713.1a Topmost-list vs removelist: which list, when?1172~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1173 1174Two adjacent ABI files cooperate with the Asyncify pass +1175strip tool: ``arch/wasm32/kernel/asyncify-topmost.txt`` (read1176by ``scripts/asyncify-strip-topmost-asserts.py``) and1177``arch/wasm32/kernel/asyncify-removelist.txt`` (read by1178``scripts/link-vmlinux-wasm.sh`` and emitted as a single1179``--pass-arg=asyncify-removelist@A,B,C`` comma-joined1180argument; see R4a in ``docs/ARCHITECTURE.md`` §17.7 FOURTH1181HISTORICAL NOTE for why one-per-arg silently drops all but1182the last). The interaction is subtle enough that the next1183person adding a topmost helper should NOT have to re-derive1184it from a confusing build failure. Decision table (post-R4b):1185 1186============================================ ============== ============== ============================================1187Function role                                topmost-list   removelist     What goes wrong if either is omitted /1188                                                                          mistakenly included1189============================================ ============== ============== ============================================1190Pure topmost helper: calls ``asyncify_start_  YES            **NO**         topmost-list-only is correct.1191unwind`` (or ``asyncify_stop_rewind``)1192directly and is otherwise call-graph-leaf1193on the asyncify side (e.g.1194``__wasm32_switch_to``). Binaryen's1195scanner sets ``info.isTopMostRuntime=true``1196for any function calling ``START_UNWIND`` or1197``STOP_REWIND`` directly;1198``needsInstrumentation`` then returns false1199unconditionally. The auto-detection is1200sufficient — no removelist entry needed.1201``asyncify-asserts`` injects asserts; we1202strip them via topmost-list.1203                                              ---            ---            **Omit topmost-list:** asserts remain;1204                                                                            function traps at runtime on the1205                                                                            legitimate state change.1206                                              ---            ---            **Mistakenly add to removelist:** see R4b1207                                                                            (``§17.7`` FIFTH HISTORICAL NOTE).1208                                                                            ``asyncify-removelist`` ALSO sets1209                                                                            ``info.canChangeState=false``, which1210                                                                            propagates back through the call graph and1211                                                                            suppresses the post-call state-check that1212                                                                            *callers* of the listed function would1213                                                                            otherwise receive. The caller's frame is1214                                                                            never saved on unwind; on rewind the buffer1215                                                                            is one frame short and the caller's prologue1216                                                                            reads garbage. Symptom: kernel trap inside1217                                                                            ``vprintk_emit`` via ``__schedule_bug``1218                                                                            triggered by a garbage call-site-idx in the1219                                                                            caller's saved-but-corrupted locals.1220-------------------------------------------- -------------- -------------- --------------------------------------------1221Driver function: observes ``state==UNWIND``  YES            YES            BOTH required.1222across a child call and explicitly calls1223``asyncify_stop_unwind`` /1224``asyncify_start_rewind`` (e.g.1225``__wasm32_yield``, ``wasm_start_kernel``).1226Binaryen would normally INSTRUMENT this for1227save/restore (because it transitively1228reaches ``asyncify_start_unwind`` via the1229child call) AND has ``canChangeState=true``1230because no removelist entry suppresses it;1231we must force NON-instrumentation via1232removelist AND strip the resulting1233asserts via topmost-list.1234 1235The post-call check suppression on1236removelisted driver callers is fine here:1237the driver IS the bottom of the asyncify1238call chain (post-unwind it re-enters the1239yield loop in the host, not a higher-level1240kernel function); no upward propagation1241matters because there is no caller frame1242that needs saving.1243                                              ---            ---            **Omit removelist:** Binaryen inserts1244                                                                            save/restore prologue; on unwind, the1245                                                                            driver silently saves its own frame and1246                                                                            propagates upward — the unwind escapes1247                                                                            the kernel (host-loop reentry, Q111248                                                                            rejected). Symptom: wasm_start_kernel1249                                                                            returns to HardwareJS with1250                                                                            state==UNWIND, kernel exits.1251                                              ---            ---            **Omit topmost-list:** asserts remain in1252                                                                            non-instrumented function; driver traps1253                                                                            the moment its child returns with1254                                                                            state==UNWIND. Symptom: kernel trap on1255                                                                            the FIRST schedule() call.1256-------------------------------------------- -------------- -------------- --------------------------------------------1257Plain whitelisted callee: appears on the     no             no             neither required. Binaryen instruments1258yield call chain (e.g. upstream                                            for save/restore, asserts don't apply1259``context_switch``, ``__schedule``). No                                    to instrumented functions.1260direct asyncify_* state-machine calls.1261============================================ ============== ============== ============================================1262 1263Cheat sheet for adding a new function:1264 1265* "I call ``asyncify_start_unwind`` directly and that's all,1266  the rest of the asyncify state-machine is somebody else's1267  problem" → topmost-list ONLY. **Do NOT add to removelist1268  — Binaryen's auto-detection already does the right thing,1269  AND removelist propagates canChangeState=false to your1270  callers in a way that breaks their frame saves.**1271* "I'm a driver that needs to observe state changes across1272  child calls" → topmost-list AND removelist.1273* "I just need to be in the unwind path" → neither (let1274  Binaryen's default instrumentation handle it).1275 1276The build fails loudly if a topmost-list name doesn't appear1277in the binary (typo / dead-code stripping detection). The1278removelist does NOT fail loudly on typos — Binaryen emits a1279warning and continues. Cross-check by reading the linker log1280for ``warning: Asyncify removelist contained a non-existing1281function name`` and fix the spelling.1282 1283The ``linux-wasm/tools/k4-binaryen-asyncify-probe/`` sub-tests1284verify both the removelist's caller-side-propagation1285behavior and the multi-entry comma-syntax against the pinned1286Binaryen; they fail loudly if a version bump changes either1287semantic.1288 12893.2 wasm globals1290~~~~~~~~~~~~~~~~1291 1292After Binaryen's asyncify pass, ``vmlinux.wasm`` contains the1293following NEW wasm globals:1294 1295============== ====== =================== =====================================1296Name           Type   Mutable             Semantics1297============== ====== =================== =====================================1298asyncify_state i32    mut                 0=NONE, 1=UNWIND, 2=REWIND.1299                                          Set by ``__wasm32_switch_to``,1300                                          read by every whitelisted function1301                                          at prologue/epilogue.1302asyncify_data  i32    mut                 Offset in default memory of the1303                                          save buffer for the current1304                                          unwind/rewind operation. Set by1305                                          ``__wasm32_switch_to`` to1306                                          ``{prev,next}->thread.asyncify_data``.1307============== ====== =================== =====================================1308 1309Plus the existing K3 globals (``__heap_base``, ``__stack_pointer``,1310``__data_end``).1311 1312The kernel C side accesses these via Binaryen's clang builtins:1313 1314::1315 1316    extern unsigned int asyncify_state __attribute__((import_module("env"),1317                                                       import_name("asyncify_state")));1318 1319Wait — Binaryen's asyncify pass DEFINES these globals as part of1320the module's local globals, not as imports. The correct C access1321pattern is via inline asm:1322 1323::1324 1325    static inline u32 __wasm32_asyncify_get_state(void) {1326        u32 r;1327        asm volatile ("global.get $asyncify_state\n local.set %0" : "=r"(r));1328        return r;1329    }1330 1331The K4 implementation commit pins the exact accessor pattern —1332this section will be updated at K4 close with the actual idiom1333that landed (the same one the Q3 cross-memory atomic probe1334pioneered: empirical proof at the K4 opener that the C-side1335access works in our toolchain).1336 13373.3 Binary-size delta tracking1338~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1339 1340Every K-phase's close-commit body from K4 onward includes a1341``vmlinux.wasm size`` line and a per-kthread overhead line.1342 1343**K4 close commit body — REQUIRED format** (this is not1344optional; the K4-close discipline depends on the K6 reduction1345target being a measurable named number, not a fuzzy memory):1346 1347::1348 1349    vmlinux.wasm size (post-Asyncify, post-user_memory-splice):1350      K3:  N bytes (pre-Asyncify, recorded retrospectively at K4 open).1351      K4:  N bytes  (Δ +X% vs K3 baseline)1352      K4 is the K6 reduction BASELINE: Q1 curated whitelist1353      deferred to K6 (see §15 row "Asyncify pass runs with1354      Binaryen DEFAULT instrumentation scope"). K6 reduction1355      target: −N% of post-Asyncify section size, OR an1356      absolute (X − Y) MiB cut, whichever is the tighter1357      number K6 commits to. Without recording the K4 number1358      explicitly NOW, the K6 measurement loses its reference.1359 1360      K5:  N bytes  (Δ +Y% vs K4)1361      K6:  N bytes  (Δ +Z% vs K4 baseline; expected NEGATIVE)1362      ...1363 1364    kthread overhead bytes:1365      formula:  nr_kthreads × (THREAD_SIZE + ASYNCIFY_DATA_SIZE)1366                            = nr_kthreads × (32 KiB + 16 KiB)1367                            = nr_kthreads × 48 KiB1368      K4:  nr_kthreads_at_steady_state × 48 KiB = ??? KiB1369      K5:  ...1370      K6:  ...1371    Notes:1372      - With NR_CPUS=8 and the kworkers + RCU helpers + ksoftirqd +1373        the usual zoo, expect ~50–80 kthreads at full kernel bring-up1374        in late K-phases, so ~2.5–4 MiB of pure kthread overhead1375        before any userspace runs.1376      - That's fine on a 64 MiB SAB but would dominate on a smaller1377        one. If this line ever exceeds 8 MiB before userspace, revisit1378        THREAD_SIZE_ORDER and ASYNCIFY_DATA_SIZE.1379 1380Companion to the K3-instituted ``wasm-inspect.py --atomic-count``1381line. Becomes a permanent line in every phase's close-commit1382metrics block, alongside ``wasm-inspect.py --asyncify-info`` for1383the asyncify-callsite tally introduced at K4.1384 1385The K6 close-commit MUST cite the K4 baseline by number when1386recording its reduction. "Reduced by N%" without naming the1387absolute number against which N is measured is not acceptable1388under this convention — the K4 number IS the contract.1389 1390--------------------------------------------------------------------------------13914. ``switch_to`` and the per-task ABI1392--------------------------------------------------------------------------------1393 1394Per Q2 above. The on-the-wire ABI between1395``__wasm32_switch_to(prev, next)`` and the rest of the kernel1396(call sites in ``kernel/sched/core.c::context_switch``) is the1397standard upstream contract — no changes. The novelty is entirely1398in ``__wasm32_switch_to``'s implementation.1399 1400Files (all K4-introduced):1401 1402* ``arch/wasm32/include/asm/switch_to.h`` (already exists from1403  K3; K4 makes its body real).1404* ``arch/wasm32/include/asm/thread_info.h`` (extends with1405  ``stack_top`` per Q2; bumps ``THREAD_SIZE_ORDER`` to 3 per Q4).1406* ``arch/wasm32/include/asm/processor.h`` (extends1407  ``struct thread_struct`` with the layout in Q2).1408* ``arch/wasm32/kernel/switch_to.c`` (the function body — sets1409  the two Asyncify globals, calls ``asyncify_start_unwind``,1410  returns; the rewind happens via Q11's reentry pattern).1411* ``arch/wasm32/kernel/asyncify.c`` (the cooperative runqueue1412  + reentry loop in ``__wasm32_yield``).1413 1414The ABI is single-Worker (vmlinux), single-CPU-fiction1415(num_online_cpus() == 1) at K4. ``switch_to`` is never invoked1416from any other Worker.1417 1418--------------------------------------------------------------------------------14195. Cooperative runqueue (Regime 2 implementation)1420--------------------------------------------------------------------------------1421 1422A small FIFO ring buffer in ``arch/wasm32/kernel/asyncify.c``,1423exposed through ``wasm_sched_class`` (``arch/wasm32/kernel/1424sched.c``) per ``docs/ARCHITECTURE.md §17.7``:1425 1426::1427 1428    static LIST_HEAD(wasm32_rq);1429    static DEFINE_RAW_SPINLOCK(wasm32_rq_lock);1430 1431    void wasm32_rq_enqueue(struct task_struct *p) {1432        unsigned long flags;1433        raw_spin_lock_irqsave(&wasm32_rq_lock, flags);1434        list_add_tail(&p->thread.rq_node, &wasm32_rq);1435        raw_spin_unlock_irqrestore(&wasm32_rq_lock, flags);1436    }1437 1438    struct task_struct *wasm32_rq_peek(void) {1439        struct task_struct *p = NULL;1440        unsigned long flags;1441        raw_spin_lock_irqsave(&wasm32_rq_lock, flags);1442        if (!list_empty(&wasm32_rq))1443            p = list_first_entry(&wasm32_rq, struct task_struct,1444                                 thread.rq_node);1445        raw_spin_unlock_irqrestore(&wasm32_rq_lock, flags);1446        return p;1447    }1448 1449There is no ``wasm32_rq_dequeue`` primitive at the rq level.1450Removal from ``wasm32_rq`` happens *exclusively* through1451``dequeue_task_wasm`` (the ``wasm_sched_class`` method), which1452is itself called *exclusively* from1453``deactivate_task`` along the state-changing yield path of1454``__schedule`` (path A — see §5.3 below). This is the1455peek-not-pop discipline.1456 1457Concurrency: the spinlock is needed because process Workers1458(Regime 1, K5+) can ``wake_up_process`` a kthread, and1459``try_to_wake_up`` lands in our ``wasm_sched_class.enqueue_task``1460which is the front-door for ``wasm32_rq_enqueue``. At K4 there's1461only the vmlinux Worker, so the lock is uncontended; the cost is1462one ``i32.atomic.rmw.cmpxchg`` per peek / enqueue (K3-real1463atomic). K5 starts to actually contend it.1464 1465The "irqsave" variant is what kernel locking convention1466mandates here even though we have no IRQs — the macros1467compile to non-IRQ-flag-saving versions on1468``CONFIG_PREEMPT_NONE``-equivalent configs but the1469discipline keeps the kernel C code uniform across arches.1470 1471``wasm_sched_class`` wires these primitives through:1472 1473* ``enqueue_task_wasm(rq, p, flags)`` → ``wasm32_rq_enqueue(p)``.1474  Called from ``activate_task`` along the1475  ``wake_up_new_task`` / ``try_to_wake_up`` paths. At K4 BUG_ONs1476  if ``p->mm != NULL`` (Regime 1 is K5+).1477* ``dequeue_task_wasm(rq, p, flags)`` → removes ``p`` from1478  ``wasm32_rq`` via ``list_del_init(&p->thread.rq_node)`` and1479  decrements ``rq->nr_running``. Called from1480  ``__schedule → block_task → dequeue_task`` when a task goes1481  to sleep (state set to a non-RUNNABLE state by the caller1482  before ``schedule()``), and from ``deactivate_task`` during1483  task migration (K5+). This is the **only** removal site1484  from ``wasm32_rq``; see §5.3 (peek-not-pop) for why.1485* ``pick_next_task_wasm(rq, prev)`` → returns1486  ``list_first_entry(&wasm32_rq, ...)`` without removing1487  (peek). If ``wasm32_rq`` is empty, returns NULL and1488  ``__schedule`` selects ``rq->idle``. The K5+ contract: a1489  task is on ``wasm32_rq`` if and only if it is runnable;1490  ``pick_next_task_wasm`` is allowed to return ``prev`` itself1491  (which then short-circuits ``__schedule``'s1492  ``prev != next`` branch — no ``context_switch``, no unwind,1493  the running task continues).1494* ``task_fork_wasm(p)`` → ensures the new task's1495  ``thread.rq_node`` is a valid ``LIST_HEAD_INIT``-equivalent1496  empty node and its ``first_entry`` flag is true.1497 1498``__wasm32_yield`` (the cooperative dispatcher, see §3.2 and §41499above) also peeks rather than pops. The dispatched task remains1500at the head of ``wasm32_rq`` while it is executing. When the1501task voluntarily yields back through ``__schedule``, the1502state-of-the-runqueue decides what happens next; see §5.3.1503 1504All other ``sched_class`` methods (``task_tick``,1505``check_preempt_curr``, ``select_task_rq``, ``update_curr``,1506``prio_changed``, ``switched_from``, ``switched_to``,1507``balance``, ``yield_task``, ``yield_to_task``,1508``migrate_task_rq``, ``rq_online``, ``rq_offline``,1509``task_dead``) are no-ops, each guarded by a ``WARN_ONCE`` so1510the first call from a path we didn't anticipate is observable.1511The no-op set is the K4 maintenance contract: a method that1512WARN_ONCE-fires in CI is a method we owe a real implementation1513of, OR a path we owe a "why this never fires" note in this1514section.1515 15165.1 Pinning ``p->sched_class`` to ``wasm_sched_class``: two arch hooks1517~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1518 1519Routing the class methods correctly requires that every task's1520``p->sched_class`` actually point at ``&wasm_sched_class``. The1521class is correctly wired into the section ordering and is1522returned by the cooperative dispatcher, but upstream code in1523``kernel/sched/`` writes ``p->sched_class`` directly at multiple1524sites. Each such site is hooked, per ``docs/ARCHITECTURE.md1525§17.7`` (post-second-amendment). At K4 there are two such sites,1526and therefore two arch hooks. See §17.7 SECOND HISTORICAL NOTE1527for the META-LESSON about enumerating all assignment sites1528before declaring an arch hook strategy complete.1529 1530**Site 1: end of ``sched_fork()``** (creation-time class1531assignment, policy-driven).1532 1533Upstream ``sched_fork`` ends with:1534 1535::1536 1537    if (rt_prio(p->prio)) {1538        p->sched_class = &rt_sched_class;1539    } else if (task_should_scx(p->policy)) {1540        p->sched_class = &ext_sched_class;1541    } else {1542        p->sched_class = &fair_sched_class;1543    }1544 1545The arch hook ``arch_sched_fork(p)`` is added by1546``linux-wasm/patches/0001-sched-add-arch_sched_fork-hook.patch``1547as a ``__weak`` no-op default; ``arch/wasm32/kernel/sched.c``1548provides the strong override that sets1549``p->sched_class = &wasm_sched_class`` unconditionally. The hook1550fires at the very end of ``sched_fork``, *after* the upstream1551policy → class mapping above runs, so the override is the last1552write that wins.1553 1554**Site 2: ``__setscheduler_class()``** (any-time class1555reassignment, also policy-driven).1556 1557Upstream ``__setscheduler_class(policy, prio)`` returns the1558class that ``__sched_setscheduler`` and ``__rt_mutex_setprio``1559should assign:1560 1561::1562 1563    if (dl_prio(prio))   return &dl_sched_class;1564    if (rt_prio(prio))   return &rt_sched_class;1565    if (task_should_scx) return &ext_sched_class;1566    return &fair_sched_class;1567 1568This is invoked at every ``sched_setscheduler*()`` call, every1569``sched_setattr()``, and every PI-mutex priority inheritance.1570The notable case for K4 is upstream ``kthread()`` (the body that1571runs as the entry function of every kthread, in1572``kernel/kthread.c``): it calls1573``sched_setscheduler_nocheck(current, SCHED_NORMAL, &param)`` as1574its first action, to reset whatever priority the new kthread1575inherited from ``kthreadd``. Without a hook at this site, the1576first instruction every kthread executes re-routes1577``p->sched_class`` from ``wasm_sched_class`` to1578``fair_sched_class``, and the next ``enqueue_task`` traps in1579``enqueue_task_fair`` against an uninitialized ``p->se`` / CFS1580state.1581 1582The arch hook ``arch_setscheduler_class(int policy, int prio)``1583is added by1584``linux-wasm/patches/0003-sched-add-arch_setscheduler_class-hook.patch``1585as a ``__weak`` default returning ``NULL`` (upstream behavior1586preserved); ``arch/wasm32/kernel/sched.c`` provides the strong1587override that returns ``&wasm_sched_class`` unconditionally.1588Upstream ``__setscheduler_class`` is patched to consult the hook1589first and short-circuit on non-NULL:1590 1591::1592 1593    const struct sched_class *arch_class;1594 1595    arch_class = arch_setscheduler_class(policy, prio);1596    if (arch_class)1597        return arch_class;1598 1599    /* upstream policy → class mapping continues */1600 1601The hook signature is intentionally minimal — returning1602``const struct sched_class *`` with ``NULL = passthrough`` — and1603NOT a boolean-veto-with-out-param shape. The rationale for the1604chosen shape is documented in ``docs/ARCHITECTURE.md §19``1605(no-premature-flexibility rule), with this hook as the worked1606example.1607 1608**Site 3 (boot-only): ``init_task``.** The boot CPU's idle task1609``init_task`` is constructed at link time, not via1610``sched_fork``, so the ``arch_sched_fork`` hook does not pick it1611up. ``arch/wasm32/kernel/head.c::wasm_start_kernel`` explicitly1612sets ``init_task.sched_class = &wasm_sched_class`` after1613``__wasm32_set_current(&init_task)`` and before the first1614``schedule()`` call. (Site 3 is not an arch hook, just a direct1615arch-side write at boot.)1616 1617The two-hook design plus the boot-time direct write together1618guarantee the invariant: *for every task that ever runs on a1619wasm32 CPU,* ``p->sched_class == &wasm_sched_class`` *for the1620task's entire lifetime.* The other sched classes1621(``fair_sched_class``, ``rt_sched_class``,1622``deadline_sched_class``, ``stop_sched_class``,1623``idle_sched_class``, ``ext_sched_class``) remain compiled in1624but are never assigned to any ``p->sched_class`` at runtime.1625 16265.2 K4 close metric: arch hook count1627~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1628 1629The K4-close commit body records the count of arch hooks added1630to upstream ``kernel/sched/`` along with the count of arch hook1631*overrides* in ``arch/wasm32/kernel/sched.c``. At K4 close:1632 1633* 2 weak hooks added upstream (``arch_sched_fork``,1634  ``arch_setscheduler_class``).1635* 2 strong overrides in ``arch/wasm32/``.1636* 1 boot-time direct write (``init_task.sched_class`` in1637  ``head.c``).1638* Total ``p->sched_class`` assignment sites hooked: 3 of 31639  (sites 1, 2, 3 above).1640 1641Future K-phases that surface a previously-missed1642``p->sched_class`` assignment site increment this count and1643extend ``docs/ARCHITECTURE.md §17.7``. The grep-then-trust1644discipline (§17.7 META-LESSON, promoted to ``docs/ARCHITECTURE.md1645§20``) is the preventive measure.1646 16475.3 Peek-not-pop: ``__wasm32_yield`` and the two yield paths1648~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1649 1650A task is on ``wasm32_rq`` if and only if it is runnable. The1651running task is on ``wasm32_rq`` (at the head) for the entire1652duration of its execution; it is removed only when it1653transitions to a non-RUNNABLE state via ``deactivate_task →1654dequeue_task_wasm``. This is the **peek-not-pop** discipline,1655and it matches upstream CFS semantics (the running task1656remains in the CFS rb-tree until it sleeps) using a list-head1657rather than an rb-tree.1658 1659Equivalently: ``__wasm32_yield``'s dispatch loop reads1660``wasm32_rq_peek()``, not a ``wasm32_rq_dequeue()`` (there is1661no such primitive). The dispatched task is referenced in1662``current`` and ``rq->curr`` but is still on ``wasm32_rq``1663while executing.1664 1665The rationale is the path-A / path-B distinction in upstream1666``__schedule``:1667 1668::1669 1670    __schedule(sched_mode)1671        ...1672        prev_state = READ_ONCE(prev->__state)1673 1674        # Path A: state-changing voluntary yield.1675        # The caller set prev->__state to TASK_*INTERRUPTIBLE1676        # / TASK_UNINTERRUPTIBLE / etc. *before* calling1677        # schedule(). __schedule blocks prev.1678        if (!preempt && prev_state &&1679            !signal_pending_state(prev_state, prev)) {1680            block_task(rq, prev, DEQUEUE_NOCLOCK)1681              -> dequeue_task(rq, p, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK)1682              -> dequeue_task_wasm1683              -> list_del_init(&prev->thread.rq_node)1684        }1685 1686        # Path B: state-preserving voluntary yield.1687        # cond_resched(), bare schedule() with state still1688        # RUNNING, preemption, or signal_pending forcing the1689        # stay-runnable branch. __schedule does NOT block prev.1690 1691        next = pick_next_task(rq, prev, &rf)1692          -> pick_next_task_wasm1693          -> wasm32_rq_peek()      # head of rq1694 1695        if (likely(prev != next))1696            context_switch(rq, prev, next, &rf)1697              -> switch_to -> __wasm32_switch_to1698              -> asyncify_start_unwind1699 1700If the dispatcher popped on dispatch and re-enqueue only1701happened on ``wake_up_process``, path B would be broken:1702``pick_next_task_wasm`` would see an empty rq (because the1703dispatcher had already removed the running task), return1704``rq->idle``, and ``prev != next`` would unwind ``prev``1705permanently — there is no other dispatcher path that would1706re-enter ``prev`` later, because ``prev`` was never put back1707on the rq (path B's whole point is that the task is *still1708runnable*).1709 1710With peek-not-pop:1711 1712* **Path A** behaves identically before and after the fix:1713  ``deactivate_task`` removes ``prev`` from ``wasm32_rq``,1714  ``pick_next_task_wasm`` returns the new head (or NULL →1715  ``rq->idle``), ``context_switch`` unwinds ``prev`` and1716  the dispatcher picks the next runnable task or parks.1717* **Path B** is handled correctly: ``prev`` is still at the1718  head of ``wasm32_rq`` (nobody removed it),1719  ``pick_next_task_wasm`` returns ``prev``, ``__schedule``1720  sees ``prev == next`` and short-circuits. ``context_switch``1721  is not called. No unwind. The running task continues1722  past the cooperative-yield point — the correct outcome1723  for a state-RUNNING voluntary yield in a1724  single-runnable-task scenario.1725* **Path B with another runnable task**: when another task1726  was enqueued (via ``wake_up_process``) before the yield,1727  the head of ``wasm32_rq`` is *that* task, not ``prev``.1728  ``pick_next_task_wasm`` returns the other task,1729  ``prev != next``, ``context_switch`` unwinds ``prev``,1730  the dispatcher resumes the other task. ``prev`` is still1731  on ``wasm32_rq`` (we peeked, didn't pop), so the next time1732  the dispatcher peeks it will pick ``prev`` back up (after1733  any higher-priority head ahead of it has either been1734  ``deactivate_task``'d via path A or dispatched and yielded1735  itself via path B). This is FIFO fairness with the1736  correctness property "no runnable task ever falls off the1737  rq."1738 1739The K4-close ``wasm32_rq`` invariants are therefore:1740 17411. A task ``p`` is on ``wasm32_rq`` ⇔ ``p`` is runnable1742   (``READ_ONCE(p->__state) == TASK_RUNNING`` AND the task1743   has not exited).17442. ``wasm32_rq`` is a FIFO list ordered by enqueue time1745   (``list_add_tail``).17463. The running task — when there is one — is at the head.1747   ``__wasm32_yield`` peeks the head and dispatches it; on1748   re-entry to ``__wasm32_yield``, the next peek either1749   returns the same head (path B, no-op cycle) or a1750   different head (path A removed prev OR a wakeup enqueued1751   a new task ahead of prev).17524. Removal from ``wasm32_rq`` happens *exclusively* through1753   ``dequeue_task_wasm``, which is called *exclusively*1754   from ``deactivate_task`` along ``__schedule``'s path A1755   (and from ``do_exit``'s scheduler teardown — also path A1756   semantically, since the exiting task transitions to1757   ``TASK_DEAD``).1758 1759See ``docs/ARCHITECTURE.md §17.7 THIRD HISTORICAL NOTE`` for1760the empirical refutation that produced this design (a1761state-RUNNING voluntary yield inside ``create_worker``'s1762post-completion resume chain caused pid 1 to fall off the1763runqueue under the pre-R3 pop-on-dispatch model). The1764META-PRINCIPLE (``§20``) about all-sites enumeration applies1765analogously to *path* enumeration: every code path through1766``__schedule`` had to be examined, not just the obvious1767voluntary-sleep path. R3 is the worked example for1768behavior-enumeration (§20.3).1769 1770--------------------------------------------------------------------------------17716. HardwareJS ↔ vmlinux idle/wakeup futex (cross-module ABI)1772--------------------------------------------------------------------------------1773 1774Per Q3 above.1775 17766.1 ABI spec1777~~~~~~~~~~~~1778 1779============================== =========================================1780Name                           ``__sched_wakeup_word``1781Type                           ``u32`` in ``env.kernel_memory``1782Initial value                  ``0``1783Allocated by                   Kernel C code (``__wasm32_sched_init``1784                               at boot) via a small bss-resident1785                               variable; the wasm-global1786                               ``__sched_wakeup_word`` is set to its1787                               offset and exported.1788Exposed to HardwareJS via      Wasm global ``__sched_wakeup_word``1789                               (i32, immutable, value = offset in1790                               ``env.kernel_memory``).1791Read-write party (kernel)      Writes (clears) at top of cooperative1792                               yield loop. Reads (waits) when runqueue1793                               empty.1794Read-write party (HardwareJS)  Writes (increments) + notifies when1795                               external event makes a kthread1796                               runnable. Never reads.1797Read-write party (user Worker) Same as HardwareJS via1798                               ``linux.kick_scheduler``.1799============================== =========================================1800 18016.2 Host-side helper1802~~~~~~~~~~~~~~~~~~~~1803 1804A new HardwareJS export ``HardwareJS.kickKernelScheduler()``:1805 1806* Reads ``__sched_wakeup_word`` global from the vmlinux instance1807  (once, cached at instantiation).1808* Performs ``Atomics.add(int32View, offset/4, 1)`` followed by1809  ``Atomics.notify(int32View, offset/4, 1)`` on the1810  ``env.kernel_memory``-backing ``Int32Array``.1811 1812The vmlinux Worker imports this as ``linux.kick_scheduler`` for1813kernel C code that wants to wake the kernel from a HardwareJS-1814side context. At K4 the only kernel C call site is the1815hrtimer host-callback (when an hrtimer fires in HardwareJS1816JS-land, it calls ``linux.kick_scheduler`` to make the timer1817softirq runnable).1818 18196.3 Idle protocol on the vmlinux side1820~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1821 1822::1823 1824    void __wasm32_park_until_runnable(void) {1825        /* Snapshot the current value, clear it, then wait if no1826         * one has set it since the snapshot. The atomic-xchg1827         * pattern below is the standard race-free idle-park. */1828        u32 v = atomic_xchg(&__sched_wakeup_word, 0);1829        if (v != 0) {1830            /* Someone already raised the flag. Don't park. */1831            return;1832        }1833        /* Atomically wait if the word is still 0. */1834        __wasm32_atomic_wait_i32(&__sched_wakeup_word,1835                                 /*expected=*/0,1836                                 /*timeout_ns=*/-1);1837    }1838 1839Where ``__wasm32_atomic_wait_i32`` is a thin wrapper around1840the wasm ``memory.atomic.wait32`` instruction (already1841implementable in K3 — the opcode is on the K3 atomics1842inventory).1843 1844This is the only place in the kernel that EVER calls1845``memory.atomic.wait32``. The kernel's spinlock paths use1846``i32.atomic.rmw.cmpxchg`` (K3 inventory) and don't ``wait``1847because spinlocks are uncontended in a single-Worker model.1848 1849--------------------------------------------------------------------------------18507. Per-task storage layout1851--------------------------------------------------------------------------------1852 1853At ``kthread_create`` time:1854 1855::1856 1857    struct task_struct *p = kmem_cache_alloc(task_struct_cache, GFP_KERNEL);1858    p->thread.stack_base = kvmalloc(THREAD_SIZE, GFP_KERNEL);1859    p->thread.stack_size = THREAD_SIZE;1860    p->thread.stack_top  = p->thread.stack_base + THREAD_SIZE;  /* wasm stack grows DOWN, kernel-side */1861    p->thread.asyncify_data = kvmalloc(ASYNCIFY_DATA_SIZE, GFP_KERNEL);1862    p->thread.asyncify_data_bytes = ASYNCIFY_DATA_SIZE;1863    INIT_LIST_HEAD(&p->thread.rq_node);1864    __wasm32_kthread_init_save_buffer(p, threadfn, data);1865 1866Lifetime: freed on ``free_thread_stack`` (kernel/fork.c1867already calls this; arch_release_thread_stack is the hook).1868``arch/wasm32/kernel/process.c`` (K4 introduces) provides:1869 1870::1871 1872    void arch_release_thread_stack(struct task_struct *p) {1873        kvfree(p->thread.asyncify_data);1874        kvfree(p->thread.stack_base);1875    }1876 1877Concurrency: as long as a kthread can be on the runqueue (its1878``rq_node`` is linked), its storage is alive. ``kthread_stop``1879ensures the kthread runs to completion before ``put_task_struct``1880fires the release.1881 1882--------------------------------------------------------------------------------18838. K4 acceptance shape1884--------------------------------------------------------------------------------1885 1886(Copied verbatim from the K4 instructions so this doc holds1887the spec.)1888 1889* K0–K3 dmesg progression still appears (regression).1890* ``rest_init`` runs to completion.1891* ``kthreadd`` is reachable; runs at least one full1892  "wait for work → no work → park" loop without busy-spinning1893  the host CPU (verified by host-side CPU usage during a 5s1894  idle window — should be near zero, NOT pinned to 100%).1895* ``kernel_init`` runs as a kthread, attempts to mount rootfs1896  and find /init, and panics cleanly with the upstream1897  "No working init found." message (or the 6.12 equivalent1898  string).1899* A vitest exercises ``kthread_create`` + ``complete`` +1900  ``wait_for_completion`` across two kthreads (producer1901  completes, consumer waits, scheduler runs both1902  cooperatively) — passes.1903* Post-Asyncify binary size is recorded in commit body1904  alongside the K3-instituted atomic-opcode count.1905* ``wasm-inspect.py`` (extended in K4) reports Asyncify1906  globals present (``asyncify_state``, ``asyncify_data``) and1907  a non-zero count of ``asyncify_start_unwind`` /1908  ``asyncify_stop_rewind`` call sites inside the whitelisted1909  region.1910* The K4-close commit body records ``wasm_sched_class``'s1911  method shape as ``N real / M no-op``. ``N`` counts methods1912  with a non-empty body that participates in the dispatcher1913  contract (at K4: ``enqueue_task``, ``dequeue_task``,1914  ``pick_next_task``, ``task_fork``). ``M`` counts methods1915  with a ``WARN_ONCE`` no-op body that future work may1916  promote to real. The count is the K6 baseline for1917  Kconfig-deselecting unused upstream classes (``fair``,1918  ``rt``, ``deadline``, ``ext``, ``stop``) — that1919  optimization shrinks the binary by approximately1920  ``5 × sizeof(struct sched_class) ≈ 5 × 112 bytes ≈ 5601921  bytes plus the function-body weight of each class's real1922  methods``, which is small per-class but cumulative across1923  the upstream class set.1924* The K4-close commit body records the count of arch hooks1925  added to upstream ``kernel/sched/`` as ``H weak / H1926  override``, where ``H`` is the number of upstream call1927  sites at which the wasm32 arch overrides class selection1928  or assignment. At K4 close ``H = 2`` (``arch_sched_fork``,1929  ``arch_setscheduler_class``); see §5.1 above for the1930  per-hook accounting. Future K-phases that surface a1931  previously-missed ``p->sched_class`` assignment site1932  increment ``H``, extend ``docs/ARCHITECTURE.md §17.7``,1933  and add a new ``patches/000N-*.patch`` for the upstream1934  weak default. This count is the trailing indicator of1935  ``§17.7`` SECOND HISTORICAL NOTE's META-LESSON: a third1936  refutation (``H = 3``) would mean we missed *another* site1937  during the K4 grep-then-trust pass; if K5 ever has to1938  bump ``H`` for ``sched_class``, that's signal to1939  re-audit the grep methodology, not just to add the1940  missing hook.1941 1942Do NOT chase past "No working init found." K5 starts there.1943 1944Tag ``v0.1.0-rc5`` at K4 close.1945 1946--------------------------------------------------------------------------------19479. Constraint on subsequent K-phases1948--------------------------------------------------------------------------------1949 1950K5 cannot start until this section is committed and the K51951implementation notes reference Q1–Q13 by number. K5's1952Regime-1 implementation (``current->mm != NULL`` parking via1953per-task futex word) is a strict extension of §6 above:1954each user task gets its own per-task ``Atomics.wait`` word in1955``env.kernel_memory``, and ``try_to_wake_up`` of a user task1956notifies that word. The master scheduler-wakeup word1957``__sched_wakeup_word`` stays the same; it's about kthread1958runqueue work.1959 1960K5+ commits that conflict with §2 here force a separate doc1961commit first updating §2.1962 1963K6 (the post-link Python pass for percpu / sched_class1964anchors per ``mm-model.rst`` §0(c)(4)) is orthogonal to the1965scheduler work but interacts at one point: the percpu post-1966link pass MUST run BEFORE Asyncify, so the Asyncify pass1967sees the final symbol layout. Recorded here so K6 doesn't1968order its own pipeline wrong.1969