Skip to content

Latest commit

 

History

History
218 lines (196 loc) · 38 KB

File metadata and controls

218 lines (196 loc) · 38 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Security

  • Daemon default listener is now AF_UNIX (0o600), TCP is opt-in with bearer-token auth. voidbox serve no longer binds 127.0.0.1:43100 by default. The daemon resolves a per-uid AF_UNIX socket path via the chain $XDG_RUNTIME_DIR/voidbox.sock$TMPDIR/voidbox-$UID.sock/tmp/voidbox-$UID.sock and binds it with mode 0o600; the voidbox CLI client consults the same chain so a same-uid invocation auto-discovers the socket. To opt back into TCP, pass --listen tcp://host:port and provide a bearer token via --token-file, VOIDBOX_DAEMON_TOKEN_FILE, or VOIDBOX_DAEMON_TOKEN; if none is set, the daemon generates a 32-byte hex token and writes it to $XDG_CONFIG_HOME/voidbox/daemon-token (default ~/.config/voidbox/daemon-token, mode 0o600); the voidbox CLI reads from this same path as a tier-3 fallback below the env vars, so the typical same-host TCP case auto-discovers the token with no further configuration. The daemon refuses to start a TCP listener with no token. All routes (including POST /v1/runs, GET /v1/runs/{id}/telemetry, .../stages/{name}/output-file, POST .../cancel, POST .../messages) inherit the bearer-token gate from a single chokepoint at the top of route_request; comparison is constant-time via subtle::ConstantTimeEq. Closes the local cross-user RCE described as R-B4.1 / T-B4.1 in the threat model. Migration: scripts that passed --listen 127.0.0.1:43100 should now pass --listen tcp://127.0.0.1:43100 and configure a token; same-uid clients work without further action.

