.. SPDX-License-Identifier: GPL-2.0

================================================================================
arch/wasm32 boot protocol (wasm_boot_info v2)
================================================================================

:Status: K2 (setup_arch v1) → K6-B1 (v2 + initramfs). Defines the
         HardwareJS → kernel handoff at module instantiation time.
         v1 was the initial K2..K5 surface; v2 lands at K6-B1 to
         carry HardwareJS-supplied initramfs bytes
         (``initrd_offset`` / ``initrd_size``). The struct stays at
         96 bytes; the v1 reserved tail shrinks from u32[4] to
         u32[2] to make room.

This document is the source of truth for *what HardwareJS hands the
kernel at boot time* and *how the kernel reads it*. It plays the same
role for the boot interface that ``memory-model.rst`` plays for the
memory interface: spec-first, code-second. Treat any divergence
between this document and either ``arch/wasm32/include/asm/boot.h`` or
``hardwarejs/src/bootKernel.ts`` as a bug in the code, not the doc.

Why a boot protocol at all
~~~~~~~~~~~~~~~~~~~~~~~~~~

Every Linux arch port has a bootloader → kernel handoff:

  - x86 receives ``boot_params`` (offset 0x1f1 in the bzImage real-mode
    stub).
  - arm64 / riscv receive a flattened devicetree pointer (in ``x0`` or
    ``a1`` respectively).
  - powerpc receives a `prom_init`-built tree pointer.
  - multiboot receives ``mb_info`` in ``%ebx``.

The handoff is small but load-bearing: it carries the RAM map, the
command line, optionally an initramfs pointer, and a magic so the
kernel can refuse to boot if the bootloader's idea of "this kernel" is
wrong. wasm32 is no different — it just runs in a JS engine instead
of on metal. HardwareJS is the bootloader.

For K2, only a small subset of what a real bootloader provides is
needed (RAM map + command line + boot-CPU id). The struct is
versioned so K3+ can grow it (initramfs, multi-bank RAM map, host
clock identity) without breaking K2 binaries.

--------------------------------------------------------------------------------
1. Entry-point ABI
--------------------------------------------------------------------------------

The kernel exports a single boot entry-point::

    (export "wasm_start_kernel" (func (param i32)))

The single ``i32`` parameter is the byte offset, into
``env.kernel_memory``, of a ``struct wasm_boot_info`` populated by
HardwareJS before the call. The kernel reads (never writes) the
struct, validates it (see §3), copies out the fields it needs, and
proceeds with ``start_kernel()``.

Rationale for "offset into ``env.kernel_memory``", not a JS pointer
type: wasm32 pointers are 32-bit offsets into the active linear
memory. HardwareJS writes the struct directly into the SAB-backed
``env.kernel_memory`` (which it owns the allocation of, §2) and hands
the kernel the address. The kernel can then dereference fields via
ordinary loads against memory 0 (= ``env.kernel_memory`` in its own
local view) — no per-field copy required, no bounce buffer, and the
struct is unambiguously inside the same address space the kernel
already operates on.

K1 used a zero-argument signature; K2's bump to ``(i32)`` is the
first ABI break in the kernel module's export shape. We tag this
explicitly: a v1 HardwareJS cannot instantiate a v0 kernel and vice
versa (the wasm engine throws a type-mismatch ``LinkError`` at
instantiate, which is the correct behaviour).

--------------------------------------------------------------------------------
2. Where the struct lives
--------------------------------------------------------------------------------

HardwareJS allocates the struct out of ``env.kernel_memory`` itself,
at the address immediately following the kernel image. Concretely:

* ``__heap_base`` (a wasm global exported by ``wasm-ld``) marks the
  first byte past the kernel's ``.text`` / ``.rodata`` / ``.data`` /
  ``.bss``.
* HardwareJS rounds ``__heap_base`` up to 16-byte alignment, places
  the ``struct wasm_boot_info`` there, copies the command-line bytes
  immediately after the struct (also 16-byte aligned), and exposes
  ``usable_start`` (see §4) as "first byte ``memblock_add`` may take".

Layout of the prologue inside ``env.kernel_memory``::

    +----------------------+ 0
    |  __wasm_call_ctors / |
    |  .text / .rodata     |
    |  .data / .bss        |  ┐
    |  __heap_base ─────── | ─┘  end-of-image marker emitted by wasm-ld
    +----------------------+
    |   16B align padding  |
    +----------------------+ <-- boot_info_offset (passed as arg to wasm_start_kernel)
    |   struct             |
    |   wasm_boot_info     |
    +----------------------+
    |   16B align padding  |
    +----------------------+ <-- boot_info.cmdline_offset
    |   command-line bytes |
    |   (NUL-terminated)   |
    +----------------------+
    |   16B align padding  |
    +----------------------+ <-- boot_info.usable_start
    |                      |
    |   memblock's domain  |
    |   (free RAM)         |
    |                      |
    +----------------------+ <-- boot_info.usable_end (== total_memory in v1)

