.. SPDX-License-Identifier: GPL-2.0

====================================================================
Conventions for post-link wasm rewrite passes
====================================================================

This document is the single home for the project's growing set of
"post-link surgical rewrite" tools that operate on ``vmlinux.wasm``
between ``wasm-ld`` and the runnable artifact handed to HardwareJS.

The conventions in this document are referenced from
``docs/ARCHITECTURE.md`` §15 debt rows that depend on a post-link
pass, and any new post-link tool MUST be reviewed against the rules
here.

---------------------------------------------------------------------
0. Why post-link surgery exists in this port at all
---------------------------------------------------------------------

The kernel image is built from a 6.12 LTS source tree that we do
not fork (§16 PR acceptance criteria). Several invariants the
kernel source assumes are normally upheld by:

* a GNU linker script (``vmlinux.lds.S``) consumed by ``ld``;
* ``wasm-ld`` and the rest of the LLVM toolchain;
* Binaryen's ``wasm-opt`` pass collection;
* the V8 / SpiderMonkey runtime.

Three of those four have gaps that we satisfy without modifying
upstream sources:

* ``wasm-ld`` does not consume GNU linker scripts. Boundary
  symbols like ``__per_cpu_start`` / ``__per_cpu_end`` /
  ``__initcall*_start/end`` / ``__setup_start/end`` therefore
  cannot be defined via ``vmlinux.lds.S``.
* ``wasm-opt`` has occasional bugs (e.g. the topmost-runtime
  asyncify-asserts FIXME — see ``sched-model.rst`` §3) whose fix
  upstream is months-to-years out, but whose effect on the kernel
  is immediate.
* The wasm module has no syntax for declaring a second imported
  memory in clang-emitted ``.wat`` form, despite Binaryen +
  V8 accepting the binary form. (See ``mm-model.rst``.)

For each, the K-phase boundary is "kernel proceeds further" and
not "upstream toolchain catches up," so the project lands a
surgical post-link rewrite that yields the desired ``vmlinux.wasm``
shape without forking the source.

---------------------------------------------------------------------
1. The hierarchy of post-link tools
---------------------------------------------------------------------

::

  vmlinux.a  (LLVM IR / wasm objects, no symbols outside arch/wasm32)
     |
     v   wasm-ld + scripts/link-vmlinux-wasm.sh
  vmlinux.wasm.linked
     |
     v   (optional) wasm-opt --asyncify --pass-arg=asyncify-asserts
  vmlinux.wasm.asyncify-with-asserts
     |
     v   (optional) scripts/asyncify-strip-topmost-asserts.py
  vmlinux.wasm.asyncify
     |
     v   (planned K5/K6) scripts/wasm-percpu-bracket.py
  vmlinux.wasm.percpu
     |
     v   (planned K5/K6) scripts/wasm-initcall-bracket.py
  vmlinux.wasm.initcall
     |
     v   scripts/wasm-add-memory.py
  vmlinux.wasm        <-- the artifact HardwareJS loads

Pass order matters; the order above is fixed by an invariant each
pass would otherwise violate. New passes MUST declare which pass
they depend on (read-after) and which depend on them (write-before)
in their module docstring.

---------------------------------------------------------------------
2. Substitutive vs. additive — which shape your pass is
---------------------------------------------------------------------

Every post-link pass falls into one of two shapes:

**Substitutive (the default, lower-risk):**

  Modifies bytes in-place, **never changing byte count**. Code/data
  section sizes, function-body sizes, and absolute offsets of every
  byte AFTER the rewrite point are preserved. This means: no LEB128
  re-encoding of section lengths, no function-body length
  recomputation, no relocation cascade across the rest of the
  binary, no DWARF offset shifts.

  ``scripts/asyncify-strip-topmost-asserts.py`` is the current
  representative. Its strategy: locate the 9-byte assertion pattern
  ``[0x23 LEB(state) 0x20 LEB(oldState) 0x47 0x04 0x40 0x00 0x0B]``
  and overwrite all 9 bytes with ``0x01`` (nop). Stack effect is
  preserved (``global.get`` and ``local.get`` push 2 i32s; the
  ``i32.ne`` / ``if`` / ``unreachable`` / ``end`` chain consumes
  them; replacing the WHOLE sequence with nops drops both pushes
  too, net stack effect = 0).

  Same-byte-count nop substitution preserving stack semantics is
  the **preferred shape** for any rewrite that is logically a
  "delete this fragment" operation. Pads with nops; arithmetic
  unchanged; offsets unchanged.

**Additive (acceptable only at section boundaries):**

  Inserts new content. Always changes byte count of the section
  it inserts into, which in turn re-encodes the section's LEB128
  length and shifts the section's start offset (and every byte
  after it). The pass MUST then update any offsets that crossed
  the insert point: function body length prefixes, table /
  memory / global / export references that index by ordinal (not
  affected) vs. by file offset (affected).

  ``scripts/wasm-add-memory.py`` is the current representative.
  It splices a second imported memory into the IMPORTS section
  (section 2). This is acceptable because:

  * The IMPORTS section is at the START of the module; nothing
    in CODE / DATA / EXPORTS references it by file offset.
  * All section indexes (memory 0, memory 1) are determined by
    ordinal within the new IMPORTS section, not by file offset,
    so referenced memidx values in the CODE section stay valid.
  * Names are the cross-module ABI (per
    ``docs/ARCHITECTURE.md`` §0(c)) so identity preservation is
    automatic.

  Additive insertion is permitted only at section boundaries
  where no internal offsets reference past the insert point.
  The pass MUST document this property in its module docstring.