Added

  • TCP port-forward rules accept a host port of 0, meaning "bind an OS-assigned free port". The resolved (host_port, guest_port) pairs are reported by the new SlirpBackend::port_forward_listener_ports(). This also deflakes tcp_port_forward_inbound_connect_succeeds (#129): the affected tests now bind port 0, removing the fixed-port collision (a foreign listener could absorb the test's connect while the rule's bind failure is warn-only), and the test's second drain pass now applies full contract matching — a synthesized SYN surfacing there was previously discarded unrecognized, burning the test's deadline.
  • Experimental credential proxy — keeps the real Claude API key off the guest (RFC-0002 milestone 0). New and opt-in: off by default, enabled per run with credential_proxy: true (YAML llm.credential_proxy, or the builder). Existing behavior is unchanged — without the flag, provider credentials are staged into the guest exactly as before. When enabled (Claude provider, Linux/KVM only; fails closed on macOS/VZ where the listener cannot yet be bound guest-only), ANTHROPIC_API_KEY is withheld from the guest and injected host-side at egress by a per-run, TLS-terminating proxy (src/proxy/): the guest holds only a non-secret placeholder, a per-sandbox name-constrained CA (installed via NODE_EXTRA_CA_CERTS), and a per-sandbox proxy token; the proxy checks the token, rewrites the credential header with the host-held key, and re-originates to the real upstream over fresh TLS. An automated check asserts no real credential reaches the staged guest env or files, gating the feature. Injection is a replace, not an add — the injector substitutes an existing credential header (the placeholder) and never introduces the secret into a request that carried none, so the key is never attached to an endpoint that did not present a credential. Upstream connections are SSRF-pinned (resolve once, reject internal ranges; a host HTTPS_PROXY cannot route around it). As milestone 0 it carries documented reduced-posture deviations (in-process TLS/HTTP parser; the per-sandbox token is the sole cross-sandbox control on KVM until the egress network rule lands; Claude Code's untokened control-plane traffic to api.anthropic.com is not yet captured — tracked in #124). The Anthropic-compatible Custom provider and codex follow in M1; OAuth in M1a.
  • aarch64/KVM guest support (RFC-0003, #114). VoidBox guests now boot on arm64 Linux/KVM hosts, at parity with x86_64/KVM and macOS/VZ — the conformance, oci_integration, e2e_mount, e2e_telemetry, and e2e_skill_pipeline suites all pass there, and the smoke spec runs real Claude end to end. The loader inflates gzip-compressed arm64 Images (distro /boot/vmlinuz has no self-decompressor; bounded at guest-RAM size) and places kernel/initramfs from the Image header's text_offset/image_size with checked arithmetic; the generated DTB describes the full platform (GICv3 with a GICv2 variant chosen by a KVM_CREATE_DEVICE_TEST probe, PSCI 0.2 vCPUs with powered-off secondaries, an ns16550a UART at 0x0900_0000 so console=ttyS0 works unchanged, per-device virtio-mmio nodes); IRQ injection uses the arm64 KVM_IRQ_LINE packing; guest shutdown (VcpuExit::SystemEvent) stops the VM. Device MMIO windows and interrupt numbers derive from a shared per-arch slot table; x86_64 values and the x86_64 kernel cmdline are byte-identical, pinned by unit tests. Previously, arm64/KVM boots stalled before the control channel came up (#114).
  • VOID_BOX_CONNECT_DEADLINE_SECS — opt-in override that extends (never shortens) the control channel's 30 s connect/handshake deadline, for validation environments where guest boot legitimately exceeds it (e.g. production-size initramfs under nested virtualization). Unset means exactly the previous behavior.
  • aarch64/KVM device discovery convention (RFC-0003, ADR-0008): devices are declared as virtio-mmio DTB nodes (no virtio_mmio.device= cmdline args, which cannot express GIC SPIs), and voidbox.network=1 — previously VZ-only — is the platform-neutral guest-agent network marker.
  • aarch64 vCPU ceilings derived from the guest memory map (ADR-0007): at most 123 vCPUs (GICv3 redistributor region must stay below the UART window) and 8 on the GICv2 fallback, both rejected with clear pre-flight errors.
  • docs/war-histories.md — retrospective log of expensive debugging sessions. First entry: the eprintln!/tty-backpressure stall that manifested as an iter-20 handshake hang on the userspace vsock backend under multiplex load. Entries follow a fixed shape: problem, wrong turns, decisive evidence, fix, lessons.
  • docs/superpowers/plans/2026-04-23-guest-observability-alternatives.md — approved design for a guest-side pprof CPU + heap profiler exposed via a new ProfileRequest / ProfileResponse pair on the persistent multiplex channel, invoked from the host by voidbox profile --run-id <id> --kind cpu|heap (implementation pending). Spec also bundles the learned debugging-tools runbook (KVM_GET_REGS via gdb, perf kvm stat live --event=ioport, /dev/port POST-debug bytes, outb milestone markers, log-count-vs-iter scaling heuristic).
  • rust-style skill rule — never println! / eprintln! in library or service code; use tracing::{trace,debug,info,warn,error}!. Acceptable only in tests and top-level CLI main.rs user-facing output.
  • Slim microVM kernel build (scripts/build_slim_kernel.sh) — upstream Linux v6.12.30 LTS + Firecracker microvm-kernel-ci-{x86_64,aarch64}-6.1.config base + void-box additions (9p, virtiofs, overlayfs, VIRTIO_MMIO_CMDLINE_DEVICES). Ships uncompressed vmlinux (~30 MB) unifying artifact shape with macOS/VZ. Disables CONFIG_MODULE_SIG* so builds work on OpenSSL 3 hosts (Fedora 40+). KERNEL_VER / FC_COMMIT / FC_CONFIG_MAJMIN env overrides for reproducible builds.
  • macOS slim-kernel cross-buildscripts/build_slim_kernel.sh now dispatches into an ubuntu:24.04 container on Darwin hosts (--platform pinned from host arch, host-side cache check skips Docker on re-runs). UBUNTU_IMAGE env var lets callers pin a digest for reproducibility. Enables CONFIG_PCI{,_HOST_GENERIC,_HOST_COMMON} + CONFIG_VIRTIO_PCI{,_LEGACY} so the slim kernel boots on Apple Virtualization.framework (which uses virtio-PCI on arm64, unlike Firecracker's virtio-MMIO).
  • voidbox --log-dir and VOIDBOX_LOG_DIR overrides for file-based runtime logs
  • Codex CLI as first-class agent peerllm.provider: codex in YAML specs exec's the bundled OpenAI Codex CLI inside the guest VM with full structured observability
  • scripts/build_codex_rootfs.sh — production Codex-capable initramfs with auto-download from GitHub releases (musl-static, no glibc shipping)
  • scripts/build_agents_rootfs.sh — combined claude+codex flavor produced in a single build
  • scripts/lib/agent_rootfs_common.sh — shared rootfs helpers (sandbox user, CA certs, finalize, claude/codex binary resolution + auto-download) extracted from the Claude flavor for reuse across agent flavors
  • src/observe/codex.rs — structured stream parser for Codex's exec --json JSONL events (tool calls, token counts, error handling)
  • ObserverKind enum on LlmProvider — typed dispatch replacing the binary-name string comparison for stream observer selection
  • LlmProvider::Codex variant with binary_name(), supports_claude_settings(), build_exec_args(), observer_kind(), image_flavor() methods
  • Auth via host ~/.codex/auth.json mount (ChatGPT OAuth) alongside OPENAI_API_KEY env var fallback
  • Codex MCP discovery — writes ~/.codex/config.toml with [mcp_servers] streamable-HTTP entries pointing at the existing void-mcp server
  • Per-agent docs at docs/agents/claude.md and docs/agents/codex.md with @ discovery imports in AGENTS.md
  • examples/specs/codex_smoke.yaml (kind: agent) and examples/specs/codex_workflow_smoke.yaml (kind: workflow)
  • VZ native snapshot/restore using Apple's saveMachineStateToURL: / restoreMachineStateFromURL: APIs (macOS 14+) with a JSON sidecar (VzSnapshotMeta) carrying session_secret, memory_mb, vcpus, network, boot_clock_secs, config_hash, and VZGenericMachineIdentifier.dataRepresentation
  • VZ restore device-set drift reconciliation: when caller-supplied memory_mb / vcpus / network drift from the sidecar's saved values, the saved values are used so Apple's strict configuration-match check does not fail the restore
  • SandboxBuilder::enable_snapshots(…) / SandboxConfig / BackendConfig opt-in plumbing that gates VZ's validateSaveRestoreSupportWithError check (cold boots that do not opt in skip the check — some device sets make Apple reject snapshot-capability validation even when the VM itself is healthy)
  • snapshot_store::resolve_snapshot_argument returning a SnapshotResolution enum (Hash / Literal / NotFound), unifying three duplicate hash-vs-literal resolution paths
  • Hash-pinned vendored agent binaries (R-B5c.1)scripts/agents/manifest.toml pins each (agent, platform, arch) tuple to a specific version, url, and sha256. The build scripts (build_claude_rootfs.sh, build_codex_rootfs.sh) consult the manifest as the default source of truth and fail loudly on SHA-256 mismatch, missing manifest, or missing tuple. Override env vars (CLAUDE_CODE_VERSION / CODEX_VERSION) now require a matching *_SHA256 only when they differ from the manifest pin; setting them to the manifest pin is a no-op that uses the pinned SHA. CLAUDE_BIN / CODEX_BIN / local-PATH discovery still works for local dev but emits a WARN and is documented as non-production. Manifest reader is shell + awk (scripts/lib/agent_manifest.sh) — no extra runtime deps. Weekly .github/workflows/bump-agents.yml job (Mondays 09:00 UTC) discovers new upstream versions, computes SHA-256 in CI, and opens one PR per agent — per-arch independent (one lagging arch doesn't wedge the job). RELEASE_DIGESTS.json (schema documented in docs/release-digests.md) is published alongside each release. Maps to threat T-B5c.1.

Changed

  • vmm::arch::Arch::load_kernel now takes a BootPlatform (vCPU count + populated virtio slots) — the aarch64 DTB needs both at load time; new public types vmm::arch::VirtioSlot and vmm::arch::BootPlatform are the single source for per-arch device MMIO bases and interrupt numbers.
  • aarch64 GIC version selection is probe-then-create (KVM_CREATE_DEVICE_TEST) instead of create-then-fallback: KVM allows one vGIC per VM, so a GICv2 fallback after a partially-created GICv3 could never succeed — a creation failure is now a clear hard error, and the DTB always names the version the VMM attempts.
  • Startup latency — cold-boot p50 cut from ~4.9 s to 252 ms (−95%) and warm-restore p50 from ~607 ms to 138 ms (−77%) on KVM. Delivered in three steps: (1) remove three hardcoded blind waits (cold 4.9 s → 3.5 s, warm 607 ms → 433 ms); (2) add initcall_blacklist=cmos_init,i8042_init to the default kernel cmdline, skipping host-distro RTC/i8042 probe timeouts (cold 3.5 s → 1.7 s); (3) ship the slim kernel (cold 1.7 s → 252 ms). Backed by voidbox-startup-bench --iters 20 --breakdown on Fedora 43 host.
  • vmm::vsock_irq_thread epoll timeout tightened 200 ms → 20 ms so stop() reclaims the thread within one poll window instead of up to a full interval — drops stop() phase from ~230 ms to ~50 ms on both cold and warm paths
  • Rust MSRV bumped to 1.88
  • Interactive voidbox shell sessions now route runtime logs to the daily log file and route guest console output away from the active terminal to avoid TUI corruption
  • voidbox shell now prefers Claude Personal when personal OAuth credentials are available via the host's cross-platform credential discovery path
  • Renamed ClaudeExecOpts / ClaudeExecResult / ClaudeStreamEventAgentExecOpts / AgentExecResult / AgentStreamEvent (flat rename, no wrapper enum — both providers populate the same struct)
  • Renamed Sandbox::exec_claude() / exec_claude_streaming()exec_agent() / exec_agent_streaming() with &LlmProvider parameter threading
  • Renamed StageResult.claude_resultagent_result
  • Renamed e2e_claude_mcp test → e2e_agent_mcp (MCP infrastructure is agent-agnostic)
  • build_claude_rootfs.sh refactored to source shared agent_rootfs_common.sh helpers
  • build_claude_rootfs.sh and build_codex_rootfs.sh auto-detect or download the respective Linux binaries when invoked from an Apple Silicon host
  • Claude-specific --settings and --mcp-config flags gated behind provider.supports_claude_settings()
  • Guest network deny list is now applied once at guest init (right after setup_network()) instead of lazily on the first exec — closes the race window between network bring-up and first exec and makes the deny list visible on the serial console at boot
  • VZ auto-snapshot uses save_state_paused followed by a direct stop() from the paused state, avoiding an unnecessary resume/pause round-trip
  • host_metrics.rs on macOS now uses the mach2 crate instead of hand-rolled Mach FFI (IntegerT, TaskFlavorT, extern "C" block)
  • Service-agent and output-monitor progress messages in agent_box.rs routed through tracing (info! / warn! / error! / debug!) instead of eprintln!
  • Snapshot format v3 → v4, bincode → postcard (src/vmm/snapshot.rs). bincode 1.x and 2.x are both flagged unmaintained (RUSTSEC-2025-0141); bincode 3.0 ships as a compile_error! protest release. Swapped to postcard (actively maintained, serde-based, same scope, simpler API). The new wire format is not compatible with pre-v4 snapshots — delete ~/.void-box/snapshots/ to recover after upgrade. SNAPSHOT_VERSION bumped so the version gate surfaces a clear error on stale state files.
  • indicatif 0.17 → 0.18, which drops the unmaintained number_prefix transitive (RUSTSEC-2025-0119) in favor of unit-prefix.
  • release.yml, release-images.yml, and guest-image.yml no longer export CLAUDE_CODE_VERSION / CODEX_VERSION — these were the only way to drive build_claude_rootfs.sh / build_codex_rootfs.sh before R-B5c.1, and scripts/agents/manifest.toml now owns that responsibility. Removing the env vars eliminates the drift surface where the workflow's hardcoded fallback could disagree with the manifest after a bump.
  • AGENTS.md#Control channel I/O model rewritten to describe the persistent multiplex design (one long-lived connection per sandbox, request_id-keyed demux, shared FrameSender under a Mutex, dedicated reader thread) alongside the existing "why not fully async" rationale. Points at src/backend/multiplex.rs as the central module
  • Vm::with_vcpu_count removed (folded into Vm::new) and cpu::create_vcpu / cpu::create_vcpu_restored replaced by cpu::prepare_vcpu / cpu::prepare_vcpu_restored + cpu::start_vcpu — part of the aarch64 vGIC ordering fix; no pre-vCPU setup consumes a vCPU count anymore, and the post-vCPU hook receives the real count.
  • .github/workflows/e2e.ymlpersistent_channel, pty_nonzero_exit_code, and the entire snapshot_integration step are disabled on the Azure ubuntu-latest E2E lane. All three pass locally on every host we've tried; all three fail only on the Azure nested-virt runner (distinct failure modes: handshake deadline, exit-127 sentinel, and fast-failing CLI tests). Tracked as a single follow-up for a dedicated diagnostic pass on the Azure runner; the suites remain fully enforced locally and in the validation contract

Fixed

  • VM teardown: stop() and snapshot() can no longer hang unboundedly on a wedged thread (#128 item 3). Teardown enqueued VmCommand::Stop with a blocking send before clearing running and kicking the vCPUs, so an event loop wedged inside a dispatched command's await — e.g. a service-mode exec, which carries no timeout — stopped draining the command channel and parked teardown at the send. This is the exact state captured live during #127 validation, where snapshot_multi_vcpu hung roughly 1 run in 3 under nested KVM. Teardown now signals first and never waits on another thread consuming a message: running is cleared and the vCPUs kicked before anything else; the control channel is shut down via the new ControlChannel::shutdown() (the first non-test caller of MultiplexChannel::shutdown()), which fails every pending RPC dispatch slot and thereby wakes a wedged event loop so it can observe the cleared flag; the Stop command is reduced to a non-blocking try_send wake. Every thread join — vCPU, event-loop, vsock-irq, net-poll — is bounded by a 10 s deadline that fails with an error naming the stuck thread (the thread is detached) instead of hanging forever. ControlChannel::shutdown() also aborts an in-flight connect/handshake retry loop within one backoff step, so an event loop establishing against a dead guest cannot outlive the join deadline, and KvmBackend::stop() / VzBackend::stop() apply the same shutdown to the backend-level channel that carries every run's RPCs (exec, telemetry, write_file). The userspace vsock worker, whose own lifecycle flag was never reached by clearing running, is now stopped explicitly during teardown — before device-state capture on the snapshot path, and even when a join deadline fired (non-blocking then, since the wedged thread may hold the device mutex). A worker that had to be detached leaks its eventfds instead of closing them under a still-running thread, which could otherwise write into an unrelated, reused descriptor.
  • Userspace vsock backend: worker death or never-start no longer leaves the listener permanently unserviced (#128 item 4). Four paths killed the worker — or never started it — while host connects kept queueing into the accept backlog until run end: a guest STATUS=0 reset (reboot or panic) stopped the worker until a DRIVER_OK that might never come; a non-EINTR epoll_wait error or an epoll_create1 failure was warn-and-die; a snapshot taken before the guest driver reached DRIVER_OK restored a device whose worker never started; and a poisoned connection-map mutex silently skipped listener registration and every subsequent sweep. The worker's lifetime is now the device's lifetime: it starts at device construction (cold boot and restore alike) and survives driver resets, so connects made while the driver is down queue in the connection map — the same buffering the backend already provides during cold boot — bounded by the host's connect deadline. Every sweep now unconditionally accepts pending connections and drains host streams, making epoll a wakeup accelerator rather than a liveness dependency: on any epoll failure the worker degrades to a 50 ms periodic sweep instead of dying. A poisoned connection-map mutex is recovered with a one-time warning instead of silently abandoning service. Because the listener is now serviced regardless of driver state, acceptance is also bounded: concurrent host connections are capped at 128 (accepts beyond the cap are dropped, closing the stream), and the 4-byte port prefix a client sends after connecting is read non-blockingly with a 2 s deadline — previously the worker blocked up to 2 s holding the connection-map lock, which the vCPU MMIO paths also need, on a client that stalled mid-prefix. Data is bounded the same way: a connection whose guest-bound buffer reaches 256 KiB, or a guest-bound packet backlog past 4 MiB, pauses reads from the host stream so backpressure lands in the socket buffer (the client's writes block or hit their send timeout) instead of host memory absorbing unbounded data while the driver is down; the worker's epoll registrations are edge-triggered so a deliberately unread stream cannot spin the sweep.
  • Control channel: host RPCs can no longer hang unboundedly on a stalled guest or device (#128 items 1, 2, 5). Three gaps let a guest or device stall convert into a silent host-side hang instead of an error. (1) Multiplex frame writes were blocking write_all calls issued directly on the caller's async task with no send timeout — tokio::time::timeout cannot preempt a blocking syscall, the write held the shared FrameSender mutex (stalling every other RPC's send), and for exec the write ran before any timeout wrapper was armed. Writes now run in spawn_blocking with a 10 s per-wait SO_SNDTIMEO on the stream plus a hard 60 s per-frame deadline that also covers mutex queueing and trickle-fed writes. A send that fails before any byte reaches the stream fails only its own RPC; a send that truncates a frame mid-write (or exceeds the deadline) marks the channel dead — the wire can no longer be trusted for framing — so the next RPC reconnects, and senders already queued on the writer mutex refuse to write after the truncation. (2) The post-handshake reader detected channel death only via EOF, so a peer that wedged without closing its fd left a zombie channel that get_or_establish_channel kept handing out; the reader now runs on a bounded 1 s SO_RCVTIMEO and checks a shutdown flag between attempts (set by explicit shutdown, by a truncating or deadline-exceeding send, or by dropping the last channel handle — which also fixes the reader thread previously leaking for the VM's lifetime when a channel was discarded) — the timeout is swallowed inside the stream adapter so mid-frame idleness never breaks framing. Channel establishment itself, which runs before every per-RPC timeout, is now bounded by the connect deadline plus a fixed margin. (3) The userspace vsock backend's write_to_host issued a single write on the non-blocking host stream and treated any Ok(n) as full delivery, silently dropping the tail of a guest→host frame on a short write and stalling the host's multiplex reader mid-frame. Short-write remainders are now buffered per connection (cursor-based, so flushing never memmoves the backlog under the connection-map lock) and flushed on the worker's sweep; fwd_cnt advances only for bytes actually delivered, and a deferred flush queues an unsolicited OP_CREDIT_UPDATE so a guest parked on exhausted credit wakes when the host drains. The buffer is capped at the advertised buf_alloc, bounding host memory against a guest that ignores credit. None of these reproduced in normal runs after the #127 fixes, but each matched the fingerprint of the snapshot_multi_vcpu hang captured live during #127 validation (event loop parked on an await with no timer, no I/O, and no completer).
  • Userspace vsock backend: connections closed by the host no longer leak and busy-spin the worker. The device never detected a host-side close: read_from_host folded EOF into "no data", connections were removed only on guest-initiated Shutdown/Rst, and a write failure to a gone host application queued an RST but left the entry in the map. Each such connection kept its fd registered in the worker's level-triggered epoll set, where EOF reads as perpetually ready — so the worker's 50 ms poll loop degenerated into a busy spin holding the connection-map lock, starving the vCPU threads. Abandoned control-channel handshake retries create exactly these connections on every boot, so every VM carried a growing set of spinning fds; on 2-core CI runners the resulting starvation is what pushed later tests past the connect deadline in the first place. The worker's stream sweep (drain_host_streams) now distinguishes EOF (HostReadOutcome), queues an RST so the guest releases its side, and removes the connection — dropping the stream closes the fd, which also deregisters it from epoll. The sweep covers every connection state: an abandoned attempt whose OP_RESPONSE never completed parks in Connecting, where a sweep restricted to established connections could never reap it (validated with a load probe on Linux/KVM — 25 abandoned connections previously pinned the worker at 100% of a core indefinitely; it now settles to 0% within a second).
  • Userspace vsock backend: host connect no longer hangs forever when the guest is slow to boot. The control channel's connect(2) to the backend's Unix socket was unbounded. The backend's accept loop starts only once the guest driver reaches DRIVER_OK, and the listener's accept backlog is drained only by accept(2) — an attempt the client has already closed still occupies its slot. When guest boot exceeded the 30 s connect deadline (CPU-starved CI runners), the handshake retries of the warm-up and first-RPC establish attempts filled the 128-entry backlog, and the next connect(2) blocked indefinitely — inside the connector, where the retry loop's deadline (checked between attempts) could never fire. This surfaced as snapshot_integration CI runs hanging with no output until the 60-minute job timeout. connect_unix now applies SO_SNDTIMEO for the duration of the connect (cleared once connected), so a full backlog fails the attempt and the 30 s deadline produces its normal deadline reached (connect or handshake) error.
  • One-shot agent runs no longer log ERROR void_box::vmm: MicroVm dropped while still running on success. VoidBox::run consumes the box, so the VM could never outlive the call — but teardown fell to MicroVm's Drop safety net, which logs an error. run now stops the sandbox gracefully before returning, on success and on error; the Drop handler remains as a genuine safety net for abnormal paths. MicroVm::stop itself now works on current-thread tokio runtimes (e.g. #[tokio::test]), joining VM threads inline where block_in_place would panic.
  • aarch64/KVM boot: vGIC initialized after vCPU creation (src/vmm/). Every arm64 Linux/KVM boot failed with KVM error: Device or resource busy (os error 16): Arch::setup_vm ran all GIC work — device creation, address attributes, KVM_DEV_ARM_VGIC_CTRL_INIT — before any KVM_CREATE_VCPU, and the arm64 KVM ABI requires the opposite order (vGIC init freezes per-vCPU redistributor state; the kernel then rejects later vCPU creation with -EBUSY). Since x86_64 has the inverse constraint (irqchip before vCPUs), the arch hook is now split: setup_vm (pre-vCPU; x86 irqchip + PIT) and setup_vm_post_vcpus (post-create, pre-run; all aarch64 GIC work). vCPU creation is split from run-thread start (prepare_vcpu* / start_vcpu) so the hook runs while every vCPU fd exists and none is running, on both cold-boot and snapshot-restore paths. Fixes #112. The remaining aarch64 platform-description gap it exposed (#114) is completed by the aarch64/KVM guest-support entry under Added.
  • aarch64/KVM: register IDs derived from the ABI layout (src/vmm/arch/aarch64/cpu.rs). The hand-rolled KVM_SET_ONE_REG / KVM_GET_ONE_REG IDs placed the coproc class at bits 48+ instead of the ABI's bits 16–27 (every access landed in the kernel's sys-reg table → -ENOENT, killing cold boot at the PC write), addressed core registers in u64 words instead of 32-bit words, ignored the sp_el1/elr_el1/spsr[5] fields preceding fp_regs, and sized FPSR/FPCR as U64 instead of U32. IDs are now built from kvm_bindings constants and offset_of! on the real struct layout, with unit tests pinning them to the documented ABI values. Latent since the aarch64 port; unreachable before the #112 fix. Fixes #113.
  • Security: quinn-proto 0.11.14 → 0.11.15 (RUSTSEC-2026-0185 — remote memory-exhaustion DoS via unbounded out-of-order QUIC stream reassembly). Transitive bump via cargo update, reachable only through reqwest's http3 feature which this workspace does not enable, so the affected code is not compiled in; lockfile-only, no API impact.
  • Security: rustls-webpki 0.103.12 → 0.103.13 (RUSTSEC-2026-0104 — reachable panic in certificate revocation list parsing). Transitive bump via cargo update; no direct API impact.
  • Guest-agent kmsg() tty-backpressure deadlock — dropped the eprintln!(msg) line from guest-agent/src/main.rs::kmsg(), keeping only the /dev/kmsg write. Under the persistent multiplex control channel, the dual-write accumulated state in the guest kernel's n_tty line discipline and blocked write(2) on wait_event_interruptible(write_room > 0) after ~20 tight-loop iterations of persistent_channel_serial_exec_many (guest idle at pv_native_safe_halt, host's multiplex-reader blocked in libc::read). Validation: 100 serial execs pass in 7.86 s on the userspace vsock backend. Full retrospective in docs/war-histories.md.
  • Userspace vsock: host→guest writes larger than 4 KiB (VsockConnectionMap::queue_host_data) — the function silently truncated any buffer longer than 4 KiB to a single 4 KiB packet and dropped the rest, so any Message whose wire size exceeded the per-packet cap hung the guest-agent's read_exact(payload) indefinitely and the host timed out after 30 s with Protocol error: IO error: Resource temporarily unavailable. Loop now produces one Rw packet per 4 KiB chunk, respects peer credit, and buffers the remainder into conn.tx_buf + flushes on peer_fwd_cnt advance. Small RPCs unchanged. Latent since the userspace backend landed (f4f14ab); first user-visible trigger was the 6 KiB hackernews-api.md skill file (~21 KiB on the wire after WriteFile JSON framing).
  • x86_64 boot loader: raw vmlinux ELF support (src/vmm/arch/x86_64/boot.rs) — load_elf_kernel now reads e_entry from ELF header offset 0x18 instead of returning kernel_end (which was the end of loaded memory, not the entry point), and read_bzimage_init_size / read_bzimage_initrd_addr_max are gated on is_bzimage() so raw ELFs don't read garbage from offsets 0x260 / 0x22c (which collapsed the initramfs placement window to 0x0 and surfaced as initramfs too large for placement window end=0x0)
  • OCI rootfs mount on macOS/VZ: setup_oci_rootfs() on the non-block-device path now mounts the virtiofs share (parsed from /proc/cmdline voidbox.mount* entries) before the is_dir check, and the in-overlay shared-dir loop skips the same entry to avoid a double mount. Unblocks every sandbox.image-based spec on Apple Silicon; previously failed with OCI_FAIL_ROOTFS_MISSING. New OCI_FAIL_LOWER_MOUNT status code for diagnostic granularity.
  • Orphan-run reconciliation on daemon restart: reconcile_orphan_runs_on_startup() flips persisted non-terminal runs (Pending/Starting/Running) to Failed with terminal_reason = "daemon_restarted" and emits a run.failed event. Previously these runs were returned from GET /v1/runs forever after a kill/restart. Reuses RunStatus::Failed to keep the serde wire format stable.
  • Interactive PTY shell handling on macOS/VZ: poll-based host relay, resize forwarding, and cleaner terminal lifecycle for Claude and other TUI-style programs
  • Guest console routing semantics are now consistent across macOS/VZ and Linux/KVM
  • Snapshot restore: capture/restore IA32_XSS MSR to prevent XRSTORS #GP on CET-enabled kernels (6.x+)
  • Silent chown failure in provision_claude_bootstrap now emits a warn! with actionable remediation
  • Codex downloader's EXIT trap is scoped to a subshell so it cannot clobber the caller's cleanup trap
  • Deterministic MAC-address bit manipulation in deterministic_mac_address ((x | 0x02) & 0xfe) now carries a bit-level comment explaining the IEEE 802 "locally administered, unicast" transform

0.1.2 - 2026-03-16

Added

  • Snapshot/Restore for KVM: base and diff snapshots with multi-vCPU support, userspace virtio-vsock backend
  • Snapshot/Restore for Apple Virtualization.framework (macOS VZ)
  • aarch64 architecture support for snapshots via Arch trait refactor
  • Guest telemetry buffering and host metrics collection
  • Daemon lifecycle events: StageQueued, StageStarted, StageSucceeded, StageFailed, StageSkipped
  • Persist stage file_output artifacts to disk after completion
  • GET /v1/runs/{run_id}/stages/{stage_name}/output-file daemon endpoint for retrieving stage output files
  • Pipeline I/O wiring with mount-based inputs/outputs
  • Host directory mounts via 9p (Linux) and virtiofs (macOS) with RW/RO support
  • Shell installer (scripts/install.sh)
  • DEB and RPM packaging via nfpm
  • Homebrew tap distribution (macOS)
  • Structured logging via tracing with StructuredLogger
  • Startup banner
  • snapshot_store module centralizing snapshot utilities
  • Snapshot CLI: create, list, delete, diff snapshots
  • Virtio-net snapshot and restore
  • OCI guest image distribution via GHCR (multi-arch: amd64 + arm64)
  • macOS native support via Virtualization.framework
  • LM Studio provider support and OpenClaw Telegram example

Changed

  • Unified pipeline execution loop (run_pipeline_core)
  • Daemon route_request returns (status, content_type, body) for binary responses
  • Rust MSRV bumped to 1.85
  • Quinn-proto updated to v0.11.14
  • Renamed e2e_mount_9p to e2e_mount with expanded virtiofs support
  • Refactored module loading logic with optional 9p kernel modules
  • Replaced info! logging with debug! for reduced noise

Fixed

  • Snapshot restore: XCR0 / LAPIC timer / CID mismatch issues
  • EPERM-resilient OCI layer unpack
  • macOS VZ examples and Apple Silicon support
  • Duplicate directory creation in artifact management
  • aarch64 musl cross-linker path in guest-image workflow
  • Diamond dependency conflict with virtio and vm-memory crates
  • BusyBox inclusion in CI guest image

0.1.0 - 2026-02-19

Added

  • Initial release of void-box
  • KVM-based micro-VM sandbox implementation
  • Mock sandbox for testing and development
  • Workflow composition engine with DAG support
  • Native observability layer (traces, metrics, logs)
  • SLIRP user-mode networking (no root required)
  • Guest-agent for VM communication
  • CLI tool (voidbox) for command-line usage
  • Pre-built artifact distribution via GitHub releases
  • Streaming tool events — real-time [vm:NAME] tool: Bash <cmd> output during execution
  • Descriptive tool logging with tool_summary() (shows command/file_path/pattern instead of tool ID)
  • Incremental JSONL parser (parse_jsonl_line) for stream processing
  • exec_agent_streaming() sandbox API (renamed from exec_claude_streaming() in the Codex flavor effort)
  • HackerNews agent example (examples/hackernews/)
  • Code review agent example with two-stage pipeline and remote skills
  • Comprehensive CI/CD pipeline with GitHub Actions
  • Comprehensive documentation:
    • README with quick start guide
    • Getting Started guide
    • Architecture documentation
    • API documentation
  • Build scripts for release artifacts
  • GitHub Actions workflow for automated releases

Features

  • Sandbox Execution:

    • Local KVM-based sandboxes
    • Mock sandboxes for testing
    • Command execution with stdin/stdout/stderr
    • File operations (read/write)
    • Environment variable support
  • Workflow Engine:

    • Step definition and composition
    • Pipeline support (pipe steps together)
    • Parallel execution
    • Retry logic with configurable backoff
    • Context isolation between steps
  • Observability:

    • OpenTelemetry-compatible tracing
    • Metrics collection (counters, gauges)
    • Structured logging
    • Span inspection and analysis
  • CLI Tool:

    • voidbox exec - Execute commands
    • voidbox workflow - Run workflows
    • Auto-detection of KVM availability
    • Fallback to mock sandbox
  • Artifact Management:

    • Download pre-built artifacts from releases
    • Environment variable configuration
    • Auto-detection of host kernel
    • Artifact caching

Changed

  • Switch from Node.js + npm claude-code to native claude-code binary (Bun SEA)
  • SLIRP networking: DNS caching, host resolv.conf forwarding, reduced timeouts
  • Guest clock sync via kernel cmdline (voidbox.clock=<epoch_secs>)
  • Net-poll background thread for improved network throughput
  • HLT sleep reduced from 10ms to 1ms for lower latency
  • NPROC limit raised to 512 (from 256) for Bun worker threads
  • Memory bump to 2048MB in HackerNews example (OOM fix)

Fixed

  • RLIMIT_AS re-enabled at 1GB — Bun/JSC needs only ~640MB virtual (vs V8's 10GB+)
  • file_output fallback when claude-code output file is missing
  • skipWebFetchPreflight added to agent config defaults

Infrastructure

  • Rust workspace with library and guest-agent
  • Multi-architecture support (x86_64, aarch64 ready)
  • Static linking for guest-agent (musl)
  • Automated release builds
  • Documentation generation