HardwareJS knows ``__heap_base`` because it reads it back from the
freshly-instantiated module's exports just before calling
``wasm_start_kernel``. The placement is **not** hardcoded in either
the C or the TypeScript; both sides compute it from ``__heap_base``.

If HardwareJS ever needs to reserve a trailing region for itself —
e.g. an in-RAM debug ring, a virtio MMIO doorbell page — it shrinks
``usable_end`` accordingly. K2 has no such reservation;
``usable_end == total_memory`` and the kernel sees one contiguous
RAM bank.

--------------------------------------------------------------------------------
3. Struct ``wasm_boot_info`` (v2; v1 → v2 diff inline)
--------------------------------------------------------------------------------

::

    /* All fields little-endian, naturally aligned, no padding bytes.
     * Total size: 96 bytes in BOTH v1 and v2 (sizeof(struct
     * wasm_boot_info) MUST equal the struct_size field's value).
     * v2 (K6-B1) reclaims 8 bytes from v1's reserved[4] for
     * initrd_offset + initrd_size.
     */
    struct wasm_boot_info {
        __le32  magic;             /* 'W' 'B' 'I' '1' == 0x31494257 */
        __le32  abi_version;       /* 1 for v1; 2 for v2 (K6-B1+) */
        __le32  struct_size;       /* 96 in v1 and v2 */
        __le32  flags;             /* reserved, MUST be 0 */

        __le64  total_memory;      /* total bytes in env.kernel_memory */
        __le64  usable_start;      /* first byte memblock may take */
        __le64  usable_end;        /* last+1 byte memblock may take */

        __le64  kernel_image_start;  /* 0 in v1 and v2 */
        __le64  kernel_image_end;    /* __heap_base */

        __le32  cmdline_offset;    /* offset into env.kernel_memory */
        __le32  cmdline_length;    /* bytes, excluding trailing NUL */

        __le32  num_cpus;          /* HardwareJS's intended max */
        __le32  boot_cpu_id;       /* 0 in v1 and v2 */

        __le64  host_timer_freq_hz;  /* for jiffies math at K5+ */

        /* --- v2 additions (K6-B1) --- */
        __le32  initrd_offset;     /* offset into env.kernel_memory
                                    * of HardwareJS-supplied cpio,
                                    * or 0 if no initramfs */
        __le32  initrd_size;       /* byte length of initrd, or 0 */

        __le32  reserved[2];       /* zero in v2; nonzero rejects.
                                    * Shrunk from u32[4] in v1. */
    };

Magic and version fields
~~~~~~~~~~~~~~~~~~~~~~~~

* ``magic`` is the ASCII string ``WBI1`` interpreted as little-endian
  u32 (i.e. ``0x31494257``). The kernel rejects any other value with
  ``panic("wasm_boot_info: bad magic 0x%08x")`` BEFORE touching
  memblock or printk's ring buffer (the early console is already up
  from K1 so this panic surfaces via ``linux.dmesg``).

* ``abi_version`` is the protocol version. The build pins the
  currently-supported version in
  ``arch/wasm32/include/asm/boot.h::WASM_BOOT_INFO_ABI_VERSION``
  (v1 == 1 in K2..K5; v2 == 2 in K6-B1+). Other values cause
  ``panic("wasm_boot_info: unsupported abi_version %u")``. Versions
  are not back-compat: kernel + HardwareJS must agree exactly. If
  we need to extend the struct, we bump this number; we do not
  try to extend in-place by relying on ``struct_size``.

* ``struct_size`` must equal ``sizeof(struct wasm_boot_info)`` for
  the given ``abi_version``. The kernel rejects any other size with
  ``panic("wasm_boot_info: struct_size mismatch")``. This guards
  against the case where HardwareJS and kernel are nominally on the
  same version but somehow have drifted (e.g. unrelated padding
  changes in a struct member type).

* ``flags`` is reserved in v1 and v2 and must be zero. Nonzero
  rejects.

* ``reserved[]`` is reserved in v1 (u32[4]) and v2 (u32[2]); every
  element must be zero. Any nonzero element rejects. This means
  HardwareJS can NEVER emit-and-ignore unknown fields — the kernel
  reads every byte and the spec is monolithic per version.

