.. SPDX-License-Identifier: GPL-2.0

================================================================================
arch/wasm32 mm model (K3 spec)
================================================================================

:Status: K3 (real ``mm_init`` / page allocator / slub / wasm atomics).
         Stable through K6; revisions require a new section + §15 row.

This document is the source of truth for *how the wasm32 port models
physical memory, page sizing, vmalloc, atomics, and per-CPU areas*. It
plays the same role for the memory subsystem that ``memory-model.rst``
plays for the cross-module memory ABI and ``boot-protocol.rst`` plays
for the boot-time handoff: spec-first, code-second. Treat any
divergence between this document and ``arch/wasm32/include/asm/page.h``
/ ``arch/wasm32/include/asm/atomic.h`` / ``arch/wasm32/include/asm/cmpxchg.h``
as a bug in the code.

--------------------------------------------------------------------------------
0. K3-phase scope at a glance
--------------------------------------------------------------------------------

(a) **New cross-module ABI introduced by K3.** Two surfaces.

  - **kernel C ↔ wasm runtime (atomics).** The K2-era atomic_t /
    refcount_t / cmpxchg call sites were lowered to plain non-atomic
    loads and stores via ``asm-generic/atomic.h``. K3 replaces that
    with real wasm-threads atomic opcodes (``i32.atomic.rmw.cmpxchg``,
    ``i32.atomic.rmw.{add,sub,and,or,xor,xchg}``, ``i64.atomic.*``, the
    fence / load / store family) backed by clang ``__atomic_*``
    builtins compiled with ``-matomics -mbulk-memory``. This is a
    silent-ABI-change in the sense that the kernel's *behaviour* under
    concurrent access becomes correct; no name or signature changes,
    so no version bump is needed in any of the named cross-module
    ABIs. §3 below names the toolchain-pinned encoding rules so a
    future toolchain update can be checked against this spec.

  - **kernel C ↔ HardwareJS (page sizing).** ``PAGE_SIZE = 4096`` (see
    Q1 in §2). This affects the eventual userspace ABI: ``mmap()``
    alignment, ``getpagesize()``, ``sysconf(_SC_PAGESIZE)``, ELF
    program-header alignment for ``binfmt_wasm`` (K-phase U1+) all
    return / require 4096-byte alignment. HardwareJS never has to
    *know* the kernel's PAGE_SIZE — it serves env.kernel_memory as a
    flat SAB and the kernel chops it into 4 KiB pages internally — but
    any HardwareJS code that *interprets* user-memory mapping requests
    (e.g. the future ``linux.mmap_user_page`` host primitive at K4+)
    will see 4096-byte chunks at 4096-aligned offsets. This is
    documented here so K4+ doesn't relitigate it.

(b) **Architectural decisions K3 forces.** Q1–Q5 in §2 below. None
    are technically reversible without an ABI version bump.

