isopod

isopod — Sandbox Breakout Security Assessment

Pre-publication assessment (2026-07-22). Scope: can code running inside an isopod guest microVM escape to, or harm, the host or other guests? Method: live breakout attempts against real sandboxes on this host + a multi-agent adversarial static review (25 agents, ~1.6M tokens) of every host-side code path that ingests guest-controlled data, with each finding independently verified or refuted. Findings are ranked by host impact; file:line references are to the tree at the time of assessment. Environment-specific values (host LAN IP, etc.) are this host's; the classes of finding are general.

Threat model & isolation boundary

Inside a guest, untrusted code runs as uid 0 (root), with all 41 capabilities, no in-guest seccomp, and a full devtmpfs (/dev/mem, /dev/vda, /dev/loop*, /dev/kmsg, …). This is by design — the guest is expendable and fully owned. The security boundary is therefore not inside the guest; it is:

  1. the Firecracker VMM + KVM (hardware virtualization),
  2. the host-side code that ingests guest-controlled bytes (the vsock RPC responses, the committed ext4 image, exec/serial output), and
  3. networking (the tap/nftables fabric).

A finding only matters if it reaches one of: host code-execution, host file read/write outside the VM, host denial-of-service, or cross-contamination of another guest or the shared stage/snapshot store.

Verdict

No guest→host escape or guest→guest breach was found, by either live testing or static review. The load-bearing control is correctly configured: Firecracker runs unprivileged with its seccomp BPF filter on and all capabilities dropped. The confirmed issues are denial-of-service, lateral-movement, and hardening gaps — not escapes.

Safe to publish the source (it is honest about the v1/v2 split), provided the README/security notes state the posture plainly and the must-fix items are tracked. Do not run genuinely hostile or multi-tenant workloads until the egress restriction (F1), the unbounded-log DoS (F3), and the jailer (F2) land.

What holds (verified secure)

# Property How verified
S1 VMM hardened: live firecracker runs as uid 1000, CapEff=0 (all caps dropped), NoNewPrivs=1, Seccomp=2 (BPF filter on). No --no-seccomp/--seccomp override. /proc/<fc>/status; process.rs
S2 Guest→host blocked on all tested ports (22/80/8080/111/445/3389) and ICMP, to both the tap gateway and the host WAN IP. live probe
S3 Guest→guest blocked, incl. two concurrent live guests (slot-1 prober could not reach a live slot-0 listener; all cross-guest connects timed out). live: two concurrent VMs
S4 --no-network airtight: no NIC, all egress fails; exec still works over vsock. live
S5 No host FS exposure: base image read-only at the VMM (/sys/block/vda/ro=1); no 9p/virtiofs/host mount in the guest; FC holds no writable host-FS handles. live + /proc/<fc>/fd
S6 No guest→host control channel: guest AF_VSOCK to the host (CID 2) is reset; MMDS not configured/reachable. live
S7 Resource requests bounded pre-boot: over-cap mem_mib/vcpus rejected cleanly, no boot, no host OOM. live
S8 Naming/commit injection-safe: commit_as labels with traversal / $(…) / backticks / --flags produced content-addressed st-<hex> ids + [a-z0-9-] names; the label is stored as pure metadata; no host artifact created. live + names.rs/stage.rs
S9 Commit never parses guest filesystems on the host: the guest ext4 is cp --sparse-copied and BLAKE3 content-addressed; the host never mounts / resize2fs / e2fscks it. (Independently re-verified; the "malicious stage attacks host tooling" hypothesis was refuted.) static (stage.rs)

Findings (ranked by host impact)

All nine, mapped onto the part of the boundary each one stresses. None is an escape — the verdict above holds. Note that three of them are not reachable from a guest at all: they are host-side governance and supply-chain gaps that a breakout assessment surfaced on the way past.

flowchart TB
    G["untrusted code, root inside the guest"]
    G --> L1["1 · Firecracker VMM + KVM<br/>the hardware boundary"]
    G --> L2["2 · host code ingesting guest bytes<br/>vsock RPC · stage image · exec and serial output"]
    G --> L3["3 · the network fabric<br/>tap devices and nftables"]
    L1 --> A["F2 single-layer isolation · MEDIUM<br/>F5 no I/O or network rate limiters · LOW-MED"]
    L2 --> B["F3 unbounded log tees · HIGH<br/>F8 unbounded vsock reads · MEDIUM<br/>F7 guest output reaches the agent · LOW"]
    L3 --> C["F1 egress to the private network · HIGH"]
    HOST["host-side only — no guest involvement"] --> D["F4 concurrent-VM RAM exhaustion · MEDIUM<br/>F6 lax artifact permissions · LOW-MED<br/>F9 kernel checksum unverified · LOW"]

F1 — Unrestricted egress to the host's private network — HIGH

The forward rule (net/setup.rs:244) accepts any tap-sourced packet routed out the WAN, with no destination filter, and the NAT masquerade covers the whole 10.107.0.0/16 supernet. A guest therefore reaches anything the host can route to, not just the public internet. Live: from a guest, the host's LAN gateway (an RFC1918 address) answered on 22/53/80/443 and was pingable; public egress and DNS worked normally. Independently confirmed by static review (HIGH).

