The device tray now shows Sound Output and Microphone Input menus for running VMs. Each direction can be toggled per-VM at runtime via the vhost-device-sound control socket. Initial state comes from the existing sound.playback/sound.capture NixOS options. NixOS module passes --initial-streams and --control-socket to vhost-device-sound. The vhost-device flake input is updated to include the new control socket support.
12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
vmsilo is a lightweight virtualization system inspired by Qubes OS. It runs isolated VMs using cloud-hypervisor (default) or crosvm with different security domains (banking, shopping, untrusted, etc.). VMs are configured declaratively via a NixOS module.
Environment
You are running under NixOS. If you need any tools not in the environment, use nix-shell.
Development Rules
- Update CLAUDE.md along with code. Keep this concise. Only add information that is not obvious and can not be easily looked up in code.
- Always update README.md for any user-visible changes (NixOS options, CLI flags, etc).
- There are no tests for the Nix modules.
nix builduses the git index for source filtering. New untracked files must begit add'd beforenix buildwill see them.
Build Commands
# Build the default rootfs image
nix build .#
Fuzzing
# Enter cargo-fuzz devShell (nightly Rust + cargo-fuzz)
nix develop .#fuzz
cd vmsilo-dbus-proxy && cargo fuzz run <target>
# Or one-liner (cargo-fuzz / libFuzzer)
nix run .#fuzz-cargo-dbus-proxy -- <target>
# Parallel cargo-fuzz (restarts automatically on crash)
nix run .#fuzz-cargo-dbus-proxy -- <target> --fork=4
# Re-test cargo-fuzz artifacts after a fix, delete those that no longer crash
nix run .#fuzz-clean-cargo-dbus-proxy -- <target>
# AFL++ with SymCC (single instance)
nix run .#fuzz-afl-dbus-proxy -- <target>
# AFL++ parallel (1 main + 3 secondaries + SymCC companion)
nix run .#fuzz-afl-dbus-proxy -- <target> --jobs=4
# Re-test AFL++ crash files after a fix
nix run .#fuzz-clean-afl-dbus-proxy -- <target>
# Generate/refresh seed corpus for both engines
nix run .#fuzz-gen-corpus
Fuzz targets live in vmsilo-dbus-proxy/fuzz/ (cargo-fuzz) and vmsilo-dbus-proxy/fuzz-afl/ (AFL++). Protocol types have Arbitrary derives behind the fuzz feature flag. Invariant assertions are in src/fuzz_helpers.rs.
Fixing fuzz findings
The threat model is an untrusted VM guest sending crafted data to the host-side dbus-proxy. The priority is preventing code execution and privilege escalation on the host — not DoS resilience (a misbehaving VM only affects its own proxy service). Therefore:
- Reject and disconnect on malformed input rather than logging and continuing. An
anyhow::bail!/return Err(...)that tears down the connection is always preferable to silently discarding bad data. - No
unsafein parsing or sanitization paths. - Validate at the boundary (immediately after deserialization), not deep in business logic.
- Tighten the invariant assertions in
src/fuzz_helpers.rswhenever you add or change a constraint — the fuzzer can only find violations it can check.
Code Style
This project uses treefmt-nix with nixfmt for formatting. Run before committing:
nix fmt
Architecture
VM Launch Flow (NixOS module)
VMs run as system services (root) for PCI passthrough and sandboxing support. crosvm drops privileges before starting the guest.
VMs start automatically when first accessed via socket activation:
vm-run banking firefoxconnects to/run/vmsilo/banking/command.socket- Socket activation triggers
vmsilo-banking@.service(proxy template) - Proxy requires
vmsilo-banking-vm.service, which starts crosvm - Proxy waits for guest vsock:5000, then forwards command
The configured user can manage VM services via polkit (no sudo required for vm-start/vm-stop).
Key Files
NixOS module (modules/): Split across many files — there is no single config.nix.
default.nix— entry point (imports all submodules)options.nix— option declarations (programs.vmsilo; includesvm.<name>.hypervisorfor VMM selection:cloud-hypervisor(default) orcrosvm)scripts.nix— VM launcher scripts (crosvm/cloud-hypervisor) and user-facing CLI scripts (vm-run,vm-start,vm-stop,vm-shell)services.nix— systemd units (VM service, proxy, console relay, session bind, GPU device backend, wayland-seccontext)usb.nix—vm-usbCLI script (USB passthrough via usbip-rs)desktop.nix— .desktop file generation and icon copyingnetworking.nix— TAP interfaces, host nftables fornetvm = "host", guest network confignetvm.nix— auto VM-to-VM and VM-to-host network linkspci.nix— VFIO PCI passthrough setupoverlay.nix— root overlay (raw/tmpfs) setupassertions.nix— validation checkspackage.nix— final package derivationlib/helpers.nix— crosvm argument building helperscss-colors.nix— CSS named color lookup table
Guest rootfs (rootfs-nixos/):
default.nix— NixOS-based rootfs builder (outputs{ erofs, kernel, initrd })configuration.nix— entry point (imports all guest submodules)guest/boot.nix— boot config, initrd, overlayfs rootguest/users.nix— user accounts, sudo, SSHguest/networking.nix— network config from kernel paramsguest/wayland.nix— wayland proxy and session setupguest/command.nix— vsock command listener, dbus proxy, idle watchdogguest/system.nix— packages, graphics, fonts, miscguest/kernel-param-helper.nix— shared shell helper for parsing kernel cmdlineguest/usb.nix— usbip-rs client listener service, vhci_hcd module- Self-contained erofs image with overlayfs root; no host /nix sharing
Rust crates:
vmsilo-balloond/— dynamic balloon memory management daemon (equalizes host/guest free memory via virtio-balloon; run--helpfor CLI options)vmsilo-dbus-proxy/— D-Bus proxy for system tray and notification forwarding between guest and host over vsock:5001vmsilo-wayland-seccontext/— creates Wayland security context socket (wp_security_context_v1); run by per-VM systemd service before the GPU device servicevmsilo-vsock/— shared async vsock connection library (kernel vsock + fc-vsock CONNECT handshake, autodetection by VM name)vmsilo-tools/— Rust workspace for small utilities. Containstap-open(opens TAP device by name, execs command with inherited FD) andvsock-connect(CLI for connecting to VM vsock ports)
Other:
patches/— KWin/Plasma patches for VM window decoration colors and clipboard isolationflake.nix— exposesnixosModules.defaultandlib.makeRootfsNixos
Generated Scripts
vm-run <name> <cmd>— run command in VM (starts VM on-demand via socket activation)vm-start <name>— start VM via systemd (polkit, no sudo)vm-stop [--all | <name>]— stop VM(s) via systemd (polkit, no sudo)vm-shell <name>— connect to VM serial console (default) or SSH with--sshvm-usb [attach|detach] [<name> <vid:pid|devpath>]— list USB devices, attach/detach USB devices to VMsvsock-connect [--timeout <s>] [--probe] <vmname> <port>— connect to VM vsock port (autodetects hypervisor type)
See README.md for full usage details and options.
Dependencies
- Custom crosvm fork:
git.dsg.is/dsg/crosvm.git - wayland-proxy-virtwl: Wayland/X11 proxying between host and guests
- NixOS 25.11 base
- usbip-rs:
git.dsg.is/dsg/usbip-rs.git— USB/IP over vsock for USB passthrough
Key Patterns
- Socket activation chain: command socket → proxy template service → VM service → crosvm
- Offline-by-default networking: VMs have no network unless
network.interfacesornetwork.netvmis configured. - Host netvm:
network.netvm = "host"routes VM traffic through the host directly (TAP with hostAddress, host nftables NAT + input filter). VMs cannot initiate connections to host services — only established/related return traffic is allowed. No bridge or netvm VM needed. - Root overlay: raw disk-backed (default) or tmpfs-backed overlayfs upper layer. Ephemeral — created at VM start, deleted at stop.
- Session bind: GPU-enabled VMs (default) are tied to the desktop session via per-VM systemd user services bound to
graphical-session.target. ForautoStartGPU VMs, the session-bind service also starts the VM on login. Non-GPUautoStartVMs start atmulti-user.target(boot). - Automatic DNS: All VMs have
systemd-resolvedenabled by default (guest rootfs). Netvm VMs getunboundas a recursive resolver viaguestConfiginjection. Downstream VMs get nameserver kernel params pointing at their netvm's IP vianetvmInjections.nameservers. VMs withnetvm = "host"or no netvm need manual DNS config. - GPU device backend:
vmsilo-<name>-gpuservice runs the GPU device backend sandboxed; selectable viagpu.backendbetweenvhost-device-gpu(default, vhost-device-gpu in rutabaga mode) andcrosvm(crosvm device gpu). Both crosvm and cloud-hypervisor VMMs attach via vhost-user.vmsilo-<name>-wayland-seccontextmust start first. GPU is enabled when any capability (wayland,opengl,vulkan) is true;waylanddefaults true. Setgpu.wayland = falseto disable. - Per-VM runtime dirs: all sockets under
/run/vmsilo/<vmname>/subdirectories (not flat). virtiofs instances get per-instance dirs at/run/vmsilo/<vmname>/virtiofs-<tag>/. - USB passthrough: usbip-over-vsock on port 5002. Guest runs
usbip-rs client listen, host runs oneusbip-rs host connectper device asvmsilo-<vm>-usb@<devpath>.service. Works with both crosvm and cloud-hypervisor. - CID file:
/run/vmsilo/<vmname>/cidwritten by prep service, read byvsock-connectand dbus-proxy for autodetection. - Sound control socket:
/run/vmsilo/<vmname>/sound/control.socket— written by vhost-device-sound when sound is enabled; used by the device tray to query and toggle playback/capture state at runtime. Initial state comes fromsound.playback/sound.captureNixOS options (passed as--initial-streams). - CH sandboxing: CH VMs use NixOS confinement (chroot), PrivateUsers=identity, PrivateNetwork, PrivatePIDs, PrivateIPC, empty CapabilityBoundingSet. TAP FDs passed via
vmsilo-tap-open+ch-remote add-net. All privileged operations in ExecStartPre=+/ExecStartPost=+/ExecStopPost=+. Gated bycloud-hypervisor.disableSandbox. - virtiofsd sandboxing: virtiofsd has built-in sandboxing (
--sandbox=namespace): creates mount/PID/network namespaces, does pivot_root, drops capabilities, and applies its own seccomp filter. The systemd unit adds non-overlapping hardening: IPC/UTS namespace isolation, seccomp-based protections (clock/modules/logs/personality), capability bounding set (as defense-in-depth), andLimitNOFILE=1048576. Per-instance runtime dirs at/run/vmsilo/<vmname>/virtiofs-<tag>/. Gated byvirtiofs.disableSandbox; seccomp controlled independently byvirtiofs.seccompPolicy.
Gotchas
nix buildgit index filtering: new files must begit add'd beforenix buildsees them (applies to allsrc = ./<path>references in flake.nix).- Explicit UID required: the configured user must have
users.users.<name>.uidset explicitly. - O_DIRECT on root disk: intentional — bypasses host page cache to avoid double-caching (erofs is read-only, guest caches internally).
- Module split: there is no
modules/config.nix. Implementation is split acrossscripts.nix,services.nix,desktop.nix,networking.nix,pci.nix,overlay.nix,assertions.nix,package.nix. - cloud-hypervisor package: sourced from a custom flake input (
git.dsg.is/dsg/cloud-hypervisor.git), not nixpkgs.
NixOS Module Usage
The configured user must have an explicit UID set (users.users.<name>.uid = <number>).
See README.md for the full configuration reference and examples.