Engineering · Manifesto

Why we compiled real Linux to WebAssembly

There are at least five well-trodden ways to put 'Linux in the browser,' and every one of them makes a tradeoff we weren't willing to make. This is the long version of why we took the hard path — a real wasm32 Linux port — and why we think it's the last missing piece of a much larger shift.

May 31, 2026 · Updated June 2026 · Engineering · ← Back to the blog

One decision sits underneath everything brintOS does, and it's bigger than the slogan "Linux in your browser" makes it sound. We decided to treat WebAssembly as a real CPU architecture and to bring it up the way you'd bring up any new piece of hardware: by co-developing two halves in lockstep. One half is HardwareJS — a JavaScript environment that plays the role of the machine itself: memory, CPUs, devices, and the mechanisms a kernel expects to find underneath it. Think of it as a hardware emulator, except the thing it presents isn't a legacy instruction set — it's the substrate a modern kernel runs on. The other half is a genuine arch/wasm32 Linux kernel port, with everything that actually entails: scheduling, memory management, signal delivery, SMP. Neither half means anything without the other, and all the interesting engineering lives in the seam between them.

That single decision is the real subject of this article, because "compile Linux to WASM" quietly hides four choices about how the kernel and its "hardware" meet that are anything but obvious:

  • WASM instances as the MMU — memory isolation that is a property of the substrate, not a check the kernel has to enforce.
  • Preemption without a timer interrupt — giving the scheduler real control over CPU-bound code on a platform that has no hardware tick to interrupt it.
  • A vmlinux next to every task — a kernel instance riding on the CPU worker that runs a task, so syscalls and signals stay local and fast instead of routing through a central broker.
  • Forkable continuations of our own making — a bespoke WebAssembly toolchain that makes a suspended process copyable, so real fork() works on a platform whose native primitives refuse to duplicate a stack.

We'll walk through the five well-known alternatives and where each one runs out of road, lay out what we built instead, and unpack those four choices in detail. Further down we'll take up the question people ask most — musl or glibc — and explain why we now ship both as equal, first-class options, and we'll close with the part that is a thesis rather than a decision: why doing all of this, fast, quietly turns the desktop operating system into a commodity.

1. Why compile real Linux to WASM instead of the five common alternatives

"Run Linux in the browser" is not a new idea. It's a fifteen-year-old idea with a graveyard and a hall of fame, and the people who attempted it are some of the most capable systems engineers alive. Before explaining what we did, it's worth being honest about what already exists, what each approach gets right, and where each one runs out of road. There are essentially five families. We're a sixth — and the difference is not incremental.

The cleanest way to see the landscape is along a single axis: how is a syscall serviced? That single question — what actually happens when a program calls fork() — separates every approach in the field.

How browser Linux approaches compare: emulate CPU, reimplement kernel, port kernel to WASM. brintOS is fast and faithful.
How a syscall is serviced separates the field. brintOS sits off the usual speed–fidelity tradeoff: native WebAssembly speed with real Linux semantics.

i. JSLinux and v86 — emulate an x86 CPU

In 2011 Fabrice Bellard — the author of QEMU, FFmpeg, and a frankly unreasonable fraction of the software you use every day — published JSLinux: a 486/586-class CPU emulated in JavaScript, booting an unmodified Linux kernel inside it. It was the first time most people saw a Linux shell prompt inside a browser tab, and it remains one of the great "wait, that's possible?" demos in computing.

v86 (copy.sh, 2014 and still actively maintained) carried the idea forward with much higher x86 fidelity and a WASM-compiled CPU core, booting a remarkable range of guest operating systems — multiple Linux distributions, Windows, even hobby OSes — from a single static page.

What's right about it. The fidelity is total. Because you're running an actual x86 machine, any unmodified x86 Linux kernel boots, with a real filesystem, real ELF binaries, and real Linux semantics inside the emulated box. There is nothing to port and nothing to fake.

Where it runs out of road. CPU emulation has an unavoidable cost. Every guest instruction has to be decoded and executed by software; even with a well-engineered, JIT-style WASM core, you are paying a large multiple over native execution, and compile-heavy workloads — the exact thing a developer reaches for a Linux box to do — feel it acutely. The IO surface is constrained too: disk and network are polyfilled through virtio drivers wired back to browser APIs. It is a brilliant demonstration and a genuinely useful tool. It is not a place you would spend eight hours a day building software.

