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:

  1. Address translation. Guest physical addresses are not host virtual addresses. Something must translate a GuestAddress to a host pointer, per region, correctly.
  2. 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.
  3. Typed access without UB. Reading a struct virtio_blk_req_header out of guest bytes must not be a raw transmute of arbitrary bytes into a type with invariants. vm-memory restricts 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.)

TraitWhat it abstractsKey methods
Addressan addressable location with arithmeticraw_value(), checked_add(), unchecked_offset_from(), mask()
Bytes<A>byte-level read/write at an addressread_obj(), write_obj(), read_slice(), write_slice(), read(), write()
ByteValueda type safe to build from any byte pattern(marker trait — unsafe impl for POD types)
GuestMemoryRegionone contiguous region of guest RAMstart_addr(), len(), get_host_address(), get_slice()
GuestMemorythe 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 ByteValued for a type with invalid bit patterns (a bool, an enum with a restricted discriminant, a NonZero*). 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 class ByteValued exists to prevent.


The concrete types

TypeIsRole in Firecracker
GuestAddressa u64 guest physical address (Address impl)every "where in guest RAM" value
GuestMemoryMmapthe default GuestMemory impl, a set of GuestRegionMmapsthe microVM's whole guest RAM
GuestRegionMmapone mmap-backed GuestMemoryRegionone contiguous slab of guest RAM
MmapRegionthe raw host mmap underneath a regionthe actual mmap (file- or anon-backed)
VolatileSlicea bounds-checked, volatile-access window into memorya 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/
StepWhat happensCrate seam
Sizemachine-config mem_size_mib + arch layout → region rangesFC arch/ + vstate/memory.rs
AllocateGuestMemoryMmap::from_ranges(...) mmaps the host regionsvm-memory
Registereach region's (guest_phys_addr, size, userspace_addr) → set_user_memory_regionvm-memory → kvm-ioctls
Use (boot)linux-loader writes the kernel + zero page via write_obj/write_slicevm-memory → linux-loader.md
Use (devices)virtio devices read_obj/get_slice request datavm-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 GuestMemoryMmap with a MAP_PRIVATE file mapping or a UFFD-managed region for lazy paging (see snapshotting). The MmapRegion underneath a GuestRegionMmap can 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:

  1. Name the three problems vm-memory solves and give a concrete malicious-guest scenario for the bounds-safety one.
  2. What is ByteValued, why is it unsafe to implement, and what goes wrong if you implement it for a bool?
  3. Distinguish GuestAddress, GuestMemoryMmap, GuestRegionMmap, and VolatileSlice. Which one does a device hand to a copy routine, and why is it "volatile"?
  4. 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?
  5. Trace the path from mem_size_mib to a region registered with KVM. Where does the same mapping serve both KVM (running the guest) and the VMM (reading RAM)?
  6. Why can GuestMemoryMmap hold multiple non-contiguous regions, and what Firecracker layout fact forces that?

Common bugs and symptoms

SymptomRoot causeWhere to look
Device returns garbage / wrong datareading the wrong GuestAddress or wrong length from a descriptorthe device's read_obj/get_slice calls
GuestMemoryError::InvalidGuestAddress on a valid requestregion layout wrong, or address computed before the gap correctionarch/ layout; find_region; the address math
Intermittent host crash near a device readunsafe impl ByteValued on a type with invalid bit patternsthe ByteValued impls; replace with a real POD type
Snapshot restore reads zeros / faultsmemory file not mapped / UFFD handler not serving pagesthe restore mapping; snapshotting
Torn reads of a guest buffer under loadnon-volatile access where the guest concurrently writesuse 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.