Two related weaknesses in the same ruleset: - Source spoofing: the egress accept matches iifname only while the masquerade is gated on ip saddr 10.107.0.0/16, so a guest that spoofs a source outside its supernet is forwarded out eth0 with the spoofed source un-rewritten — it can inject spoofed-source packets onto the LAN/WAN. - IPv6 is only latently contained: the inet forward chain would accept un-NATed v6 tap→wan if v6 forwarding were ever enabled.

F2 — No second isolation layer (no jailer) in v1 — MEDIUM

Firecracker is not run under the jailer. The live process shares the host's root net/mnt/pid/user/cgroup namespaces and is not chrooted (root=/); FcProcess::spawn (process.rs:223) sets no setrlimit/cgroup. Its only defenses are its own seccomp filter and dropped capabilities.

F3 — Unbounded host-disk / host-RAM DoS via exec + serial log tees — HIGH

Guest exec stdout/stderr is teed 1:1 to ~/.isopod/vms/<id>/exec-*.log with no size cap (StreamSink::write, agent.rs:577; self.total is counted but never checked). The guest serial console is a second, independent unbounded sink teed to console.log (vm/console.rs, drain_to_log). VM directories and logs are explicitly retained ("keep every log regardless", vm/mod.rs:1031). Live: 50 MiB of guest stdout produced a 52,428,868-byte host file. Three amplifiers found by static review: - Host OOM on timeout: the host-wall-timeout path (capture_from_log, vm/mod.rs:~1703) does tokio::fs::read(whole_log) into a single Vec<u8> before truncating — a guest that streams a multi-GB log and never sends ExecDone forces the whole file into host RAM. - timeout_s is not upper-clamped (CLI/MCP), so one run can stream to disk for an arbitrarily long window. - The host never re-enforces EXEC_CHUNK_LEN (32 KiB) — only the 8 MiB MAX_FRAME_LEN — so a malicious guest agent can send 8 MiB frames to maximize fill throughput.

F4 — Host memory-exhaustion DoS via concurrent sandboxes — MEDIUM

resources.rs validates each VM's mem_mib against static MemTotal (minus 512 MiB headroom, hard-capped 4096) with no accounting of running VMs and no global governor. With 8 slots, up to 8 concurrent VMs × up to 4096 MiB (= 32 GiB) can be requested against a 5.9 GiB host.

F5 — No I/O or network rate limiters (claimed but not implemented) — LOW–MEDIUM

PLAN.md "Security posture" lists "per-drive/NIC token-bucket rate limiters as throttles," but every drive (Drive::virtio hard-codes rate_limiter: None, models.rs:173) and NIC (rx/tx_rate_limiter: None, vm/mod.rs:543) is unthrottled. The types exist; nothing sets them.

F6 — Lax permissions on host artifacts — LOW–MEDIUM (deployment-dependent)

~/.isopod/vms/<id> is created world-traversable 0755 (paths.rs) and logs (exec-*.log, console.log, firecracker.log) and snapshot files inherit the default umask (0644). The per-VM directory holding the Firecracker API socket and the vsock UDS is not 0700 and the sockets are not 0600, so their reachability by another local user depends on ambient umask (vm/mod.rs:~954).

F7 — Untrusted guest output reaches the orchestrating agent verbatim — LOW (agentic-sandbox-specific)

Exec stdout/stderr (head-capped 64 KiB, mcp/main.rs:164) and stored commit_as labels are returned verbatim to the driving agent (Claude) via MCP. Not a host escape, but a guest can emit crafted text attempting prompt injection of the orchestrator — precisely what the sandbox exists to contain.

F8 — Slot/VM leak via unbounded host-side vsock reads — MEDIUM

Neither read_frame variant applies a read/idle timeout (proto/frame.rs:81): read_exact(...).await blocks forever if the peer completes the vsock CONNECT handshake then withholds bytes. The post-exec teardown awaits halt without a timeout (let _ = vm.agent.halt(true).await;, vm/mod.rs:1619), and the run has no outer deadline.

F9 — Guest-kernel supply chain: checksum recorded but never verified — LOW

fetch_kernel (image/kernel.rs) downloads the CI vmlinux from S3 and computes its SHA-256 only to report it (FetchKernelOutcome.sha256) — there is no got != expected comparison, asymmetric with the SHA-256-pinned busybox/apk artifacts. Image fetches (S3, Alpine CDN) use default reqwest cert validation with no pinning.

Verified robustness / hardening gaps (INFO–LOW)

Confirmed real by static review; low host impact today, worth tracking:

Considered and dismissed (refuted on verification)

These plausible-looking hypotheses were checked against the code and found not to be host risks — recorded so they aren't re-raised:

Before publishing — checklist

Must state in the README/security notes (no code change required): - v1 is single-layer isolation (Firecracker seccomp + KVM only; no jailer), intended for single-tenant use. (F2) - Networked guests reach the host's whole routable network; use --no-network for untrusted code until egress is restricted. (F1)

Should fix before hostile/multi-tenant use: - F1 egress restriction + anti-spoof + v6 default-deny. - F3 exec/console log byte caps + bounded timeout read-back + timeout_s clamp. - F8 vsock read timeouts / outer run deadline (slot-leak DoS). - F2 jailer; F4 concurrent-VM memory governor.

Nice to have: F5 rate limiters (or correct the claim), F6 artifact/socket perms, F7 output fencing, F9 kernel checksum pinning, and the robustness list.

Appendix — how this was tested

Rendered from docs/security-assessment.md on the main branch.