~/satyajit

NVX: Microsoft's micro-VM sandbox builds the machine up from nothing

mdjsonmcp

2026-10-23 · 17 min · agents · security · infrastructure · rust

A 1:48 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Runnel! Here is how Microsoft's NVX gives each agent workload its own tiny virtual machine. The idea. Do not trim a whole PC. Build only the machine you allow, the same on every hypervisor. A general monitor emulates a PC, with firmware and a PCI bus, and you switch parts off. NVX boots Linux directly, puts six devices at fixed addresses, and rejects everything else. The host decides policy, and hands it to OpenVMM, which checks everything twice. One machine runs on three hypervisors, with no paravirtual clock. Inside, guest agents find the fixed devices and build the workload's root. The workload runs as a non root user with no capabilities, and the agent cleans up after it. Fast starts come from snapshots. The guest asks for a snapshot, and its machine stops for good. Each restore maps the saved memory privately, so clones keep their own writes. The host sends sixty four fresh random bytes, so clones never share keys. The guest fixes its clock before any workload runs. The network card offers only two features. No checksum or segmentation offload. Frames reach a network stack inside the monitor, checked twice. Allowed traffic leaves through an ordinary host socket. No tap device. On the project's own test machines, a cold start takes about two hundred eighty milliseconds. A restore, about sixty. The takeaway. The isolation comes from what this machine refuses to have. To recap. Build the machine, restore with fresh entropy, keep the network small. Every source is in the full article. I'm Runnel. Bye!

Why read this

Notabletop 60%

NVX read from its design docs, Rust guest agents and the OpenVMM fork it pins, with its own 288-commit CI history and the nvx-rs tests run on CPU.

  • Original analysis
  • Runs on a laptop CPU
  • A lasting reference

Security & safetyMITPractitioner tool

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
1 of 3: A specialist community
Only here?
2 of 3: A teardown or measurement few others did

Score 67 of 100, ranked 181 of 577 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

On October 19, Liam Arzola posted that Microsoft Research's systems group and Azure Research had open-sourced NVX, "a sandbox built to support most agent workloads while stripping out all unnecessary components resulting in the lightest hardware isolated uVM I've seen". He was clear that he did not work on it; his internship mentors did. The replies asked the right questions: how cold boot compares to Firecracker, whether it has network policies, whether it runs only on Hyper-V, and one person argued that gVisor already does this without a hypervisor.

I cloned the repository at 1f9e0c7 (October 22), read the design documents, the Rust guest agents and the OpenVMM fork it pins, ran its Rust test suite, and pulled its own benchmark history out of data/. I could not boot it: this machine has no /dev/kvm. So the latency numbers below are the project's, from its CI, and I say so each time.

microsoft/nvx@1f9e0c7 · snapshot 2026-10-23
tracked files
593
license
MIT
branch
HEAD
tests
80 files
source
9.8 MB
commit date
2026-10-23
source by language
Rust5.3 MB(287)Python4.2 MB(62)Shell126.7 kB(32)PowerShell122.5 kB(5)Dockerfile13.1 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-23 at 1f9e0c7 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

What it is

NVX is a micro-VM sandbox: one small virtual machine, with its own Linux kernel, per untrusted workload. The virtual machine monitor is OpenVMM, Microsoft's Rust VMM, run from a fork branch called feature/microvm/dev. The README credits the MSR Systems Research Group and Azure Research – Systems, "building on research results from the Nanvix system". The licence is MIT and the version file says 0.1.0.

The repository is the distribution, not the VMM: version pins, three kernel patches, a kernel config, guest images, a Python harness (scripts/nvx.py), a Rust crate (nvx-rs) with the host launcher and nine guest binaries, a Rust lifecycle API (aci_edge_sandboxes), 32 design documents and the CI performance history. It tracks 593 files (measured). The guest kernel is Linux 6.18.38; the userlands are Alpine 3.24.1, Ubuntu 26.04.1 and Azure Linux, all booted with the same NVX kernel.

It runs on three hypervisor backends: KVM and MSHV on Linux, and the Windows Hypervisor Platform (WHP) on Windows. So yes, there is a KVM path; it is the default in the quick start.

Build the machine, don't trim it

The single design decision everything else follows from is in section 2.1 of the design docs:

The machine profile constructs the required machine directly instead of creating a standard PC and removing unwanted devices.

