vm-memory
Guest RAM is just a host mmap. KVM runs the guest directly against that host
mapping (you registered it with set_user_memory_region in the
previous chapter), so when the guest writes to
guest physical address 0x100000, it writes into your host memory at
userspace_addr + (0x100000 - guest_phys_addr). The VMM and every emulated
device also need to read and write that same memory — a virtio-block device reads
a request header the guest wrote, a network device copies a packet into a guest
buffer, the boot code writes the zero page. Doing that safely — bounds-checked
against attacker-controlled guest addresses, correct across multiple
non-contiguous regions, and without a transmute footgun on every typed access —
is the job of vm-memory. It is the single most-used rust-vmm crate in the
Firecracker codebase.
After this chapter you can: name the core traits (GuestMemory,
GuestMemoryRegion, Bytes, Address, ByteValued) and the core types
(GuestAddress, GuestMemoryMmap, GuestRegionMmap, VolatileSlice); read and
write guest memory with read_obj/write_obj/read_slice/get_slice; explain
why the bounds checks live here and matter; and find where Firecracker builds
guest memory and hands it to both KVM and the devices.
Note: This is the guest-memory deep dive's foundation. That chapter is about Firecracker's layout (where the kernel loads, the MMIO gap, high RAM); this chapter is about the crate that represents and guards every byte of it.
The problem vm-memory solves
Three problems, really, all of them sharp edges:
- Address translation. Guest physical addresses are not host virtual
addresses. Something must translate a
GuestAddressto a host pointer, per region, correctly. - Bounds safety. The guest controls the addresses in a virtqueue descriptor, a request header, a packet length. Every access from a guest-supplied address must be checked against the region's real bounds — or a malicious guest reads or writes host memory outside the guest's RAM.
- Typed access without UB. Reading a
struct virtio_blk_req_headerout of guest bytes must not be a rawtransmuteof arbitrary bytes into a type with invariants.vm-memoryrestricts typed reads to types that are safe to construct from any byte pattern.
# Confirm the dependency and read the pinned-version docs:
rg -n "vm-memory" Cargo.toml src/vmm/Cargo.toml Cargo.lock
cargo doc -p vm-memory --no-deps --open
docs.rs: docs.rs/vm-memory.
The trait hierarchy
vm-memory is built as a stack of traits so that devices can be written against
the abstraction and not against the concrete mmap backend. (Cloud Hypervisor
plugs in a different backend; the device code is unchanged — that's the point.)
| Trait | What it abstracts | Key methods |
|---|---|---|
Address | an addressable location with arithmetic | raw_value(), checked_add(), unchecked_offset_from(), mask() |
Bytes<A> | byte-level read/write at an address | read_obj(), write_obj(), read_slice(), write_slice(), read(), write() |
ByteValued | a type safe to build from any byte pattern | (marker trait — unsafe impl for POD types) |
GuestMemoryRegion | one contiguous region of guest RAM | start_addr(), len(), get_host_address(), get_slice() |
GuestMemory | the whole guest address space (possibly many regions) | find_region(), read_obj(), write_obj(), get_slice(), to_region_addr() |
The two you call constantly are Bytes (for read_obj/write_obj) and
GuestMemory (the whole-address-space view). The two that keep you safe are
ByteValued (you can only read_obj::<T>() if T: ByteValued) and the
internal bounds checks in the GuestMemory/GuestMemoryRegion impls.
flowchart TD
GM["GuestMemory (whole address space)"] --> R0["GuestMemoryRegion #0\n0x0000_0000..MMIO gap"]
GM --> R1["GuestMemoryRegion #1\n4 GiB..high RAM top"]
R0 --> M0["GuestRegionMmap → host mmap"]
R1 --> M1["GuestRegionMmap → host mmap"]
GM -.->|"read_obj::<T: ByteValued>(GuestAddress)"| R0
R0 -.->|"VolatileSlice (bounded window)"| dev["a device's transient access"]
ByteValued is the safety gate
You cannot do mem.read_obj::<MyStruct>(addr) unless MyStruct: ByteValued.
ByteValued is an unsafe marker trait you implement only for plain-old-data
types where every bit pattern is a valid value — integers, #[repr(C)] POD
structs with no padding invariants, no enums-with-niches, no references. That is
what makes reading attacker-controlled guest bytes into a typed value sound: the
guest can put any bytes there, and any bytes are a valid MyStruct.
# See what FC marks ByteValued (its virtio request headers, boot structs, etc.):
rg -n "ByteValued|unsafe impl ByteValued" src/vmm/src/
Warning: Never
unsafe impl ByteValuedfor a type with invalid bit patterns (abool, an enum with a restricted discriminant, aNonZero*). The guest controls the bytes; if some bytes are an invalid value for the type, reading them is instant undefined behavior — a guest-triggered UB primitive in the host. This is exactly the bug classByteValuedexists to prevent.
The concrete types
| Type | Is | Role in Firecracker |
|---|---|---|
GuestAddress | a u64 guest physical address (Address impl) | every "where in guest RAM" value |
GuestMemoryMmap | the default GuestMemory impl, a set of GuestRegionMmaps | the microVM's whole guest RAM |
GuestRegionMmap | one mmap-backed GuestMemoryRegion | one contiguous slab of guest RAM |
MmapRegion | the raw host mmap underneath a region | the actual mmap (file- or anon-backed) |
VolatileSlice | a bounds-checked, volatile-access window into memory | a device's transient view of a guest buffer |
GuestMemoryMmap is the backend Firecracker uses. It holds the host mappings —
the same mappings whose userspace_addr you registered with KVM. That is the key
unification: one set of mmaps, registered with KVM so the guest runs against
them, and wrapped in GuestMemoryMmap so the VMM and devices access them
safely. There is no copy, no second representation.
VolatileSlice deserves a callout. When a device needs to read or write a guest
buffer described by a descriptor, it takes a VolatileSlice over exactly that
range. "Volatile" because the guest (on another vCPU) may be concurrently touching
the same memory; the access must not be optimized away or torn by the compiler
assuming exclusive access. The slice is also bounded — you got it from
get_slice(addr, len), which already checked addr + len is inside a region.
This is how devices read attacker-controlled buffers without re-checking bounds on
every byte.
A worked example: typed and bulk access
#![allow(unused)] fn main() { use vm_memory::{ Bytes, GuestAddress, GuestMemory, GuestMemoryMmap, ByteValued, }; // A POD header a virtio-style device might read from guest memory. #[repr(C)] #[derive(Clone, Copy, Default)] struct ReqHeader { request_type: u32, reserved: u32, sector: u64, } // SAFETY: every field is an integer; any byte pattern is a valid ReqHeader. unsafe impl ByteValued for ReqHeader {} fn demo(mem: &GuestMemoryMmap) -> Result<(), vm_memory::GuestMemoryError> { let hdr_addr = GuestAddress(0x1000); // Typed read — bounds-checked, only legal because ReqHeader: ByteValued. let hdr: ReqHeader = mem.read_obj(hdr_addr)?; println!("type={} sector={}", hdr.request_type, hdr.sector); // Typed write back. let out = ReqHeader { request_type: 1, reserved: 0, sector: hdr.sector + 8 }; mem.write_obj(out, GuestAddress(0x2000))?; // Bulk read into a host buffer (e.g. a disk block the guest asked for). let mut buf = vec![0u8; 512]; mem.read_slice(&mut buf, GuestAddress(0x3000))?; // A bounded, volatile window a device can hand to a copy routine. let slice = mem.get_slice(GuestAddress(0x4000), 512)?; // checks bounds once slice.copy_to(&mut buf[..]); // safe, no re-check Ok(()) } }
Every one of those calls returns a Result. An out-of-bounds GuestAddress — a
guest that put a wild address in a descriptor — produces a
GuestMemoryError::InvalidGuestAddress (or similar), not a host crash and not a
host out-of-bounds access. That Result is the bounds check. Propagating it (the
?) instead of unwrapping is how a device correctly rejects a malicious request
instead of trusting it.
How Firecracker builds guest memory and wires it in
Firecracker constructs a GuestMemoryMmap early in the build path, sized from the
machine config, laid out per the architecture's memory layout,
then does two things with it: registers each region with KVM, and hands a
handle to the device managers and the boot code.
# Where guest memory is created (size from machine-config, layout from arch/):
rg -n "GuestMemoryMmap|GuestRegionMmap|create_guest_memory|from_ranges|memory" src/vmm/src/vstate/memory.rs
rg -n "GuestMemoryMmap|GuestAddress|arch_memory_regions|mem_size_mib" src/vmm/src/builder.rs src/vmm/src/arch/
# Where those regions get registered with KVM (the link to kvm-ioctls):
rg -n "set_user_memory_region|kvm_userspace_memory_region|register|iter\(\).*region" src/vmm/src/vstate/
# Where devices and the boot path read/write it (read_obj/write_obj/get_slice):
rg -n "read_obj|write_obj|read_slice|write_slice|get_slice|VolatileSlice" src/vmm/src/devices/virtio/
| Step | What happens | Crate seam |
|---|---|---|
| Size | machine-config mem_size_mib + arch layout → region ranges | FC arch/ + vstate/memory.rs |
| Allocate | GuestMemoryMmap::from_ranges(...) mmaps the host regions | vm-memory |
| Register | each region's (guest_phys_addr, size, userspace_addr) → set_user_memory_region | vm-memory → kvm-ioctls |
| Use (boot) | linux-loader writes the kernel + zero page via write_obj/write_slice | vm-memory → linux-loader.md |
| Use (devices) | virtio devices read_obj/get_slice request data | vm-memory → FC devices |
The MMIO gap and high-memory split you'll read about in the deep dive are exactly
why GuestMemoryMmap can hold multiple non-contiguous regions — low RAM below
the gap, high RAM above 4 GiB. find_region() is what makes a single
read_obj(GuestAddress(huge)) resolve to the right region transparently.
Tip: Snapshotting interacts with this crate too. The memory file is the guest RAM; on restore, Firecracker can back the
GuestMemoryMmapwith aMAP_PRIVATEfile mapping or a UFFD-managed region for lazy paging (see snapshotting). TheMmapRegionunderneath aGuestRegionMmapcan be file-backed — that's the hook.
Reading exercise
# 1. The dependency and pinned version.
rg -n "vm-memory" Cargo.toml Cargo.lock
# 2. The traits and types for the pinned version.
cargo doc -p vm-memory --no-deps --open
# 3. What FC marks ByteValued — and convince yourself each is real POD.
rg -n "unsafe impl ByteValued|: ByteValued" src/vmm/src/
# 4. Where guest memory is created and sized.
rg -n "GuestMemoryMmap|from_ranges|create_guest_memory" src/vmm/src/vstate/memory.rs src/vmm/src/builder.rs
# 5. The KVM registration seam.
rg -n "set_user_memory_region|userspace_addr|guest_phys_addr" src/vmm/src/vstate/
# 6. Device-side access patterns.
rg -n "read_obj|write_obj|get_slice|VolatileSlice" src/vmm/src/devices/virtio/block/ src/vmm/src/devices/virtio/net/
Answer:
- Name the three problems
vm-memorysolves and give a concrete malicious-guest scenario for the bounds-safety one. - What is
ByteValued, why is itunsafeto implement, and what goes wrong if you implement it for abool? - Distinguish
GuestAddress,GuestMemoryMmap,GuestRegionMmap, andVolatileSlice. Which one does a device hand to a copy routine, and why is it "volatile"? - Walk the worked example: which call performs the bounds check, and what does an out-of-bounds guest address produce instead of a host crash?
- Trace the path from
mem_size_mibto a region registered with KVM. Where does the same mapping serve both KVM (running the guest) and the VMM (reading RAM)? - Why can
GuestMemoryMmaphold multiple non-contiguous regions, and what Firecracker layout fact forces that?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| Device returns garbage / wrong data | reading the wrong GuestAddress or wrong length from a descriptor | the device's read_obj/get_slice calls |
GuestMemoryError::InvalidGuestAddress on a valid request | region layout wrong, or address computed before the gap correction | arch/ layout; find_region; the address math |
| Intermittent host crash near a device read | unsafe impl ByteValued on a type with invalid bit patterns | the ByteValued impls; replace with a real POD type |
| Snapshot restore reads zeros / faults | memory file not mapped / UFFD handler not serving pages | the restore mapping; snapshotting |
| Torn reads of a guest buffer under load | non-volatile access where the guest concurrently writes | use VolatileSlice, not a raw &[u8] |
Next: linux-loader — the crate that parses a vmlinux, copies
its segments into the GuestMemoryMmap you just built, and writes the boot
parameters the kernel reads on entry.