Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"

[[package]]
name = "crc32c"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47"
dependencies = [
"rustc_version",
]

[[package]]
name = "crc32fast"
version = "1.5.0"
Expand Down Expand Up @@ -1665,6 +1674,26 @@ dependencies = [
"vm_resource",
]

[[package]]
name = "disklayer_vhdx"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"blocking",
"disk_backend",
"disk_backend_resources",
"disk_layered",
"guestmem",
"inspect",
"pal_async",
"scsi_buffers",
"tempfile",
"thiserror 2.0.16",
"vhdx",
"vm_resource",
]

[[package]]
name = "dissimilar"
version = "1.0.10"
Expand All @@ -1688,9 +1717,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"

[[package]]
name = "elfcore"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e254a61387a9d5706e00576e8ddc08705a8ce3f2d306280459ece426378f94f2"
checksum = "0fdaa3d1c27119b3394513f4596894a40cd53cb4acec7fce636a9ca0c4abb171"
dependencies = [
"libc",
"nix 0.31.2",
Expand Down Expand Up @@ -4768,9 +4797,9 @@ dependencies = [

[[package]]
name = "ntapi"
version = "0.4.3"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
Expand Down Expand Up @@ -5474,6 +5503,7 @@ dependencies = [
"disk_backend_resources",
"disk_vhd1",
"disk_vhdmp",
"disklayer_vhdx",
"fs-err",
"get_resources",
"hypervisor_resources",
Expand Down Expand Up @@ -5532,6 +5562,7 @@ dependencies = [
"disk_vhdmp",
"disklayer_ram",
"disklayer_sqlite",
"disklayer_vhdx",
"gdma",
"guest_crash_device",
"guest_emulation_device",
Expand Down Expand Up @@ -8779,6 +8810,26 @@ dependencies = [
"zerocopy",
]

[[package]]
name = "vhdx"
version = "0.0.0"
dependencies = [
"bitfield-struct 0.11.0",
"bitvec",
"crc32c",
"event-listener",
"futures",
"guid",
"mesh",
"pal_async",
"parking_lot",
"tempfile",
"thiserror 2.0.16",
"tracing",
"windows 0.62.0",
"zerocopy",
]

[[package]]
name = "vhost_user_backend"
version = "0.0.0"
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ disk_delay = { path = "vm/devices/storage/disk_delay" }
disk_prwrap = { path = "vm/devices/storage/disk_prwrap" }
disk_striped = { path = "vm/devices/storage/disk_striped" }
disk_vhd1 = { path = "vm/devices/storage/disk_vhd1" }
disklayer_vhdx = { path = "vm/devices/storage/disklayer_vhdx" }
disk_vhdmp = { path = "vm/devices/storage/disk_vhdmp" }
disklayer_ram = { path = "vm/devices/storage/disklayer_ram" }
disklayer_sqlite = { path = "vm/devices/storage/disklayer_sqlite" }
Expand All @@ -325,6 +326,7 @@ storvsc_driver = { path = "vm/devices/storage/storvsc_driver" }
storvsp = { path = "vm/devices/storage/storvsp" }
storvsp_protocol = { path = "vm/devices/storage/storvsp_protocol" }
storvsp_resources = { path = "vm/devices/storage/storvsp_resources" }
vhdx = { path = "vm/devices/storage/vhdx" }
device_emulators = { path = "vm/devices/support/device_emulators" }
fuse = { path = "vm/devices/support/fs/fuse" }
lx = { path = "vm/devices/support/fs/lx" }
Expand Down Expand Up @@ -455,6 +457,7 @@ cc = "1.2.34"
cfg-if = "1"
clap = "4.2"
constant_time_eq = "0.3"
crc32c = "0.6"
crc32fast = { version = "1.3.2", default-features = false }
criterion = { version = "0.7", default-features = false }
crossterm = { version = "0.29.0", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions Guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
- [Serial]()
- [Graphics and Input]()
- [Storage](./reference/backends/storage.md)
- [VHDX Parser](./reference/backends/vhdx.md)
- [Networking](./reference/backends/networking.md)
- [Consomme](./reference/backends/consomme.md)
- [Architecture](./reference/architecture.md)
Expand Down
1 change: 1 addition & 0 deletions Guide/src/reference/architecture/devices/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ For the OpenHCL settings model (`StorageController`, `Lun`, `PhysicalDevice`), s
| FileDisk | [`disk_file`](https://openvmm.dev/rustdoc/linux/disk_file/index.html) | Host file | Cross-platform | Simplest backend |
| Vhd1Disk | [`disk_vhd1`](https://openvmm.dev/rustdoc/linux/disk_vhd1/index.html) | VHD1 fixed file | Cross-platform | Parses VHD footer |
| VhdmpDisk | `disk_vhdmp` | Windows vhdmp driver | Windows | Dynamic/differencing VHD/VHDX |
| VhdxDisk | [`vhdx`](../../backends/vhdx.md) | VHDX file | Cross-platform | Pure-Rust VHDX parser |
| BlobDisk | [`disk_blob`](https://openvmm.dev/rustdoc/linux/disk_blob/index.html) | HTTP / Azure Blob | Cross-platform | Read-only, HTTP range requests |
| BlockDeviceDisk | [`disk_blockdevice`](https://openvmm.dev/rustdoc/linux/disk_blockdevice/index.html) | Linux block device | Linux | io_uring, resize via uevent, PR passthrough |
| NvmeDisk | [`disk_nvme`](https://openvmm.dev/rustdoc/linux/disk_nvme/index.html) | Physical NVMe (VFIO) | Linux/Windows | User-mode NVMe driver, resize via AEN |
Expand Down
1 change: 1 addition & 0 deletions Guide/src/reference/backends/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ blob, or a layered composition of multiple backends.
| FileDisk | [`disk_file`](https://openvmm.dev/rustdoc/linux/disk_file/index.html) | Host file | Cross-platform | Simplest backend. Blocking I/O via `unblock()`. |
| Vhd1Disk | [`disk_vhd1`](https://openvmm.dev/rustdoc/linux/disk_vhd1/index.html) | VHD1 fixed file | Cross-platform | Parses VHD footer for geometry. |
| VhdmpDisk | `disk_vhdmp` | Windows vhdmp driver | Windows | Dynamic and differencing VHD/VHDX. |
| VhdxDisk | [`vhdx`](vhdx.md) | VHDX file | Cross-platform | Pure-Rust VHDX parser. Dynamic, fixed, and differencing. |
| BlobDisk | [`disk_blob`](https://openvmm.dev/rustdoc/linux/disk_blob/index.html) | HTTP / Azure Blob | Cross-platform | Read-only. HTTP range requests. |
| BlockDeviceDisk | [`disk_blockdevice`](https://openvmm.dev/rustdoc/linux/disk_blockdevice/index.html) | Linux block device or file | Linux | io_uring, resize via uevent, PR passthrough. Default for raw files on Linux in both OpenHCL and OpenVMM. |
| NvmeDisk | [`disk_nvme`](https://openvmm.dev/rustdoc/linux/disk_nvme/index.html) | Physical NVMe (VFIO) | Linux/Windows | User-mode NVMe driver. Resize via AEN. |
Expand Down
109 changes: 109 additions & 0 deletions Guide/src/reference/backends/vhdx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# VHDX parser

The `vhdx` crate (`vm/devices/storage/vhdx/`) is a pure-Rust
implementation of the
[VHDX format specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-vhdx/).
It supports dynamic, fixed, and differencing VHDX virtual hard disk
files on all platforms — no Windows APIs or kernel drivers required.

## Features

- **Create** and **open** VHDX files (read-only or writable)
- Dynamic block allocation with four-priority free space management
- Write-ahead log (WAL) for crash-consistent metadata updates
- Sector bitmap tracking for partially-present (differencing) blocks
- Block trim/unmap with multiple modes (file space, free space, zero,
transparent, soft-anchor removal)
- Concurrent flush coalescing
- Parent locator parsing for differencing disk chains

## Architecture

A VHDX file stores a virtual disk as a collection of fixed-size data
blocks (default 2 MiB) tracked by a Block Allocation Table (BAT).
The crate's write path uses a three-stage pipeline for crash
consistency:

```text
┌───────────┐ commit ┌──────────┐ apply ┌────────────┐
│ Cache │ ──────────►│ Log Task │ ─────────►│ Apply Task │
│ (dirty │ dirty │ (WAL │ logged │ (final │
│ pages) │ pages │ writer) │ pages │ offsets) │
└───────────┘ └──────────┘ └────────────┘
```

1. The **cache** accumulates dirty 4 KiB metadata pages (BAT entries,
sector bitmap bits). When the dirty count reaches a threshold or
`flush()` is called, pages are committed to the log task.
2. The **log task** writes WAL entries to the circular log region in
the VHDX file. On crash, `replay_log()` restores metadata from
the WAL.
3. The **apply task** writes logged pages to their final file offsets.

Backpressure is managed by a permit semaphore that limits in-flight
pages. A flush sequencer coalesces concurrent flush requests so at
most one file flush is in progress at a time.

## Lifecycle

```rust,ignore
// Create a new empty VHDX file.
create::create(&file, &mut params).await?;

// Open for writing.
let vhdx = VhdxFile::open(file)
.block_alignment(2 * 1024 * 1024)
.writable(&spawner)
.await?;

// Resolve a read — returns file-level ranges.
let mut ranges = Vec::new();
let guard = vhdx.resolve_read(offset, len, &mut ranges).await?;
// ... perform file I/O at the returned offsets ...
drop(guard);

// Resolve a write — returns file-level ranges + I/O guard.
let mut ranges = Vec::new();
let guard = vhdx.resolve_write(offset, len, &mut ranges).await?;
// ... write data at the returned offsets ...
guard.complete().await?;

// Flush to stable storage.
vhdx.flush().await?;

// Clean close (clears log GUID).
vhdx.close().await?;
```

## I/O model

The crate separates **metadata I/O** from **payload I/O**.

Metadata I/O (headers, BAT pages, sector bitmaps, WAL entries) is
handled internally through the `AsyncFile` trait — the caller provides
an `AsyncFile` implementation at open time and never thinks about
metadata again.

Payload I/O (guest data reads and writes) is the caller's
responsibility. `resolve_read()` and `resolve_write()` translate
virtual disk offsets into file-level byte ranges (`ReadRange` /
`WriteRange`). The caller performs its own data I/O at those offsets
using whatever mechanism it prefers (io_uring, standard file I/O,
etc.), then finalizes metadata via the returned I/O guard. This
separation lets the caller use a different, potentially more
performant I/O path for bulk data without the crate imposing any
particular strategy.

- The `vhdx` crate provides the low-level VHDX format implementation
and I/O resolution API. For OpenVMM integration, the `disklayer_vhdx`
crate supplies a `LayerIo`-compatible backend used in the layered
disk storage pipeline.
- For differencing disks, the `vhdx` crate parses parent locator
metadata, while `disklayer_vhdx::chain::open_vhdx_chain` walks and
opens parent chains automatically.

## Related pages

- [Storage backends](./storage.md) — catalog of all storage backends
- [Storage pipeline](../architecture/devices/storage.md) — how
frontends, backends, and layers connect
2 changes: 1 addition & 1 deletion Guide/src/reference/openvmm/management/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ as well as the generated CLI help (via `cargo run -- --help`).
pass `--hv`. The `DISK` argument can be:
* A flat binary disk image
* A VHD file with an extension of .vhd (Windows host only)
* A VHDX file with an extension of .vhdx (Windows host only)
* A VHDX file with an extension of .vhdx

On Linux, raw files and block devices use the `disk_blockdevice` backend
(io_uring-based async I/O) by default. Append `;direct` to the path to
Expand Down
14 changes: 8 additions & 6 deletions Guide/src/user_guide/openvmm/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ docs.

The file `windows.vhdx` can be any format of VHD(X).

Note that OpenVMM does not currently support using dynamic VHD/VHDX files on
Linux hosts. Unless you have a fixed VHD1 image, you will need to convert the
image to raw format, using the following command:
VHDX files (dynamic, fixed, and differencing) are supported on non-Windows
platforms via the pure-Rust [`vhdx`](../../reference/backends/vhdx.md)
parser. On Windows, `.vhdx` files use the native kernel-mode VHD path
instead. Fixed VHD1 images work on all platforms. Dynamic and differencing VHD1
files are **not** supported — convert them to VHDX first:

Comment thread
jstarks marked this conversation as resolved.
```shell
qemu-img convert -f vhdx -O raw windows.vhdx windows.img
```bash
qemu-img convert -f vpc -O vhdx dynamic.vhd converted.vhdx
```

Also, note the use of `memdiff`, which creates a memory-backed "differencing
Expand All @@ -157,7 +159,7 @@ integration tests.
First, build the test artifacts from Linux or WSL using `vmm-tests-run --build-only`.
The IGVM must be built on Linux:

```shell
```bash
cargo xflowey vmm-tests-run --build-only --dir <out> --target windows-x64
```

Expand Down
1 change: 1 addition & 0 deletions openvmm/openvmm_helpers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rust-version.workspace = true
[dependencies]
disk_backend_resources.workspace = true
disk_vhd1.workspace = true
disklayer_vhdx.workspace = true
get_resources.workspace = true
hypervisor_resources.workspace = true
openvmm_defs.workspace = true
Expand Down
13 changes: 10 additions & 3 deletions openvmm/openvmm_helpers/src/disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ pub struct OpenDiskOptions {
/// Opens the resources needed for using a disk from a file at `path`.
///
/// If the file ends with .vhd and is a fixed VHD1, it will be opened using
/// the user-mode VHD parser. Otherwise, if the file ends with .vhd or
/// .vhdx, the file will be opened using the kernel-mode VHD parser.
/// the user-mode VHD parser. Otherwise, if the file ends with .vhd, the
/// file will be opened using the kernel-mode VHD parser (Windows only).
///
/// If the file ends with .vhdx, the kernel-mode VHD parser is used on
/// Windows. On Linux, the pure-Rust VHDX parser is used, with automatic
/// parent-locator walking for differencing chains.
Comment thread
jstarks marked this conversation as resolved.
Comment thread
jstarks marked this conversation as resolved.
pub async fn open_disk_type(
path: &Path,
options: OpenDiskOptions,
Expand Down Expand Up @@ -91,7 +95,10 @@ pub async fn open_disk_type(
))
}
#[cfg(not(windows))]
anyhow::bail!("VHDX not supported on Linux");
{
ensure_no_direct(".vhdx")?;
disklayer_vhdx::chain::open_vhdx_chain(path, read_only).await?
}
}
Some("iso") if !read_only => {
anyhow::bail!("iso file cannot be opened as read/write")
Expand Down
1 change: 1 addition & 0 deletions openvmm/openvmm_resources/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ disk_file.workspace = true
disk_layered.workspace = true
disk_prwrap.workspace = true
disk_vhd1.workspace = true
disklayer_vhdx.workspace = true
disklayer_ram.workspace = true
disklayer_sqlite = { workspace = true, optional = true }

Expand Down
1 change: 1 addition & 0 deletions openvmm/openvmm_resources/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ vm_resource::register_static_resolvers! {
disklayer_ram::resolver::RamDiskLayerResolver,
#[cfg(feature = "disklayer_sqlite")]
disklayer_sqlite::resolver::SqliteDiskLayerResolver,
disklayer_vhdx::resolver::VhdxDiskLayerResolver,

// PCI devices
gdma::resolver::GdmaDeviceResolver,
Expand Down
13 changes: 13 additions & 0 deletions vm/devices/storage/disk_backend_resources/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,16 @@ pub struct SqliteAutoCacheDiskLayerHandle {
impl ResourceId<DiskLayerHandleKind> for SqliteAutoCacheDiskLayerHandle {
const ID: &'static str = "sqlite-autocache";
}

/// Handle for a VHDX disk layer.
#[derive(MeshPayload)]
pub struct VhdxDiskLayerHandle {
/// The open file handle for the VHDX file.
pub file: std::fs::File,
/// Whether to open the VHDX as read-only.
pub read_only: bool,
}

impl ResourceId<DiskLayerHandleKind> for VhdxDiskLayerHandle {
const ID: &'static str = "vhdx";
}
Loading
Loading