A general VMM emulates a PC and you switch parts off. NVX's profile is an allowlist. It rejects UEFI, PCAT and IGVM firmware, caller-supplied ACPI, SMBIOS and device trees, PCI and PCIe, vPCI, VMBus, ISA DMA, IDE, floppy, graphics and "every device outside the machine profile" (reported, design doc 2.1.1). What is left:

The slots are fixed rather than allocated in order, so an omitted device never moves another, and the guest finds them from kernel command-line tokens that only the machine profile may emit. A caller cannot inject a device declaration through the command line.

The kernel config matches. CONFIG_ACPI and CONFIG_PCI are off, and so are CONFIG_KVM_GUEST and CONFIG_HYPERV: the guest gets no paravirtual interface from whichever hypervisor runs it. The config enables 945 options and builds no modules (both measured, from kernel/config-microvm).

Block diagram in three groups. OpenVMM contains command-line entry and management RPC, the machine profile, the VM worker, chipset and platform devices, virtio devices, the snapshot publisher and the hypervisor backends. The host group holds the NVX launcher, sandbox lifecycle API, host control library, snapshot storage and external attachments, linked to OpenVMM by control sessions. The guest group runs Linux kernel to guest agents to workload.
NVX's three layers: the host owns policy, OpenVMM owns the virtual platform, the guest agents own the workload. Rendered from the Mermaid source in the repository (NVX design docs, Figure 1.2).

The split is policy against mechanism. The host decides identity, attachments and network policy. OpenVMM enforces the platform and validates every input twice, before it opens any host resource and again when the worker is built. The workload is the only tenant of its VM, so the VM boundary is also the boundary between workloads.

One clock, three hypervisors

The part I did not expect is how much of the design is about time. A sandbox that snapshots and restores has to answer an awkward question: when a frozen machine wakes on another host, minutes later, what time is it, and how fast does its clock tick?

NVX answers with what it calls the time ABI, and it is strict. There is no paravirtual clock: no kvm-clock, no Hyper-V enlightenments. The TSC-deadline timer is hidden on every backend, so every guest uses the one-shot counting local APIC timer. The guest kernel reads the TSC and APIC timer rates from the hypervisor identity's frequency registers and never calibrates them, and OpenVMM refuses a command line that tries to set them (E_CMDLINE_CLOCK_TOKEN). The only hypervisor interface the guest sees is a minimal identity that is the same on KVM, MSHV and WHP (reported, design docs 2.1 and 3.2).

That uniformity is what lets one snapshot format work across three hypervisors. It does not let a snapshot move between them. Restore requires the same backend and a CPU profile of the same generation as the capture, and the non-goals list rules out cross-backend restore, live migration and capture-and-continue.

Snapshots: the guest pulls the trigger

A snapshot in NVX is not a host-side memory dump. The guest requests it, and a successful capture terminates the source VM. The VMM quiesces the devices, writes guest memory, processor and device state into a new directory, and renames it into place. The restored copies are the only continuation. A restored VM can never be captured again.

Snapshots come in three tiers (reported, design doc 5.4.4):

TierRestoreWhat it saves youWho may share it
platformclonekernel and guest-agent start-upa trusted platform build, fleet-wide
workload-startclonealso the runtime's own start-upone tenant: it holds workload memory and often secrets
instance-checkpointresumea single live instance, claimed onceone instance

Restore is ten checks before any vCPU runs: read a bounded manifest, check the time contract and CPU profile, require the same backend, validate topology, devices, attachments, command line, memory and block geometry, and then map the captured memory privately, copy-on-write. Every VM restored from one snapshot shares the clean pages of one immutable memory file and gets private copies of the pages it writes. The design doc is careful about what that does not mean: copy-on-write "isolates writes, not physical pages", so clean pages stay shared through the host page cache, and it is not a tenant-isolation mechanism on its own (reported, design doc 1.4.4).

Then the guest repairs itself before anything else runs. Clones of one snapshot would otherwise share the same random-number generator state and hand out the same keys. So before any processor runs, the VMM draws 64 bytes from the host's cryptographic generator. They reach the guest in a restore packet; the first 16 bytes become the VM's generation ID. A clone credits all 64 bytes to the kernel's entropy pool, forces a reseed, and derives a fresh machine ID from them. The guest also steps its wall clock by the measured offset and corrects its rate, clamped to ±500 ppm (reported, design doc 5.5).

Perplexity's SPACE forks the same way, from a file with private writes; NVX differs in writing entropy and clock repair into the restore protocol itself.

Inside the guest: nine binaries and a supervisor

