Feature/microvm sandbox using libkrun - #372
Conversation
|
A concise summary of what is in this changeset for reviewers: Core microVM backend (
Scheduler isolation (fixes typing stutter when VM is busy)
Runtime diagnostics (
PTY integration tests (
Security hardening (from review)
Docs (
Two guest images: Debian (bookworm-slim) and Alpine (3.21) A couple of known caveats worth calling out in the PR description:
|
|
Ah that's a neat idea, it would be nice to be able to do clean isolation in a micro vm and just letting the agent go yolo. |
a8acae1 to
eb49831
Compare
|
it is so very close the main idea is there but there's some elusive rough edges, around the agent getting hung up by read/grep/write on host vs in the sandbox, since the file paths aren't isomorphic/symmetric
the rendering refactor you did earlier was a big help in catching some, but you'll see i really went deep into the edge case testing i need to read up on some ssh, tty, pty RFC's the ssh session still stutters a bit and seems a little sluggish through the PTY went as far as to consider loom, tsa+, prop testing here's a music video of doing an attach 2026-06-05.22-20-26.mp4 |
0877163 to
8e35f9f
Compare
Hardware-isolated sandbox backend using libkrun microVMs: - Runner binary (src/bin/dirge-microvm-runner.rs) boots KVM guest - SSH-based command execution via ssh2 crate + ephemeral ed25519 keys - virtio-fs workspace mirroring at /workspace - OCI image support: buildah for local images, pure-Rust puller for remote - Rootfs caching with CoW-optimized per-session clones - /sandbox slash commands: attach (PTY relay), snapshot, reboot - PTY relay for interactive SSH sessions with scheduler isolation - Config keys: sandbox.mode/image/cpus/memory_mib - Three built-in images: debian, alpine, dev (Rust toolchain) - Permission popup rendering fix (last_paint throttle + render_frame!) - Windows/Unix cfg-gating for all sandbox-specific code paths - CI: all 10 build variants pass (including Windows cross-build) - Docs: docs/microvm/* (8 files, ~47 KB)
765f7f4 to
fbed369
Compare
yogthos
left a comment
There was a problem hiding this comment.
Reviewed the whole thing locally (worktree off the PR head). It's a serious, well-documented piece of work — pure-Rust OCI pull with digest verification, ephemeral keys, snapshotting, the scheduler-isolation tuning, and a lot of tests. But there's one merge-blocker and a handful of security/correctness gaps that matter given the feature's whole point is isolation. Grouped by severity, with file:line.
Blockers
-
The rebase reverted #388 (the v0.3.1 unified rendering/input refactor).
src/ui/state.rsdrops the entireInputModestate machine (input_mode field, InputMode/QuestionState/PermissionState/DialogConfirm/DialogSelect), andsrc/ui/mod.rsreintroduces the four nested blocking modal loops (let decision = loopat ~2289,let answer = loopat ~3280 and ~3345,let accepted = loopat ~3423).grep -c "dispatch_modal\|input_mode" ui/mod.rs ui/state.rsis 0 on this branch vs 34 on main. Merging as-is re-breaks the questionnaire-freeze bug class and undoes shipped work. Needs a clean re-rebase that preserves #388 and re-applies only the microvm-specific UI bits (pty_relay, status badge, sandbox slash command) on top. -
src/sandbox/backend.rs(277 lines) is dead code.mod backendis never declared anywhere, so the file isn't compiled, tested, or used — the real exec logic is duplicated inline inSandbox::exec(src/sandbox/mod.rs:340). Either wire the trait in and delete the inline duplication, or delete backend.rs. Right now it advertises an abstraction that doesn't exist. -
No SSH host-key verification.
ssh_exec(src/sandbox/microvm/ssh.rs:182) handshakes and does pubkey auth but never checks the guest host key against the injectedHostKeyspubkey. The guest sshd is reached over a 127.0.0.1 ephemeral-port forward; on a shared host another local user can race/hijack that port and MITM the "sandbox" — feeding attacker-controlled output back to the agent and running the agent's commands in an attacker-controlled context. You already generate and inject a known host key, so pin it: comparesession.host_key()to the injected ed25519 key before userauth.
High
-
File tools bypass the sandbox entirely. Only
bashroutes through the VM (src/agent/tools/bash/mod.rs:174 -> Sandbox::exec). read/write/edit/apply_patch/list_dir/find_files all hit the host filesystem directly in every mode — grep for sandbox usage under src/agent/tools/ only matches bash. So in microvm mode the agent still has full host-FS read/write through tools, which undercuts the isolation claim. At minimum call this out prominently in docs/microvm/SECURITY.md as a non-goal; ideally confine the file tools to the workspace. -
The bash
timeoutis silently dropped in microVM mode.Sandbox::execignorestimeout_secson the SSH path (src/sandbox/mod.rs:354-406; same in the dead MicrovmBackend::exec). Only ssh.rs's 60s socket read-timeout applies, and only when the command is silent — a hung or slow-streaming guest command won't be killed at the configured timeout. Wrap the spawn_blocking intokio::time::timeoutand/or run the guest command undertimeout N. -
OCI blob size cap is bypassable. download_blob (src/sandbox/microvm/oci.rs:404) only checks Content-Length; a registry that omits it (chunked) skips the 2 GiB cap and
resp.bytes()buffers the whole body in memory. Stream with a running byte counter and abort past the cap. -
No OCI whiteout handling, and extraction leans on system tar for traversal safety. extract_or_cache_layer untars layers in sequence with no
.wh./.wh..wh..opqprocessing (src/sandbox/microvm/oci.rs:355), so a file a later layer deletes persists — incorrect layer composition. And a malicious/typosquatted image's../symlink members are only stopped by whatever the host tar does by default. Process whiteouts, and either extract with explicit traversal guards or document the image-trust assumption.
Medium
-
Runner stderr is discarded then "read" in dead code. mod.rs spawns the runner with
.stderr(Stdio::null())(src/sandbox/microvm/mod.rs:230) but the crash path readschild.stderr(line 287) — always None. Every assert!/expect! in dirge-microvm-runner is invisible; the user just gets "(empty)". Use Stdio::piped(). -
rootfs base-cache has no locking or atomicity. prepare() does
if !cached_base.exists() { pull }(src/sandbox/microvm/rootfs.rs:44). Two concurrent sessions race into the same base dir, and a pull that fails midway leaves a partial base that later runs treat as valid. Build into a temp dir + atomic rename, guarded by a lock file. -
Predictable temp dirs in shared /tmp. ssh.rs temp_dir() and prepare_local use
temp_dir().join("...-{pid}-{nanos}")+ create_dir_all, not 0700 mkdtemp (src/sandbox/microvm/ssh.rs:120, rootfs.rs:74). Predictable names in a world-writable dir invite symlink/pre-creation attacks. Use tempfile's mkdtemp. -
Port-map bind scope unverified. The runner maps host:ssh_port -> guest:22 (src/bin/dirge-microvm-runner.rs:88). If libkrun binds 0.0.0.0 rather than 127.0.0.1, the guest sshd is network-exposed. Confirm/document localhost-only (and pin the host key per #3 regardless).
-
set_microvm_image / set_microvm_resources / ssh_connect_info use try_lock() and silently no-op on contention (backend.rs, but the same pattern would apply if wired). Switching image while the VM is busy silently does nothing — surface a busy error.
Low / polish
- sandbox-microvm isn't in the CI matrix, so none of this (including the dead backend.rs) is built in CI. Add at least a build-only job so it doesn't bit-rot.
- validate_snapshot_name (mod.rs:400) blocks
/,\,..,empty but allows "." and control chars — tighten to an allowlist. - Minor TOCTOU on the ephemeral SSH port (bind -> drop -> re-bind in the runner). Acceptable, just noting.
Overall: the OCI/rootfs/ssh plumbing and the docs are strong. Please re-rebase to restore #388 first (that's the hard blocker), then the host-key pinning and the "file tools aren't sandboxed" caveat are the two I'd want resolved before this lands as a security feature.
689c11b to
3dc7cea
Compare
… timeout, locking, temp dirs Phase 1 — Hard Blockers: - Rebase to restore dirge-code#388 InputMode state machine (dispatch_modal/render_frame) - Delete dead src/sandbox/backend.rs (trait never wired in) - SSH host-key verification: compare guest ed25519 host key after handshake Phase 2 — High Severity: - Document file-tool sandbox gap in SECURITY.md + startup warning - Wire bash timeout: timeout<N> prefix + tokio::time::timeout around spawn_blocking - OCI blob size cap: stream chunked responses with running counter - OCI whiteout handling: process .wh.<name> and .wh..wh..opq after layer extract - OCI tar safety: --no-absolute-filenames, reject .. path traversal Phase 3 — Medium Severity: - Pipe runner stderr (was Stdio::null(), now Stdio::piped()) - Rootfs cache: lock file + atomic rename via staging directory - mkdtemp: replace PID-based temp dirs with UUID-based names - Document krun_set_port_map 127.0.0.1 bind scope - Surface try_lock errors on config setters (return Result) Phase 4 — Polish: - Add sandbox-microvm to CI build matrix - Tighten snapshot name validation to allowlist [a-zA-Z0-9._-]+ - Document ephemeral port TOCTOU (acceptable risk)
Phase 2.2: Add timeout_kills_long_running_command integration test - Boots microVM, runs sleep 300 with 2s timeout, verifies prompt return Phase 1.3 bugfix: session.host_key() returns SSH wire-format blob (51 bytes for ed25519), not raw key. Add extract_ed25519_raw_key() to parse wire format and compare raw keys correctly. Add 4 unit tests. Phase 3.1: Add runner_stderr_captured_on_crash test - Spawns runner with garbage JSON, verifies stderr is captured
a567c02 to
88a4300
Compare
…diation changes - Replace dead backend.rs/MicrovmBackend references with Sandbox::exec dispatch - Document host-key verification step in ssh_exec and SSH handshake - Document dual-layer command timeout (guest-side timeout + tokio::time::timeout) - Document rootfs cache advisory lock and atomic staging → base rename - Document OCI streaming byte counter cap for chunked-encoded responses - Correct runner line count (~200 → 109) - Update cache directory layout to include .lock and .staging/
|
Very cool, love the video of it in action, and glad the UI refactor helped. I should have a chance to review and merge it in later today. |
copy_file_range (rootfs reflink) and the runner's libkrun calls are Linux-only but weren't cfg-gated, so --features sandbox-microvm and --all-features failed to build off Linux. Gate both to cfg(target_os = "linux") — std::fs::copy fallback for file copies, a Linux-only stub main for dirge-microvm-runner — and gate the two reflink unit tests that exercise the real syscall. Runtime still needs Linux + KVM; this is a build-portability fix only. Fixes the bug filed against the v0.4.0 microVM feature (#372).
this one is a bit involved but i wanted to get the idea out here it takes some of my learnings from zypi, kvm-shim, brood-box, go-microvm, and applies them to dirge
a few issues to iron out, and some force pushes to clean up my messy rebasing today
but the idea overall is taped together decently, its a step up in isolation compared to
bwrapwhich suffers from the same issues linux cgroups do, in that its monolithic and shared kernel so a container escape in bwrap would place me on a host systemwrapping the sandbox into a lightweight vm like this with virtiofs theoretically would add a harder boundary around bash/sh stuff mitigating the need to have a million+ regex gvisor like things
i'm not sure about soft linking, symlinking between virtio-fs.
the other thing with this is that the read/write pathways are not sandboxed so they still read off host system same as !blah type commands
i tried to abstract some things out so i could attach a PTY for test harness (specifically load testing
pty_harness.rsbetween the dirge tty over ssh to the running vm)sandboxed bash runs:

example of
!uname -aand dirge comparing bash things in the sandbox:example of ssh into the running sandbox vm