public

Linux cells: a real process in a browser tab

read-only

AI authored to showcase ironpad capabilities.

Introduction

Every other cell in ironpad compiles to wasm32-unknown-unknown and runs in a Web Worker, where there is no filesystem, no process table, and no thread the operating system knows about. A Linux cell compiles to wasm32-browserpod-linux-musl and runs as a real Linux process, serviced by BrowserPod's kernel inside this tab.

The two import tables, counted from the compiled modules:

cell kindimportsserviced by
regularabout a dozen host functionsironpad's executor
Linux431 syscalls, plus 4 WASIan in-browser kernel

clone3, execve, futex, pipe2 and wait4 are all in that table, so std::fs, std::thread and std::process::Command behave the way they behave on a server. Threads become Web Workers over one shared WebAssembly.Memory, and Command::spawn is a real fork and exec.

Nothing here runs until you click it. Starting a machine is a deliberate act, so Linux cells never autorun and never join Run All on page load. Work down the notebook in order: every cell below is a separate process on one shared machine, and the later ones read files the earlier ones write.

A real processlinuxBrowserPod

One pod, one filesystem

Regular ironpad cells pipe typed Rust values downstream through the injected cell0, cell1 and last bindings. Linux cells have none of that, and do not need it. One pod serves the whole notebook, so these cells are separate processes on the same machine, and a file written by one is a file read by the next. That is the Unix answer to the same problem.

The first click pays for the machine, roughly 1.5 to 2 seconds of it, and nothing after that does. The runtime pulls five assets from its CDN inside the first 337ms and never contacts that origin again, so for the rest of the session the pod is simply there.

Write a datasetlinuxBrowserPod
Read it from another pidlinuxBrowserPod

Real subprocesses

fork, vfork, execve, wait4 and pipe2 are all imported, so std::process::Command is not an emulation. The kernel spawns a Web Worker for the child, and the parent gets a real pid back and waits on a real exit status.

The pod ships 352 executables on PATH, mostly BusyBox applets but also git, curl, node, npm and python3. So a Rust program here can do what a shell script does: hand one child's stdout to the next child's stdin and let the kernel move the bytes.

A three-process pipelinelinuxBrowserPod

Threads are Workers

std::thread::spawn lowers to clone3 with futex parking, and the kernel answers by starting a Web Worker that shares the same WebAssembly.Memory. The target spec is what allows it: --shared-memory, --import-memory, +atomics so memory.atomic.wait and memory.atomic.notify exist, and --export=__startThread as the entry point the kernel calls inside each new Worker. This is the same primitive ironpad uses for rayon cells, taken further, which is why Linux cells inherit its cross-origin-isolation requirement.

One habit does not carry over. available_parallelism() returns 1 in a pod, and so does nproc, so the usual way of sizing a pool hands you a pool of one. The count below is hard-coded because of it.

The threads themselves are real, and so is the throughput: on the machine this was written on, counting the primes below 6,000,000 took 1,798 ms on one thread and 209 ms across 16, an 8.6x speedup. Your numbers will differ, which is the point of running it.

Fan out across WorkerslinuxBrowserPod

What it will not do

  • Nothing autoruns. Starting a pod is starting a machine, so Linux cells sit out public autorun and Run All on page load, and the agent protocol refuses cells run against one: a boot with no human watching is a boot nobody asked for.
  • Process slots are finite. Each child is a Web Worker. A probe that ran fourteen Command::output() calls back to back had the fourteenth fail with EAGAIN, and std panics there rather than returning the error to you.
  • Pods are ephemeral. There is no storageKey, so reloading this page hands you a fresh machine with an empty filesystem. A notebook whose behaviour depended on invisible history would read differently for its author than for you.
  • No saved output. Every other public notebook renders its author's last run on arrival. Capturing one of these would mean booting a pod during a build, so these cells start blank and stay blank until you click.
  • Embeds refuse them. Threads need SharedArrayBuffer, which needs cross-origin isolation, which an iframe on another origin does not have. The embed says so rather than offering a Run button that would do nothing.
  • The runtime arrives from a CDN. A notebook with no Linux cell never contacts it at all, and when it is unreachable the cell reports that instead of hanging.