Chain Config Protocol¶
The chain config is a fixed-layout data structure written by the VMM into guest memory before starting an operation. It provides metadata about every device in the backing chain so guest operations can perform format-aware I/O without re-parsing image headers.
This document covers the binary layout and data flow. For how the VMM discovers backing chains in the first place, see chain-discovery.md.
Data Structures¶
Both structures are defined in src/shared/src/lib.rs and use #[repr(C)]
for a stable memory layout.
ChainConfig (16 bytes header + device array)¶
Written at CHAIN_CONFIG_ADDR (0x00082000) in guest physical memory.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 4 | u32 | magic |
0x4348414E ("CHAN") |
| 4 | 4 | u32 | device_count |
Number of valid entries (1 = no backing files) |
| 8 | 4 | u32 | version |
Structure version (currently 1) |
| 12 | 4 | u32 | _reserved |
Reserved, written as 0 |
| 16 | 512 | devices |
Array of up to 16 ChainDeviceInfo entries |
Total struct size: 528 bytes (16 header + 16 x 32 device entries). The
memory allocation at CHAIN_CONFIG_ADDR is 1024 bytes
(CHAIN_CONFIG_MAX_SIZE) to allow for future growth.
ChainDeviceInfo (32 bytes per device)¶
Each device entry starts at offset 16 + (device_index * 32) within the
chain config.
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 4 | u32 | format |
ImageFormat enum value (see below) |
| 4 | 4 | u32 | flags |
Feature flags from the info operation |
| 8 | 8 | u64 | virtual_size |
Virtual size in bytes |
| 16 | 8 | u64 | actual_size |
Real file size in bytes (see note below) |
| 24 | 4 | u32 | cluster_size |
Cluster/grain size in bytes (0 for raw) |
| 28 | 4 | u32 | _reserved |
Reserved, written as 0 |
Device Indexing¶
- Device 0 is always the top (primary) image
- Devices 1 through N-1 are backing files in chain order (higher index = closer to the base image)
- The base image (no backing file) is at index
device_count - 1
ImageFormat Values¶
| Value | Variant | Description |
|---|---|---|
| 0 | Unknown |
Format could not be determined |
| 1 | Raw |
Raw disk image |
| 2 | Qcow2 |
QCOW2 v2/v3 |
| 3 | Vmdk4 |
VMDK version 4 (monolithicSparse, streamOptimized) |
| 4 | Vmdk3 |
VMDK version 3 (COWD) |
| 5 | Vhd |
VHD/VPC |
| 6 | Vhdx |
VHDX |
| 7 | Qcow1 |
QCOW version 1 |
| 8 | Vdi |
VDI (VirtualBox) |
| 9 | Qed |
QED (deprecated) |
| 10 | Iso |
ISO 9660 |
| 11 | Luks |
LUKS encrypted container |
Feature Flags¶
The flags field uses the same bit definitions as InfoResult:
| Bit | Constant | Meaning |
|---|---|---|
| 0 | FLAG_HAS_BACKING_FILE |
Image references a backing file |
| 1 | FLAG_HAS_EXTERNAL_DATA |
Image has an external data file |
| 2 | FLAG_ENCRYPTED |
Image is encrypted |
| 3 | FLAG_COMPRESSED |
Image has compressed clusters/grains |
| 4 | FLAG_HAS_SNAPSHOTS |
Image contains snapshots |
| 5 | FLAG_DIRTY |
Dirty bit set (unclean shutdown) |
| 6 | FLAG_CORRUPT |
Corrupt bit set |
| 7 | FLAG_HAS_MBR |
Raw image has MBR partition table |
| 8 | FLAG_HAS_GPT |
Raw image has GPT partition table |
VMM to Guest Data Flow¶
1. Chain Discovery¶
The VMM discovers the backing chain by running sandboxed info operations
on each image (see chain-discovery.md). This
produces a BackingChain containing ChainImage entries with format,
sizes, flags, and backing file paths. Source:
discover_backing_chain() in src/vmm/src/main.rs.
2. Populating actual_size¶
The guest info operation reports actual_size = 0 for non-QCOW2 formats
(by design -- the guest has no filesystem access). The VMM fills in the
real file size from std::fs::metadata() when the guest reports 0:
let file_size = std::fs::metadata(&path)
.map(|m| m.len()).unwrap_or(0);
let actual_size = if info_result.actual_size > 0 {
info_result.actual_size
} else {
file_size
};
This is critical for formats that locate structures relative to EOF
(e.g. streamOptimized VMDK footer at file_size - 1024). Using
capacity * sector_size instead would overshoot when the file size
doesn't evenly divide the sector size.
3. Writing to Guest Memory¶
The VMM writes the chain config via write_chain_config() in
src/vmm/src/main.rs. The function:
- Writes the 16-byte header (magic, device_count, version, reserved)
at
CHAIN_CONFIG_ADDR - Iterates over chain images and writes each 32-byte
ChainDeviceInfoatCHAIN_CONFIG_ADDR + 16 + (i * 32) - All writes use
guest_mem.write_obj()for type-safe memory access
Guest-Side Access¶
Operations read the chain config in one of two ways:
Direct Memory Cast¶
Most operations cast CHAIN_CONFIG_ADDR directly to a ChainConfig
reference:
let chain_config = &*(CHAIN_CONFIG_ADDR as *const ChainConfig);
if !chain_config.is_valid() {
// Error: missing or corrupt chain config
}
Call Table Function¶
The check operation uses the call table's get_chain_config() function,
which returns a ConfigResult with a pointer and length:
let chain_result = (call_table.get_chain_config)();
if !chain_result.ptr.is_null() && chain_result.len > 0 {
let chain_config = &*(chain_result.ptr as *const ChainConfig);
// ...
}
Validation¶
ChainConfig::is_valid() checks that magic == 0x4348414E and
device_count > 0. Operations should always validate before accessing
device entries.
Per-Operation Usage¶
| Operation | How it uses chain config |
|---|---|
| convert | Reads each device's format to select the appropriate chain reader (QCOW2 cluster lookup, VMDK grain lookup, or raw sector read). Walks the chain to flatten backing files into a standalone output image. |
| compare | Reads format for both comparison sides. For QCOW2/VMDK images, walks the backing chain to resolve unallocated clusters/grains before comparing virtual content. Supports multi-level chains on both sides. |
| check --chain | Validates each backing image's format consistency, virtual size, and header integrity (QCOW2 magic, version, table bounds). Reports chain errors separately from primary image errors. |
Format-Specific Notes¶
QCOW2¶
The guest info operation computes actual_size from the QCOW2 header
(L1 table extent + refcount table extent), so the VMM receives a
non-zero value directly. This matches qemu-img's "file length"
calculation for minimal QCOW2 files.
VMDK¶
The guest info operation reports actual_size = 0, so the VMM uses the
filesystem file size. The VMDK chain reader uses actual_size in
VmdkState::init() to locate the streamOptimized footer at
actual_size - 1024. Without the real file size, the footer search
would use capacity * sector_size, which overshoots for files not
aligned to the sector size boundary.
VHD (future)¶
VHD stores its footer at the last 512 bytes of the file. Like VMDK,
the chain reader will need actual_size to locate the footer correctly.
Constants Reference¶
All constants are defined in src/shared/src/lib.rs: