Kandelo is a POSIX-compatible multi-process kernel for WebAssembly that runs command-line tools, server stacks, and early graphical demos in the browser or Node.js with syscall-level compatibility.
Live demo: Open Kandelo in the browser
User guide: Read the browser and VFS guide
ATTENTION: This repo may contain .wasm binary builds in its history. In the future, history will likely be rewritten to remove these as they are offloaded to a better data store.
Real, unmodified software compiled to WebAssembly:
| Software | Version | Notes |
|---|---|---|
| nginx | 1.27 | Static serving, reverse proxy, FastCGI, multi-worker fork |
| PHP | 8.4 | CLI + PHP-FPM, FastCGI protocol |
| MariaDB | 10.5 | SQL database, Aria storage engine, 5 threads |
| Redis | 7.2 | In-memory store, 3 background threads |
| WordPress | 6.7 | Full CMS: nginx + PHP-FPM + SQLite or MariaDB |
| CPython | 3.13 | REPL, script execution, stdlib |
| Git | 2.47 | Core version control operations |
| Vim | 9.1 | Full editor with ncurses terminal UI |
| NetHack | 3.6.7 | Classic roguelike with curses UI |
| fbDOOM | (maximevince) | id Software's DOOM via the kernel's /dev/fb0 Linux fbdev surface |
| espeak-ng | 1.52 | Speech synthesis; plays through upstream pcaudiolib's OSS backend on /dev/dsp |
| Perl | 5.40 | Interpreter with core modules |
| Ruby | 3.3 | Interpreter with core stdlib |
| SpiderMonkey | 140 ESR | JavaScript engine backing the Node.js-compatible runtime with Intl, SharedArrayBuffer, worker_threads, and npm package installs. |
| GNU nano | 8.3 | Terminal text editor |
| dash | 0.5.12 | POSIX shell with pipes, redirects, job control |
| GNU coreutils | 9.6 | 50+ utilities (ls, cat, sort, wc, etc.) |
| GNU grep | 3.11 | Regular expression search |
| GNU sed | 4.9 | Stream editor |
| GNU make | 4.4 | Build automation |
| curl | 8.11 | HTTP client with TLS |
| GNU Netcat | 0.7.1 | TCP/UDP client and server utility |
| wget | 1.25 | HTTP file retrieval |
| gawk | 5.3 | Pattern scanning and processing |
| GNU findutils | 4.10 | find, xargs |
| GNU diffutils | 3.10 | diff, cmp |
| tar | 1.35 | Archive utility |
| gzip, bzip2, xz, zstd | — | Compression utilities |
| less | 668 | Terminal pager |
| bc | 1.07 | Calculator |
| m4 | 1.4.19 | Macro processor |
| file | 5.46 | File type identification |
All run in both Node.js and the browser with no source modifications.
A single shared kernel serves all processes via channel IPC (SharedArrayBuffer + Atomics):
┌─────────────────────────────────────────┐
│ User Programs (C → Wasm) │
│ Each in its own Web Worker │
│ Linked against musl libc + glue │
├─────────────────────────────────────────┤
│ Kernel (Rust → Wasm) │
│ One instance, all processes │
│ Syscalls, fd table, pipes, signals, │
│ sockets, PTY, memory management │
├─────────────────────────────────────────┤
│ Host Runtime (TypeScript) │
│ Node.js: fs, net, crypto │
│ Browser: SharedArrayBuffer FS, fetch │
└─────────────────────────────────────────┘
- Kernel (Rust → Wasm) — 170+ syscall implementations. One instance manages all processes via a
ProcessTable. - Host (TypeScript) — Loads kernel and user Wasm binaries, provides host I/O, bridges blocking syscalls to async APIs via
Atomics.waitAsync. - Glue (C) — Syscall dispatcher compiled into every user program. Translates musl's
__syscallABI into channel writes that the kernel reads.
See docs/architecture.md for the full architecture reference.
170+ POSIX syscalls across these subsystems:
| Subsystem | Highlights |
|---|---|
| File I/O | open, close, read, write, seek, dup/dup2/dup3, pipe, readv/writev, pread/pwrite, sendfile, ftruncate, fsync, copy_file_range, splice, statx |
| fcntl | Advisory locking (F_GETLK/F_SETLK/F_SETLKW), file flags, FD_CLOEXEC, cross-process locks |
| Process | fork (wasm-fork-instrument), exec, posix_spawn, exit, getpid/getppid, process groups, sessions, waitpid |
| Threads | clone with CLONE_VM|CLONE_THREAD, per-thread TLS and channels |
| Signals | kill, sigaction (SA_SIGINFO), sigprocmask, sigsuspend, sigaltstack, alarm, setitimer/getitimer, RT signals, sigqueue, sigtimedwait, signalfd |
| Memory | mmap (MAP_ANONYMOUS + MAP_PRIVATE file + MAP_SHARED file), munmap, mremap, brk/sbrk, memfd_create |
| Networking | AF_INET UDP/TCP sockets, local virtual networking between Kandelo machines, AF_UNIX sockets, bind/listen/accept/connect, send/recv, sendto/recvfrom, sendmsg/recvmsg, SCM_RIGHTS |
| Directories | opendir/readdir, mkdir, rmdir, rename, symlink, readlink, chmod, chown, statvfs, all *at() variants |
| Time | clock_gettime, gettimeofday, nanosleep, utimensat, timer_create/settime/gettime/delete |
| Terminal | Full PTY support (/dev/ptmx + /dev/pts/N), line discipline, canonical/raw mode, 16 terminal ioctls |
| Virtual devices | /dev/null, /dev/zero, /dev/urandom, /dev/full, /dev/fd/N, /dev/tty, /dev/ptmx, /dev/pts/* |
| Procfs | /proc/self, /proc/<pid>/stat, status, cmdline, environ, maps, fd/*, /proc/net/tcp, unix |
| IPC | SysV msg queues, semaphores, shared memory; POSIX mqueues |
| Event/Notification | eventfd, timerfd, signalfd |
| Poll/Select | poll, ppoll, pselect6, epoll (host-intercepted in browser) |
See docs/posix-status.md for the full syscall-by-syscall status.
- Rust nightly (for
build-stdand atomics) — pinned viarust-toolchain.toml - LLVM 21+ with
clangandwasm-ld(macOS:brew install llvm) - Node.js 24+
Or use the Nix flake (see Using Nix below) and skip per-tool installs.
A flake.nix provides a reproducible dev shell with the pinned Rust nightly,
Nix's LLVM 21 package set, Node 24, minimal Erlang 28, and the
autotools/cmake/binaryen/wabt stack the build scripts need. With
Nix installed (flakes enabled —
Determinate Systems Nix has them on by default):
scripts/dev-shell.sh bash # interactive pure shell
# or
scripts/dev-shell.sh ./run.sh setup # one-shot hermetic buildThe shellHook exports LLVM_BIN / LLVM_PREFIX / LLVM_VERSION so the
build scripts pick up the Nix-provided LLVM toolchain. It also exports the
LLVM source paths used to rebuild the repo's libcxx package reproducibly.
The first shell entry downloads the toolchain (~10–15 min); subsequent entries
are near-instant.
./run.sh setup is the front door — run it first and most of the time
that is all you need:
| Command | What it does |
|---|---|
./run.sh setup |
One command to get a working repo: provisions the musl sysroot/SDK and builds the fork-instrument tool, every package, the rootfs image, and the TypeScript host. Run this first, and again after pulling upstream changes. |
./run.sh build <target> |
Build one thing: the kernel, the host, the rootfs, a package (e.g. php), or a VFS product (e.g. shell-vfs). |
./run.sh rebuild <target> |
Force a rebuild of one target, bypassing the cache. |
./run.sh clean <target> |
Remove a target's build outputs, plus any products that embed it. |
./run.sh browser |
Build (fast, cached after setup) and serve the browser demos. |
./run.sh local-build [--json] |
The packages-only primitive underneath setup and browser: builds just the package/VFS-product graph, optionally emitting machine-readable JSON. Reach for it directly for a focused package rebuild, scripted/CI output, or VFS-product iteration. |
./run.sh list shows the full set of build targets, examples, and
test suites. Under the hood, these subcommands delegate to a typed
cargo xtask engine (bootstrap, verify-fresh, clean, and more);
see docs/agent-guidance/packages-and-builds.md
if you need to work at that layer directly.
For consumers that do not need to rebuild Kandelo itself, the npm packages are the easiest entry point:
npm install wasm-posix-host wasm-posix-sdkwasm-posix-host ships the compiled host runtime JS, worker entry
points, kernel.wasm, and rootfs.vfs.zst. wasm-posix-sdk ships the
compiler wrappers, musl sysroot, and host glue files used when linking
your own C/C++ programs. You still need LLVM 21+ on PATH (or
WASM_POSIX_LLVM_DIR) because the SDK wraps clang rather than
bundling a native compiler.
From a source checkout, npm run pack:packages builds npm tarballs
after ./run.sh setup has produced the sysroot, kernel, and rootfs
artifacts.
git submodule update --init libc/musl
# Hermetic build: musl sysroot(s) and SDK, fork-instrument tool,
# local-build engine (all packages), rootfs image, then the
# TypeScript host
./run.sh setup./run.sh setup is the single entry point for a working repo: it
provisions the musl sysroot (building it from scratch on a fresh
checkout, or just re-syncing overlay headers if it already exists),
builds the fork-instrument host tool, resolves and builds every
package in the local-build graph, produces the rootfs VFS image, and
builds the TypeScript host. See Which command do I
run? above for the rest of the command
surface.
Library dependencies (zlib, openssl,
sqlite, libcxx, etc.) and ported programs (vim, git, php, etc.) are resolved
on demand by cargo xtask build-deps resolve <name>, which prefers
the per-user cache, then falls back to the published binary release at
binaries-abi-v<ABI_VERSION>,
then to a source build via the per-library build-<name>.sh. See
docs/package-management.md for the
full schema, resolution order, and release-archive contract.
If you prefer to skip cargo-driven dep resolution and pull every
pre-built artifact at once, run bash scripts/fetch-binaries.sh after
./run.sh setup. It walks every packages/registry/<pkg>/package.toml
with a [binary.<arch>] block and resolves the archives into the
content-addressed cache plus binaries/programs/<arch>/ symlinks.
To source-build the current seven browser VFS products and their declared dependency graph, use the local DAG builder from the repository root:
./run.sh local-buildThe command uses an active direnv/Nix development shell when one is already
loaded; otherwise it enters the repository dev shell for you. It selects
package and product inputs from their checked-in manifests, builds with up to
16 concurrent jobs, reuses completed content-addressed nodes, and writes the
validated SourceOnly projection to local-binaries/source-only-v1.
The default output ends with a concise node, cache, build, and product
summary. Use ./run.sh local-build --json when automation needs the canonical
machine-readable result.
On PR branches, CI publishes temporary binaries to
pr-<PR_NUMBER>-staging. Those archives remain available for release
inspection through ./run.sh --pr-staging fetch. The browser command does
not consume them: it always builds or reuses the local SourceOnly graph.
If you are editing a package's package.toml to iterate locally, the
resolver detects the cache-key mismatch, logs a warning, and falls
through to a source build via the package's build-<name>.sh —
no flag needed. See Iterating on a package
locally.
cd sdk && npm linkThis installs 8 CLI tools that wrap LLVM for the wasm32-posix target:
| Tool | Purpose |
|---|---|
wasm32posix-cc |
C compiler |
wasm32posix-c++ |
C++ compiler |
wasm32posix-ar |
Static archive tool |
wasm32posix-ranlib |
Archive index generator |
wasm32posix-nm |
Symbol lister |
wasm32posix-strip |
Symbol stripper (no-op) |
wasm32posix-pkg-config |
pkg-config with sysroot awareness |
wasm32posix-configure |
Autoconf configure wrapper |
See docs/sdk-guide.md for detailed SDK usage.
wasm32posix-cc examples/hello.c -o hello.wasm
npx tsx examples/run-example.ts hello# Build/reuse the local SourceOnly products, then start the dev server
./run.sh browser./run.sh browser runs the same cached DAG as ./run.sh local-build, points
Vite at the validated local-binaries/source-only-v1 projection, and never
fetches a prebuilt browser binary selection. Subsequent launches reuse
unchanged nodes.
Open http://127.0.0.1:5401 to use the Kandelo UI. The network lab at http://127.0.0.1:5401/pages/network/ boots multiple local Kandelo machines in one browser session and exercises POSIX UDP/TCP with GNU Netcat (nc) and curl.
To build the deployable static site, run:
./run.sh build-browser # site root, e.g. https://kandelo.dev/
./run.sh build-browser --base /kandelo/ --out build/kandelo # under a prefixIt runs the local build, produces the authenticated VFS asset group, runs the
production Vite build, and checks the result. Upload the contents of the
output directory (default apps/browser-demos/dist/) so it is served at
exactly the chosen base.
Production output is bound to the absolute prefix selected at build time. The
equivalent manual steps, here building separate distributions for /a/ and
/candidate-b/, are:
scripts/dev-shell.sh bash -lc '
set -euo pipefail
export WASM_POSIX_RESOLUTION_POLICY=source-only-v1
export WASM_POSIX_SOURCE_ONLY_BINARY_ROOT="$PWD/local-binaries/source-only-v1"
npx tsx scripts/build-local-vfs-asset-group.ts \
"$PWD/local-binaries/vfs-group" \
"$PWD/local-binaries/pages-vfs-products.private.json"
export KANDELO_PAGES_PRODUCT_MAP="$PWD/local-binaries/pages-vfs-products.private.json"
export KANDELO_PAGES_VFS_ASSET_GROUP_DIR="$PWD/local-binaries/vfs-group"
VITE_BASE=/a/ npm --prefix apps/browser-demos run build -- \
--outDir "$PWD/build/a" --emptyOutDir
VITE_BASE=/candidate-b/ npm --prefix apps/browser-demos run build -- \
--outDir "$PWD/build/candidate-b" --emptyOutDir
'Host those output directories at exactly /a/ and /candidate-b/; do not
move a completed build to a different prefix. See
Directory-scoped production hosting
for worker ownership, VFS relocation, and troubleshooting.
The browser app routes cross-origin fetches through the service worker and
defaults to the main WordPress Playground CORS proxy:
https://wordpress-playground-cors-proxy.net/?. To test an alternate proxy in
dev, preview, or production builds, set VITE_CORS_PROXY_URL before starting or
building the browser app:
cd apps/browser-demos
VITE_CORS_PROXY_URL='https://your-proxy.example/?' npm run devProxy prefixes ending in a bare ? receive the raw target URL. Other prefix
forms, such as https://your-proxy.example/cors?url=, receive a
percent-encoded target URL. If you change the proxy while a service worker is
already active, reload the page; clearing site data may be needed if the browser
keeps an older service worker around.
The application owns one complete proxy profile. The current profile relays
only Accept, Content-Type, git-protocol, wp_blog, and wp_install, by
case-insensitive field name, at every configured proxy dispatch. Unsupported
fields may be omitted with a diagnostic only for anonymous bodyless GETs;
lossy credentialed, body-bearing, or non-GET requests fail before dispatch.
This is a browser transport boundary, not full HTTP-header fidelity. Direct
browser requests and Node.js-host networking are not filtered by this profile.
Browser Kandelo supports local loopback and virtual machine-to-machine UDP/TCP. External raw TCP/UDP sockets are still constrained by the browser sandbox and require fetch, service-worker, proxy, or future WebRTC-backed transports behind the POSIX socket layer.
The browser UI uses pre-built VFS images - binary filesystem snapshots that load instantly at runtime. See docs/browser-support.md for details.
Build scripts for all ported software are in packages/registry/:
bash packages/registry/dash/build-dash.sh # dash shell
bash packages/registry/coreutils/build-coreutils.sh # GNU coreutils
bash packages/registry/php/build-php.sh # PHP 8.4
bash packages/registry/redis/build-redis.sh # Redis 7.2
bash packages/registry/mariadb/build-mariadb.sh # MariaDB 10.5
bash packages/registry/cpython/build-cpython.sh # CPython 3.13
bash packages/registry/git/build-git.sh # Git 2.47
bash packages/registry/vim/build-vim.sh # Vim 9.1
bash packages/registry/perl/build-perl.sh # Perl 5.40
bash packages/registry/ruby/build-ruby.sh # Ruby 3.3
bash packages/registry/spidermonkey/build-spidermonkey.sh # SpiderMonkey JS + Node.js compat
bash packages/registry/nano/build-nano.sh # GNU nano 8.3
bash packages/registry/curl/build-curl.sh # curl
bash packages/registry/netcat/build-netcat.sh # GNU Netcat 0.7.1
bash packages/registry/make/build-make.sh # GNU make
bash packages/registry/espeak-ng/build-espeak-ng.sh # espeak-ng 1.52See docs/porting-guide.md for how to port your own software.
# Kernel unit tests (700 tests)
cargo test -p kandelo --target aarch64-apple-darwin --lib
# Host, package, and browser-adjacent integration tests
cd host && npx vitest run
# musl libc-test suite (0 unexpected failures)
scripts/run-libc-tests.sh
# Open POSIX test suite (0 failures)
scripts/run-posix-tests.sh
# Sortix test suite (4817+ pass, 0 failures)
scripts/run-sortix-tests.sh --allcrates/
shared/ Shared types (Errno, syscall numbers, flags, channel layout)
kernel/ Kernel implementation (syscalls, fd table, signals, pipes, sockets, PTY)
host/
src/ TypeScript host runtime shared by Node.js and browser hosts
node-kernel-host.ts / node-kernel-worker-entry.ts
Node.js host main-thread proxy and kernel worker entry
browser-kernel-host.ts / browser-kernel-worker-entry.ts
Browser host main-thread proxy and kernel worker entry
worker-main.ts Shared process-worker runtime used by both hosts
vfs/ networking/ framebuffer/
Shared host services used by Node.js, browser, and tests
test/ Host/kernel runtime behavior tests
wasm/ Compiled Wasm binaries
sdk/
src/bin/ CLI tool wrappers for LLVM cross-compilation
src/lib/ Toolchain discovery, compiler flags, arg parsing
apps/
browser-demos/ Vite Kandelo UI app that consumes the browser host runtime
web-libs/
kandelo-session/ Reusable Kandelo session/UI integration contracts
packages/
registry/ Kandelo package manifests and build scripts
<name>/test/ Package-owned tests and fixtures
sets/ Named package sets for CI and product scenarios
tests/
package-system/ Package registry and binary-fetching automation tests
libc/ musl libc-test suite and overlays
posix/ Open POSIX test suite
sortix/ Sortix os-test suite and overlays
images/
rootfs/ Source tree for the base VFS image
vfs/scripts/ VFS image and archive builders
tools/
mkrootfs/ Root filesystem image builder
xtask/ Rust package/release automation CLI
libc/glue/
channel_syscall.c Channel-based syscall dispatcher (compiled into every user program)
compiler_rt.c Soft-float and 64-bit compiler runtime builtins
libc/musl/ musl libc (git submodule)
libc/musl-overlay/ Wasm32-specific architecture patches for musl
tests/
libc/ musl libc-test submodule, overlays, and build output
posix/ Open POSIX test suite
sortix/ Sortix os-test submodule and build output
scripts/ Build scripts and test runners
examples/
*.c / *.wasm Simple C example programs
docs/
architecture.md Architecture reference
sdk-guide.md SDK usage guide
porting-guide.md Guide to porting software and creating demos
browser-support.md Browser capabilities and limitations
posix-status.md Full syscall-by-syscall status tracker
wasm-limitations.md Fundamental WebAssembly platform limitations
| Document | Description |
|---|---|
| User Guide | Browser UI usage, custom browser apps, VFS images, publishing, and API stability notes |
| Architecture | Kernel design, syscall flow, multi-process model, memory layout |
| Repository Organization | Top-level ownership boundaries and CI-oriented path categories |
| SDK Guide | Compiling programs, toolchain setup, autoconf/CMake integration |
| Porting Guide | How to port software and create package builds |
| Browser Support | Browser architecture, capabilities, and limitations |
| Shareable Computer URLs | Boot descriptor design for sharing computer topology, signed bases/packages, mounts, and overlays |
| Package Management | packages/registry/<name>/package.toml schema, resolver, release archives |
| Package Sources | Reusable workflows and scripts for third-party Kandelo package repositories |
| Package Management — Future Work | Deferred items: WASI caching, semver, multi-arch [binary], etc. |
| Binary Releases | index.toml ledger, package-system .tar.zst archive layout, fetch + verify flow |
| Profiling & Benchmarking | Syscall profiler, benchmark suite, cross-host comparison |
| POSIX Status | Syscall-by-syscall implementation status |
| Wasm Limitations | Fundamental platform constraints |
-
Compilation: C source → clang (wasm32-unknown-unknown) → linked against musl
libc.a+ glue layer. The glue translates musl's__syscall(number, args...)into typed writes to a SharedArrayBuffer channel. -
Loading: The TypeScript host instantiates the kernel Wasm module with host I/O imports, then creates process workers that each get their own Wasm memory with a channel region.
-
Syscall execution: When user code calls e.g.
open("/etc/hosts", O_RDONLY):- musl
open()→__syscall(SYS_openat, AT_FDCWD, path, flags, mode) - Glue writes syscall number + args to the channel, then
Atomics.store(status, SYSCALL_READY)+Atomics.notify() - Kernel worker wakes, reads channel, dispatches to
sys_open(), writes result back - Glue resumes with the return value (fd or negative errno)
- musl
-
Multi-process:
fork()useswasm-fork-instrumentto snapshot and restore the Wasm call stack. The host copies process memory to a new Web Worker, and the child resumes from the fork point.exec()replaces the process image by terminating the old worker and starting a new one with fresh memory. Cross-process pipes, signals, and locks are coordinated through the shared kernel instance.
This project uses a split license model:
- GPL-2.0-or-later — The platform (kernel, host runtime, SDK, build scripts, examples)
- MIT — Runtime library components linked into user programs (libc/musl-overlay/ and libc/glue/)
You can compile and run your own programs — including proprietary ones — without the GPL applying to your code. The runtime code linked into your program is MIT-licensed, and the kernel communicates via IPC, not linking.
See LICENSE for full details.