The guest side is nine static x86_64-unknown-linux-musl binaries from nvx-rs, installed at /sbin/nvx-* in all three images. The ones that do the work are nvx-managed-agent, which serves the host's control session and runs workloads one at a time; nvx-time, which enforces the time ABI and repairs the clock after a restore; nvx-reseed; nvx-ingress, which announces the guest's listening ports; and nvx-exit, which stops the VM with an exit code through I/O port 0x604.

The agent never becomes the workload. It stays the guest's init, runs each workload as its child, and reaps whatever it leaves. Before a workload starts, the agent sets a host-chosen non-root UID and GID, clears supplementary groups and every capability, and sets no_new_privs. With every capability gone the workload cannot do port I/O, so only the agent can request a snapshot. When a workload's first process exits, the agent kills everything left in its cgroup and reports the result only once the cgroup is empty.

There are two roots, and they are not equal. The layered root stacks a read-only distro image under a private writable scratch disk with overlayfs, and gives the workload private mount, PID and UTS namespaces, a private /proc, a read-only /sys and a restricted /dev. The blockless managed root, which is the default backend of the Rust lifecycle API, runs each execution in the guest's own initramfs root with the agent's namespaces. The design doc says so plainly: it "has no such isolation"; credentials and a cgroup contain the workload, but it sees the guest's own /proc, /sys and /dev (reported, design doc 4.3.2). The VM boundary still holds either way. What you lose is isolation between the workload and the agent inside it.

The network path has no TAP device

Networking is off unless you pass --net. When it is on, the NIC is a virtio-net device backed by an in-process userspace network stack inside the VMM. There is no TAP interface, bridge or host virtual switch, and the host needs no IP forwarding. A guest TCP connection ends in that stack; the VMM opens an ordinary host socket to the destination. It proxies sockets rather than rewriting packets, so a remote server sees the host.

Egress is checked twice: once on the frame headers as the device reads the TX queue, and again on the endpoint's own copy of the complete frame. The second check exists because guest memory stays writable after the first, so a guest could change a frame between the check and the send. A policy has a default action and up to 256 allow rules and 256 deny rules over addresses, CIDRs, protocols and ports. A matching deny always wins, whatever the order (reported, design doc 3.5.3).

Then I checked what the device tells the guest. OpenVMM's general virtio-net device, over this backend, offers checksum offload, TCP and UDP segmentation offload, packed rings and a dozen other features. The microVM profile masks all of that away:

// openvmm/openvmm_defs/src/microvm.rs, OpenVMM fork at 7ff31ed
/// Exact microVM virtio-net feature mask: MAC and virtio version 1.
pub const MICROVM_VIRTIO_NET_FEATURES: u64 = (1 << 5) | (1 << 32);

The worker passes !MICROVM_VIRTIO_NET_FEATURES as the disabled-feature set when it builds the transport, so of the 15 feature bits in the widget below the guest can negotiate two (measured, from source).

This matters more than it looks. The guest-to-host escape I covered in the SPACE article, CVE-2026-99999, starts with exactly the metadata this mask removes: a guest offering a packet with a partial checksum and a note on where to finish it, delivered through a TAP device into the host kernel's IPv4 path. NVX's guest cannot negotiate CSUM, and its frames never reach a host TAP. My reading is that NVX is not on that bug's path (reasoned, from the mask and the design doc; I did not test it). The parsing has not disappeared, though. It moved into the VMM's userspace network stack, so that Rust code is now the code that reads hostile packets. A bug there compromises the VMM process, which runs as an ordinary host user, not the host kernel. That is a better place to have a bug, but it is not no bug.

The two defaults to change before you give it an agent

Two of the network defaults are friendly to development and wrong for a hostile agent:

Inbound access defaults to none, and the Rust API's quick start uses NetworkPolicy::deny_all(), the right example to copy. So yes, it has network policies; switch them on.

What the CI measures

NVX keeps its performance history in the repository: one CSV per backend and vCPU count, where each row is the median (p50) of one CI run at one commit, and CI gates on regressions against it. There are 288 commits in each series (measured). Every runner is an Azure Standard_D8s_v5 virtual machine with Intel Xeon CPUs, so every number is nested virtualization: a microVM inside a cloud VM. Bare-metal results, the docs say, are a separate history; none is committed.