ii. container2wasm — emulate RISC-V via QEMU

Kohei Tokunaga's container2wasm (NTT Research) takes a clever packaging angle: bundle a container image, a kernel, a rootfs, and qemu-system itself, compile QEMU to WebAssembly, and run the kernel inside the emulator. The pipeline is WASM → QEMU-system (in WASM) → emulated RISC-V → a Linux kernel built for RISC-V.

What's right about it. Any container image runs unmodified. You hand it an OCI image and it boots, which is a wonderful property for "let someone try this container in a tab" use cases.

Where it runs out of road. It is, if anything, heavier than v86: QEMU is a general-purpose emulator rather than a hand-tuned browser CPU core, and emulating RISC-V buys no speed over emulating x86. Boot times are commonly measured in the tens of seconds to minutes. Again — a superb way to demo a container; not a development environment you live in.

iii. Browsix and WASIX — reimplement Linux-shaped services

A different school skips emulation entirely and reimplements the kernel's services in a host language. Browsix (a Stanford project, EuroSys '17) implemented filesystem, processes, signals, and IPC in JavaScript and ran unmodified Linux x86 binaries via Emscripten and NaCl. WASIX (Wasmer, 2023) extends WASI with fork, exec, threads, signals, and sockets, with the kernel-side semantics reimplemented by Wasmer's engineers in Rust.

What's right about it. This is fundamentally faster than emulation, because user code runs as native WebAssembly — there is no instruction-set translation. The user program is "real"; only the kernel underneath it is synthetic.

Where it runs out of road. The "kernel" is a reimplementation, not Linux. And Linux is not a specification — it is a 30-million-line behavior. Real software is written against what Linux actually does, including the corners: the exact ordering of signal delivery, the precise semantics of /proc, the way job control interacts with process groups and controlling terminals, the edge behavior of poll on a half-closed pipe. A from-scratch reimplementation can cover the common path beautifully and still be subtly, maddeningly wrong on the long tail — and there are hundreds of syscalls, plus namespaces, cgroups, FUSE, ptrace, /proc, /sys, and real job control to get exactly right. "POSIX-like in spirit" is not the same contract as "Linux," and the software that matters knows the difference.

iv. WebContainers — Node compiled to WASM

StackBlitz's WebContainers compiles Node.js to WebAssembly with extensive polyfilling, wrapped in a proprietary, closed-source runtime. For Node-ecosystem work it is genuinely excellent — fast cold starts, instant npm install, a slick in-browser dev experience. It deserves the attention it gets; it proved there is real demand for development environments that live in a tab.

Where it runs out of road. It is not Linux — it is Node with a POSIX-flavored surface. You can't run an arbitrary compiled C binary, you can't run Python or a native Rust binary, you can't run anything outside of what npm and the polyfill layer reach. That's a perfectly defensible product decision for StackBlitz's audience. It is also a hard ceiling for anyone whose work isn't Node-shaped.

v. WASI (and WALI) — the standardized system interface

WASI — the WebAssembly System Interface, Preview 1 (~2019) and Preview 2 (~2023) — is the standards-track answer, with multiple compliant runtimes and an upstream wasi-libc. It is stable, well-specified, and broadly supported, and for sandboxed compute it is exactly the right tool.

Where it runs out of road — by design. WASI deliberately omits fork, exec, signals, full job control, and the real process model. This is not an oversight; it's the point. WASI is "sandboxed WASM modules with limited filesystem and IO access," not "Linux," and its omissions are conscious tradeoffs in service of portability and capability-based security. The trouble is that essentially every real Linux application assumes fork/exec/signals exist. A shell is fork+exec. A build system is fork+exec a few thousand times. On WASI, they don't run.