**A pass that is logically substitutive should never expand
to additive without an explicit justification in its module
docstring** of why the additive form is necessary. Expanding to
additive cascades the cost of every following pass (each must
now recompute offsets from the new section sizes).

---------------------------------------------------------------------
3. Mandatory properties of a post-link pass
---------------------------------------------------------------------

A new post-link tool must:

3.1. **Produce a deterministic output** for a given input. Two
     runs of the pass against the same input bytes must yield
     byte-identical output. CI cross-checks this by re-running
     the pass and comparing.

3.2. **Fail loudly on input-shape mismatch.** If the pass relies
     on a specific Binaryen / wasm-ld / clang output shape (an
     export-name convention, a section ordering, a function-body
     prologue pattern), it must detect a mismatch and exit
     non-zero with a message that points the maintainer at the
     toolchain version expected vs. observed. Do not "best-
     effort" past an unexpected shape — the symptom would
     surface five subsystems later in the kernel boot.

3.3. **Ship a sibling probe under** ``linux-wasm/tools/``. The
     probe compiles a 1-3-function toy module, runs the same
     post-link pipeline on it, and asserts the pass's contract
     against the toy output. The probe runs in CI on every
     build. This catches Binaryen / clang / wasm-ld bumps that
     change the pass's input shape BEFORE vmlinux.wasm mysteriously
     traps. See ``docs/ARCHITECTURE.md`` §15.1 for the broader
     pattern.

3.4. **Document its pass-order dependencies** (read-after,
     write-before) in the module docstring.

3.5. **Declare its shape** in the module docstring: substitutive
     (same byte count) or additive (boundary insertion). If
     additive, declare which section it inserts into and why no
     internal-offset references cross the insert point.

3.6. **Reference a §15 row** that owns the debt the pass exists
     to satisfy. If the upstream gap closes, the §15 row closes
     and the pass either retires or moves into permanent
     toolchain-glue. (See §15.1 "Probe-driven debt retirement"
     for the retirement protocol.)

---------------------------------------------------------------------
4. Naming conventions
---------------------------------------------------------------------

* Live in ``linux-wasm/scripts/``.
* Filename starts with ``wasm-`` for additive / structural rewrites
  (``wasm-add-memory.py``, planned ``wasm-percpu-bracket.py``,
  ``wasm-initcall-bracket.py``).
* Filename starts with ``asyncify-`` for passes that operate on
  Asyncify-pass output (``asyncify-strip-topmost-asserts.py``,
  planned ``asyncify-whitelist.py``).
* Filename starts with ``link-`` for build-flow drivers
  (``link-vmlinux-wasm.sh``).
* Sibling probes live in
  ``linux-wasm/tools/k{N}-{pass-name}-probe/`` where N is the
  K-phase that introduced the pass.

---------------------------------------------------------------------
5. Listed passes (current + planned)
---------------------------------------------------------------------

==================================================== ============ ============ =========== =====================================
Pass                                                  Phase intro  Shape        Sibling probe   §15 row
==================================================== ============ ============ =========== =====================================
``scripts/wasm-add-memory.py``                       K1           additive     —           (none; permanent glue)
``scripts/wasm-inspect.py``                          K2           read-only    —           (none; read-only diagnostic)
``scripts/asyncify-strip-topmost-asserts.py``        K4           substitutive ``k4-asyncify-asserts-probe``   "asyncify-asserts enabled with named topmost-strip exceptions"
``scripts/asyncify-whitelist.py``                    K4           generator    —           (generates ``arch/wasm32/asyncify-whitelist.txt``)
``scripts/wasm-percpu-bracket.py`` (planned)         K5/K6        substitutive (planned)   "Linker-script-anchor symbols stubbed"
``scripts/wasm-initcall-bracket.py`` (planned)       K6           substitutive (planned)   "vmlinux.lds.S doesn't land __initcall_* ordering"
==================================================== ============ ============ =========== =====================================

When adding a new pass, append a row here in the same commit, or
the commit is incomplete (mirrors the §15 invariant).

---------------------------------------------------------------------
6. Anti-patterns
---------------------------------------------------------------------

* **Do not** rewrite by re-parsing → modifying → re-emitting the
  whole module. That cascades offsets and breaks every following
  pass. Operate on the smallest possible byte window.

* **Do not** rely on Binaryen or wasm-ld output details that
  aren't tested by the sibling probe. Toolchain bumps will
  surprise you and the surprise will arrive in vmlinux behavior,
  not in the pass itself.

* **Do not** silently no-op when input shape is unexpected. Fail
  loudly per §3.2.

* **Do not** combine multiple unrelated rewrites into one pass.
  Each pass owns one logical transform; pass ordering in §1 is
  how they compose.

* **Do not** mutate the input file in place. Always write a new
  output file. Build systems retry on partial failure; in-place
  mutation makes that retry corrupt the build.