At the latest commit, on one vCPU with a 128 MiB guest (all reported, from data/*.csv; the medians and ranges over 288 commits are my arithmetic):

KVMMSHVWHP
Cold start to shell278.0 ms225.3 ms536.4 ms
Snapshot restore to shell61.5 ms33.0 ms154.7 ms
Guest exit and teardown63.3 ms21.4 ms60.2 ms
Snapshot generation1.2 ms4.6 ms1041.2 ms
OpenVMM peak RSS, cold80.0 MiB81.1 MiB76.4 MiB
OpenVMM peak RSS, restored35.5 MiB61.4 MiB18.7 MiB

A few things I would take from this.

Restore is the fast path. On KVM restore takes about a quarter of a cold start: 61.5 ms against 278.0 ms, roughly 4.5× (reasoned). On MSHV the ratio is about 6.8× (reasoned). That is the reason the snapshot tiers exist: a fleet boots once into a platform snapshot and clones it.

The KVM restore history is bimodal. Its median over 288 commits is 59.4 ms, but the fastest commit recorded 29.7 ms and the slowest 72.2 ms (reasoned, from the CSV). The 512 MiB shell restore shows the same spread, 27.5 to 75.5 ms. On a shared nested host I would not read a single commit's number as the product's.

Peak RSS on restore is not memory saved. RSS counts guest pages actually touched in the private mapping, and the docs warn that Windows reports a cumulative working set instead. The 18.7 MiB on WHP says that little was faulted in by the readiness marker, not that a restored VM uses 18.7 MiB.

Kernel tuning barely moves cold start. The cold-start suite adds one boot parameter at a time to a quiet baseline of 308.1 ms on KVM. Only mitigations=off moves it, to 280.1 ms (reported); the other seven land within 7 ms of it. So the fixed cost is not in kernel options a caller could tune.

More vCPUs cost some restore time. The 512 MiB restore goes from 62.6 ms on one vCPU to 72.1 ms on eight on KVM, though two vCPUs recorded 52.2 ms at the same commit, and from 41.1 to 52.3 ms on MSHV (reported).

What is not measured anywhere in the repository is a comparison with Firecracker, Cloud Hypervisor or gVisor. I searched the docs; the only mention of any of them says the Azure setup does not need Cloud Hypervisor or Kata. So the reply asking how cold boot compares with Firecracker has no answer from NVX, and the CI hosts are nested, which makes cross-project numbers from elsewhere a poor comparison anyway. "The lightest hardware isolated uVM I've seen" is an opinion; the repository does not make that claim.

What I ran

The run exception here was the Rust crate. With Rust 1.95.0 (the repository's pinned toolchain), two build jobs on a shared 4-core cloud VM with no GPU and no /dev/kvm:

cd nvx-rs
NVX_BUNDLE=skip cargo test --all-features -j2 -- --test-threads=2

It built in about a minute and the whole run took 95 s (measured). The library tests, which cover the guest agents, cgroup containment, the time and reseed code and the host supervisor against a fake OpenVMM, passed: 959 passed, 0 failed, 10 ignored (measured). The CLI parity test, which replays recorded nvx.py scenarios against the Rust launcher, passed 26 of 27. The one failure was my environment: I ran as root, and the "caller-owned share" scenarios correctly refuse a shared directory owned by UID 0 with --mount-owner caller squashes guest root to the owner of the shared directory, so … must not be owned by UID 0 or GID 0 (measured). That is the check doing its job.

I did not boot a guest, run the benchmark suite, or test the egress policy. All three need a hypervisor.

Where it sits

The site has covered three other answers to "where do you put an agent that runs code". DeepSeek's DSec is a production platform with four backends and a 50× CPU overcommit, described in a paper with no code. SPACE gives each task a jailed Firecracker microVM and keeps credentials in a gateway outside it. OpenShell skips the hypervisor and puts the boundary in the kernel with Landlock and seccomp. That last one is the shape of the gVisor argument in the replies: a userspace kernel or kernel-level policy can contain a workload without a VM, at the cost of sharing one host kernel.

NVX is the hypervisor answer, done narrowly. What is new is how much it refuses: firmware, PCI, paravirtual clocks, offloads, device enumeration, cross-backend restore. Each is attack surface or a source of nondeterminism across snapshot and restore.

Where it costs

If I were putting agents in it, I would start from the Rust API's deny_all() example, add --host-loopback deny, use the layered root rather than the blockless one, clone from a platform snapshot, and keep workload-start snapshots per tenant. That gets most of what the design offers, and the design is careful.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "NVX: Microsoft's micro-VM sandbox builds the machine up from nothing", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026nvx,
  author = {Satyajit Ghana},
  title  = {NVX: Microsoft's micro-VM sandbox builds the machine up from nothing},
  url    = {https://ai.thesatyajit.com/articles/nvx},
  year   = {2026}
}
share