WALI (CMU's WebAssembly Linux Interface, ~2023) is a thoughtful extension of the idea: expose Linux syscalls as imports the module can call. But the engine on the other side of those imports still has to implement the syscalls — which lands you back at the same place as WASIX: the semantics are only as real as someone's reimplementation.

Our approach: port Linux itself to a wasm32 architecture

We took the path that sounds the most absurd at first and turns out to be the only one that gives you both speed and truth: we treat WebAssembly as a new CPU architecture and port Linux to it, the same way Linux has been ported to dozens of real architectures over thirty years.

  • We maintain a Linux mainline fork with a new arch/wasm32 port. vmlinux.wasm is a WebAssembly module containing the actual Linux kernel: do_fork, the scheduler, the VFS, the networking stack, the signal subsystem — all real Linux code, compiled rather than reimplemented.
  • User binaries are wasm modules built against our wasm32 libc — musl or glibc, your choice. They import linux.syscall(num, a1..a6) and call it for every syscall — exactly the role the syscall instruction plays on a hardware architecture — and that call is serviced by the kernel instance living in the very same Worker, not a remote one.
  • HardwareJS — our JavaScript environment — is the "hardware" the whole system runs on: it provides the shared kernel memory, brings up a fixed pool of CPU Workers at boot, backs the devices, and supplies the SharedArrayBuffer + Atomics coordination that lets those Workers behave as one SMP machine.
  • binfmt_wasm, inside the kernel, recognizes our wasm binary format and routes execution — the same extensible mechanism Linux already uses to know how to launch an ELF.

The consequence is the important part. When user code calls fork(), the real Linux do_fork runs — with wasm-specific arch hooks we wrote in arch/wasm32 to copy the child's address space and place the new task on the scheduler's run queue, to be picked up by the CPU-worker pool. The semantics are Linux's, because the code is Linux. And because user code and kernel code are both native WebAssembly, there is no emulator translation tax to pay.

  CPU WORKER  (a fixed pool of these = the machine's "cores")
  ┌─────────────────────────────────────────────────────┐
  │  vmlinux.wasm  ── real Linux kernel, runs the         │
  │  scheduler and time-slices many tasks onto this core  │
  │     ▲  resume task A      ▲  resume task B   …         │
  │     │                     │                           │
  │  ┌──┴───────────┐      ┌──┴───────────┐                │
  │  │ task A .wasm │      │ task B .wasm │  (musl)        │
  │  │  own user    │      │  own user    │                │
  │  │  memory (SAB)│      │  memory (SAB)│                │
  │  └──────────────┘      └──────────────┘                │
  │  a syscall is an in-core call → wasm_syscall(nr, …)    │
  └─────────────────────┬─────────────────────────────────┘
                        │  shared kernel memory (SAB + Atomics)
                        ▼
  ┌─────────────────────────────────────────────────────┐
  │  HardwareJS ── the JS "hardware": memory, the CPU     │
  │  worker pool, device backends                         │
  └─────────────────────────────────────────────────────┘
  Each task has its own address space (genuine wasm isolation);
  the kernel's coroutine context switch multiplexes tasks onto a
  fixed set of CPU workers, exactly as a real OS multiplexes
  processes onto a handful of cores.

What treating WASM as an architecture actually bought us

Co-developing the "hardware" and the kernel together — rather than porting Linux onto someone else's fixed runtime — is what let three otherwise-hard things fall out cleanly, and it framed the fourth, where the platform fought back and we built our own tooling to win anyway. These are the choices hiding inside "we compiled Linux to WASM," and they're where the real work went.

  • WASM instances as the MMU (memory isolation). On real hardware the kernel programs a memory-management unit to give each process a private address space and to make one process physically unable to read another's memory. WebAssembly hands us that primitive for free, and stronger: each process runs in its own wasm instance with its own linear memory, and one instance has no way even to name another's bytes. There are no page tables to walk and nothing to flush on a context switch, and an entire category of memory-corruption and cross-process leak bugs isn't so much "prevented" as unrepresentable. Our wasm32 memory management is built around this: isolation is a property of the substrate — enforced by the same engine the browser already spent decades hardening — not a check our kernel has to get right on every access.
  • Preemption without a timer interrupt. A preemptive operating system depends on a hardware timer that can interrupt a running CPU and hand control back to the scheduler. WebAssembly has no such interrupt — a module busy in a tight loop cannot be stopped from the outside. The path of least resistance is cooperative scheduling, where programs yield politely or not at all; that is emphatically not how Linux behaves, and one developer's runaway build step should never be able to freeze the whole machine. What a context switch really needs is the ability to suspend a running wasm call stack — same program counter, same locals — and resume it later exactly where it left off. WebAssembly has no instruction for that. The early workaround in this field was a whole-program rewriter (Binaryen's Asyncify) that taxed every build with a 2–3× size penalty and composed badly with exception handling; engines later grew a one-shot stack-switching API (JSPI), but it can suspend a stack and not duplicate one — useless for fork(). We use neither. We built our own compiler primitive: an LLVM pass, HwjsCoroutinize, that turns every function on a suspend-reachable call chain into a coroutine whose live state lives in ordinary linear memory. Our arch/wasm32 switch_to drives it directly: when a task blocks, its continuation is parked and the scheduler resumes another's. The scheduler takes control at exactly the places Linux already yields — every crossing into the kernel for a syscall, every schedule(), cond_resched(), or blocking wait — and, for the case Linux can't yield politely (a pure compute loop with no syscalls), a per-task budget checked at the coroutine's suspend points lets a timer tick force a reschedule within a bounded number of iterations. It is the kernel, not the program's goodwill, that decides what runs next — exactly as on real hardware.
  • A vmlinux on every core. The obvious design is a single kernel instance that every process calls into across some message channel. We did the opposite: every CPU worker runs its own instance of vmlinux right beside the task it is currently running, so a syscall is an in-core function call into a local kernel rather than a round-trip to a distant kernel thread. Those instances all import one shared kernel memory, so they are genuinely the same kernel — one process table, one PID namespace, real SMP across the worker pool (genuine concurrent access to shared kernel memory, with the kernel's own spinlocks, RCU, and IPIs carrying their full upstream semantics) — but the syscall path stays local. Most of signal handling stays local too: the pending-signal state lives in shared kernel memory, and it is the kernel running on the target's core that inspects the disposition and runs the delivery, so no central kernel decides signal semantics on everyone's behalf. The one part that isn't fully peer-to-peer is waking a parked target — because a sleeping worker's wait address isn't visible to the kernel, HardwareJS looks the target up in its registry and issues the Atomics.notify that nudges it awake, after which the local kernel takes over. That is still a host-side notify plus local delivery, not a round-trip through a scheduled central kernel thread — which is what we mean by fast signaling.
  • Forkable continuations — our own WASM toolchain. The same coroutine substrate that lets us suspend and resume a task is also what makes fork() possible — and that is the whole point of building it ourselves rather than leaning on the engine. The engine's own stack-switching API is deliberately one-shot: you can suspend a stack and resume it once, but you cannot duplicate it, on purpose, to avoid the cost of copying a stack. Yet duplicating a suspended stack is precisely what fork() is: parent and child must both continue from the same point, one returning the child's PID and the other zero. Because our HwjsCoroutinize pass already reifies every suspendable task's continuation into its own linear memory as ordinary, copyable data — not opaque engine memory — do_fork simply copies that region into the child's fresh address space and resumes both halves independently, the one capability the platform refuses to hand us. One mechanism, paid for once: the coroutine frames that make context switching and blocking syscalls work are the very same frames that make fork() a memory copy.

Why this beats every alternative

  • Beats the emulators (i, ii) because there is no ISA translation overhead. User code and kernel code execute as native WebAssembly at the speed the browser's own engine delivers.
  • Beats the reimplementations (iii) because the semantics are Linux's own, not a best-effort approximation. There is no long tail of "almost-Linux" bugs to chase, because the behavior is produced by the same source tree that runs on servers.
  • Beats WebContainers (iv) because it's real Linux, not a Node-shaped surface. Any binary you can compile for our target runs — C, Python, Rust, Go, shells, the lot.
  • Beats WASI and WALI (v) because we expose the real Linux ABI rather than a curated subset, and the syscalls are serviced by real kernel code instead of an engine's reimplementation.

To be clear about what we are not claiming: porting an architecture is hard, ongoing work, and the emulator approaches will always boot a wider range of pre-built, foreign-architecture images than we will. That's a real and honest tradeoff. What we get in exchange is the only combination that makes a daily-driver Linux environment in a tab plausible at all: real semantics, at native wasm speed.

2. musl and glibc — both, as equal first-class citizens

The single most common question we get from people who know Linux well is about the C library: "musl or glibc?" For most of this project's life the honest answer was "musl, and here's why." Today the answer is both — brintOS maintains first-class, interchangeable wasm32 ports of musl and glibc, for the widest possible coverage of the Linux ecosystem. This section is the story of why we started with musl, why we've since added a full glibc port, and why we now maintain both as equals.

It's worth clearing up one thing first, because people ask: "musl? Isn't that the tiny libc for embedded boxes — why not the real one, glibc?" That premise is backwards, and unpacking it is the best way to explain why musl was the right first target.

A short history, because the order matters

glibc came first (the GNU C Library dates to 1988) and is the heavyweight default of the Linux world. musl came much later — Rich Felker started it in 2011 — as a deliberate, clean-slate alternative: MIT-licensed, static-link-first, strict about POSIX, and dramatically smaller. The folk wisdom that "glibc is the big featureful one and musl is the cut-down embedded one" gets the relationship exactly inverted. glibc is the heavyweight carrying decades of compatibility obligations. musl is the deliberate, modern, small alternative — and "small" here means "comprehensible," not "incomplete."

Why glibc dominates mainstream Linux — and why little of it applied at first

glibc's dominance is well-earned and entirely rational for the world it serves. When we were bringing up a brand-new architecture from nothing, though, most of those reasons didn't yet pull their weight — which is exactly why musl came first.

  • Backward binary compatibility. A binary linked against glibc 2.5 (2007) still runs on glibc 2.39 (2024). The symbol-versioning machinery (memcpy@GLIBC_2.14 and friends) exists to make that promise hold. It is essential for distributing proprietary, pre-compiled software across a decade of distros. We compile everything from source, for one target, so the promise buys us nothing.
  • LSB compatibility and distro inertia. The Linux Standard Base and the gravity of Debian/Red Hat ecosystems keep glibc in place. We don't ship to Debian; we ship a single, coherent in-browser distribution.
  • IFUNC and hand-tuned SIMD. glibc selects optimized memcpy/libm routines at load time based on the host CPU's vector extensions. Our CPU is wasm32; there are no per-microarchitecture SIMD variants to dispatch between.
  • Locale archives, NSS plugins, iconv gconv modules. These are powerful features — and they fundamentally depend on dynamic linking to load modules at runtime. We static-link.

Why musl was the right first target

  • Static linking is first-class. musl was designed for it. glibc treats static linking as second-class: NSS, locale, iconv, and dlopen simply don't work statically. A statically linked glibc is a partially broken glibc. A statically linked musl is just musl.
  • It's small enough to understand. musl is roughly 95,000 lines; glibc is about 1.55 million. When you are porting a libc to a brand-new architecture, every line is a line you may have to reason about. A 16× difference in surface area is the difference between a tractable project and an open-ended one.
  • License. musl's MIT-style license is simpler to embed and redistribute than glibc's LGPL.
  • The arch port stays small. Our entire wasm32 musl port is about 4,000 lines — roughly 1,900 in arch/wasm32/ and ~2,050 in src_overrides/ (mostly the threading and signal-handling work for our concurrency and signals milestones). glibc's wasm32 arch surface is larger, and its 1.55-million-line body is a heavier lift under any toolchain — which is exactly why musl was the tractable first port, and glibc followed once the kernel, the coroutine substrate, and real dynamic loading were all in place.
  • No pervasive symbol versioning. glibc's symbol-versioning is woven through the source and can't be cleanly switched off. We don't need it, and not having to carry it is a real simplification.
  • Lighter threading. glibc's NPTL (~30,000 lines) leans on kernel features like set_robust_list, FUTEX_PI, and rseq. Our kernel could implement those — but musl's pthread implementation (~5,000 lines) doesn't need them, which lowers the porting cost on both sides of the syscall boundary at once.
  • The road is already paved. Alpine Linux has built roughly 30,000 packages against musl and hardened them over a decade-plus of heavy production use as a container base image. Void Linux, BusyBox-based distributions, and postmarketOS all chose musl. We inherit that compatibility work rather than discovering it.

Why we added glibc too

Everything above is still true — musl is a superb default, and it stays the basis of our leanest images. But "small and static" is a virtue for some workloads and a wall for others. An enormous amount of real-world Linux software is written, knowingly or not, against glibc specifically: GNU extensions, the exact semantics of NSS and the locale/iconv machinery, symbol-versioned expectations, and dynamic loading treated as a first-class feature rather than an add-on. For that software, "just port it to musl" ranges from a ten-line shim to a real research problem. So once the hard platform pieces were in place — a kernel that does real dynamic loading, and a coroutine substrate that carries fork/setjmp/longjmp across module boundaries — a full glibc port stopped being a fool's errand and became a straightforward (if large) engineering job. We did it. glibc is now a first-class citizen on brintOS: its own real ld.so, NSS, and locale support, running on the same kernel and the same toolchain as musl.

One enabling detail is worth calling out, because it's genuinely recent. Our toolchain is LLVM-only: the coroutine transform is an LLVM pass, so clang is the only compiler we support — GCC's WebAssembly backend isn't yet able to build a kernel or a libc this way, though we're tracking it and expect a GCC path in time. glibc, historically, effectively required GCC to build. That changed only with the latest release: glibc 2.43, in January 2026, shipped experimental support for building with clang. That is the release we port from — without it, a glibc/wasm32 port under our LLVM-only toolchain would not have been practical at all. The timing is a big part of why glibc is arriving now rather than at the start.

The honest tradeoffs

Neither libc is free of edges. On musl's side:

  • musl's DNS resolver is simpler than glibc's; a few resolution edge cases behave differently.
  • The default thread stack is 80 KB on musl versus 8 MB on glibc. Deeply recursive code may need an explicit pthread_attr_setstacksize.
  • musl fully supports only the C and C.UTF-8 locales.
  • Some GNU extensions (mempcpy, strchrnul, getauxval, and the like) were historically absent. In practice these are ~10-line shims you add when a package needs them — something Alpine has done thousands of times, and a well-worn path rather than a research problem.

And on glibc's side:

  • It's far larger. Binaries are bigger, images are heavier, and the arch port and its build are a bigger surface to maintain — the price of the compatibility it buys.
  • Its clang support is still experimental upstream (new in 2.43), so we're tracking a moving target and carry a handful of toolchain workarounds musl doesn't need.
  • Full static linking remains a poor fit for glibc — NSS, locale, and iconv want the dynamic loader. On brintOS that's fine, because our ld.so path is real, but a "tiny static glibc binary" isn't the thing to reach for. That's musl's job.

Conclusion

So we maintain both, and we don't play favorites. brintOS ships first-class wasm32 ports of musl and glibc for the widest possible coverage of the Linux ecosystem — musl for the software and lean images built around it, glibc for the enormous body of software written against it. We built musl first and glibc second, and only once LLVM/clang could compile glibc from source — but that's an accident of order, not a statement of preference. The same kernel, the same coroutine substrate, and the same clang toolchain sit underneath both, and both are fully and equally supported. You bring the workload; whichever libc it expects is already a first-class citizen here.

3. Why fast Linux-on-WASM is the last step to making the OS a commodity

The first two sections are engineering arguments. This one is a claim about where computing is going — and we'll try to make it the way a careful analyst would, not the way a manifesto usually does. The thesis is simple to state and easy to overstate, so we'll do both the stating and the calibrating.

What the OS was actually for

For decades the desktop operating system was indispensable because it was the thing that provided the primitives everything else needed: a filesystem, processes, networking, hardware access, identity, security, and a way to distribute applications. If you wanted any of those, you went through the OS. That was the whole bargain.

The browser quietly acquired all of them

Over the last fifteen years the browser absorbed those responsibilities one at a time, until the list is essentially complete:

  • Networkingfetch, WebSockets, WebRTC, WebTransport.
  • Hardware — WebGPU, WebUSB, WebHID, WebSerial, Web Bluetooth.
  • Filesystem — the Origin Private File System (OPFS) and IndexedDB for real, persistent local storage.
  • Identity — passkeys and WebAuthn.
  • Security — the same-origin policy, COOP/COEP isolation, content security policy.
  • Distribution — URLs and PWAs: the most frictionless install mechanism ever shipped.

And so, for most general-purpose work, web apps already replaced desktop apps: email, documents, communication, video, social, design, project management. For most people, most of the day, the "operating system" they use is the browser. The native OS underneath is plumbing they don't think about.

The one frontier that held out

One category stubbornly resisted: real Linux development environments and Linux-native tooling. Developers, sysadmins, data scientists, and infrastructure engineers still needed a real desktop OS — not for email, but for the things only a real Linux box could do:

  • Compilers — gcc, clang, rustc.
  • Package managers — apt, dnf, pacman, nix.
  • Shells with real job control — bash, zsh, fish.
  • Process inspection — htop, strace, lsof, ps.
  • Real filesystems — ext4, btrfs, overlay, FUSE.
  • Real kernel features — cgroups, namespaces, eBPF, /proc, /sys.

StackBlitz and WebContainers proved the demand for development environments in the browser is large and real. But Node-shaped is too narrow to fully retire the Linux box on a developer's desk. The frontier stayed open because the thing it required — a real Linux kernel, running fast, inside the browser substrate — didn't exist yet.

What changes when that frontier closes

When real Linux runs at native wasm speed in a browser tab, the last load-bearing reason to maintain a particular non-browser desktop OS goes away. The desktop OS doesn't vanish — someone still runs the browser — but it reduces to a commodity: it boots, it runs a browser, and the browser runs everything else. The choice of OS becomes a question about hardware and a chassis, increasingly orthogonal to the actual work. "Which laptop's browser do I want to sit in front of?" is a very different question from "which OS can run my toolchain?"

We call this the last step deliberately, because every other piece already fell into place:

  • Web standards matured into a genuine application platform.
  • WebAssembly matured into a fast, portable compile target.
  • SharedArrayBuffer + Workers + Atomics gave us real shared-memory concurrency — the substrate an SMP kernel needs.
  • WebAssembly's exception-handling and multi-memory proposals matured enough that we could build a compiler-native coroutine substrate on top of them — the context-switch and fork() primitive a preemptive kernel had been missing.
  • OPFS gave us real persistent storage.
  • WebGPU gave us real local compute.

The one missing piece was a real Linux kernel running at native speed inside that substrate. Linux-in-the-Browser is that piece.

What this unlocks

Here is the part we're genuinely excited about. When a real Linux box is one URL away, the friction that has defined computing for forty years simply evaporates. No install. No "works on my machine." No provisioning a VM, no waiting on IT, no thirty-step onboarding doc that's already out of date. You send a link, and the person on the other end is inside the exact environment you built — same kernel, same toolchain, same packages, same devices — in the time it takes a tab to open. A whole Linux workstation becomes as shareable as a Google Doc and as disposable as a browser tab, because that is precisely what it now is.

That changes what a computer is for most people. Any device with a modern browser — a school Chromebook, a locked-down work laptop, a tablet, a machine you've never touched before — becomes a full Linux development environment on demand, with your CPU and your RAM doing the work and nothing left behind when you close the tab. The OS you happen to be sitting in front of stops being a gatekeeper that decides what you're allowed to run, and becomes what it should have been all along: a fast, secure chassis for the browser, where the real work happens. The center of gravity moves to the tab — and the tab goes everywhere.

The line that captures it: the environment finally follows the developer, instead of the developer forever chasing the environment. That's the future we're building toward — and the kernel work in the first half of this post is exactly what turns it from a pitch deck into something you can open right now, in this tab.

The competitive moment

Most attempts at browser Linux have been one of two things: dazzling technical curiosities (JSLinux, v86) or excellent but narrow products (StackBlitz). What hasn't existed is a real Linux distribution that runs in a browser tab with full syscall fidelity, native performance, and the day-to-day experience of a native Linux box. That's the gap. Closing it is the entire point of brintOS — and it's why we were willing to do the genuinely hard thing and port the kernel itself.

If you want the architecture in more detail, read about Linux in the Browser, the HardwareJS hardware platform, and the brintOS toolchain. Or just create an account and boot a machine in your tab. That, in the end, is the argument.