Memory map fields
~~~~~~~~~~~~~~~~~

* ``total_memory`` is the total byte-length of ``env.kernel_memory``
  *at instantiation time*. The kernel uses this as an upper bound;
  HardwareJS is responsible for sizing the Memory's ``initial`` and
  ``maximum`` pages such that this value lies in the closed
  ``[initial * 64KiB, maximum * 64KiB]`` interval. The kernel does
  not currently call ``memory.grow``; that is K3+ territory.

* ``usable_start`` is the first byte of RAM that ``memblock_add``
  may claim. It MUST be ``>= kernel_image_end`` and 16-byte aligned.
  HardwareJS computes it as
  ``(boot_info_offset + struct_size + cmdline_length + 1)`` rounded
  up to the next 16-byte boundary, after rounding ``boot_info_offset``
  itself to 16 (so the struct, cmdline, and ``usable_start`` are all
  aligned).

* ``usable_end`` is one past the last byte of usable RAM. In v1 this
  is always ``total_memory``. If HardwareJS reserves a trailing
  region (post-K2), ``usable_end`` shrinks; the kernel never sees
  HardwareJS-reserved RAM as part of its allocator domain.

* ``kernel_image_start`` is always 0 in v1 (the kernel module's
  load address is byte 0 of its memory-0 view).

* ``kernel_image_end`` is the value of the ``__heap_base`` wasm
  global as observed by HardwareJS just before the boot call. The
  kernel verifies this is ``>= 0x1000`` (paranoia floor; a kernel
  smaller than 4 KiB would be a build bug worth panicking on) and
  ``< total_memory``. ``memblock_reserve(0, kernel_image_end)`` is
  the first thing setup_arch does, so the kernel image's static
  data is never handed back to the buddy allocator.

Command line
~~~~~~~~~~~~

* ``cmdline_offset`` is the byte offset into ``env.kernel_memory``
  of the command-line string. The bytes are valid for at least
  ``cmdline_length`` bytes; HardwareJS additionally writes a NUL
  byte at ``cmdline_offset + cmdline_length`` (the kernel relies
  on this for ``strlen`` paths). It MUST satisfy ``cmdline_offset
  + cmdline_length < usable_start`` (i.e. the cmdline lives within
  the prologue, not in memblock-managed RAM).

* ``cmdline_length`` is the byte length excluding the trailing
  NUL. Maximum is 4095; the kernel rejects ``cmdline_length >
  COMMAND_LINE_SIZE - 1`` with a panic. Strings shorter than 4095
  are fine and arrive as-is (no per-arch ``""`` substitution).

* If ``cmdline_length == 0`` the kernel treats the command line as
  empty (``""``). ``cmdline_offset`` is then ignored, but
  HardwareJS SHOULD still set it to ``boot_info_offset +
  struct_size`` rounded to 16 so a memory dump shows the prologue
  in its expected shape.

CPU topology
~~~~~~~~~~~~

* ``num_cpus`` is the maximum number of logical CPUs HardwareJS
  intends to support in this boot. K2 ignores this (we hardcode
  ``num_online_cpus() == 1``); K4 onwards reads it to size
  per-CPU areas and CPU-id allocation. Must be in
  ``[1, CONFIG_NR_CPUS]``; the kernel rejects out-of-range with
  ``panic("wasm_boot_info: num_cpus out of range")``.

* ``boot_cpu_id`` is the logical id of the CPU running
  ``wasm_start_kernel``. Always 0 in v1; the kernel rejects any
  other value to keep K2's setup_arch monomorphic.

Host timer
~~~~~~~~~~

* ``host_timer_freq_hz`` is the host clock frequency in Hz at
  which ``linux.now_ns`` will tick. In practice this is always
  10^9 (nanoseconds) on Node and the browser, but the field
  exists so the kernel does not have to assume that — K5's
  hrtimer integration reads this to set the clocksource shift /
  mult fields. K2 stores the value but does not act on it; if
  HardwareJS reports anything other than 10^9 in K2 we log it
  via pr_info and continue.

