unkernel 0.0.0 → 0.1.0-261115-75add72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,546 @@
1
+ <h1 align="center"><img alt="unkernel" src="assets/logo-wordmark.svg" width="400"></h1>
2
+
3
+ **A real Linux kernel, compiled to WebAssembly. It boots to a shell in less than a second.**
4
+
5
+ <p align="center">
6
+ <a href="https://unkernel.dev/"><b>Try it</b></a> ·
7
+ <a href="#-performance">Performance</a> ·
8
+ <a href="#-a-real-kernel">Features</a> ·
9
+ <a href="#-run-it-from-the-terminal">CLI</a> ·
10
+ <a href="#-how-it-works">How it works</a> ·
11
+ <a href="#-benchmarks">Benchmarks</a> ·
12
+ <a href="#-use-it-from-javascript">JavaScript SDK</a>
13
+ </p>
14
+
15
+ unkernel is Linux 7.x built as a single wasm module with a real MMU. The
16
+ kernel (syscalls, scheduler, memory management, ext4, TCP/IP) is compiled
17
+ code, not emulated, so programs get Linux's own behavior down to the edge
18
+ cases. Only user programs are emulated, with a JIT: unmodified riscv64
19
+ Linux binaries, so Alpine and Ubuntu packages run as they are.
20
+
21
+ [Open a page](#-try-it-in-the-browser) and you have a Linux machine with a
22
+ shell, a package manager, a persistent disk, internet access, and a screen
23
+ with sound. It runs entirely on your device, with nothing to install and no
24
+ server, and every layer is open source. [How it works](#-how-it-works)
25
+ explains the design.
26
+
27
+ <p align="center">
28
+ <img alt="Linux programs run on kwemu, a riscv64 CPU linked into the Linux kernel, built together into unkernel.wasm, which talks to the JavaScript SDK" src="assets/architecture.svg" width="760">
29
+ </p>
30
+
31
+ ## ⚡ Performance
32
+
33
+ Much of what makes a system feel slow lives in the kernel: starting
34
+ processes, syscalls, page faults, file and network I/O. In unkernel that
35
+ path runs at wasm speed.
36
+
37
+ - **Boot**: the kernel reaches `/init` about 70 ms after it starts, and the Alpine
38
+ prompt appears 0.3 s after `node` launches. The page downloads about
39
+ 10 MB, and Alpine boots in 48 MiB of guest memory.
40
+ - **Syscalls at close to native cost**: `getppid` takes about 170 ns (110
41
+ native, 1.7 µs under qemu-system), a 64-byte `read` about 170 ns (160
42
+ native).
43
+ - **fork at native speed**: fork + exit + wait takes about 60 µs, against
44
+ about 70 µs natively and 1.2 ms under qemu-system.
45
+ - **A JIT for user code**: hot riscv64 code is compiled to wasm while it
46
+ runs. A tight loop takes 2.5 ns per iteration (1.8 native, 28 without the
47
+ JIT), and https downloads in the guest reach 30 MB/s.
48
+
49
+ CPU-heavy programs are still well behind native. [Benchmarks](#-benchmarks)
50
+ has the full numbers.
51
+
52
+ ## 🐧 A real kernel
53
+
54
+ Anything a program relies on, from `/proc` and `epoll` to cgroups and
55
+ overlayfs, behaves as it does on a server, because it runs the same code.
56
+
57
+ - **A real MMU** (`arch/wasm` with Sv39 page tables): `fork`, `mmap`,
58
+ copy-on-write, shared memory, threads and signals behave as they do on
59
+ hardware. This is not a NOMMU port where programs are limited to `vfork`.
60
+ - **Linux's own file systems**: ext4, overlayfs, tmpfs, loop devices, FUSE,
61
+ and 9p, which [mounts file systems](#mount-host-files) your program serves
62
+ from the host. In the browser the root file system is an overlay on a
63
+ persistent ext4 disk, so everything you change survives a reload, and a safe
64
+ mode recovers a system you broke.
65
+ - **Linux's own TCP/IP stack**, with DHCP and DNS, reaching the internet
66
+ through your host's sockets or a Wisp proxy
67
+ ([Add a network](#add-a-network)).
68
+ - **cgroups, namespaces, userfaultfd, fanotify and TUN**: enough for
69
+ systemd to boot Ubuntu 24.04 to `multi-user.target`.
70
+ - **Devices**: a [framebuffer (`/dev/fb0`), evdev keyboard, mouse and touch,
71
+ sound through ALSA and `/dev/dsp`](#display-input-and-sound),
72
+ [AF_VSOCK](#talk-over-vsock), and [shared memory](#share-memory) with the
73
+ host.
74
+ - **riscv64 user space**: RV64GC plus Zba, Zbb, Zbs, Zbc, Zbkb, Zbkc, Zbkx,
75
+ Zkne, Zknd, Zicond and the scalar rest of RVA23U64, so distribution
76
+ binaries and OpenSSL's AES and carry-less multiply paths run unchanged.
77
+ riscv64 is a modern 64-bit architecture with official ports in Alpine,
78
+ Debian and Ubuntu, so their full, current package archives are available.
79
+
80
+ [Known limits](#-known-limits) lists what is still missing.
81
+
82
+ ## 🧰 What you can do with it
83
+
84
+ - **[Carry a Linux shell anywhere](#-try-it-in-the-browser).** Alpine Linux
85
+ 3.24 runs in Chrome, Edge, Firefox and Safari, including on phones. Install
86
+ anything with `apk add` from Alpine's regular mirrors. Type a command that
87
+ isn't installed, such as `htop`, and its package is installed for you. apk
88
+ starts with only the `main` repository, because it is about 4x faster that
89
+ way; `community` is added the first time a command needs it.
90
+ - **Run real runtimes and tools**: Node and npm, Python, OpenSSL, curl and
91
+ `wget`, straight from the distribution. Ubuntu 24.04 boots from a disk,
92
+ with apt and, once you install it, systemd.
93
+ - **[Write graphics and sound programs](#display-input-and-sound)** against
94
+ `/dev/fb0`, evdev and ALSA. Try `glimmer`, a tiny game with sound.
95
+ - **[Embed Linux in your app](#-use-it-from-javascript)** with a
96
+ zero-dependency TypeScript SDK, published as
97
+ [`unkernel`](https://www.npmjs.com/package/unkernel). The same kernel file
98
+ runs in Node and in every supported browser, optionally [in a
99
+ worker](#run-it-in-a-worker). [Disks](#add-disks) can be local ext4 images,
100
+ http(s) images fetched by range, or a persistent disk in IndexedDB. The SDK
101
+ is MIT-licensed, and there is no closed engine or license key behind it, so
102
+ you can ship it in your product and host it wherever you like.
103
+ - **Talk to the guest** through [terminal sessions](#run-programs), each
104
+ with its own pty and shell, [host directories and in-memory file
105
+ systems](#mount-host-files) mounted in the guest, [vsock byte
106
+ streams](#talk-over-vsock) and [zero-copy shared memory](#share-memory),
107
+ with no network in between.
108
+ - **Script and test**: [the CLI](#-run-it-from-the-terminal) types input on a
109
+ schedule, stops on a timeout, and reports how the guest ended in its exit
110
+ code.
111
+
112
+ ## 🌐 Try it in the browser
113
+
114
+ Open **[unkernel.dev](https://unkernel.dev/)**. A shell appears
115
+ after a download of about 10 MB. Your changes are saved in the browser, and
116
+ the status icon opens options, diagnostics, and export or reset for your
117
+ disk. Ctrl-A n (or ⌘A n on a Mac) or `+` on the status line opens another
118
+ terminal window with its own shell, up to 9; Ctrl-A and a digit switches.
119
+
120
+ The page takes a few URL parameters:
121
+
122
+ | Parameter | Does |
123
+ | --- | --- |
124
+ | `?mem=512` | Guest memory in MiB |
125
+ | `?append=...` | Extra kernel command line |
126
+ | `?net=0` | Boot without a network |
127
+ | `?wisp=wss://...` | Use your own [Wisp](https://github.com/MercuryWorkshop/wisp-protocol) proxy |
128
+ | `?jit=0` | Interpreter only |
129
+ | `?data=0` | No persistent disk, a fresh system every time |
130
+ | `?worker=1` | Run the kernel in a Web Worker (experimental) |
131
+
132
+ Browsers can't open raw TCP sockets, so the page reaches the internet
133
+ through a Wisp proxy. The default one accepts only known origins, so if you
134
+ host the page somewhere else, point `?wisp=` at your own proxy.
135
+
136
+ | Browser | Status |
137
+ | --- | --- |
138
+ | Chrome / Edge 137+ (desktop and Android) | ✅ works |
139
+ | Firefox 153+ | ✅ works |
140
+ | Safari 27+ on macOS, any browser on iOS/iPadOS 27 | ✅ works |
141
+ | Anything without [JSPI](#-how-it-works) | the page tells you what's missing |
142
+
143
+ ## 💻 Run it from the terminal
144
+
145
+ ```sh
146
+ npx unkernel
147
+ ```
148
+
149
+ This boots Alpine from RAM, with networking over your host's sockets. Press
150
+ **Ctrl-]** to quit.
151
+
152
+ ```sh
153
+ npx unkernel --disk alpine.ext4 # boot from an ext4 image, changes persist
154
+ npx unkernel --data my-data.img # a persistent /data disk, created on first use
155
+ npx unkernel --display --sound # a framebuffer and a sound card
156
+ npx unkernel --mem 1024 --no-net # more memory, no network
157
+ npx unkernel --mount .:/work # this directory at /work in the guest (:ro for read-only)
158
+ npx unkernel --forward 8000:80 # host 127.0.0.1:8000 to port 80 in the guest
159
+ npx unkernel --timeout 60 --input '{8}uname -a\n{2}poweroff\n' # scripted: wait, type, wait, type
160
+ npx unkernel --help # everything else
161
+ ```
162
+
163
+ Exit codes: 0 power off, 1 halt or boot failure, 2 restart, 124 timeout, 253
164
+ kernel panic, 254 wasm trap, 255 fatal error. The CLI is built on the
165
+ [JavaScript SDK](#-use-it-from-javascript): `--mount` uses its [host
166
+ mounts](#mount-host-files) and `--forward` its
167
+ [`vm.connect()`](#talk-over-vsock).
168
+
169
+ ## 🔍 How it works
170
+
171
+ Most "Linux in the browser" projects take one of three approaches:
172
+
173
+ - **Emulate a whole PC**, kernel included. This is faithful but slow,
174
+ because every syscall and page fault goes through emulated hardware.
175
+ - **Emulate the CPU and reimplement the syscalls.** Programs run on an
176
+ emulated CPU, often with a JIT, and a runtime answers their syscalls, with
177
+ no Linux kernel at all. This is fast, but it covers only as much of Linux
178
+ as the runtime implements.
179
+ - **Compile programs to wasm**, and run them on a kernel without an MMU or
180
+ on a runtime that provides POSIX calls. This is fast, but native programs
181
+ have to be rebuilt, and `fork` and anything else that needs an MMU breaks
182
+ or is approximated.
183
+
184
+ unkernel keeps the best of each: the real Linux kernel, running at native
185
+ wasm speed, under unmodified binaries with a real MMU.
186
+
187
+ | Approach | Kernel | User space | MMU |
188
+ | --- | --- | --- | --- |
189
+ | Whole-machine emulators (v86, JSLinux, container2wasm, QEMU's wasm build) | Linux, emulated | unmodified, emulated | emulated |
190
+ | Syscall emulators | none: a runtime reimplements the syscalls | unmodified, emulated | the runtime's own |
191
+ | POSIX runtimes for wasm (Wasmer WASIX, WebContainers, BrowserPod) | none: a runtime provides the syscalls | rebuilt for wasm, or JavaScript | none |
192
+ | Linux without an MMU (linux-wasm, tombl/linux, WasmLinux (LKL)) | Linux, native wasm | rebuilt for wasm | none |
193
+ | **unkernel** | **Linux, native wasm** | **unmodified riscv64, with a JIT** | **real Sv39 page tables** |
194
+
195
+ The kernel is compiled straight to WebAssembly. User programs run on
196
+ **kwemu**, a riscv64 CPU written in Zig and linked into the kernel. kwemu
197
+ walks the kernel's own page tables, so memory management is done by the
198
+ real Linux code.
199
+
200
+ When a program makes a syscall, kwemu returns to the kernel, which handles
201
+ it as native wasm. There is no emulated trap and no emulated kernel mode in
202
+ between. Task switches use JSPI (`WebAssembly.Suspending`), so the kernel
203
+ gets real stacks that it can suspend and resume.
204
+
205
+ Hot user code goes through a second tier: a JIT that compiles riscv64 to
206
+ wasm modules at run time, with output that matches the interpreter bit for
207
+ bit. [Benchmarks](#-benchmarks) shows what each tier costs.
208
+
209
+ Devices are small paravirtual drivers that talk to the JavaScript host:
210
+ [block](#add-disks) (kwblk), [network](#add-a-network) (kwnet, with a
211
+ user-mode TCP/IP stack in JS), [display, input and
212
+ sound](#display-input-and-sound) (kwfb, kwinput, kwsnd),
213
+ [vsock](#talk-over-vsock) (kwvsock) and [shared memory](#share-memory)
214
+ (kwshm).
215
+
216
+ ### 📊 Benchmarks
217
+
218
+ The design helps most with syscalls. These were measured under Node:
219
+
220
+ <p align="center">
221
+ <img alt="Time per operation for native, qemu-user, qemu-system and unkernel: getppid 110, 320, 1662 and 172 ns; read 64 B from /dev/zero 161, 481, 2247 and 172 ns; pipe write + read 417, 1032, 7256 and 482 ns; fork + exit + wait 73, 537, 1178 and 57 µs" src="assets/performance.svg" width="760">
222
+ </p>
223
+
224
+ In this test fork is faster than native. Pure computation is slower. The JIT
225
+ runs a tight integer loop at about 3900 MIPS (about 270 interpreted), and
226
+ real Alpine work (`sort`, `gzip`, `sha256sum`, `awk`) runs 1.7 to 4.6 times
227
+ faster than without it, which is still well behind native.
228
+
229
+ ### 🚧 Known limits
230
+
231
+ - One CPU, no SMP.
232
+ - No IPv6, no UDP beyond DHCP and DNS, no inbound network connections
233
+ ([`vm.connect()`, `vm.fetch()`](#talk-over-vsock) and `--forward` reach
234
+ guest servers over vsock instead).
235
+ - No ptrace register sets, so `strace` and `gdb` don't work. No seccomp.
236
+ - The JIT leaves atomics and system instructions to the interpreter.
237
+ - A wasm trap in the kernel ends the session.
238
+
239
+ ## 🧩 Use it from JavaScript
240
+
241
+ ```sh
242
+ npm install unkernel
243
+ ```
244
+
245
+ ```js
246
+ import { createKernel } from "unkernel";
247
+
248
+ const vm = await createKernel(); // bundled Alpine, no network
249
+ vm.onOutput((bytes) => process.stdout.write(bytes));
250
+ vm.write("uname -a; poweroff\n");
251
+
252
+ const { code, kind } = await vm.exited; // 0, "halt"
253
+ ```
254
+
255
+ The same code runs in the browser. Importing `unkernel` picks the Node or
256
+ browser entry automatically, and `unkernel/node` or `unkernel/web` selects
257
+ one explicitly. The kernel ships in the package and loads from next to the
258
+ entry, streamed and compiled while it downloads.
259
+
260
+ ### Add a network
261
+
262
+ ```js
263
+ import { createKernel, directBackend, wispBackend, DEFAULT_WISP_SERVERS } from "unkernel";
264
+
265
+ await createKernel({ net: directBackend() }); // Node: host sockets
266
+ await createKernel({ net: wispBackend(DEFAULT_WISP_SERVERS) }); // anywhere: a Wisp proxy
267
+ ```
268
+
269
+ Networking is off unless you pass `net`. Any object with `connect` and
270
+ `lookup` methods can carry the guest's TCP and DNS, so you can write your
271
+ own backend.
272
+
273
+ ### Add disks
274
+
275
+ ```js
276
+ import { createKernel, openDisk } from "unkernel/node";
277
+
278
+ const vm = await createKernel({
279
+ disks: [await openDisk("alpine.ext4")], // or an https:// URL
280
+ cmdline: "root=/dev/wda rw",
281
+ });
282
+ ```
283
+
284
+ In the browser, `openHttpDisk(url)` keeps your writes in IndexedDB and
285
+ `openDataDisk()` gives you the persistent data disk that
286
+ [the page](#-try-it-in-the-browser) uses. Anything that
287
+ implements `BlockDevice` works too.
288
+
289
+ ### Console and lifecycle
290
+
291
+ ```js
292
+ // vm.output: a ReadableStream, everything since boot and then live
293
+ vm.output.pipeTo(new WritableStream({ write: (b) => process.stdout.write(b) }));
294
+ vm.write("ls /\n"); // or write to vm.input, a WritableStream
295
+ vm.resize(120, 40); // the foreground program gets SIGWINCH
296
+
297
+ await vm.sync(); // flush the guest's disks
298
+ console.log(vm.stats()); // memory, disk, network and JIT counters
299
+ const { code, reason, kind } = await vm.exited; // stop() parks it at once
300
+ ```
301
+
302
+ ### Run programs
303
+
304
+ ```js
305
+ const { stdout, exitCode } = await vm.exec("uname", ["-a"]);
306
+ const { stdout: sum } = await vm.exec("sha256sum", [], { input: "hello" });
307
+ ```
308
+
309
+ `exec()` runs a program on pipes and resolves with `{ stdout, stderr,
310
+ exitCode, signal }`, also for a non-zero exit. Pass `encoding: "buffer"`
311
+ for bytes, and `cwd` or `env` as you would to `child_process`.
312
+
313
+ ```js
314
+ const sh = await vm.spawn(); // a login shell on its own pty
315
+ sh.output.pipeTo(new WritableStream({ write: (b) => process.stdout.write(b) }));
316
+ await sh.write("cat /etc/alpine-release; exit\n");
317
+ console.log(await sh.ended); // { type: "exit", code: 0 }
318
+ ```
319
+
320
+ `spawn(file, args, { cols, rows })` runs a program on a pty, in the
321
+ zigpty / node-pty shape, with `resize()`, `kill(signal)` and `close()` (a
322
+ hangup). Run as many as you like. Keep reading `output`: the guest program
323
+ waits while nobody does.
324
+
325
+ ```js
326
+ const gz = await vm.spawn("gzip", ["-c"], { pty: false });
327
+ gz.stderr.cancel(); // read both outputs, or cancel one
328
+ const zipped = new Response(gz.stdout).bytes();
329
+ await new Response("hello\n").body.pipeTo(gz.stdin); // then EOF
330
+ console.log((await zipped).length);
331
+ ```
332
+
333
+ With `pty: false` the program gets pipes: `stdin`, `stdout` and `stderr`
334
+ web streams, byte for byte.
335
+
336
+ ### Mount host files
337
+
338
+ ```js
339
+ import { createKernel, MemoryFs, nodeFs } from "unkernel";
340
+
341
+ const mem = new MemoryFs();
342
+ mem.onChange((c) => console.log(c.type, c.path)); // what the guest changed
343
+
344
+ const vm = await createKernel({
345
+ mounts: {
346
+ "/mnt/host": { "hello.txt": "hi", dir: { "a.bin": bytes }, "now.txt": () => Date() },
347
+ "/mnt/mem": mem,
348
+ },
349
+ });
350
+ await vm.exec("cp", ["/etc/os-release", "/mnt/mem/"]);
351
+ console.log(mem.readText("/os-release"));
352
+
353
+ const work = await vm.mount("/work", nodeFs("."), { readOnly: true }); // Node only
354
+ await work.unmount();
355
+ ```
356
+
357
+ A file system is a plain object tree (a function is a computed file,
358
+ evaluated once per open), a `MemoryFs` you read and write from the host,
359
+ `nodeFs(dir)`, or your own `Fs`: `getattr`, `readdir` and `open` by path,
360
+ sync or async, plus whichever change methods you want. Mounts are served
361
+ over 9p on vsock. Each guest write reaches the host as it happens, and a
362
+ host change shows in the guest within a second.
363
+
364
+ ### Talk over vsock
365
+
366
+ ```js
367
+ const listener = vm.vsock.listen(5000); // guest programs connect to CID 2, port 5000
368
+ vm.write("echo hello | kwvsock connect 5000\n");
369
+ const conn = await listener.accept();
370
+ console.log(await new Response(conn.readable).text()); // "hello\n"
371
+ conn.close();
372
+
373
+ const c = await vm.vsock.connect(6000); // a guest program listening on port 6000
374
+ await c.writable.getWriter().write("ping");
375
+ ```
376
+
377
+ These are byte streams with real flow control and no network in between.
378
+ vsock is on unless you pass `vsock: false`.
379
+
380
+ ```js
381
+ const sock = await vm.connect(8000); // TCP to the guest's 127.0.0.1:8000
382
+ await new Response("hello\n").body.pipeTo(sock.writable); // then EOF (a half-close)
383
+ console.log(await new Response(sock.readable).text());
384
+ ```
385
+
386
+ `vm.connect(port)` reaches a TCP server on the guest's loopback through
387
+ the guest's kwtty, over vsock, so it needs no network. It resolves once
388
+ the guest has connected (`PtyError` `ECONNREFUSED` if nothing listens) with
389
+ `readable` and `writable` byte streams, half-close included, `close()` and
390
+ `closed`. [The CLI's](#-run-it-from-the-terminal) `--forward
391
+ HOSTPORT:GUESTPORT` uses it.
392
+
393
+ `vm.fetch(url, init)` sends an HTTP/1.1 request to a server in the guest
394
+ and returns a `Response` whose body streams. It always connects to the
395
+ guest's 127.0.0.1 at the URL's port; the host name only goes into the
396
+ `Host` header, so `vm.fetch("http://localhost:8000/")` reaches a dev server
397
+ listening on port 8000. `fetchOver(connect, url, init)` is the same client
398
+ over any byte stream.
399
+
400
+ ### Share memory
401
+
402
+ ```js
403
+ const vm = await createKernel({ shm: true });
404
+ await vm.exec("sh", ["-c", "printf result > /dev/shm/output"]);
405
+
406
+ const output = await vm.shm.open("output"); // a file a guest program made
407
+ const data = await output.read();
408
+ const views = output.views(); // zero-copy, until the kernel runs again
409
+
410
+ const input = await vm.shm.create("input", 1 << 20); // /dev/shm/input in the guest
411
+ await input.write(0, bytes);
412
+ ```
413
+
414
+ `/dev/shm` exists once the guest's init has mounted it, so wait for the
415
+ guest before `create()` (an `exec()` waits for it). `openId(id)` opens a
416
+ memfd that a guest program exported through `/dev/kwshm`. `views()`
417
+ exists only when the kernel runs inline.
418
+
419
+ ### Display, input and sound
420
+
421
+ ```js
422
+ import { createKernel, domCodeToLinux, pcmToFloat32 } from "unkernel";
423
+
424
+ const vm = await createKernel({
425
+ display: { width: 800, height: 600 }, // the guest gets /dev/fb0
426
+ onDisplay: (mode) => console.log(mode), // a program opened it (its mode) or closed it (null)
427
+ sound: { onChunk: ({ data, format }) => play(pcmToFloat32(data, format)) },
428
+ });
429
+
430
+ const frame = vm.frame(); // RGBA, or null while nothing draws
431
+ if (frame) ctx.putImageData(new ImageData(frame.data, frame.width, frame.height), 0, 0);
432
+
433
+ addEventListener("keydown", (e) => {
434
+ const key = domCodeToLinux(e.code);
435
+ if (key && vm.evdev.keyboardOpen) vm.evdev.key(key, true);
436
+ });
437
+ ```
438
+
439
+ `evdev` also has `button()`, `move()`, `moveTo()` and `wheel()` for a
440
+ mouse or tablet. `onInput` tells you when a program opens the keyboard
441
+ and mouse; until then, send keys to the console. `readFrame()` waits for
442
+ a fresh frame.
443
+
444
+ ### Run it in a worker
445
+
446
+ ```js
447
+ const vm = await createKernel({
448
+ worker: true,
449
+ net: { kind: "net", backend: { kind: "direct" } },
450
+ disks: [{ kind: "file", path: "alpine.ext4" }],
451
+ });
452
+ ```
453
+
454
+ The kernel runs in a worker thread or Web Worker, with the same API. Disks
455
+ and the network are given as specs, which the worker builds: `{ kind:
456
+ "file", path }` on Node, `{ kind: "http", url }` and `{ kind: "data",
457
+ template: "bundled" }` in browsers. Input and frames go through shared
458
+ memory when it is available (`vm.transport` says). This mode is
459
+ experimental.
460
+
461
+ ### Boot options and helpers
462
+
463
+ ```js
464
+ import { checkSupport, ConsoleRouter, createKernel } from "unkernel";
465
+
466
+ const support = await checkSupport(); // JSPI and the rest, before you boot
467
+ if (!support.ok) throw new Error(support.problems.join("\n"));
468
+
469
+ const vm = await createKernel({
470
+ kernel: "./my-kernel.wasm", // a URL, bytes, a Response or a WebAssembly.Module
471
+ initrd: "./initramfs.cpio.gz", // null: none
472
+ cmdline: "console=hvc0 rdinit=/init",
473
+ memoryMiB: 1024,
474
+ jit: false, // interpreter only
475
+ onProgress: (p) => console.log(p.stage),
476
+ });
477
+
478
+ const router = new ConsoleRouter({ // kernel log lines apart from program output
479
+ onKernelLine: (line, { important }) => important && console.warn(line),
480
+ onUserOutput: (text) => process.stdout.write(text),
481
+ });
482
+ vm.onOutput((bytes) => router.write(bytes));
483
+ ```
484
+
485
+ The SDK has some limits, on top of the kernel's [known
486
+ limits](#-known-limits). There is no guest agent beyond terminal sessions, so
487
+ you drive the guest through a terminal or its console, or over vsock with your
488
+ own program. `stop()` doesn't shut the guest down cleanly, so prefer
489
+ `poweroff`. Only one kernel can run per thread. Types ship as `.d.mts` and use
490
+ the DOM's `WebAssembly` types, so a Node-only TypeScript project needs `"lib":
491
+ ["dom"]` or `skipLibCheck`.
492
+
493
+ ## 🛠️ Building from source
494
+
495
+ You need Linux (x86-64 or arm64), Zig 0.16, Node 24, clang 21+ with the
496
+ wasm target, and the usual build tools. `./setup.sh --check` lists what's
497
+ missing. The first build takes about 12 minutes, most of it spent on a
498
+ patched `wasm-ld`, and uses 5 GiB of disk.
499
+
500
+ ```sh
501
+ ./setup.sh # check, then build everything
502
+ node js/cli.ts # boot your build
503
+ pnpm install && pnpm dev # the web page at http://localhost:8080
504
+ zig build test # emulator tests
505
+ ```
506
+
507
+ [kernel/README.md](kernel/README.md) covers the kernel, its devices and the
508
+ guest image. [AGENTS.md](AGENTS.md) and [.agents/](.agents/) hold the
509
+ contributor notes: invariants, workflows, which tests to run, and the
510
+ [roadmap](.agents/status.md).
511
+
512
+ ## 🙏 Credits
513
+
514
+ Made with ♥ by [Pooya Parsa](https://x.com/_pi0_) ([pi0](https://github.com/pi0)), sponsored by
515
+ [▲ Vercel](https://vercel.com/open-source-program).
516
+
517
+ Thanks to:
518
+
519
+ - [Kernarium](https://github.com/imxieyi/Kernarium), for the idea: a native
520
+ kernel with emulated user space behind a software MMU.
521
+ - [linux-wasm](https://github.com/joelseverin/linux-wasm) by Joel Severin,
522
+ for the `arch/wasm` port this kernel grew from, the wasm-ld linker-script
523
+ patches and the Kbuild toolchain wrapper.
524
+ - [iSH](https://github.com/ish-app/ish) and [v86](https://github.com/copy/v86),
525
+ for showing how to build a fast user-mode emulator and a wasm JIT.
526
+ - [Alpine Linux](https://alpinelinux.org/), [wterm](https://wterm.dev/) and
527
+ [Binaryen](https://github.com/WebAssembly/binaryen).
528
+
529
+ ## 📄 License
530
+
531
+ Everything is open source: the kernel patches, the emulator and its JIT,
532
+ the patched linker, the SDK and the page. `./setup.sh` rebuilds the kernel
533
+ file from source.
534
+
535
+ The project is GPL-2.0-or-later ([LICENSE](LICENSE)) unless a file says
536
+ otherwise. The JavaScript SDK and page (`js/`, `web/`, SDK tests) are MIT
537
+ ([LICENSE-MIT](LICENSE-MIT)), so you can embed them anywhere.
538
+
539
+ The npm package ships a Linux kernel (GPL-2.0) with an Alpine userland whose
540
+ packages keep their own licenses, so it is `MIT AND GPL-2.0-only`. The
541
+ kernel's source is Linux 7.3.4 plus
542
+ [kernel/patches/linux](kernel/patches/linux) (`arch/wasm` is based on
543
+ linux-wasm's). Those patches and
544
+ `kernel/tools/fake-llvm` are GPL-2.0-only, and the patches in
545
+ `kernel/patches/llvm` follow LLVM's license (Apache-2.0 WITH
546
+ LLVM-exception).