A port, not an emulation layer
The system compiles Linux itself to WebAssembly. Architecture-owned code lives under arch/wasm32/; generic Linux code continues to supply VFS, the
scheduler, task and signal semantics, networking, terminals, filesystems, and device
models. Browser-specific behavior enters through architecture hooks and normal Linux
drivers.
Compiler substrate: there is no Emscripten, Binaryen, Asyncify,
or JSPI in the build. A pinned LLVM fork and the HwjsCoroutinize pass
transform explicitly published suspension paths.
- Kernel boundary
- Only the wasm32 architecture layer and final-link machinery are port-specific.
- Userspace
- musl and glibc overlays provide the ABI; bash, coreutils, and BusyBox remain ordinary sources.
- Host boundary
- HardwareJS supplies hardware-facing WebAssembly imports. It does not replace Linux subsystems.
SOURCE MAP · arch/wasm32 · wasm architecture documentation
One kernel memory, separate process memories
Current vmlinux.wasm is a single-memory module and imports one shared WebAssembly.Memory as env.kernel_memory. Physical addresses
and kernel virtual addresses are offsets into that memory. Linux still maintains its
page allocator and software page tables, but WebAssembly supplies no hardware MMU.
A process Worker owns the current user address space. Three host-serviced leaf imports bridge Linux uaccess operations to that Worker's user memory. Each validates bounds and follows the Linux convention of returning bytes not copied. A kernel-thread binding has no user address space and rejects these operations loudly.
The kernel uses 4 KiB pages and a flat, single ZONE_NORMAL. Shared memory
growth and maximums are fixed as part of module instantiation; host and guest must
agree before boot.
Historical note: earlier designs linked kernel and user memory into the kernel module simultaneously. The current single-memory kernel avoids that multi-memory dependency and keeps user access behind explicit imports.
SOURCE MAP · uaccess.h · arch/wasm32/mm · memory model
HardwareJS is the bootloader
The host writes a packed, 96-byte boot_info version 3 record into kernel
memory and calls the kernel entry point. Magic, version, size, and every bounded range
are validated before the port trusts the record.
| Field group | Purpose | Kernel use |
|---|---|---|
| Identity | Magic, ABI version, structure size | Reject incompatible bootloaders |
| RAM | Physical base and byte length | Initialize the flat memory map |
| Command line | Pointer and length | Populate Linux boot parameters |
| CPU topology | Virtual CPU count and boot CPU | Bring the wasm32 SMP topology online |
| Clock | Timer frequency and epoch data | Initialize timekeeping and timer ticks |
| Initrd | Optional range | Mount the initial userspace image |
| Entropy | Seed bytes | Initialize the kernel random pool |
SOURCE MAP · bootinfo.h · head.c · setup.c
A Worker is a CPU and an address-space host
CPU 0 runs in a dedicated kernel Worker and is where kernel threads execute. A configured pool of process Workers supplies the remaining virtual CPUs. Each pool Worker owns one current user-memory binding and may host multiple resident tasks serially as HardwareJS schedules runnable continuations.
cpu=N- N process pool Workers plus the dedicated CPU 0 kernel Worker.
- Address space
- One shared user memory associated with the process Worker’s current task binding.
- Kernel image
- Each Worker has a WebAssembly instance of the kernel module against the same shared kernel memory.
- Maximum
- The kernel is configured for 129 CPUs: CPU 0 plus as many as 128 pool CPUs.
Kernel data is shared; JavaScript call stacks are not. Entry, wakeup, parking, and cross-Worker coordination therefore use explicit shared state and Atomics. The topology is cooperative rather than pretending the browser can migrate a live JavaScript stack.
SOURCE MAP · smp.c · host vCPU policy
A strict six-argument trap
User programs import linux.syscall. HardwareJS routes that trap into the
current process Worker’s kernel instance and calls the exported syscall entry with a
number plus six machine-word arguments. Linux uses the x86_64 syscall numbering
scheme, which gives libc and generated tables one stable vocabulary.
long linux.syscall(
long nr,
long a0, long a1, long a2,
long a3, long a4, long a5
); WebAssembly indirect calls require exact signatures, so generated wrappers normalize
every implementation to the same form. The kernel dispatches only entries present in
its generated table; absent or unsupported calls fail as -ENOSYS.
Pointer arguments are user offsets and must pass through Linux access checks and the
user-copy leaves.
Blocking result: a suspendable syscall may return the internal -517 park/restart token to HardwareJS. It is scheduler protocol, not
an errno exposed to a correctly integrated userspace.
SOURCE MAP · syscall_table.c · entry.S · syscall.h
Blocking paths become resumable continuations
Ordinary C control flow cannot survive a return to the browser event loop. The compiler pass transforms functions reachable from published suspension points into LLVM-coroutine state machines. A parked call retains its frames in a bounded arena; HardwareJS can later invoke its continuation on the owning Worker.
| Mechanism | Meaning | Enforcement |
|---|---|---|
HWJS_SUSPENDS | Publishes that a function may reach a suspension point | Call-graph publication tools and compiler pass |
HWJS_NATIVE | Marks a deliberately non-suspending arm | Local declaration and provenance checks |
| Park token | Transfers control from a blocking syscall to the host scheduler | Slot witness and generation validation |
| Continuation arena | Stores the transformed frame tree outside the JS stack | Capacity bounds and arena census |
| Wake word | Signals work or an interrupt across Workers | Shared Atomics state |
The default userspace behavior parks blocking waits. A kernel-command-line wasm_user_pin fallback instead keeps a task pinned to its Worker;
current glibc images require that compatibility mode because their fast syscall path
does not decode the internal park token.
SOURCE MAP · hwjs-cc · continuation semantics · toolchain specification
Fork copies a continuation; exec replaces the image
A process is more than user-memory bytes: it also has a suspended kernel continuation, Worker ownership, Linux task state, signal state, and host bindings. Fork captures that complete boundary. The child receives copied user memory and a forked continuation whose memory delta is zero; Linux supplies the expected parent and child return values.
Exec remains kernel-routed and runs in place: the same pid and Worker replace their user image, stack, and dynamic-loader records. ELF parsing, argument/environment construction, and credentials remain Linux responsibilities. HardwareJS prepares and instantiates the resulting WebAssembly program only through explicit loader contracts.
Dynamic objects are tracked by runtime loader records, allowing dlopen and symbol resolution while preserving the same address-space and syscall contracts.
SOURCE MAP · process.c · binfmt_wasm.c
Linux semantics, Worker delivery
Linux decides which signal is pending, blocked, ignored, fatal, or delivered to a handler. HardwareJS provides the cross-Worker transport. Shared signal rings and Atomics wake the Worker that owns the target task; the wasm32 architecture builds and restores the userspace signal frame.
- Generation
- Kernel code records signal state using normal Linux paths.
- Notification
- A shared ring identifies the task/Worker work that must run.
- Delivery
- The owning Worker enters its kernel instance and prepares the user handler frame.
- Return
rt_sigreturnrestores the saved wasm32 user context.
SOURCE MAP · signal.c · signal-ring.ts
One pinned compiler lineage
The kernel is compiled by a pinned clang/LLVM fork with the checked-out HwjsCoroutinize plugin. Kbuild calls clang directly; userspace uses the wasmcc and wasmld drivers. The final kernel artifact is vmlinux.wasm.
| Stage | Owner | Invariant |
|---|---|---|
| Compile | Pinned clang/LLVM | wasm32 object code with suspension annotations intact |
| Transform | HwjsCoroutinize | Only published suspension closure is transformed |
| Link | Kernel final-link scripts | Imports, exports, memory limits, and witnesses are checked |
| Attest | Plugin provenance gate | Plugin source SHA equals the clean checked-out submodule SHA |
| Boot preflight | HardwareJS | Shared memory, threads, bulk memory, and module shape must match |
The plugin is built from the submodule automatically and stamped with its exact source revision. Stale, dirty, or unattested plugins are refused rather than silently reused.
SOURCE MAP · toolchain environment · plugin build entry
Normal drivers at a browser-shaped machine edge
The reference kernel is SMP-enabled with PREEMPT_NONE and a 100 Hz tick.
It includes the generic subsystems needed by the shipped machines and wasm32 drivers
for HardwareJS device boundaries.
| Area | Enabled surface | HardwareJS boundary |
|---|---|---|
| Filesystems | tmpfs, devtmpfs, proc, sysfs, FUSE, virtio-fs | Browser-backed FUSE request server |
| Console | virtio console, PTYs, N_TTY | Shared terminal rings and browser streams |
| Display | DRM, fbdev, input, virtio-input | Shared scanout, Canvas, keyboard and pointer |
| Network | virtio-net, IPv4/IPv6 stack, Unix sockets | Packet transport, fetch shim, optional WebSocket uplink |
| USB | wasm virtual HCD, USB core, FTDI USB serial | WebUSB URB forwarding |
| Serial | wasm serial TTY | Web Serial byte transport |
| Isolation | UTS and IPC namespaces | Entirely kernel-owned |
SOURCE MAP · wasm32 defconfig · wasm32 drivers
Deliberate omissions and browser constraints
The reference configuration does not enable loadable kernel modules, swap, block devices, cgroups, audit, PID/user/network namespaces, KASAN, lockdep, or ftrace. Audio is not part of the current published device surface. These are explicit scope choices, not browser-side reimplementations.
Cross-origin isolation
Shared WebAssembly memory requires a secure, cross-origin-isolated browser context.
Permissioned devices
WebUSB and Web Serial require user action and remain subject to browser policy.
glibc compatibility
Current glibc images use wasm_user_pin for blocking syscall compatibility.
For the matching host behavior, continue to Section 02: HardwareJS. For the exact seam between them, use the boundary contract reference.