A fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code.
A real, kernel-enforced container in ~3.6 ms, out of one ~1.8 MB binary with no daemon.
0 RAM at rest · no daemon, no socket, nothing to start · one static binary, libc the only dependency
Runs everywhere Linux does: bare Linux, Windows (via WSL2), and ARM boards (Raspberry Pi, NVIDIA Jetson, Arduino UNO Q), where a 186 MB Docker daemon is a poor fit (on the Pi 5 tested here, no engine was installed at all).
Quickstart · When to use it, and when not · Embed it · How it works · Benchmarks · Security · Config
🐧 Linux & ARM boards
curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh🪟 Windows (via WSL2)
irm https://raw.githubusercontent.com/getkern/kern/main/install.ps1 | iexthen kern box dev --image alpine -- sh
Isolation is the first resource kern manages this way, not the only one: the same model also slices CPU
(vcpu:), memory, disk (vdisk:) and devices (vgpio:) per process, with or without a full box. The
container is one case of a smaller idea, and it is why there are two verbs. Embed it from Python, Node or
Demo timings on an Intel i7-14700KF (20-core / 28-thread x86_64, Linux 7.0, systemd-user, cgroup delegated): the timings in it are capped box starts, the vcpu:heavy vdisk:scratch slice the demo attaches, which is why they differ from the uncapped figure above; each is a full kern box lifecycle (fork, isolate, run, tear down), not kern's own setup phase in isolation, and the Performance table was measured on the same machine on the same night. Your hardware and cgroup delegation differ, so measure your own: kern bench --rootfs <dir>. See Benchmarks for methodology, the capped-vs-uncapped split, and on-device board numbers.
kern runs Linux workloads in real, kernel-enforced sandboxes: user + PID + mount + network + UTS + IPC namespaces, an overlay or read-only root pivoted in, an always-on seccomp filter, and cgroup limits. It pulls OCI images, builds them, runs them, and gets out of the way. No background daemon, one short-lived process per box, started in single-digit milliseconds.
It's built around one idea, virtual resources, exposed as two verbs: box wraps a process
in a full isolated slice; run caps a resource on a process you launch yourself. Isolation is just
the first resource; the same model virtualizes CPU (vcpu:), memory, disk (vdisk:) and GPIO
devices (vgpio:) today, with GPU on the roadmap. On the box side that's a full daemonless
container UX (OCI pull and build, overlay, volumes, secrets, in-box SSH, cp/pause/attach,
ps/exec/logs, compose, health, tag/push, save/load) in ~1.8 MB.
kern box dev --image alpine -- sh # a throwaway, isolated Alpine shell, in a few ms…or embed it, a fresh isolated box per call, for untrusted or agent-generated code (cloud-code-interpreter territory, but local and ~1.8 MB: no cloud, no account, no VM):
import kern_sandbox as kern # pip install kern-sandbox
r = kern.run_code("print(sum(range(100)))") # network OFF, hard caps, a timeout the binding enforces
print(r.stdout, r.success) # → a fresh, discarded-after boxThat call measures 16 ms end to end on the x86_64 desktop (median of 60, after warm-up), and the
subtraction is worth publishing rather than a summary of it: ~8.4 ms is CPython starting at all
(python3 -c pass on the host, same machine), ~3.6 ms is the box, and the remaining ~4.3 ms is
the binding's own work, which is not attributed further. So the interpreter is about half of it, not
"nearly all" - the shape this figure used to be described with.
One move under all of these: a fresh, isolated, resource-capped box in single-digit milliseconds, wherever you would otherwise reach for a container, a cloud sandbox, or root.
- Development: one command, in a real box, in milliseconds, then gone. A service, a database or a clean per-language build box starts, runs, and is thrown away with your host untouched and nothing left resident (docker-shim.sh · compose-declared-ports.sh · familiar-commands.sh · database-box.sh).
- AI agents: run each model-generated tool call in a fresh, network-off box, sandbox faults come back as data, not crashes (warm-kernel.py · kern-mcp for Claude Desktop / Cursor · agent-tool-runner.py).
- CI: run each step in a capped, daemonless box, no Docker-in-Docker (ci-in-a-box.sh).
- Edge / ARM: one 1.8 MB binary on a Pi 5 / Jetson where a Docker daemon does not fit (edge-many-services.sh).
- Device access / IoT: hand a workload exactly the device nodes you name (an I2C or SPI bus, a GPIO chip, a sensor) and nothing else, deny-by-default. The closest thing is CDI (the Container Device Interface), but that's a JSON spec you register with a supporting engine; kern's is a line of TOML, engine included, and it works on a bare
kern runtoo (device-isolation.sh). - Untrusted or customer code: execute it isolated and resource-capped, on your own machine, no cloud (code-interpreter.py).
- Serverless / functions: a fresh, throwaway box per request, so one call's crash or timeout stays contained to its own box (per-request-workers.py).
- Data & batch: ETL, scraping, per-file fan-out, each input in its own capped box (data-pipeline.sh · batch-process.sh).
- Build and run OCI images:
kern build/kern box, speaks Docker formats, no daemon.
| kern | Docker | Podman | |
|---|---|---|---|
| Daemon | no | yes (dockerd + containerd) |
no |
| Rootless | yes | opt-in (rootless mode) | yes |
| Cold start (a bare box) | ~2.2 ms | ~289 ms | ~288 ms |
| Cold start (from an OCI image) | ~3.6 ms | ~289 ms | ~288 ms |
| Footprint | one 1.8 MB static binary | ~186 MB daemon stack | multi-binary install |
| OCI images (pull / build) | yes | yes | yes |
| Resource caps without a full box | yes (kern run) |
no | no |
Startup numbers are from a labeled benchmark on one machine, measure your own. kern covers a smaller surface than Docker (no overlay networks, no swarm) and spends it on a fast, rootless core: single-digit-millisecond starts from one ~1.8 MB binary, no daemon. It is a kernel-boundary sandbox for your own or semi-trusted code; for actively hostile multi-tenant code, a microVM is the right tool, and SECURITY.md says exactly where the line is.
What that trade costs, stated here rather than discovered later. A kern compose stack is one
pod: no network segmentation between services, no deploy.replicas, nothing on docker.sock.
See when NOT to use kern.
"So it's a reimplementation of Docker?" No, and the dependency list is the shortest proof: kern is
~58,000 lines of Rust with one external crate (libc), no vendored code and no Docker source. What
it shares with other runtimes are published standards, the OCI image spec
and the Compose Specification, the same way two browsers share HTML. Reading
a format is interoperability, not lineage. The architecture goes the other way on purpose: Docker runs a
container through a daemon and several binaries, kern runs one through a single static binary with
nothing resident between calls. And the docker-syntax shim is deliberately partial and says so: it
translates what maps, refuses what would change behaviour, and never silently ignores a flag
(Docker compatibility).
- ⚡ Daemonless & tiny. No
dockerd-style service. A ~1.8 MB static binary, one Rust dependency (libc); it shells out to the system'scurl/taronly to pull images (running a box needs neither). A cold start faster than a daemon round-trip; ~2 MB RSS per box vs an always-on ~186 MB daemon (dockerd+containerd).kern psreads state straight from the kernel. - 👤 Rootless by default. Unprivileged user namespaces: your uid maps to root inside the box,
and only there. Single-uid is the default and is
libc-pure (no helper, smallest id surface);--uid-rangeopts into a full sub-id range (apt,www-data-style drops) via the standardnewuidmap+/etc/subuid. kern says plainly that path is not helper-free. No host privilege is gained either way. - 🧱 Correct by construction. The mount sequence is a typestate: remounting the root read-only
before pivoting into it doesn't compile, so a class of sandbox-escape bug is unrepresentable, not
just untested.
--planprints the exact isolation sequence without running anything. - 🔍 Honest about its boundaries. Filesystem / process / namespace isolation is a real kernel boundary, the right tool for your own or semi-trusted code (CI, dev, edge, your agents' code). For actively hostile multi-tenant code, reach for a microVM. SECURITY.md says exactly when to use which, and marks every guarantee that is cooperative or opt-in.
| Verb | Question it answers | What it does | Status |
|---|---|---|---|
kern box |
"Isolate this workload, and slice its resources." | Its own namespaces, overlay/read-only fs, private process tree, seccomp (the container), plus the same resource slices (--memory, --cpus, vcpu:, vdisk:, vgpio:). |
✅ works now |
kern run |
"Just slice resources, no sandbox." | Run a command against a CPU / memory quota with no isolation: the lean governor on its own. (A GPU slice is on the roadmap.) | ✅ works now |
Both take resource slices; the difference is the sandbox. box = isolation + slices; run =
slices without the sandbox. They compose (run inside box). Both ship today.
One model: a box gets only what you slice for it; a bare run adds the same caps to a process that
otherwise still sees the host. Every shipping cap is a real cgroup v2 or kernel control; devices are
deny-by-default. GPU slices are on the Roadmap.
| Resource | Flag / profile | What the box gets | Enforcement |
|---|---|---|---|
| CPU | --cpus · --cpuset-cpus · --nice · vcpu: |
Fractional CPU-time quota, core pinning, priority | cgroup cpu.max / cpuset, hard |
| Memory | --memory · --memory-swap-max |
Hard RAM ceiling (+ swap allowance) | cgroup memory.max, hard¹ |
| Disk | vdisk: · --size (named volumes) |
Size-capped scratch at /vdisk/<name> |
rootless: a RAM-backed tmpfs (counts against the box's memory cap); privileged: ext4-on-loop quota on real disk |
| Devices | vgpio: |
Only the named GPIO/I²C/SPI/LED nodes, nothing else | fresh /dev + fd-pinned bind + capability deny-list (raw-mem/disk/kvm refused) |
| PIDs | --pids-limit |
Fork-bomb ceiling | cgroup pids.max, hard |
| Block I/O | --io-weight |
I/O bandwidth weight | cgroup io |
| GPU | (roadmap) | Not shipped | see Roadmap |
¹ Where the memory controller is not delegated to a non-root user's scope, kern warns and shows the one-line
.wslconfig fix; enforced natively on Linux.
How to check a cap, and how not to. free, top and htop inside a box report the host's memory,
not the box's, because the kernel does not namespace /proc/meminfo. That is true of every container
runtime without a /proc shim, and it makes a cap that is enforced look absent. The distinction that
matters in practice: a runtime that sizes itself from the cgroup (a modern JVM, Go's memory limit, most
container-aware tooling) gets the right number; one that reads /proc/meminfo gets the host's. Read the
cgroup, or hit the ceiling and watch the kernel do it:
kern box check --image alpine --memory 256m -- cat /sys/fs/cgroup/memory.max # 268435456
kern box oom --image alpine --memory 256m -- \
sh -c 'dd if=/dev/zero of=/dev/shm/x bs=1M count=512' ; echo "exit=$?" # exit=137, OOM-killedThe cap binds on every host tested. On the Arduino UNO Q the BOX exits 143 rather than 137, and the
reason is measurable rather than a platform quirk: the workload itself is SIGKILLed by the cgroup
(dd exits 137 there, captured to a file before the shell dies), and the box's own init is
terminated afterwards, which is the 143. Android's lmkd is not running on that board, so it is
the cgroup doing the killing, not a host-level low-memory killer. memory.max reads back 33554432
throughout.
cpu.max reads the same way: 50000 100000 is half a core, 200000 100000 is two.
--pids-limit counts every task in the box, not just the ones your workload forks, so the forks
available to you are the limit minus whatever is already there. That baseline is not a fixed number: it
depends on whether the command is a shell or an exec'd binary, and on whether the box is detached. On the
box measured here it was 2, so --pids-limit 30 allowed 28 forks before the kernel returned EAGAIN
(can't fork: Resource temporarily unavailable). It is a fork-bomb ceiling, not an exact budget for your
own processes. Profiles (vcpu:/vdisk:/vgpio:) are reusable presets in
~/.config/kern/kern.toml, see docs/CONFIG.md. Author them with kern probe (list the
host resources you can slice), kern examples (print a sample kern.toml), and kern validate (check one).
Name a slice once, attach it by name, and stop repeating flags. Profiles live in
~/.config/kern/kern.toml, so nothing has to be passed on the command line:
kern config add vcpu:slim --cpus 0.5 --memory 256m # or edit the file by hand
kern box app --image alpine vcpu:slim -- ./app # no flag: the profile is just there
kern run vcpu:slim -- ./train.sh # the same slice, no sandboxWhat that wrote, and what a hand-written one looks like:
[[vcpu]]
name = "slim"
backend = "host" # the host resource being sliced: "host" is the whole CPU, so no [[cpu]] block
cpus = 0.5
memory = "256 MB"
[[vgpio]]
name = "sensor" # the interesting one: exactly one device node, everything else denied
backend = "host"
i2c = ["/dev/i2c-1"]Profile tokens go BEFORE the --: vcpu: · vdisk: · vgpio:. Use --config ./kern.toml only to
point at a per-project file instead of the default, or export KERN_CONFIG to make that the file every
command reads and writes. The full schema, every field, the 7-layer precedence and extends are in
docs/CONFIG.md; a runnable walk-through is
resource-profiles.sh.
No daemon, no root: one ~1.8 MB binary.
kern box try --image alpine -- shDeny-by-default: only the peripheral you name crosses the boundary.
kern box iot --image alpine vgpio:sensor -- ./read.py # only /dev/i2c-1 crosses inServerless-style, on your own machine: one throwaway box per call.
echo "$payload" | kern box fn --image python -- handler.pyThe same box on a Pi or an Android-kernel board: just copy the one binary.
scp kern pi: && ssh pi 'kern box edge --image alpine -- ./agent'kern build -t app:1 . && kern tag app:1 registry.example/app:1 && kern push registry.example/app:1Daemonless, rootless, and complete: the full container UX plus resource slices, in one binary.
For every flag and command, run kern --help: it is generated from the parser, so it cannot
drift from what the binary accepts the way a hand-kept list in here would.
- Run anything, isolated: OCI images from any registry (v2 auth, multi-arch, gzip + zstd) or a
--rootfs; CoW overlay (image immutable, scratch discarded) or--read-only;-itTTY;--initPID-1 reaper. - Governed slices: hard cgroup v2 caps on any
box/run:--memory·--cpus·--cpuset-cpus·--memory-swap-max·--pids-limit·--io-weight·--nice.kern runis the governor with no sandbox. - Data & devices:
-vvolumes (symlink-safe) · named volumes with a--sizequota · network volumes (nfs/smb/sshfs) ·--secret→/run/secrets(RAM,0400) ·vdisk:scratch ·vgpio:device passthrough (deny-by-default) ·--tmpfs. - Network & identity: isolated by default;
--network hostfor outbound;--egress-allow d1,d2(⚠️ experimental) restricts outbound to an allowlist of domains via a kern-run filtering proxy (an agent canpip installbut can't exfiltrate to an arbitrary domain), with honest known gaps documented in docs/EGRESS.md;-prootless publish (loopback unless you ask); in-box--ssh;--podshared-net pods (--no-outbound);--tun;--user. - Least privilege: 13 dangerous caps always dropped (
--cap-add/--cap-drop); an always-on seccomp denylist (kexec, modules, ptrace, mount API,setns, …) that also kills wrong-arch + x86_64 x32-ABI aliases; opt-in Landlock (LSM) write-allowlist (--landlock-rw <path>): the box root is read+exec and writes are confined to the paths you name, a kernel-enforced second boundary the workload can't lift. - Lifecycle, no daemon:
--restart+--health-cmd;cp/pause/attach/exec/rename/update/wait/diff/events;ps/top/stats/logs/inspect/prune/gc/history/recover;compose(readsdocker-compose.ymltoo); reusable[[vcpu]]/[[vgpio]]/[[vdisk]]profiles;kern doctor.
Built-in hardening. User+PID+net+UTS+IPC+mount namespaces, self-pivot root, nosuid,nodev box
root, always-on seccomp, least-privilege caps, hard cgroup caps (via systemd-run where present);
every pulled blob sha256-verified and every layer vetted in-process (no ../absolute/device escapes,
decompression- & inode-bomb caps) before an isolated no-follow merge. Where a guarantee is cooperative
or opt-in (vGPIO/vDisk trust scope, network volumes), SECURITY.md says so.
🐧 Linux & ARM boards (Raspberry Pi · Jetson · Arduino UNO Q). One line; auto-detects x86-64 / aarch64:
curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | shServed from github.com (read the script first if you like). It downloads the release binary for
your arch and verifies the sha256 before installing. No Rust toolchain required. (getkern.dev/install.sh
is a short alias.)
🪟 Windows. One line in PowerShell (no Docker Desktop, no Ubuntu):
irm https://raw.githubusercontent.com/getkern/kern/main/install.ps1 | iexkern runs inside WSL2, a real Linux kernel, so the isolation (namespaces + seccomp) and --cpus
cap work for real, --memory included: measured on a stock WSL2 kernel (6.18), a 128m box reads back
memory.max = 134217728. Where a host does not delegate the memory controller (a stock Raspberry Pi
OS, and older WSL2 kernels), kern warns and shows the one-line fix rather than pretending, see
Requirements & limitations. On a native Linux host --memory is enforced
out of the box. The installer ensures the WSL2 engine (self-elevating for the one reboot it may need, then
resuming on its own), imports kern's own pre-baked distro (a tiny Alpine + kern, no Ubuntu, no
manual steps), drops the kern.exe shim on your PATH, and verifies end-to-end. Every download is
sha256-checked. After it finishes: kern box dev --image alpine -it -- sh. Honest caveat: kern runs
inside the WSL2 kernel, so it doesn't shed the VM weight native Linux does; the win is "no Docker
Desktop", not "no VM".
Where the milliseconds go on Windows. The figures at the top of this page are Linux hosts. A command
typed on the Windows side spawns wsl.exe to cross into the distro, once per command, and that crossing
is not kern's work but it dwarfs kern's work. Measured on two Windows 11 hosts: 6.5 and 7.0 ms per box
typed inside the distro, against 70.5 ms per command through kern.exe. So run kern
inside the distro; use the bridge for the occasional command from a PowerShell you are already in, not for
a loop that starts hundreds of boxes. Your project can live on C: either way, that made no measurable
difference to box startup.
Benchmarks has the table, the variance, and what was
not measured.
If your antivirus deletes kern.exe. Some products remove an unsigned executable from
%LOCALAPPDATA% on sight; kern is not signed. The Linux side is untouched and still works, and the
installer also writes a kern.cmd companion that takes over automatically (PATHEXT resolves .EXE
before .CMD, so it is inert until the exe is gone), which keeps kern working in a new terminal.
It is a safety net, not a replacement, because cmd.exe is now in the path the exe was not: it does not
translate Windows paths (write -v /mnt/c/data:/data), it re-parses arguments so %VAR%, !, ^, &
and | are consumed before kern sees them, Ctrl-C on an interactive box asks "Terminate batch job (Y/N)?"
first, and it is not an executable, so the Python and Node SDKs run from Windows cannot spawn it. To
get the exe back, allow the folder the installer names in your antivirus and re-run the installer.
Throughout, wsl -d kern -- kern ... and the SDKs run inside the distro are unaffected.
📦 Offline / air-gapped (a board or locked-down server with no internet). kern is a single ~1.8 MB static binary, so copying that one file is the install:
scp kern pi@raspberrypi:~/ # then: ssh pi@raspberrypi kern box dev --image alpine -- shNo daemon, no package, nothing to install on the target, which is why it runs where Docker can't (see EDGE.md).
kern uninstall is a dry run by default: it lists every path kern created, with sizes, and marks
which of them are data you made rather than a cache it can refetch. Nothing is removed until you add
--yes.
kern uninstall # show what would go, remove nothing
kern uninstall --yes # do it
kern uninstall --keep-images # keep the image cache, remove the restIt refuses while boxes are running, and it only touches paths kern owns: the image cache, named
volumes, your kern.toml, the runtime state, units written by --restart, and the binary itself when
it sits where an installer put it. A [[disk]] you pointed somewhere is your data in your location and
is left alone.
On Windows the state lives in kern's WSL2 distro, so removal happens from PowerShell:
irm https://raw.githubusercontent.com/getkern/kern/main/uninstall.ps1 | iex # dry runThat prints what it found; the command it echoes performs it. It unregisters the kern distro, removes
the kern.exe shim, and takes its entry back out of your PATH. Your other WSL distros are not touched.
A sandboxed shell from any OCI image. The image stays read-only; your writes go to a scratch overlay that vanishes on exit.
kern box dev --image alpine -it -- shThat box starts in ~3.6 ms, not the ~2.2 ms of a bare rootfs box: an OCI image gets a rootless
uid-range mapping so an official image can drop privilege in its entrypoint, and that costs two
setuid helpers (~1.1 ms, run concurrently, unavoidable without CAP_SETUID). Measure it yourself
with kern bench --image alpine, or drop it with --no-uid-range if your workload stays root.
Hard memory + CPU limits (cgroup v2). kern run is the leanest path: a quota on a host command, no sandbox.
kern run --memory 256M --cpus 0.5 -- ./crunch-numbers
kern box build --image alpine --memory 512M --cpus 1.5 -v "$PWD:/src" -w /src --net -- makeDetached, a published port, restarts if it dies, health-checked, without a daemon.
kern box svc --image alpine -d -p 8080:80 --restart \
--health-cmd 'wget -qO- localhost:80' -- httpd -fkern ps # running boxes, with PORTS + HEALTH
kern top # live TUI: boxes, CPU/RAM, profiles
kern exec svc -it -- sh # shell into a running box
kern logs svc -f # its output, followed live (--tail N for the last N)
kern diff svc # files changed vs the image (C = changed, D = deleted)
kern update svc --memory 1g # retune caps live; rename/wait/events too
kern stop svc # stop it (kern stop --all for everything)Never baked into the image or env; delivered on a pipe, readable only inside the box. The network is
isolated by default, so --egress-allow opens exactly the one host it may reach (and wget is
Alpine's busybox built-in, no extra install):
printf "$DB_TOKEN" | kern box job --image alpine --secret TOKEN=- --cap-drop ALL \
--egress-allow api.example.com \
-- sh -c 'wget -qO- --header="Authorization: Bearer $(cat /run/secrets/TOKEN)" https://api.example.com/v1/me'kern doctor # will boxes even run on this host? preflight it
kern compose stack.toml # bring up a stack in dependency order (TOML or compose.yml)kern builds OCI images from a Dockerfile without a daemon: each RUN is a real kern box, each
step a content-addressed layer, reused on an unchanged rebuild.
kern build -t app:1 -f Dockerfile . # FROM RUN COPY ADD ENV WORKDIR USER CMD ENTRYPOINT SHELL …
kern build -t app:1 --build-arg VER=9 . # build args; multi-stage (FROM … AS b; COPY --from=b)
kern save app:1 -o app.tar # export a docker-load-compatible image tar …
kern load -i app.tar # … and import one (docker save format)
kern tag app:1 registry.example/app:1 # give a cached image a second name
kern commit devbox warmenv:1 # snapshot a running box's fs into a reusable image
kern login registry.example # (private) creds stored 0600
kern push registry.example/app:1 # publish as a single-layer OCI imageWarm start (kern commit). Bake an expensive one-time setup (apt/pip installs, a warmed cache,
compiled artifacts) into a local image once, then start the next box from it instantly. It reads the
box's kernel-merged overlay through /proc/<pid1>/root, so whiteouts are already resolved, and skips
every nested mount, so a -v volume or a secret is never baked into the image. It's docker commit,
daemonless. A filesystem snapshot, not live memory: processes restart fresh (write state to disk if you
need it back).
kern parses real-world Dockerfiles as-is (comments inside \ continuations, SHELL, BuildKit
RUN --mount/ADD <url> with --checksum/--chmod, COPY <<heredoc, FROM scratch, # escape
and BOM) and honours .dockerignore (also .kernignore), so a COPY . /app won't bake your
.git, .env or secrets into the image. Multi-stage builds run each stage in its own box and
confine COPY --from=<stage> to that stage's filesystem (a hostile source path or symlink can't read
the host). Layers pull as gzip or zstd. push normalizes ownership and strips setuid/setgid, so an
untrusted base can't smuggle a privilege-bit into what you publish. (build/push are the newest
surface, see Project status.)
Run a sandboxed command straight from your program: a fresh isolated box per call (untrusted code, agent tools, per-request workers), and a structured result back, including rich mime-typed results (charts, tables, the last expression) the way a Jupyter cell returns them, but local and daemonless.
from kern_sandbox import Sandbox # pip install kern-sandbox
with Sandbox(memory_mb=512, network=False) as s:
print(s.run_code("print(6 * 7)").stdout) # 42import { withSandbox } from "kern-sandbox"; // npm install kern-sandbox
await withSandbox({ memory: "512m", network: false }, async (s) => {
console.log((await s.runCode("print(6*7)")).stdout); // 42
});use kern_isolation::Sandbox; // the crate the CLI itself uses
let out = Sandbox::builder().memory_max(512 << 20).build()?.run("echo", &["hi"])?;Pure standard library on both sides, no transitive dependencies: the bindings shell out to the same
kern binary you already have. Full API, limits, file transfer and the rich-result shape:
Python · Node ·
agent-code-interpreter.py.
Linux, multi-architecture. Prebuilt static (musl) binaries for linux-x86_64 and
linux-aarch64: one ~1.8 MB file, no Rust deps beyond libc (the pull path shells out to system
curl/tar).
| Platform | Arch | Status |
|---|---|---|
| x86_64 Linux | x86_64 | ✅ primary + automated CI |
| Windows 10/11 (via WSL2) | x86_64 | ✅ CI-built shim + distro (install.ps1) |
| NVIDIA Jetson (L4T) | aarch64 | ✅ manually validated |
| Raspberry Pi 5 | aarch64 | ✅ manually validated |
| Arduino UNO Q (Android kernel, Debian userland) | aarch64 | ✅ manually validated |
kern needs a Linux kernel with unprivileged user namespaces + cgroup v2, and a Linux
userland. The kernel flavor doesn't matter: kern runs even on an Android kernel with a Linux
userland (the Arduino UNO Q). On Windows, WSL2 is that Linux kernel, and the one-line PowerShell
installer sets up WSL2 and drops in a pre-baked kern distro, so isolation, --cpus and --memory are
all enforced for real (measured on a stock 6.18 WSL2 kernel: a 128m box reads back memory.max = 134217728). Honest
caveat: you're inside the WSL2 VM, so it's "no Docker Desktop", not "no VM". kern does not run on stock Android-the-OS (Bionic, SELinux, userns off). Daemonless is a big
win on RAM-constrained boards (0 resident vs ~186 MB), see EDGE.md. ARM CI is tracked
in the issues.
kern speaks Docker's formats, so your existing images and stacks just work, but it does not reimplement the Docker Engine API. It's a lightweight alternative, not a drop-in clone.
| From your Docker setup | kern |
|---|---|
| OCI images (Docker Hub, GHCR, quay, Harbor, self-hosted) | ✅ pull & run: multi-arch, WWW-Authenticate v2 auth, gzip + zstd |
docker-compose.yml |
✅ kern compose <file> [up|down|stop|start|restart|ps|logs|build|pull|config] reads real-world files as-is: depends_on (+ service_healthy/_completed conditions), healthcheck, deploy.resources.limits, ulimits, sysctls, labels, extra_hosts, init, stop_signal/stop_grace_period, YAML anchors/merge (<<: *x), extends, x- extension fields, the project .env, ${VAR:-default} and bare $VAR interpolation, network aliases. Multiple files merge (-f base.yml -f override.yml), plus -p/--env-file/--profile. up reconciles: a service still matching the file is left running, a changed one is recreated |
Dockerfile build |
✅ kern build: all common instructions, multi-stage, COPY --from=… (a build stage or an external image), COPY globs (*.txt, src/*, [ab].conf), BuildKit heredocs, ADD <url> (+ --checksum/--chmod), COPY --chmod (recursive, Docker-parity), FROM scratch, SHELL, # escape/BOM, --build-arg, a whole-build cache, and honours .dockerignore. Daemonless: each RUN is a real box. The cache is keyed on the whole Dockerfile + context, NOT per layer as Docker's is: an identical build is reused (2040 ms to 24 in one measurement), and changing any instruction re-runs from the first, including steps before the edit |
.dockerignore (also .kernignore) |
✅ excluded from the build context: keeps .git/secrets out of the image (last-match-wins, ! re-include, **) |
docker save / load archives |
✅ kern save / kern load: export/import an image tar, docker load-compatible |
tag / push to a registry |
✅ kern tag / kern push |
Image management (docker images / rmi / search) |
✅ kern images (list cached), kern rmi (remove, frees unshared layers), kern search (Docker Hub) |
docker commit (container → image) |
✅ kern commit <box> <image>: snapshots the box's filesystem to a reusable image (warm start); skips volumes/secrets |
Docker Engine API / docker.sock |
❌: tools that attach to the socket (Docker Desktop, some IDE/CI plugins) won't connect |
| Swarm (multi-host orchestration) | ❌ and there is no workaround: clustering, service replicas and rolling updates across machines are out of scope for a single-host, daemonless runtime. kern compose is one machine, one pod. |
One stack, one network namespace. The services of a kern compose stack share a
single network namespace, like the containers of a Kubernetes pod: they reach each
other by service name on 127.0.0.1, with no bridge, no IPAM and no DNS server.
That is what makes a stack start in milliseconds, and it has one consequence worth
knowing before you choose kern: two services cannot both listen on the same
container port, even when their published ports differ. Two apps that both default
to :3000 is the common case, so kern refuses it before starting anything and names
both services. The same applies to net.* sysctls, which belong to the namespace and
therefore to the whole stack.
Declare the port each service listens on and the conflict goes away:
services:
api: { image: node:20-slim, port: 3000 }
admin: { image: node:20-slim, port: 3100 }kern passes it as PORT, which most images read, and reserves it for that service, so
peers keep using the name (http://admin:3100) with nothing remapped at run time.
Docker's own expose: says the same thing and is honoured identically, so a stack that
already uses it needs no edit.
Three spellings, one space: ports: (published), port: (declared and passed as
PORT), expose: (declared only). A service that publishes nothing is visible to the
check only if it declares something. Every edge case is decided rather than left to chance
(a conflicting PORT= is refused by name, a range in ports: is expanded and checked
port by port, a range in expose: is never silently expanded, --no-pod lifts the
constraint entirely): compose-declared-ports.sh runs
through them.
One deliberate asymmetry: a malformed entry in your kern profile is refused with its
line number, while the same entry in someone else's docker-compose.yml is warned about
and skipped. Failing a whole stack over one line of documentation is the wrong trade for a
file kern did not write; for a file you did, a typo should be named at once.
kern compose <file> config prints what kern understood, reservations included, and
refuses exactly what up would refuse: a dry run that disagreed with the bring-up would
be worse than no dry run.
kern is daemonless, so after a reboot PID 1 starts, not kern. kern compose <file> systemd prints a
unit on stdout and installs nothing: where it belongs is a decision about your machine.
$ kern compose stack.yml systemd > ~/.config/systemd/user/kern-shop.service
$ systemctl --user daemon-reload && systemctl --user enable --now kern-shop.service
$ loginctl enable-linger $USER # or the unit stops when you log outIt does not supervise. The unit brings the stack up and tears it down; a service that dies an hour later is not restarted, and the generated unit says so in its own comments rather than letting you assume otherwise. Walk-through: compose-systemd-unit.sh.
Most container-lifecycle verbs you type daily have a 1:1 kern equivalent (same name where it makes sense):
docker … |
kern … |
Notes |
|---|---|---|
run / create |
box |
one verb; -d detaches, -it for a PTY |
exec |
exec |
joins the box's namespaces |
ps |
ps |
-q, --filter name=/status=/id=, --format '{{.Field}}', --json |
logs |
logs |
--tail N, -f/--follow (bounded read, cheap on GB-size logs) |
stop / kill |
stop / kill |
SIGKILL the box's process group |
pause / unpause |
pause / unpause |
cgroup v2 freezer |
attach |
attach |
Ctrl-C detaches, box keeps running |
cp |
cp |
host↔box, symlinks can't escape the box root |
inspect |
inspect |
--json |
stats |
stats |
per-box CPU / memory |
top (box processes) |
exec <box> ps |
plus kern top, the live TUI for every box |
rename |
rename |
in place, pid unchanged |
update |
update |
live cgroup caps, no restart (needs a delegated cgroup) |
wait |
wait |
prints the exit code (137 after stop) |
diff |
diff |
overlay-upper changes: C changed/added, D deleted |
events |
events |
poll-based stream (start/die/rename); daemonless, best-effort |
commit |
commit |
box → reusable image (warm start) |
start (resume a stopped container) |
(none) | a box can run as long as you want (detach with -d --restart for a DB or server that stays up for days) and its volumes persist on disk; what's not supported is resuming a box you already stopped - you launch a fresh one that re-attaches the same volume |
Multi-service stacks are supported: kern compose reads your docker-compose.yml and brings the
services up in a pod, with depends_on ordering and healthchecks, daemonless. What needs a daemon
does not exist here: swarm / service / stack, docker.sock, and anything that attaches to it.
✅ Use kern when you want:
- a fast, daemonless sandbox for your own or agent/LLM-generated code (fresh box per call, embeddable from Rust/Python);
- CI / build boxes, or a throwaway dev environment, without a background daemon;
- containers on edge / ARM boards where a Docker daemon is too heavy or absent (Pi, Jetson, Android-kernel);
- resource slices beyond containers:
vcpu:/vdisk:/vgpio:(GPU on the roadmap).
❌ Do NOT use kern when you need: (reach for the tool named after each)
- a hard boundary against actively hostile multi-tenant code → a microVM (Firecracker) or gVisor. kern's FS/PID/namespace/cgroup isolation is a real kernel boundary for your own or semi-trusted code, not a VM;
- the Docker Engine API / Docker Desktop workflow, or a true CLI drop-in → Docker / Podman;
- network isolation between the services of one stack, or
deploy.replicas→ Docker Compose or Kubernetes. Akern composestack is one pod on one machine: services share a namespace and reach each other by name, which is what makes it start in milliseconds and is also its ceiling; - Kubernetes CRI integration → containerd / CRI-O;
- a low-level OCI runtime to slot under containerd/podman (the runc layer) → crun, youki (also Rust), or runc. kern isn't a runc-replacement; it's the whole daemonless UX (pull, build, run, compose) in one binary, not a runtime another engine drives.
kern states every boundary that is cooperative or opt-in plainly in SECURITY.md; being honest about the edges is the point. You do not have to take any of it on trust: the four
adversarial suites in pentest/ assert those boundaries against the kernel rather than
against kern's own reporting, and sh pentest/run-with-local-registry.sh ./target/release/kern pentest/pentest-ports.sh runs them with no registry account and no network.
kern · one static binary, no daemon box · run · compose · exec · pull · top …
│
▼
runtime (kern-isolation)
├─ namespaces user · pid · net · mnt · uts · ipc
├─ rootfs OCI overlay → pivot_root (typestate: Mounted → OldRootReady → ReadOnly)
├─ devices fresh /dev · vgpio passthrough · -v volumes (symlink-safe)
├─ cgroups v2 MemoryMax · CPUQuota · TasksMax
├─ seccomp always-on denylist (+ wrong-arch / x32)
└─ supervisor fork → PID 1 → reap → exec / stats / stop
│
▼
images (kern-oci) registry v2 · sha256 per blob · in-process tar vetting
A kern box is one short-lived process tree: no daemon, no shared state.
- Namespaces.
unshareinto a fresh user + PID + UTS + IPC namespace (and, by default, an isolated loopback-only net namespace;--netshares the host's, opt-in, flagged in the status panel). A single-uid map makes your uid root inside the box only;--uid-rangeopts into a full sub-id range. - Root filesystem. An overlay by default (image = read-only lower, a private upper takes
writes);
--read-onlyremounts it read-only after a self-pivot (pivot_root(".", ".")), which works even where a bind remount-RO is denied (some Android-kernel boards). Nothing is written into the rootfs, so many boxes share one read-only rootfs concurrently. (--bind-rootfsswaps the overlay for a direct bind: faster on a slow overlayfs, at the cost of a mutable shared source.) - Devices, volumes & secrets. A fresh
/devwith the safe nodes (+ /dev/net/tunon--tun);-vvolumes bound in with targets resolved symlink-safely, confined to the new root; secrets on a RAM/run/secrets(0400);vdisk:/vgpio:mounting exactly their declared disk/peripherals. - Lockdown. A clean env (no host secrets leak in), capabilities stripped to least-privilege, an
optional
--userdrop, an always-on seccomp denylist (incl. wrong-arch + x32), and cgroup caps:
kern box <name> --plan prints the exact sequence for your invocation, without running it: that
output is generated from the code, so it cannot drift the way a description here would.
See ARCHITECTURE.md for the design and SECURITY.md for
where each boundary is real, cooperative, or opt-in.
One isolated /bin/true, warm image cache, one box per run. The x86_64 row below was measured on an
Intel i7-14700KF (20-core / 28-thread), Linux 7.0, NVMe, systemd-user desktop, which is the same
machine the hero and demo numbers come from, in one sitting against the runtimes installed there
(exact per-runtime commands in BENCHMARKS.md). Your numbers vary with hardware,
kernel and load, so measure your own with kern bench --rootfs <dir>; board rows are from on-device
runs.
Cold start is quoted everywhere because it is easy to measure, but nobody notices 300 ms once. What a
developer feels is the twentieth exec of the afternoon. Same machine, same images, real commands,
timed with the shell:
| what you actually type | kern | docker | podman |
|---|---|---|---|
a throwaway box (run … true) |
3.6 ms | 292.9 ms | 287.5 ms |
exec into a running service |
1.2 ms | 42.2 ms | 150.6 ms |
list what is running (ps) |
0.7 ms | 8.2 ms | 15.3 ms |
| read logs | 0.7 ms | 8.3 ms | 39.9 ms |
| stop a service (init handles SIGTERM) | 4.7 ms | ~300 ms | |
| bring a 2-service stack up | 185 ms | 301 ms |
Reproduce any row with
time, on both sides. No script of ours is involved.
One row needs a caveat we would rather state than be caught on. Stopping a service whose init does
not handle SIGTERM takes 10 s on Docker and Podman, and 4.7 ms on kern. That is not Docker being
slow: a PID-namespace init discards signals it has no handler for, so the container genuinely cannot
die of SIGTERM, and waiting the full grace before SIGKILL is the correct, documented behaviour. kern
reads /proc/<pid>/status first and skips a wait that provably cannot end. The honest comparison is
the row above, with an init that does handle the signal: 4.7 ms against ~300, for the same reason every
other row is fast, not because of a container that ignores signals.
| host | kernel | kern | bubblewrap | runc | docker |
|---|---|---|---|---|---|
| x86_64 desktop | v7.0 | 2.6 ms | 2.9 ms | 13.8 ms | 292.9 ms |
| Raspberry Pi 5 | v6.6-rpi | 11.8 ms † | not installed | not installed | not installed |
| Jetson Orin Nano | v5.15-tegra | 12.5 ms † | 5.6 ms | 32 ms ‡ | 472 ms ‡ |
| Arduino UNO Q | v6.16 Android | 91.5 ms † | 15.0 ms | 76 ms ‡ | 858 ms ‡ |
kern and bubblewrap were measured together on 2026-07-30, three repeats each, median of medians, on an idle host. ‡ = runc and docker are carried over from the earlier round and were not re-measured that night. On the Pi 5, kern is the only runtime present at all: docker, podman, runc, crun, bwrap, nerdctl, lxc and systemd-nspawn are all absent, checked one by one. One ~1.8 MB static binary just works where the others are each a setup step (Docker alone is a ~186 MB daemon stack). That reach, not a latency crown, is what the boards are here to show.
† The kern column enforces resource caps and the bubblewrap column does not, because bubblewrap has no
caps to enforce. memory.max was read back as 268435456 inside the box on every host in the table. The
boards give kern no delegated cgroup slice, so the only way to make a limit bite there is a systemd
transient scope, and that scope is the entire difference: systemd-run --user --scope /bin/true alone
costs 9.4 ms on the Pi, 9.0 on the Jetson and 59.9 on the Arduino, against gaps of 9.2, 9.3 and 58.2 ms
between kern's capped and uncapped runs on those same boards. The arithmetic closes to within 1.7 ms
everywhere, which is why the boards' figures are a fallback path rather than the engine.
The toll is avoidable, and that is the point. An SSH login sits under the SYSTEM systemd manager
while kern's delegated kern.slice lives under the USER manager, and cgroup v2 will not migrate a
process across that boundary. Enter the user manager's tree once and every box after it takes the
direct path, caps still enforced:
systemd-run --user --scope bash # pay it once, then run kern inside that shell| board | as an SSH login | inside one scope | + --bind-rootfs |
|---|---|---|---|
| Raspberry Pi 5 | 11.8 ms | 3.0 | 2.8 |
| Jetson Orin Nano | 12.5 ms | 4.6 | 4.2 |
| Arduino UNO Q | 91.5 ms | 35.5 | 11.3 |
All four columns come from ONE sitting per board, so they compare with each other; the small drift against the table above (11.7 vs 11.9 on the Pi) is two rounds an hour apart, not a change.
Eight times faster on the Arduino, four on the Pi. kern doctor measures the toll on your host and
prints that command. It also settles the bubblewrap comparison on the boards: kern enforcing a memory
limit is 4.2 ms against bubblewrap's 5.6 on the Jetson and 11.3 against 15.0 on the Arduino,
while bubblewrap enforces nothing.
At the same level of work, from the same round:
| board | kern, cgroup off | bubblewrap | |
|---|---|---|---|
| x86_64 desktop | 2.2 ms | 2.9 ms | kern |
Jetson Orin Nano, --bind-rootfs |
3.5 ms | 5.6 ms | kern |
Arduino UNO Q, --bind-rootfs |
9.6 ms | 15.0 ms | kern |
Raspberry Pi 5, --bind-rootfs |
2.3 ms | not installed |
--bind-rootfs is the like-for-like flag because bubblewrap binds rather than overlays; comparing kern's
default overlay against it compares two mount strategies, not two runtimes. It is worth reaching for only
on the Arduino, whose Android kernel spends 22.4 ms in the overlay mount alone against ~0.1 ms on
x86: it takes that board from 91.7 ms to 69.2 with caps enforced, and from 33.5 to 9.6 with the cgroup
off. Elsewhere it is worth a few tenths. Every row in the first table is the default path, so the boards
are compared with each other rather than each with its own best flag.
Wherever kern can take its direct cgroup path it does not pay the scope toll at all: 2.6 ms on x86
with the cap enforced, against 4.2 ms for systemd-run --user --scope /bin/true on that same machine,
which is the round trip kern is avoiding. 3.4 ms inside WSL2 with the cap enforced, re-measured on
2026-07-31; systemd-run does not exist there at all, so the direct path was already the only one.
kern is the fastest sandbox on the desktop at ~2.2 ms, ahead of bubblewrap at 2.9 by 0.7 ms, and the top tier is all within a few ms: nobody wins single-shot latency outright. The real gap is to the engines, ~125x faster than podman (~288 ms) and Docker (~289 ms), which round-trip a daemon every run. Beyond one start: ~457 boxes/s, ~2 MB RSS per box, 0 resident where Docker keeps ~186 MB before you run anything. Where the time goes, phase by phase: BENCHMARKS.md.
Reproduce with examples/benchmark.py (auto-detects the runtimes you
have). kern does less than Docker (no overlay networks yet, see Roadmap); this compares
the run path.
Runnable, live-verified scripts in examples/:
| Scenario | Example |
|---|---|
| A guided tour: a tool, your code, resource caps, untrusted code, a service | showcase.sh |
| Try to break out: an adversarial isolation battery + 50 boxes at once | hardening.sh |
Safely vet an untrusted curl … sh install script (no net, no host access) |
safe-install-script.sh |
| Per-job pipeline: read-only input → isolated processing → output | data-pipeline.sh |
| Build/test a repo in a clean box (laptop or on-device) | ci-in-a-box.sh |
| Many isolated services on a small board (few MB vs a 186 MB daemon) | edge-many-services.sh |
Head-to-head timing: kern vs docker run |
compare-vs-docker.sh |
…plus governed runs, port-published services, compose stacks and more, see examples/README.md.
kern trades breadth for a small, honest core. What it needs, and what it deliberately does not do:
Requires:
- A Linux kernel with unprivileged user namespaces + cgroup v2. On Windows it runs under WSL2; there is no native macOS/Windows port (Roadmap).
- Hard
--memory/--cpus/--pidscaps need a delegated cgroup (a systemd user manager, or root); without one they degrade to best-effort and kern says so. Microsoft's default WSL2 kernel and a stock Raspberry Pi OS, and WSL2 kernels older than the current one) don't delegate thememorycontroller, so--memoryis accepted-but-unenforced there (same as Docker/Podman) until you enable it. A current WSL2 kernel does enforce it, measured. To enable it where it is missing: on WSL, addcgroup_enable=memory cgroup_memory=1tokernelCommandLineunder[wsl2]in%UserProfile%\.wslconfig, thenwsl --shutdown; on Raspberry Pi OS, addcgroup_enable=memory cgroup_memory=1to/boot/firmware/cmdline.txt, then reboot. newuidmap+/etc/subuidfor a full uid range (--uid-range,--ssh); a single-uid box works without them.
Deliberately not here:
- Not a microVM, not for hostile multi-tenancy. A kernel vulnerability isn't contained: kern is a kernel-boundary sandbox for your own or semi-trusted code. When to reach for a microVM (Firecracker) or gVisor instead is spelled out in When to use kern (and when not) and the threat model.
- No overlay / software-defined networking (a box gets an isolated netns, or the host's; a pod shares one) and no Docker plugin ecosystem.
kern execcaps are inherited only where kern can join the box's cgroup (root, or a delegatedkern.slice); on a rootless per-box-scope host the exec'd command runs outside the box's--memory/--pids(namespaces + seccomp still isolate it), and kern warns.- GPU slices are on the Roadmap, not shipped.
0.6.30. Everything in Features works today and is tested (689 Rust, 62 Python and 51
Node tests; clippy-clean, cargo-deny-clean, adversarially reviewed slice by slice); the isolation is
real. kern trades Docker's breadth (overlay networks, a plugin ecosystem) for a small, fast core that
starts in single-digit milliseconds from one ~1.8 MB binary. Versioned under semver: each release is the official
build of that version, and pre-1.0 means only that the CLI and config surface can still change between
minor versions, always called out in CHANGELOG.md.
What kern knows it does not know, or does not do yet, is written down in OPEN_ITEMS.md rather than left for you to discover: a gap in a startup measurement we have not attributed, why the seccomp filter is still a denylist, which fleet limit is a guard rail instead of a boundary. Declared debt is cheaper than silent debt.
Recently: 0.6.30 is a correctness release rather than a feature one. An OCI whiteout that could
not be applied was reported as applied, so a file an image declared deleted could stay in the rootfs
with nothing saying so; image content is now deleted by a no-follow tree walk, directory modes are
changed by descriptor rather than by name, a cache entry counts as complete only when its rootfs is
there too, an absent exit code from a box is no longer read as success by the Python and Node
bindings, and no unwrap, expect or panic! is left in production code. Before that: an
explicit-backend resource-profile schema; kern exec joins the box's cgroup so an
exec'd command inherits its --memory/--pids caps (with an honest warning where a rootless per-box
scope can't be joined); a warm kernel and an MCP server (kern-mcp) for the Python/Node
bindings; kern commit warm-start snapshots; an --egress-allow allowlist and --landlock-rw; and
daemonless image build / tag / push with a Dockerfile/compose parser checked against a
real docker build. Full per-release detail in CHANGELOG.md.
Deliberately not here yet: the headline GPU slices (on the Roadmap) and Docker-style overlay networking.
kern starts as a small, fast sandbox/OCI runtime and grows deliberately: the resources it governs are driven by what proves useful. These are directions under consideration, not commitments or dates, and some may never ship if they would change what kern is. Recently shipped work is under Project status, not here.
- GPU slices. A workload gets a slice of a GPU, not the whole device. Not shipped, and this README will describe it when it is, not before. Nothing here touches a GPU today.
- More governed resources. The same profile model could extend to other cgroup or kernel-real resources (I/O bandwidth, network shaping) as they prove useful.
- Snapshot / warm-start (CRIU). Same-host checkpoint and restore of a warm box for subsecond restarts. Feasible but gated: rootless CRIU needs a capability and suspending the seccomp filter, so it would be an explicit opt-in mode, not the default, and only for same-host, non-GPU boxes. Not committed.
- macOS. No native port, and it is a non-goal: a daemonless kernel + cgroup sandbox has no macOS equivalent. The only path considered is a thin shim over a Linux VM, the same shape as WSL2.
- 1.0, freeze: CLI + config under semver, threat model + architecture finalised.
In progress
- Per-stack supervisor. Today
upcatches a service that dies at startup; one that dies an hour later is not detected. Under measurement on a Pi before it gets built.
Deliberately out, not missing
- Network segmentation between services,
deploy.replicas,docker.sock/ Engine API,--privileged. These follow from rootless + daemonless + one pod as the unit of isolation, not from missing work.
A stack is one pod. Within that model kern is complete: what is listed above as out is a consequence of the model, not a gap in it.
See ARCHITECTURE.md for the design.
kern isolates with Linux namespaces + seccomp + a self-pivoted root (a copy-on-write overlay by
default: the image stays immutable, writes land in a private upper discarded on exit, and --read-only
locks the whole root), a kernel-level boundary, deny-by-default on devices, with the host's sysfs/procfs
masked, and an opt-in Landlock (LSM) layer on top (plus an experimental egress allowlist). That's
strong for first-party and noisy-neighbour workloads. For adversarial, multi-tenant untrusted code where you want a
hardware-virtualization boundary, a microVM/VM adds a layer kern doesn't: a deliberate trade for
single-digit-millisecond starts and a ~1.8 MB footprint.
The full threat model, per-feature notes, and the honest "kern vs a microVM, when to use what" guidance live in SECURITY.md. Found a vulnerability? Report it privately via GitHub Security Advisories ("Report a vulnerability" on the repo); please don't open a public issue.
Issues and PRs welcome, see CONTRIBUTING.md. Contributions are covered by a lightweight CLA; the project follows a Code of Conduct. Security reports: follow SECURITY.md (don't open a public issue).
Apache-2.0, permissive, with an explicit patent grant. See NOTICE. The dependency
tree is deliberately tiny (libc only on the Rust side; standard-library-only on the Python side) and
copyleft-free; cargo deny check licenses is clean.
Trademark. The code is free under Apache-2.0: fork it, embed it, run it, no strings. The licence itself grants no rights to the project's names or logo (§6), so please don't use "kern" for a fork, a modified build, or a competing product or service without permission, see TRADEMARK.md. Open code, protected name, the same split Rust and Firefox use.
