Every crossing has one owner and one failure shape
All integer fields are little-endian and all guest addresses are byte offsets, not JavaScript references. Shared structures have declared size, alignment, capacity, and version. Producers publish payload before advancing a sequence/head word; consumers validate before acting and publish completion after side effects are durable.
Validate identity
Magic, version, structure size, slot generation, and expected module provenance precede use.
Validate ranges
Offset plus length is checked without overflow against the owning memory or carve.
Publish atomically
Atomics order cross-Worker state; a wake word is notification, never the authoritative payload.
Reject stale work
Generation/witness checks prevent a delayed completion from mutating a reused slot.
Keep policy guest-side
HardwareJS transports requests and I/O. Linux owns permissions, scheduling, and POSIX semantics.
Fail attributed
Unsupported imports, devices, malformed descriptors, and bad transitions report their boundary.
The complete public seam
| Contract | Producer → consumer | Transport | Purpose | Failure rule |
|---|---|---|---|---|
boot_info v3 | HardwareJS → kernel | 96-byte record in kernel RAM | Describe RAM, CPUs, clock, cmdline, initrd, entropy | Kernel panics before generic boot on bad identity/range |
env.kernel_memory | HardwareJS ↔ all kernel instances | Shared WebAssembly memory | Physical RAM and global Linux state | Instantiation/preflight refusal on shape mismatch |
env.user_memory | Loader ↔ user module | Per-address-space shared memory | Executable data, heap, stack, DSOs | Exec/load failure on incompatible limits |
linux.raw_copy_*_user | Kernel → process Worker | Three function imports | Cross kernel/user memory boundary | Bytes-not-copied; kthread context throws |
linux.syscall | User program → HardwareJS → kernel | Function import/export pair | Enter Linux syscall table with six arguments | -ENOSYS for absent table entries |
| park/resume | Kernel continuation ↔ Worker scheduler | Return token plus slot witness | Release JS stack while a kernel path blocks | Generation/witness mismatch aborts the resume |
| spawn ring | Kernel → spawn consumer | Fixed shared ring | Fork/thread/exec host work | Malformed/stale slot rejected |
| signal ring | Kernel → signal consumer | Fixed shared ring | Route target delivery to owning Worker | Bad target/region/signal is attributed |
| virtio-mmio | Linux driver ↔ device backend | MMIO imports + shared virtqueues | Filesystem, input, and network data plane | Descriptor/range/protocol failure tied to device |
| scheduler wake | Backend → kernel Worker | Atomic word + notify | Wake idle kernel after external completion | Alignment checked; payload stays elsewhere |
| display control | UI ↔ wasmdrm | Four shared u32 words | Request and acknowledge visible mode | Request clamped to fixed carve maximum |
| PTY bridge ring | Terminal UI ↔ Linux PTY | Shared byte rings | Master input/output and winsize | Bounds/state errors report at bridge |
| serial bridge ring | Linux tty ↔ Web Serial backend | Shared byte rings | /dev/ttySER0 byte and signal transport | Permission/disconnect surfaced as events |
A fixed record at a host-selected RAM offset
HardwareJS writes exactly 96 bytes, then calls the architecture entry with its offset. The header makes extensions explicit: older sizes remain recognizable while the current kernel requires the fields needed by its configured boot path.
WASM_BOOT_INFO_MAGICEvery pointed-to byte range must fall inside env.kernel_memory. Reserved fields must carry their defined neutral
value. The command line is ASCII and capped at Linux COMMAND_LINE_SIZE - 1. Optional initrd/entropy fields are paired offset
and length values rather than host objects.
HardwareJS Linux / wasm32
----------- --------------
allocate shared RAM
write cmdline/initrd/seed
write payload fields
write identity header
call wasm_start(info_offset) ─────▶ validate magic/version/size
validate every range
initialize memblock + CPUs
start_kernel() NORMATIVE SOURCES · host layout · kernel layout
Addresses name bytes only inside their declared memory
Kernel pointers and guest physical addresses are offsets in env.kernel_memory. User pointers are offsets in the current process
Worker’s env.user_memory. A value has no meaning until the contract also
identifies which memory owns it.
| Import | Arguments | Operation | Result |
|---|---|---|---|
linux.raw_copy_to_user | user dst, kernel src, length | Copy kernel bytes into current user memory | Number of bytes not copied |
linux.raw_copy_from_user | kernel dst, user src, length | Copy current user bytes into kernel memory | Number of bytes not copied |
linux.raw_clear_user | user dst, length | Zero a current-user range | Number of bytes not cleared |
The host uses overflow-safe range checks before creating typed-array views. Partial or invalid ranges follow Linux’s residual-byte convention. Kernel-thread bindings install throwing implementations because treating kernel memory as a user address space would be silent corruption.
Single-memory invariant: the kernel module imports only env.kernel_memory. The three usercopy leaves are the only generic
kernel data path into a process memory.
Strict WebAssembly signatures normalize the ABI
linux.syscall (nr, a0, a1, a2, a3, a4, a5) → result
wasm_syscall (nr, a0, a1, a2, a3, a4, a5) → result
HardwareJS binds the user import to the process Worker’s kernel instance. The kernel validates the syscall number, selects a generated uniform wrapper, establishes the current task context, and returns a signed machine word. Linux errno values remain negative. Pointers are user-memory offsets and cannot bypass uaccess.
The process Worker—not the main page—performs this entry. It already owns the current user-memory binding and its kernel instance, so no synchronous cross-Worker RPC is needed for the ordinary trap.
NORMATIVE SOURCES · kernel dispatch · process Worker binding
A park result carries a resumable kernel frame tree
Published suspension paths are compiler-transformed. When the kernel must wait, the active continuation moves into a bounded slot and returns an internal park result to the Worker drive loop. The slot’s generation, root, arena census, and witness identify exactly what may resume.
The internal -517 result selects this protocol. HardwareJS must not
expose it as an application errno. A resume with stale generation or inconsistent
arena state is refused, because resuming the wrong frames would be memory corruption.
NORMATIVE SOURCES · coroutine semantics · toolchain specification
Fixed layouts move control work across Workers
Spawn, signal, PTY, and serial boundaries use records in kernel memory. Each ring has
a fixed header, bounded slot count or byte capacity, monotonic producer/consumer
positions, and a notify word. Ring offsets are exported by the kernel and returned in
the BootResult only when the matching contract exists.
| Ring | Payload | Producer | Consumer | Completion |
|---|---|---|---|---|
| spawn | request kind, task/image data, slot generation | Linux process path | Main host spawn consumer | Result published to request slot and Worker awakened |
| signal | target tid, signal, sender pid | Linux signal path | Host signal consumer | Owning Worker notified for kernel delivery |
| PTY | input/output bytes, requested winsize | UI and Linux tty | Linux tty and UI bridge | Head/tail advance plus notify |
| serial | RX/TX bytes, modem/control events | Linux tty and SerialPort | SerialPort and Linux tty | Progress/event state plus notify |
Shared rings transport identity and intent. Authoritative process groups, open files, pipes, signal masks, and tty state remain normal kernel structures.
Guest offsets are validated before backend access
Virtio frontends publish descriptor tables, available rings, and buffers in physical RAM. MMIO queue notification crosses to the matching TypeScript backend. The backend walks chains with cycle/count/range checks, performs the operation, writes used lengths, and exposes a pending interrupt.
Linux driver HardwareJS backend
------------ ------------------
write descriptor chain
append available-ring index
QUEUE_NOTIFY(q) ──────▶ read queue geometry
validate descriptor chain
perform device operation
write used element + index
read interrupt status ◀────── set VRING pending + wake
ack interrupt The scheduler wake word is incremented and atomically notified after externally driven work. Interrupt status and queue payload stay in their device-defined locations; the wake word alone carries no completion data.
Memory ordering: payload and used-ring writes happen before the interrupt/wake publication observed by the guest.
NORMATIVE SOURCES · host virtqueue contract · wasm32 virtio transport
Request, apply, acknowledge
The framebuffer carve has fixed base, stride, maximum width, and maximum height. Four
32-bit words exported by the kernel coordinate a visible mode change: request, applied, sequence, and acknowledgement. Width and height are packed into one word.
The host clamps requests to the carve maximum, stores request before sequence, and
wakes the kernel. The wasmdrm hotplug path commits the mode, updates applied, then acknowledges the sequence. Renderers read applied on each frame so they never blit stale geometry.
The creator owns every resource it starts
| Resource | Creator | Lifetime | Teardown |
|---|---|---|---|
| Kernel memory | Machine caller / PhysRam | Entire machine | References released after Workers/backends stop |
| Kernel Worker | bootKernel | Boot through halt/dispose | Terminate exactly once |
| Process Worker pool | Kernel spawn handler | Machine lifetime | Terminate and clear task bindings |
| Ring consumers | bootKernel | After ring offset publication | Stop timers/listeners before memory release |
| Device backends | bootKernel or caller | Device negotiated to machine dispose | Detach listeners, close optional transports |
| Permissioned device | Application/user gesture | Explicit session | Backend disconnect; browser object handled by owner |
KernelControls.dispose() is idempotent and best-effort: it clears pause
state, stops the scheduler tick and consumers, disposes the spawn handler, and
terminates Workers. Navigation and relaunch must call it so old machines cannot keep
running in invisible Workers.
Detect shape; do not guess by browser name
- Boot record
- Magic + version + size; a mismatch refuses boot before generic Linux initialization.
- WebAssembly
- Feature probes run before Worker allocation; module compile/instantiate failures remain distinct.
- Optional exports
- Ring/display offsets are absent when an older kernel does not export the matching symbol.
- Memory
- Initial/maximum/shared declarations must match every importer and compiled module.
- Toolchain
- Plugin provenance attests the exact clean
hwjs-ccsource revision. - Devices
- Negotiated features and explicit browser capability checks select behavior.
This specification describes the source snapshot published on 2026-07-23. When a contract changes incompatibly, its identity or negotiated shape must change with it; silently accepting two meanings for one layout is forbidden.
Trace prose to code
| Area | Linux / wasm32 | HardwareJS |
|---|---|---|
| Boot | bootinfo.h, head.c | boot-info-abi.ts, bootKernel.ts |
| Memory/uaccess | uaccess.h, mm/ | physRam.ts, uaccessBoundary.mjs |
| Syscalls | syscall_table.c | worker.mjs |
| Processes/signals | process.c, signal.c | kernelSpawnHandler.ts, signalRingConsumer.ts |
| Virtio | wasm32 virtio transport | virtio_mmio.ts |
| Toolchain | arch/wasm32 | hwjs-cc semantics |
SNAPSHOT · kernel 9d3a35665d22084858b63b7a39b68b5cbff51024 · HardwareJS 431a0e4476a0b80e973358a7d005e384a7a82a8d · superproject 513d7b0755479c267d42df2557497dfef0c42ef9