Initramfs (v2 additions, K6-B1)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* ``initrd_offset`` is the byte offset into ``env.kernel_memory``
  of a HardwareJS-supplied initramfs (newc/crc cpio archive). 0
  means "no initramfs supplied"; in that case ``initrd_size``
  must also be 0 (the kernel rejects the mismatched pair with
  ``panic("wasm_boot_info: initrd_offset=%u initrd_size=%u (one
  zero, one not)")``).

* ``initrd_size`` is the byte length of the initramfs. 0 paired
  with ``initrd_offset == 0``. When nonzero, the window
  ``[initrd_offset, initrd_offset + initrd_size)`` MUST:

  - lie entirely inside ``[0, total_memory)``,
  - start at or past ``kernel_image_end`` (not overlap the
    kernel image), and
  - not overlap the memblock window
    ``[usable_start, usable_end)``.

  Violations of any of these three are rejected with a precise
  panic line naming the overlap. HardwareJS' canonical layout
  places the initrd in the boot prologue between the cmdline
  buffer and ``usable_start``; the kernel does not require that
  exact placement, only the disjointness invariants.

* On the kernel side, ``setup_arch`` reads these two fields and
  sets ``initrd_start = initrd_offset`` and ``initrd_end =
  initrd_start + initrd_size`` (the upstream globals from
  ``init/do_mounts_initrd.c``). It also sets
  ``initrd_below_start_ok = 1`` because the canonical placement
  is below ``min_low_pfn = PFN_UP(usable_start)`` and the
  init/main.c sanity check would otherwise reject the initrd.

* wasm32's ``__initcall*_start/end`` section symbols are 0-imports
  (see ``arch/wasm32/kernel/sections.c``) so the upstream
  ``rootfs_initcall(populate_rootfs)`` never runs. Instead,
  ``arch_kernel_init_pre_run_init`` (the K5-C2 arch hook) calls
  ``unpack_to_rootfs(initrd_start, initrd_end - initrd_start)``
  directly. This is the "wasm32 doesn't dispatch initcalls" §15
  row's K6-B1-scoped workaround; a generic initcall-dispatch
  shim is a future K-phase concern.

--------------------------------------------------------------------------------
4. Strict validation
--------------------------------------------------------------------------------

The kernel's first action in ``wasm_start_kernel(boot_info_offset)``
is to read 4 bytes at ``boot_info_offset + offsetof(magic)``,
compare to the v1 magic, and panic on mismatch BEFORE doing anything
else — including before calling ``start_kernel`` or even
``__wasm32_set_current``. The boot console registered by K1 is still
available because the wasm module's static initializers already ran
during ``__wasm_call_ctors`` (run by HardwareJS before
``wasm_start_kernel``); the early dmesg path therefore works.

Rejection cases (each panics with a distinct message so the test
harness can assert the specific failure mode):

============================================================  =================================================
Cause                                                         panic() reason
============================================================  =================================================
``magic`` ≠ ``WBI1``                                          ``wasm_boot_info: bad magic 0x%08x``
``abi_version`` ≠ supported                                   ``wasm_boot_info: unsupported abi_version %u``
``struct_size`` ≠ ``sizeof(struct wasm_boot_info)``           ``wasm_boot_info: struct_size %u, expected %zu``
``flags`` ≠ 0                                                 ``wasm_boot_info: flags must be 0, got 0x%x``
any ``reserved[i]`` ≠ 0                                       ``wasm_boot_info: reserved[%u] must be 0``
``kernel_image_end`` < 0x1000                                 ``wasm_boot_info: kernel_image_end < 4 KiB``
``kernel_image_end`` ≥ ``total_memory``                       ``wasm_boot_info: kernel_image_end >= total_memory``
``usable_start`` < ``kernel_image_end``                       ``wasm_boot_info: usable_start < kernel_image_end``
``usable_end`` > ``total_memory``                             ``wasm_boot_info: usable_end > total_memory``
``usable_start`` ≥ ``usable_end``                             ``wasm_boot_info: empty memblock window``
``cmdline_length`` ≥ ``COMMAND_LINE_SIZE``                    ``wasm_boot_info: cmdline_length too large``
``cmdline_offset + cmdline_length`` ≥ ``usable_start``        ``wasm_boot_info: cmdline overlaps memblock``
``num_cpus`` < 1 or > ``CONFIG_NR_CPUS``                      ``wasm_boot_info: num_cpus out of range``
``boot_cpu_id`` ≠ 0                                           ``wasm_boot_info: boot_cpu_id != 0``
(v2) one of ``initrd_offset``/``initrd_size`` zero, other not ``wasm_boot_info: initrd_offset=%u initrd_size=%u``
(v2) initrd window past ``total_memory``                      ``wasm_boot_info: initrd window ... extends past total_memory``
(v2) ``initrd_offset`` < ``kernel_image_end``                 ``wasm_boot_info: initrd_offset ... < kernel_image_end``
(v2) initrd overlaps ``[usable_start, usable_end)``           ``wasm_boot_info: initrd window ... overlaps memblock``
============================================================  =================================================

These checks live in ``arch/wasm32/kernel/setup.c::wasm_validate_boot_info``,
which is called exactly once from ``wasm_start_kernel`` before
``start_kernel()``. Adding validation rules requires a doc-side
addition to this table in the same commit.

--------------------------------------------------------------------------------
5. HardwareJS-side construction
--------------------------------------------------------------------------------

The reference implementation lives in
``hardwarejs/src/kernelWorker.mjs`` (struct writer) with the
field-offset / version constants mirrored in
``hardwarejs/src/boot-info-abi.ts`` (TypeScript module exported
from ``hardwarejs`` so callers can validate boot_info bytes
without depending on private offsets). It MUST satisfy all of §3
and §4. In particular:

1. Read ``__heap_base`` from ``instance.exports``.
2. Align that value to the next 16-byte boundary; call this
   ``boot_info_offset``.
3. Write the magic/version/size fields first (atomic vs. the
   rest of the struct is unimportant — the kernel runs in a
   single Worker that is not yet awake), then the memory map
   fields, then the cmdline_offset/length, then the
   cpu/timer fields, then the v2 initrd_offset/initrd_size,
   then reserved.
4. Copy the command-line string at
   ``boot_info_offset + struct_size`` rounded to 16, with a NUL
   byte one past the end.
5. If ``bootKernel({ initramfs })`` was supplied, copy the cpio
   bytes at ``(cmdline_offset + cmdline_length + 1)`` rounded to
   16; record that offset and length in
   ``initrd_offset`` / ``initrd_size``. Otherwise leave both
   fields zero.
6. Set ``usable_start`` to the next 16-byte boundary past either
   the cmdline NUL (no initramfs) or the initrd region (with
   initramfs).
7. Set ``usable_end = total_memory``.
8. Call ``instance.exports.wasm_start_kernel(boot_info_offset)``.

If HardwareJS itself wants to verify before calling the kernel —
useful for fuzz-test harnesses that construct malformed boot_info on
purpose — it MAY parse the struct back and re-check; the kernel's
validation is the authoritative one.

--------------------------------------------------------------------------------
6. ABI stability promise
--------------------------------------------------------------------------------

The v1 protocol shipped at K2 and was the stable surface through
K5. K6-B1 bumps to v2 to add ``initrd_offset`` /
``initrd_size`` for HardwareJS-supplied initramfs delivery. Field
additions, renames, type changes, semantic changes: all require
a ``abi_version`` bump.

The ``reserved[]`` array exists to let us experiment in
non-stable branches (e.g. an out-of-tree K-experiment that wants
to plumb a new field) without burning a version number on every
attempt; production branches between v0.1.0 and v1.0.0 must
either land their new field as a new versioned ABI, or stay
within ``reserved[]``.

After v1.0.0, any new ABI version is a breaking change of the
``wackywasm`` userspace too (since userspace binaries depend on a
kernel that depends on a HardwareJS) and is gated on the next
major version of the project.

--------------------------------------------------------------------------------
7. v1 → v2 historical note (K6-B1)
--------------------------------------------------------------------------------

K6-B1 bumped the ABI from v1 to v2. The changes were
intentionally minimal:

* ``abi_version`` 1 → 2.
* ``initrd_offset`` (u32) and ``initrd_size`` (u32) added at
  byte offsets 80 and 84.
* ``reserved[4]`` shrunk to ``reserved[2]`` (offsets 88..95) to
  keep ``sizeof(struct wasm_boot_info)`` at 96.
* Validation gained four new rejection cases (see §4 table) that
  enforce disjointness of the initrd window from the kernel
  image and the memblock window.

A C ``static_assert`` in ``arch/wasm32/include/asm/boot.h`` pins
each new field's byte offset; the TypeScript mirror in
``hardwarejs/src/boot-info-abi.ts`` exports the same offsets as
named constants and is cross-checked at runtime by
``hardwarejs/test/k6-b1-bootinfo-v2.test.ts``.

A v1 caller hitting a v2 kernel (or vice versa) is rejected at
``wasm_boot_panic("wasm_boot_info: unsupported abi_version
%u")``. We intentionally did NOT add forward/backward-compat
handling at K6-B1; the K6-B1 commit bumps both sides in lockstep.
Forward-compat support (e.g. v2 kernel accepting v1 callers with
no initrd) is a §15 future row to revisit only if a real demo
client cannot be updated in lockstep with kernel builds.