(c) **§15 debt this phase predictably opens.** Predicted at K3 open,
    refined at K3 close as the implementation surfaced two additional
    rows that were not visible from the spec alone. Final K3-close
    debt list (all six rows landed in docs/ARCHITECTURE.md §15):

  1. **vmalloc is contiguous-backed.** If a future subsystem actually
     needs ``vmalloc()`` to return *virtually*-contiguous /
     *physically*-discontiguous memory, this is the design
     constraint to revisit. See §2 Q2 and §6 below.
  2. **Multi-memory atomic textual syntax is LLVM-blocked.** Our
     ``arch/wasm32/include/asm/cmpxchg-user.h`` (introduced for K4
     futexes) MUST use raw ``.byte`` inline asm to emit the
     multi-memory form of atomic ops. Filed as LLVM-side debt with
     pointer to the spec-side text describing the encoding. When
     LLVM upstream adds the operand, we delete the ``.byte``
     wrappers — see §3.2.
  3. **NR_CPUS = 8 wastes percpu memory at K3.** Slub allocates
     per-CPU caches for 8 CPUs; only CPU 0 is online. ~7× memory
     waste, ~700 KiB of slub bookkeeping. Justified because K4
     "CPUs coming online" then doesn't have to resize allocations.
     Re-tunable at v0.1.0+ if browser concurrency reality differs
     from the K4 plan.
  4. **Linker-script-anchor symbols for percpu and sched_class
     ordering** are stubbed via ``arch/wasm32/kernel/percpu_anchors.c``
     using 1-byte char-array placeholders in synthetic sections
     plus ``--no-merge-data-segments`` at link time. The kernel's
     ``include/asm-generic/vmlinux.lds.h::PERCPU_INPUT`` and
     ``SCHED_DATA`` macros enumerate the segment ordering wasm-ld
     does not consume from the linker script. Discovered during K3
     implementation. Owed real fix at K5/K6 via a post-link Python
     pass (sibling to ``wasm-add-memory.py``) that walks data
     segments and emits ``--defsym=__per_cpu_start=...``
     ``--defsym=__per_cpu_end=...`` /
     ``--defsym=__{stop,dl,rt,fair,idle}_sched_class=...`` before
     final link. See §7 below.
  5. **CONFIG_SLUB_TINY=y as a K3 workaround.** Discovered K3
     closing: with per-CPU SLUB freelists enabled
     (``!CONFIG_SLUB_TINY``), kmalloc-256 allocations return
     misaligned pointers under sustained traffic — traced to
     ``static_size``-driven offset miscomputation in the per-CPU
     dynamic allocator (the per-CPU anchor row above doesn't
     express the kernel's real percpu image size). SLUB_TINY
     removes the per-CPU fastpath entirely; allocations go via
     the per-node partial list and are stable. K3 acceptance
     test confirms kmalloc/kfree work under concurrent SAB
     pressure. Re-flippable to ``CONFIG_SLUB_TINY=n`` once the
     post-link percpu pass lands at K5/K6.
  6. **30-second test watchdog** in the K3 vitest happy-path
     because the kernel hangs in ``rest_init`` / kthreadd setup
     rather than panicking — exactly the K3 acceptance shape, but
     also a hint that K4's scheduler work has a specific entry
     point. Recorded so K4 doesn't accidentally relitigate.

--------------------------------------------------------------------------------
1. Physical memory model
--------------------------------------------------------------------------------

In this port, "physical memory" is the bytes inside ``env.kernel_memory``
— a single SharedArrayBuffer-backed ``WebAssembly.Memory`` allocated
by HardwareJS at boot. The kernel sees a flat physical address space
``[0, total_memory)``. There is no MMIO, no DMA, no NUMA. There is no
``ZONE_HIGHMEM`` (no MM aperture distinction in wasm), no
``ZONE_DMA`` / ``ZONE_DMA32`` (no DMA), no ``ZONE_DEVICE`` (no PMEM /
HMM). The only zone is ``ZONE_NORMAL``.

The mapping between physical addresses (``phys_addr_t``) and kernel
virtual addresses (``unsigned long``) is the identity — there is no
``PAGE_OFFSET`` translation. ``__pa(x) == x``, ``__va(x) == x``. This
is the simplest possible flat model and matches NOMMU arches like
ARC's hsdk-noplv1 configuration; the kernel's flat-mode macros in
``include/asm-generic/`` Just Work.

Memblock is seeded at K2 from ``boot_info.usable_{start,end}`` (see
``Documentation/wasm/boot-protocol.rst``). The kernel image is
``memblock_reserve``-d from offset 0 through ``__heap_base``, so the
buddy allocator never reclaims kernel ``.text`` / ``.rodata`` /
``.data`` / ``.bss`` pages. memblock hands its remaining regions to
the buddy allocator during ``mm_init``.

--------------------------------------------------------------------------------
2. Architectural decisions Q1–Q5 (the K3 opening five)
--------------------------------------------------------------------------------

Q1. PAGE_SIZE = 4096
~~~~~~~~~~~~~~~~~~~~

**Decision: PAGE_SIZE = 4096, PAGE_SHIFT = 12.**

Considered:

* **4 KiB.** Matches x86, matches every Linux userspace ABI
  expectation, matches musl / glibc / POSIX defaults for
  ``sysconf(_SC_PAGESIZE)``. Finer-grained metadata: every 4 KiB
  region gets its own ``struct page``, which is `~8 % memory
  overhead on a 64 MiB heap (sizeof(struct page) ≈ 64 bytes, 16384
  pages = 1 MiB of ``mem_map``). Forces PAGE_SIZE / wasm-page
  conversion arithmetic at the memblock / page-allocator boundary
  (1 wasm-page = 16 kernel pages), but this is a one-time
  shift-by-4 at every allocator hand-off and is not on any hot path.

* **65536.** Matches wasm's own ``memory.grow`` page size. No
  PAGE_SIZE-vs-wasm-page conversion needed at the allocator
  boundary. Half the ``mem_map`` overhead (~0.5 MiB on a 64 MiB
  heap). BUT: every userspace binary built against musl / glibc
  expects 4 KiB pages — ``mmap(NULL, 4096, ...)`` would have to
  silently round up to 64 KiB, which violates ``POSIX_MAP_FIXED``
  semantics; ``sysconf(_SC_PAGESIZE)`` would return 65536, which
  most userspace handles correctly but some hard-coded
  ``#define PAGE_SIZE 4096`` in third-party code does not; ELF
  program-header alignment for binfmt_wasm (U1+) would be 64 KiB
  rather than 4 KiB, which is fine but unusual and may confuse
  cross-built ELF inspection tools. The userspace ABI cost is
  irrecoverable: this is a kernel ↔ userspace ABI decision that
  cannot be changed after v0.1.0.

**Rationale for 4096.** Userspace ABI expectations are more
constraining than internal alignment with ``memory.grow``. The
conversion arithmetic is mechanical and one-time. ``mem_map``
overhead at 8 % of a small heap is acceptable; on a 1 GiB heap
(the wasm32 ceiling) it is ~16 MiB which is comfortable. K7's
rootfs and U2's bash/coreutils will both run unmodified upstream
binaries built for 4 KiB-page hosts; choosing 65536 here would
force at minimum a sed-pass over upstream package configure
scripts that introspect PAGE_SIZE, which is the exact category of
"fork a file outside arch/wasm32/" that §16's PR rejection
criterion forbids.

Q2. vmalloc semantics
~~~~~~~~~~~~~~~~~~~~~

**Decision: vmalloc is contiguous-backed by the buddy allocator
with a hard upper bound of ``MAX_ORDER * PAGE_SIZE`` (== 4 MiB at
PAGE_SIZE=4096, MAX_ORDER=10). Real virtually-contiguous /
physically-discontiguous mappings are not supported.**

Considered:

* **(i) Contiguous-backed.** ``vmalloc(N)`` becomes
  ``__alloc_pages(get_order(N))`` returning the kernel-virtual
  (== physical) address of the contiguous run. ``vfree`` calls
  ``free_pages`` on the same range. Most kernel users of
  ``vmalloc`` want "large allocation that doesn't fail if the
  *physical* heap is fragmented" — they don't care that the
  mapping is virtually contiguous. On our model, fragmentation
  of ``env.kernel_memory`` is the *only* fragmentation, so the
  "physically discontiguous" property has no meaning to begin
  with: any contiguous run we can give them IS the answer they
  want.

* **(ii) Disable vmalloc entirely** by removing
  ``CONFIG_HAVE_VMALLOC`` (or equivalent) and forcing every
  caller to use ``alloc_pages_node`` / ``alloc_pages_exact``.
  This is the technically cleanest answer but breaks a huge
  number of upstream-Linux callers that just want
  ``vmalloc(N)`` to "give me N bytes of contiguous-ish kernel
  memory."

* **(iii) Pretend to vmalloc by setting up a separate kernel
  virtual address range** that the buddy allocator's
  ``alloc_pages`` calls populate via wasm's ``memory.grow``.
  Discarded: wasm has no page tables, ``memory.grow`` adds
  pages at the *end* of the linear memory not at arbitrary
  virtual addresses, and the kernel cannot create the
  "discontiguous mapping" illusion without a page table or
  equivalent address translation.

**Rationale for (i).** Most callers don't need the
discontiguous property; the hard upper bound from
``MAX_ORDER`` is the same upper bound a real ``vmalloc`` would
hit on a fragmented heap anyway (just from a different cause).
Subsystems that genuinely need >4 MiB single allocations are
the same subsystems that would have failed on a real arch with
a moderately-fragmented heap, so they already have fallback
strategies; we're not adding a new failure mode, we're
exposing a slightly more predictable version of an existing
one.

**§15 debt row to add at K3 close.** "vmalloc-contiguous-
backed; if a future subsystem actually needs discontiguous
mapping, this is the design constraint to revisit (would
require either a pretend-pgtable in arch/wasm32 or a
``linux.kernel_vmap`` host primitive that asks HardwareJS to
maintain a paddr→vaddr remap table)."

Q3. Cross-memory atomics from the kernel against user memory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

**Decision: cross-memory atomic ops emit the wasm-threads ×
multi-memory binary form via raw ``.byte`` inline asm
wrappers (similar to how arch/wasm32/lib/uaccess.S wraps
multi-memory ``memory.copy``). LLVM 22's textual wasm
assembler does NOT yet accept the memidx operand on atomic
ops, but the runtime DOES accept the emitted bytes.
EMPIRICALLY VERIFIED on the K3 opening commit's pinned
toolchain (see evidence below). All cross-memory atomic
ops live in ``arch/wasm32/include/asm/cmpxchg-user.h`` and
``arch/wasm32/lib/atomics-user.S`` (introduced for K4
futexes; declared here so the K3 plan can encode the
constraint).**

Evidence captured at K3 opening (write-up of the empirical
probe in ``linux-wasm/tools/k3-q3-probe/`` for
reproducibility):

Pinned toolchain:

* ``clang version 22.0.0git`` (LLVM-project commit
  ``60513b8d6ebacde46e8fbe4faf1319ac87e990e3``)
* ``LLD 22.0.0`` (same commit)
* Node ``v25.2.0`` (V8)

Probe encoding (memidx = 1, align = 2, offset = 0)::

    fe 48 42 01 00
    │  │  │  │  └── offset (ULEB128 = 0)
    │  │  │  └───── memidx (ULEB128 = 1, = env.user_memory)
    │  │  └──────── align byte with bit 6 set
    │  │            (0x40 | 0x02 = 0x42 → "memidx follows, align=2")
    │  └─────────── opcode (i32.atomic.rmw.cmpxchg)
    └────────────── atomic-opcode prefix

Observed runtime behaviour: cross-memory cmpxchg against
``env.user_memory`` offset 0 with expected=0 desired=99
returned 0 (the old value) AND ``user_memory[0]`` is 99
after the call. The cross-memory atomic executed
**correctly** under V8.

``llvm-objdump`` mis-disassembles the bytes (it parses the
``0x42`` as a normal align byte, reads the next ``0x01`` as
the offset, then mis-decodes the trailing ``0x00`` as
``unreachable`` instead of a continuation of the memarg).
That's a disassembler bug, not a binary bug. The
encoded-and-instantiated wasm executes as the spec
specifies.

**§15 debt row to add at K3 close.** "Multi-memory atomic
textual syntax is LLVM-blocked. Our cross-memory atomic
wrappers use raw ``.byte`` inline asm; replace with
textual operand when upstream LLVM lands the syntax. Owed:
toolchain-upstream-bump (not K-phase tied)."

Q4. CONFIG_NR_CPUS for K3
~~~~~~~~~~~~~~~~~~~~~~~~~

**Decision: ``CONFIG_NR_CPUS=8``, ``CONFIG_SMP=y``,
``num_online_cpus() == 1`` for all of K3.**

Considered:

* **NR_CPUS=1, CONFIG_SMP=n.** Simplifies K3: no per-CPU
  caches, no IPI machinery, no smp_processor_id rerouting.
  But forces K4 to redo slub allocations when CPUs come
  online. slub's per-CPU caches are allocated at
  ``kmem_cache_create`` time, indexed by ``smp_processor_id``;
  resizing them after-the-fact is non-trivial (slub does
  not currently support dynamic NR_CPUS change). K4 would
  have to either re-init the slab allocator (risky) or
  pre-allocate but not use the extra slots (which is what
  NR_CPUS=8 already does).

* **NR_CPUS=8, CONFIG_SMP=y.** Wastes ~7× per-CPU memory at
  K3 (one Worker, 7 unused slots × per-CPU caches × every
  kmem_cache ≈ 700 KiB total slub-bookkeeping on a stock
  6.12 config). K4 just flips Workers to online via
  ``set_cpu_online(cpu, true)`` without resizing any
  allocation. The slub per-CPU caches that K3 created are
  immediately usable by K4 CPUs.

* **NR_CPUS=hardwareConcurrency.** Tempting (matches
  browser reality), but ``CONFIG_NR_CPUS`` is compile-time;
  we'd have to recompile per-browser at boot, which is the
  exact bad-engineering pattern. Browsers report 4–8
  cores in 95% of cases; picking 8 (a fixed ceiling) is the
  conservative choice. Beyond 8, K4+ can either spawn more
  Workers via virtual SMP (Workers > CPUs, scheduling at
  the JS engine layer) or bump the constant.

* **NR_CPUS=64.** The standard "many-CPU" pick on real
  servers; gross overkill in a browser. Would waste ~63×
  per-CPU memory for no gain.

**Rationale for 8.** Forward-compat with K4 without
re-allocating; matches typical browser hardware concurrency;
``CONFIG_NR_CPUS`` is a compile-time constant in mainline
Linux and 8 is a small enough number to keep the
"NR_CPUS-dependent allocation" footprint bounded. Recorded
in ``arch/wasm32/configs/wasm32_defconfig`` so the next
``make configure`` picks it up.

Q5. The K0 page-table model
~~~~~~~~~~~~~~~~~~~~~~~~~~~

**Decision: the K0 ``nopud + nopmd + degenerate single-level
PTE used as VMA bookkeeping`` model still holds for K3. No
real PTE allocation is triggered by ``mm_init`` /
``kmem_cache_init`` / ``kmem_cache_init_late`` on our
NOMMU-style flat model.**

Verification path:

* ``mm_init`` calls (in order, ignoring x86 / s390 -isms):
  ``set_max_mapnr``, ``init_mem_size_check``, ``mem_init``
  (zone walkers, no page-table use), ``kvfree_rcu_init``
  (slab cache, no page-table use), ``kmem_cache_init``
  (slab init, allocates ``kmem_cache_node`` and
  ``kmem_cache`` slabs — no page-table use),
  ``kmem_cache_init_late`` (creates the kmalloc caches,
  ``kfree`` works after — no page-table use), ``vmalloc_init``
  (initialises the vmalloc tree structures; on our model
  this resolves to "buddy-backed contiguous" per Q2 above,
  no page-table use), ``pgtable_init`` (creates the
  ptlock_caches slab IFF ``CONFIG_SPLIT_PTE_PTLOCKS`` is on
  — which we explicitly turn off in our defconfig, so this
  is a no-op call).

* ``pte_t`` is laid out per ``include/asm-generic/pgtable-nopud.h``
  + ``pgtable-nopmd.h`` + the wasm32 ``arch/wasm32/include/
  asm/pgtable.h``. ``pte_t`` is one ``unsigned long`` whose
  high bits are VMA flags and whose low bits would be the
  PFN on a real arch (but are unused on us since
  ``__pa == __va``). No pte allocation chain exercised at K3.

* ``mem_map[]`` IS allocated by memblock during
  ``mm_init`` (specifically inside ``free_area_init`` →
  ``alloc_node_mem_map``). With PAGE_SIZE=4096 and a 64 MiB
  test heap, that's 16384 ``struct page`` entries × ~64
  bytes = 1 MiB of bookkeeping, sized at ``mem_map`` init
  time and never re-grown.

**Conclusion: model unchanged for K3.** The first K-phase
that DOES touch pte allocation is K4 (when ``do_fork`` /
``copy_mm`` runs against a new ``mm_struct``); K7 (when
``mmap`` is exposed to userspace via binfmt_wasm) is when
the degenerate PTE-as-VMA-bookkeeping property gets
exercised end-to-end. We revisit then if anything surprises.

--------------------------------------------------------------------------------
3. Atomics implementation
--------------------------------------------------------------------------------

3.1 Same-memory atomics (kernel-internal)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``arch/wasm32/include/asm/atomic.h`` and ``arch/wasm32/include/
asm/cmpxchg.h`` wrap clang's ``__atomic_*`` / ``__c11_atomic_*``
builtins, with explicit memory-order arguments. The kernel's
``atomic_t``, ``atomic_long_t``, ``refcount_t``, RCU pointer
swaps, ``cmpxchg``, ``xchg``, ``set_bit`` / ``clear_bit`` /
``test_and_set_bit``, and the wait/wake futex word
manipulations all go through these wrappers. The Kbuild flag
``-matomics -mbulk-memory`` causes clang to lower the builtins
to wasm-threads atomic opcodes against the default memory
(== ``env.kernel_memory`` in the kernel module's view).

Memory-order mapping (Linux ↔ wasm):

============  =====================  ================================
Linux         clang builtin order    wasm opcode emitted
============  =====================  ================================
relaxed       __ATOMIC_RELAXED       atomic op (acquire-release-free)
acquire       __ATOMIC_ACQUIRE       atomic op + fence
release       __ATOMIC_RELEASE       atomic op + fence
acq_rel       __ATOMIC_ACQ_REL       atomic op + 2 fences
seq_cst       __ATOMIC_SEQ_CST       atomic op + 2 fences
============  =====================  ================================

The wasm-threads proposal defines all atomic ops to be
sequentially consistent; the fence machinery exists for code
generators that want the Linux-style fine-grained ordering.
We honour the Linux memory-order argument to maximise the
chance that a relaxed atomic stays a single opcode rather
than acquiring fence pollution.

3.2 Cross-memory atomics (K4 futex preview)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

K4 needs ``cmpxchg`` (and ``xchg``, ``add``) against
``env.user_memory`` from the kernel module's code — for futex
word manipulation: the futex word lives in user memory; the
kernel module manipulates it atomically when ``schedule()``
sleeps a task or a wake_up_process delivers a notify.

Per Q3's empirical probe, V8 accepts the multi-memory atomic
binary encoding but LLVM 22 does not accept it in textual
form. The K4 implementation lives in
``arch/wasm32/include/asm/cmpxchg-user.h`` (header for the
C-side API: ``cmpxchg_user(ptr_in_user, old, new)``) and
``arch/wasm32/lib/atomics-user.S`` (the inline-asm or
raw-byte function bodies). Mirrors the
``arch/wasm32/lib/uaccess.S`` pattern of "C wraps a tiny
.S that wraps the wasm opcode we can't yet name in textual
syntax."

3.3 wasm-inspect atomic-opcode count
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Every K-phase's close-commit body includes a line of the form::

    wasm-inspect atomic-opcode counts (vmlinux.wasm):
      i32.atomic.rmw.cmpxchg  N
      i32.atomic.rmw.add      N
      ...
      i32.atomic.store         N
      i32.atomic.load          N
      memory.atomic.wait32     N
      memory.atomic.notify     N

K2 baseline (pre-K3) was: 1 ``i32.atomic.rmw.cmpxchg`` (in
``__wasm_init_memory``), 1 ``i32.atomic.store``, 1
``memory.atomic.wait32``, 1 ``memory.atomic.notify``. All
were wasm-ld-injected or host-import-shim, **not** kernel C
code. K3 acceptance requires the cmpxchg count to be
≫ 1 specifically because kernel C code lowered through real
atomics.

--------------------------------------------------------------------------------
4. Buddy allocator
--------------------------------------------------------------------------------

``mm/page_alloc.c`` runs unmodified. ``mem_map[]`` is sized by
memblock to ``(usable_end - usable_start) / PAGE_SIZE`` page
descriptors. ``free_area_init`` populates a single
``ZONE_NORMAL`` zone. ``MAX_ORDER`` (the maximum buddy order)
stays at the upstream default of 10 — 4 MiB max single
allocation at PAGE_SIZE=4096; this also caps ``vmalloc`` per
Q2 above. A future ``CONFIG_FORCE_MAX_ZONEORDER`` bump can
raise it if a subsystem demands it; the present default
matches stock x86_64 and is the most-tested code path.

--------------------------------------------------------------------------------
5. Slub (SLUB allocator)
--------------------------------------------------------------------------------

K3 picks ``CONFIG_SLUB=y`` (slub is the upstream default;
slob is removed in 6.12; slab is being deprecated).

**K3 close: ``CONFIG_SLUB_TINY=y``.** Per the §0(c) debt row
5, K3 ships with ``CONFIG_SLUB_TINY=y``, which removes the
per-CPU SLUB fastpath. Allocations go through the per-node
partial list. This is a workaround: when ``CONFIG_SLUB_TINY=n``
plus our current per-CPU anchor setup, the per-CPU dynamic
allocator's ``static_size``-driven offset math hands out
``cpu_slab`` struct pointers that overlap addressable
adjacencies in the per-CPU dynamic area, corrupting
kmalloc-256's freelist pointer with the address of an adjacent
kmalloc-128 partial field, which then fails the alignment
``BUG_ON`` in ``kernel/workqueue.c::init_pwq``. Real fix is the
percpu post-link Python pass (§0(c) debt row 4); when it lands,
``CONFIG_SLUB_TINY=n`` becomes selectable again.

``kmem_cache_init`` follows the upstream choreography:
bootstrap a tiny static ``kmem_cache_node`` slab, then a
static ``kmem_cache`` slab, then promote both to dynamically-
allocated. ``kmem_cache_init_late`` creates the kmalloc
caches (sized 8, 16, 32, …, 8192, 16384) and sets the
``slab_state`` to ``UP``. After that ``kmalloc`` / ``kfree``
work.

Arch hooks required:

* None confirmed at K3 open; none added at K3 close. The
  early-boot ``kmem_cache_node`` bootstrap in ``mm/slub.c:5177``
  (which K2 panicked in) is what K3 unblocks by making
  memblock and the page allocator populate ``mem_map``
  correctly. No new ``arch/wasm32/`` files are expected for
  slub itself; the SLUB_TINY workaround is a Kconfig flip.

--------------------------------------------------------------------------------
6. vmalloc
--------------------------------------------------------------------------------

Per Q2: ``vmalloc(N)`` → ``__get_free_pages(get_order(N))``;
``vfree(p)`` → ``free_pages(p, get_order(known_size))``.

K3 implementation: a thin wrapper file
``arch/wasm32/mm/vmalloc.c`` overriding the upstream
``vmalloc`` / ``vfree`` / ``vzalloc`` / ``vmalloc_node`` /
``vmalloc_array`` entry points. The wrapper records the
order of each allocation in a small associative array
(keyed by returned pointer) so ``vfree`` can return the
right run size without taking a size argument.

The wrapper file is in ``arch/wasm32/mm/`` — *not* a fork of
``mm/vmalloc.c``. The upstream ``mm/vmalloc.c`` continues to
build (its internal machinery is unused; the symbols we
define here override the ones it would have provided via
``weak`` linkage).

--------------------------------------------------------------------------------
7. Per-CPU areas
--------------------------------------------------------------------------------

``setup_per_cpu_areas`` is the generic upstream version
(``mm/percpu.c::pcpu_alloc_alloc_info``-driven). Per-CPU
storage lives in ``env.kernel_memory`` like everything else;
each Worker reads ``smp_processor_id`` from its per-instance
wasm global (set by HardwareJS at Worker spawn — K4 work,
default 0 at K3) and indexes the per-CPU arrays
accordingly. The K3 ``num_online_cpus() == 1`` invariant
means only the CPU-0 area sees traffic during the K3
acceptance run.

7.1 Linker-anchor shim (K3 debt #4)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``__per_cpu_start`` / ``__per_cpu_end`` / ``__per_cpu_load``
are linker-script anchors in mainline Linux, bracketing the
``.data..percpu*`` family of segments via
``include/asm-generic/vmlinux.lds.h::PERCPU_INPUT``. wasm-ld
does not consume the linker script. K3 ships
``arch/wasm32/kernel/percpu_anchors.c`` as a discovery-time
workaround:

* Three ``char[1]`` placeholders in synthetic sections
  ``.data..percpu..aa_start`` and ``.data..percpu..zz_end``
  with naming chosen so wasm-ld's alphabetical-by-section-name
  emission order brackets the real percpu data between them.
* ``--no-merge-data-segments`` passed to ``wasm-ld`` in
  ``scripts/link-vmlinux-wasm.sh`` so the synthetic sections
  remain as separate data segments rather than being folded
  into a single ``.data``.
* The same trick brackets the five
  ``stop`` / ``dl`` / ``rt`` / ``fair`` / ``idle`` sched_class
  symbols. ``kernel/sched/core.c::sched_init`` BUG_ONs unless
  ``&stop_sched_class < &dl_sched_class < &rt_sched_class <
  &fair_sched_class < &idle_sched_class`` in memory order;
  mainline's ``SCHED_DATA`` macro enforces this via the
  linker script.

This is enough to satisfy the K3 invariants — ``static_size``
is set to a small positive value (1 + page alignment ≈ 4097)
so ``pcpu_setup_first_chunk`` doesn't BUG_ON. It is NOT
enough for correct per-CPU allocator math, which is why
``CONFIG_SLUB_TINY=y`` is also needed (§5 above and §0(c)
debt row 5). The real fix is the K5/K6 post-link Python pass
that emits real ``--defsym`` boundary symbols for both
percpu and sched_class — at which point both the
``percpu_anchors.c`` shim and the SLUB_TINY workaround
disappear together.

--------------------------------------------------------------------------------
8. Constraint on subsequent K-phases
--------------------------------------------------------------------------------

K4 cannot start until this section is committed and the K4
implementation notes reference each Q1–Q5 answer by number.
Any K4 commit that conflicts with §2 here forces a separate
doc commit first updating §2 — the spec doesn't silently
drift.

K5 / K6 inherit the constraint. K7 (userspace) is when Q1
(PAGE_SIZE) and Q5 (PTE-as-VMA) become observable to
external code; either of those changing after K7 is an
ABI break and requires a major-version bump.
