linux-wasm: building a Linux system that runs on the web in native WebAssembly
WebAssembly (Wasm) arch support for the Linux kernel
At a glance
- What is it?
- Scripts that download, patch and build LLVM, musl, BusyBox and a Linux kernel into a bootable WebAssembly image, plus a JavaScript host that starts it, with 64-bit kernel support on the 7.0 baseline. The obstacles are structural rather than accidental: WebAssembly has no MMU, so the kernel is built NOMMU and userland is position-independent.
- Who is it for?
- linux-wasm suits a systems programmer who wants to see a real Linux userland running in a browser tab, who can read a kernel patch series and a libc hack without flinching, and who has the patience for a toolchain that builds LLVM from source. It does not suit a project that needs a browser runtime with an MMU, shared memory between processes, or a production userland, because the kernel is NOMMU and the userland side of the 64-bit support is still experimental.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 163 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A whole Linux userland, assembled from scripts
This project contains scripts to download, build and run a Linux system that can be executed on the web, using native WebAssembly. The scripts can be run directly on a host machine, in a generic docker container, or in a specific docker container, and the README points at a Dockerfile for the last case.
The scope is bigger than a kernel port. The README enumerates the pieces the system is built from: the LLVM project, an LLVM-Wasm compatibility wrapper, the Linux kernel, musl, modified kernel headers for BusyBox, BusyBox itself, a minimal initramfs and a runtime.
That last pair deserves attention. The initramfs packages the BusyBox installation into a compressed cpio archive, sets up a pty for proper signal, session and job management, and drops you into a shell. The runtime is example code of how a minimal JavaScript Wasm host could look like, and the README is candid that its error handling is not very graceful, being geared towards debugging rather than user experience. It is the glue code that kicks everything off, spawns web workers and creates Wasm instances.
So the result of a build is a bootable system image, not a library. You end up with a shell in a browser tab, and the userland is the BusyBox binary set with symlinks for ls, cat, mv and the rest.
The repository is small at the top level, with `linux-wasm.sh` as the entry point, plus `docker/`, `patches/`, `runtime/`, `tools/` and the `LICENSE` and `README.md` files. The primary language in the metadata is JavaScript, which is the runtime.
Every layer needs a patch, and the README says how hacky each one is
The Parts section of the README is the most useful part of the document, because it lists each component, its base version, its patches and its artefacts, and it does not oversell the patches.
The kernel is based on version 7.0, with the original release based on 6.4.16. Its patches add Wasm architecture support to the kernel, add a wasm binfmt feature so that `.wasm` files can be run as executables, add a console driver for a Wasm web console, add support for catching `panic()` calls in an arch-specific way to improve the debugging experience on the host, and include various bug fixes to the base kernel unrelated to Wasm. The artefacts are `vmlinux` and the exported, unmodified kernel headers, and compiler-rt is explicitly not needed for the kernel.
musl is based on 1.2.5 with a patch described as minimal and incorrect that adds Wasm as a target and allows musl to be built with clang and wasm-ld, with the author noting in parentheses that he guessed and cheated a lot on that one.
BusyBox is based on 1.36.1 with a patch described as minimal and incomplete that allows it to be built with clang and wasm-ld and adds a Wasm defconfig.
The Linux kernel headers for BusyBox come from the kernel itself, with a patch series originally hosted by Sabotage Linux and modified to suit a newer kernel. The README explains why they are needed, since including kernel headers is not really supported by Linux, and says the series magically just works with glibc but needs modding for musl.
LLVM is based on 18.1.2 with a hack patch enabling GNU ld-style linker scripts in wasm-ld, plus fixes to make Wasm more compatible with normal ELF programs. The artefacts are clang, wasm-ld from lld and compiler-rt.
What makes the list readable is the honesty. Each component is described with its real state, including the ones that are hacks, and the reader can tell which parts are solid and which are guesses.
The wrapper that translates an ELF compiler command line
The LLVM-Wasm compatibility wrapper is the piece that makes the rest of the build tractable, and it is a small addition rather than a core part of the original work.
It is described as a thin Python wrapper that translates a compiler command line that originally targeted an ELF build into something more suitable for Wasm. The README notes that this was not part of the initial Linux 6.4-era release, but that it keeps things cleaner and more generic and is needed for the 7.0-era patches. The artefact is the fake-llvm Python wrapper, and its dependencies are clang and wasm-ld.
The reasoning is straightforward once stated. The kernel build system invokes the compiler with the flags and paths a native build would use, and the wrapper intercepts that command line and rewrites it. Without it, every cross-compilation invocation in the kernel build would need patching, and the kernel already needs a patch of its own for the architecture support.
This is the piece that makes the port a set of scripts rather than a fork of the kernel build system, and it explains why the wrapper had to grow when the base kernel moved to 7.0.
The linker's role is the other half. The LLVM patch that enables GNU ld-style linker scripts in wasm-ld exists because the kernel build depends on linker scripts, and wasm-ld did not support them natively. The README notes that the kernel needs wasm-ld with linker script support, and that for musl and BusyBox the linker script support may be needed or might be unnecessary.
No MMU means a NOMMU kernel and position-independent binaries
The hint paragraph at the end of the Parts section is the design constraint that explains most of the patch list.
MVP Wasm lacks an MMU, with an edit noting that an MMU is now available as an early proposal. That means Linux has to be built in a NOMMU configuration, and Wasm programs consequently have to be built with `-fPIC` and `-shared`.
The alternative is a proxy. Existing Wasm programs can run alongside a proxy that forwards syscalls to the kernel, and in that case each thread that wants to execute syscalls independently has to map to a thread in the proxy. The drawback is that memory cannot be mapped and shared between processes. The README observes that from a memory protection standpoint that property could also be beneficial.
So the two designs trade differently. The NOMMU route gives a real kernel with real processes, at the cost of position-independent binaries and the loss of a shared address space. The proxy route keeps unmodified Wasm programs, at the cost of losing shared memory and adding an indirection for every syscall.
This is also why MMU support is first on the future list. With an MMU proposal available, the NOMMU constraint is the thing most likely to change, and everything that follows from it, the position-independent build, the restrictions on shared memory, the shape of the runtime, becomes negotiable.
Two parallel ports, and a 7.0 rebase that took the best of both
The History section tells a story of two independent efforts that converged.
The initial release of the Wasm port was made by Joel, the repository's author. After the release he got into contact with Tom, who had also been working on an independent Wasm port, sometimes in the same timeframe. The README points at Tom's port at github.com/tombl/linux, and says that both arrived at very similar solutions, sometimes eerily similar, with some important differences in detail.
Both ports were based on early 6.x-era kernels. The author has since worked on picking the best parts out of both and rebasing on Linux 7.0, and says that this is still work in progress but that it has the basics and is now the default build target of the repository.
The important parts of Tom's port that have not been added yet are virtio and device tree support. That is a precise statement of the gap, and it is worth knowing exactly what it means: this port has no virtio devices and no device tree, so the hardware model of the machine is fixed rather than described.
What the 7.0 rebase did bring is 64-bit kernel support, with the caveat that the user space part of that is still quite experimental while the kernel part seems to work quite well. On top of that the 7.0 release sees a fair number of bug fixes, and it can now build `allnoconfig` and `allyesconfig`. All KUnit tests pass for both wasm32 and wasm64, with the note that you may see a few spurious errors for tests that completely hog the CPU in kernel mode and time out.
That KUnit result is the single most useful quality signal in the README, because it is measured against the kernel's own test suite rather than a hand-picked subset.
Running the build, and the two bugs you will hit
Running the script is simple. The README says to run `./linux-wasm.sh` to see usage, and that downloads happen first and building afterwards, and that you may partially select what to download or rebuild.
The script takes environment variables that affect its inner workings, with the source for a full list, and the README stresses that you do not need to set any of them because they default to sane defaults. The ones it documents are the three paths, and the comment on the first explains the relationship between them:
# A path (relative or absolute) to where everything ends up. The default is a directory workspace/ in the repo root. Not used directly but derives LW_SRC (downloads), LW_BUILD (build files) and LW_INSTALL (final artifacts). For maximum flexibility, LW_SRC/LW_BUILD/LW_INSTALL can be set directly instead.
# Examples: my_workspace, workspaces/my_workspace, /absolute/So one variable derives the other three, and you can set the three directly if you want to.
Two bugs are documented, and both are the kind that cost a day if you hit them cold.
The first is an LLVM build system bug. Building LLVM a second time fails when building runtimes, complaining that clang fails to build a simple test program. The workaround is to build it yet again, and the README notes that it works each other time, meaning the first, third, fifth and so on.
The second is a limitation in the Linux kernel's build system. The absolute path of the cross compiler, meaning the install path of LLVM, cannot contain spaces. Since LLVM is built by the script, this more or less means the workspace directory, or at least the install directory, has to be in a space-free path. On a system where the home directory contains a space, that means relocating the workspace before you start.
Where linux-wasm is the wrong tool
The first wrong case is a production userland. The runtime's error handling is described as geared towards debugging rather than user experience, the 64-bit user space is still quite experimental, and the patches for musl and BusyBox are described by their own author as minimal and incorrect, and minimal and incomplete. This is a research project with a working system, not a distribution.
The second wrong case is a project that needs shared memory between processes, which the NOMMU design rules out, or a browser runtime that can use the MMU proposal. The future list has MMU support first for that reason, and until it lands the address space model is fixed.
The third is anyone without patience for a from-source toolchain build. LLVM 18.1.2 is built from source by the script, with patches, and the build is long. There is no prebuilt artifact offered in the README, and the second-build failure means the process is not even smooth on the second attempt.
The fourth is a project that needs a device model. Without virtio and device tree support, as the README notes is still missing from this port, there are no pluggable devices, so the machine is a fixed target rather than something you describe.
The fifth is a team that wants Linux in the browser as a product. The runtime spawns web workers and creates Wasm instances, which is the right shape for an experiment and the wrong shape for something with a service level agreement.
What the project gives you is a real kernel with a real userland running in a browser tab, built by scripts rather than by a fork, with the kernel's own KUnit test suite passing on both wasm32 and wasm64. That is a genuinely unusual artefact, and the roadmap shows the author knows exactly which pieces would have to change to make it more than that.
Editorial conclusion
linux-wasm suits a systems programmer who wants to see a real Linux userland running in a browser tab, who can read a kernel patch series and a libc hack without flinching, and who has the patience for a toolchain that builds LLVM from source. It does not suit a project that needs a browser runtime with an MMU, shared memory between processes, or a production userland, because the kernel is NOMMU and the userland side of the 64-bit support is still experimental. Before starting a build, read `linux-wasm.sh` for the environment variables and keep the workspace in a path without spaces, since the kernel build system rejects a cross-compiler path containing one, and be aware of the LLVM build bug, where a second build of LLVM fails and the documented workaround is to build it again.
Frequently asked questions
What does linux-wasm actually build?
A Linux system for native WebAssembly: an LLVM toolchain with wasm-ld, a patched musl, a patched BusyBox, a Wasm-enabled Linux kernel, a minimal initramfs that sets up a pty and drops you into a shell, and a JavaScript host that spawns web workers and creates Wasm instances.
Why is the Linux kernel built in a NOMMU configuration?
Because MVP WebAssembly lacks an MMU, with the README noting that an MMU is now available as an early proposal. Wasm programs therefore have to be built with `-fPIC` and `-shared`, and the alternative is a syscall proxy that prevents memory from being shared between processes.
How do I choose where the build output goes?
The script takes environment variables that default to sane values. One path variable derives the download, build and install directories, named LW_SRC, LW_BUILD and LW_INSTALL, and the README notes you can set those three directly instead. The default is a `workspace/` directory in the repository root.
What is still missing from the linux-wasm port?
The README says virtio and device tree support from the other port have not been added yet. On the future list are MMU support, ELF support, driver support such as virtio and WASI, a better runtime, Yocto support and suspending execution in the kernel.
How is the linux-wasm kernel tested?
All KUnit tests pass for both wasm32 and wasm64, with the README noting a few spurious errors for tests that completely hog the CPU in kernel mode and time out. The 7.0 release can also build allnoconfig and allyesconfig.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/joelseverin-linux-wasm)Was this useful?