.. SPDX-License-Identifier: GPL-2.0

================================================================================
binfmt_wasm: userspace WebAssembly binary container for arch/wasm32
================================================================================

:Author: Linux-in-the-Browser project
:Status: STABLE for ABI version 1
:Implementation: ``arch/wasm32/kernel/binfmt_wasm.c`` and
   ``hardwarejs/src/binfmt.ts``

Scope
=====

This document specifies the userspace binary container format that
``arch/wasm32`` understands. It is the **only** userspace binary
format supported by the wasm32 port. There is no ELF on wasm32; there
is no thin-ELF wrapper around the wasm payload either. The container
is a *raw WebAssembly module*, distinguished from a generic wasm
module by the presence of a single project-owned custom section.

This format is part of the public ABI of the project. The custom
section name and the payload layout described below are NOT permitted
to change in a non-breaking way. Breaking changes require bumping the
``abi_version`` field; the kernel rejects unknown ``abi_version`` with
``-ENOEXEC``.

Rationale
=========

We chose raw wasm + custom section over a thin-ELF wrapper for four
reasons:

1. WebAssembly is the ISA. A thin-ELF wrapper would be ELF cosplay to
   make ``file(1)`` happy; the "code" is not machine code for any
   ``e_machine``.
2. HardwareJS, the userspace loader, reads our metadata via stock
   ``WebAssembly.Module.customSections()``. No custom parser.
3. Standard wasm tooling (``wasm2wat``, ``wasm-objdump``, ``wabt``,
   ``binaryen``) Just Works on the artifacts.
4. Custom sections are a first-class wasm construct and MUST be
   ignored by runtimes that don't recognise them. Our artifacts are
   therefore also valid plain wasm modules that can be inspected in
   any wasm runtime.

Custom section name
===================

::

    wasm.linux.exec

The name is part of the ABI. The kernel matches it exactly,
case-sensitive, as a UTF-8 byte sequence.

(Rationale for this particular name: reverse-DNS-ish and unlikely to
collide with names other ecosystems already use — Emscripten uses
``emscripten_metadata``, the wasm spec defines ``name``, etc.)

Custom section payload format
=============================

Binary, little-endian, fixed-prefix header followed by length-prefixed
extensions. Total payload MUST be ≤ 256 bytes for ABI version 1;
larger payloads are reserved for future versions and the kernel
rejects them on ABI version 1 with ``-ENOEXEC``.

Header (24 bytes, fixed)::

    offset  size  field                  notes
    ------  ----  ---------------------  -----------------------------------
     0      4     magic                  ASCII "WLEX" (0x57 0x4c 0x45 0x58)
     4      4     abi_version            u32 LE; MUST be 1
     8      4     flags                  u32 LE; reserved, MUST be 0
    12      4     required_stack_size    u32 LE; bytes, 0 = use loader default
    16      4     required_heap_init     u32 LE; wasm pages (64 KiB each), 0 = loader default
    20      4     required_heap_max      u32 LE; wasm pages, 0 = loader default

After the header, a length-prefixed string (entry symbol name):

::

    24      1     entry_symbol_len       u8; 0..127.  See below.
    25      N     entry_symbol           N bytes of ASCII, NOT null-terminated

Where ``N == entry_symbol_len``. For ABI version 1, ``entry_symbol``
MUST be exactly ``"_start"`` and ``entry_symbol_len`` MUST be exactly
``6``. The field is reserved for future versions but encoded now so
that older kernels reject newer entry conventions explicitly.

After the entry symbol, an optional syscall capability declaration:

::

    25+N    1     cap_len                u8; 0 means "no declaration, all caps allowed"
    26+N    M     cap_bitmap             ceil((max_syscall_nr+1)/8) bytes; bit i set => syscall i required
                                          M == cap_len; if cap_len == 0, the field is omitted entirely

For ABI version 1, the kernel ignores ``cap_bitmap`` (treats every
binary as if it required every syscall). The field is reserved so
that adding capability enforcement in a later version is a non-
breaking change for binaries that set ``cap_len == 0``.

Any bytes after the capability bitmap (up to the 256-byte payload
cap) MUST be zero in ABI version 1. The kernel does NOT inspect
them but reserves the right to in future versions.

Field semantics
---------------

* ``magic``: 4-byte literal ``"WLEX"`` (Wasm Linux EXecutable). Distinguishes
  this section from accidental name reuse by other tooling.
* ``abi_version``: monotonically increasing. ABI version 1 covers the
  layout above. Breaking layout changes bump this. Backwards-compatible
  additions (e.g. consuming previously-reserved fields) MAY land
  without a bump only if a clean default exists for older binaries.
