The preserved Linux 0.11 kernel — scheduler, 72-entry system call table,
VFS, MINIX filesystem, buffer cache, TTY line discipline, pipes, signals,
fork/exec/exit/wait — compiled to a single WebAssembly module and running
in the browser with a VT100-style terminal. No fake kernel, no simulated
Linux in JavaScript: the C sources of the original kernel are compiled
(unmodified where possible) by Emscripten into linux011.wasm.
browser (index.html + terminal.js)
│ boot / keyboard / pump / screen cells
▼
linux011.wasm ── preserved kernel (linux011/) + Web platform layer (arch/web/)
│ MINIX root filesystem image (disk.img, built by scripts/mkrootfs.py)
▼
init → /bin/sh → ls, cat, echo, ps, uname, mkdir, rm, cp, mv, hello, clear
Verified by tests/smoke.js (Node, drives the real module end to end —
ALL 11 CHECKS PASS, three consecutive deterministic runs), plus a
live-browser session (Chromium via CDP): boot, typing, fork/exec and
echo web > /tmp/w + cat /tmp/w round-trip.
| Check | Status |
|---|---|
| Kernel boots, banner renders (console con_write path) | PASS |
mount_root() on a genuine MINIX v1 ramdisk image |
PASS |
| init runs, rc shell + login shell respawn cycle works | PASS |
echo — fork → exec → tty write → exit → wait |
PASS |
ls (directory read through the preserved namei/iget) |
PASS |
cat /etc/motd (file read via the buffer cache) |
PASS |
uname -a (0.11's own placeholder strings) |
PASS |
echo data123 > /tmp/f1 — redirect, O_CREAT, read-back |
PASS |
cp (create + copy through the buffer cache) |
PASS |
rm (unlink) + read-back failure check |
PASS |
ps (walks the real task table) |
PASS |
mkdir + cd + pwd |
PASS |
Boot messages are real kernel output, e.g.:
Linux version 0.11-Web (Hybrid WebAssembly Port)
1362/1440 free blocks
40/64 free inodes
247 buffers = 252928 bytes buffer space
Free mem: 9011200 bytes
The "init respawns shells that exec /bin/#" cycle had four stacked root causes, all fixed and verified:
- wasm-ld signature-mismatch trap stubs. The karottc source tree
declares
do_exitasvolatile voidin two headers but defines it asint— harmless on i386 gcc, but wasm-ld wired every call from the void-declaring translation units to anunreachablestub. Aligned all declarations (sched.h, signal.c, panic.c's sys_sync, sys_signal arity). - Positional asyncify rewind alignment. Binaryen's asyncify lowering
restores frames positionally: the JS export that re-enters a suspended
fiber must be the function whose frame is the spill's outermost one.
Every task fiber is now born through
dynCall_vi(which IS instrumented and sits aboveweb_fiber_resume), so the cascade realigns exactly. - Fork mark moved off globals. The parent/child discriminator lived in
a global; a delayed parent resume (cooperative scheduling!) could run
after a second fork overwrote it, inverting the fork-return values and
re-running init. The mark is now a local in
web_fork_prepare's frame, which the fork-copied continuation carries for free. - create_tables argv layout. 0.11 stages
[argc][argv-ptr][envp-ptr]then the arrays;task_entrytreated the argv-ptr slot as the array, so every exec'd program read pointer bytes as its first argument.
Also fixed: MINIX ./.. directory entries (relative paths), spare inodes
for O_CREAT, inode-0 bitmap reservation, tty_read probe removal.
- The
-ENOSPCcreate bug (root cause found and fixed).patch_kernel.pyhad translated super.c's localset_bit— which is the x86btinstruction, a bit TEST that does not write memory — with test-and-SET semantics copied from fs/bitmap.c'sbtsl.mount_root's free-space counting loops scan every inode and zone bit through that macro, so the kernel poisoned its own bitmap caches at mount: the banner printed "40/64 free inodes" while the bitmap in the buffer cache had every bit set. The firstnew_inodethen found no free bit and every create failed with -ENOSPC. The translation now preserves thebtsemantics (read-only).O_CREAT,creat,mkdir,cp,rmand redirection all pass. Diagnosis trail in PORTING_NOTES.md (iteration 4 addendum). - Browser input freeze (the 0.11 ring-0 busy-wait).
tty_read's partial-line wait busy-spins at ring 0 on a PC until the next keyboard interrupt delivers a newline. A wasm call stack cannot be interrupted, so the identical code froze the browser event loop — the very thing that delivers keystrokes. In exactly the busy-wait case the task now yields to the host event loop (same semantics, event-paced). Found by CDP-pausing the wedged page (tty_read ← rw_char ← sys_read ← sh_main), fixed insleep_if_emptyvia patch_kernel.py. - index.html loads
linux011.js— the glue script defining theLinux011factory was never loaded; the boot could never start. - /etc/rc is empty — the shell has no comment syntax; the previous sample text executed as a boot-time command.
- sh reports redirect-open failures instead of exiting silently.
See PORTING_NOTES.md §6 for the full list. Highlights: custom signal
handlers fall back to the default action (v1); no demand paging (flat
memory, honest page-table bookkeeping); single interactive console;
ls -l/pipes/job control are v1-userland gaps (the 72-syscall surface
is complete); WEB_TRACE=1 debug builds may stall the boot.
The dbgopen diagnostic command on the rootfs prints raw syscall
returns for open/creat/mkdir — it is the regression probe that caught
bug #1.
| Path | Contents |
|---|---|
linux011/ |
pristine Linux 0.11 sources (vendored, never modified) |
arch/web/ |
Web platform layer (HAL): fibers, syscall entry, console, disk |
arch/web/include/ |
overlay headers shadowing x86-specific kernel headers |
scripts/ |
gen_overlays.py, patch_kernel.py, gen_syswrappers.py, mkrootfs.py |
user/ |
user programs compiled into the module (sh, ls, cat, …) |
frontend/ |
index.html, terminal.js, terminal.css (plain JS, no framework) |
tests/ |
smoke.js end-to-end test, debug-boot.js boot diagnostics |
build/web/ |
build output: linux011.wasm, linux011.js, disk.img, frontend |
./build-web.sh # needs emsdk; see BUILDING.md
cd build/web && python3 -m http.server 8000
# open http://localhost:8000 — click the terminal, type commandsFor the headless smoke test:
node tests/smoke.js