sharness.ai / the code / linux in the browser · this page as markdown

Linux in the browser: the harness under a second kernel

Status: built and measured, 25 Oct 2026. Dinis Cruz's question of 25 October 2026: can Linux run in a browser and verify this, how much of the harness runs there, and can the same Linux run in the agent's own environment, so there are two kinds of test? Yes to all three, as of 25 Oct 2026: one guest (Alpine 3.23 for 32-bit x86 with Alpine's linux-virt kernel, which ships Landlock, Python 3.12 and the sharness-ai package) boots under v86 in Node and in a browser tab, and the same binaries run on the host kernel through chroot. sharness-ai hello held 6 of 6 in all three. This is experiment E09 on the ladder; the code is experiments/browser/ in the CLI repository, and the page itself is here.

0. What was built and what it showed

FormKernelCPUWhat it is for
Node (npm run node)the guest's, Linux 6.18.58 i686emulated by v86, x86 in WebAssemblythe second kernel in any agent session or CI job, no browser, no root
browser (npm run browser, or open the page)the guest'semulated by v86, in the tabthe same, for anyone with a browser and nothing installed
chroot (sudo npm run native)the host's, through the i386 syscall tablethe real onethe guest's 32-bit binaries driving the host's Landlock: same code, no emulator

Measured 25 Oct 2026 in one Claude Code cloud session (Ubuntu 24.04, Linux 6.18.44 x86_64, uid 0; Node 22.22; Chromium 153 headless through Playwright; v86 0.5.472; guest linux-virt 6.18.58-0-virt, Python 3.12.15), each form run once, alone, <span class="asof">as of 25 Oct 2026</span>:

FormThe guest reportshello (E04)run (E03)Wall clock to hello's exit / to power-off
Nodelandlock: Up and running.; probe: Landlock ABI 7, memfd_secret yes, cgroups v26 of 6 heldunder P1 (port 443 only): 443 ENETUNREACH, allowed by the policy and failing for want of a network backend; 80 EACCES. Under P2 (read-only): write refused, read allowed. Under P0: a 200 MiB allocation refused by the 64 MiB cap58.0 s / 74.6 s
browserthe same6 of 6 heldthe same75.9 s / 95.9 s; crossOriginIsolated false, no SharedArrayBuffer, no page errors
chroothost kernel 6.18.44, Landlock ABI 7; the binaries' ELF e_machine is 0x0003, i3866 of 6 heldnot run in this formunder 2 s

The seven example policies check and hash in the guest to the same values as on the host: the second kernel agrees with the first on the one value the person supplies.

What this is and is not. The same syscalls (444 to 446 for Landlock, unshare, the rlimits, the cgroup files) on a second kernel, on an emulated CPU, applied by a process to its child: a Boundary for the child, a Setting for the page's guest. The timings are of a single emulated core and say nothing about the harness's cost on real hardware; they are here so a reader knows what the page will do. Nothing in a tab says anything about the host. And the guest has no network backend, so the port-443 probe shows the policy allowed it (the error is the network's, ENETUNREACH, not Landlock's EACCES) rather than a connection.

Three things learned building it:

The build (build_guest.py, standard library only) fetches the pinned Alpine packages, unpacks them with tarfile, adds the package and the policies, writes the initramfs with its own cpio writer and assembles the page; a rebuild from the cache takes seconds. v86 is the one dependency, pinned by version, and lives only under experiments/browser/. Not yet: a network backend (v86 can proxy over fetch or a WebSocket), so the 443 probe can connect without a policy and fail with EACCES under one; a booted-state snapshot so the page opens in seconds; a non-root user in the guest; a kernel without Landlock as the negative control.

The rest of this page is the research of the same morning that chose this build, kept as written (as of 25 Oct 2026), with what it got right and the one inference it got wrong.

1. What the guest has to offer

hello and run call Landlock through syscalls 444, 445 and 446, probe memfd_secret as 447, and use unshare for the empty network namespace. So the guest needs a real Linux kernel of 6.7 or later (TCP rules, ABI 4), ideally 6.15 or later (ABI 7, the same as the measured environment), built with CONFIG_SECURITY_LANDLOCK=y and landlock in its LSM list, user and network namespaces, cgroups, and Python 3.11 or later with ctypes.

Three kernel facts decide the rest, all from the kernel's own sources:

2. The options

OptionReal kernel?ArchKernel shippedLandlockseccomp, namespaces, cgroupsPythonLicenceCost to embed on this site
v86 + Alpine 3.23 x86 (own image)yesi386 onlyAlpine linux-virt 6.18.58yes: CONFIG_SECURITY_LANDLOCK=y, landlock first in CONFIG_LSM; ABI 7 expectedyes (USER_NS, memcg, pids, freezer)3.12.15 (an 8.3 MB apk)BSD-2 (v86), distro licences2.4 MB runtime, no special headers, 30 to 60 MB of kernel and rootfs fetched lazily over 9p; fits GitHub Pages
v86 with the copy.sh Buildroot imageyesi3866.8.12no (no Landlock string in the kernel)partlynoBSD-2same; a useful negative control
v86 + Debian bookworm i386yesi3866.1.190ABI 2 only: no TCP rulesyes3.11.2BSD-2larger image
JSLinux (TinyEMU) x86_64yesx86_646.19.3, Bellard's buildno (CONFIG_SECURITY appears off)seccomp yesnot shownTinyEMU MIT; the web build's terms unstatednot redistributable with confidence
WebVM / CheerpX 1.2.8no: syscall emulation, no kernel32-bit user modenonenono3.7 (Debian buster)engine free only for individuals and FOSS; no self-hostingCOOP/COEP headers, 600 MB streamed disk
container2wasm 0.8.4 (default)yes (Bochs or TinyEMU)x86_64, riscv646.1no (CONFIG_SECURITY unset in its config)yesany OCI imageApache-2.0 plus LGPL Bochs150 to 200 MB per visitor; needs a service worker for the headers on GitHub Pages
container2wasm with a forked kernel ≥ 6.15yesx86_64customyes (expected)yesanyas abovesame, plus a kernel rebuild
qemu-wasm (QEMU 8.2)yesx86_64, aarch64, riscv64yoursdepends on your kernelyesyoursGPL/LGPLpthreads, SharedArrayBuffer, experimental
Pyodide 314noWasmnonenono3.14.2MPL-2.0one script tag; no ctypes syscalls at all
Wasmer SDK 0.19 (WASIX)noWasmnonenono3.13.20modified MITCOOP/COEP
browser_wasi_shim, jconoWasmnonecapabilities insteadnoa CPython-wasi buildMIT/Apachetiny

The split is clean. Anything without a kernel (WebVM, Pyodide, Wasmer, WASI) cannot run Landlock, seccomp, namespaces or cgroups: there is no process in the Linux sense to confine. Anything with a kernel can, if the kernel was built with them, and of those only v86 with Alpine's kernel has Landlock on by default, runs without special response headers, and has a licence that allows self-hosting on a static site.

  1. Build the guest with v86's own tools/docker/alpine, moving the base image from i386/alpine:3.21.0 to i386/alpine:3.23 (kernel 6.18.58, Landlock ABI 7, the same as the measured workspace) and adding python3 and the sharness_ai package (no dependencies) to the rootfs. Keep the 9p initramfs. Snapshot the booted state with build-state.js so the page opens in seconds, not after a full boot.
  2. Check inside the guest: dmesg | grep landlock must say "Up and running"; sharness-ai probe must report Landlock ABI 7, user namespaces, cgroups. Add a non-root user so the network probe exercises Landlock's TCP rule (as root, hello takes the network-namespace path instead).
  3. Make the network probe honest. With no network backend in v86, a connect fails anyway. Either turn on the fetch or wisp backend and show the probe succeed without a policy and fail with EACCES under one, or print the errno so the refusal is attributable to Landlock. Run the same page against the copy.sh Buildroot kernel as a negative control: no Landlock, landlock_restrict_self returns ENOSYS.
  4. The page: libv86.mjs and v86.wasm from the v86 npm package, pinned (0.5.472 as of 25 Oct 2026, 2.4 MB, self-hosted), the BIOS files, Alpine's kernel and initramfs, the 9p filesystem index and files, and the state snapshot; an xterm.js console; sharness-ai hello run on the serial line at boot. Static files only; the large ones can sit on a CORS-enabled CDN, as copy.sh's do.
  5. Label the result on Evidence as measured in "v86 0.5.472, Alpine 3.23 x86, Linux 6.18.58, browser and date", and say what it is: the same kernel code paths, on an emulated 32-bit CPU, applied by a process to its child. A Boundary for the child, a Setting for the page's guest.

A 64-bit route for later: fork container2wasm, raise its kernel to 6.15 or later with CONFIG_SECURITY=y and CONFIG_SECURITY_LANDLOCK=y, convert python:3.12-alpine plus sharness-ai, host with a service worker for the headers. It costs 150 to 200 MB per visitor and runs slower, but it is x86_64 and keeps memfd_secret clear of the 32-bit caveat.

4. How much of the harness can run there

PartIn the v86 guestHonest label
E01 probe, E02 policy hashyesthe same code
E03 run, E04 hello: Landlock files and ports, no_new_privs, the caps, the network namespaceyes, on the guest kernelthe same syscalls, emulated CPU
separate uids per component, cgroups, the freezeryes, inside the guestreal inside the guest; says nothing about the host
mutual TLS over loopback between components (E05), the KMS protocol (E08), the process-tree proof of concept (E06)yes: all of it is processes and loopback socketsa full harness in a tab is possible
memfd_secretkernel logic runs; the physical property it provides is meaningless against a real hostdemonstrate, do not claim
timing figures (3.2 ms handshakes, 0.6 s hello)no: single emulated corenever quote browser timings as measurements
real egress, hardware keys, TPM, x86_64-only behaviour, anything about the hostnoout of scope

5. The WASI variant, for what it can show

Pyodide or a WASI build can show the policy pipeline without the kernel: hashing (the mockup does this already), validation, and a simulator of "what this policy would allow". With a WASI runtime, files.read maps to read-only preopened directories, files.write to writable ones, network: "none" to no sockets at all; exec, no_new_privs and scoping have no equivalent. It demonstrates deny-by-default at the host layer and the shape of the policy, with the JavaScript host as the enforcer: a Boundary for the guest, a Setting for the page. It is a different thing from the kernel demo and must be labelled as one.

6. Not verified when the research was written, and since

Sources (all read 25 Oct 2026): the kernel's Landlock documentation, landlock(7), memfd_secret(2), security/landlock/Kconfig, the x86 and generic syscall tables and mm/Kconfig in torvalds/linux; v86 (README, docs/linux-9p-image.md, docs/networking.md, tools/docker/alpine), the v86 npm package 0.5.472; Alpine aports main/linux-lts/virt.x86.config and main/python3/APKBUILD for 3.21 to 3.23; Debian's 6.1.190 kernel config; JSLinux and TinyEMU; CheerpX and WebVM; container2wasm and its kernel configs; qemu-wasm; Pyodide; the Wasmer JS SDK; browser_wasi_shim; GitHub Pages limits.