* ``flags``: reserved; the kernel rejects nonzero flags in ABI 1.
* ``required_stack_size``: hint to the loader. The loader is allowed
  to round up, never down. Zero means "use loader default" (currently
  1 MiB).
* ``required_heap_init`` / ``required_heap_max``: hints to the
  loader for the wasm memory's ``initial`` / ``maximum`` page counts.
  Zero means "use loader default" (currently 32 init / 16384 max,
  i.e. 2 MiB / 1 GiB). The loader is allowed to round up the
  initial size if the wasm module's own ``memory`` import declares
  a larger minimum.
* ``entry_symbol``: fixed at ``"_start"`` in ABI 1. The named export
  is called with no arguments after ``__wasm_call_ctors`` (if exported)
  runs.
* ``cap_bitmap``: reserved enforcement; bit i corresponds to syscall
  number i; the syscall table is the upstream x86_64 syscall table
  (mirrored at ``arch/wasm32/include/asm/unistd_64.h``).

Detection sequence
==================

``binfmt_wasm.c::load_wasm_binary()`` performs, in this order:

1. ``bprm->buf`` (which holds the first ``BINPRM_BUF_SIZE`` bytes of
   the file) must contain the wasm magic at offset 0:
   ``\x00 \x61 \x73 \x6d \x01 \x00 \x00 \x00``. If not, return
   ``-ENOEXEC``.
2. Walk the WebAssembly section table starting at offset 8. For each
   section: read the 1-byte section id, the LEB128 section length.
   If id == 0 (custom section), read the LEB128 name length, then
   the name. If the name is exactly ``"wasm.linux.exec"``, found.
   Otherwise skip ``length`` bytes and continue. If end-of-file is
   reached without finding the section, return ``-ENOEXEC``.
3. Within the custom section, verify ``magic == "WLEX"``. If not,
   return ``-ENOEXEC`` (another tool happened to use the same custom
   section name — extremely unlikely, but explicit).
4. Verify ``abi_version == 1``. If not, return ``-ENOEXEC``.
5. Verify ``flags == 0``. If not, return ``-ENOEXEC``.
6. Verify ``entry_symbol == "_start"`` (length 6, byte equality). If
   not, return ``-ENOEXEC``.
7. Hand off to the HardwareJS-spawn-Worker pathway with the parsed
   metadata (stack size, heap pages). The kernel does NOT itself
   load the wasm bytes into memory; HardwareJS does.

Step 1's check for the wasm magic prefix at file offset 0 makes
``binfmt_wasm`` the only handler that ever fires for our binaries,
even if other binfmts are registered. There is no fallback ELF
binfmt on wasm32; ABI version 1 reserves no second-chance behaviour.

Hand-off to HardwareJS
======================

After successful detection, ``binfmt_wasm.c``:

1. Calls the upstream helpers it needs to install the new process:
   ``begin_new_exec(bprm)``, ``setup_new_exec(bprm)``,
   ``setup_arg_pages(bprm, ...)``, ``copy_string_kernel(...)`` for
   argv/envp.
2. Builds a small argv/envp blob in the kernel's shared memory.
3. Issues the host call
   ``linux.spawn_worker(pid, blob_ptr, blob_len, argv_ptr, envp_ptr,
   required_stack_size, required_heap_init, required_heap_max)``.
4. The host call does not return on success — control transfers to
   the new Worker. The calling Worker is terminate-and-respawn'd by
   HardwareJS (the PID is preserved; the same ``task_struct`` is
   reused on the kernel side).

On any failure between step 1 and step 3, ``binfmt_wasm.c`` does the
upstream cleanup (``send_sig(SIGSEGV, current, 0)``) and returns the
appropriate errno.

Generation
==========

The wasm binary is produced by ``wackywasm-tools/bin/wasmld``, which
appends the ``wasm.linux.exec`` custom section as a post-link step
(simplest: a separate ``wasm-objcopy``-equivalent run, or a tiny
``wasm-meta`` helper script shipped in ``wackywasm-tools/bin/``). The
defaults are: ``abi_version = 1``, ``flags = 0``,
``required_stack_size = 0`` (use loader default), ``cap_len = 0``.

For ABI version 1, the entry symbol is hard-coded to ``"_start"``;
``wasmld`` always emits it as such.

Backward / forward compatibility
================================

* The kernel ABI version is bumped on any layout change. Older
  kernels reject newer ``abi_version`` values explicitly with
  ``-ENOEXEC``.
* The HardwareJS loader follows the same versioning; the kernel and
  HardwareJS MUST agree on the ABI version they implement. A version
  mismatch between the kernel and HardwareJS is treated as a fatal
  configuration error at boot, not a per-exec error.

History
=======

* ABI version 1 (this document): initial format. Spec'd 2026-05-22.
