Compare commits

...

609 Commits
v52.0 ... v53.0

Author SHA1 Message Date
Rob Bradford
9ed824d6d0 build: Release v53.0
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-12 19:47:13 +00:00
Rob Bradford
58306b6f28 vmm: Support setting seccomp to errno
This will generate -EPERM on seccomp violations as opposed to causing
the VMM to exit with SIGSYS.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-10 21:12:26 +00:00
dependabot[bot]
9c5180fc2c build(deps): bump lycheeverse/lychee-action from 2.8.0 to 2.9.0
Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/lycheeverse/lychee-action/releases)
- [Commits](8646ba3053...e747777578)

---
updated-dependencies:
- dependency-name: lycheeverse/lychee-action
  dependency-version: 2.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 08:00:15 +00:00
dependabot[bot]
8588981863 build(deps): bump the non-rust-vmm group across 2 directories with 13 updates
Bumps the non-rust-vmm group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [zerocopy](https://github.com/google/zerocopy) | `0.8.52` | `0.8.53` |
| [crc-any](https://github.com/magiclen/crc-any) | `2.5.1` | `3.0.0` |
| [rand](https://github.com/rust-random/rand) | `0.10.1` | `0.10.2` |
| [ssh2](https://github.com/alexcrichton/ssh2-rs) | `0.9.5` | `0.9.6` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.65` | `1.2.66` |
| [crossbeam-utils](https://github.com/crossbeam-rs/crossbeam) | `0.8.21` | `0.8.22` |
| [defmt](https://github.com/knurling-rs/defmt) | `1.1.0` | `1.1.1` |
| [jobserver](https://github.com/rust-lang/jobserver-rs) | `0.1.34` | `0.1.35` |
| libredox | `0.1.17` | `0.1.18` |
| [rustls-pki-types](https://github.com/rustls/pki-types) | `1.14.1` | `1.15.0` |

Bumps the non-rust-vmm group with 5 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [zerocopy](https://github.com/google/zerocopy) | `0.8.52` | `0.8.53` |
| [rand](https://github.com/rust-random/rand) | `0.10.1` | `0.10.2` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.65` | `1.2.66` |
| [jobserver](https://github.com/rust-lang/jobserver-rs) | `0.1.34` | `0.1.35` |
| [rustls-pki-types](https://github.com/rustls/pki-types) | `1.14.1` | `1.15.0` |



Updates `zerocopy` from 0.8.52 to 0.8.53
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.52...v0.8.53)

Updates `crc-any` from 2.5.1 to 3.0.0
- [Commits](https://github.com/magiclen/crc-any/compare/v2.5.1...v3.0.0)

Updates `rand` from 0.10.1 to 0.10.2
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.1...0.10.2)

Updates `ssh2` from 0.9.5 to 0.9.6
- [Commits](https://github.com/alexcrichton/ssh2-rs/compare/0.9.5...0.9.6)

Updates `cc` from 1.2.65 to 1.2.66
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.65...cc-v1.2.66)

Updates `crossbeam-utils` from 0.8.21 to 0.8.22
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/main/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-utils-0.8.21...crossbeam-utils-0.8.22)

Updates `defmt` from 1.1.0 to 1.1.1
- [Release notes](https://github.com/knurling-rs/defmt/releases)
- [Changelog](https://github.com/knurling-rs/defmt/blob/main/CHANGELOG.md)
- [Commits](https://github.com/knurling-rs/defmt/compare/defmt-v1.1.0...defmt-v1.1.1)

Updates `defmt-macros` from 1.1.0 to 1.1.1
- [Release notes](https://github.com/knurling-rs/defmt/releases)
- [Changelog](https://github.com/knurling-rs/defmt/blob/main/CHANGELOG.md)
- [Commits](https://github.com/knurling-rs/defmt/compare/defmt-macros-v1.1.0...defmt-macros-v1.1.1)

Updates `jobserver` from 0.1.34 to 0.1.35
- [Commits](https://github.com/rust-lang/jobserver-rs/compare/0.1.34...0.1.35)

Updates `libredox` from 0.1.17 to 0.1.18

Updates `libssh2-sys` from 0.3.1 to 0.3.2
- [Commits](https://github.com/alexcrichton/ssh2-rs/compare/libssh2-sys-0.3.1...libssh2-sys-0.3.2)

Updates `rustls-pki-types` from 1.14.1 to 1.15.0
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.14.1...v/1.15.0)

Updates `zerocopy-derive` from 0.8.52 to 0.8.53
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.52...v0.8.53)

Updates `zerocopy` from 0.8.52 to 0.8.53
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.52...v0.8.53)

Updates `rand` from 0.10.1 to 0.10.2
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.1...0.10.2)

Updates `cc` from 1.2.65 to 1.2.66
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.65...cc-v1.2.66)

Updates `jobserver` from 0.1.34 to 0.1.35
- [Commits](https://github.com/rust-lang/jobserver-rs/compare/0.1.34...0.1.35)

Updates `rustls-pki-types` from 1.14.1 to 1.15.0
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.14.1...v/1.15.0)

Updates `zerocopy-derive` from 0.8.52 to 0.8.54
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.52...v0.8.53)

---
updated-dependencies:
- dependency-name: zerocopy
  dependency-version: 0.8.53
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crc-any
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: non-rust-vmm
- dependency-name: rand
  dependency-version: 0.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: ssh2
  dependency-version: 0.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.66
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crossbeam-utils
  dependency-version: 0.8.22
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: defmt
  dependency-version: 1.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: defmt-macros
  dependency-version: 1.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: jobserver
  dependency-version: 0.1.35
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: libredox
  dependency-version: 0.1.18
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: libssh2-sys
  dependency-version: 0.3.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rustls-pki-types
  dependency-version: 1.15.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.53
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.53
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rand
  dependency-version: 0.10.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.66
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: jobserver
  dependency-version: 0.1.35
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rustls-pki-types
  dependency-version: 1.15.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.54
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 00:53:35 +00:00
Wei Liu
4fe133d2bd tests: add KDNET over virtio-net integration test
Add a Windows integration test that verifies kernel network debugging
(KDNET) works over a Cloud Hypervisor virtio-net device.

The test boots a Windows guest with a dedicated second virtio-net NIC,
enables KDNET on it via bcdedit (selecting the adapter by the PCI bus
params discovered over SSH), reboots, and then listens on the debugger
host address. Receiving a KDNET poll datagram from the debuggee proves
the whole virtio-net device path works: discovery, feature negotiation,
virtqueue setup and the TX doorbell. No debugger is needed because KDNET
connections are initiated by the target.

Gated to x86-64, where the Windows image ships the virtio-net KDNET
module. The test exercises only the generic virtio-net doorbell path,
so it runs under both KVM and MSHV.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:Opus-4.8
2026-07-09 21:06:20 +00:00
Wei Liu
ab1ba13f7e docs: document KDNET over virtio-net
Windows can run its KDNET kernel-debugging transport over a Cloud
Hypervisor virtio-net device. Add a guide describing how it works.

Link to the new guide from the Windows support document.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:Opus-4.8
2026-07-09 21:06:20 +00:00
Wei Liu
1f487fbc18 docs: split Windows serial debugging into its own file
The Windows serial (COM/KDCOM) kernel debugging instructions lived in a
long subsection of the Windows support document. Move them verbatim
into a dedicated docs/windows-kdcom-debugging.md and leave a short
pointer in windows.md, so the debugging methods can be documented and
extended independently.

Drop the useless disclaimer. Things should work the same across
different Windows versions.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:Opus-4.8
2026-07-09 21:06:20 +00:00
Wei Liu
85c0725f5c virtio-devices: signal queue eventfd for PCI_CFG doorbells
A virtqueue notification (doorbell) is normally delivered to the device
through an ioeventfd registered on the notify address, so a plain MMIO
write to the notify register is consumed by the hypervisor and never
reaches write_bar().

It does reach write_bar() when the driver rings the doorbell through the
VIRTIO_PCI_CAP_PCI_CFG window (write_cap_pci_cfg -> write_bar) instead
of a mapped BAR, or on backends that deliver the write to the VMM such
as SEV-SNP.

The notification arm of write_bar() only re-signalled the matching
queue eventfd under the sev_snp feature and logged an error
otherwise, so a doorbell delivered through the PCI_CFG window was
silently dropped on standard builds and the queue was never
processed.

The virtio spec allows driving the device purely through the PCI_CFG
window, so signal the matching queue eventfd for any doorbell that
reaches write_bar() on every build.

Add unit tests that ring a queue's doorbell via write_bar() and
assert only the addressed queue's eventfd is signalled.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:Opus-4.8
2026-07-09 21:06:20 +00:00
Wei Liu
ae3c517368 scripts: fix Windows test dm/loopback cleanup
The Windows integration test scripts tore down their device-mapper
snapshot and loop devices with 'dmsetup remove_all -f' and 'losetup
-D'. Both operate on every such device on the host, not just the ones
the script created.

On a host whose root filesystem is device-mapper backed (for example an
LVM root), 'dmsetup remove_all -f' replaces the in-use root device's
table with an error target. The host is wedged until reboot.  This is
harmless in CI, which runs in a disposable VM, but destroys a developer
machine.

It turns out windows-snapshot-base is not used at all. Drop it.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:Opus-4.8
2026-07-09 21:06:20 +00:00
Rob Bradford
5ce8732196 vmm: openapi: Correct vmm.nmi endpoint to vm.nmi
This is a VM operation not a VMM operation and was wrongly recorded in
the openapi YAML file.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-09 17:17:41 +00:00
Pulak Kanti Bhowmick
03e229786d vmm: memory_manager: use available_parallelism for prefault threads
Use std::thread::available_parallelism() when sizing the prefault
worker threads so the number of threads scales with the host CPU
count instead of relying on unsafe sysconf.

Fixes: #8495

Signed-off-by: Pulak Kanti Bhowmick <pkbhowmick007@gmail.com>
2026-07-09 11:55:43 +00:00
Rob Bradford
818fc07266 vmm: config: Fix generic vhost-user parsing
The generic vhost-user device took its virtio device type on the
command line via the `virtio_id` parameter, but the same value is
called `device_type` in the API and the resulting config struct. This
irregularity was due to churn during the review process, `device_type`
was the intended name.

Accept `device_type` on the command line and keep `virtio_id` as a
deprecated alias that logs a warning. The alias will then be removed in
a later release.

Fixes: #8545

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-09 10:56:46 +00:00
Muminul Islam
30c0fdaff1 hypervisor: mshv: splice GVA page offset into translated GPA
On Intel MSHV the memory-intercept guest_physical_address and the
MSHV_VP_TRANSLATE_GVA ioctl both return a page-aligned GPA, while
guest_virtual_address is byte-exact. Returning the cached/translated
GPA unchanged made byte-sized MMIO land at BAR offset 0: virtio
device_status writes (BAR+0x14) hit device_feature_select, so
VIRTIO_F_VERSION_1 was never acked and virtio_blk/net/rng probes
failed with -EINVAL, leaving the guest unable to mount rootfs.

Splice gva & 0xfff into the returned GPA on both the intercept fast
path and the translate_gva fallback, and relax the cached-GVA match
to page granularity so it still hits for other byte offsets in the
same page.

This issue is reproducible on Intel machine, launching
Cloud-Hypervisor on nested scenario, using the Linux Dom0
image as the guest image to turn on nested hypervisor
into the guest.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-07-09 03:52:45 +00:00
Rob Bradford
5b53f4202d vmm: Reject removal of already removed devices
A hot-unplug leaves the PCI node in the device tree until the guest
acknowledges the ejection, but VmConfig drops the device entry
immediately.

Move the config removal into DeviceManager::remove_device() and fail
when it returns false, so a second remove-device request cannot reuse
the stale device-tree node.

Assisted-by: OpenAI:Codex-GPT-5
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 21:42:49 +00:00
Wei Liu
b2d1065a55 build: disallow building tdx feature
It is broken. There is no use in producing something that doesn't work.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-07-08 20:04:25 +00:00
Alyssa Ross
e8f021b05e vmm: use let/else instead of if/unwrap
There shouldn't have been a safety comment here to begin with, as it's
not unsafe to call unwrap.

Signed-off-by: Alyssa Ross <hi@alyssa.is>
2026-07-08 18:59:36 +00:00
Henry Hrvoje Tonkovac
d897db17e2 vmm: de-duplicate common_thread_rules syscalls
common_thread_rules() holds the syscalls every thread needs for the
SIGSYS violation handler added in #8449: the handler prints a
diagnostic to stderr (write()) after identifying the offending
thread (gettid()).

#8449 added gettid() to common_thread_rules() but left the per-thread
copies in place (9 rule sets still list it, 2 do not). write() is
likewise listed unconditionally in every thread's rule set.

Add write() to common_thread_rules() and drop the per-thread
duplicates of both gettid() and write(). Every thread already allowed
both, so there is no change in behaviour.

write() suggested by @phip1611 in #8490.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-07-08 18:15:37 +00:00
Bo Chen
595a24d270 build: Mark vfio runner as required for MQ
Across the last 20 MQ runs, all 13 vfio runner failures came from two
flaky tests. Both are now skipped and tracked in #8548 and #8549.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-07-08 17:55:35 +00:00
Rob Bradford
19289a3b82 tests: Add integration tests to snapshot after/during restore
Check that we can make a successful snapshot (and restore it) after
another restore. Also check that snapshot it refused until restore is
complete.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 17:30:26 +00:00
Rob Bradford
6de90bdec6 vmm: Error out on migration & snapshot if on-demand restoring
If there is an active on-demand restoration then reject any requests to
migrate or snapshot this VM as the memory will not be available for the
snapshot.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 17:30:26 +00:00
Rob Bradford
b38ae72a5a vmm: Record when the on-demand memory restore is done
In order to deny migration or snapshot when currently doing an on-demand
restore it is necessary to track whether the prefaulting is completed.
This is a proxy for on-demand restoring being completed as pages that
have been restored by a userfaultfd request will excluded from the
prefault set.

Fixes: #8525

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 17:30:26 +00:00
Rob Bradford
1ba5f15198 block: Check request total length is a multiple of SECTOR_SIZE
The request can be spread over multiple descriptors but the virtio-block
specification (and this code) expects that is a whole number of sectors
(512 bytes).

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 17:11:59 +00:00
Rob Bradford
5b99f9ce41 block: qcow: Preserve the compression type when writing the header
When writing the header after a resize the compression type field was
always set to 0, which selects zlib, even when the image was originally
created with zstd. The resized image would then no longer be usable.

Write the actual configured compression type instead.

Fixes: #8558

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 15:40:26 +00:00
Rob Bradford
8d3859af0d block: qcow: Read/write on disk structure via zerocopy on a struct
Use a pair of structs for the basic QCOW V2 header and V3 additional
fields and serialize them to/from disk via the zerocopy crate.

This removes the need to manage the position to read from/write to.

Assisted-by: Claude:Opus-4.8

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 15:40:26 +00:00
Anatol Belski
039b4e6013 block: vhdx: Flatten internal and worker modules
Remove the internal and worker submodule layers from the VHDX format
directory. The bat, header, io, and metadata parsers move up as
direct children, internal/mod.rs becomes parser.rs, and the sync
backend moves up as engine_sync.rs. The declaration only
worker/mod.rs is dropped.

The public types are surfaced at the vhdx module level, so callers
use block::formats::vhdx instead of reaching into the internal
module.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-08 12:33:30 +00:00
Anatol Belski
6e0c39964a block: vhd: Flatten internal and worker modules
Remove the internal and worker submodule layers from the VHD format
directory. The footer and fixed parsers move up as footer.rs and
fixed.rs, and the backends move up as engine_sync.rs and
engine_uring.rs. Both internal/mod.rs and worker/mod.rs held only
module declarations and are dropped.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-08 12:33:30 +00:00
Anatol Belski
ecf72ba787 block: raw: Flatten worker module
Remove the worker submodule layer from the raw format directory. The
backend files move up as engine_sync.rs, engine_uring.rs, and
engine_aio.rs, the shared test helpers move up as tests.rs, and the
two alignment helper functions from worker/mod.rs merge into the raw
module.

The vhd backends that reused the raw io_uring and sync engines are
updated to the new block::formats::raw::engine_* paths.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-08 12:33:30 +00:00
Anatol Belski
7120311462 block: qcow: Flatten internal and worker modules
Remove the internal and worker submodule layers from the QCOW2
format directory. The former internal files become direct children
of qcow, with internal/mod.rs turning into parser.rs. The worker
backends move up as engine_sync.rs and engine_uring.rs, and
worker/mod.rs, which held only module declarations, is dropped.

The public parser types are now surfaced at the qcow module level,
so external callers use block::formats::qcow instead of reaching
into the internal module.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-08 12:33:30 +00:00
Bo Chen
b5aeabe77c build: Exclude the qcow2 block tests from the metrics runner
The metrics test list has grown significantly and no longer finishes
even within the 60-minute timeout. Exclude the block_qcow2 group (30
tests) in addition to the micro benchmarks, leaving a 30-test set that
completes meaningfully (~41 minutes in my testing). Trimming and
re-tuning the metrics test list is tracked in #8551.

Since the underlying bare-metal system is also retired, run the metrics
tests on the garm-jammy-16 (Azure VM) runner instead.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-07-08 08:53:57 +00:00
Bo Chen
b353e82085 performance-metrics: Support comma-separated test filters
The '--test-filter' and '--test-exclude' arguments only accepted a
single keyword, even though the underlying selection logic already
matches against a list. Add a comma value delimiter to both so multiple
keywords can be passed in one invocation.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-07-08 08:53:57 +00:00
Rob Bradford
ad3dbcd130 virtio-devices: Use SmallVec for descriptor chains
Rather than instantiating a vector for parsing the descriptor chain in
advance instead use a SmallVec bounded by the expected length of the
descriptor chain. This removes vector allocations from those paths.

As smallvec was already a block dependency move it to a workspace
dependency and use it from there.

Fixes: #5079

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-08 02:15:19 +00:00
Henry Hrvoje Tonkovac
ba3cfd7d9d virtio-devices: restrict vhost/vsock worker socket() to AF_UNIX
The vhost-user (fs, net, block, generic) and vsock worker threads allow
socket() unconditionally in their seccomp rules. These threads only ever
open AF_UNIX sockets: the vhost-user transport connects/binds a Unix
socket (via the vhost crate's Endpoint/Listener), and the vsock device's
host side is a Unix socket. None of them open AF_INET/AF_INET6 sockets.

Restrict their socket() rule to AF_UNIX. Because these threads are
spawned by the VMM thread and inherit its filter, socket() was already
limited to the VMM's set (AF_UNIX/AF_INET/AF_INET6); this narrows it
further to just AF_UNIX, so each worker is confined to what it actually
uses rather than the broader inherited set.

Related to #8490 (giving each thread a more restrictive filter than the
VMM thread).

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-07-07 19:09:15 +00:00
Alyssa Ross
d4660b4fc5 vmm: api: fix parameter name
Fixes: 085a7a49f ("vmm: generic vhost-user: add support")
Signed-off-by: Alyssa Ross <hi@alyssa.is>
2026-07-07 15:58:09 +01:00
Alyssa Ross
43eb6f08a5 vmm: openapi: fix GenericVhostUserConfig properties
Fixes: df86b2864 ("vmm: add HTTP API endpoints for generic vhost-user")
Signed-off-by: Alyssa Ross <hi@alyssa.is>
2026-07-07 14:07:09 +00:00
Anatol Belski
72796d62eb net_util: queue_pair: Use vnet_hdr_len when locating num_buffers
num_buffers sits at offset 10 of the virtio net header, so only
vnet_hdr_len() bytes need to translate contiguously to compute
its host address. Shrink the translate_gva length from desc.len()
to vnet_hdr_len() so the request matches what is actually read.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-07 12:54:03 +00:00
Alexander Lvov
fa7cad4aee block: vhdx: enable bounds check in sync I/O worker
VhdxSync::submit_data_operation() passed every read/write straight to
the underlying Vhdx without checking the request against the virtual
disk's logical size. A request that started inside the image but
extended past its end (or an offset past the end entirely) was passed
through unchecked, silently reading/writing out of the intended
bounds.

Call AsyncIoOperation::validate_bounds() from submit_data_operation()
before dispatching the operation, the same way the VHD sync worker
does. The check rejects any request whose offset + length exceeds the
logical size.

Signed-off-by: Alexander Lvov <alexander.lvov.git@gmail.com>
2026-07-07 09:56:20 +00:00
Alexander Lvov
4bb3e1ca04 block: vhd: switch to AsyncIoOperation::validate_bounds
Reuse global validate_bounds() operation helper instead of
having a local implementation in vhd/worker/common.rs

Signed-off-by: Alexander Lvov <alexander.lvov.git@gmail.com>
2026-07-07 09:56:20 +00:00
Alexander Lvov
73efde72b3 block: vhdx: extract dynamic_vhdx test helper into test_util
Extract the dynamic VHDX qemu-img helper into a shared vhdx::
test_util module to reuse inside the upcoming VhdxSync bounds-check.

Signed-off-by: Alexander Lvov <alexander.lvov.git@gmail.com>
2026-07-07 09:56:20 +00:00
Alexander Lvov
43c29096eb block: async_io: add AsyncIoOperation::validate_bounds
Implement global helper to validate vhd and vhdx sync workers'
I/O requests whose offset + length exceeds the virtual disk's
logical size.

Signed-off-by: Alexander Lvov <alexander.lvov.git@gmail.com>
2026-07-07 09:56:20 +00:00
Rob Bradford
854bd6bf65 devices: Fix clippy: unneeded late initialization
```
warning: unneeded late initialization
   --> devices/src/legacy/gpio_pl061.rs:258:9
    |
258 |         let value;
    |         ^^^^^^^^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
    = note: `-D clippy::needless-late-init` implied by `-D clippy::all`
```

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-07 08:31:22 +00:00
Rob Bradford
52f5d5fc9a pci: Fix clippy: use mem::take instead of drain().collect()
```
warning: you seem to be trying to move all elements into a new `Vec`
   --> pci/src/configuration.rs:951:24
    |
951 |                 return self.pending_bar_reprogram.drain(..).collect();
    |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `mem::take` to avoid creating a new allocation: `std::mem::take(&mut self.pending_bar_reprogram)`
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#drain_collect
    = note: `-D clippy::drain-collect` implied by `-D clippy::all`
```

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-07 08:31:22 +00:00
Rob Bradford
308033de40 block: Fix clippy: unused import
```
warning: unused import: `Bytes`
   --> block/src/io/request.rs:659:21
    |
659 |     use vm_memory::{Bytes as _, GuestMemoryMmap};
    |                     ^^^^^^^^^
    |
    = note: `-D unused-imports` implied by `-D warnings`
```

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-07 08:31:22 +00:00
Rob Bradford
f5468a6dae block: Fix clippy: chunks_exact with constant chunk size
```
warning: using `chunks_exact` with a constant chunk size
   --> block/src/formats/qcow/internal/header.rs:253:39
    |
253 |                     for entry in data.chunks_exact(FEATURE_NAME_ENTRY_SIZE) {
    |                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using `as_chunks` instead
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#chunks_exact_to_as_chunks
    = note: `-D clippy::chunks-exact-to-as-chunks` implied by `-D clippy::all`
```

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-07 08:31:22 +00:00
Wei Liu
2a3512fb71 docs: update SEV-SNP build information
sev_snp no longer selects a hypervisor backend implicitly.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-07-06 23:29:52 +00:00
Wei Liu
47e88d26e5 build: decouple igvm and sev_snp from mshv
igvm and sev_snp only need the MSHV backend when a caller selects that
backend. Stop enabling mshv implicitly from those top-level features so
KVM SEV-SNP builds do not compile unused MSHV backend code.

Require igvm and sev_snp to be built with kvm or mshv. Backend-less
configurations having no runtime hypervisor are not supported.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-07-06 23:29:52 +00:00
Wei Liu
8f38928757 ci: cover explicit igvm sev_snp backends
Decoupling removes the old implicit MSHV coverage from standalone igvm
and sev_snp jobs. Replace those jobs with explicit KVM feature sets and
add MSHV igvm and sev_snp build and clippy jobs so both backends remain
covered.

This keeps the previous implicit combinations tested while making the
selected backend visible in CI.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-07-06 23:29:52 +00:00
Wei Liu
d71ef33b83 vmm: decouple igvm loader from mshv
IGVM no longer implies the MSHV backend at the feature layer. Gate the
loader paths that use MSHV page types, MSHV SNP launch defaults, or
MSHV-specific CPUID page rewriting on the MSHV feature.

This preserves existing MSHV behavior while letting KVM SEV-SNP use the
shared IGVM loader without compiling the full MSHV backend.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-07-06 23:29:52 +00:00
Sayed Kaif
6882276e2d block: vhdx: reject overflowing region table entries
The region table overlap fix in RegionInfo::new computes each entry's
end offset as `file_offset + length`. Both values are taken verbatim
from the image, so a crafted or corrupt VHDX can set a file offset near
u64::MAX and make that addition wrap. A wrapped end offset compares as a
small value, which can slip a genuinely overlapping region past the
half-open interval check that #8483 added.

Use checked_add for the end offset and return a new RegionEntryOverflow
error when it wraps, so a malformed entry is rejected instead of being
folded into a valid-looking range. The computed end is now reused for
the region_entries map so the bound is only calculated once. Add a
regression test for a wrapping entry.

Signed-off-by: Sayed Kaif <metsw24@gmail.com>
2026-07-06 22:50:24 +00:00
Rob Bradford
4b120c3702 virtio-devices: Gate VIRTIO_NET_S_ANNOUNCE on feature acked
Only set the VIRTIO_NET_S_ANNOUNCE status bit if the feature
VIRTIO_NET_F_GUEST_ANNOUNCE was acknowledged.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-06 18:46:56 +00:00
Rob Bradford
9161b18f9b virtio-devices: iommu: Translate buffers spanning adjacent mappings
The virtio-iommu translation required the whole [addr, addr+size) span
to be covered by a single mapping. A guest is free to describe one
contiguous buffer with several adjacent mappings: the specification lets
the driver map at page granularity and the Linux IOMMU core splits a
single mapping request at page size boundaries. A descriptor buffer
backed this way was rejected as an invalid translation even though every
page was mapped, wedging the device.

Walk consecutive mappings when no single one covers the span, accepting
the translation once the mappings are adjacent in IOVA space and
contiguous in guest-physical space. A non-contiguous span cannot be
represented by the single returned address and is still rejected.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-06 18:35:11 +00:00
Anirudh Rayabharam
0b150ea560 tests: add integration test for PPTT cache topology
Add an integration test to verify the newly added code to expose
cache topology information in PPTT.

Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-07-06 16:31:21 +00:00
Anirudh Rayabharam
ec73733b21 vmm: add cache topology info to ACPI PPTT
Read the host cache info from sysfs and pass through the same to the
guest via the PPTT table. This is the same as the approach taken for
FDT. Similar to that, assume that the L3 cache is always shared and the
L2 cache is unique per CPU.

Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-07-06 16:31:21 +00:00
Anirudh Rayabharam
9446dad1d0 arch: aarch64: extract cache topology collection into a helper
Both arch/src/aarch64/fdt.rs and vmm/src/cpu.rs declared the same
~14 cache info locals (size/line_size/sets for L1D/L1I/L2/L3, plus
shared flags for L2/L3), checked for /sys/devices/system/cpu/cpu0/cache,
and populated those locals via get_cache_size /
get_cache_coherency_line_size / get_cache_number_of_sets /
get_cache_shared. Two near-identical ~30-line blocks.

Add a CacheTopologyInfo struct in arch/src/aarch64/cache.rs whose
field names mirror the existing locals, plus a read_cache_topology()
helper that returns None when the sysfs hierarchy is missing and
only queries get_cache_shared for L2/L3 when their size is non-zero
(preserving current behavior).

Both call sites now obtain the info via read_cache_topology() and
destructure it back into locals of the same name, so all downstream
references are unchanged.

Assisted-by: Copilot-CLI:Claude-Opus-4.7
Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-07-06 16:31:21 +00:00
Anirudh Rayabharam
c30732c9c1 arch: use bit-shift expressions for cache size units
Replace 1024, 1024u32.pow(2), and 1024u32.pow(3) with 1u32 << 10,
1u32 << 20, and 1u32 << 30 in get_cache_size. The shift form makes
the binary (KiB/MiB/GiB) nature of the conversion immediately
obvious and is easier to read at a glance.

Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-07-06 16:31:21 +00:00
Anirudh Rayabharam
2bb02d51ed arch: aarch64: move cache helpers to a new file
fdt.rs has helper functions to query host cache details (topology, size
etc.). Extract these helpers to a new file cache.rs so that they can be
used for PPTT construction as well.

No functional change.

Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-07-06 16:31:21 +00:00
Anirudh Rayabharam
f5089c705b vmm: use acpi_tables helpers for PPTT
Use the helpers from the acpi_tables crate to construct the PPTT. This
is in preparation for adding cache hierarchy info to the PPTT which is
simpler using the helpers.

Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-07-06 16:31:21 +00:00
doge
3d5f06ff03 vmm: acpi: provide FADT PM1a event/control blocks for nested Hyper-V
A Windows guest that launches nested Hyper-V (for example to run WSL2)
fails to start its hypervisor on cloud-hypervisor's HW-reduced-ACPI
FADT. hvloader's hypervisor-launch path (0x18000f01c -> 0x180015628 ->
0x180015788) registers every legacy PM register block via 0x1800158dc
and rejects any block whose GAS address is 0 with status 8
(STATUS_INVALID_DEVICE_REQUEST). hvix64 then never launches and
HypervisorPresent stays False. The HW-reduced FADT leaves those blocks
zero.

Emit valid PM1a event/control blocks (I/O ports, lengths and X_GAS) in
the FADT and reserve those ports in the I/O allocator so nothing else
claims them. The HW-reduced guest OS ignores the legacy ports; only
hvloader's ACPI validation reads them.

These blocks are only useful to a guest that itself runs an enlightened
hypervisor, so emit them only when both guest nesting and the Hyper-V
enlightenments are enabled (--cpu nested=on,kvm_hyperv=on).

Signed-off-by: doge <me@crackerben.com>
2026-07-06 14:42:11 +00:00
Gauthier Jolly
5b3416adf4 ci: Add riscv64 cross-build check
The previous CI (removed in #7760) built natively inside a riscv64 QEMU
VM over SSH on a self-hosted host. That setup was disabled for chronic
infrastructure flakiness (#7758).

Reintroduce coverage deliberately minimal: cross-compile the
cloud-hypervisor binary on a stock ubuntu-latest runner, and nothing
more.

- No integration tests: no riscv64 hardware needed.
- No clippy: cross clippy with -D warnings currently trips on
  riscv64-gated lints that need a separate cleanup commit first.

Cross-compiling needs only a riscv64 linker, not Docker/cross: the
binary does not pull openssl-sys (that arrives via a dev-dependency of
test_infra, unused by -p cloud-hypervisor), so no cross sysroot is
required.

Signed-off-by: Gauthier Jolly <contact@gjolly.fr>
2026-07-06 14:41:40 +00:00
Anatol Belski
67095b0da1 block: Drop the AlignedFile Read, Write, and Seek impls
Nothing reads or writes the AlignedFile through a cursor anymore, so
remove the Read, Write, and Seek impls together with the in memory
position field. SeekHole no longer tracks a position. The cursor unit
tests move to read_at and write_at, dropping the one that duplicated
existing coverage.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-06 14:41:08 +00:00
Anatol Belski
4596c502fb vhost_user_block: Query the disk size instead of seeking
The disk size in sectors was computed by seeking the AlignedFile to the
end. Use query_device_size instead, which also handles block devices,
and drop the now unused Seek and SeekFrom imports.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-06 14:41:08 +00:00
Anatol Belski
15b59fdd9f block: Execute synchronous block requests positionally
Request::execute seeked the disk to the request sector and then read or
wrote sequentially through the cursor. Walk a running offset with
read_exact_at and write_all_at instead, and flush with fsync, so the
bound becomes FileExt and FileSync rather than Seek, Read, and Write.

The seek step is gone, so drop the now unused ExecuteError::Seek.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-06 14:41:08 +00:00
Anatol Belski
06cad2cdcd block: Detect the image type positionally
detect_image_type read the first block through the AlignedFile Read
cursor. Read it with read_exact_at at offset 0 instead, so image type
detection no longer depends on the cursor.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-06 14:41:08 +00:00
Anatol Belski
26080f264a block: vhd: Read the VHD footer positionally
Read the trailing footer sector with query_device_size and
read_exact_at instead of seeking to the end of the AlignedFile and
reading through its cursor.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-06 14:41:08 +00:00
Gauthier Jolly
c257cdd695 vmm: Gate tpm import for riscv64
The `tpm` module in the `devices` crate is disabled on riscv64 (see
commit 0042447fb "devices: Disable tpm module for riscv64"), and every
use of `tpm` in the device manager is already gated behind
`#[cfg(not(target_arch = "riscv64"))]`. However, the import itself was
merged into the unconditional `use devices::{...}` line in commit
025e782e5 "vmm: trim qualified paths", which broke the riscv64 build:

    error[E0432]: unresolved import `devices::tpm`
      --> vmm/src/device_manager.rs:70:83

Split the `tpm` import out into its own line gated with
`#[cfg(not(target_arch = "riscv64"))]`, matching all of its usages.

Signed-off-by: Gauthier Jolly <contact@gjolly.fr>
2026-07-06 07:24:56 +00:00
Wei Liu
de4ff39f4d pci: align VFIO MMIO mmap to a huge page boundary
mmap(NULL, ...) only guarantees page-sized alignment, so the device
MMIO regions mapped for VFIO passthrough were not necessarily aligned
on a huge page boundary and thus could not take fast paths in the
kernel.

Align the address to the largest possible size. This allows VFIO to take
fast paths when walking the page table.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:Opus-4.8
Assisted-by: Copilot:GPT-5.5
2026-07-05 17:08:50 +00:00
Rob Bradford
14481800e3 build: Only run MSHV integration tests on MQ
There is a single runner for this so avoid overloading/queueing by
running only on MQ.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-04 18:42:59 +00:00
Anatol Belski
cbef28e5cd ci: Switch to Windows Server 25H2 for AARCH64
The updated image is configured in a same way as before

SSH and RDP are enabled.

Includes latest stable virtio-win 0.1.285 drivers.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-04 18:42:06 +00:00
Anatol Belski
899c2df248 block: qcow: Make the qcow image compressor test positional
Convert the qcow image compressor test helper to positional access
instead of the cursor. Test only change.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-03 20:29:52 +00:00
Anatol Belski
6be2a52d14 block: qcow: Make the qcow sync worker tests positional
Convert the qcow sync worker test helpers to positional access instead
of the cursor. Test only change.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-03 20:29:52 +00:00
Anatol Belski
62bdf2b209 block: qcow: Make the qcow header tests positional
Convert the qcow header test helpers to positional access instead of
the AlignedFile cursor. Test only change.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-03 20:29:52 +00:00
Anatol Belski
8c068fce5e block: qcow: Make the qcow raw file tests positional
Convert the QcowRawFile unit test verifications to positional access
instead of the cursor. Test only change.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-03 20:29:52 +00:00
Anatol Belski
aa4eb943f3 block: qcow: Access the qcow header feature bits positionally
Convert the qcow header feature bit writes to positional access and
drop the now unused Seek imports. The result is unchanged.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-03 20:29:52 +00:00
Anatol Belski
43682e22b0 block: qcow: Access the qcow header positionally
Convert the qcow header reader and writer to positional access instead
of the AlignedFile Read, Write, and Seek cursor. The writes still go
through the AlignedFile O_DIRECT bounce, so alignment is unchanged.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-03 20:29:52 +00:00
Adel-Ayoub
4abdb3419b block: Make the sparse-file size test portable across filesystems
zfs and virtiofs do not eagerly account a mode-0 fallocate() in
st_blocks, so detect those filesystems with fstatfs() and skip the
physical-size assertions there. A skip now names a proven platform
limitation instead of being inferred from the value under test, and
every assertion still runs unconditionally on ext4/xfs.

Fixes #8296

Signed-off-by: Adel-Ayoub <adelayoub.maaziz@gmail.com>
2026-07-03 16:27:55 +00:00
Sebastian Eydam
4ac2340c1f virtio-devices: net: offer VIRTIO_NET_F_GUEST_ANNOUNCE to guests
The virtio-net guest announce feature bit was accidentally dropped
during upstreaming, so the device never advertised it to guests. Restore
the feature offer for virtio-net.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-07-03 13:47:11 +00:00
dependabot[bot]
629151481b build(deps): bump the non-rust-vmm group across 2 directories with 18 updates
Bumps the non-rust-vmm group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.103` |
| [env_logger](https://github.com/rust-cli/env_logger) | `0.11.10` | `0.11.11` |
| [jiff](https://github.com/BurntSushi/jiff) | `0.2.29` | `0.2.31` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.3` | `1.23.4` |
| [crc-any](https://github.com/magiclen/crc-any) | `2.5.0` | `2.5.1` |
| [io-uring](https://github.com/tokio-rs/io-uring) | `0.7.12` | `0.7.13` |
| [arc-swap](https://github.com/vorner/arc-swap) | `1.9.1` | `1.9.2` |
| [chacha20](https://github.com/RustCrypto/stream-ciphers) | `0.10.0` | `0.10.1` |
| [debug-helper](https://github.com/magiclen/debug-helper) | `0.3.13` | `0.3.14` |
| [hashbrown](https://github.com/rust-lang/hashbrown) | `0.17.0` | `0.17.1` |
| [hybrid-array](https://github.com/RustCrypto/hybrid-array) | `0.4.12` | `0.4.13` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.102` | `0.3.103` |

Bumps the non-rust-vmm group with 8 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.103` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.3` | `1.23.4` |
| [crc-any](https://github.com/magiclen/crc-any) | `2.5.0` | `2.5.1` |
| [arc-swap](https://github.com/vorner/arc-swap) | `1.9.1` | `1.9.2` |
| [chacha20](https://github.com/RustCrypto/stream-ciphers) | `0.10.0` | `0.10.1` |
| [debug-helper](https://github.com/magiclen/debug-helper) | `0.3.13` | `0.3.14` |
| [hybrid-array](https://github.com/RustCrypto/hybrid-array) | `0.4.12` | `0.4.13` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.102` | `0.3.103` |



Updates `anyhow` from 1.0.102 to 1.0.103
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.103)

Updates `env_logger` from 0.11.10 to 0.11.11
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.10...v0.11.11)

Updates `jiff` from 0.2.29 to 0.2.31
- [Release notes](https://github.com/BurntSushi/jiff/releases)
- [Changelog](https://github.com/BurntSushi/jiff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/jiff/compare/jiff-static-0.2.29...jiff-static-0.2.31)

Updates `uuid` from 1.23.3 to 1.23.4
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4)

Updates `crc-any` from 2.5.0 to 2.5.1
- [Commits](https://github.com/magiclen/crc-any/compare/v2.5.0...v2.5.1)

Updates `io-uring` from 0.7.12 to 0.7.13
- [Commits](https://github.com/tokio-rs/io-uring/compare/v0.7.12...v0.7.13)

Updates `arc-swap` from 1.9.1 to 1.9.2
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/commits)

Updates `chacha20` from 0.10.0 to 0.10.1
- [Commits](https://github.com/RustCrypto/stream-ciphers/compare/chacha20-v0.10.0...chacha20-v0.10.1)

Updates `debug-helper` from 0.3.13 to 0.3.14
- [Commits](https://github.com/magiclen/debug-helper/compare/v0.3.13...v0.3.14)

Updates `env_filter` from 1.0.1 to 2.0.0
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/env_filter-v1.0.1...env_filter-v2.0.0)

Updates `hashbrown` from 0.17.0 to 0.17.1
- [Release notes](https://github.com/rust-lang/hashbrown/releases)
- [Changelog](https://github.com/rust-lang/hashbrown/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/hashbrown/compare/v0.17.0...v0.17.1)

Updates `hybrid-array` from 0.4.12 to 0.4.13
- [Changelog](https://github.com/RustCrypto/hybrid-array/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/hybrid-array/compare/v0.4.12...v0.4.13)

Updates `jiff-static` from 0.2.29 to 0.2.31
- [Release notes](https://github.com/BurntSushi/jiff/releases)
- [Changelog](https://github.com/BurntSushi/jiff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/jiff/compare/jiff-static-0.2.29...jiff-static-0.2.31)

Updates `js-sys` from 0.3.102 to 0.3.103
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `wasm-bindgen` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `wasm-bindgen-macro` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `wasm-bindgen-macro-support` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `wasm-bindgen-shared` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `anyhow` from 1.0.102 to 1.0.103
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.103)

Updates `uuid` from 1.23.3 to 1.23.4
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4)

Updates `crc-any` from 2.5.0 to 2.5.1
- [Commits](https://github.com/magiclen/crc-any/compare/v2.5.0...v2.5.1)

Updates `arc-swap` from 1.9.1 to 1.9.2
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/commits)

Updates `chacha20` from 0.10.0 to 0.10.1
- [Commits](https://github.com/RustCrypto/stream-ciphers/compare/chacha20-v0.10.0...chacha20-v0.10.1)

Updates `debug-helper` from 0.3.13 to 0.3.14
- [Commits](https://github.com/magiclen/debug-helper/compare/v0.3.13...v0.3.14)

Updates `hybrid-array` from 0.4.12 to 0.4.13
- [Changelog](https://github.com/RustCrypto/hybrid-array/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/hybrid-array/compare/v0.4.12...v0.4.13)

Updates `js-sys` from 0.3.102 to 0.3.103
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `wasm-bindgen` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `wasm-bindgen-macro` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `wasm-bindgen-macro-support` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

Updates `wasm-bindgen-shared` from 0.2.125 to 0.2.126
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.125...0.2.126)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.103
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: env_logger
  dependency-version: 0.11.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: jiff
  dependency-version: 0.2.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: uuid
  dependency-version: 1.23.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crc-any
  dependency-version: 2.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: io-uring
  dependency-version: 0.7.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: arc-swap
  dependency-version: 1.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: chacha20
  dependency-version: 0.10.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: debug-helper
  dependency-version: 0.3.14
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: env_filter
  dependency-version: 2.0.0
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: non-rust-vmm
- dependency-name: hashbrown
  dependency-version: 0.17.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: hybrid-array
  dependency-version: 0.4.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: jiff-static
  dependency-version: 0.2.31
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.103
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: anyhow
  dependency-version: 1.0.103
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: uuid
  dependency-version: 1.23.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crc-any
  dependency-version: 2.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: arc-swap
  dependency-version: 1.9.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: chacha20
  dependency-version: 0.10.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: debug-helper
  dependency-version: 0.3.14
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: hybrid-array
  dependency-version: 0.4.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.103
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.126
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 00:55:56 +00:00
dependabot[bot]
43d97b0546 build(deps): bump dorny/paths-filter from 4.0.1 to 4.0.2
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](fbd0ab8f3e...7b450fff21)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 00:37:37 +00:00
Rob Bradford
6f815c4546 vmm: config: Check NetConfig::socket set if vhost_user is set
This check was already included in the DiskConfig validation but missing
for NetConfig.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-02 15:35:39 +00:00
Rob Bradford
e9ee46f62b vmm: config: Reject invalid virtio queue sizes
The queue size must fit into a u16 and be a power of 2 according to the
spec.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-02 15:35:39 +00:00
Rob Bradford
7c7fe7091c vmm: config: Replace integer list conversion
Now that IntegerList can handle any width number type use that for the
queue_sizes parameter on `--generic-vhost-user`

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-02 15:35:39 +00:00
Rob Bradford
1a441fb7ed option_parser: Make IntegerList generic
This allows its use for all integer types and will generate an error if
the value is too wide for the type.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-02 15:35:39 +00:00
Philipp Schuster
da80fde887 vmm: streamline printing of error chains
In [0] we agreed on the current format.

- Use `: ` over ` => `
- Streamline usages in a helper

Context https://github.com/cloud-hypervisor/cloud-hypervisor/issues/8510

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-07-02 15:24:20 +00:00
Sebastian Eydam
53a00c0514 virtio-devices: net: add guest-announce plumbing
Advertise `VIRTIO_NET_F_GUEST_ANNOUNCE` on virtio-net devices, surface
`VIRTIO_NET_S_ANNOUNCE` through config status, and handle
`VIRTIO_NET_CTRL_ANNOUNCE_ACK` on the control queue.

This adds the guest-visible state needed for post-migration or
post-restore announce requests; the VMM side triggering is added in
follow-up commits.

The motivation is to reduce post-migration and post-restore
connectivity gap. After a live migration or after restoring, it can
take the guest several seconds to be reachable again over the network.
With these announcements, the network path should be refreshed within a
few milliseconds.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-07-02 15:17:40 +00:00
Sebastian Eydam
e17c0be127 virtio-devices: net: report link up in config status
Expose `VIRTIO_NET_S_LINK_UP` through the virtio-net config status field
when `VIRTIO_NET_F_STATUS` was negotiated.

This makes the guest-visible status bits reflect the device runtime
state and prepares the config status path used by later post-migration
announce handling.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-07-02 15:17:40 +00:00
Saravanan D
f5967cf287 docs: Document VFIO snapshot and restore support
Add a Snapshot and Restore section to docs/vfio.md covering the
migration v2 requirements (Linux 5.18 kernel, variant VFIO
driver such as mlx5_vfio_pci) and the restore sequence for
devices that advertise migration v2.

The behavior description covers the full restore sequence. It
documents the RUNNING to RESUMING single transition (the kernel
walks the intermediate STOP arc), the post load PCI_COMMAND push
to the device, and the MSI or MSI-X eventfd rearm that the
kernel state does not carry. Behavior matches QEMU
vfio_pci_load_config().

It notes one limitation, the snapshot format stores the opaque
device blob as base64 inside the snapshot JSON, which may benefit
from a binary transport path for very large state.

docs/snapshot_restore.md replaces its VFIO out of scope
limitation with a short section that points to docs/vfio.md for
the requirements and behavior.

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
2026-07-02 01:13:03 +00:00
Saravanan D
8b31602650 pci: vfio: Add unit tests for migration state machine
The VFIO save and load paths drive the kernel migration state machine
through a strict transition sequence and must recover correctly when a
transition or the data transfer fails. Validating that on real hardware
needs a migratable device, which CI does not have, so the behavior would
otherwise go unverified until it breaks in the field.

Introduce a mock Vfio wrapper that records state transitions and keeps
the migration blob in memory, then test VfioCommon save and load against
it. This pins the transition ordering, the data round trip, and the
failure recovery in CI on any host, independent of hardware. The trait
defaults and the VfioMigrationState conversions are covered too, so a
non migratable device and an unknown state value stay well defined. A
restore that carries migration state onto a device without migration
support is checked to fail instead of dropping the state.

A shadow sync test guards the related save path change, confirming a non
BAR config write reaches the PciConfiguration shadow so a snapshot
captures the live value instead of the post init zero.

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
2026-07-02 01:13:03 +00:00
Saravanan D
4102768561 pci: vfio: Implement restore path state transitions
When a snapshot is loaded, walk the migration v2 state machine
from VfioCommon::set_state() after interrupt state has been
restored. If the device supports migration and a blob is
present, drive RUNNING to RESUMING in a single transition and
write the blob to the data_fd. The kernel handles the
intermediate STOP arc internally. An explicit STOP dwell was
observed to make mlx5_vfio_pci re initialize SQ, CQ, and EQ
indices on top of the just loaded blob, wedging queue state on
resume. The device is left in RESUMING and resume() drives it
to RUNNING during VM resume.

set_state() also pushes PCI_COMMAND to the device via
write_config() after the blob load. Rebuilding the in memory
MSI or MSI-X structs does not touch the kernel's view of
PCI_COMMAND, so without this the VF sits at post reset defaults
with no bus master and mlx5_core ACCESS_REG times out. It
rearms VFIO_DEVICE_SET_IRQS via enable_msi or enable_msix for
the same reason, since replaying the in memory interrupt state
does not reissue the ioctl and the kernel has no eventfds for
this device until it does. Both match QEMU
vfio_pci_load_config().

In allocate_bars, skip add_pci_bar and add_pci_rom_bar on
restore. PciConfiguration::new(Some(state)) already populated
the BAR registers with used=true, so the extra call trips
BarInUse. The bars vec and mmio_regions pushes still need to
happen so the caller can wire bus mappings.

set_state() retrieves the migration blob from the snapshot
unconditionally and rejects a snapshot that carries migration
state when the device does not support migration, rather than
silently dropping the saved state. A device without migration
support and no blob, including vfio-user, still skips the load.

On any transition or write failure during restore, STOP is
attempted as best effort before bubbling the error.

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
2026-07-02 01:13:03 +00:00
Saravanan D
e7c0d690d0 pci: vfio: Implement save path state transitions
Wire a migratable VFIO device's migration state to the VM lifecycle so
the device's internal state survives snapshot and restore. A device such
as a ConnectX VF bound to mlx5_vfio_pci would otherwise come back blank,
because a plain snapshot saves only the PCI configuration Cloud
Hypervisor owns, not the device's own state.

On save, pause moves the device to STOP and snapshot() drives it through
STOP_COPY to extract the opaque state blob, attached to the device
snapshot as a base64 encoded child. resume() returns it to RUNNING.

All new behavior is gated on migration_flags.is_some(), so devices
without migration support (including vfio-user) retain their previous
snapshot behavior.

If the data read fails after STOP_COPY was entered, the device is
returned to STOP before the error is bubbled, since the STOP_COPY
to STOP arc stays valid. A failed transition into STOP_COPY returns
immediately because a STOP from the resulting ERROR state cannot
help. Full recovery including device reset is deferred.

Since the non BAR write path goes directly to the VFIO device and not
the shadow, the PciConfiguration shadow can get stale. Mirror every
non BAR, non MSI config write into the shadow via write_byte /
write_word / write_reg so snapshot() can capture PCI_COMMAND. Without
this the shadow keeps the values set at device init and snapshot()
encodes PCI_COMMAND as zero.

Use the raw write_byte, write_word, and write_reg helpers rather than
PciConfiguration::write_config_register, which would otherwise drain
pending_bar_reprogram, consumed by the BAR block below, and rerun
MSI-X set_msg_ctl, already done by update_msix_capabilities.

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
2026-07-02 01:13:03 +00:00
Saravanan D
41ffd04644 pci: vfio: Probe migration v2 capabilities
Probe VFIO_DEVICE_FEATURE_MIGRATION during VfioCommon::new() and store
the result in a new migration_flags field so later migration phases can
gate state machine transitions.

The probe runs on every instantiation, including snapshot restore,
because migration capability is a property of the host kernel and its
variant driver rather than of any saved VM state.

migration_flags() is added to the internal Vfio trait with a default
implementation that returns Ok(None), meaning not migratable.
VfioDeviceWrapper overrides it to issue the kernel ioctl, while
vfio-user devices keep the default and are always treated as non
migratable.

Allow the VFIO_DEVICE_FEATURE ioctl in the VMM seccomp filter.

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
2026-07-02 01:13:03 +00:00
dependabot[bot]
f5344414e9 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 00:43:24 +00:00
Anatol Belski
30fdf384ec block: Test write_unaligned closure error propagation
Add a write side counterpart to read_unaligned_propagates_closure_error.
The test drives write_unaligned with a gather closure that returns an
error and checks that write_unaligned surfaces it unchanged.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 22:08:04 +00:00
Anatol Belski
9e441eb899 block: qcow: Add unit tests for detect_image_type
Cover the qcow2 magic and the non qcow magic cases of the AlignedFile
detect_image_type, which now reads the magic positionally.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 19:31:16 +00:00
Anatol Belski
3937c224a7 block: qcow: Query backing raw size without the cursor
Opening a raw backing file issued a seek to the end for its size and
then rewound the cursor. RawBacking reads through read_exact_at, so
the cursor reset was dead. Query the size through query_device_size,
matching the crate convention and returning the right size for a
block device backing file as well, and drop the rewind. The now
unused SeekFrom import is removed.

The result is unchanged.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 19:31:16 +00:00
Anatol Belski
6a1dee64e7 block: qcow: Read the image magic positionally
detect_image_type saved the cursor, rewound, read the magic, then
restored the cursor. Read the four magic bytes with read_exact_at at
offset 0 and decode with from_be_bytes, so the save, rewind, and
restore go away. BeUint moves to the test module, its only remaining
user in this file.

The result is unchanged.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 19:31:16 +00:00
Anatol Belski
abc03f998a block: qcow: Read refcount rebuild markers positionally
The refcount rebuild check issued a seek to the refcount table and to
the first refblock before each cursor read. Read the fixed size fields
with read_exact_at at their offsets and decode with from_be_bytes. The
seeks and the matching error paths go away.

The result is unchanged.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 19:31:16 +00:00
Anatol Belski
f893a13af0 block: qcow: Convert metadata cluster I/O to positional
The compressed cluster write and read and the L1 resize size query
went through a seek on the AlignedFile cursor before the access. Pass
the target offset to write_at and read_exact_at, and read the file
length from physical_size.

The result is unchanged. The compressed paths keep routing through the
AlignedFile O_DIRECT bounce.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 19:31:16 +00:00
Rob Bradford
eb838144f6 vmm: memory_manager: Check MMIO access size is correct
Check that the MMIO accesses is 4 bytes long as otherwise it would
be possible for the guest to trigger a panic when the memory ranges base
and length are copied for fulfilling the MMIO read.

This pattern of check matches similar checks in CpuManager and
DeviceManager.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-01 17:29:19 +00:00
Rob Bradford
c5104a9f17 devices: fw_cfg: Correctly handle short and long reads
Fill the target MMIO buffer with zeroes to handle reads with access
sizes larger than the data and also check that the read access length
does not exceed the size of the backing slice (previously it just
checked the access size vs length not taking the offset into account).

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-01 17:22:36 +00:00
Tushar Khatri
5f5de2ebca docs: clarify log level guidance in logging.md
Broaden error!() to cover any user-initiated action that fails to do
what was expected (e.g. failed hotplug or live migration), not only
unrecoverable startup errors. Retarget info!() at operators and users,
clarify the warn!() and debug!() audiences, and document trace!().

Part of #8440.

Co-authored-by: Philipp Schuster <phip1611@gmail.com>
Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-07-01 16:45:38 +00:00
Henry Hrvoje Tonkovac
ae7c0dd9f1 build: exempt arch from clippy::absolute_paths, use full paths
Suggested by phip1611 on #8446.

This adds the repo's first clippy.toml, carving arch out of the
absolute_paths deny from #7670. Glob imports and trait imports that
must be in scope for method-call resolution (e.g. DeviceInfoForFdt for
.irq()) are left as-is.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-07-01 16:27:46 +00:00
Anatol Belski
a8a3a59544 block: Test the unaligned O_DIRECT bounce helpers
Cover read_unaligned and write_unaligned directly: a scatter read at an
unaligned offset, a short read at EOF, a read-modify-write gather that
preserves head and tail padding, and error propagation from the
scatter closure.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 16:27:25 +00:00
Anatol Belski
55b3bad2c3 block: Drop the middle buffer on the unaligned O_DIRECT path
run_unaligned_operation staged every unaligned request in a plain Vec
and then handed it to AlignedFile, which bounced again through an
aligned buffer. That Vec only gave the operation a contiguous range to
scatter into or gather from, which the aligned buffer already is, so
each slow path request paid for an extra allocation and a full length
copy.

Add read_unaligned and write_unaligned on AlignedFile that own the
single aligned bounce and scatter or gather through a closure over the
staging slice. run_unaligned_operation and the FileExt read_at and
write_at impls both route through them, so the staging and
read-modify-write logic lives in one place. The closures keep
AlignedFile free of any AsyncIoOperation dependency.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-07-01 16:27:25 +00:00
Rob Bradford
f62e2615a9 block: vhdx: Use zerocopy for structs
Replace the use of unsafe struct casting with zerocopy trait derivation.
This fixes a Rust UB where the struct was being filled with a slice of
length greater than the size of the struct.

As a compromise the guid handling was changed to handle the uuids as
opaque bytes as they are mixed endian. This has no impact on the
functionality as they are only used for comparison and has the positive
impact of reducing some of the uuid handling complexity.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-01 12:51:55 +00:00
Rob Bradford
f5f7b092e1 arch: smbios: Avoid unsafe slice::from_raw_parts()
The struct already implements ByteValued so this unsafe block can be
changed to call as_slice() from that trait.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-01 10:44:10 +00:00
Rob Bradford
330f2fda1c arch: mptable: Avoid unsafe slice::from_raw_parts()
The struct already implements ByteValued so this unsafe block can be
changed to call as_slice() from that trait.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-07-01 10:44:10 +00:00
Aastha Rawat
efedfb47e9 ci: consolidate mshv integration tests into ci.yaml
Migrate the MSHV integration tests to run natively on the self-hosted
runner instead of spinning up a separate VM. This simplifies the
workflow pipeline & mitigates Azure capacity issues.

Signed-off-by: Aastha Rawat <aastharawat@microsoft.com>
2026-07-01 08:23:41 +00:00
dependabot[bot]
b72a1cd6b4 build(deps): bump crate-ci/typos from 1.47.2 to 1.48.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.47.2 to 1.48.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](37bb98842b...bee27e3a4f)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 00:41:35 +00:00
Anatol Belski
dee6918d7a block: qcow: Add unit tests for qcow refcount positional access
Cover refcount block round trip for the byte aligned and sub byte
paths, and add_cluster_end appending an aligned cluster and staying
within the maximum offset bound.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-30 17:11:35 +00:00
Anatol Belski
56aec72e25 block: qcow: Use physical_size in add_cluster_end
add_cluster_end queried the file length by seeking to the end. Use the
existing physical_size helper instead, which reads the length from the
file metadata. This removes the final cursor access in QcowRawFile, so
the Seek and SeekFrom imports are no longer needed.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-30 17:11:35 +00:00
Anatol Belski
4da6067408 block: qcow: Convert qcow refcount block I/O to positional
The refcount block read and write helpers took a file whose cursor was
positioned by a preceding seek. Pass the target offset down instead and
use positional read_exact_at and write_all_at on the AlignedFile, so
the block methods no longer seek. The byte aligned and sub byte writers
build a buffer and issue one positional write, keeping the previous
batching.

The result is unchanged, as the calls still route through the
AlignedFile O_DIRECT bounce.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-30 17:11:35 +00:00
Philipp Schuster
fa5c2cc562 docs: scope testing container overview
Make the testing overview describe the dev_cli.sh workflow instead of
implying that every Cloud Hypervisor build must run in a container.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 17:03:30 +00:00
Philipp Schuster
f508e6b1bb docs: remove stale testing TOC entry
Drop the CI workflows link from testing.md because the document has no
matching section.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 17:03:30 +00:00
Philipp Schuster
d0cf0a4e20 docs: clarify D-Bus API names
Distinguish the configured D-Bus service name from the fixed DBusApi1
interface name in the API documentation.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 17:03:30 +00:00
Philipp Schuster
00a713e5ba docs: update CPU option examples
Use the structured --cpus boot= syntax in cloud-hypervisor launch
examples so they match the current VM config parser.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 17:03:30 +00:00
Philipp Schuster
cc6426d87c docs: fix migration command examples
Use the keyed receiver_url and destination_url parameters expected by
ch-remote's current migration parsers [0].

[0] 6fa044d101

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 17:03:30 +00:00
Philipp Schuster
2e62081bec docs: update SEV-SNP backend support
Remove the stale MSHV-only wording now that the docs and CVM test path
cover KVM SEV-SNP with IGVM stage0 and fw_cfg [0, 1].

[0]: https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7942
[1]: https://github.com/cloud-hypervisor/cloud-hypervisor/pull/8347

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 17:03:30 +00:00
Philipp Schuster
013981b649 vm-migration: improve debuggability on receiver for failed migrations
We cannot reliably send Request::abandon() on every kind of failure on
the sender side, as we might be in the middle of a memory transmission.
The receiver would not reliably know what to do with that. So instead,
when the receiver cannot read from the socket, we log that the migration
sender failed, which is the only likely cause of that failure.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 16:59:20 +00:00
Philipp Schuster
81022ab087 vm-migration: improve logging to make clear if receiver caused failure
This improves the observability whether a migration failed because of
the sender or because of some error on the receiving side.

Using a simple log message is simpler than introducing a new error enum
to differentiate between SendError and RemoteError.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 16:59:20 +00:00
Sebastian Eydam
969d2f855d vm-migration: stop sending abandon after error response
When the receiver of a live migration encounters an error, it sends an
error response. The sender of the migration would then send an abandon
request and wait for a response. This abandon request is not necessary,
because the receiver already abandoned the migration due to the error it
encountered.

From now on this function will not send an abandon request to the
receiver anymore, thus it was renamed to "ok_or_error".

Also, this case was always broken, because after sending the error
response, the receiver just exits without waiting for the additional
abandon request.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-30 16:59:20 +00:00
Philipp Schuster
f35c9842be docs: update new prefault behavior in memory.md
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-30 16:56:42 +00:00
Sebastian Eydam
22cf10b6ab vmm: return error if prefaulting fails
Prefaulting pages was done on a best-effort basis before, meaning that
errors were ignored. This could lead to errors during runtime,
especially when used with hugepages, because there was no guarantee that
enough pages are available. With this change errors during prefaulting
will be reported.

Co-authored-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-30 16:56:42 +00:00
Daniel Verkamp
494c30be19 block: Avoid unsafe Vec construction in detect_image_type()
The `read_aligned_block_size()` function used `Vec::from_raw_parts()`
incorrectly, causing undefined behavior when deallocating the `Vec<u8>`.

One of the safety invariants of `Vec::from_raw_parts()` is that the
provided pointer must be allocated with the exact same alignment as `T`
(`u8` in this case), but this is clearly not true: `align_of::<u8>()` is
1, but the pointer was allocated with aligment of `blocksize` (typically
512 or greater).

Fix this by using the existing `AlignedFile` helper to read the header
block when probing the image type. This is slightly less efficient than
using `AlignedBuffer` directly, but since this is only called once per
disk image at startup, the difference is probably not worth the extra
verbosity.

Signed-off-by: Daniel Verkamp <drv@meta.com>
2026-06-29 23:26:17 +00:00
Anatol Belski
ab593accb3 virtio-devices: Test device config capability presence
Add unit tests for add_pci_capabilities covering the configless
device path. The device config capability is present when the
config region is sized and absent when the size is zero.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-29 23:09:52 +00:00
Anatol Belski
3c90f91491 virtio-devices: Report no config space for configless devices
The watchdog, rng, and rtc devices expose no device specific
configuration fields. Each now reports a config size of zero so the
transport omits the device configuration capability instead of
advertising an unbacked region that the device cannot service.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-29 23:09:52 +00:00
Anatol Belski
d1dd6b13d5 virtio-devices: Size the device config capability per device
Add a config_size method to VirtioDevice and use it when building the
PCI device configuration capability. The transport advertises the size
reported by the device and omits the capability entirely when the size
is zero, because the virtio driver rejects a zero length capability.

The method defaults to None, so every device keeps its current
capability size.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-29 23:09:52 +00:00
Anatol Belski
9c085e11bb block: Add unit tests for qcow raw file positional access
Cover read_pointer_table round trip and masking, and the
write_cluster then zero_cluster round trip, exercising the positional
read_exact_at, write_all_at, and write_all_zeroes_at paths on the
AlignedFile.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-29 21:55:08 +00:00
Anatol Belski
833b360e18 block: Convert qcow raw file table and cluster writes to positional
Replace the seek then read/write metadata access in QcowRawFile with
positional read_exact_at, write_all_at, and write_all_zeroes_at on the
AlignedFile. read_pointer_table, write_pointer_table,
write_pointer_table_direct, zero_cluster, and write_cluster no longer
move the file cursor.

These calls still route through AlignedFile, which implements FileExt
and WriteZeroesAt with the O_DIRECT alignment bounce, so the unaligned
behavior is preserved. Each access already issued an absolute seek
before touching the file, so the cursor never carried state between
calls and dropping it is unobservable.

Decoding the pointer table now uses native from_be_bytes over the read
buffer, matching the to_be_bytes path on the write side.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-29 21:55:08 +00:00
Henry Hrvoje Tonkovac
fe8bd6e62c block: fix VHDX region table overlap detection
The region table overlap check in RegionInfo::new only rejected a new
region that strictly engulfed an existing one. Identical, fully
contained, and partially overlapping regions passed undetected, so a
malformed VHDX with overlapping region entries was wrongly accepted.

Per [MS-VHDX] all region objects MUST be non-overlapping, so such an
image should be rejected. Replace the faulty predicate with a correct
half-open interval overlap test, extracted into a small pure helper
(ranges_overlap).

Add a unit test for the predicate and an integration test that feeds a
crafted region table with two overlapping entries through the real
RegionInfo::new, confirming it is now rejected with RegionOverlap.

Related to #8009 (broader VHDX overlap validation).

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-29 21:51:12 +00:00
Philipp Schuster
19e67a2c7e vmm: streamline migration related API error variants
Receiving a migration happens inside the VMM thread, which blocks the
API until a migration was received. On the other hand, sending a
migration is actually just a dispatch operation. We adjust the wording
to improve clarity of the error messages.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-29 18:07:51 +00:00
CMGS
f8d0186a26 arch: x86_64: recommend Hyper-V paravirt TLB flush and cluster IPI
Adds three recommendation bits to CPUID 0x40000004.EAX so Windows /
Hyper-V-aware guests use the corresponding paravirtualized hypercalls
instead of falling back to architectural primitives. The hypercalls
themselves are emulated unconditionally by KVM and surfaced via the
corresponding KVM_CAP_HYPERV_* info caps; no userspace cap negotiation
is needed because KVM advertises support to the guest at hypercall
issue time:

  KVM_CAP_HYPERV_TLBFLUSH advertises HvFlush{VirtualAddressSpace,Ex,
                          List,ListEx} (api.rst 8.18, info-only cap).
  KVM_CAP_HYPERV_SEND_IPI advertises HvCallSendSyntheticClusterIpi{,Ex}
                          (api.rst 8.20, also info-only).

Leaf 0x40000004.EAX (HV_CPUID_ENLIGHTMENT_INFO):
  bit 1  LocalTlbFlushRecommended
  bit 2  RemoteTlbFlushRecommended
         Recommend HvFlushVirtualAddressSpace / List in place of
         architectural INVPCID / INVLPG broadcasts. Remote shoot-down
         via hypercall lets the host skip vCPUs that are not currently
         scheduled, instead of waiting for an IPI ack.
  bit 10 ClusterIpiRecommended
         Recommend HvCallSendSyntheticClusterIpi over per-target APIC
         ICR writes. A single hypercall can target up to 64 vCPUs (or
         all of them via the Ex variant) versus one VM exit per APIC
         access on the architectural path.

These bits depend on AccessVpIndex (0x40000003.EAX bit 6), which is
advertised by the partition-privileges change.

Sources:
  Microsoft Hypervisor Top-Level Functional Specification 7.4.5
  qemu/qemu docs/system/i386/hyperv.rst (hv-tlbflush, hv-ipi)
  Linux Documentation/virt/kvm/api.rst 8.18, 8.20

Signed-off-by: CMGS <ilskdw@gmail.com>
2026-06-29 18:03:44 +00:00
CMGS
ba11523760 arch: x86_64: advertise additional Hyper-V CPUID enlightenments
Extends the Hyper-V partition feature CPUID leaf 0x40000003 with bits
that KVM emulates unconditionally and that Windows / Hyper-V-aware
guests consult to skip slow fallback paths. No KVM capability
negotiation is required for any of these -- they are hints to the
guest about what is already legal to use.

Leaf 0x40000003.EAX (HV_CPUID_FEATURES):
  bit 0  AccessVpRuntimeReg     -- HV_X64_MSR_VP_RUNTIME (0x40000010)
  bit 4  AccessIntrCtrlRegs     -- HV_X64_MSR_{EOI,ICR,TPR,APIC_ASSIST}
  bit 11 AccessFrequencyMsrs    -- HV_X64_MSR_{TSC,APIC}_FREQUENCY (skips
                                   guest TSC/APIC calibration loops)

Leaf 0x40000003.EDX (HV_CPUID_FEATURES, TLFS rev 6.0c):
  bit 4  FastHypercall          -- HV_HYPERCALL_PARAMS_XMM_AVAILABLE
  bit 8  ExtendedGvaRangesForFlushVirtualAddressList -- pairs with the
                                   tlbflush-ext recommendation bit

AccessHypercallMsrs (bit 5) and AccessVpIndex (bit 6) are already
advertised by the partition-privileges change.

Sources:
  Microsoft Hypervisor Top-Level Functional Specification 7.4.{2,5}
  qemu/qemu docs/system/i386/hyperv.rst (hv-vapic, hv-frequencies,
                                          hv-vpruntime)

Signed-off-by: CMGS <ilskdw@gmail.com>
2026-06-29 18:03:44 +00:00
Sebastian Eydam
aa629a1485 vmm: make aborted migrations on receiver side return an error
On the receiver side, a live migration with status "aborted" does not
return an error. Thus, management software will think that the live
migration was successful (from just looking at the API response). This
is not expected behaviour.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-29 17:19:28 +00:00
Philipp Schuster
1ae1cc787d vmm: migration seccomp: add for TCP workers (send and receive)
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-29 17:15:14 +00:00
Philipp Schuster
fd88e23ecb vmm: migration seccomp: add for migration worker (coordinator thread)
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-29 17:15:14 +00:00
Philipp Schuster
b5c028c286 vmm: migration seccomp: add plumbing for all migration-related threads
So far, we only have seccomp rules for the postcopy-send thread. This
commit introduces the basic plumbing to add seccomp rules also for the
migration worker (the migration coordinator) as well as the TCP workers
(both, send and receive) in the following.

To streamline code setup, all filters are created at a central place
early in the migration code. Although this means that some filters are
created without the need to do so (e.g., postcopy), this massively
simplifies code setup and error handling. This overhead is negligible.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-29 17:15:14 +00:00
dependabot[bot]
e7371984b8 build(deps): bump the non-rust-vmm group across 2 directories with 12 updates
Bumps the non-rust-vmm group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [itertools](https://github.com/rust-itertools/itertools) | `0.14.0` | `0.15.0` |
| [jiff](https://github.com/BurntSushi/jiff) | `0.2.28` | `0.2.29` |
| [log](https://github.com/rust-lang/log) | `0.4.32` | `0.4.33` |
| [rustls](https://github.com/rustls/rustls) | `0.23.40` | `0.23.41` |
| [getrandom](https://github.com/rust-random/getrandom) | `0.4.2` | `0.4.3` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.64` | `1.2.65` |
| [quote](https://github.com/dtolnay/quote) | `1.0.45` | `1.0.46` |
| [syn](https://github.com/dtolnay/syn) | `2.0.117` | `2.0.118` |
| [zeroize](https://github.com/RustCrypto/utils) | `1.8.2` | `1.9.0` |

Bumps the non-rust-vmm group with 6 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [itertools](https://github.com/rust-itertools/itertools) | `0.14.0` | `0.15.0` |
| [log](https://github.com/rust-lang/log) | `0.4.32` | `0.4.33` |
| [rustls](https://github.com/rustls/rustls) | `0.23.40` | `0.23.41` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.64` | `1.2.65` |
| [quote](https://github.com/dtolnay/quote) | `1.0.45` | `1.0.46` |
| [syn](https://github.com/dtolnay/syn) | `2.0.117` | `2.0.118` |



Updates `itertools` from 0.14.0 to 0.15.0
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.14.0...v0.15.0)

Updates `jiff` from 0.2.28 to 0.2.29
- [Release notes](https://github.com/BurntSushi/jiff/releases)
- [Changelog](https://github.com/BurntSushi/jiff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/jiff/compare/jiff-static-0.2.28...jiff-static-0.2.29)

Updates `log` from 0.4.32 to 0.4.33
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.32...0.4.33)

Updates `rustls` from 0.23.40 to 0.23.41
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.40...v/0.23.41)

Updates `getrandom` from 0.4.2 to 0.4.3
- [Changelog](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/getrandom/compare/v0.4.2...v0.4.3)

Updates `cc` from 1.2.64 to 1.2.65
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.64...cc-v1.2.65)

Updates `hashbrown` from 0.15.5 to 0.17.0
- [Release notes](https://github.com/rust-lang/hashbrown/releases)
- [Changelog](https://github.com/rust-lang/hashbrown/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/hashbrown/compare/v0.15.5...v0.17.0)

Updates `jiff-static` from 0.2.28 to 0.2.29
- [Release notes](https://github.com/BurntSushi/jiff/releases)
- [Changelog](https://github.com/BurntSushi/jiff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/jiff/compare/jiff-static-0.2.28...jiff-static-0.2.29)

Updates `quote` from 1.0.45 to 1.0.46
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.45...1.0.46)

Updates `syn` from 2.0.117 to 2.0.118
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.117...2.0.118)

Updates `wit-bindgen` from 0.51.0 to 0.57.1
- [Release notes](https://github.com/bytecodealliance/wit-bindgen/releases)
- [Commits](https://github.com/bytecodealliance/wit-bindgen/compare/v0.51.0...v0.57.1)

Updates `zeroize` from 1.8.2 to 1.9.0
- [Commits](https://github.com/RustCrypto/utils/compare/zeroize-v1.8.2...zeroize-v1.9.0)

Updates `itertools` from 0.14.0 to 0.15.0
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.14.0...v0.15.0)

Updates `log` from 0.4.32 to 0.4.33
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.32...0.4.33)

Updates `rustls` from 0.23.40 to 0.23.41
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.40...v/0.23.41)

Updates `cc` from 1.2.64 to 1.2.65
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.64...cc-v1.2.65)

Updates `quote` from 1.0.45 to 1.0.46
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.45...1.0.46)

Updates `syn` from 2.0.117 to 2.0.118
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.117...2.0.118)

---
updated-dependencies:
- dependency-name: itertools
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: jiff
  dependency-version: 0.2.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rustls
  dependency-version: 0.23.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: getrandom
  dependency-version: 0.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.65
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: hashbrown
  dependency-version: 0.17.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: jiff-static
  dependency-version: 0.2.29
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: quote
  dependency-version: 1.0.46
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: syn
  dependency-version: 2.0.118
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wit-bindgen
  dependency-version: 0.57.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zeroize
  dependency-version: 1.9.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: itertools
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rustls
  dependency-version: 0.23.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.65
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: quote
  dependency-version: 1.0.46
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: syn
  dependency-version: 2.0.118
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-26 16:47:02 +00:00
Philipp Schuster
ebc653d0c8 vmm: silence some info!() messages with little value-add
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-26 16:46:30 +00:00
Philipp Schuster
48c9e9d86b vmm: split code in ReceiveAdditionalConnections into smaller chunks
This ensures the code stays maintainable.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-26 16:46:30 +00:00
Philipp Schuster
6b441f0746 vmm: improve rustdoc for ReceiveAdditionalConnections
ReceiveAdditionalConnections got quite complicated, especially with the
many threads involved for precopy and the special-case of postcopy. We
therefore should add comprehensive documentation.

I tried to keep it short and concise - what remains provides high value
and improves the mental model of the code.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-26 16:46:30 +00:00
Philipp Schuster
4d727c4899 vmm: keep virtio activation alive in migration
Live migration can deadlock if the guest triggers a virtio device
activation while the migration worker owns the VM.

The failure shows up when starting live migrations during boot and
firmware startup, where the guest can reset and reinitialize virtio
devices while precopy is running. In the failing case, the source log
shows a pending virtio activation that never completes:

    8.115833s _virtio-pci-net_0: Needs activation; returning barrier
    8.115854s vmm/src/vm.rs:464 -- Waiting for barrier
    24.875452s Entering downtime phase
    24.875481s stopping vcpu throttling thread
    ...
    vCPU thread did not respond in 10ms to signal - retrying
    vCPU thread did not respond in 20ms to signal - retrying
    ...
    thread 'throttle-vcpu' (1029) panicked
    ...
    Pause(Error signalling vCPUs: Timeout when waiting for signal
        to be acknowledged)

The vCPU blocks on the activation barrier and never reaches the normal
pause checkpoint. Later, migration enters downtime and stops the vCPU
throttle thread. In the failing case, that thread is still inside a
CpuManager::pause() call, which waits for every vCPU to acknowledge
the signal. The blocked vCPU never does, so the pause times out.

Fix this by storing the DeviceManager inside VmOwnership::Migration.
This keeps just enough state on the VMM thread to drain pending virtio
activations while the migration worker owns the Vm. The barrier logic
stays unchanged. The VMM now releases the same activation barrier during
migration that it already released before migration started.

This keeps the guest from getting stuck in the activation wait and
lets the later pause succeed.

Co-authored-by: Leander Kohler <leander.kohler@cyberus-technology.de>
On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-26 16:42:37 +00:00
Sebastian Eydam
08526a65b5 main: print seccomp syscall details
When seccomp traps a SIGSYS, print the syscall number that caused it,
the current thread id and thread name to make violations easier to
debug.

This change requires that all threads are allowed to execute the
`gettid` and the `prctl` syscalls, thus the seccomp filters have also
been adjusted.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-26 16:40:45 +00:00
Sebastien Boeuf
693c236e06 vm-migration: Use zerocopy for safe serialization
Getting rid of the unsafe ByteValued implementation for MemoryRange,
Request and Response structures, by relying on zerocopy's safe
implementation instead.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-8
2026-06-26 14:59:45 +00:00
Philipp Schuster
516caed5cc vmm: streamline rustdoc of tls_dir
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-26 01:12:59 +00:00
Philipp Schuster
9dc970630b vmm: log successful TCP/TLS handshake
This helps operators and developers to easily verify if the TLS
handshake was successful.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-26 01:12:59 +00:00
Philipp Schuster
90429c56c8 vmm: streamline error messages
In [0] we agreed on the current format.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:13:13 +00:00
Philipp Schuster
40d8f9ee51 pci: streamline error messages
In [0] we agreed on the current format.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:13:13 +00:00
Philipp Schuster
fdc076d22f net_util: streamline error messages
In [0] we agreed on the current format.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:13:13 +00:00
Philipp Schuster
786d40f549 block: streamline error messages
In [0] we agreed on the current format.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:13:13 +00:00
Philipp Schuster
9ba653dc16 misc: persist Error-message style in CONTRIBUTING.md and AGENTS.md
In [0] we agreed on the current format.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:13:13 +00:00
Philipp Schuster
252702049e vmm: migration: print whole error chain on error
In [0] we agreed on the current format. When ch-remote or
cloud-hypervisor exit with an error, they nicely print the whole chain.
This, however, doesn't work when simply doing `error!("error: {e}")`
- which is what we currently do for migration-related errors.

This commit walks the chain of errors and prints all components in a
single line. This massively improves the quality of error messages and
helps tracing down where an error is originating from. Using ` => ` as
separator is better than `\n` which doesn't work well in our current
log format.

# Example (Before - Bad)

```
cloud-hypervisor:   2.859287s: <vmm> ERROR:vmm/src/lib.rs:2021 -- Migration failed: Failed to send migratable component snapshot
```

# Example (New - Better)

```
cloud-hypervisor:   2.296160s: <vmm> ERROR:vmm/src/lib.rs:2038 -- Migration failed: Failed to send migratable component snapshot => Error connecting to TCP socket => Connection refused (os error 111)
```

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7066

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:12:15 +00:00
Philipp Schuster
8c71a0d821 vmm: make migration errors more helpful
Keep the original error sources while adding operation context to the
straightforward migration send and receive paths. This keeps up a
chain of errors that can be printed nicely and in a helpful way.

This commit does that for all MigratableError-occurences where this
change is easily applicable.

Co-authored-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-25 19:12:15 +00:00
Henry Hrvoje Tonkovac
05f026440a devices: rtc_pl031: update outdated ARM TRM doc link
static.docs.arm.com no longer serves the PL031 RTC technical reference
manual; point the comment at the current developer.arm.com location.
Clears the lychee link check, as requested on the review.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-25 16:54:35 +00:00
Henry Hrvoje Tonkovac
f720e619c1 misc: use prelude size_of
size_of is part of std::prelude as of Rust 1.80 (with size_of_val,
align_of, align_of_val), and the workspace MSRV is 1.89, so qualifying
it (mem::size_of, std::mem::size_of, core::mem::size_of) is unnecessary.

Convert every qualified size_of call-site to the bare prelude form and
drop the now-redundant `use std::mem::size_of;` imports, keeping
`use std::mem;` where it still serves non-prelude items (transmute,
swap, replace, take, zeroed, MaybeUninit, offset_of). size_of is the
only one of the four currently used in the tree.

Pure refactor, no behavioural change. Follow-up to the
clippy::absolute_paths cleanup (#7670), as discussed in #8444.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-25 16:54:35 +00:00
Henry Hrvoje Tonkovac
f56fa3a865 tests: trim qualified paths in integration
Import the modules used in the integration tests instead of spelling
the full paths at every use site, and drop the file's now-unnecessary

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-24 15:13:07 +00:00
Henry Hrvoje Tonkovac
025e782e50 vmm: trim qualified paths
Import the modules used in the crate instead of spelling the full paths
at every use site, and drop the now-unnecessary crate-level

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-24 15:12:32 +00:00
Philipp Schuster
0caa3ee73f vmm: silence some info! messages
We have larger cloud deployments and analyzed the logs. Let's silence
some messages that generally provide little value on the `info!` level.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-24 13:58:46 +00:00
Philipp Schuster
dcaccf21ea virtio-devices: silence some info! messages
We have larger cloud deployments and analyzed the logs. Let's silence
some messages that generally provide little value on the `info!` level.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-24 13:58:46 +00:00
Sebastien Boeuf
cc98a232e6 vmm: Add seccomp filter for migrate-send-postcopy thread
Applying seccomp filtering to the migration postcopy thread running on
the source VM during migration.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-8
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
80958acdab vmm: Wire postcopy live migration from source VM
Wire up the source side of postcopy migration over TCP. When
`mode=postcopy` is requested on vm.send-migration, the source skips
the pre-copy dirty-tracking loop and lets the destination resume early,
then serves guest pages on demand over a dedicated connection.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
60398f11ff offload_daemon: Add --ondemand restore mode
Add an --ondemand flag to the offload daemon's restore subcommand to
support the post-copy mechanism from the live migration protocol.

In on-demand mode, the daemon creates empty memfds to back the guest
memory and sends them over to the VMM. This lets the VM start quickly,
right after the memfds are mapped into CH's address space.

At runtime, when the guest accesses a page (or the prefault handler
requests it), the daemon faults it in by copying the page content into
its shared memory mapping, then replies to the PageFault request so the
VMM can consider the page present.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
48ba1f1417 vmm: Add postcopy support to receive-migration
Plumb the SocketUffdMemorySource into the receiving side of live
migration. When memory_mode=postcopy is requested, the destination
brings up a dedicated fault connection, registers userfaultfd on the
restored memory regions, and serves guest pages on demand over that
connection while the VM resumes early.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
282d1c989d vmm: Add SocketUffdMemorySource implementation
Add the socket-backed UffdMemorySource that resolves each fault by
sending a Command::PageFault request to the peer over a dedicated fault
connection.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
0d82d16432 vmm: Introduce MigrationMode migration option
Introducing a migration mode to both sides of the migration (send and
receive), so that a user can desribe which way the memory should be
migrated between the source and destination VMs.

For now, we only introduce `precopy` and `postcopy` as viable options,
but we can expect other modes (more optimized) to be added in the
future.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-8
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
3807dc82de vmm: Refactor restore_by_uffd behind UffdMemorySource trait
Extract the page content provider out of the userfaultfd handler so it
can be plugged with different backends in followup commits.

No functional change intended.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-24 12:51:40 +00:00
Sebastien Boeuf
a887d87c2a vm-migration: Add PageFault command for postcopy and on demand restore
Introducing PageFault as the new wire command needed by both postcopy
live migration and on demand restore from the offload daemon. This new
command describes the need from the destination to fault the page
content in. This request describes the page through a MemoryRange
structure, and the response can be either 0 or the actual page size.

In case it is 0, that means the source had access to the guest memory
and was able to copy the page content directly. In case the response is
the actual page size, there is a payload associated which contains the
page content.

We can expect local live migration and offload restore to run locally
and therefore have access to the guest memory. The remote live migration
over the network is the case where we would expect the page content to
be sent over the wire.

This command is served through an additional connection happening on the
UNIX or TCP socket. The goal is to keep the same codepath between local
and remote migrations. This additional channel allows PageFault commands
to be issued asynchronously so they can be served without blocking the
main connection.

A connection role is introduced in order to identify an additional
connection related to pre-copy memory versus the newly introduced
channel for serving post-copy requests.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-24 12:51:40 +00:00
Anatol Belski
755d42eec0 block: Remove redundant BlockBackend trait
BlockBackend predated the disk_file trait family and only carried
logical_size and physical_size, which the disk backends expose
through the disk_file traits DiskSize and PhysicalSize.

It added no polymorphism while its Read, Write and Seek supertraits
forced an unused cursor. Dropping the trait removes the dead code
it was keeping alive.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-24 01:26:12 +00:00
Henry Hrvoje Tonkovac
17cc156ccb tests: trim qualified paths in integration_cvm
Import the modules used in the shared common test helpers instead of
spelling the full paths at every use site, and drop the now-unnecessary
crate-level #![expect(clippy::absolute_paths)] from integration_cvm.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-23 18:15:40 +00:00
Henry Hrvoje Tonkovac
01de980615 hypervisor: trim qualified paths
Import the modules used in the crate instead of spelling the full paths
at every use site, and drop the now-unnecessary crate-level
generated msr_index.rs was trimmed separately.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-23 16:19:10 +00:00
Henry Hrvoje Tonkovac
bb81c6650b ch-remote: trim qualified paths
Import the modules used in the binary instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-23 00:58:11 +00:00
Rob Bradford
993ac5c90b vmm: cpu: Re-sync vCPU TSC offsets after restore
Restoring a snapshot (snapshot restore or live-migration receive) sets
each vCPU's TSC by writing MSR_IA32_TSC as the vCPU is created.

However because CpuManager creates and restores vCPUs one at a time, the
host TSC advances between the per-vCPU writes and KVM derives a slightly
different TSC offset for each vCPU.

KVM only engages its masterclock when every offset matches. This has a
side effect of breaking the HyperV TSC reference clock page resulting in
significantly reduced performance on Windows.

After restore synchronise all vCPU's TSC offset to the boot vCPU's via
the KVM_VCPU_TSC_CTRL device attribute group (Linux 5.16+) this allows
the KVM TSC masterclock to engage and mitigates performance issues with
the KVM HyperV emulation.

See: #8383

Signed-off-by: Rob Bradford <rbradford@meta.com>
Assisted-by: Claude <claude-opus-4-8>
2026-06-22 20:13:22 +00:00
Rob Bradford
08c82a7352 hypervisor: kvm: Reduce fully qualified paths
Reduce, but don't fully eliminate the fully qualified std:: paths.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-22 20:13:22 +00:00
Henry Hrvoje Tonkovac
f905a4e9d2 main: trim qualified paths
Import the modules used in the binary instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-22 19:14:50 +00:00
Philipp Schuster
6a16b65ea6 tests: adjust to new dispatch semantics of ch-remote send-migration
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-22 19:11:42 +00:00
Philipp Schuster
a56594324c vmm: move migration modules into folder
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-22 19:11:42 +00:00
Philipp Schuster
7c7a827ded vmm: streamline request handlers to use match{} on vm
This streamlines the behavior with the other request handlers so that
now almost every request handler uses a match on self.vm.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-22 19:11:42 +00:00
Philipp Schuster
796fc055bd vmm: migration: handle in dedicated thread (make async)
This puts the send-migration action into a dedicated thread, laying the
groundwork for many follow-ups towards first-class live-migration in
CH.

This means:

1. The send-migration call will exit sooner (just trigger the
   migration - dispatch semantics)
2. Other API calls can be triggered while a migration is ongoing but
   will not be able to alter the VM as the VM's ownership is transferred
   from the VMM to the migration thread. Example: hotplugging won't work
   (which is good).
3. This is the basis for migration statistics via a dedicated endpoint
   (future work).

The whole change was done with a special focus on graceful recover and
cleanup: even if anything on the migration paths go wrong, the proper
cleanups are already executed and the VMM can take back the ownership
of the VM.

The receive-migration API call remains blocking. To observe any status
changes about the migration on the sender side, one can observe the
event-monitor output and look for `vm.migration-{failed,finished}`.

These changes are inspired by [0] but differ significantly in details.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7038

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-22 19:11:42 +00:00
Philipp Schuster
5d835bdff4 vmm: migration: prepare EventFd for async migration events
This is a pre-requisite for the following commit which puts the
migration into a dedicated thread. It allows the VMM to react to
migration events (success/failure).

The commit series was inspired by @ljcore [0] but was changed quite
significantly.

[0] https://github.com/cloud-hypervisor/cloud-hypervisor/pull/7038

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-22 19:11:42 +00:00
Philipp Schuster
e034567690 vmm: init migration worker module
This initializes the module and the thread that will handle (control)
the migration. This introduces the new types without the necessary
wiring.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-22 19:11:42 +00:00
Henry Hrvoje Tonkovac
a8d7441c59 main: trim qualified paths in lib
Trim the one fully-qualified std::iter::successors path in the library
crate down to an imported module, and drop the now-unnecessary
crate-level #![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-22 14:20:46 +00:00
Anatol Belski
1db8858fac virtio-devices: block: Reuse descriptor chain's memory for queue enable
The two synchronous completion paths add the head to the used ring with
desc_chain.memory() but reload self.mem.memory() to enable
notifications. Keep both on the snapshot the chain was parsed from so
the used ring update and the notification enable always act on one
guest memory view rather than two independent atomic loads.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-21 19:46:46 +00:00
Anatol Belski
dd2f18e73e tests: Cover direct IO data disks on 4k sector FS
Add a parameterized helper that creates a 1 GiB ext4 loop filesystem
with 4096 byte sectors, populates it with a small data disk in the
requested format, attaches that disk with direct=on, and runs a 4096
byte aligned dd round trip with oflag=direct and iflag=direct
followed by cmp.

Wrappers exercise raw, qcow2, fixed VHD, and vhdx. The qcow2 and vhdx
wrappers expect the guest to see the on disk LBS of 512. The others
expect the host LBS of 4096.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
3f20fd0759 block: qcow: Drop own O_DIRECT alignment handling
The qcow workers carried their own O_DIRECT alignment requirement
and bounced unaligned cluster accesses through AlignedBuffer. Now
that the data file is an AlignedFile that handles O_DIRECT
transparently, the qcow layer can read and write through plain
buffers and let AlignedFile perform the aligned bounce.

Remove the alignment field and the per cluster AlignedBuffer RMW
branches from both the sync and async workers. The async io_uring
fast path still needs to avoid submitting unaligned guest iovecs
under O_DIRECT, so gate it on is_direct rather than on a stored
alignment value.

Drop the QcowAsync alignment override so it reports the trait
default sector size, matching QcowSync. qcow never submits guest
iovecs to the kernel under O_DIRECT, so reporting a larger value
only forced the request layer into an extra bounce buffer.

This adds one buffer copy per unaligned O_DIRECT cluster but moves
all alignment handling into a single place. The buffered path is
unchanged.

With qcow no longer the only caller, AlignedBuffer::read_exact_from
becomes dead code, so remove it and switch its tests to read_from.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
6633072a28 block: vhd: Enable O_DIRECT for fixed VHD images
Thread the direct flag from the disk open options through VhdDisk into
the AlignedFile the workers run on, so a fixed VHD opened with direct=on
issues O_DIRECT I/O instead of buffered I/O. Alignment is probed once on
that AlignedFile and reused by the sync and io_uring workers.

Advertise host topology from VhdDisk::topology by probing the underlying
file. On a 4096 byte sector filesystem opened with O_DIRECT this reports
logical_block_size 4096 to the guest, so the guest never issues 512 byte
I/O that the host kernel would reject as misaligned. Falls back to the
default topology with a warning when the probe fails.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
693987b9e2 block: vhd: Read footer via AlignedFile
Read the trailing footer sector through an AlignedFile rather than
probing the device topology and reading a full logical block. The
AlignedFile bounce buffer serves the trailing sector of an O_DIRECT
fd whose offset is unaligned against the device block size, so the
read no longer fails with EINVAL on a 4k sector backing store.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
2359003001 block: raw: Handle O_DIRECT in the raw async workers
The raw sync, io_uring and AIO workers now own an AlignedFile and use
it for the O_DIRECT alignment value and for the unaligned fallback.
Aligned operations keep the fast preadv and pwritev iovec path straight
to the kernel. When the offset or an iovec base or length is not a
multiple of the probed alignment, the worker gathers the iovecs into
one contiguous host buffer and runs a synchronous RMW through
AlignedFile, then scatters the result back into guest memory.

RawDisk constructs the AlignedFile from the disk file and the direct
flag and passes it into each worker, so alignment is probed once at
open time. The fixed VHD workers are threaded through the same
AlignedFile based constructors using a non-direct AlignedFile to
preserve current behavior.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
516f4e447d block: aligned: Add seeking cursor and drop RawFile
RawFile wrapped AlignedFile only to add a seek position and the file
trait impls that the qcow and vhost_user_block code expects. Fold that
position and every impl onto AlignedFile so the wrapper layer goes away
and callers work with a single O_DIRECT aware file type.

AlignedFile now tracks a cursor and implements Read, Write, Seek,
WriteZeroesAt, PunchHole, FileSync, SeekHole, BlockBackend, Clone,
AsRawFd and AsFd in addition to the positional FileExt path. The
direct_io flag is dropped because alignment already encodes it, where
a zero alignment means the file was not opened with O_DIRECT.

All RawFile uses in the qcow internals and vhost_user_block move to
AlignedFile, and raw_file.rs is removed.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
4c7e2b83c1 block: aligned: Query direct alignment in AlignedFile
Move the statx STATX_DIOALIGN probe out of DiskTopology into a free
probe_direct_alignment helper keyed on a raw fd. The helper gates on the
O_DIRECT open flag and returns the kernel reported alignment only when
direct I/O is in effect, and None otherwise. DiskTopology::probe keeps
the same call path and result.

AlignedFile::new now determines its O_DIRECT block alignment from
probe_direct_alignment instead of trial reads at 512 and 4096, falling
back to SECTOR_SIZE when the kernel does not report a value. This
matches how the raw and fixed VHD workers determine alignment, so all
backends agree on one source of truth.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 11:46:17 +00:00
Anatol Belski
80cc980d05 performance-metrics: Detect boot via the cloud-init callback
The boot time tests inferred guest readiness from the two debug I/O
port markers on stderr. Heavier boots could miss the fixed sleep
window, capture a single marker, and panic.

Wait on the cloud-init injected notify-booted callback through
guest.wait_vm_boot() instead. Once the callback fires, both markers
are guaranteed present, so they are parsed only for the metric. The
host side keeps an overall timeout, so a guest that never boots is
still reaped.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-20 08:29:23 +00:00
dependabot[bot]
b0610f3e91 build(deps): bump the non-rust-vmm group across 2 directories with 18 updates
Bumps the non-rust-vmm group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.2` | `1.23.3` |
| [zerocopy](https://github.com/google/zerocopy) | `0.8.50` | `0.8.52` |
| [smallvec](https://github.com/servo/rust-smallvec) | `1.15.1` | `1.15.2` |
| [block-buffer](https://github.com/RustCrypto/utils) | `0.12.0` | `0.12.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.63` | `1.2.64` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.100` | `0.3.102` |
| [memchr](https://github.com/BurntSushi/memchr) | `2.8.1` | `2.8.2` |
| [openssl-src](https://github.com/alexcrichton/openssl-src-rs) | `300.6.0+3.6.2` | `300.6.1+3.6.3` |
| [openssl-sys](https://github.com/rust-openssl/rust-openssl) | `0.9.116` | `0.9.117` |
| [regex](https://github.com/rust-lang/regex) | `1.12.3` | `1.12.4` |
| [rustls-pki-types](https://github.com/rustls/pki-types) | `1.14.0` | `1.14.1` |
| [wasip2](https://github.com/bytecodealliance/wasi-rs) | `1.0.3+wasi-0.2.9` | `1.0.4+wasi-0.2.12` |

Bumps the non-rust-vmm group with 8 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.2` | `1.23.3` |
| [zerocopy](https://github.com/google/zerocopy) | `0.8.50` | `0.8.52` |
| [smallvec](https://github.com/servo/rust-smallvec) | `1.15.1` | `1.15.2` |
| [block-buffer](https://github.com/RustCrypto/utils) | `0.12.0` | `0.12.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.63` | `1.2.64` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.100` | `0.3.102` |
| [memchr](https://github.com/BurntSushi/memchr) | `2.8.1` | `2.8.2` |
| [wasip2](https://github.com/bytecodealliance/wasi-rs) | `1.0.3+wasi-0.2.9` | `1.0.4+wasi-0.2.12` |



Updates `uuid` from 1.23.2 to 1.23.3
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.2...v1.23.3)

Updates `zerocopy` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `smallvec` from 1.15.1 to 1.15.2
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.1...v1.15.2)

Updates `block-buffer` from 0.12.0 to 0.12.1
- [Commits](https://github.com/RustCrypto/utils/compare/block-buffer-v0.12.0...block-buffer-v0.12.1)

Updates `cc` from 1.2.63 to 1.2.64
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.63...cc-v1.2.64)

Updates `js-sys` from 0.3.100 to 0.3.102
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `memchr` from 2.8.1 to 2.8.2
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.1...2.8.2)

Updates `openssl-src` from 300.6.0+3.6.2 to 300.6.1+3.6.3
- [Release notes](https://github.com/alexcrichton/openssl-src-rs/releases)
- [Commits](https://github.com/alexcrichton/openssl-src-rs/commits)

Updates `openssl-sys` from 0.9.116 to 0.9.117
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-sys-v0.9.116...openssl-sys-v0.9.117)

Updates `regex` from 1.12.3 to 1.12.4
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.3...1.12.4)

Updates `regex-syntax` from 0.8.10 to 0.8.11
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/regex-syntax-0.8.10...regex-syntax-0.8.11)

Updates `rustls-pki-types` from 1.14.0 to 1.14.1
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.14.0...v/1.14.1)

Updates `wasip2` from 1.0.3+wasi-0.2.9 to 1.0.4+wasi-0.2.12
- [Commits](https://github.com/bytecodealliance/wasi-rs/compare/wasip2-1.0.3...wasip2-1.0.4)

Updates `wasm-bindgen` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro-support` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-shared` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `zerocopy-derive` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `uuid` from 1.23.2 to 1.23.3
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.2...v1.23.3)

Updates `zerocopy` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `block-buffer` from 0.12.0 to 0.12.1
- [Commits](https://github.com/RustCrypto/utils/compare/block-buffer-v0.12.0...block-buffer-v0.12.1)

Updates `cc` from 1.2.63 to 1.2.64
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.63...cc-v1.2.64)

Updates `js-sys` from 0.3.100 to 0.3.102
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `memchr` from 2.8.1 to 2.8.2
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.1...2.8.2)

Updates `rustls-pki-types` from 1.14.0 to 1.14.1
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.14.0...v/1.14.1)

Updates `smallvec` from 1.15.1 to 1.15.2
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.1...v1.15.2)

Updates `wasip2` from 1.0.3+wasi-0.2.9 to 1.0.4+wasi-0.2.12
- [Commits](https://github.com/bytecodealliance/wasi-rs/compare/wasip2-1.0.3...wasip2-1.0.4)

Updates `wasm-bindgen` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro-support` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-shared` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `zerocopy-derive` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `uuid` from 1.23.2 to 1.23.3
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.2...v1.23.3)

Updates `zerocopy` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `smallvec` from 1.15.1 to 1.15.2
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.1...v1.15.2)

Updates `block-buffer` from 0.12.0 to 0.12.1
- [Commits](https://github.com/RustCrypto/utils/compare/block-buffer-v0.12.0...block-buffer-v0.12.1)

Updates `cc` from 1.2.63 to 1.2.64
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.63...cc-v1.2.64)

Updates `js-sys` from 0.3.100 to 0.3.102
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `memchr` from 2.8.1 to 2.8.2
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.1...2.8.2)

Updates `wasip2` from 1.0.3+wasi-0.2.9 to 1.0.4+wasi-0.2.12
- [Commits](https://github.com/bytecodealliance/wasi-rs/compare/wasip2-1.0.3...wasip2-1.0.4)

Updates `wasm-bindgen` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro-support` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-shared` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `zerocopy-derive` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `uuid` from 1.23.2 to 1.23.3
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.2...v1.23.3)

Updates `zerocopy` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

Updates `block-buffer` from 0.12.0 to 0.12.1
- [Commits](https://github.com/RustCrypto/utils/compare/block-buffer-v0.12.0...block-buffer-v0.12.1)

Updates `cc` from 1.2.63 to 1.2.64
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.63...cc-v1.2.64)

Updates `js-sys` from 0.3.100 to 0.3.102
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `memchr` from 2.8.1 to 2.8.2
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.1...2.8.2)

Updates `smallvec` from 1.15.1 to 1.15.2
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.1...v1.15.2)

Updates `wasip2` from 1.0.3+wasi-0.2.9 to 1.0.4+wasi-0.2.12
- [Commits](https://github.com/bytecodealliance/wasi-rs/compare/wasip2-1.0.3...wasip2-1.0.4)

Updates `wasm-bindgen` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-macro-support` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `wasm-bindgen-shared` from 0.2.123 to 0.2.125
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.123...0.2.125)

Updates `zerocopy-derive` from 0.8.50 to 0.8.52
- [Release notes](https://github.com/google/zerocopy/releases)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.50...v0.8.52)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.23.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: smallvec
  dependency-version: 1.15.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: block-buffer
  dependency-version: 0.12.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.64
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.102
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: memchr
  dependency-version: 2.8.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: openssl-src
  dependency-version: 300.6.1+3.6.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: openssl-sys
  dependency-version: 0.9.117
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: regex
  dependency-version: 1.12.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: regex-syntax
  dependency-version: 0.8.11
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rustls-pki-types
  dependency-version: 1.14.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasip2
  dependency-version: 1.0.4+wasi-0.2.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.52
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: uuid
  dependency-version: 1.23.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: block-buffer
  dependency-version: 0.12.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.64
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.102
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: memchr
  dependency-version: 2.8.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: rustls-pki-types
  dependency-version: 1.14.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: smallvec
  dependency-version: 1.15.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasip2
  dependency-version: 1.0.4+wasi-0.2.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.52
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: uuid
  dependency-version: 1.23.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: smallvec
  dependency-version: 1.15.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: block-buffer
  dependency-version: 0.12.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.64
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.102
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: memchr
  dependency-version: 2.8.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasip2
  dependency-version: 1.0.4+wasi-0.2.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.52
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: uuid
  dependency-version: 1.23.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: block-buffer
  dependency-version: 0.12.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.64
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.102
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: memchr
  dependency-version: 2.8.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: smallvec
  dependency-version: 1.15.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasip2
  dependency-version: 1.0.4+wasi-0.2.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.125
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.52
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 09:46:02 +00:00
Rob Bradford
6fee484715 vmm: Reject resizing below the boot size with an error
This was already handled with user memory zones but not with the default
memory. Make a small refactoring to move the boot RAM check into
MemoryManager rather than split across Vm and MemoryManager.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-19 08:11:16 +00:00
Wei Liu
dbb33a5645 pci: leak the address when munmap fails
This is more lenient than aborting the whole process. Leaking memory is
safe in Rust.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-19 00:53:58 +00:00
dependabot[bot]
9d743b012b build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 00:40:04 +00:00
Henry Hrvoje Tonkovac
066091a54c arch: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-18 23:08:10 +00:00
Atish Patra
eb1c64e4f0 tests: integration: assert same-host pause/resume keeps aarch64 clock
Add an aarch64 test that pauses a running VM, waits out an interval, and
resumes it on the same host, then asserts the guest wall clock still
matches the host. On aarch64 the architected counter free-runs across
the pause, so the guest self-corrects.

The downtime and skew tolerance are shared with the snapshot clock test.
x86_64 has its own kvmclock path and is covered by the snapshot clock
test.

Signed-off-by: Atish Patra <atishp@meta.com>
2026-06-18 22:59:37 +00:00
Atish Patra
eb2dc28edc tests: integration: assert the guest clock catches up across restore
Add a variation of _test_snapshot_restore that, after taking a snapshot,
waits out a simulated off-host interval and then restores and resumes,
asserting that the guest's wall clock has caught up to the host. This
exercises the clock catch-up that each architecture provides on restore:
kvmclock (KVM_CLOCK_REALTIME) on x86_64 today, and the CNTVCT advance on
aarch64 with later commits.

On x86_64 the guest is booted with clocksource=kvm-clock as the guest
clock is caught up after pause/resume only in that mode. A
tsc-clocksource guest's restored TSC freezes across the interval and
would never catch up.

Take this opportunity to improve the snapshot restore test as the
existing bare boolean mechanism was bit hard to read with new test.

Signed-off-by: Atish Patra <atishp@meta.com>
2026-06-18 22:59:37 +00:00
Atish Patra
25271c9d0c hypervisor: aarch64: advance the guest counter on restore and migration
Currently, Cloud Hypervisor round-trips CNTVCT_EL0 through
KVM_GET_REG_LIST/SET_ONE_REG, which leaves a cold-restored or migrated
guest behind real UTC by the downtime. Same-host pause/resume
self-corrects (the physical counter keeps running across the pause), so
only restore and migration cases required the clock to catch up to wall
clock time.

Since ARM has no kernel helper, compute the difference in wall clock
time and compute the ticks so that it can advance the CNTVCT correctly.
It is set via vcpu0 only as it affects a single VM wide value after
Linux 6.4. For older kernels, it was a truly vcpu value which needs to
be invoked for every vcpu.

Gated on all(target_arch = "aarch64", feature = "kvm"); x86 is
unchanged.

Basic manual test case (aarch64 + KVM) verified both in intra host and
inter host snapshot save/restore:

1. Boot a Linux guest; in the guest, `date -u` tracks the host's UTC.
2. Pause and snapshot the VM (ch-remote pause; ch-remote snapshot
   file:///<dir>).
3. Leave it down for several minutes (the off-host interval).
4. Restore and resume into a fresh VMM (ch-remote restore
   source_url=file:///<dir>,resume=true).
5. In the guest, run `date -u` again and compare to the host: the guest
   now tracks current UTC, having advanced by ~the time it spent down.

Before this change the restored guest reads behind real UTC by the
downtime; after it, the guest clock is back in sync (to within the
snapshot-to-restore sampling slop).

Signed-off-by: Atish Patra <atishp@meta.com>
2026-06-18 22:59:37 +00:00
Atish Patra
69637dde69 hypervisor: aarch64: capture the guest counter for snapshot/restore
Unlike x86, ARM64 has no kvmclock support to sync guest time upon
required. However, the guest reads the architected virtual timer
(CNTVCT_EL0) directly which can be modified by the VMM to update the
time after snapshot restore. Since the CNTVCT is in ticks, we also need
to read CNTFRQ (via mrs due to lack of ONEREG interface) to compute the
ticks from wall clock difference.

Because the counter is a vCPU register, the capture must run with the
vCPUs quiesced, so the VMM now captures the clock just after
cpu_manager.pause() through the boot vCPU. This is behaviorally
identical for x86, whose clock is VM-wide. There is no restore/advance
yet, so aarch64 guests still resume behind real time until the following
commit.

Signed-off-by: Atish Patra <atishp@meta.com>
2026-06-18 22:59:37 +00:00
Atish Patra
ad909a3d71 vmm: drive guest clock save/restore through the vm abstraction
Currently, VM pause/resume/snapshot paths invoke architecture specific
bits for guest clock udpates which ideally belongs to hypervisor layer.

Route it through the snapshot_clock()/restore_clock() pair added in the
previous commit instead, so the VMM no longer depends on an architecture
specific clock API and the upcoming aarch64 backend can hook the same
path without a parallel branch in vm.rs.

Signed-off-by: Atish Patra <atishp@meta.com>
2026-06-18 22:59:37 +00:00
Atish Patra
d68b93ea93 hypervisor: add a generic guest-clock save/restore abstraction
Preserving the guest clock across pause/resume and snapshot/restore is
currently open-coded in the VMM against the x86-only
get_clock/set_clock. aarch64 needs the same correction but via a
different mechanism (i.e. the architected counter, CNTVCT). Having a
common backend-agnostic interface that VMM can drive uniformly allows us
to keep the architecture details behind the Hypervisor abstraction.

This commit only introduces the abstraction while the future commits
will actually move the implementation to use it.

Use this opportunity to fix the full path to get SystemTime as well.

Suggested-by: Sebastien Boeuf <sboeuf@meta.com>
Signed-off-by: Atish Patra <atishp@meta.com>
2026-06-18 22:59:37 +00:00
Henry Hrvoje Tonkovac
00edf5b34d hypervisor: trim qualified paths in msr_index
Import the std module used in the generated MSR-index file instead of
spelling the full paths at every use site.

The crate-level #![expect(clippy::absolute_paths)] is kept; the
remaining sites in the rest of the crate are handled by a follow-up.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-18 21:17:14 +00:00
Thomas Prescher
490a9a87af vmm: Emit complete ACPI S5 sleep package
OpenBSD expects the ACPI _S5_ object to provide both sleep type values.
The single-value package made acpi_init_states() parse an invalid object
and fault during early ACPI setup with:

```
...
acpi0 at bios0: ACPI 6.3
acpi0: sleep statesfatal protection fault in supervisor mode
trap type 4 code 0 rip ffffffff814af264 cs 8 rflags 10282 cr2 0 cpl e rsp ffffffff81a06a30
gsbase 0xffffffff81755ff0  kgsbase 0x0
panic: trap type 4, code=0, pc=ffffffff814af264
Starting stack trace...
panic(ffffffff81a06980,4,ffffffff81a06a58,ffffffff81756ae0,ffffffff81a06960,ffffffff81a068e0) at panic+0x12e
kerntrap(ffffffff81a06aa0,ffff800000232400,ffffffff81261681,ffffffff81a06970,ffffffff81a06980,4) at kerntrap+0xe1
alltraps_kern_meltdown(4,ef0d316e102be1f4,ffff800000232480,0,ffffffff81a06aa0,ffff800000232400) at alltraps_kern_meltdown+0x7b
aml_val2int(ef0d316e102be1f4,0,10,ffffffff81a06a30,10282,8) at aml_val2int+0x24
acpi_init_states(1,ffff800000232400,ffff800000232470,0,ef0d316e102be1f4,5f35535f) at acpi_init_states+0xd5
acpi_attach_common(ffff800000235300,ffffffff81a06cf0,ffffffff81762a00,ffff800000232400,ffff800000232424,ef0d316e102be1f4) at acpi_attach_common+0x311
config_attach(ffffffff81a06d30,ffff800000235300,50,118,ffff80003158c004,ffffffff813e3270) at config_attach+0x1d2
bios_attach(ffff800000235280,ffffffff81a06e28,ffffffff8175ca50,ffff800000235300,ffff800000235324,ef0d316e102be1f4) at bios_attach+0x898
config_attach(ffffffff81a06e28,ffff800000235280,ffffffff81757e68,ffff800000235280,ffff8000002352a4,ffffffff8128c510) at config_attach+0x1d2
mainbus_attach(0,0,ef0d316e102be1f4,ffffffff81a06e50,ffffffff81a06ec0,3000000010) at mainbus_attach+0x70
config_attach(8,1001000,805f50,1000000,ffffffff81a00008,0) at config_attach+0x1d2
cpu_configure(8,1001000,ffffffff814f3859,ffffffff81a06f20,8,1001000) at cpu_configure+0x29
main(1001000,ef0d316e102be1f4,ffffffff812e8b2f,ffffffff81a06f40,8,1001000) at main+0x3af
end trace frame: 0x0, count: 244
End of stack trace.
```

Advertise S5 as the conventional four-element package as described in
the ACPI spec [1]. Cover the generated AML bytes with a unit test.

In AML, the package now looks like this:

```
Name (_S5, Package () {
  0x05, 0x05, 0x00, 0x00
})
```

[1] https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/07_Power_and_Performance_Mgmt/oem-supplied-system-level-control-methods.html#sx-system-states

On-behalf-of: SAP thomas.prescher@sap.com
Signed-off-by: Thomas Prescher <thomas.prescher@cyberus-technology.de>
2026-06-18 18:38:01 +00:00
Rob Bradford
24aeb1ed71 build: Don't use aws-lc-rs for TLS
In #8053 it was agreed to use the ring backend in preference to
aws-lc-rs as it is pure Rust. However since aws-lc-rs is a default
feature of rustls the addition of ring was additive not a replacement.
Fix the features to exclude awc-lc-rs.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-18 18:27:33 +00:00
Rob Bradford
7e2e7a164b block: Drain io_uring in-flight operations on teardown
Closing an io_uring fd does not synchronously finish requests that
already reached the kernel. During block worker teardown this can let
an io-wq worker keep using retained guest-memory iovecs after reset.

Drain UringDataIo in Drop: retry any published SQEs and wait for CQEs
until no retained operation remains. If draining fails, leak retained
buffers. Drop QcowAsync's ring before its data fd so retrying
published SQEs still uses a valid descriptor.

To avoid a potential infinite loop when completions fail to be delivered
cap the number of iterations of the loop (2x the number of inflight
requests).

Assisted-by: Codex:GPT-5
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-18 17:35:27 +00:00
Rob Bradford
1699a81f21 build: Disable rate-limiter job on MQ
This is not a blocking job but the worker is no longer handling the jobs
so it remains pending indefinitely.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-18 16:56:32 +00:00
Henry Hrvoje Tonkovac
74a749b960 virtio-devices: trim qualified paths
Import the modules used in the crate instead of spelling the full paths
at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-18 15:55:15 +00:00
Sebastien Boeuf
2f2f709a0e docs: Document offload snapshot/restore
Extending the snapshot/restore documentation so that it explains what
are the goals behind this offloaded snapshot/restore feature, how to use
it in practice, and also by documenting the protocol used by the offload
daemon so that anyone could write its own daemon.

By relying on the existing local live migration support and reusing the
semantics and the protocol associated with it, we intend to provide a
way for snapshotting and restoring a VM to/from a dedicated process that
we can call the offload daemon.

By allowing an external process to perform the snapshot/restore actions
on behalf of Cloud Hypervisor, we give our users the opportunity to
implement their own offloaded daemon. The goal is to avoid bloating
Cloud Hypervisor with numerous features related to snapshot/restore, and
let the user decide how to perform the snapshot/restore actions. One
example is that we can decide to encrypt the guest RAM on the fly in
order to avoid writing an unencrypted version to local disk. Another
example is to be able to send guest RAM and associated state/config data
over the network without having to persist the data first to local
storage.

There might be other reasons to choose going with an offloaded daemon to
perform the snapshot/restore of the VM, but in every case, this empowers
the user to make their own choice.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-18 13:45:36 +00:00
Sebastien Boeuf
28b6b5d467 offload_daemon: Implement sparse snapshot/restore
Copy only populated extents when writing the snapshot file and when
filling the restore memfd, leaving unwritten ranges as holes. Both
the on-disk snapshot and the restored guest RAM stay sparse, so that
untouched guest pages cost no disk space or host memory.

This brings the offload daemon closer to be at feature parity with CH's
internal implementation of snapshot/restore. The only missing piece is
on-demand paging at this point.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-18 13:45:36 +00:00
Sebastien Boeuf
df5d2d6003 vmm: Extract sparse file-copy helpers into a reusable module
Move next_data_extent and write_region_sparse out of memory_manager.rs
into a new vmm::sparse module so the snapshot writer, the restore
reader, and the offload daemon can share one implementation.

No functional change intended.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-18 13:45:36 +00:00
Sebastien Boeuf
6a74021ad5 ci: Add integration test for offload snapshot
Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-18 13:45:36 +00:00
Sebastien Boeuf
4992fabd15 offload_daemon: Introduce a reference implementation
Adding a new dedicated binary that is meant to be used as a reference
implementation for validating that offloaded snapshot/restore works and
meant to be used through tests in general.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-18 13:45:36 +00:00
Sebastien Boeuf
cca8e1cd52 vmm: Export VmMigrationConfig as public
Expose VmMigrationConfig as a public facing structure that can be used
by an offload daemon to act as if it was the VM to migrate to, or the VM
to migrate from.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-06-18 13:45:36 +00:00
Henry Hrvoje Tonkovac
d7c86b8b67 devices: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary
#[cfg_attr(target_arch = "x86_64", expect(clippy::absolute_paths))].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-18 11:33:58 +00:00
Henry Hrvoje Tonkovac
4b06dacc0b test_infra: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-18 11:16:12 +00:00
yanjianqing
58c08ffcca vmm: seccomp: Add SYS_fsync to vcpu thread
Fix disk hot unplug failure caused by seccomp SIGSYS kill.

When performing disk hot unplug, the vcpu thread calls fsync()
on the block device file descriptor to flush pending I/O.
The seccomp filter previously blocked SYS_fsync, triggering SIGSYS
and terminating the vcpu thread, which makes the hot unplug
operation fail. This issue exists on both x86 and AArch64.

Strace log snippet captured during failure:
```
[pid 3118852] fsync(142) = 142
[pid 3118852] ---SIGSYS {si_signo=SIGSYS,si_code=SYS_SECCOMP,si_call_addr=0xffff9c931df8, si_syscall=__NR_fsync,si_arch=AUDIT_ARCH_AARCH64}
```
Add unrestricted SYS_fsync entry to vcpu thread syscall allowlist,
consistent with existing file I/O syscalls such as fcntl and fstat.

Signed-off-by: yanjianqing <yanjianqing@kylinos.cn>
2026-06-18 08:52:39 +00:00
Rob Bradford
61193de6e3 pci: msix: Reject mis-sized MSI-X table and PBA reads
Replace assertions for incorrect access sizes with logged errors. The
write_table() method already handled it like this and this commit
extends the same pattern to read_table() and read_pba().

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-17 19:48:43 +00:00
Henry Hrvoje Tonkovac
427c4de928 performance-metrics: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-17 17:28:07 +00:00
Henry Hrvoje Tonkovac
6683ae2d51 block: trim qualified paths in vhdx tests
Import the std modules used in the test module instead of spelling the
full paths at every use site, and drop the now-unnecessary
#[expect(clippy::absolute_paths)] on the vhdx internal test module.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-17 17:14:44 +00:00
Tushar Khatri
ff83b27937 vhost_user_net: reevaluate #[allow] attributes
Convert the still-needed #[allow] to #[expect] so it warns if the
lint stops firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Tushar Khatri
5467db8c54 tracer: reevaluate #[allow] attributes
Convert the still-needed #[allow] to #[expect] so it warns if the
lint stops firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Tushar Khatri
a87b73a585 test_infra: reevaluate #[allow] attributes
Convert the still-needed #[allow]s to #[expect] so they warn if the
lints stop firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Tushar Khatri
bf68b35ab7 performance-metrics: reevaluate #[allow] attributes
Convert the still-needed #[allow] to #[expect] so it warns if the
lint stops firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Tushar Khatri
510aa438f8 tests: reevaluate #[allow] attributes
Convert the still-needed #[allow]s to #[expect] so they warn if the
lints stop firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Tushar Khatri
1555b59d42 ch-remote: reevaluate #[allow] attributes
Convert the still-needed #[allow] to #[expect] so it warns if the
lint stops firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Tushar Khatri
7be97937ef block: reevaluate #[allow] attributes
Convert the still-needed #[allow]s to #[expect] so they warn if the
lints stop firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-17 17:14:00 +00:00
Henry Hrvoje Tonkovac
9c0bceceb1 tpm: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-17 17:10:29 +00:00
Henry Hrvoje Tonkovac
2a121b807e serial_buffer: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary
#[expect(clippy::absolute_paths)] on the test module.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-17 17:10:03 +00:00
Henry Hrvoje Tonkovac
c0e133bf05 vm-migration: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site, and drop the now-unnecessary crate-level
#![expect(clippy::absolute_paths)].

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-17 17:09:22 +00:00
Rob Bradford
e9f2f14e4c build: Add KVM SEV-SNP testing on MQ
Using a new dedicated runner do SEV-SNP testing on the MQ using the CVM
test suite.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-17 15:02:53 +00:00
Rob Bradford
4a88395a35 tests: Add missing preparatory steps needed for CVM tests
The tests expect some generated images and assets so make sure that they
are there (directly copied from the x86-64 test).

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-17 15:02:53 +00:00
Rob Bradford
e4a6208a88 build: Temporarily drop the MSHV CI jobs
These fail more than they pass due to infrastructure reasons (rather
than tests failing) and so because they are constantly seen as failing
they are ignored (i.e. alarm fatigue).

It is better not to run them until the infrastructure issues have been
fixed.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-17 14:59:22 +00:00
Alexander Lvov
e0801bda3b block: vhd: fix incomplete bounds check in sync I/O worker
The sync I/O worker only checked that the operation offset did
not start past the end of the virtual disk (offset >= size) -
did not verify that the operation end (offset + len) stays
within bounds.

A read or write that started inside the image but extended
beyond the logical size was silently passed to the raw backend.

The async io_uring worker already had the correct check
(offset + len > size with overflow protection). I extracted it
into a shared helper in worker/common.rs and reused inside the
sync path to eliminate duplication and close the gap.

Fixes #8311

Signed-off-by: Alexander Lvov <alexander.lvov.git@gmail.com>
2026-06-17 14:26:25 +00:00
Rob Bradford
7ca99204ed vmm: cpu: Reject mis-sized ACPI CPU hotplug register accesses
Reject without asserting that the ACPI CPU hotplug register accesses
match those that are specified by the ACPI definitions.

Signed-off-by: Rob Bradford <rbradford@meta.com>
Assisted-by: Claude:Opus-4.7
2026-06-17 18:06:21 +01:00
Rob Bradford
df09e80b89 vmm: device_manager: Reject mis-sized PCI hotplug register accesses
Reject without asserting that the ACPI PCI hotplug register request
matches what is defined in the ACPI definition.

Signed-off-by: Rob Bradford <rbradford@meta.com>
Assisted-by: Claude:Opus-4.7
2026-06-17 18:06:21 +01:00
Rob Bradford
8a4b3efec9 devices: acpi: Reject mis-sized accesses to shutdown and GED devices
These devices should only be accessed by single byte accesses as
specified through the ACPI definitions for them.

Signed-off-by: Rob Bradford <rbradford@meta.com>
Assisted-by: Claude:Opus-4.7
2026-06-17 18:06:21 +01:00
Meng Zhuo
14aa30cd2e vmm: retrieve timebase-frequency from KVM instead of hardcoding
The RISC-V device tree's timebase-frequency was hardcoded to 10 MHz
(0x989680). Actual hardware uses different frequencies.

Read the timebase frequency from KVM_GET_ONE_REG via
KVM_REG_RISCV_TIMER (offset 0, kvm_riscv_timer.frequency),
thread it through the VMM to arch to FDT layers, and fall back to
the 10 MHz default when KVM returns no value.

Signed-off-by: Meng Zhuo <mengzhuo@iscas.ac.cn>
2026-06-17 16:05:53 +01:00
Meng Zhuo
085642dd42 hypervisor: riscv64: clamp AIA SRCS to KVM device capacity
The number of wired interrupt sources (SRCS) must be less than the
KVM device's maximum interrupt identities (kvm_riscv_aia_max_ids).
Platforms with smaller IMSIC capacity reject values that exceed this
limit.

Query KVM_DEV_RISCV_AIA_CONFIG_IDS before setting SRCS and clamp the
requested value to the reported nr_ids (which equals max_ids - 1).

Signed-off-by: Meng Zhuo <mengzhuo@iscas.ac.cn>
2026-06-17 16:05:53 +01:00
Rob Bradford
2bc968ba1d build: Deny clippy::absolute_paths
Removal of absolute paths is currently in progress. To avoid regressing
those changes add a clippy deny at the workspace level and at the crate
level override with #[expect(clippy::absolute_paths)]

See: #7670

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-17 14:38:25 +01:00
Pascal Scholz
257a00547a block: Retry locking when interrupted by EINTR
Acquiring an image lock can be interrupted with EINTR. In this case, we
returned with an error. Instead, we now retry acquiring the lock.

Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
On-behalf-of: SAP pascal.scholz@sap.com
2026-06-17 08:44:50 +00:00
Bo Chen
ca2f847e5f tests: Add integration test for FD-based VFIO device
This also covers the usage of pre-opened iommufd FD.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
19fb12c28b vmm: device_manager: Use externally-supplied iommufd FD
Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
867b5796a5 vmm: Accept an externally-opened iommufd FD
The CLI `--platform` option now accepts `iommufd_fd=<n>` alongside the
existing `iommufd=on|off`.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
e4934de3c9 ch-remote: Support FD-based VFIO devices
Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
0419ab6f39 vmm: OpenAPI: Make DeviceConfig path optional
`path` is no longer required in the DeviceConfig, since a VFIO
device may also be supplied via a pre-opened cdev FD passed via
SCM_RIGHTS alongside the /vm.add-device request.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
c315d5fd96 vmm: Enable FD-based VFIO devices
Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
d9f89ef2ab vmm: http_api: Accept a VFIO device FD via SCM_RIGHTS
Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
0e4b98ae8e vmm: DeviceConfig: Add fd field for an externally-opened vfio cdev
Add a new `fd: Option<i32>` field to DeviceConfig so a caller can
supply a pre-opened vfio cdev FD (e.g. /dev/vfio/devices/vfioN) in
addition to the existing sysfs path. The CLI `--device` option now
accepts `fd=<n>`, parsed alongside the existing options.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
bc5363823a vmm: DeviceConfig: Store path as Option<PathBuf>
Relax DeviceConfig::path from PathBuf to Option<PathBuf> in preparation
to accept an externally-opened vfio cdev FD. The parser and OpenAPI spec
still enforces that `path` is set, so callers see no behavior change.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-06-17 08:39:43 +00:00
Bo Chen
96ea24339d vmm: seccomp: Allow IOMMU_DESTORY ioctl
Update the seccomp filter for vmm and vcpu thread, because `Drop for
VfioIommufd` since vfio-ioctls v0.6.1 now issues IOMMU_DESTROY
to release the IOAS allocated for each VM boot.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-06-17 08:39:43 +00:00
Bo Chen
75e4a46969 build: Update rust-vmm dependencies in /fuzz
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-06-17 08:39:43 +00:00
Anatol Belski
fd4a923c37 virtio-devices: block: Test parse failure head reclamation
Submit a head only virtio-blk chain, confirm Request::parse rejects
it, and verify that add_used with len 0 returns the head to the
guest by advancing the used ring index.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-16 22:02:46 +00:00
Anatol Belski
e1a63b41ff virtio-devices: block: Reclaim head on malformed descriptor chain
When Request::parse failed, for example for a chain containing only
the head descriptor, process_queue_submit returned the error via
`?`. The caller process_queue_submit_and_signal swallowed
Error::RequestParsing with a warn! and returned Ok(()), but
queue.iter().next() had already consumed the head from the avail
ring. The head was never written to the used ring, so the descriptor
slot leaked and the queue could be stalled by a guest that keeps
submitting malformed chains.

Handle the parse error in line. Log a warning, add the head to the
used ring with len 0, reenable notifications, and continue draining
the queue. A VIRTIO_BLK_S_IOERR status cannot be written because the
status descriptor address is exactly what failed to parse.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-16 22:02:46 +00:00
Philipp Schuster
4491a3e412 vmm: improved error messages
Unrelated improvements that help to catch common pitfalls.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-16 22:01:30 +00:00
Philipp Schuster
15cab7ee6a vmm: improve VM ownership handling
Introducing a new enum that models the various states of VM ownership
from the perspective of the VMM.

This is an important prerequisite for the asynchronization of the
migration, where the ownership of the Vm struct is transferred to the
migration thread. Specifically, this allows to introduces a new
"Migration(ThreadHandle)" variant and all existing match statements
can be easily extended to react accordingly.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-16 22:01:30 +00:00
Rob Bradford
b4c1d85327 block: vhdx: Use AlignedFile for O_DIRECT-safe I/O
By redirecting VHDx I/O through the AlignedFile the required RMW
semantics can be achieved for writes less than the logical block size
whilt reusing the same logic used for other backend implementations.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
80a0393edd block: qcow: Port RawFile to AlignedFile
Reuse the functionality in the AlignedFile wrapper for the QCOW RawFile
wrapper. This makes alignment handling more transparent.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
3fe5225a44 block: Add AlignedFile
Provide a single home for O_DIRECT alignment and RMW behavior behind an
std::os::unix::fs::FileExt implementation built on AlignedBuffer.

Unaligned requests are bounced through an AlignedBuffer (applying RMW
for writes) and aligned requests pass straight through to the inner
File.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
201ddaef55 block: qcow: Port to FileExt
Replace the use of the pread64/pwrite64 helpers with versions from
std::os::unix::fs::FileExt.

As this was the last use of these pread functions remove them and their
tests.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
108d251c1d block: qcow: Port to AlignedBuffer
Replace the qcow specific AlignedBuf along with the pread64/pwrite64
helpers with the new common AlignedBuffer implementation.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
2e2167368e block: qcow: Port backing file support to FileExt
Replace use of raw pread64/pwrite64 functions with
std::os::unix::fs::FileExt for I/O without a cursor.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
30e79de26f block: qcow: Port RawFile to AlignedBuffer
Replace the manual alloc_zeroed/dealloc and pread64/pwrite64 with use of
the new AlignedBuffer structure.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
4ad95e1cc6 block: Add AlignedBuffer
The block code repeatedly open-codes O_DIRECT alignment calculations and
bounce-buffer allocation at each I/O site. Add an AlignedBuffer struct
that handles the alignment and allocation in one place, using FileExt
(read_exact_at and write_all_at) for the I/O (so no need for custom libc
wrappers).

The caller creates an AlignedBuffer with an offset, length and
alignment, then uses read_from and write_to for aligned I/O and as_slice
and as_mut_slice to access the logical data portion within the aligned
region.

This is a lot like the AlignedBuf that was already existing in the QCOW2
code but is a more generalised version.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
a8b059328f block: Implement FileExt for RawFile
Implement std::os::unix::fs::FileExt for RawFile by delegating to the
inner File. This enables callers holding a reference to a RawFile to use
read_exact_at and write_all_at directly for non-cursor I/O (like pread,
etc) without going through custom helper functions.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 14:43:11 +00:00
Rob Bradford
aad8ea0bd7 build: Bump some rust-vmm crates
Bump rust-vmm crates that don't require bumping vm-memory (which will
require broader porting).

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-16 13:09:48 +00:00
Henry Hrvoje Tonkovac
0e2e8d332a vm-virtio: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-16 11:07:02 +00:00
Henry Hrvoje Tonkovac
88baef1449 vm-device: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-16 11:02:03 +00:00
Henry Hrvoje Tonkovac
11bacdee46 vhost_user_block: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-16 10:54:05 +00:00
Henry Hrvoje Tonkovac
11fab725ae vhost_user_net: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-16 10:53:30 +00:00
Henry Hrvoje Tonkovac
e5f32e986f block: trim qualified paths in io and lib
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-16 10:52:57 +00:00
Wei Liu
b51dfec09c tests: re-enable some MSHV tests
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-16 07:06:47 +00:00
tonic
ce9416a9c2 arch: x86_64: advertise mandatory Hyper-V partition privileges
The Microsoft "Requirements for Implementing the Microsoft Hypervisor
Interface" document marks exactly two privileges in CPUID leaf
0x40000003 EAX as "Must be set": AccessHypercallMsrs (bit 5) and
AccessVpIndex (bit 6). Cloud Hypervisor advertised neither.

Without bit 5, Windows guests abort enlightened-mode initialization
before timer-API selection: HalpHvTimerApi is left NULL and every
QueryPerformanceCounter call falls back to reading
HV_X64_MSR_TIME_REF_COUNT (0x40000020), costing one VM exit per call.
The guest never writes HV_X64_MSR_REFERENCE_TSC (0x40000021) to enable
the reference TSC page, even though AccessPartitionReferenceTsc (bit 9)
is advertised.

With both bits set, Windows 10 22H2 and Windows 11 25H2 guests enable
the reference TSC page at boot. Measured QueryPerformanceCounter
throughput on a nested-KVM host went from ~71K calls/sec (14 us/call,
one MSR exit each) to ~1.3M calls/sec (free, no exits); on bare metal
from ~390K to ~1.9M calls/sec. Guest idle CPU and interrupt-service
time drop correspondingly.

Both MSR ranges are already handled in-kernel by KVM unconditionally,
so no backend change is needed. Bisection across the full delta to
QEMU's Hyper-V CPUID layout (vendor ID, max leaf, leaves 4-6 contents,
build number) shows bit 5 is the only load-bearing change; bit 6 is
included per the conformance document's mandate.

Signed-off-by: Tonic Li <tonic@simular.ai>
Signed-off-by: tonic <tonicbupt@gmail.com>
2026-06-15 13:54:01 +00:00
Henry Hrvoje Tonkovac
fffa200240 rate_limiter: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-15 13:55:12 +01:00
Anatol Belski
0e4e3277a9 block: Add WriteZeroes sector overflow regression test
Cover the prior commit by constructing a Request directly and a stub
AsyncIo whose backend methods are unreachable, then submit a payload
with sector + num_sectors past u64::MAX and assert BadRequest.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-15 13:54:39 +01:00
Anatol Belski
f7ebb4b871 block: Bounds check WriteZeroes before sector multiplication
In Request::execute_async the WriteZeroes arm multiplied wz_sector
by SECTOR_SIZE before the checked_add of sector and num_sectors.
A wz_sector near u64::MAX overflows the multiplication.

Reorder the arm to run the checked_add and disk_nsectors check
first, matching the Discard arm above.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-15 13:54:39 +01:00
Henry Hrvoje Tonkovac
d9f3400aba tracer: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-15 11:20:05 +01:00
Henry Hrvoje Tonkovac
2c22159802 block: trim qualified paths in formats
Import the std modules used in the disk-format handlers instead of
spelling the full paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-15 08:22:19 +00:00
Dylan Reid
50f2fd369f virtio-devices: block: drain async I/O before pausing
During pause the block backend's async I/O path can have unfinished I/O
requests. A snapshot or migration RAM copy taken after pause returns can
then race with kernel writes and capture torn pages.

Since vCPUs are already paused, the VMM thread can stop new block
submissions and wait for the worker to drain before parking the worker
threads.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-13 18:13:37 +00:00
Tushar Khatri
859bce5cae hypervisor: reevaluate #[allow] attributes
Remove stale #[allow]s whose lints no longer fire, convert the
unconditionally-firing ones to #[expect], and keep the conditional
ones as #[allow] (e.g. large_enum_variant only fires when both kvm
and mshv are enabled; a nonminimal_bool only on x86). The many
unreachable_patterns allows are feature-gated and left as #[allow].

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-13 19:11:27 +01:00
Tushar Khatri
d6e59a0be7 arch: reevaluate #[allow] attributes
Remove stale #[allow]s whose lints no longer fire (a module-level
non_camel_case_types in mpspec, and a too_many_arguments on a riscv64
configure_system that no longer exceeds the argument threshold), and
convert the still-needed ones to #[expect].

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-13 19:11:27 +01:00
Tushar Khatri
f5f74aaa29 devices: reevaluate #[allow] attributes
Convert the still-needed #[allow]s to #[expect] so they warn if the
lints stop firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-13 19:11:27 +01:00
Rob Bradford
3b9229e434 build: Consolidate sev_snp feature usage
Since igvm is a required feature of sev_snp and also sev_snp is x86-64
only the cfg attributes at build time can be consolidated & simplified.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-12 22:29:26 +00:00
Rob Bradford
bf3bc325e2 vmm: Validate the PCI segment ID without platform configuration
Validate that the PCI segment specified is a valid PCI segment ID (less
than the number of segments specified) defaulting to default if no
segments are specified because no there is no platform configuration.

See: #8376

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-12 13:08:25 +00:00
Rob Bradford
e94bb6bce6 main: Fix test_vmm_vm_cold_add_user_device
This test has a copy and paste error where the PCI segment ID was being
set with no extra segments configured.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-12 13:08:25 +00:00
Rob Bradford
24f8ccf5a7 main: Fix test_valid_vm_config_serial_console
This test has a copy and paste error where the PCI segment ID was being
set with no extra segments configured.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-12 13:08:25 +00:00
Sebastian Eydam
3a01ad081a docs: document live migration TLS encryption
On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
cf28552085 vmm: encrypt migration data with TLS if configured
Wire in the code paths that activate the TLS encrypting if the necessary
API arguments are provided.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
320403a11e vmm: validate TLS related files
Validate that all files that are necessary for TLS encryption are in the
given folder. The knowledge which files are necessary is part of the TLS
module.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
58baee16ac vmm: add TLS API option to receive migration call
As we now have more than one parameter for the receive migration call,
this commit also adds parsing and validation for those parameters. We
maintain backwards compatibility by also correctly parsing the case
where the caller only provides a URL.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
c23edda98b vmm: add TLS API option to send migration call
To enable TLS, the caller has to provide a path to a directory that
contains the necessary files.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
001bdde75f vmm: tighten migration URL validation
For TLS we have to parse the hostname from the given migration URL. For
that we have to make a few assumptions about the URL (e.g. it always has
a port). To catch problems early, we tighten the URL validation.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
0ec2ae376b vmm: accept migration connections over TLS
Extend ReceiveListener with a TLS-backed listener variant for migration
receivers.

Store the TCP listener together with the server TLS configuration, wrap
accepted sockets in TlsStream::new_server(), and preserver the existing
listener cloning and fd polling behavior so receive-side migration code
can treat TLS listeners like the existing TCP and UNIX cases.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
f3623e6403 vmm: add TLS streams to migration transport
Teach the migration transport to handle TLS-backed streams alongside
plain TCP and UNIX sockets.

Introduce a Tls variant in SocketStream and implement the necessary
traits.

Also updates the local-migration error path to reject any non-UNIX
transport, which now includes TLS-wrapped TCP connections.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
eaceef5aa0 vm-migration: server-side of a TLS connection
Code for the TLS server, i.e. the receiver of a live migration.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
8e9d779688 vm-migration: client-side of a TLS connection
TLS connections have a TLS server (listens for incoming connections) and
a TLS client (initiates the connection). This commit adds the code for
the client side, which is the sender of a migration

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
6e501cc2dc vmm: remove redundant SocketStream overrides
ReadVolatile already provides a default read_volatile_exact()
implementation, and WriteVolatile a default write_volatile_exact()
implementation. Overriding these functions adds no behavioral value, but
duplicates logic and needs to be updated whenever SocketStream gains or
changes a variant.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Sebastian Eydam
d181d2c2ac vmm: remove AsRawFd trait for SocketStream
The trait is not used and thus can be removed.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
2026-06-12 10:11:56 +00:00
Rob Bradford
01593ad181 virtio-devices: vsock: Handle host half-close
When the host half-closed the socket this was wrongly interpreted as a
full shutdown preventing the guest from sending any more data. Instead
propagate the half-close by setting just `VSOCK_FLAGS_SHUTDOWN_SEND`,
leaving the connection alive so that guest-to-host writes are still
forwarded. The connection is only torn down once the guest also shuts
down its send side or a host write fails.

Fixes: #8300

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-12 08:20:00 +00:00
Rob Bradford
5eb539b6ef virtio-devices: vsock: Handle guest half-close
When the guest did a half-close (shutting down only its send side) the
connection state was updated but the write half of the host Unix socket
was never closed so the host peer never saw an EOF. This caused issues
with newer systemd (v256+) as it now half closes its socket and waits
for the host side to react and fully close the connection.

Propagate the guest's half-close to the host by shutting down the write
half of the backing stream. This is deferred until any buffered guest
data has been flushed so that no data is lost, and the connection is
left open so that host-to-guest data keeps flowing.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-12 08:20:00 +00:00
wuxinyue
75b0fe5a21 vmm: defer PCI device visibility to fix hotplug race condition
Split `add_pci_device()` into two phases: `allocate_pci_bars()` which
only allocates BAR address space, and `commit_pci_device()` which
makes the device visible to the guest on the PCI bus.

All callers now follow the pattern: allocate BARs → perform device-
specific setup (ioeventfd, device_tree, mmio mapping) → commit device.
This eliminates a race window where the guest could discover a
partially-initialized device via `acpiphp_check_bridge()` during rapid
sequential hotplug, causing BAR reprogramming to fail because
ioeventfds and device_tree entries were not yet in place.

Signed-off-by: wuxinyue <wuxinyue.wxy@antgroup.com>
Assisted-by: Claude:Opus-4.6
2026-06-12 08:19:42 +00:00
wuxinyue
f787bd7a17 vmm: move BAR mapping registration from PciBus to DeviceManager
`PciBus::register_mapping()` operates on `mmio_bus` and `io_bus`
which are passed in as external parameters and have nothing to do
with PciBus internal state. Move this logic into
`DeviceManager::register_bar_mapping()` where it belongs, and move
the `PioInsert`/`MmioInsert` error variants from `PciRootError` to
`DeviceManagerError` accordingly.

Signed-off-by: wuxinyue <wuxinyue.wxy@antgroup.com>
Assisted-by: Claude:Opus-4.6
2026-06-12 08:19:42 +00:00
dependabot[bot]
bf4b856dd2 build(deps): bump the non-rust-vmm group across 2 directories with 15 updates
Bumps the non-rust-vmm group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde_with](https://github.com/jonasbb/serde_with) | `3.20.0` | `3.21.0` |
| [bitflags](https://github.com/bitflags/bitflags) | `2.11.1` | `2.12.1` |
| [log](https://github.com/rust-lang/log) | `0.4.30` | `0.4.32` |
| [zbus](https://github.com/z-galaxy/zbus) | `5.15.0` | `5.16.0` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.99` | `0.3.100` |

Bumps the non-rust-vmm group with 5 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [serde_with](https://github.com/jonasbb/serde_with) | `3.20.0` | `3.21.0` |
| [bitflags](https://github.com/bitflags/bitflags) | `2.11.1` | `2.12.1` |
| [log](https://github.com/rust-lang/log) | `0.4.30` | `0.4.32` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.99` | `0.3.100` |
| [libfuzzer-sys](https://github.com/rust-fuzz/libfuzzer) | `0.4.12` | `0.4.13` |



Updates `serde_with` from 3.20.0 to 3.21.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.20.0...v3.21.0)

Updates `bitflags` from 2.11.1 to 2.12.1
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.11.1...2.12.1)

Updates `log` from 0.4.30 to 0.4.32
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.30...0.4.32)

Updates `zbus` from 5.15.0 to 5.16.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus-5.15.0...zbus-5.16.0)

Updates `js-sys` from 0.3.99 to 0.3.100
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `serde_with_macros` from 3.20.0 to 3.21.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.20.0...v3.21.0)

Updates `wasm-bindgen` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `wasm-bindgen-macro` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `wasm-bindgen-macro-support` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `wasm-bindgen-shared` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `zbus_macros` from 5.15.0 to 5.16.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus_macros-5.15.0...zbus_macros-5.16.0)

Updates `zvariant` from 5.11.0 to 5.12.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant-5.11.0...zvariant-5.12.0)

Updates `zvariant_derive` from 5.11.0 to 5.12.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant_derive-5.11.0...zvariant_derive-5.12.0)

Updates `zvariant_utils` from 3.3.1 to 3.4.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant_utils-3.3.1...zvariant_utils-3.4.0)

Updates `serde_with` from 3.20.0 to 3.21.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.20.0...v3.21.0)

Updates `bitflags` from 2.11.1 to 2.12.1
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.11.1...2.12.1)

Updates `log` from 0.4.30 to 0.4.32
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.30...0.4.32)

Updates `js-sys` from 0.3.99 to 0.3.100
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `serde_with_macros` from 3.20.0 to 3.21.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.20.0...v3.21.0)

Updates `wasm-bindgen` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `wasm-bindgen-macro` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `wasm-bindgen-macro-support` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `wasm-bindgen-shared` from 0.2.122 to 0.2.123
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.122...0.2.123)

Updates `libfuzzer-sys` from 0.4.12 to 0.4.13
- [Changelog](https://github.com/rust-fuzz/libfuzzer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-fuzz/libfuzzer/compare/0.4.12...0.4.13)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: bitflags
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zbus
  dependency-version: 5.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.100
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_with_macros
  dependency-version: 3.21.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zbus_macros
  dependency-version: 5.16.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zvariant
  dependency-version: 5.12.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zvariant_derive
  dependency-version: 5.12.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zvariant_utils
  dependency-version: 3.4.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: serde_with
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: bitflags
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.100
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_with_macros
  dependency-version: 3.21.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.123
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: libfuzzer-sys
  dependency-version: 0.4.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-12 00:52:12 +00:00
Tushar Khatri
b059475dfb vmm: reevaluate #[allow] attributes
Remove stale #[allow]s whose lints no longer fire, convert the
unconditionally-firing ones to #[expect], and keep the
arch/feature-conditional ones as #[allow]. Verified across kvm/mshv,
x86_64/aarch64, and --all-features.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-11 19:06:49 +00:00
Tushar Khatri
4f68b687aa virtio-devices: reevaluate #[allow] attributes
Convert the still-needed #[allow]s to #[expect] so they warn if the
lints stop firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-11 19:06:49 +00:00
Tushar Khatri
712d42e6ac vm-device: reevaluate #[allow] attributes
Drop stale #[allow]s whose lints no longer fire and convert the
still-needed ones to #[expect] so they warn if the lints stop firing.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-11 19:06:49 +00:00
Tushar Khatri
407dd12dc5 vm-virtio: reevaluate #[allow] attributes
Remove a stale #[allow(non_camel_case_types)] whose lint no longer
fires; the enum variants are already CamelCase.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-11 19:06:49 +00:00
Dylan Reid
0487035512 vmm: release vIOMMU VFIO container on device eject
Currently ejecting a device leaks its mapping keeping the container fd
open. Remove the mapping so the fd can be closed.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-11 15:15:38 +00:00
Leander Kohler
686b8b40ec docs: document migration protocol versioning
Document how live migration protocol versions are handled, including
the supported current/previous version window and the need to migrate
through an intermediate Cloud Hypervisor version for larger version
gaps.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-06-11 13:24:16 +00:00
Leander Kohler
59e92f1972 main: print supported protocol versions
Print the supported vm-migration protocol version range in
cloud-hypervisor --version as an extra line:

  vm-migration protocol versions v0-v1

This makes the currently supported compatibility window
visible without having to inspect the migration code.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-06-11 13:24:16 +00:00
Leander Kohler
a098920e19 build, main: add vm-migration dependency
The cloud-hypervisor binary prints vm-migration protocol constants
directly, so it needs its own dependency on the vm-migration crate
instead of relying on vmm's transitive dependency.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-06-11 13:24:16 +00:00
Leander Kohler
51bd9d24f3 vmm, vm-migration: validate protocol version at start
Validate the sender's migration protocol version when
handling the initial Start request.

Read the version from the Start command header, accept only
the supported version window n-1..=n, and reject unsupported
versions with Error. A rejected Start moves the receiver to
the aborted state.

This keeps compatibility one-way, from older protocol
versions to newer ones, and leaves later version-based
branching on the receiver side.

Log the protocol version on both sender and receiver to make
the active migration path visible.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-06-11 13:24:16 +00:00
Leander Kohler
b27faaaa45 vm-migration: add protocol versioning
Add protocol-side support for migration protocol versioning.

Use the existing 6-byte Start command header, which was
previously zero padding, to carry the sender's migration
protocol version without changing the wire layout.

Store the version as a little-endian u16 in the first two
bytes and ignore the remaining four bytes. A zeroed command
header continues to mean a legacy v0 sender.

This keeps the message flow unchanged for rollout:
Start is still followed by plain OK or Error (Aborted), and no new
command is needed.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-06-11 13:24:16 +00:00
Henry Hrvoje Tonkovac
181d29ee90 devices: trim qualified paths
Import the modules used in the crate instead of spelling the
fully-qualified paths at every use site, and collapse Result<T,
io::Error> into io::Result<T>. This covers the feature-gated modules
(fw_cfg, ivshmem, pvmemcontrol) as well, leaving the whole crate free
of clippy::absolute_paths warnings.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-11 11:01:15 +00:00
Tushar Khatri
9eff92fb4b pci: reevaluate #[allow] attributes
Drop stale #[allow]s whose lints no longer fire and convert the
rest to #[expect], which warns if they ever stop being needed.

Part of #8326.

Signed-off-by: Tushar Khatri <hello@tusharkhatri.in>
2026-06-11 09:19:35 +00:00
Rob Bradford
7042922e83 vmm: Add a final memory pass after capturing snapshot
During migration send one final set of changed memory after capturing
the snapshot/state. This captures any memory changed as a side effect of
capturing that state. In particular with vhost-user capturing the device
state can lead to inflight requests being drained/flushed which could
change memory. As this is related to the snapshot account for this
memory transfer in the snapshot metrics.

No equivalent change is needed for snapshot as the memory is written
after the state is snapshotted.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-11 08:00:50 +00:00
Rob Bradford
c194f63cf4 virtio-devices: vhost_user: Save the dirty log before shutdown
Query and save the dirty log before shutting down the vhost-user device.
This allows any final dirty memory ranges to be recorded before it
becomes impossible to do that as the vhost-user handle has been closed.
This is required to ensure that all memory writes have been correctly
recorded that may be triggered by inflight I/O drains from vhost-user
device state capture.

One small implementation wrinkle: with local migrations there is no
dirty logging (since we just pass the memory FD over the socket) so
calling dirty_log() would generate an error. As there is no clean way to
query if dirty logging has been started add a boolean to track if its
active.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-11 08:00:50 +00:00
Rob Bradford
47e61f3b2a virtio-devices: Move guest_memory to VhostUserCommon
Rather than each device implementation holding a reference to the guest
memory move this to VhostUserCommon. This refactoring simplifies the
function signatures but also allows for methods that act on
VhostUserCommon that don't have the memory available to them.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-11 08:00:50 +00:00
Ruben Hakobyan
e9b47ebacd vmm: Make sev_snp depend on igvm
Currently both kvm and mshv require an IGVM file to boot a SEV-SNP VM.
This is already configured in the top-level cloud-hypervisor
Cargo.toml where sev_snp depends on igvm.

Add a similar dependency in the vmm crate which helps simplify some of
the in-code cfg blocks by removing the ones that are within a sev_snp
cfg block.

Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-10 21:29:40 +00:00
Henry Hrvoje Tonkovac
57b02c765f pci: trim qualified paths
Import the modules used in the crate instead of spelling the
fully-qualified paths at every use site. This covers std paths along
with a few crate-internal and external-crate paths, leaving pci free of
clippy::absolute_paths warnings.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-10 20:47:59 +00:00
Wei Liu
027b1a4c46 tests: enable MSHV fw_cfg coverage
Re-enable the fw_cfg integration tests for MSHV now that port string I/O
is handled by the hypervisor backend.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:GPT-5.5
2026-06-10 20:34:45 +00:00
Wei Liu
b3149e87fd hypervisor: mshv: emulate string port I/O
Handle MSHV INS/OUTS port intercepts by translating the guest string
operand through MshvEmulatorContext and copying data between guest
memory and existing PIO callbacks.

Support REP counts, zero-count REP, and direction-flag based RSI/RDI
updates. Commit RIP plus RCX/RSI/RDI after the transfer completes.

This removes the fw_cfg/debug-port skip. OVMF can now use the real
string I/O path instead of relying on ignored ports.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:GPT-5.5
2026-06-10 20:34:45 +00:00
Wei Liu
cfc7775f80 hypervisor: mshv: add port I/O helpers
Decode MSHV port access size, direction, string, and REP state through
small helpers instead of open-coded bitfield reads.

Keep scalar I/O behavior unchanged and continue rejecting string I/O in
this step. Put the x86_64 port I/O helpers in their own module so the
string emulation path can grow without bloating the top-level MSHV code.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:GPT-5.5
2026-06-10 20:34:45 +00:00
Wei Liu
43138da0d7 hypervisor: x86: share string op helpers
Move common string instruction bookkeeping into the x86 instruction
emulator so MOVS, STOS, and MSHV port string I/O use one implementation
for REP counts, direction-flag handling, and index advancement.

This keeps existing MOVS/STOS behavior unchanged while removing the need
for MSHV to open-code the same string-operation details.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:GPT-5.5
2026-06-10 20:34:45 +00:00
Wei Liu
0cd68e8f3b hypervisor: mshv: refactor port I/O exits
Move scalar HVMSG_X64_IO_PORT_INTERCEPT handling into a helper so the
string I/O implementation can build on the same dispatch path.

Keep the existing fw_cfg/debug-port skip plus string/REP assertions in
place. This is only code movement so later changes are easier to review.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Copilot:GPT-5.5
2026-06-10 20:34:45 +00:00
Ian Klemm
e8e532faf3 tests: exercise memory reserve on the hugepage UFFD restore zone
Turn reserve=on for the hugepage-backed memory zone in the UFFD
snapshot/restore integration test. Hugepages are the most likely place
to want reserve (an over-committed huge page pool is exactly the case
that otherwise SIGBUSes the guest), so this is the natural test to give
the option real coverage, as suggested in review.

It exercises the reserve mmap path twice: once on the source VM boot and
once on the demand-paged restore. The existing skip guard already
requires the 256 free 2MiB pages this zone needs, and the source VM is
killed before the restore VM is started, so reserving from the pool
never has to back two VMs at once.

Assisted-by: Claude Code (Opus 4.8)
Signed-off-by: Ian Klemm <hi@ianklemm.de>
2026-06-10 12:30:25 +00:00
Ian Klemm
8d05407799 vmm: add memory reserve option to opt out of MAP_NORESERVE
Cloud Hypervisor maps guest RAM with MAP_NORESERVE, so the kernel never
reserves the backing pages at mmap time. On a host whose hugepage pool
cannot satisfy every guest, a VM is created successfully and then takes
a SIGBUS when the guest faults a page the pool can no longer back. This
is the failure mode reported in #5730 and #7387. As noted on #5730,
checking free pool headroom up front is not a reliable fix: another
process can consume pages between the check and the fault.

Add a reserve=on parameter to --memory and --memory-zone (default off,
preserving the current MAP_NORESERVE behaviour). When set, the region
is mapped without MAP_NORESERVE, so the kernel reserves the backing
pages (swap, or huge pages for hugepage-backed memory) at mmap time,
atomically with the mapping. An over-committed configuration then
fails cleanly at VM creation with an mmap ENOMEM instead of crashing
the guest later. Unlike prefault it does not fault the memory in, so
it does not slow down boot.

This mirrors QEMU's memory-backend reserve property, which has the same
name and meaning (reserve=off maps with MAP_NORESERVE). reserve is
threaded through the same mmap paths as the existing prefault option,
and is exposed in the OpenAPI schema, CLI help and docs. The top-level
--memory reserve=on path is unchanged: the default zone is synthesised
from MemoryConfig and inherits its reserve value.

Assisted-by: Claude Code (Opus 4.8)
Signed-off-by: Ian Klemm <hi@ianklemm.de>
2026-06-10 12:30:25 +00:00
Dylan Reid
38bee23d89 vmm: memory_manager: free memslot in remove_userspace_mapping
remove_userspace_mapping tears down the KVM mapping but never returns
the slot id to the allocator's free list. Call `free_memory_slot` to
avoid the leak.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-10 10:15:46 +00:00
Dylan Reid
a048fa982d devices: ivshmem: handle short BAR0 register reads
BAR0 can technially be read with 1 or 2 byte MMIO. Don't panic in that
case.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-10 08:49:23 +00:00
Henry Hrvoje Tonkovac
4b2a77b86e vm-migration: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-09 15:51:32 +00:00
Ruben Hakobyan
2fc37a3235 tests: enable KVM SEV-SNP confidential VM integration tests
Bring the confidential VM (CVM) integration tests up on the KVM SEV-SNP
backend in addition to MSHV. On KVM the IGVM is an Oak stage0 firmware
image and the guest kernel is supplied separately: stage0 reads the
kernel, cmdline and E820 over fw_cfg. The test harness selects this
model when a guest kernel is present at /igvm_files/bzImage, mirroring
how the stage0 IGVM is discovered; MSHV keeps using the monolithic IGVM
with the kernel baked in.

  - test_infra: stage0 + direct-kernel + fw_cfg boot wiring (both the
    command line and the HTTP/D-Bus API path) plus an on_kvm_sev_snp()
    helper for tests to branch on.
  - tests: the CVM tests that don't work on the KVM SEV-SNP path yet are
    gated with #[cfg(not(feature = "kvm"))] inside the common_cvm module.
    The MSHV build enables mshv,igvm,sev_snp (no kvm feature) while the
    KVM build enables kvm,igvm,sev_snp,fw_cfg, so the cfg compiles these
    tests into the MSHV binary only and drops them on KVM; both
    hypervisors run the single common_cvm nextest profile. They all still
    run on MSHV:
      * test_pci_multiple_segments - stage0 places all 64-bit BARs in a
        single global window, so a BAR allocated in a different
        per-segment window is relocated cross-window and wedges boot.
      * test_dmi_uuid / test_dmi_oem_strings /
        test_dmi_system_and_chassis - SMBIOS is not delivered to SEV-SNP
        guests on the KVM stage0 boot path, so the guest's DMI tables
        read empty. VMM follow-up.
      * test_vdpa_block - needs host vdpa_sim_blk setup, and vDPA DMA
        into SEV-SNP-encrypted memory is unsupported (the guest hangs).

Assisted-by: Claude:Opus-4.8
Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-09 15:49:42 +00:00
Ruben Hakobyan
b1d33ec9aa vmm: vm_config: apply serde defaults to FwCfgConfig
FwCfgConfig already has a Default impl (e820/kernel/cmdline/initramfs/
acpi_tables = true, items = None), but deserialization did not use it:
without serde default every field was mandatory, so any caller building
a payload config over the API had to spell out the whole object even to
flip a single flag.

Add a container-level #[serde(default)] so missing fields fall back to
FwCfgConfig::default(). The container form is required here because the
defaults are all true; a per-field #[serde(default)] would resolve bool
to false and contradict the Default impl.

This lets callers send only the fields that differ from the defaults.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-09 15:49:42 +00:00
Ruben Hakobyan
1071a3d301 test_infra: improve guest MemTotal assertion message
validate_memory() asserts that the guest's reported MemTotal exceeds the
expected size, but on failure printed nothing about either value.
Include both the actual and expected figures in the panic message so a
failing run is self-explanatory.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-09 15:49:42 +00:00
Ruben Hakobyan
ec157d7eb9 seccomp: allow openat + read on the HTTP API thread under KVM SEV-SNP
The KVM SEV-SNP net-hotplug integration tests (supported added in
later commits) intermittently kill the VMM with SIGSYS on the
http-server thread. The thread is seen reading
/proc/sys/vm/overcommit_memory (openat + read).

Allow the syscalls there, gated on sev_snp+kvm. seccomp can't match
a path, so the open is restricted to O_RDONLY.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-09 15:49:42 +00:00
Ruben Hakobyan
dc6a56a0db vmm: vm_config: allow opening /dev/sev after landlock
On KVM, an SEV-SNP guest opens /dev/sev at VM creation and passes
its fd as kvm_sev_cmd.sev_fd. KVM uses it to issue the SNP_LAUNCH_*
commands. The open happens after the landlock ruleset is applied,
so grant rw access to /dev/sev when the sev_snp platform feature
is enabled.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-09 15:49:42 +00:00
Ruben Hakobyan
9e6c817192 virtio-devices: add VIRTIO_F_ACCESS_PLATFORM to watchdog and iommu
A confidential guest (e.g. SEV-SNP) requires every virtio device to
advertise VIRTIO_F_ACCESS_PLATFORM so the guest driver routes DMA
through the platform's bounce-buffer path; the driver refuses a device
that does not offer it.

Add VIRTIO_F_ACCESS_PLATFORM support to virtio-{watchdog,iommu} which
are exercised as part of the CVM integration tests.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-06-09 15:49:42 +00:00
Henry Hrvoje Tonkovac
2b71ffd48e api_client: trim qualified paths
Import the std modules used in the crate instead of spelling the full
paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-09 14:37:04 +00:00
Henry Hrvoje Tonkovac
b7526ec069 option_parser: trim qualified paths
Import std::fmt and std::result instead of spelling the full paths at
every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-09 12:45:21 +00:00
Muminul Islam
724ed2a4f2 README: update guest kernel version to ch-6.16.9
The guest kernel build instructions in the README referenced the
ch-6.12.8 branch, which was inconsistent with the version used by the
test scripts (scripts/test-util.sh uses ch-6.16.9). Update the README
to point at ch-6.16.9 so documentation and CI stay in sync.

Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-06-09 09:35:01 +01:00
Rob Bradford
829676e640 block: qcow: Delete QcowFile
Delete the now unused QcowFile implementation it was only used for
creating disk images for the tests and for the performance-metrics. It
was not used for the virtio-block device.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
1120fe74f5 block: qcow: Extract rebuild_refcounts() function
Pull rebuild_refcounts out of QcowFile so QcowFile can be removed in a
follow up commit. This function is still required by parse_qcow().

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
9da113ff1f block: qcow: Port internal tests to QcowDisk
Port the internal/mod.rs tests from QcowFile to QcowDisk so
the surviving tests exercise the code paths that are hit when used via
QcowDisk (and thus virtio-block).

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
cf55698168 fuzz: qcow: Switch fuzz to using QcowDisk
Fuzzing QcowFile, which is not used by virtio-block, is less helpful
than using the abstraction that is. Ensure we cover at least the same
functionality as before.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
34e8e3dbf9 performance-metrics: Switch to QcowDisk
Switch from QcowFile to QcowDisk taking advantage of QcowTempDisk where
appropriate.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
cfce14edd1 block: qcow: Port tests to QcowTempDisk
Port tests over to QcowTempDisk and also over to QcowDisk rather than
QcowFile where necessary.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
31ee5e99e4 block: qcow: Add QcowTempDisk helper
A common pattern in the test code is to create a temporary file, format
it as QCOW2 and then open it as a QcowDisk. Create a helper struct that
can be used in those tests. This is marked as #[cfg(test)] as initially
it will only be used by the test suite.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Rob Bradford
b3cf8a84cd block: qcow: Add qcow::create_image()
Add a method to format a file as a QCOW2 file which will mainly be used
by the test infrastructure. This copies the logic from QcowFile. It
doesn't refactor it as the removal of QcowFile is planned.

Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-08 13:19:32 +00:00
Max Makarov
7f6df9e870 tests: drain the replayed serial backlog and stop the pty echo loop
With socket serial output now buffered and replayed on connect, a
late-connecting client receives the whole boot backlog. The pty
interaction test had three problems with that:

- pty_read() slept a second between 512-byte reads and the loop consumed
  one chunk per two-second tick, far too slow to drain the backlog. Read
  in larger chunks without the per-read sleep and drain everything
  available each round; bound the loop so a missing marker can't run to
  the harness timeout.

- it wrote the login keystrokes before reading, so the unread backlog
  back-pressured the sender and the keystrokes never reached the prompt.
  Start reading concurrently with typing instead.

- the socat pty was created with echo on, so the replayed backlog was
  echoed back to the guest as serial input, flooding it (UART input
  overrun, login never completing). Create the pty with echo=0.

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-08 10:21:04 +00:00
Max Makarov
9889f6d403 vmm: buffer socket serial output for late-connecting clients
In Socket serial mode the device output sink was only installed once a
client connected, so output produced beforehand (kernel boot messages,
cloud-init) was dropped, and a client attaching after boot saw a blank
screen. Only PTY mode wrapped the sink in a SerialBuffer.

Install a persistent SerialBuffer as the Socket device's output sink at
SerialManager construction (discarding downstream via io::sink() until a
client connects), so output is captured into the 1 MiB ring even with no
client attached. On connect, retarget the buffer at the accepted client
and flush the backlog before live output resumes; on disconnect, keep
buffering so output produced while no client is attached is delivered to
the next one. The accepted socket is made non-blocking via
set_nonblocking() so a slow client cannot stall the vCPU thread
(SerialBuffer re-buffers on WouldBlock).

The serial-manager thread gains two syscalls under seccomp: sendto
(replaying the backlog is the first time it writes to the socket) and
ioctl restricted to FIONBIO, which is what set_nonblocking() issues.

Fixes: #7907

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-08 10:21:04 +00:00
Max Makarov
3955fdd22a serial_buffer: add set_out() to retarget the downstream writer
SerialBuffer owns its downstream writer privately, with no way to
replace it. Buffering the Socket console requires keeping one buffer
alive across client connects and disconnects and pointing it at each
newly accepted client (or a discarding sink when none is connected)
without dropping bytes buffered while no client was attached.

Add set_out(), which swaps the writer while leaving the buffered
contents intact, plus unit tests covering accumulate-while-detached,
replay on connect, live pass-through, delivery of while-detached output
to the next client, and that bytes already drained by one client are not
resent to the next.

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-08 10:21:04 +00:00
Henry Hrvoje Tonkovac
89afb088ec serial_buffer: trim qualified paths
Import std::io instead of spelling the full paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-08 09:28:19 +00:00
Henry Hrvoje Tonkovac
0b3af8aed2 net_util: trim qualified paths
Import the std modules used across the crate instead of spelling the
full paths at every use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.8
2026-06-08 09:27:29 +00:00
Nikolas Kyx
4b671954a0 hypervisor: kvm: Support pre-XSAVE x86 CPUs
KVM will emulate XSAVE for us, so we need only to skip XCRS
setting/retrieval if the respective CPU feature is not available.

Signed-off-by: Nikolas Kyx <55556836+nyx191@users.noreply.github.com>
2026-06-06 11:39:47 +00:00
Dylan Reid
dd3a2f2649 virtio-devices: block: make shutdown join the worker
eject_device calls shutdown, but Block doesn't implement it, so the
worker thread was never joined. Drop for Block does not help either,
VirtioPciDevice keeps an Arc<Mutex<Block>> past eject, so Drop never
runs. The async worker keeps completing in-flight I/O into the guest RAM
that backed it when issued.

Implement shutdown() to call wait_for_epoll_threads(), which drops the
WorkerThreads handle and ensures that worker's io completes.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-05 08:00:10 +00:00
dependabot[bot]
2c702645d1 build(deps): bump the non-rust-vmm group across 2 directories with 10 updates
Bumps the non-rust-vmm group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.1` | `1.23.2` |
| [zerocopy](https://github.com/google/zerocopy) | `0.8.48` | `0.8.50` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.62` | `1.2.63` |
| libredox | `0.1.16` | `0.1.17` |
| [libz-sys](https://github.com/rust-lang/libz-sys) | `1.1.28` | `1.1.29` |
| [memchr](https://github.com/BurntSushi/memchr) | `2.8.0` | `2.8.1` |
| [toml_edit](https://github.com/toml-rs/toml) | `0.25.11+spec-1.1.0` | `0.25.12+spec-1.1.0` |
| [typenum](https://github.com/paholg/typenum) | `1.20.0` | `1.20.1` |

Bumps the non-rust-vmm group with 6 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.1` | `1.23.2` |
| [zerocopy](https://github.com/google/zerocopy) | `0.8.48` | `0.8.50` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.62` | `1.2.63` |
| [memchr](https://github.com/BurntSushi/memchr) | `2.8.0` | `2.8.1` |
| [toml_edit](https://github.com/toml-rs/toml) | `0.25.11+spec-1.1.0` | `0.25.12+spec-1.1.0` |
| [typenum](https://github.com/paholg/typenum) | `1.20.0` | `1.20.1` |



Updates `uuid` from 1.23.1 to 1.23.2
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.1...v1.23.2)

Updates `zerocopy` from 0.8.48 to 0.8.50
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.48...v0.8.50)

Updates `cc` from 1.2.62 to 1.2.63
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.62...cc-v1.2.63)

Updates `libredox` from 0.1.16 to 0.1.17

Updates `libz-sys` from 1.1.28 to 1.1.29
- [Release notes](https://github.com/rust-lang/libz-sys/releases)
- [Commits](https://github.com/rust-lang/libz-sys/compare/1.1.28...1.1.29)

Updates `memchr` from 2.8.0 to 2.8.1
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.0...2.8.1)

Updates `shlex` from 1.3.0 to 2.0.1
- [Changelog](https://github.com/comex/rust-shlex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/comex/rust-shlex/commits)

Updates `toml_edit` from 0.25.11+spec-1.1.0 to 0.25.12+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.25.11...v0.25.12)

Updates `typenum` from 1.20.0 to 1.20.1
- [Release notes](https://github.com/paholg/typenum/releases)
- [Changelog](https://github.com/paholg/typenum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/paholg/typenum/compare/v1.20.0...v1.20.1)

Updates `zerocopy-derive` from 0.8.48 to 0.8.50
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.48...v0.8.50)

Updates `uuid` from 1.23.1 to 1.23.2
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.1...v1.23.2)

Updates `zerocopy` from 0.8.48 to 0.8.50
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.48...v0.8.50)

Updates `cc` from 1.2.62 to 1.2.63
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.62...cc-v1.2.63)

Updates `memchr` from 2.8.0 to 2.8.1
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.0...2.8.1)

Updates `shlex` from 1.3.0 to 2.0.1
- [Changelog](https://github.com/comex/rust-shlex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/comex/rust-shlex/commits)

Updates `toml_edit` from 0.25.11+spec-1.1.0 to 0.25.12+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.25.11...v0.25.12)

Updates `typenum` from 1.20.0 to 1.20.1
- [Release notes](https://github.com/paholg/typenum/releases)
- [Changelog](https://github.com/paholg/typenum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/paholg/typenum/compare/v1.20.0...v1.20.1)

Updates `zerocopy-derive` from 0.8.48 to 0.8.50
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.8.48...v0.8.50)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.50
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.63
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: libredox
  dependency-version: 0.1.17
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: libz-sys
  dependency-version: 1.1.29
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: memchr
  dependency-version: 2.8.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: shlex
  dependency-version: 2.0.1
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: non-rust-vmm
- dependency-name: toml_edit
  dependency-version: 0.25.12+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: typenum
  dependency-version: 1.20.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.50
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: uuid
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy
  dependency-version: 0.8.50
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.63
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: memchr
  dependency-version: 2.8.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: shlex
  dependency-version: 2.0.1
  dependency-type: indirect
  update-type: version-update:semver-major
  dependency-group: non-rust-vmm
- dependency-name: toml_edit
  dependency-version: 0.25.12+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: typenum
  dependency-version: 1.20.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zerocopy-derive
  dependency-version: 0.8.50
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 00:49:37 +00:00
Andrei Vagin
f9709d6f92 ch-remote: Fix error message deserialization from JSON response
server_api_error_display_modifier deserialized the JSON error response
into a `Vec<&str>`.  However, if the error message contained escaped
characters, it could not deserialize it into a borrowed string `&str`
because unescaping requires allocation. This resulted in a
deserialization error and a failure to print the error chain.

This change switches the deserialization target to `Vec<String>` to
allow allocation.

Signed-off-by: Andrei Vagin <avagin@google.com>
2026-06-04 10:16:29 +00:00
Andrei Vagin
1b0dfc0da3 ch-remote: Make snapshot and restore config arguments required
The snapshot and restore subcommands in ch-remote had optional
snapshot_config and restore_config arguments, but the implementation was
unconditionally unwrapping them.

This change marks these arguments as required to handle the missing
argument validation and report a proper error message instead of letting
the application panic.

Signed-off-by: Andrei Vagin <avagin@google.com>
2026-06-04 10:16:29 +00:00
Aastha Rawat
14717a94a3 ci: implement SKU capacity & quota validation for mshv workflow
Prevent `SkuNotAvailable` errors for mshv workflow by checking capacity
restrictions for each location. Enhance the VM provisioning logic to
validate resource availibility before deployment.

Signed-off-by: Aastha Rawat <aastharawat@microsoft.com>
2026-06-04 10:59:15 +01:00
dependabot[bot]
62c8f71287 build(deps): bump crate-ci/typos from 1.47.1 to 1.47.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.47.1 to 1.47.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](44e2070e60...37bb98842b)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.47.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 04:27:33 +00:00
Muminul Islam
d83ffd7d7d docs: document hypervisor auto-detection in testing guide
Describe --hypervisor as an optional override that defaults to
auto-detection from the host device node (/dev/mshv or /dev/kvm) for
both the build and tests commands and the shared test-script arguments.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-06-04 01:46:36 +00:00
Muminul Islam
bbaeae7cb0 github: rely on hypervisor auto-detection for mshv CI
Drop the explicit --hypervisor mshv from the mshv integration workflow.
The runner exposes /dev/mshv, so dev_cli.sh now selects MSHV through
auto-detection.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-06-04 01:46:36 +00:00
Muminul Islam
5bed1600b4 scripts: auto-detect hypervisor in dev_cli.sh
Make the --hypervisor argument optional for the build and tests
commands. Add detect_hypervisor_device() and resolve_hypervisor_device()
helpers that map an explicit kvm/mshv name to its device node, or fall
back to probing /dev/mshv and /dev/kvm on the host when the flag is
omitted. The resolved device is validated before it is mounted into the
container, and an explicit --hypervisor is still forwarded to the test
scripts.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-06-04 01:46:36 +00:00
Muminul Islam
5932b11f13 scripts: auto-detect hypervisor in test-util.sh
Make the --hypervisor argument optional in process_common_args().
When it is not supplied, detect_hypervisor() now selects the backend
from the host device node: /dev/mshv for MSHV, /dev/kvm for KVM, and
errors out when neither is present. An explicit --hypervisor still
overrides the detection.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-06-04 01:46:36 +00:00
Dylan Reid
b88d5de85e main: logger: capture local timezone before seccomp
Local-time log fields called `jiff::Zoned::now()`, which resolves the
system timezone on every record by reading
`/etc/localtime`/`/etc/timezone` if it doesn't hit the cached version.
This cache miss could then cause a seccomp violation depending on the
thread it was run from.

Avoid this by capturing the value in `Logger`. This avoids opening the
seccomp filter for the whole process.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-03 20:38:54 +00:00
Daniel Verkamp
56e891a405 hypervisor: kvm: preserve kvmclock realtime and fill if needed
If `KVM_GET_CLOCK` already filled out the `realtime` field, it sets the
`KVM_CLOCK_REALTIME` flag, but if we instead preserve this flag, the
kernel will automatically adjust the kvmclock clock when calling
`KVM_SET_CLOCK` based on the elapsed wall-clock time between pause and
resume. This just requires removing the `reset_flags()` function, which
allows the `KVM_CLOCK_REALTIME` flag to persist in the serialized clock
state.

However, the kernel does not always fill the `realtime` field, depending
on clock source; in this case, fill `realtime` during pause based on the
system time. This is not as precise as the automatic `KVM_GET_CLOCK`
version, since we query the time slightly after the vCPU was paused, but
it allows the clock to be resumed mostly in sync instead of being wildly
off. In this case, we also set the `KVM_CLOCK_REALTIME` flag in the
saved `struct kvmclock` so `KVM_SET_CLOCK` will adjust the clock on
resume.

Basic test case:

1. Run a VM with a Linux guest.
2. Pause the guest via `vm.pause` API.
3. Wait several minutes.
4. Resume the guest via `vm.resume` API.
5. Verify the guest time (e.g. via `date` command) is valid.
6. Verify guest is still using `kvm-clock` timesource:

   cat /sys/devices/system/clocksource/clocksource0/current_clocksource

Before applying the patch, the guest clock would be off by the delta
time between pause and resume; after the patch, the clock is (more or
less) in sync with the correct wall-clock time.

Old snapshots will not have the `KVM_CLOCK_REALTIME` flag populated, so
they will not be affected by the new behavior.

Signed-off-by: Daniel Verkamp <drv@meta.com>
2026-06-03 20:32:58 +00:00
Max Makarov
81f9cd068f main: only remove the API socket after a clean run
The API socket path was removed unconditionally when the process
exited, including on a failed start. On a failed start where another
running instance already held the path, that deleted the live
instance's socket.

Remove the socket only when start_vmm returned Ok, meaning this process
owned and bound it. A stale socket left by a crash is cleaned up under
the lock by the next start, so dropping the unconditional removal does
not leak sockets, and a failed start no longer clobbers a socket owned
by another instance.

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-03 19:42:35 +00:00
Max Makarov
0a08f6551a vmm: clean up a stale API socket under a lock before bind
When Cloud Hypervisor crashed or was killed, the API socket file was
left on disk, so the next start failed with EADDRINUSE ("Address already
in use") and the VMM could not restart. This affects any environment
where the socket directory survives across restarts (systemd services,
Kubernetes emptyDir volumes, and so on).

Before binding the path-based API socket, take an exclusive lock on a
sidecar "<socket>.lock" file using the block crate's OFD-lock helper.
Holding it proves no other instance is bound to this path, so a stale
socket left by a crashed run can be removed safely and race-free. If
the lock is already held, fail with a clear "API socket is already in
use" error instead of clobbering the live instance. The lock is held
for the process lifetime and released by the kernel on exit or crash.
The fd-based (socket-activation) path is left unchanged.

This implements the lock-file approach suggested by @DemiMarie.

Fixes: #7784

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-03 19:42:35 +00:00
Dylan Reid
6219613bce vmm: allow madvise in the event-monitor seccomp filter
Fix racy seccomp kill on shutdown. When a VM shuts down the
event-monitor thread's recv() loop ends and the thread exits. glibc's
thread teardown then runs __malloc_arena_thread_freeres, which trims the
per-thread malloc arena with madvise(MADV_DONTNEED).

Add madvise to the allowed calls to match other threads. The crash is
intermittent because it only fires when that thread's arena accumulated
trimmable memory by shutdown.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-03 19:05:46 +00:00
dependabot[bot]
d53e3955b3 build(deps): bump crate-ci/typos from 1.47.0 to 1.47.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.47.0 to 1.47.1.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](f8a58b6b53...44e2070e60)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.47.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 18:30:34 +00:00
Philipp Schuster
13e727efed virtio-devices: vmm: replace #[allow(unnused)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
360e155cac vmm: replace #[allow(dead_code)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
619f964747 virtio-devices: replace #[allow(dead_code)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
0a3f594f4f rate_limiter: replace #[allow(dead_code)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
04322c185c pci: replace #[allow(dead_code)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
179d56ca72 hypervisor: replace #[allow(dead_code)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
9a60472ace devices: replace #[allow(dead_code)] with expect()
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
f1f02e7144 vmm: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
c348d6598e vm-virtio: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
6745ff87ef virtio-devices: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
42fc9fd151 vhost_user_block: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
4a6add8808 performance-metrics: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
9fb09bd35b pci: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
5da4406a87 devices: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
e9fa6e6295 block: remove unneeded #[allow(dead_code)]
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
1c484e8725 block: streamline inclusion of test-only code
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
d840b6dca1 performance-metrics: remove unused dependencies
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
760b0962c6 pci: remove dead code
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
1f4b1f60a9 devices: remove unused code
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
0f12650c02 main: remove unused dependencies
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:56:31 +00:00
Philipp Schuster
212986f013 vmm, docs: make PCI BDF configurable for ivshmem
Add shared PCI config to ivshmem.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:54:56 +00:00
Philipp Schuster
5aa0587f2a vmm: make PCI BDF configurable for balloon
Add shared PCI config to virtio-balloon.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-03 13:54:56 +00:00
Max Makarov
08bd7727ff vmm: omit unset Option fields from API responses
VmConfig and its nested configuration structs, the VmInfoResponse
wrapper and DeviceNode serialize their Option<T> fields as JSON null
when unset. The OpenAPI specification types these fields as
non-nullable, so strict client generators (for example ogen for Go)
reject /vm.info responses and cannot generate a working API client.

Apply serde_with's skip_serializing_none to the affected structs so
that unset optional fields are omitted from the serialized JSON instead
of being emitted as null. API responses now validate against the
existing specification unchanged; no nullable annotations are required.

Fixes: #7775

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-03 15:47:38 +01:00
Max Makarov
d595856748 vmm: return 404 for API requests against a non-created VM
The HTTP API mapped every ApiError to 500 Internal Server Error, so an
API client could not distinguish "the VM has not been created yet" from
a genuine server-side failure without parsing the error message text.

Derive the HTTP status code from the error itself in error_response():
errors whose root cause is VmError::VmNotCreated or VmMissingConfig are
now reported as 404 Not Found, regardless of which API action surfaced
them. The existing 400 (bad request) and 429 (too many requests)
mappings are preserved.

State-conflict errors such as VmNotRunning would ideally map to 409
Conflict, but micro_http's StatusCode has no Conflict variant, so they
remain 500 for now.

Fixes: #7774

Signed-off-by: Max Makarov <maxpain@linux.com>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-06-03 12:37:51 +00:00
Anatol Belski
a89600aeba virtio-devices: Test activator success path
Feed Ok into VirtioPciDeviceActivator and assert that activate returns
Ok, device_activated becomes true, DEVICE_NEEDS_RESET is not set,
status is otherwise unchanged, no Config interrupt is delivered, and
the barrier waiter unblocks normally.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-02 14:49:32 +01:00
Anatol Belski
64c552cc96 virtio-devices: Test activator failure releases barrier
Feed BadActivate into VirtioPciDeviceActivator and assert that the
error propagates, device_activated stays false, DEVICE_NEEDS_RESET is
set in status, a single Config interrupt is delivered, and a thread
waiting on the activation barrier unblocks. The barrier release is
the deadlock fixed by the NEEDS_RESET on activation failure change.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-02 14:49:32 +01:00
Anatol Belski
7cdb724346 virtio-devices: Add activator unit test scaffolding
Add TestVirtioDevice with a controllable ActivateResult,
TestVirtioInterrupt that records delivered interrupt types, and a
make_activator helper that builds a complete VirtioPciDeviceActivator
with observable status, activated flag, interrupt log, and barrier.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-02 14:49:32 +01:00
Anatol Belski
2d2931a76e virtio-devices: vmm: Signal NEEDS_RESET on activation failure
When the guest writes DRIVER_OK and the device fails to activate, the
VMM previously bubbled the error up via VirtioActivate and never
released the activation barrier, leaving the vCPU that wrote DRIVER_OK
blocked on the barrier and effectively deadlocking the guest.

Per virtio 1.3 section 2.1.2, a device that has experienced an error
it cannot recover from should set DEVICE_NEEDS_RESET in its status and
notify the driver via a configuration change interrupt. Do that on
activation failure through the existing mark_device_needs_reset
helper, then release the activation barrier so the vCPU can resume.

DeviceManager::activate_virtio_devices now logs and continues instead
of aborting the whole pending list, so one failing device does not
take down the VMM or block pause and migration. The activator has
already reported the failure with the device id.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-02 14:49:32 +01:00
Wei Liu
d92e1ea77b tests: add Windows TPM integration test
Boot Windows with vTPM enabled and verify the TPM device enumerates
after the guest is reachable.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
4527ae449b tests: exercise TPM after reboot
Extend test_tpm to issue random, fixed-property, PCR read, and PCR
event commands before and after a guest reboot.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
8d0dc52cfa tpm: add startup response tests
Cover the accepted TPM2_Startup(CLEAR) response codes so the swtpm
reset handling keeps tolerating already-initialized TPMs.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
f4f5fcc06d devices: add TPM CRB regression tests
Cover the short CRB register accesses and data-buffer boundary
conditions used by Windows Server 2025.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
934910b94d tpm: refactor startup response check
Extract the TPM2_Startup(CLEAR) response-code check so the accepted
swtpm reset results can be covered directly.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
df41b03221 devices: refactor TPM CRB helpers
Extract CRB completion, register-read, and data-buffer range handling
so the fixed access rules can be tested without a live TPM backend.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
f91b748253 devices: lower a TPM log line to debug level
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
d262c81210 tpm: fix swtpm reset handling
Windows reboot recreates the TPM device while the swtpm process keeps
running. Leaving the transferred data fd open made a later CmdSetDatafd
fail, and the backend could remain unstarted after CmdInit.

Close both ends on setup failure, close the local transferred fd after
success, close the data fd on drop, and issue TPM2_Startup(CLEAR) after
CmdInit while tolerating an already-started TPM.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Wei Liu
7d237b5e20 devices: fix TPM CRB register byte-sized access
Windows can access CRB registers with byte-sized writes and reads. The
TPM device model used the byte offset as a u32 register index, which
corrupted unaligned accesses and could expose invalid CRB state.

Preserve the containing register on partial writes, read from the
correct byte lane, allow exact-end buffer accesses.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-02 09:18:44 +00:00
Philipp Schuster
727b704606 virtio-devices: vsock: improve error handling
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
cfc639de35 virtio-devices: vsock: remove dead code
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
3fa29920e8 vmm: preserve error chain for invalid memory path
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
f607d0143d arch: preserve error chain for memmap table
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
67a661aff0 arch: preserve error chain for smbios
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
85109ebae0 vm-migration: preserve error chain
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
b4c37def26 hypervisor: improve error chain
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Philipp Schuster
aa898db8d7 devices, vmm: improve error chain for ivshmm
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-06-02 09:17:12 +00:00
Dylan Reid
778c9f53dc virtio-devices: vsock: validate packet len on commit
RX packet assembly checks descriptor capacity before the backend sees
the packet. The backend then updates hdr.len before the header is
written back.

Validate that final length before committing the header, so we never
tell the guest that more bytes were written than fit in the RX buffer.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-01 18:54:03 +00:00
Dylan Reid
c6ecc13d08 virtio-devices: vsock: use volatile packet I/O
Remove the need for unsafely materializing slices from guest memory
pointers which is, by definition, undefined behavior.

Achieved by introducing a TxBufSource trait that is implemented for both
types of sources (Guest Memory or local copy) and by using the volatile
read/write primities for moving data from a readable or writable to
guest memory.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-01 18:54:03 +00:00
Dylan Reid
44f0360d28 virtio-devices: vsock: add volatile packet I/O
Keep packet data as a checked guest memory range and add helpers for
volatile reads and writes. Arguably VsockPacket should hold a
VolatileSlice for the guest memory usecase, but the lifetime tracking
involved wasn't worth it.

Keep the old slice accessors for now so existing callers still build.
The next commit switches them over.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-01 18:54:03 +00:00
Dylan Reid
e08f70b6d3 virtio-devices: vsock: drop unsafe test len helper
The helper only needs to update the packet len field. Use write_slice()
instead of rebuilding a mutable slice from a raw host pointer.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-06-01 18:54:03 +00:00
Wei Liu
dfcc02f547 tests: reenable TPM test for MSHV
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-01 17:46:00 +00:00
Wei Liu
d834c85697 tpm: rename established_flag to established_bit and match TCG semantics
The helper used to communicate the TPM Establishment bit between the
swtpm backend and the CRB device had inverted semantics:

    self.established_flag = est.resp.bit == 0;

so `established_flag == true` actually meant "*not* established". The
device-side call site then double-negated:

    if !self.emulator.get_established_flag() {
        val |= 0x1;  // tpmEstablished in TPM_LOC_STATE
    }

The end-to-end behaviour was correct but the boundary between the
swtpm-specific backend and the (TCG-spec defined) CRB device was hard
to follow and easy to misuse -- the now-removed pre-init check in
Emulator::new() was an example of that confusion (it errored out with
"TPM not in established state" precisely when the TPM *was*
established).

Per the TCG PC Client Platform TPM Profile (PTP) specification, bit 0
of TPM_LOC_STATE_x is `tpmEstablished`:

  * 0 = default state after a cold reset
  * 1 = a TPM2_Startup from Locality 3 or 4 has occurred

Rename the backend accessor to `get_established_bit()` and return the
bit value directly (true == 1, false == 0). The CRB device then simply
forwards the bit, with no inversion, which makes the spec mapping
obvious and removes swtpm-flavoured naming from the device layer.

No functional change.

Asissted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-01 17:46:00 +00:00
Wei Liu
746b760f69 tpm: remove spurious TPM-establishment check at startup
Emulator::new() refused to start the VMM unless the TPM Establishment
bit (TPM_LOC_STATE.tpmEstablished, bit 0) was already set, aborting
with "TPM not in established state" otherwise.

That gate is not justified by the TCG PC Client Platform TPM Profile
(PTP) specification:

  * tpmEstablished == 0 is the defined default state after a cold
    reset of the TPM.
  * The bit transitions to 1 only after a TPM2_Startup is issued from
    Locality 3 or 4 -- something the guest firmware/OS may or may not
    ever do, and which has not happened by the time the VMM is wiring
    up the device.

So the check was rejecting the spec-defined normal case. It also had
inverted internal naming (the boolean called "established_flag" was
true when the bit was 0), which is what made the conditional read as
if it were testing the opposite of what it actually tested.

In practice the check happened to pass on KVM and fail on MSHV (issue
socket, but the bug is independent of the backend: the VMM has no
business gating startup on tpmEstablished at all.

Drop the check. The bit is still surfaced to the guest from
Tpm::read() when CRB_LOC_STATE is read, which is the only place the
PTP spec requires it to be visible.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-01 17:46:00 +00:00
Wei Liu
ff5a6dcdb9 tpm: read swtpm control responses in full and handle short error replies
The swtpm control socket is a Unix SOCK_STREAM, so a single read() is
not guaranteed to return the full response in one shot. swtpm may
split a response into multiple writes, in which case the existing
single read() returns only the first chunk and subsequent parsing
fails with "Response for ... cmd is of incorrect length". This has
been observed on Azure Linux during emulator initialization.

In addition, when swtpm encounters an error processing a control
command (e.g. PTM_BAD_ORDINAL = 0x0A returned for commands issued
before CMD_INIT), the swtpm protocol returns only the 4-byte result
code instead of the full response. Blindly looping until msg_len_out
bytes arrive would deadlock in that case.

Add SocketDev::read_exact() that loops until the requested number of
bytes has been received (retrying on EINTR), and rework
run_control_cmd() to:

  * read_exact the 4-byte result code first;
  * on error, set the result code on the PTM message and return a
    clean error without waiting for a payload that will never arrive;
  * on success, read_exact the remaining (msg_len_out - 4) payload
    bytes.

Assisted-by: Copilot:GPT-5.5
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-06-01 17:46:00 +00:00
Anatol Belski
362a9ecc4f block: qcow: Test rejection of backing file offset with zero size
Cover the malformed header case where backing_file_offset is non
zero but backing_file_size is zero, which must be rejected with the
new dedicated error.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
679892f56e block: qcow: Test rejection of backing file size with zero offset
Cover the malformed header case where backing_file_offset is zero
but backing_file_size is non zero, which must be rejected with the
new dedicated error.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
05cac5657c block: qcow: Test rejection when backing file overlaps header
Cover the case where backing_file_offset points inside the fixed
header fields, which the new header overlap check must reject.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
9c85aab85d block: qcow: Test backing file fitting exactly at cluster end
Cover the boundary positive case where backing_file_offset plus
backing_file_size equals the cluster size, which the spec allows
and the new bound check must accept.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
cdd7220384 block: qcow: Test rejection when backing file end exceeds cluster
Cover the case where backing_file_offset lies inside the first
cluster but backing_file_offset + backing_file_size crosses the
cluster boundary, so the end of the name spills outside.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
a4e8d79650 block: qcow: Test rejection when backing file offset exceeds cluster
Cover an offset that lies well past the end of the first cluster
to make sure the bound check fires for arbitrary out of range
offsets rather than only the boundary case.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
2d8811ad82 block: qcow: Test rejection when backing file offset equals cluster size
A backing file string placed exactly at cluster_size starts past
the first cluster boundary, so QcowHeader::new must reject it.

Introduce a read_header_with_patched_backing helper that builds
a valid header, patches backing_file_offset and backing_file_size,
writes it out and re-parses it. Use it to cover this case.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
53a9ae08c2 block: qcow: Reject backing file offset with zero size
A qcow2 header with non-zero backing_file_offset that points at a
zero length name is malformed. The parser would otherwise read an
empty path string and store it as a backing file. Reject it with a
dedicated error so the user gets a clear diagnostic.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
39e9376f5b block: qcow: Reject backing file size with zero offset
A qcow2 header with backing_file_offset == 0 indicates the image
has no backing file, so any non-zero backing_file_size is malformed.
Qemu silently ignores the size in this case, which hides image
corruption. Reject it explicitly with a dedicated error so the user
gets a clear diagnostic.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
4294a4b862 block: qcow: Reject backing file name overlapping the header
The qcow2 spec requires the backing file name string to live in the
remaining space between the end of the header extension area and
the end of the first cluster. Header parsing accepted any backing
file offset above zero, including offsets pointing into the fixed
header fields themselves, so a corrupt or malicious image could
redirect the parser into reinterpreting header bytes as the backing
path.

Reject any backing_file_offset that is less than header.header_size
via a new BackingFileOverlapsHeader error. The check fires before
the existing first cluster bound, since an overlap is a different
class of corruption and deserves a distinct diagnostic.

Ref: #8261
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Anatol Belski
4a2a9390be block: qcow: Reject backing file name outside first cluster
The qcow2 spec requires the backing file name string to live
entirely within the first cluster, between the end of the header
extension area and the cluster boundary. The parser previously
only validated the 1023 byte cap on the name length and accepted
any backing_file_offset, so a corrupt or malicious image could
place the name string anywhere in the file.

Add the cluster bound check in QcowHeader::new and report it via
a new BackingFileOutsideFirstCluster error.

Fixes: #8261

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-06-01 16:27:50 +00:00
Rob Bradford
3a1cf6e740 virtio-devices: Remove panic when duplicating activate EventFds
Replace the (unlikely) panic when duplicating the EventFds with a
propagated error.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
Dylan Reid
2bcacbe19d virtio-devices: Introduce WorkerThreads handle
Take the recent thread refactor further. Bundle a device's worker
JoinHandles together with the kill event that stops them into a single
WorkerThreads value, owned by VirtioCommon. Its Drop signals the workers
to exit, unparks any parked for migration, and joins them.

This makes a detached/leaked worker unrepresentable. reset(),
wait_for_epoll_threads() and VhostUserCommon::shutdown() now happen when
dropping the WorkerThreads, and the unpark-before-join teardown now
lives in one place.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Dylan Reid <dgreid@fb.com>
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
Rob Bradford
815a1f0801 virtio-devices: vhost_user: Use VhostUserCommon::spawn_worker
Replace manual thread management with the use of
VhostUserCommon::spawn_worker() which is just a thin layer over
VirtioCommon::spawn_worker() but handling VhostUserCommon reset. This
removes some manual thread management and also triggers a reset upon
spawn failure.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
Rob Bradford
7538398bc5 virtio-devices: vhost_user: Add VhostUserCommon::spawn_worker helper
Wrap VirtioCommon::spawn_worker() to also include vhost-user specific
backend cleanup.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
Rob Bradford
b5f1632177 virtio-devices: Use VirtioCommon::spawn_worker()
Replace use of spawn_virtio_thread() helper with the new method on
VirtioCommon to handle thread management as well as spawning. As a
result this cleanly handles reset if it fails to spawn the thread.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
Rob Bradford
81e5e6d9dc virtio-devices: Add VirtioCommon::spawn_worker helper
Implement a method on VirtioCommon for spawning the worker thread. This
method also handles storing the handle to the thread and triggering a
reset on the device if thread spawning fails. The addition of this
helper replaces a repeated pattern across virtio and vhost-user devices
but also ensures correct cleanup when spawning multiple threads for a
device with multiple queues.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
Rob Bradford
1f58e74978 virtio-devices: Simplify epoll thread handling
Rely on the vector of threads in VirtioCommon for storing all the
handles of the spawned threads rather than storing them in the devices
or in VhostUserCommon directly.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-06-01 17:07:23 +01:00
dependabot[bot]
683afbf22a build(deps): bump crate-ci/typos from 1.46.3 to 1.47.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.46.3 to 1.47.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](7b04f660f4...f8a58b6b53)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 18:38:09 +00:00
Rob Bradford
d98918d4e9 build: Put iommufd-ioctls in the rust-vmm dependabot group
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-29 17:51:00 +00:00
Ian Klemm
a667d85055 block: qcow: reject out-of-bounds cluster offsets
Standard L2 data offsets and L1-referenced L2 table offsets must be
aligned and covered by the current refcount table. The write path
checked both constraints at one call site, while read, cache population,
and deallocation paths only checked alignment or relied on later
refcount lookup errors.

Centralize the validation in QcowState and use it before reading L2
tables, mapping standard L2 entries for reads and writes, and
deallocating existing clusters. Invalid offsets set the corrupt bit and
fail with EIO before data I/O or refcount updates.

Add QcowSync regression tests that corrupt a standard L2 entry past the
refcount-addressable range and verify that reads and writes fail with
EIO and mark the header corrupt.

Assisted-by: Codex:GPT-5
Signed-off-by: Ian Klemm <hi@ianklemm.de>
2026-05-29 15:22:57 +00:00
Ian Klemm
17b6fd91ca block: qcow: decouple pointer table writes from cursor state
write_pointer_table() used a BufWriter over a cloned fd because the
per-entry callback also needs mutable access to QcowRawFile.

That couples the final write location to the ambient kernel cursor while
the callback is allowed to perform metadata I/O. Materialize the encoded
entries first, then seek and write the table after callback execution
has finished.

This keeps the pointer-table write independent from current and future
callback behavior without depending on proving that a cursor-moving
callback is reachable in today's synchronous CH path.

Apply the same materialize-then-write shape to
write_pointer_table_direct() for consistent semantics, and cover both
paths with unit tests.

Assisted-by: Codex:GPT-5
Signed-off-by: Ian Klemm <hi@ianklemm.de>
2026-05-29 15:22:57 +00:00
Philipp Schuster
b241084d0e vmm: migration: add vm.migration-receive-ready event
The new event allows management software to handle the migration better
via events. The `vm.migration-receive-ready` event tells that the VMM is
ready to accept connections whereas `vm.migration-receive-started` means
a migration is incoming.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-29 13:38:27 +00:00
Damian Barabonkov
bbd271e85b test_infra: improve NVIDIA diagnostics
Avoid panicking when nvidia-smi fails during GPU checks. Run the
command through a shell wrapper that preserves stdout and stderr
even when nvidia-smi exits with a failure status.

Keep printing guest dmesg on failure and label the nvidia-smi
text as diagnostic output, since SSH-level failures are reported
through the same path.

Assisted-by: OpenCode:gpt-5.5

Signed-off-by: Damian Barabonkov <dbctl@pm.me>
2026-05-29 12:20:22 +00:00
Ruben Hakobyan
99f5537984 arch: x86_64: Stop applying SME c-bit reduction to phys_bits
get_host_cpu_phys_bits() subtracts the SME c-bit reduction from
PhysAddrSize (CPUID 0x80000008 EAX bits 7:0). The result sets the
guest's CPUID and MMIO address space size.

The c-bit reduction is not needed here. QEMU's equivalent
(host_cpu_phys_bits() in target/i386/host-cpu.c) returns
PhysAddrSize without reduction.

Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-05-29 12:04:47 +00:00
Ruben Hakobyan
eb75a4ead7 arch: x86_64: Set GuestPhysAddrSize in CPUID leaf 0x80000008
When generating guest CPUID, we set PhysAddrSize (EAX bits 7:0)
based on the host's physical address bits. On AMD hosts with SME,
get_host_cpu_phys_bits() subtracts the c-bit reduction from this
value, but the code here only writes the result to bits 7:0 and
leaves GuestPhysAddrSize (bits 23:16) at the unmodified host value.
This creates a gap: e.g. PhysAddrSize=43 but GuestPhysAddrSize=48.

Guest firmware that reads GuestPhysAddrSize will see a larger
address space than the VMM provides, and may place PCI BARs beyond
the MMIO bus range. However, Cloud-hypervisor sizes its MMIO bus to
phys_bits.

Fix by setting both PhysAddrSize (bits 7:0) and GuestPhysAddrSize
(bits 23:16) to phys_bits, using mask 0xff00_ff00 instead of
0xffff_ff00.

Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-05-29 12:04:47 +00:00
dependabot[bot]
b4747dfd7f build(deps): bump the non-rust-vmm group across 2 directories with 15 updates
Bumps the non-rust-vmm group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde_json](https://github.com/serde-rs/json) | `1.0.149` | `1.0.150` |
| [jiff](https://github.com/BurntSushi/jiff) | `0.2.24` | `0.2.26` |
| [log](https://github.com/rust-lang/log) | `0.4.29` | `0.4.30` |
| [landlock](https://github.com/landlock-lsm/rust-landlock) | `0.4.4` | `0.4.5` |
| [autocfg](https://github.com/cuviper/autocfg) | `1.5.0` | `1.5.1` |
| [bumpalo](https://github.com/fitzgen/bumpalo) | `3.20.2` | `3.20.3` |
| [crypto-common](https://github.com/RustCrypto/traits) | `0.2.1` | `0.2.2` |
| [either](https://github.com/rayon-rs/either) | `1.15.0` | `1.16.0` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.98` | `0.3.99` |
| [pastey](https://github.com/as1100k/pastey) | `0.2.2` | `0.2.3` |

Bumps the non-rust-vmm group with 9 updates in the /fuzz directory:

| Package | From | To |
| --- | --- | --- |
| [serde_json](https://github.com/serde-rs/json) | `1.0.149` | `1.0.150` |
| [log](https://github.com/rust-lang/log) | `0.4.29` | `0.4.30` |
| [landlock](https://github.com/landlock-lsm/rust-landlock) | `0.4.4` | `0.4.5` |
| [autocfg](https://github.com/cuviper/autocfg) | `1.5.0` | `1.5.1` |
| [bumpalo](https://github.com/fitzgen/bumpalo) | `3.20.2` | `3.20.3` |
| [crypto-common](https://github.com/RustCrypto/traits) | `0.2.1` | `0.2.2` |
| [either](https://github.com/rayon-rs/either) | `1.15.0` | `1.16.0` |
| [js-sys](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.98` | `0.3.99` |
| [pastey](https://github.com/as1100k/pastey) | `0.2.2` | `0.2.3` |



Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `jiff` from 0.2.24 to 0.2.26
- [Release notes](https://github.com/BurntSushi/jiff/releases)
- [Changelog](https://github.com/BurntSushi/jiff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/jiff/compare/jiff-static-0.2.24...jiff-static-0.2.26)

Updates `log` from 0.4.29 to 0.4.30
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

Updates `landlock` from 0.4.4 to 0.4.5
- [Release notes](https://github.com/landlock-lsm/rust-landlock/releases)
- [Changelog](https://github.com/landlock-lsm/rust-landlock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/landlock-lsm/rust-landlock/compare/v0.4.4...v0.4.5)

Updates `autocfg` from 1.5.0 to 1.5.1
- [Commits](https://github.com/cuviper/autocfg/compare/1.5.0...1.5.1)

Updates `bumpalo` from 3.20.2 to 3.20.3
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.20.2...v3.20.3)

Updates `crypto-common` from 0.2.1 to 0.2.2
- [Commits](https://github.com/RustCrypto/traits/compare/crypto-common-v0.2.1...crypto-common-v0.2.2)

Updates `either` from 1.15.0 to 1.16.0
- [Commits](https://github.com/rayon-rs/either/compare/1.15.0...1.16.0)

Updates `jiff-static` from 0.2.24 to 0.2.28
- [Release notes](https://github.com/BurntSushi/jiff/releases)
- [Changelog](https://github.com/BurntSushi/jiff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/jiff/compare/jiff-static-0.2.24...jiff-static-0.2.28)

Updates `js-sys` from 0.3.98 to 0.3.99
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `pastey` from 0.2.2 to 0.2.3
- [Release notes](https://github.com/as1100k/pastey/releases)
- [Changelog](https://github.com/AS1100K/pastey/blob/master/CHANGELOG.md)
- [Commits](https://github.com/as1100k/pastey/compare/v0.2.2...v0.2.3)

Updates `wasm-bindgen` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro-support` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-shared` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `log` from 0.4.29 to 0.4.30
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

Updates `autocfg` from 1.5.0 to 1.5.1
- [Commits](https://github.com/cuviper/autocfg/compare/1.5.0...1.5.1)

Updates `bumpalo` from 3.20.2 to 3.20.3
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.20.2...v3.20.3)

Updates `crypto-common` from 0.2.1 to 0.2.2
- [Commits](https://github.com/RustCrypto/traits/compare/crypto-common-v0.2.1...crypto-common-v0.2.2)

Updates `either` from 1.15.0 to 1.16.0
- [Commits](https://github.com/rayon-rs/either/compare/1.15.0...1.16.0)

Updates `js-sys` from 0.3.98 to 0.3.99
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `landlock` from 0.4.4 to 0.4.5
- [Release notes](https://github.com/landlock-lsm/rust-landlock/releases)
- [Changelog](https://github.com/landlock-lsm/rust-landlock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/landlock-lsm/rust-landlock/compare/v0.4.4...v0.4.5)

Updates `pastey` from 0.2.2 to 0.2.3
- [Release notes](https://github.com/as1100k/pastey/releases)
- [Changelog](https://github.com/AS1100K/pastey/blob/master/CHANGELOG.md)
- [Commits](https://github.com/as1100k/pastey/compare/v0.2.2...v0.2.3)

Updates `wasm-bindgen` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro-support` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-shared` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `log` from 0.4.29 to 0.4.30
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

Updates `landlock` from 0.4.4 to 0.4.5
- [Release notes](https://github.com/landlock-lsm/rust-landlock/releases)
- [Changelog](https://github.com/landlock-lsm/rust-landlock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/landlock-lsm/rust-landlock/compare/v0.4.4...v0.4.5)

Updates `autocfg` from 1.5.0 to 1.5.1
- [Commits](https://github.com/cuviper/autocfg/compare/1.5.0...1.5.1)

Updates `bumpalo` from 3.20.2 to 3.20.3
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.20.2...v3.20.3)

Updates `crypto-common` from 0.2.1 to 0.2.2
- [Commits](https://github.com/RustCrypto/traits/compare/crypto-common-v0.2.1...crypto-common-v0.2.2)

Updates `either` from 1.15.0 to 1.16.0
- [Commits](https://github.com/rayon-rs/either/compare/1.15.0...1.16.0)

Updates `js-sys` from 0.3.98 to 0.3.99
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `pastey` from 0.2.2 to 0.2.3
- [Release notes](https://github.com/as1100k/pastey/releases)
- [Changelog](https://github.com/AS1100K/pastey/blob/master/CHANGELOG.md)
- [Commits](https://github.com/as1100k/pastey/compare/v0.2.2...v0.2.3)

Updates `wasm-bindgen` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro-support` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-shared` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `log` from 0.4.29 to 0.4.30
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

Updates `autocfg` from 1.5.0 to 1.5.1
- [Commits](https://github.com/cuviper/autocfg/compare/1.5.0...1.5.1)

Updates `bumpalo` from 3.20.2 to 3.20.3
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.20.2...v3.20.3)

Updates `crypto-common` from 0.2.1 to 0.2.2
- [Commits](https://github.com/RustCrypto/traits/compare/crypto-common-v0.2.1...crypto-common-v0.2.2)

Updates `either` from 1.15.0 to 1.16.0
- [Commits](https://github.com/rayon-rs/either/compare/1.15.0...1.16.0)

Updates `js-sys` from 0.3.98 to 0.3.99
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

Updates `landlock` from 0.4.4 to 0.4.5
- [Release notes](https://github.com/landlock-lsm/rust-landlock/releases)
- [Changelog](https://github.com/landlock-lsm/rust-landlock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/landlock-lsm/rust-landlock/compare/v0.4.4...v0.4.5)

Updates `pastey` from 0.2.2 to 0.2.3
- [Release notes](https://github.com/as1100k/pastey/releases)
- [Changelog](https://github.com/AS1100K/pastey/blob/master/CHANGELOG.md)
- [Commits](https://github.com/as1100k/pastey/compare/v0.2.2...v0.2.3)

Updates `wasm-bindgen` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-macro-support` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

Updates `wasm-bindgen-shared` from 0.2.121 to 0.2.122
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/compare/0.2.121...0.2.122)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: jiff
  dependency-version: 0.2.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: landlock
  dependency-version: 0.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: autocfg
  dependency-version: 1.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: bumpalo
  dependency-version: 3.20.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crypto-common
  dependency-version: 0.2.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: either
  dependency-version: 1.16.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: jiff-static
  dependency-version: 0.2.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.99
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: pastey
  dependency-version: 0.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: autocfg
  dependency-version: 1.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: bumpalo
  dependency-version: 3.20.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crypto-common
  dependency-version: 0.2.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: either
  dependency-version: 1.16.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.99
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: landlock
  dependency-version: 0.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: pastey
  dependency-version: 0.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: landlock
  dependency-version: 0.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: autocfg
  dependency-version: 1.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: bumpalo
  dependency-version: 3.20.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crypto-common
  dependency-version: 0.2.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: either
  dependency-version: 1.16.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.99
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: pastey
  dependency-version: 0.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: log
  dependency-version: 0.4.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: autocfg
  dependency-version: 1.5.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: bumpalo
  dependency-version: 3.20.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: crypto-common
  dependency-version: 0.2.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: either
  dependency-version: 1.16.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: js-sys
  dependency-version: 0.3.99
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: landlock
  dependency-version: 0.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: pastey
  dependency-version: 0.2.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-macro-support
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: wasm-bindgen-shared
  dependency-version: 0.2.122
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 11:35:45 +00:00
dependabot[bot]
6ac614edf8 build(deps): bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 00:56:10 +00:00
Anatol Belski
883e3ab7fb virtio-devices: vsock: Use desc_chain.memory() for header commit
process_rx writes the packet header back into the descriptor chain
it is currently processing, so the write must go through that
chain's memory snapshot. Rederefing self.mem.memory() resolves to
the same snapshot today, but couples the write on the chain to the
device's atomic handle and obscures intent. Match the pattern used
by the rest of the device by writing through desc_chain.memory().

Suggested-by: Rob Bradford <rbradford@meta.com>
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
496ac197e6 vm-virtio: checked_descriptor: Add unit test for accessors
Drive a two descriptor chain with a writable head and a zero
length tail and verify the CheckedDescriptor accessors addr, len,
is_empty, is_write_only and has_next agree with the descriptor
flags and length on each entry.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
43ced114aa vm-virtio: checked_descriptor: Add unit test for exhausted chain
After the iterator yields the only descriptor in a chain, the
subsequent None must reflect exhaustion rather than a validation
failure, so failed and failed_addr stay unset.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
f700b10cfd vm-virtio: checked_descriptor: Add unit test for translation failure
Introduce a FailingTranslator stub whose translate_gva always
returns an error and verify CheckedDescriptorIter rejects the
descriptor, with failed_addr returning the original descriptor
address before translation.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
dd236eca90 vm-virtio: checked_descriptor: Add unit test for GVA translation
Introduce an OffsetTranslator stub implementing AccessPlatform and
verify CheckedDescriptorIter applies the translation, so the
yielded descriptor's addr reflects the translated GPA rather than
the raw descriptor address.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
e790d3b7bc vm-virtio: checked_descriptor: Add unit test for address overflow
Submit a descriptor whose addr plus len would wrap around the u64
address space and verify CheckedDescriptorIter rejects it without
panicking, with failed_addr returning the descriptor's address.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
acda9b1380 vm-virtio: checked_descriptor: Add unit test for one past memory end
Submit a descriptor whose buffer extends exactly one byte past the
end of guest RAM and verify CheckedDescriptorIter rejects it, with
failed_addr returning the descriptor's start address.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
c271a20573 vm-virtio: checked_descriptor: Add unit test for boundary descriptor
Submit a descriptor whose buffer ends exactly at the last byte of
guest RAM and verify CheckedDescriptorIter accepts it. Guards
against an off by one in the range check that would reject an
otherwise valid descriptor at the memory boundary.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
2eddaf506e vm-virtio: checked_descriptor: Add unit test for failed_addr
Drive CheckedDescriptorIter with an out of range descriptor and
assert that failed_addr returns Some carrying the GuestAddress of
the rejected descriptor, not just the failed boolean.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
356f153085 vm-virtio: checked_descriptor: Add unit test for next_checked Err
Cover the rejection path of the trait method next_checked,
asserting that an out of range descriptor is reported as
Err(addr) carrying the original descriptor address.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
3b1afe3e3d vm-virtio: checked_descriptor: Add unit test for next_checked exhaustion
Cover the exhausted path of the DescriptorChainExt::next_checked
trait method, asserting that Ok(None) is returned once the chain
has no further descriptors.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
be432bda07 vm-virtio: checked_descriptor: Add unit test for next_checked success
Cover the success path of the DescriptorChainExt::next_checked
trait method, asserting that a valid single descriptor is returned
as Ok(Some(_)) with the expected addr and len.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
d345118854 vm-virtio: checked_descriptor: Add unit test for chain stop on invalid
Add yields_valid_prefix_then_stops_on_invalid which submits a two
descriptor chain where the first descriptor is valid and the second
overshoots guest memory. The test verifies CheckedDescriptorIter
yields the valid prefix, then terminates with the failed flag set.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
3ced92702a vm-virtio: checked_descriptor: Add unit test for zero length descriptor
Add passes_through_zero_length_descriptor which submits a descriptor
with len 0 and verifies CheckedDescriptorIter yields it without
performing a guest memory range check.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
782ce21c1a vm-virtio: checked_descriptor: Add unit test for out of range descriptor
Add rejects_out_of_range_descriptor which submits a descriptor whose
length overshoots guest memory and verifies CheckedDescriptorIter
yields no descriptor and sets the failed flag.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
d24e2fd140 vm-virtio: checked_descriptor: Introduce unit_tests module
Add a unit_tests module with the first test for CheckedDescriptorIter,
covering the happy path where a valid single descriptor is yielded and
the iterator reports no failure. Wire up the virtio-bindings dev
dependency and local test helpers needed by the test.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
2bf5464f92 virtio-devices: vsock: Use checked descriptor iterator
Drop the local next_checked_desc helper and the inline
translate_gva calls in from_tx_virtq_head and from_rx_virtq_head.
Buffer ranges are now validated by the shared next_checked helper
in vm-virtio, and the validated guest address is read directly
from CheckedDescriptor::addr.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
a5962db441 virtio-devices: iommu: Validate descriptor ranges
Add check_range calls on request and status descriptor addresses to
reject buffers that extend past guest memory.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
795e606183 virtio-devices: mem: Validate descriptor ranges
Add check_range calls on request and status descriptor addresses to
reject buffers that extend past guest memory.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
661b9dee8f virtio-devices: pmem: Use checked descriptor iterator
Replace inline translate_gva and check_range with checked_iter, which
validates the descriptor buffer range against guest memory before
yielding each descriptor.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
39264eea50 virtio-devices: balloon: Use checked descriptor iterator
Replace manual translate_gva calls with checked_iter in both the
inflate/deflate and reporting queue handlers.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
33e3f4e29e virtio-devices: watchdog: Use checked descriptor iterator
Replace raw desc.addr() usage with checked_iter which validates the
descriptor buffer range against guest memory before I/O.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
4192f5101b block: Validate descriptor buffer ranges via checked iterator
Switch Request::parse over to the CheckedDescriptorIter helper from
vm-virtio so the block crate validates each descriptor's translated
(addr, len) range against guest memory through the same centralized
path used by virtio-devices. Any descriptor whose buffer is not fully
backed by guest RAM is now rejected before any I/O is set up against
it.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
a5c9634f7a virtio-devices: console: Use checked descriptor iterator
Replace manual translate_gva calls in both input and output queue
handlers with checked_iter for centralized range validation.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
45cba34101 virtio-devices: rng: Use checked descriptor iterator
Replace manual translate_gva call with checked_iter which validates
the descriptor buffer range against guest memory before I/O.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Anatol Belski
4c6d60f162 vm-virtio: Add centralized descriptor range validation
Introduce a CheckedDescriptorIter adapter over DescriptorChain that
validates each descriptor's translated (addr, len) range against guest
memory before yielding it. Any descriptor whose buffer is not fully
backed by guest RAM is rejected, so the device never performs I/O
against memory the guest does not actually own.

The helper lives in vm-virtio so it can be shared across the
virtio-devices and block crates, both of which already depend on
vm-virtio.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 21:41:45 +00:00
Rob Bradford
e033b593d9 scripts: Add SHA-1 checksums for kernel and static binary assets
Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-28 21:40:08 +00:00
Rob Bradford
531666bc67 scripts: Remove downloads from integration test scripts
Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-28 21:40:08 +00:00
Rob Bradford
4442d3b09b scripts: Make dev_cli.sh download assets before entering container
This will allow us to work towards removing network access from the
container.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-28 21:40:08 +00:00
Rob Bradford
23140ca013 scripts: Add asset files for all test downloads
This is structured such that if multiple test groups need the same asset
it is only listed once.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-28 21:40:08 +00:00
Rob Bradford
ff6de33e07 scripts: Add a tool for downloading test assets
This will allow us to remove the download steps from the build scripts
themselves.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-28 21:40:08 +00:00
Leander Kohler
3b79503e2f tests: cover new SMBIOS platform fields
The structured SMBIOS platform config introduced six new keys
(system_manufacturer, system_product_name, system_version,
system_family, system_sku_number, chassis_asset_tag), but
integration coverage only existed for serial_number, uuid, and
oem_strings.

Add _test_dmi_system_and_chassis, which boots a guest with all
six keys set and checks each value via `dmidecode -s` using the
same leaf name as the CLI key. Execute it in both the regular
and SEV-SNP integration suites.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-05-28 16:32:41 +00:00
Leander Kohler
b3f79e3a2f arch: smbios: add tests for table serialization
Add unit tests that walk the SMBIOS binary layout in guest memory and
verify structure ordering, string-set encoding, and error paths.

Tests added:
  - smbios_chassis_empty_string_set_has_double_null: verify that
    a chassis with no strings emits the double-NUL terminator required
    by SMBIOS DSP0134 §6.1.3.
  - smbios_chassis_oem_strings_layout: verify the full chain
    (BIOS → System → Chassis → OEM → End) when a chassis asset tag and
    OEM strings are configured.
  - smbios_strings_terminators_default: verify the default table chain
    (BIOS → System → End) and check that string indices and string-set
    contents match for both structures.
  - smbios_strings_too_many: exercise alloc_index up to the u8 limit
    (255 strings) and verify the 256th is rejected.
  - smbios_uuid_invalid_rejected: ensure a malformed UUID string is
    rejected with Error::ParseUuid.
  - smbios_uuid_written_le: ensure the UUID is stored in little-endian
    byte order as required by SMBIOS Spec 7.2.1.
  - smbios_write_fails_with_too_small_memory: verify that setup_smbios
    fails with Error::WriteData when guest memory is too small to hold
    anything beyond the entry point.

All tests also succeed when run with miri:
  cargo +nightly miri test -p arch smbios

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-05-28 16:32:41 +00:00
Leander Kohler
56439f3964 vmm: deprecate legacy SMBIOS keys in API and CLI
Mark serial_number/uuid as deprecated in the OpenAPI schema and emit
warnings when those legacy --platform keys are used, while continuing to
accept them for compatibility.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-05-28 16:32:41 +00:00
Leander Kohler
063caca4a8 vmm: platform: add structured SMBIOS config
Extend SMBIOS System Information with manufacturer, product,
version, family, sku, serial, and uuid fields, add a chassis
asset tag, and pass a structured SMBIOS config from --platform
into arch setup. Keep OEM strings and legacy serial_number/uuid
options working for compatibility. The platform option naming
follows `dmidecode -s <field>`.

Fields:
  - system_manufacturer
  - system_product_name
  - system_version
  - system_family
  - system_serial_number
  - system_uuid
  - chassis_asset_tag

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-05-28 16:32:41 +00:00
Leander Kohler
e097d7d495 vmm: plumb legacy SMBIOS config
Add a small SMBIOS config that carries serial_number, uuid,
and OEM strings, and pass it from platform config into
x86_64 setup.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-05-28 16:32:41 +00:00
Leander Kohler
0141635a5c arch: x86_64: refactor SMBIOS helpers
Split the System Information write into helper functions and
reuse the string writer so the table layout and inputs are
unchanged.

On-behalf-of: SAP leander.kohler@sap.com
Signed-off-by: Leander Kohler <leander.kohler@cyberus-technology.de>
2026-05-28 16:32:41 +00:00
Oliver Anderson
1a8c76b0dd arch: Fix typo in generate_common_cpuid
We fix a pre-existing typo in an info event in
the `generate_common_cpuid` function.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
dbf489c1e5 arch: Apply CPU profiles
We refactor `generate_common_cpuid` to take CPU profiles into account.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
ed3a42bc9d vmm: Enable AMX states prior to checking CPUID compatibility
Since enabling AMX tile state components affect the result returned by
`Hypervisor::get_supported_cpuid` we want this enabled prior to checking
CPUID compatibility between the source and destination VMs.

Although this is not required right now, it will be necessary once we
introduce CPU profiles and it will also be necessary if we decide to
make `check_cpuid_compatibility` (arguably) more thorough by also taking
state components into account.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
39c27e1312 arch: Refactor check_cpuid_compatibility
We make a slightly more general `check_cpuid_compatibility` function
that permits the caller to specify something else than "source VM" and
"destination VM" when logging an error.

This way we can reuse the existing CPUID compatibility checks to
ensure that the host VM is compatibile with the user selected CPU
profile.

In order to avoid a "refactor the world scenario" we keep the old
function with its signature and instead refactor it to call the new
more general internal function.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
549f3d6c04 misc: Make CPU profile part of various configs
We integrate the CPU profile into the various configs that
ultimately get set by the user.

This quickly ends up involving multiple files, luckily Rust
helps us find which ones via compilation errors.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
a9440d2919 arch: adjust_cpuid method on CpuProfile
Introduce a method on `CpuProfile` that is used to adjust the given
CPUID entries according to the chosen CPU profile.

This will be used in a later commit to apply the user chosen CPU
profile.

This commit also introduces a few unit tests using relatively simple,
somewhat contrived input values.

We will introduce snapshot tests with realistic adjustments
and CPUID entries in our planned follow up PR bringing in our
pregenerated CPU profiles.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
d96de000f2 arch: CpuProfile enum
Introduce a CpuProfile enum that will be deserialized from the user's
selected CPU profile.

Currently we only have a "host" variant, but in the future there will
be a build script automatically constructing this enum based on
pre-generated CPU profiles.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
b1109f7328 arch: CpuidProfileData struct
Introduce a struct for holding CPUID adjustments related to a
CPU profile.

Instances of this struct will typically be de-serialized from JSON files
describing a CPU profile.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
e81c51c49b arch: CpuidAdjustments type
Introduce a type for adjusting CPUID entries.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
9f38cec485 arch: Serializable CPUID parameters
We introduce a type representing CPUID parameters that will be utilized
by the CPU profiles.

We place this new type in a module that will be further populated with
types related to adjusting CPUID entries based on the CPU profile
in a follow up commit.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
7700f4b585 arch: Helper functions for u32 hex (de-) serialization
These helper functions will later be used to (de-) serialize CPUID
leaves and MSR register addresses.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
110c295edf arch: Add proptest as a dev-dependency
Not all invariants can be effectively enforced by Rust's type system
and when this is the case we typically want tests that assert that the
promised invariants do indeed hold.

To get good confidence that our invariants do indeed hold we want to
check against many inputs, but having to write down many inputs is
tedious, and we may also be "biased" in our choices.

Property based testing helps here as it provides several randomly
generated inputs for us and we can focus on just writing the
test logic.

We thus add the popular user friendly `proptest` library as a
dev-dependency in order to write property based tests in follow up
commits. We emphasize that this will be particularly important in
a follow up PR where we have some relatively complex logic for
filtering MSRs.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
64545eb7dc arch: Introduce common traits for CpuidReg
Also derive Eq, Serialize and Deserialize for CpuidReg. This will make
it possible to reuse this existing type in the context of CPU profiles.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Oliver Anderson
5cb619ff7c hypervisor: Implement common traits for CpuVendor
Serialized CPU profiles will contain information about the CPU vendor.
While there are other ways to encode this, such as going via CPUID, we
find simply serializing the pre-existing enum the simplest.

We also implement some other common traits such as `Debug` and `Eq`
which make it more convenient to work with this type.

Signed-off-by: Oliver Anderson <oliver.anderson@cyberus-technology.de>
On-behalf-of: SAP oliver.anderson@sap.com
2026-05-28 15:19:15 +00:00
Anatol Belski
c80aaa1b58 block: Remove vhdx compat alias
Re-export Vhdx from formats::vhdx and update the fuzz target to
use block::formats::vhdx::Vhdx. Remove the vhdx compat alias
from lib.rs.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
2c8978bbf2 block: Remove qcow compat alias
Update external consumers to use formats::qcow::internal instead
of the top level qcow alias. Keep a crate private use for the
QcowError variant in lib.rs.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
f8327faa33 block: Remove raw_disk compat alias
Update external consumers and internal test modules to use
formats::raw instead of the raw_disk alias, then remove the
re-export from lib.rs.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
25afb8898c block: Move QCOW2 format files into formats/qcow/
Move QCOW2 format implementation into a structured directory layout:

  qcow/              -> formats/qcow/internal/  (filenames unchanged)
  qcow_disk.rs       -> formats/qcow/mod.rs               (QcowDisk)
  qcow_sync.rs       -> formats/qcow/worker/sync.rs       (QcowSync)
  qcow_async.rs      -> formats/qcow/worker/async_uring.rs (QcowAsync)
  qcow_common.rs     -> formats/qcow/common.rs

All internal cross references continue to resolve through
re-exports in lib.rs: formats::qcow::internal as qcow,
formats::qcow as qcow_disk, and
formats::qcow::common as qcow_common.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
b595f1dbc3 block: Move VHDX format files into formats/vhdx/
Move VHDX format implementation into a structured directory layout:

  vhdx/mod.rs        -> formats/vhdx/internal/mod.rs      (Vhdx)
  vhdx/vhdx_bat.rs   -> formats/vhdx/internal/bat.rs
  vhdx/vhdx_header.rs -> formats/vhdx/internal/header.rs
  vhdx/vhdx_io.rs    -> formats/vhdx/internal/io.rs
  vhdx/vhdx_metadata.rs -> formats/vhdx/internal/metadata.rs
  vhdx_sync.rs       -> formats/vhdx/mod.rs               (VhdxDisk)

Extract VhdxSync from vhdx_sync.rs into formats/vhdx/worker/sync.rs.
Drop the vhdx_ prefix from internal file names since the parent
directory already provides the namespace. Update all internal cross
references to use the new module paths. Re-export formats::vhdx as
vhdx_sync in lib.rs for backward compatibility.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
1ca0c39b4d block: Move VHD format files into formats/vhd/
Move VHD format implementation into a structured directory layout:

  fixed_vhd.rs       -> formats/vhd/internal/fixed.rs     (FixedVhd)
  fixed_vhd_disk.rs  -> formats/vhd/mod.rs                (VhdDisk)
  vhd.rs             -> formats/vhd/internal/footer.rs     (VhdFooter)
  fixed_vhd_sync.rs  -> formats/vhd/worker/sync.rs         (FixedVhdSync)
  fixed_vhd_async.rs -> formats/vhd/worker/async_uring.rs  (FixedVhdAsync)

Add #[allow(dead_code)] to VhdFooter struct and impl because the
module is now pub(crate) and the compiler can see that several
fields and getters are only exercised by unit tests. Re-export
formats::vhd as fixed_vhd_disk in lib.rs for backward
compatibility with external consumers.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
a11f551572 block: Move raw format files into formats/raw/
Move raw format implementation into a structured directory layout:

  raw_disk.rs        -> formats/raw/mod.rs                (RawDisk)
  raw_sync.rs        -> formats/raw/worker/sync.rs        (RawSync)
  raw_async.rs       -> formats/raw/worker/async_uring.rs (RawAsync)
  raw_async_aio.rs   -> formats/raw/worker/async_aio.rs   (RawAio)
  raw_async_io_tests.rs -> formats/raw/worker/tests.rs

Update imports in fixed_vhd_sync.rs and fixed_vhd_async.rs to use
the new paths. Re-export formats::raw as raw_disk in lib.rs to
preserve the external API.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
b68349f8b3 block: Create io/ module for shared I/O infrastructure
Move async_io.rs, fcntl.rs, and request.rs into block/src/io/. These
files are generic I/O infrastructure shared by all formats rather
than format specific code.

The io/ directory name clashes with std::io in lib.rs, so the module
is declared as io_impl via #[path] and the submodules are re-exported
at the crate root to keep existing import paths working.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
2689cf9bdb block: vhd: Rename FixedVhdDisk to VhdDisk
FixedVhdDisk is the format level DiskFile wrapper for VHD images.
Rename it to VhdDisk to match the <Format>Disk convention used by
RawDisk, QcowDisk, and VhdxDisk. The Fixed prefix remains on the
workers (FixedVhdSync, FixedVhdAsync) since those are specific to
the fixed subformat.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
82e1c08e1f block: vhdx: Rename VhdxDiskSync to VhdxDisk
Align with the <Format>Disk wrapper naming convention. VHDx
has a single on disk format so no variant suffix is needed.
The async backend will be added to VhdxDisk.

No functional change.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
45fd51652f block: raw: Rename RawFileAsyncAio to RawAio
Apply the consistent <Format><Backend> naming convention.

No functional change.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
ac3c53cebb block: raw: Rename RawFileAsync to RawAsync
Apply the consistent <Format><Backend> naming convention.

No functional change.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
Anatol Belski
6279214dff block: raw: Rename RawFileSync to RawSync
Apply the consistent <Format><Backend> naming convention.

No functional change.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-28 11:58:58 +00:00
dependabot[bot]
8835656f21 build(deps): bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 01:42:32 +00:00
Henry Hrvoje Tonkovac
b415fbf5ac arch: trim qualified paths in tdx
Import std::io and std::mem instead of spelling the full paths at every
use site.

Signed-off-by: Henry Hrvoje Tonkovac <htonkovac@gmail.com>
Assisted-by: Claude:Opus-4.7
2026-05-27 23:59:56 +01:00
Rob Bradford
003e878344 build: Parallelise docker container build
Build both targets in parallel and then recombine them into a multiarch
digest later. This matches the official docker/build-push-action
multi-platform pattern and preserves the existing tagging behaviour.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 20:29:46 +00:00
Rob Bradford
2545a6f8b5 tests: Remove building of virtiofsd from the test scripts
It is now provided by the container.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 20:29:46 +00:00
Rob Bradford
1bce33923b build: Bump container version
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 20:29:46 +00:00
Rob Bradford
98356bc683 build: Dockerfile: Bump the version of spdk used
This one no longer requires pip as all the dependencies should be
included in modern Ubuntu.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 20:29:46 +00:00
Rob Bradford
a73779dad9 build: Dockerfile: Build virtiofsd
Use the same hash that we have been testing with when building from
within the integration tests.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 20:29:46 +00:00
Rob Bradford
66441e763e build: Dockerfile: Cleanup cargo registry at the end
Cleaning up the cargo registry so that it can be used in the container
should be done towards the end of the Dockerfile allowing more Rust
tools be to used.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 20:29:46 +00:00
Rob Bradford
29f392f8d4 tests: Fix clippy: uninlined_format_args
Replace format arguments with inlined versions.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
eca621c4e6 tests: Fix clippy: useless_borrows_in_formatting
Replace use of redundant & (leading to &&) in format strings.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
4d27640d08 vmm: Fix clippy: uninlined_format_args
Inline the format argument rather than provide it as a parameter.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
85415cd05b test_infra: Fix clippy: uninlined_format_args
Inline the format argument rather than provide it as a parameter.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
ead1094629 arch: Fix clippy: unused_format_specs
When formatting as hex the minimum format size is 4 not 2.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
7a386eca4a vmm: Fix clippy: useless_borrows_in_formatting
Replace & in formatting arguments where it is already a reference
(avoiding &&).

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
c93677c426 virtio-devices: Fix clippy: useless_borrows_in_formatting
Replace & in formatting arguments where it is already a reference
(avoiding &&).

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
c10f237529 test_infra: Fix clippy: useless_borrows_in_formatting
Replace & in formatting arguments where it is already a reference
(avoiding &&).

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
2a20f570b2 vmm: Fix clippy: for_kv_map
Fix use of a paired map iterator when only the values are cared about.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
c78497c314 virtio-devices: Fix clippy: for_kv_map
Fix use of a paired map iterator when only the values are cared about.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Rob Bradford
673d4a441c hypervisor: Fix clippy: for_kv_map
Fix use of a paired map iterator when only the values are cared about.

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-27 16:50:41 +00:00
Anatol Belski
d0634d18d9 virtio-devices: Test PCI CFG data_len as access bound
Verify that bar_access_params uses data_len when it is smaller than
cap.length.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-27 13:36:15 +00:00
Anatol Belski
28aa81e66b virtio-devices: Test PCI CFG access length clamping
Verify that bar_access_params clamps the access length to cap.length
when the PCI config read buffer is larger.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-27 13:36:15 +00:00
Anatol Belski
feb1c4a2d6 virtio-devices: Respect PCI CFG cap.length for BAR access
The VIRTIO_PCI_CAP_PCI_CFG indirect access mechanism was ignoring
the cap.length field written by the guest driver. PCI config register
reads always produce a 4 byte buffer, so when a driver set cap.length
to 1 for a byte wide access to device_status at common config offset
0x14, the VMM passed all 4 bytes to read_bar, dispatching to the
dword handler which does not cover that offset.

Use cap.length to determine the actual BAR access width per virtio
spec 4.1.4.9.1. Also replace the unsafe transmute with the safe
Le32::to_native() conversion.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-27 13:36:15 +00:00
Dylan Reid
cc2e528c88 block: Avoid raw iovecs in VHDX sync I/O
The VHDX synchronous async I/O backend still converted owned
AsyncIoOperation targets back into raw iovec slices before calling the
VHDX read and write helpers. That kept pointer dereferences in the owned
path and allowed a backend mistake to violate the safety boundary.

Handle owned VHDX reads and writes through AsyncIoOperation copy helpers
instead. The VHDX file operations still run synchronously, but data is
copied through operation-owned buffers or guest-memory targets without
reconstructing Rust slices from raw iovec pointers.

Assisted-by: Codex:GPT-5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
92d9a53ed3 block: Avoid raw iovecs in qcow sync I/O
This removes the qcow sync raw-iovec unsafe path and drops the
now-unused qcow iovec scatter/gather helpers.

Assisted-by: Codex:GPT-5.5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
892bc16806 block: Avoid raw iovec access in qcow async fallbacks
Similar to the other functions fixed in this series, qcow has helpers
that dereference whatever pointers are passed but are labeled safe.
Use the newly added ops helpers to, instead, provide a safe interface
and implementation.

Assisted-by: Codex:GPT-5.5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
1b2326fde4 block: Remove legacy async I/O API
Drop the borrowed iovec AsyncIo entry points now that all callers use
owned operations. Rename the transitional owned batch and completion
methods to the final trait names and remove the borrowed submission
helpers from the queue wrappers.

This removes a bunch of known safety foot-guns so future-us don't
accidentally use them.

Assisted-by: Codex:GPT-5.5
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
2da8507d21 block, virtio-devices: Use owned async I/O requests
Switch virtio-blk request construction and the users to the owned
AsyncIo data path added in the series. Read bounce buffers now return
through AsyncIoCompletion before being copied back to guest memory.

This makes the main virtio async block I/O path use retained request
memory. qcow still has raw-iovec fallback paths at this point; those
are removed in follow-up commits.

Leave the legacy borrowed iovec trait methods in place for a follow-up
cleanup commit to minimize single-commit churn.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
1dfc642e9a performance-metrics: Use owned async block I/O
Switch the in-process block microbenchmarks to allocate prefaulted
GuestMemory regions and submit through the memory-target AsyncIo API.

This adds a few setup steps as the existing benchmarks relied on the
unsound iovec API. The new behavior is intended to be as close as
possible to the existing tests and the common path for running
cloud-hypervisor.

Assisted-by: Codex:GPT-5.5
Assisted-by: Claude:Opus-4.7
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
358f7671ff block: Add owned async request helpers
Add helper routines for building and transferring data to/from async
io requests.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
afdc87e971 block: Add owned qcow io_uring I/O path
Similar to the previous commits, use UringDataIo for qcow async. Again,
the legacy interfaces are kept(at the expense of some temporary code).
The temporary code is unsound, like the existing code, but will be
removed soon.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
9143039805 block: Add owned raw Linux AIO path
Start using AioDataIo from RawFileAsyncAio. This adds a temporary
submit_borrowed_operation to enable preserving the unsafe iovec api
until we can remove it in the forthcoming commits.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
a541baa9e4 block: Add owned fixed VHD io_uring I/O path
Add the new AsyncIo apis to fixed_vhd_async. Later commits update
callers to use them and remove their unsound counterparts that take
iovecs.

While doing this, make the owned path validate offset plus length
instead of only the starting offset, so requests that extend past the
VHD size are rejected.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
849dace891 block: Add owned raw io_uring I/O path
Route RawFileAsync data I/O through UringDataIo's owned-operation
retention path while keeping borrowed submissions available for the
legacy AsyncIo calls during the transition.

This mostly moves code around, temporarily moving uring handling from
RawFileAsync to the UringDataIo, including the unsafe iovec access.

This enables the UringIo to be added to RawFileAsync incrementally.
Later commits will remove the unsafe paths when the callers are updated
to use the new functions.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
6a0c0191c8 block: Add owned qcow sync I/O path
Implement the new AsyncIo members for qcow_sync while keeping the
legacy borrowed iovec methods in place.

The code before and after this commit is equally unsound. This
intermediate state is not a safety regression and allows for a
bisectable transition to the fully sound code at the end of the
series.

This temporary state breaks out the iovec accesses to helpers used
from both the old and new code and updates the safety comments to
reflect reality.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
491f4e3343 block: Add owned VHDX sync I/O path
Refactor VhdxSync around shared iovec helpers which are marked unsafe.
Use these to implement safe wrappers for the new AsyncIo trait.

Leave the existing, unsound read/write vectored calls in place until
all callers are converted to the new interface later in this series.

In addition VHDX code assume it's safe to create slices to GuestMemory
via the AsyncAdaptor in existing code and explicitly after this
change. This is technically unsound as it can easily create multiple
mut refs. At least this is 'llvm update breaks the code' UB, not guest
exploitable UB...

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
0bb4b87bcd block: Add owned fixed VHD sync I/O path
Implement the safe AsyncIo interface the fixed VHD synchronous wrapper
and delegate the actual I/O through RawFileSync. This maintains the
existing interfaces until the callers are converted later in the series.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
68b9ddd01c block: Add owned raw sync I/O path
Implement the owned AsyncIo path for RawFileSync while keeping the
legacy borrowed iovec methods available until all callers can be
converted.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
83a294505e block: Add owned AsyncIo trait methods
Add owned data-operation, completion, and batch methods to AsyncIo.
These will be used as safe alternatives to the existing, comically
unsafe, but marked safe interface.

Over the course of the following commits, users are converted to the
new interface and after all users of the unsound interfaces are
removed, they are removed.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
ae5f4664ac block: Add AIO async struct
Similar to uring io added in the parent commit. These async ops deal
with buffer ownership across aio calls.

This will be used in the (increasingly rare) case of io_uring not being
available or desirable.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
b9ec794719 block: Add io_uring async I/O struct
Add the shared helpers and UringDataIo queue that drive uring async I/O
operations. UringDataIo is the key component responsible for keeping the
memory pointers active while async operations are ongoing. It uses the
async core added in previous commits.

Later commits will change the block backends to use this instead of the
lower level abstractions directly.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
dd1fb36f36 block: Add owned async I/O operations
Add `AsyncIoOperation` and `AsyncIoCompletion` as the owned request and
completion types that will be used to ensure buffers for async io
outlive the operations that use them. Later commits will update the
`AsyncIo` trait to expose apis using only these instead of raw iovecs.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
5bf029f3b7 block: Add owned async I/O buffers
Add `OwnedIoBuffer` to be used for host owned buffers. These are buffers
backed with either a `Vec` or an aligned allocation and will be used for
bounce buffers. This is host owned memory that can later be copied to
guest memory.

Later commits will use this to ensure backing memory outlives async
operations in a centralized, verified way.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Dylan Reid
110487a55b block: Add guest-memory async I/O targets
Introduce GuestMemoryTarget to own the combination of an Arc to
GuestMemory and a set of ranges/iovecs. This will be used in the
following commits to replace the iovec pointers that are passed to the
backend operations unsafely.

Assisted-by: Codex:GPT-5
Assisted-by: Claude:Opus-4.7
Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-27 12:07:03 +00:00
Roman
a1a84477cf vmm: openapi: Fix integer schema formats
The current OpenAPI schema format is wrong because uint16 and uint32 are
not valid top-level OpenAPI types. Describe queue_size and virtio_id as
integer types with explicit uint formats instead.

Signed-off-by: Roman <roman@vanesyan.com>
2026-05-27 09:08:23 +00:00
Cameron Baird
f73eb3ef91 ci: Add integration test for virtio-rtc
Integration test for virtio-devices/src/rtc.rs. Requires that
the test kernel has the following configs:

CONFIG_PTP_1588_CLOCK=y
CONFIG_VIRTIO_RTC=y
CONFIG_VIRTIO_RTC_PTP=y

Signed-off-by: Cameron Baird <cameronbaird@microsoft.com>
2026-05-26 20:00:24 +00:00
Cameron Baird
b452440f6c virtio-devices: Implement virtio-device rtc
This change will allow us to get accurate time over ptp in guests
started from a MSHV-virtualized Linux host. Implementing it as a
virtio device is preferable to using the existing kvm_ptp because:

kvm_ptp relies on hypercalls that only exist on host kernels running
kvm. Virtio-rtc gives us more flexibility in what clock types we want
to provide. We can later extend the device to implement multiple clocks
(smeared UTC, TAI, monotonic, etc.). Virtio-rtc protocol supports
alarms. Alarms may later enable usecases where the guests can do their
own VM lifecycle management without relying on a host-side
orchestrator.

Implement device backend for virtio-rtc. Currently this implementation
encompasses:

1. CONFIG, CAP, READ, CROSSCAP (returns false)
2. One PTP clock is presented of type
VIRTIO_RTC_CLOCK_UTC_MAYBE_SMEARED with leap_second_smearing
VIRTIO_RTC_SMEAR_UNSPECIFIED

The device is disabled by default, requiring --rtc to be passed

Not implemented but theoretically supported by virtio-rtc is:

1. Cross-timestamping support
2. The alarm queue

Fixes #7730

Signed-off-by: Cameron Baird <cameronbaird@microsoft.com>
2026-05-26 20:00:24 +00:00
wangyf0611
1e18716fbd arch, vmm: Fix riscv64 build gaps
Several riscv64 paths are compiled by the KVM build but missed
imports or cfg coverage needed by the current code.

Import the vm-memory Bytes trait for the RISC-V UEFI loader, keep
Instant available for migration timing code, and enable the UEFI flash
error path for riscv64.

Assisted-by: OpenAI-Codex:GPT-5

Signed-off-by: wangyf0611 <wangyufeng@iscas.ac.cn>
2026-05-26 15:23:47 +00:00
wangyf0611
7d1321515c vmm: Allow riscv64 path syscalls in seccomp filters
riscv64 does not provide the legacy readlink and unlink syscalls, so
libc uses readlinkat and unlinkat for paths that are otherwise allowed
on other architectures.

Permit readlinkat and unlinkat for the VMM and vCPU seccomp filters on
riscv64, matching the existing aarch64 rules.

Assisted-by: OpenAI-Codex:GPT-5

Signed-off-by: wangyf0611 <wangyufeng@iscas.ac.cn>
2026-05-26 15:23:47 +00:00
wangyf0611
649ca23345 arch, hypervisor: Report KVM IMSIC interrupt IDs
The RISC-V AIA FDT node currently advertises a fixed riscv,num-ids
value. That can diverge from the interrupt identity count configured by
KVM, which matters for guests running with an emulated IMSIC.

Record the NR_IDS value reported by KVM and expose that value through
the generated device tree. Read back the KVM-selected AIA mode without
forcing an emulation mode.

Assisted-by: OpenAI-Codex:GPT-5

Signed-off-by: wangyf0611 <wangyufeng@iscas.ac.cn>
2026-05-26 15:23:47 +00:00
wangyf0611
9c2e2a67cc hypervisor: Enable sstateen0 for KVM vCPUs
KVM initializes RISC-V vCPUs with sstateen0 cleared. When AIA is
exposed to the guest, Linux touches supervisor AIA CSRs while bringing
up the IMSIC path, and those accesses fail if the stateen bits remain
disabled.

Program sstateen0 for newly created vCPUs so the guest can use the
supervisor interrupt state needed by AIA.

Assisted-by: OpenAI-Codex:GPT-5

Signed-off-by: wangyf0611 <wangyufeng@iscas.ac.cn>
2026-05-26 15:23:47 +00:00
dependabot[bot]
5596f0aec1 build(deps): bump crate-ci/typos from 1.46.2 to 1.46.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.46.2 to 1.46.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](aca895bf05...7b04f660f4)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.46.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 06:04:02 +00:00
Bo Chen
8fd8224ee3 vmm: Use HashSet for VmConfig::preserved_fds
Each VM reboot re-entered the VFIO/virtio-net add paths and re-appended
the same originating fds, leaving duplicates in preserved_fds and a
double-close hazard at final teardown. Switching to HashSet makes
add_preserved_fds idempotent.

Signed-off-by: Bo Chen <bchen@crusoe.ai>
Assisted-by: Claude:Opus-4.7
2026-05-23 07:29:10 +00:00
Ruben Hakobyan
5c25d82f59 hypervisor, vmm: Fix aarch64 SVE register save/restore
When SVE is enabled, KVM replaces the FPSIMD V-registers with wider
SVE Z-registers. Attempting to access the old FPSIMD offsets returns
EINVAL.

Fix by classifying each register from KVM_GET_REG_LIST as core, system,
or extended. Extended registers (currently SVE only) are saved as
generic `ExtendedReg` entries split into `pre_finalize_regs` (registers
like SVE VLS that must be written before `vcpu_finalize`) and
`extended_regs`. FPSIMD registers are only accessed when SVE is absent.
Unrecognized register families error immediately so future extensions
like SME fail clearly rather than silently losing state.

The snapshot is deserialized before vCPU init to make pre-finalize
register state available for the init -> VLS -> finalize ordering
required by KVM.

Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-05-22 20:03:06 +00:00
Ruben Hakobyan
e390f0bdc1 vmm: Extract SVE finalization from vCPU init
Split the SVE detection and finalization logic out of `init()` into a
dedicated `finalize_sve()` method. This separates the concerns of vCPU
initialization (preferred target, processor features, KVM init) from SVE
finalization, and enables a subsequent commit to insert SVE VLS register
in the restore path without needing to add a restore state in `init()`.

Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-05-22 20:03:06 +00:00
Ruben Hakobyan
177d8b80e2 hypervisor: Extract FPSIMD register access into helper methods
Move the inline FPSIMD register read/write code from `get_regs()` and
`set_regs()` into dedicated `get_fpsimd_regs()` and `set_fpsimd_regs()`
methods on `KvmVcpu`.

This helps keep the larger `{get,set}_regs()` easier to understand and
prepares them for a subsequent commit that needs to conditionally
skip FPSIMD access when SVE registers are used instead.

Signed-off-by: Ruben Hakobyan <hruben@meta.com>
2026-05-22 20:03:06 +00:00
Ian Klemm
2b61bda35a block: qcow: check compressed L2 entries before zero flag
QCOW2 compressed L2 entries encode their extent layout in bits that
overlap with the flags used by standard L2 entries. In particular,
bit 0 can be part of the compressed entry layout, so it must not be
interpreted as ZERO_FLAG until the entry has first been ruled out as
compressed.

Keep compressed deallocation ahead of zero-flag handling in both the
shared QcowMetadata path and the legacy QcowFile path. This ensures
WRITE_ZEROES deallocates compressed clusters instead of treating a
compressed entry with bit 0 set as an existing logical-zero marker.

Add regression coverage that forces bit 0 on a compressed L2 entry
and verifies WRITE_ZEROES still clears the entry through the
compressed-cluster path.

Assisted-by: Codex:GPT-5
Signed-off-by: Ian Klemm <hi@ianklemm.de>
2026-05-22 14:26:04 +00:00
Ian Klemm
39e253ff9c block: qcow: Preserve WRITE_ZEROES with backing files
QCOW2 empty L2 entries in an overlay mean that reads fall
through to the backing file. Reusing the punch_hole path for
WRITE_ZEROES therefore turns a full-cluster zero operation on an
unallocated overlay cluster into backing data exposure.

Keep discard/punch_hole behavior unchanged, but let WRITE_ZEROES
request a logical-zero marker when the image has a backing file.
ZERO_FLAG entries now read as zeros in both the legacy QcowFile
path and the shared runtime metadata path. Partial writes after
such entries seed new clusters from zeros instead of backing data.

Treat ZERO_FLAG entries as logical holes for SEEK_HOLE/SEEK_DATA.
Empty overlay entries with a backing file still report data because
the data exists in the backing file.

Avoid cluster-sized userspace zero buffers when materializing
zero-flagged clusters by zeroing the allocated host range directly.
This keeps recycled clusters safe without making partial writes
allocate large zero-filled Vecs.

Add regression coverage for legacy QcowFile, QcowSync, direct I/O,
QcowAsync/io_uring overlay paths, and a large-cluster partial-write
case.

Assisted-by: Codex:GPT-5

Signed-off-by: Ian Klemm <hi@ianklemm.de>
2026-05-22 14:26:04 +00:00
dependabot[bot]
4992d4bf7d build(deps): bump the non-rust-vmm group across 2 directories with 2 updates
Bumps the non-rust-vmm group with 1 update in the / directory: [openssl-sys](https://github.com/rust-openssl/rust-openssl).
Bumps the non-rust-vmm group with 1 update in the /fuzz directory: [winnow](https://github.com/winnow-rs/winnow).


Updates `openssl-sys` from 0.9.115 to 0.9.116
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-sys-v0.9.115...openssl-sys-v0.9.116)

Updates `winnow` from 1.0.2 to 1.0.3
- [Changelog](https://github.com/winnow-rs/winnow/blob/main/CHANGELOG.md)
- [Commits](https://github.com/winnow-rs/winnow/compare/v1.0.2...v1.0.3)

---
updated-dependencies:
- dependency-name: openssl-sys
  dependency-version: 0.9.116
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: winnow
  dependency-version: 1.0.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 00:48:04 +00:00
Rob Bradford
e0c68dbfaa main: Add support for "glog style" timestamps
Add support for formatting logs following the popular glog crate style.
In particular this can use local or UTC time along with a single
character level.

As an implementation detail of that implement finegrained date time log
outputs including both local and UTC variants.

Signed-off-by: Rob Bradford <rbradford@meta.com>
Assisted-by: Claude:Opus-4.6
2026-05-21 22:38:15 +00:00
Philipp Schuster
1924153185 tests: cover restored VM disk hotplug after TCP migration
Extend the TCP live-migration test with a hotplugged block device using
a stable ID before migration. After migration, verify that the disk
still exists on the restored destination VM.

Then hot-remove the disk and add it again with the same ID. This covers
the stale restore snapshot case because the disk ID exists in the
migration snapshot, but the live device tree no longer contains it after
hot-remove.

Assisted-by: Codex:GPT-5.5
On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-21 15:49:51 +00:00
Rob Bradford
44ed81e66f vmm: Don't store the snapshot on the DeviceManager
Storing the snapshot causes issues when needing to do a subsequent
hotplug instead just pass it through on all the methods that need it
making the lifecycle cleaner.

Assisted-by: Claude:Opus-4.6
Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-20 11:10:39 +00:00
Wei Liu
fe57bb8846 docs: do not discourage agents from running integration tests
Agents are totally capable of setting up the host and run dev_cli.sh.
The removed text discouraged them to do that even when asked to.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-19 23:11:54 +00:00
Julian Schindel
0ee3349889 net_util: remove MAX_INTERFACE_NAME_LEN
`MAX_INTERFACE_NAME_LEN` is equivalent to `libc::IFNAMSIZ`, so we use
that instead of maintaining our own const.

On-behalf-of: SAP julian.schindel@sap.com
Signed-off-by: Julian Schindel <julian.schindel@cyberus-technology.de>
2026-05-19 20:24:35 +00:00
Wei Liu
3837c87f1f vmm: add seccomp filter for serial-manager thread
The serial-manager thread was the only VMM-managed thread without a
seccomp filter. Add a Thread::SerialManager variant and whitelist the
31 syscalls needed for its epoll-based I/O loop (read, write, socket
ops, signal handling, memory allocation, glibc internals).

The filter is computed in start_thread() and applied before the epoll
loop, matching the pattern used by other VMM threads.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
Assisted-by: Pi-agent:Claude-Opus-4.7
2026-05-19 16:57:53 +00:00
Wei Liu
73146be06b vmm: simplify seccomp code
Only Thread::Vmm and Thread::Vcpu need to know the hypervisor type.
Make the type optional, and then simplify the users.

Assisted-by: Pi-agent:Claude-Opus-4.7
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-19 16:57:53 +00:00
Rowen-Ye
59b72c51c2 block: Resolve relative QCOW2 backing paths
QCOW2 backing file paths stored in image headers may be
relative. These paths should be interpreted relative to the image
that references them, but the block backend opened them relative to
the process working directory.

Resolve the current image path inside parse_qcow() from the open
file descriptor and use its parent directory for relative backing
paths. Recursive backing chains work the same way because each layer
is parsed from its own file descriptor.

Signed-off-by: Rowen-Ye <rowenye1@gmail.com>
2026-05-19 15:05:56 +00:00
Sebastien Boeuf
6ef9b58615 vmm: Prefault snapshot pages in background
Userfaultfd is a great mechanism for providing fast restore to Cloud
Hypervisor VMs. But that means the price to pay for bringing pages in
happens at runtime, which might slow down the guest when it's touching
pages which haven't been brought in yet.

By prefaulting the pages in the background, we're trying to get the best
of both worlds. That means we still get a very fast restore with the
uffd handler, but within a few seconds (depending on VM's RAM size), we
also get the pages fully faulted and we can stop the uffd handler thread
at that point.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
2026-05-19 12:54:55 +00:00
dependabot[bot]
0e7a42c9e7 build(deps): bump crate-ci/typos from 1.46.1 to 1.46.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.46.1 to 1.46.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](5374cbf686...aca895bf05)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.46.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 10:56:40 +00:00
Anirudh Rayabharam
fcd69fb707 tests: consolidate test config into nextest profiles
Centralize test configuration (filters, retries, fail-fast,
sequential/parallel scheduling) into nextest profiles, replacing
scattered flags across shell scripts. This simplifies the scripts
and provides a single source of truth for test behavior.

Enable JUnit XML output per profile, giving CI systems structured
test results for better reporting.

Not all test invocations are converted to profiles yet. Just the
ones that are repeated across scripts.

Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-05-19 09:09:55 +00:00
Anirudh Rayabharam
a4f0a18fb5 ci: update Docker image to use nextest 0.9.128
Update the container image tag to trigger a rebuild. The new image
pins cargo-nextest to version 0.9.128, which supports profile
inheritance, needed for the following nextest configuration changes.

Assisted-by: Copilot-CLI:Claude-Opus-4.7
Signed-off-by: Anirudh Rayabharam <anrayabh@microsoft.com>
2026-05-19 09:09:55 +00:00
tchaton
5080d03714 vmm: openapi: Fix typo in iommu_address_width field name
The JSON parser expects the field to be named
`iommu_address_width_bits`, but the code declared it as
`iommu_address_width`. This mismatch caused the field to be
unrecognized when deserializing configuration from JSON.

Rename the field to `iommu_address_width_bits` to match the
expected schema.

Signed-off-by: tchaton <thomas.chaton.ai@gmail.com>
2026-05-19 08:30:16 +00:00
CMGS
f8f92bd628 virtio-devices: 8 MiB-aligned initial BAR placement
Windows 11 PnP rebalance rewrites peer BARs into the same range CH
packed the initial layout at, causing move_bar() failures and boot
deadlock. Pack Mmio64 BARs at 8 MiB stride. Mmio32 isn't wide enough
for the same stride, but its BARs don't participate in guest BAR
rebalancing.

On restore, pin the BAR to the snapshot address (alignment=None) so a
guest-relocated BAR with smaller alignment is accepted.

Signed-off-by: CMGS <ilskdw@gmail.com>
2026-05-19 08:19:25 +00:00
Dylan Reid
a495841515 virtio-devices: iommu: log per-request errors
IommuEpollHandler::request_queue() can fail because the guest put in a
bad request or because of a fatal error. Handle those cases differently,
letting the guest continue, but see the error if it can.

This makes debugging from the guest easier as one mistake doesn't cause
a VM reset if it's avoidable.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-19 08:16:33 +00:00
Dylan Reid
ab38a77c01 pci: vfio_user: replace unwrap() with explicit error
VfioUserDmaMapping::map panicked when find_region returned an anonymous
mmap region. Change this so a user gets an error instead of a panic.

When the VMM hotplugs a region into a guest that also has a vfio-user
device, all region's handlers are called. With the anonymous memory
backing (no file=, shared=on, or hugepages), region.file_offset()
returns None and the .unwrap() panics the VMM. Replace the unwrap with
an explicit error and use checked_add for the offset combine.

Signed-off-by: Dylan Reid <dgreid@fb.com>
2026-05-19 08:15:54 +00:00
Philipp Schuster
a917f5208a docs: be more explicit in AGENTS.md
Otherwise, codex often says "Codex:GPT-5" which is rather unspecific.
"Codex:GPT-5.4" would be better.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-18 21:09:25 +00:00
Philipp Schuster
75baa46816 docs: Mention EditorConfig in agent guide
Tell agents to respect .editorconfig alongside the formatter guidance
from CONTRIBUTING.md.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-18 21:09:25 +00:00
Philipp Schuster
466b5a8e08 docs: Move logging guidance to CONTRIBUTING
Keep logging and comment-style guidance in CONTRIBUTING.md and trim
duplicate policy from the agent guide.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-18 21:09:25 +00:00
Philipp Schuster
a07097af1f docs: Document supported targets
Move supported architecture and backend guidance from AGENTS.md into
README.md and CONTRIBUTING.md.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-18 21:09:25 +00:00
Wei Liu
8ff6114c46 build: make Windows tests blocking again
See #8211. The bug is with a change in memory allocation behaviour, not
with Windows guests.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-18 21:05:37 +00:00
Anatol Belski
2c86362674 virtio-devices: Test cap_len of sibling compound virtio PCI caps
Assert VirtioPciNotifyCap and VirtioPciCap64 size cap_len from
their own type. Catches a future regression of the same shape as
the VirtioPciCfgCap one in any of the sibling capabilities.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-18 14:30:46 +00:00
Anatol Belski
b776c6d317 virtio-devices: Test cfg_type of VIRTIO_PCI_CAP_PCI_CFG
Assert the emitted VirtioPciCfgCap carries cfg_type 5, the value
assigned to PciCapabilityType::Pci by virtio 1.2 section 4.1.4.1.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-18 14:30:46 +00:00
Anatol Belski
e7d394e286 virtio-devices: Test cap_len of VIRTIO_PCI_CAP_PCI_CFG
Regression test for the cap_len fix. The emitted VirtioPciCfgCap
must report cap_len 20, covering the trailing pci_cfg_data window
per virtio 1.2 section 4.1.4.9.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-18 14:30:46 +00:00
Anatol Belski
b4dea599a3 virtio-devices: Fix cap_len for VIRTIO_PCI_CAP_PCI_CFG
VirtioPciCfgCap::new built its inner header via VirtioPciCap::new,
which sized cap_len from the bare virtio_pci_cap layout, yielding
16. The emitted capability is VirtioPciCfgCap, which appends a four
byte pci_cfg_data window, so the correct value is 20.

The virtio 1.2 specification defines this cap as virtio_pci_cap
followed by pci_cfg_data[4] and requires cap_len to
cover the whole structure. Build the header inline so cap_len
reflects the actual emitted size, matching VirtioPciNotifyCap and
VirtioPciCap64.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-18 14:30:46 +00:00
Rob Bradford
547a78999e vmm: Revert "vmm: create memfd for private mappings"
This reverts commit ced3762a67.

This change lead to a serious memory regression when not using hugepages
or shared=on.

`MAP_PRIVATE` creates an anonymous memory allocation for every page
written when the backing store is a file. This CoW behaviour is useful
but leads to double allocations when the backing store is an empty file
created by `memfd_create()`. When the page is written to, the CoW
semantics require a real page to be created in the memory for the memfd
(previously before the page was touched they would all point to the zero
page). This real page is filled with zeroes because in theory this page
would be accessible via read/write syscalls on the FD even though in our
implementation it is only ever `mmap()`ed.

The intention of the commit was to enable `fallocate()` to be used to
punch holes but that would only affect the inaccessible backing page and
the page in the CoW anonymous memory would be unaffected. Leading it
likely not to have the desired effect.

Fixes: #8211

Signed-off-by: Rob Bradford <rbradford@meta.com>
2026-05-18 07:23:36 -07:00
Chris Webb
7b29691932 vmm: return all-ones for unregistered PIO reads
When reading from an unregistered PIO address, pio_read() wasn't
initialising the buffer, so guests were reading stale bytes from the
previous PIO transaction rather than all 0xff bytes like master abort
on real hardware.

Fill data with 0xff on invalid reads.

Correct 'read to unregistered address' info message to 'read from
unregistered address' while we're touching this block.

Signed-off-by: Chris Webb <chris@arachsys.com>
2026-05-16 15:40:55 +00:00
Chris Webb
4091e965b8 vmm: return all-ones for unregistered MMIO reads
When reading from an unregistered MMIO address, mmio_read() wasn't
initialising the buffer, so guests were reading stale bytes from the
previous MMIO transaction rather than all 0xff bytes like master abort
on real hardware.

Fill data with 0xff on invalid reads.

Correct 'read to unregistered address' info message to 'read from
unregistered address' while we're touching this block.

Signed-off-by: Chris Webb <chris@arachsys.com>
2026-05-16 15:40:55 +00:00
Muminul Islam
fba55b3d9f tests: poll for source VM exit after live-migration
The post-migration check used a fixed `thread::sleep(3s)` followed by
`try_wait()` to verify the source VM had exited cleanly. That window
is too tight when the source process is the release binary used by
`test_live_upgrade_*` (i.e. `~/workloads/cloud-hypervisor-static`,
pinned to `migratable_version`).

The released binary is older than the locally-built destination and
its virtio-device teardown (resume-paused-thread -> kill -> join
across pmem, block, net, console, rng workers) regularly takes
longer than 3s on contended hosts, causing the test to report:

  thread 'common_parallel::test_live_upgrade_basic' panicked:
  Test failed: source VM was not terminated successfully.

even though the source process eventually exits with status 0.

Replace the fixed sleep with a `wait_until(Duration::from_secs(30),
...)` poll that returns as soon as `try_wait()` reports a reaped
child, then keep the existing `success()` check on the exit status.
This makes the assertion robust against the slower release-binary
shutdown path while still failing fast on a genuine error.

The same pattern was duplicated across eight migration helpers plus
the virtio-fs migration variant; convert all nine call sites for
consistency.

Signed-off-by: Muminul Islam <muislam@microsoft.com>
2026-05-16 00:19:45 +00:00
Philipp Schuster
2168ddf16b vmm: migration: better observe and log invalid states
This increases debugability.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 17:59:02 +00:00
Philipp Schuster
5899d062bc vm-migration: be explicit about commands
Reordering commands or adding commands in-between is breaking the
migration protocol. By using explicit numbers, we can increase the
attention required when touching this code.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 17:59:02 +00:00
Philipp Schuster
ba04c4f318 virtio-devices, vmm: replace Vec<T> with Box<[T]> in config structs
I started by looking at all `Option<Vec<T>>` values in config.rs and
vm_config.rs, and replaced them with `Option<Box<[T]>>`. This has the
advantage that one now can see at a glance if this field will ever
resize during operation or not, reducing cognitive load and increasing
maintainability. All fields that need the properties of a Ver or where
this change was not trivial are kept intact.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 16:05:01 +00:00
Philipp Schuster
29e2319247 vmm: store CPU affinity lists as slices
Each affinity host CPU list is copied from configuration.
It is only iterated afterwards, so a boxed slice is enough.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 16:05:01 +00:00
Philipp Schuster
74392eccae vmm: store PCI segments as a slice
The PCI segment list is created once from the configured count.
Later code mutates entries, but does not add or remove segments.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 16:05:01 +00:00
Philipp Schuster
c2c3178038 vmm: store PCI MMIO allocators as slices
The allocator lists are sized from the fixed PCI segment count.
They are only indexed afterwards, so boxed slices fit the use.

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 16:05:01 +00:00
Philipp Schuster
8574bf4c13 docs: remove LTS information from releases.md
Cloud Hypervisor doesn't do LTS releases for quite some time now. This
outdated information already caused confusion [0].

[0] https://github.com/NixOS/nixpkgs/pull/520434#issuecomment-4460008719

On-behalf-of: SAP philipp.schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-15 15:40:01 +00:00
dependabot[bot]
078d57e6b9 build(deps): bump the non-rust-vmm group across 2 directories with 7 updates
Bumps the non-rust-vmm group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde_with](https://github.com/jonasbb/serde_with) | `3.18.0` | `3.19.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.61` | `1.2.62` |
| [hybrid-array](https://github.com/RustCrypto/hybrid-array) | `0.4.11` | `0.4.12` |
| [winnow](https://github.com/winnow-rs/winnow) | `1.0.0` | `1.0.2` |
| [zvariant](https://github.com/z-galaxy/zbus) | `5.10.1` | `5.11.0` |

Bumps the non-rust-vmm group with 3 updates in the /fuzz directory: [serde_with](https://github.com/jonasbb/serde_with), [cc](https://github.com/rust-lang/cc-rs) and [hybrid-array](https://github.com/RustCrypto/hybrid-array).


Updates `serde_with` from 3.18.0 to 3.19.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.18.0...v3.19.0)

Updates `cc` from 1.2.61 to 1.2.62
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.61...cc-v1.2.62)

Updates `hybrid-array` from 0.4.11 to 0.4.12
- [Changelog](https://github.com/RustCrypto/hybrid-array/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/hybrid-array/compare/v0.4.11...v0.4.12)

Updates `serde_with_macros` from 3.18.0 to 3.20.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.18.0...v3.20.0)

Updates `winnow` from 1.0.0 to 1.0.2
- [Changelog](https://github.com/winnow-rs/winnow/blob/main/CHANGELOG.md)
- [Commits](https://github.com/winnow-rs/winnow/compare/v1.0.0...v1.0.2)

Updates `zvariant` from 5.10.1 to 5.11.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant-5.10.1...zvariant-5.11.0)

Updates `zvariant_derive` from 5.10.1 to 5.11.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant_derive-5.10.1...zvariant_derive-5.11.0)

Updates `serde_with` from 3.18.0 to 3.19.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.18.0...v3.19.0)

Updates `cc` from 1.2.61 to 1.2.62
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.61...cc-v1.2.62)

Updates `hybrid-array` from 0.4.11 to 0.4.12
- [Changelog](https://github.com/RustCrypto/hybrid-array/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/hybrid-array/compare/v0.4.11...v0.4.12)

Updates `serde_with_macros` from 3.18.0 to 3.20.0
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.18.0...v3.20.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.62
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: hybrid-array
  dependency-version: 0.4.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_with_macros
  dependency-version: 3.20.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: winnow
  dependency-version: 1.0.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: zvariant
  dependency-version: 5.11.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: zvariant_derive
  dependency-version: 5.11.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: serde_with
  dependency-version: 3.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
- dependency-name: cc
  dependency-version: 1.2.62
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: hybrid-array
  dependency-version: 0.4.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: non-rust-vmm
- dependency-name: serde_with_macros
  dependency-version: 3.20.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: non-rust-vmm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 00:59:24 +00:00
Wei Liu
7d7f24382c tests: add block device integration tests
Assisted-by: Claude:Opus-4.7
Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-14 22:35:02 +00:00
Wei Liu
2fe775fce2 block: use BLKDISCARD/BLKZEROOUT ioctls for block devices
Some block devices (ZFS volume) may require BLKDISCARD and BLKZEROOUT
ioctls for discard and write_zeroes operations respectively.

There is no good way to probe whether fallocate is supported on a block
device. Arguably, punch_hole and write_zeroes are rare. Instead of
having a complex scheme for the IO uring backend, we force it to always
use ioctls. The code can be changed if the synchronized ioctls become a
performance issue.

Changes:
- Detect block devices at construction time
- Use BLKDISCARD ioctl for punch_hole (discard) on block devices
- Use BLKZEROOUT ioctl for write_zeroes on block devices
- Add BLKDISCARD/BLKZEROOUT to VirtioBlock seccomp whitelist
- Keep fallocate() path for regular files (no behavior change)
- Consolidate some helper functions to the new sparse module

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-14 22:35:02 +00:00
Wei Liu
12919dbce9 block: extract is_block_device() helper and dedupe inline fstat probes
probe_sparse_support() and DiskTopology::is_block_device() each carry
their own copy of the same fstat()+S_IFMT dance to ask "is this fd a
block device?". Hoist a single pub helper

    pub(crate) fn is_block_device(fd: RawFd) -> bool

into block::lib and route both call sites through it. Drop the
MaybeUninit gymnastics in favour of mem::zeroed() since libc::stat is
POD.

Drop DiskTopology::is_block_device since it is now just a one line
wrapper around the new helper function.

Pure refactor in preparation for the BLKDISCARD/BLKZEROOUT support,
which needs the same probe in three more backends.

Signed-off-by: Wei Liu <liuwe@microsoft.com>
2026-05-14 22:35:02 +00:00
306 changed files with 33756 additions and 17825 deletions

View File

@@ -1,3 +1,38 @@
[profile.default]
# Don't let one individual test run for more than 10 minutes
slow-timeout = { period = "60s", terminate-after = 10 }
[profile.integration]
fail-fast = false
retries = 3
[profile.common_tests]
inherits = "integration"
default-filter = 'test(common_parallel::) | test(common_sequential::) | test(aarch64_acpi::)'
junit.path = "/root/workloads/junit/common.xml"
[[profile.common_tests.overrides]]
filter = 'test(common_sequential::)'
# use up all the available test threads for each of the sequential tests
# i.e. no other test can be running while a sequential test is running.
threads-required = 'num-test-threads'
[profile.dbus]
inherits = "integration"
default-filter = 'test(dbus_api::)'
junit.path = "/root/workloads/junit/dbus.xml"
[profile.fw_cfg]
inherits = "integration"
default-filter = 'test(fw_cfg::)'
junit.path = "/root/workloads/junit/fw_cfg.xml"
[profile.ivshmem]
inherits = "integration"
default-filter = 'test(ivshmem::)'
junit.path = "/root/workloads/junit/ivshmem.xml"
[profile.common_cvm]
inherits = "integration"
default-filter = 'test(common_cvm::)'
junit.path = "/root/workloads/junit/cvm.xml"

View File

@@ -8,6 +8,7 @@ updates:
interval: weekly
allow:
- dependency-name: "acpi_tables"
- dependency-name: "iommufd-ioctls"
- dependency-name: "kvm-bindings"
- dependency-name: "kvm-ioctls"
- dependency-name: "linux-loader"
@@ -44,6 +45,7 @@ updates:
semver-patch-days: 3
ignore:
- dependency-name: "acpi_tables"
- dependency-name: "iommufd-ioctls"
- dependency-name: "kvm-bindings"
- dependency-name: "kvm-ioctls"
- dependency-name: "linux-loader"

View File

@@ -20,11 +20,11 @@ jobs:
ci: ${{ steps.changes.outputs.ci }}
docs: ${{ steps.changes.outputs.docs }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
filters: |
rust:
@@ -69,7 +69,7 @@ jobs:
needs: [preflight]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v6
with:
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
# PR head, not the merge ref, so gitlint sees the PR's commits.
ref: ${{ github.event.pull_request.head.sha }}
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get changed files in PR
@@ -139,7 +139,7 @@ jobs:
fi
- name: Link Availability Check (Diff Only)
if: ${{ steps.changed-files.outputs.all_changed_files != '' }}
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
args: --verbose --config .lychee.toml ${{ steps.changed-files.outputs.all_changed_files }}
failIfEmpty: false
@@ -151,7 +151,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
@@ -166,7 +166,7 @@ jobs:
if: needs.preflight.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions-rust-lang/audit@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -177,7 +177,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Run the shell script checkers
uses: luizm/action-sh-checker@883217215b11c1fabbf00eb1a9a041f62d74c744 # v0.10.0
env:
@@ -190,7 +190,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Lint Dockerfile
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
@@ -205,7 +205,7 @@ jobs:
if: needs.preflight.outputs.full == 'true' || needs.preflight.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v6
formatting:
@@ -223,7 +223,7 @@ jobs:
RUSTFLAGS: -D warnings
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
@@ -241,7 +241,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install dependencies
@@ -271,7 +271,7 @@ jobs:
RUSTFLAGS: -D warnings
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
@@ -290,7 +290,7 @@ jobs:
runs-on: ubuntu-latest
container: openapitools/openapi-generator-cli
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Validate OpenAPI
run: |
/usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml
@@ -300,8 +300,8 @@ jobs:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1
- uses: actions/checkout@v7
- uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
quality:
name: quality
needs: [preflight]
@@ -328,7 +328,7 @@ jobs:
experimental: false
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Rust toolchain (${{ matrix.rust }})
@@ -408,30 +408,46 @@ jobs:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "ivshmem" -- -D warnings
- name: Clippy (sev_snp)
- name: Clippy (kvm + sev_snp)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "sev_snp" -- -D warnings
- name: Clippy (igvm)
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,sev_snp" -- -D warnings
- name: Clippy (mshv + sev_snp)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "igvm" -- -D warnings
- name: Clippy (kvm + tdx)
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,sev_snp" -- -D warnings
- name: Clippy (mshv + igvm + sev_snp)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "tdx,kvm" -- -D warnings
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,igvm,sev_snp" -- -D warnings
- name: Clippy (kvm + igvm)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,igvm" -- -D warnings
- name: Clippy (mshv + igvm)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,igvm" -- -D warnings
- name: Clippy (kvm + igvm + sev_snp + fw_cfg)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
@@ -470,7 +486,7 @@ jobs:
- x86_64-unknown-linux-musl
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install musl-gcc
@@ -484,8 +500,6 @@ jobs:
run: cargo build --locked --bin cloud-hypervisor
- name: Build (kvm)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm"
- name: Build (default features + tdx)
run: cargo build --locked --bin cloud-hypervisor --features "tdx"
- name: Build (default features + dbus_api)
run: cargo build --locked --bin cloud-hypervisor --features "dbus_api"
- name: Build (default features + guest_debug)
@@ -498,18 +512,53 @@ jobs:
run: cargo build --locked --bin cloud-hypervisor --features "ivshmem"
- name: Build (mshv)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv"
- name: Build (sev_snp)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "sev_snp"
- name: Build (mshv + igvm)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,igvm"
- name: Build (mshv + sev_snp)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,sev_snp"
- name: Build (mshv + igvm + sev_snp)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,igvm,sev_snp"
- name: Build (kvm + sev_snp)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,sev_snp"
- name: Build (kvm + igvm + sev_snp + fw_cfg)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,igvm,sev_snp,fw_cfg"
- name: Build (igvm)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "igvm"
- name: Build (kvm + igvm)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,igvm"
- name: Build (mshv + kvm)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,kvm"
- name: Release Build (default features)
run: cargo build --locked --all --release --target=${{ matrix.target }}
- name: Check build did not modify any files
run: test -z "$(git status --porcelain)"
build-riscv64:
name: build-riscv64
needs: [preflight]
if: needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust:
- stable
- "1.89.0" # MSRV — keep quoted.
env:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
steps:
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install riscv64 cross linker
run: sudo apt-get update && sudo apt-get install -y gcc-riscv64-linux-gnu
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: riscv64gc-unknown-linux-gnu
- name: Build (kvm)
run: cargo build --locked --package cloud-hypervisor --no-default-features --features "kvm" --target riscv64gc-unknown-linux-gnu
- name: Check build did not modify any files
run: test -z "$(git status --porcelain)"
# garm-jammy + gnu: runs on PR and MQ. Other 3 matrix entries are in
# integration-x86-64-mq (sibling, MQ-only, runs in parallel).
integration-x86-64-pr:
@@ -526,7 +575,7 @@ jobs:
runs-on: garm-jammy-16
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Docker
@@ -571,7 +620,7 @@ jobs:
runs-on: ${{ format('{0}-16', matrix.runner) }}
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Docker
@@ -610,7 +659,7 @@ jobs:
- name: Fix workspace permissions
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Run unit tests (musl)
@@ -635,7 +684,7 @@ jobs:
IMG_BASENAME=windows-11-iot-enterprise-aarch64.raw
IMG_PATH=$HOME/workloads/$IMG_BASENAME
IMG_GZ_PATH=$HOME/workloads/$IMG_BASENAME.gz
IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-9-min.raw.gz
IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-25h2-6.raw.gz
cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/"
pushd "$HOME/workloads"
if sha1sum "$IMG_BASENAME.sha1" --check; then
@@ -643,6 +692,7 @@ jobs:
fi
popd
mkdir -p "$HOME/workloads"
rm -f "$IMG_PATH" "$IMG_GZ_PATH"
az storage blob download --container-name private-images --file "$IMG_GZ_PATH" --name "$IMG_GZ_BLOB_NAME" --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
gzip -d "$IMG_GZ_PATH"
- name: Run Windows guest integration tests
@@ -661,7 +711,7 @@ jobs:
- name: Fix workspace permissions
run: sudo chown -R github-runner:github-runner "${GITHUB_WORKSPACE}"
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Run VFIO integration tests
@@ -679,7 +729,7 @@ jobs:
runs-on: garm-jammy-16
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Docker
@@ -711,38 +761,108 @@ jobs:
- name: Run Windows guest integration tests for musl
timeout-minutes: 15
run: scripts/dev_cli.sh tests --integration-windows --libc musl
integration-rate-limiter:
name: integration-rate-limiter
integration-mshv-x86-64:
name: integration-mshv-x86-64
needs: [preflight, dco, quality, build]
if: >-
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
runs-on: bare-metal-9950x
env:
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
timeout-minutes: 35
runs-on: mshv
steps:
# mshv runner user is "lsgunner"
- name: Fix workspace and Docker socket permissions
run: |
sudo chown -R lsgrunner:lsgrunner ${GITHUB_WORKSPACE}
sudo chmod 666 /var/run/docker.sock
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Run rate-limiter integration tests
- name: Prepare for VDPA
run: scripts/prepare_vdpa.sh
- name: Run integration tests
timeout-minutes: 30
run: scripts/dev_cli.sh tests --integration
# Rate-limiter host is not available
# integration-rate-limiter:
# name: integration-rate-limiter
# needs: [preflight, dco, quality, build]
# if: >-
# github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
# runs-on: bare-metal-9950x
# env:
# AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
# steps:
# - name: Code checkout
# uses: actions/checkout@v7
# with:
# fetch-depth: 0
# - name: Run rate-limiter integration tests
# timeout-minutes: 20
# run: scripts/dev_cli.sh tests --integration-rate-limiter
integration-sev-snp:
name: integration-sev-snp
needs: [preflight, dco, quality, build]
if: >-
github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
timeout-minutes: 30
runs-on: noble-sevsnp
steps:
# Self-hosted runners reuse their workdir; a previous privileged
# container run can leave root-owned files behind.
- name: Fix workspace permissions
run: sudo chown -R "$(id -un):$(id -gn)" "${GITHUB_WORKSPACE}"
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Sanity-check SEV-SNP prerequisites
run: |
set -eufo pipefail
echo "Checking hypervisor device nodes..."
test -e /dev/kvm || { echo "::error::/dev/kvm missing"; exit 1; }
test -e /dev/sev || { echo "::error::/dev/sev missing"; exit 1; }
echo "Checking staged IGVM/kernel artifacts..."
test -d /usr/share/cloud-hypervisor/cvm \
|| { echo "::error::/usr/share/cloud-hypervisor/cvm missing"; exit 1; }
ls -l /usr/share/cloud-hypervisor/cvm
- name: Run CVM (SEV-SNP) integration tests
timeout-minutes: 20
run: scripts/dev_cli.sh tests --integration-rate-limiter
run: scripts/dev_cli.sh tests --integration-cvm --hypervisor kvm
# Rate-limiter host is not available
# integration-rate-limiter:
# name: integration-rate-limiter
# needs: [preflight, dco, quality, build]
# if: >-
# github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
# runs-on: bare-metal-9950x
# env:
# AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
# steps:
# - name: Code checkout
# uses: actions/checkout@v7
# with:
# fetch-depth: 0
# - name: Run rate-limiter integration tests
# timeout-minutes: 20
# run: scripts/dev_cli.sh tests --integration-rate-limiter
# The single required-status check. Branch protection requires this one job.
all-green:
name: all-green
needs:
- audit
- build
- build-riscv64
- dco
- formatting
- fuzz-build
- gitlint
- hadolint
- integration-arm64
# VFIO worker is failing #8160
# - integration-vfio
# See: #8211
# - integration-windows
- integration-sev-snp
- integration-vfio
- integration-mshv-x86-64
- integration-windows
- integration-x86-64-mq
- integration-x86-64-pr
- openapi

View File

@@ -14,11 +14,28 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
main:
build:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
runs-on: ubuntu-latest
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -27,12 +44,52 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to ghcr
if: ${{ github.event_name == 'push' }}
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
with:
file: ./resources/Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }}
- name: Export digest
if: ${{ github.event_name == 'push' }}
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
if: ${{ github.event_name == 'push' }}
uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs: build
if: ${{ github.event_name == 'push' }}
steps:
- name: Download digests
uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Docker meta
id: meta
@@ -41,25 +98,22 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# generate Docker tags based on the following events/attributes
tags: |
type=raw,value=20251114-0
type=raw,value=20260522-0
type=sha
- name: Build and push
if: ${{ github.event_name == 'push' }}
uses: docker/build-push-action@v7
- name: Login to ghcr
uses: docker/login-action@v4
with:
file: ./resources/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build only
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v7
with:
file: ./resources/Dockerfile
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}

View File

@@ -7,16 +7,26 @@ on:
jobs:
build:
name: Tests (Metrics)
runs-on: bare-metal-9950x
runs-on: garm-jammy-16
env:
METRICS_PUBLISH_KEY: ${{ secrets.METRICS_PUBLISH_KEY }}
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Docker
run: |
set -eufo pipefail
sudo apt-get update
sudo apt-get -y install ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt install -y docker-ce docker-ce-cli
- name: Run metrics tests
timeout-minutes: 60
run: scripts/dev_cli.sh tests --metrics -- --test-exclude micro_ -- --report-file /root/workloads/metrics.json
run: scripts/dev_cli.sh tests --metrics -- --test-exclude micro_,block_qcow2 -- --report-file /root/workloads/metrics.json
- name: Upload metrics report
run: 'curl -X PUT https://ch-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json'

View File

@@ -1,246 +0,0 @@
name: MSHV Infra Setup
on:
workflow_call:
inputs:
ARCH:
description: 'Architecture for the VM'
required: true
type: string
KEY:
description: 'SSH Key Name'
required: true
type: string
OS_DISK_SIZE:
description: 'OS Disk Size in GB'
required: true
type: number
RG:
description: 'Resource Group Name'
required: true
type: string
VM_SKU:
description: 'VM SKU'
required: true
type: string
secrets:
MI_CLIENT_ID:
required: true
RUNNER_RG:
required: true
STORAGE_ACCOUNT_PATHS:
required: true
ARCH_SOURCE_PATH:
required: true
USERNAME:
required: true
outputs:
RG_NAME:
description: 'Resource group of the VM'
value: ${{ jobs.infra-setup.outputs.RG_NAME }}
VM_NAME:
description: 'Name of the VM'
value: ${{ jobs.infra-setup.outputs.VM_NAME }}
PRIVATE_IP:
description: 'Private IP of the VM'
value: ${{ jobs.infra-setup.outputs.PRIVATE_IP }}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
infra-setup:
name: ${{ inputs.ARCH }} VM Provision
runs-on: mshv
outputs:
RG_NAME: ${{ steps.rg-setup.outputs.RG_NAME }}
VM_NAME: ${{ steps.vm-setup.outputs.VM_NAME }}
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
steps:
- name: Install & login to AZ CLI
env:
MI_CLIENT_ID: ${{ secrets.MI_CLIENT_ID }}
run: |
set -eufo pipefail
echo "Installing Azure CLI if not already installed"
if ! command -v az &>/dev/null; then
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
else
echo "Azure CLI already installed"
fi
az --version
echo "Logging into Azure CLI using Managed Identity"
az login --identity --client-id "${MI_CLIENT_ID}"
- name: Get Location
id: get-location
env:
SKU: ${{ inputs.VM_SKU }}
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
run: |
set -eufo pipefail
# Extract vCPU count from SKU (e.g., "Standard_D2s_v3" => 2)
if ! [[ "$SKU" =~ ^Standard_[A-Za-z]+([1-9][0-9]*) ]]; then
printf 'Cannot extract vCPU count from SKU: %q\n' "$SKU"
exit 1
fi
vcpu=${BASH_REMATCH[1]}
SUPPORTED_LOCATIONS=$(echo "$STORAGE_ACCOUNT_PATHS" | jq -r 'to_entries[] | .key')
for location in $SUPPORTED_LOCATIONS; do
family=$(az vm list-skus --size "$SKU" --location "$location" --resource-type "virtualMachines" --query '[0].family' -o tsv)
if [[ -z "$family" ]]; then
echo "Cannot determine VM family for SKU: $SKU in $location"
continue
fi
remaining=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json |
jq '(.limit | tonumber) - (.currentValue | tonumber) >= ($ARGS.positional[0] | tonumber)' --jsonargs "$vcpu")
if [[ "$remaining" = true ]]; then
echo "Sufficient quota found in $location"
echo "location=$location" >> "$GITHUB_OUTPUT"
exit 0
fi
done
echo "No location found with sufficient vCPU quota for SKU: $SKU"
exit 1
- name: Create Resource Group
id: rg-setup
env:
LOCATION: ${{ steps.get-location.outputs.location }}
RG: ${{ inputs.RG }}
STORAGE_ACCOUNT_PATHS: ${{ secrets.STORAGE_ACCOUNT_PATHS }}
run: |
set -eufo pipefail
echo "Creating Resource Group: $RG"
# Create the resource group
echo "Creating resource group in location: ${LOCATION}"
az group create --name "${RG}" --location "${LOCATION}"
echo "RG_NAME=${RG}" >> $GITHUB_OUTPUT
echo "Resource group created successfully."
- name: Generate SSH Key
id: generate-ssh-key
env:
KEY: ${{ inputs.KEY }}
run: |
set -eufo pipefail
echo "Generating SSH key: $KEY"
mkdir -p ~/.ssh
ssh-keygen -t rsa -b 4096 -f ~/.ssh/"${KEY}" -N ""
- name: Create VM
id: vm-setup
env:
KEY: ${{ inputs.KEY }}
LOCATION: ${{ steps.get-location.outputs.location }}
OS_DISK_SIZE: ${{ inputs.OS_DISK_SIZE }}
RG: ${{ inputs.RG }}
RUNNER_RG: ${{ secrets.RUNNER_RG }}
USERNAME: ${{ secrets.USERNAME }}
VM_SKU: ${{ inputs.VM_SKU }}
VM_IMAGE_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_image
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
run: |
set -eufo pipefail
echo "Creating $VM_SKU VM: $VM_NAME"
# Extract subnet ID from the runner VM
echo "Retrieving subnet ID..."
SUBNET_ID=$(az network vnet list --resource-group "$RUNNER_RG" --query "[?contains(location, '${LOCATION}')].{SUBNETS:subnets}" | jq -r ".[0].SUBNETS[0].id")
if [[ -z "${SUBNET_ID}" ]]; then
echo "ERROR: Failed to retrieve Subnet ID."
exit 1
fi
# Extract image ID from the runner VM
echo "Retrieving image ID..."
IMAGE_ID=$(az image show --resource-group "$RUNNER_RG" --name "$VM_IMAGE_NAME" --query "id" -o tsv)
if [[ -z "${IMAGE_ID}" ]]; then
echo "ERROR: Failed to retrieve Image ID."
exit 1
fi
# Create VM
az vm create \
--resource-group "${RG}" \
--name "${VM_NAME}" \
--subnet "${SUBNET_ID}" \
--size "${VM_SKU}" \
--location "${LOCATION}" \
--image "${IMAGE_ID}" \
--os-disk-size-gb "${OS_DISK_SIZE}" \
--public-ip-sku Standard \
--storage-sku Premium_LRS \
--public-ip-address "" \
--admin-username "${USERNAME}" \
--ssh-key-value ~/.ssh/"${KEY}".pub \
--security-type Standard \
--output json
az vm boot-diagnostics enable --name "${VM_NAME}" --resource-group "${RG}"
echo "VM_NAME=${VM_NAME}" >> "$GITHUB_OUTPUT"
echo "VM creation process completed successfully."
- name: Get VM Private IP
id: get-vm-ip
env:
RG: ${{ inputs.RG }}
VM_NAME: ${{ inputs.ARCH }}_${{ steps.get-location.outputs.location }}_${{ github.run_id }}
run: |
set -eufo pipefail
echo "Retrieving VM Private IP address..."
# Retrieve VM Private IP address
PRIVATE_IP=$(az vm show -g "${RG}" -n "${VM_NAME}" -d --query privateIps -o tsv)
if [[ -z "$PRIVATE_IP" ]]; then
echo "ERROR: Failed to retrieve private IP address."
exit 1
fi
echo "PRIVATE_IP=$PRIVATE_IP" >> "$GITHUB_OUTPUT"
- name: Wait for SSH availability
env:
KEY: ${{ inputs.KEY }}
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
USERNAME: ${{ secrets.USERNAME }}
run: |
echo "Waiting for SSH to be accessible..."
timeout 120 bash -c 'until ssh -o StrictHostKeyChecking=no -i ~/.ssh/"${KEY}" -- "${USERNAME}@${PRIVATE_IP}" "exit" 2>/dev/null; do sleep 5; done'
echo "VM is accessible!"
- name: Remove Old Host Key
env:
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
run: |
set -eufo pipefail
echo "Removing the old host key"
ssh-keygen -R "$PRIVATE_IP"
- name: SSH into VM and Install Dependencies
env:
KEY: ${{ inputs.KEY }}
PRIVATE_IP: ${{ steps.get-vm-ip.outputs.PRIVATE_IP }}
USERNAME: ${{ secrets.USERNAME }}
run: |
set -eufo pipefail
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" << EOF
set -eufo pipefail
echo "Logged in successfully."
echo "Installing dependencies..."
sudo tdnf install -y git moby-engine moby-cli clang llvm pkg-config make gcc glibc-devel
echo "Installing Rust..."
curl -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable --profile default -y
export PATH="\$HOME/.cargo/bin:\$PATH"
cargo --version
sudo mkdir -p /etc/docker/
echo '{"default-ulimits":{"nofile":{"Hard":65535,"Name":"nofile","Soft":65535}}}' | sudo tee /etc/docker/daemon.json
sudo systemctl stop docker
sudo systemctl enable docker.service
sudo systemctl enable containerd.service
sudo systemctl start docker
sudo groupadd -f docker
sudo usermod -a -G docker "${USERNAME}"
sudo systemctl restart docker
EOF

View File

@@ -1,129 +0,0 @@
name: Cloud Hypervisor Tests (MSHV) (x86_64)
on: [pull_request_target, merge_group]
permissions: {}
jobs:
infra-setup:
name: MSHV Infra Setup (x86_64)
uses: ./.github/workflows/mshv-infra.yaml
with:
ARCH: x86_64
KEY: azure_key_${{ github.run_id }}
OS_DISK_SIZE: 512
RG: MSHV-INTEGRATION-${{ github.run_id }}
VM_SKU: Standard_D16s_v5
secrets:
MI_CLIENT_ID: ${{ secrets.MSHV_MI_CLIENT_ID }}
RUNNER_RG: ${{ secrets.MSHV_RUNNER_RG }}
STORAGE_ACCOUNT_PATHS: ${{ secrets.MSHV_STORAGE_ACCOUNT_PATHS }}
ARCH_SOURCE_PATH: ${{ secrets.MSHV_X86_SOURCE_PATH }}
USERNAME: ${{ secrets.MSHV_USERNAME }}
run-tests:
name: Integration Tests (x86_64)
needs: infra-setup
if: ${{ always() && needs.infra-setup.result == 'success' }}
runs-on: mshv
steps:
- name: Run integration tests
timeout-minutes: 60
env:
KEY: azure_key_${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_URL: https://github.com/cloud-hypervisor/cloud-hypervisor.git
REPO_DIR: cloud-hypervisor
PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }}
RG: MSHV-${{ github.run_id }}
USERNAME: ${{ secrets.MSHV_USERNAME }}
run: |
set -eufo pipefail
echo "Connecting to the VM via SSH..."
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" << EOF
set -e
echo "Logged in successfully."
export PATH="\$HOME/.cargo/bin:\$PATH"
if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then
git clone --depth 1 "$REPO_URL" "$REPO_DIR"
cd "$REPO_DIR"
git fetch origin pull/${{ github.event.pull_request.number }}/merge
git checkout FETCH_HEAD
else
git clone --depth 1 --single-branch --branch "${{ github.ref_name }}" "$REPO_URL" "$REPO_DIR"
cd "$REPO_DIR"
fi
echo "Loading VDPA kernel modules..."
sudo modprobe vdpa
sudo modprobe vhost_vdpa
sudo modprobe vdpa_sim
sudo modprobe vdpa_sim_blk
sudo modprobe vdpa_sim_net
echo "Creating VDPA devices..."
sudo vdpa dev add name vdpa-blk0 mgmtdev vdpasim_blk
sudo vdpa dev add name vdpa-blk1 mgmtdev vdpasim_blk
sudo vdpa dev add name vdpa-blk2 mgmtdev vdpasim_net
echo "Setting permissions..."
for i in 0 1 2; do
dev="/dev/vhost-vdpa-\$i"
if [ -e "\$dev" ]; then
sudo chown \$USER:\$USER "\$dev"
sudo chmod 660 "\$dev"
else
echo "Warning: Device \$dev not found"
fi
done
sudo ./scripts/dev_cli.sh tests --hypervisor mshv --integration
EOF
- name: Dump dmesg
if: always()
continue-on-error: true
env:
KEY: azure_key_${{ github.run_id }}
PRIVATE_IP: ${{ needs.infra-setup.outputs.PRIVATE_IP }}
USERNAME: ${{ secrets.MSHV_USERNAME }}
run: |
ssh -i ~/.ssh/"${KEY}" -o StrictHostKeyChecking=no -- "${USERNAME}@${PRIVATE_IP}" sudo dmesg
- name: Dump serial console logs
if: always()
continue-on-error: true
env:
RG_NAME: ${{ needs.infra-setup.outputs.RG_NAME }}
VM_NAME: ${{ needs.infra-setup.outputs.VM_NAME }}
run: |
set -eufo pipefail
az vm boot-diagnostics get-boot-log --name "${VM_NAME}" --resource-group "${RG_NAME}" | jq -r
cleanup:
name: Cleanup
needs: run-tests
if: always()
runs-on: mshv
steps:
- name: Delete RG
env:
RG: MSHV-INTEGRATION-${{ github.run_id }}
run: |
if az group exists --name "${RG}"; then
az group delete --name "${RG}" --yes --no-wait
else
echo "Resource Group ${RG} does not exist. Skipping deletion."
fi
echo "Cleanup process completed."
- name: Delete SSH Key
env:
KEY: azure_key_${{ github.run_id }}
run: |
if [ -f ~/.ssh/"${KEY}" ]; then
rm -f ~/.ssh/"${KEY}" ~/.ssh/"${KEY}.pub"
echo "SSH key deleted successfully."
else
echo "SSH key does not exist. Skipping deletion."
fi
echo "Cleanup process completed."

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install musl-gcc
if: contains(matrix.platform.target, 'musl')
run: sudo apt install -y musl-tools

View File

@@ -23,6 +23,9 @@ exclude = [
'^tcp://192\.168\.1\.10',
# Slack invite endpoints reject automated GETs and return 403.
'^https://join\.slack\.com/t/',
# Metrics publish endpoint only answers authenticated PUTs; a plain GET
# returns 404.
'^https://ch-metrics\.azurewebsites\.net/api/publishmetrics',
]
# Exclude loopback addresses

View File

@@ -11,9 +11,8 @@ reviewable, and compatible with the project's normal engineering constraints.
- Start with `README.md` for the project shape and `CONTRIBUTING.md` for the
contribution rules, coding style, commit message guidance, and LLM assistance
disclosure policy. Following `CONTRIBUTING.md` is crucial!
- The main supported architectures are `x86_64` and `aarch64`; the main
hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the most regular
exercise, but changes must not make the other first-class targets worse.
- Respect `.editorconfig` when editing files, in addition to any
language-specific formatter required by `CONTRIBUTING.md`.
### Change Guidelines
@@ -32,6 +31,11 @@ reviewable, and compatible with the project's normal engineering constraints.
migration implications.
- Do not invent APIs, behavior, or requirements. If something is uncertain,
state the uncertainty and proceed only with minimal, explicit assumptions.
- For `thiserror`-style errors, start messages with a capital letter and keep
the outer `Display` text short. Put all non-`#[source]` attributes in the
message to improve helpfulness, but do not repeat a `#[source]` value
inline: Cloud Hypervisor prints the full error chain, so only include the
concrete failure text directly when there is no source to report.
### Safety and Domain Notes
@@ -39,11 +43,6 @@ reviewable, and compatible with the project's normal engineering constraints.
comment with the invariants, and make sure the surrounding code upholds them.
- Assume concurrency matters. Avoid races, unsynchronized shared state, and
implicit ordering assumptions; prefer clear ownership and synchronization.
- Keep docs and comments short and useful. Document non-trivial invariants at
struct definitions and critical state transitions.
- Logging should be minimal and high signal. Use `info!` for important normal
state changes that matter in production; use `warn!` or `error!` only for
abnormal conditions. Keep `debug!` for focused diagnostics.
### Build and Test Notes
@@ -61,9 +60,7 @@ reviewable, and compatible with the project's normal engineering constraints.
host privileges, workloads, and container setup. To build the integration-test
code directly without the infrastructure from `./scripts`, set the Rust cfg
`devcli_testenv` or simply build through `clippy` which automatically includes
these code paths; otherwise the integration-test code is not included. Do not
assume the tests can be run directly in a restricted agent environment; ask
the developer to run them when real integration coverage is needed.
these code paths; otherwise the integration-test code is not included.
### Commit and Patch Formatting
@@ -74,8 +71,9 @@ reviewable, and compatible with the project's normal engineering constraints.
specified in `./scripts/gitlint/rules`.
- For LLM-assisted changes, follow the disclosure guidance in `CONTRIBUTING.md`:
use the project's `Assisted-by:` trailer when disclosure is needed, and do not
add `Co-authored-by` or similar trailers unless that policy changes.
add `Co-authored-by` or similar trailers unless that policy changes. Prefer
explicit version numbers, such as `Assisted-by: Claude:Opus-4.7`, rather than
`Assisted-by: Claude:Opus-4`.
- Temporary allowances such as `#[allow(unused)]` or ignored tests are only
acceptable if resolved within the same commit series or paired with a clear
TODO referencing a ticket. Ask the developer if in doubt.

View File

@@ -11,6 +11,11 @@ license of those projects.
New code should be under the [Apache v2
License](https://opensource.org/licenses/Apache-2.0).
Cloud Hypervisor's main supported architectures are `x86_64` and `aarch64`,
and the main hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the
most regular exercise, but changes should not make the other supported
architecture and backend combinations worse.
## Coding Style & Code Comments
We use the [Rust Style] guide and enforce formatting and linting in CI,
@@ -34,6 +39,17 @@ For new code, add documentation and comments where they **provide additional val
Comments should be concise and add additional context or information to the code.
Logging should be minimal and high signal. Use `info!` for important normal
state changes that matter in production; use `warn!` or `error!` only for
abnormal conditions. Keep `debug!` for focused diagnostics. Please find more
information in [`docs/logging.md`](docs/logging.md).
Error messages should be sentence-style: start with a capital letter and stay
concise. For `thiserror`-style errors, put all non-`#[source]` attributes
(if they provide clear value) in the outer `Display` text to improve helpfulness,
but do not repeat a `#[source]` value there because Cloud Hypervisor prints the
full chain elsewhere.
[Rust Style]: https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src
## Basic Checks

832
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ members = [
"event_monitor",
"hypervisor",
"net_util",
"offload_daemon",
"option_parser",
"pci",
"performance-metrics",
@@ -52,16 +53,16 @@ resolver = "3"
[workspace.dependencies]
# rust-vmm crates
acpi_tables = "0.2.0"
iommufd-ioctls = "0.1.0"
kvm-bindings = "0.14.0"
kvm-ioctls = "0.24.0"
acpi_tables = "0.2.1"
iommufd-ioctls = "0.2.0"
kvm-bindings = "0.14.1"
kvm-ioctls = "0.25.0"
linux-loader = "0.13.2"
mshv-bindings = "0.6.9"
mshv-ioctls = "0.6.9"
seccompiler = "0.5.0"
vfio-bindings = { version = "0.6.2", default-features = false }
vfio-ioctls = { version = "0.6.0", default-features = false }
vfio-ioctls = { version = "0.7.0", default-features = false }
vfio_user = { version = "0.1.3", default-features = false }
vhost = { version = "0.16.0", default-features = false }
vhost-user-backend = { version = "0.22.0", default-features = false }
@@ -77,11 +78,12 @@ igvm_defs = "0.4.0"
# serde crates
serde = "1.0.228"
serde_json = "1.0.149"
serde_with = { version = "3.18.0", default-features = false }
serde_json = "1.0.150"
serde_with = { version = "3.19.0", default-features = false }
# other crates
anyhow = "1.0.102"
base64 = "0.22.1"
bitflags = "2.11.1"
byteorder = "1.5.0"
cfg-if = "1.0.4"
@@ -91,16 +93,23 @@ dirs = "6.0.0"
env_logger = "0.11.10"
epoll = "4.4.0"
flume = "0.12.0"
itertools = "0.14.0"
itertools = "0.15.0"
jiff = { version = "0.2", default-features = false, features = ["std"] }
libc = "0.2.186"
log = "0.4.29"
log = "0.4.30"
rustls = { version = "0.23.40", default-features = false, features = [
"logging",
"ring",
"std",
] }
sha2 = "0.11.0"
signal-hook = "0.4.4"
signal-hook-registry = "1.4.8"
smallvec = "1.15.1"
thiserror = "2.0.18"
uuid = { version = "1.23.1" }
uuid = { version = "1.23.2" }
wait-timeout = "0.2.1"
zerocopy = { version = "0.8.48", default-features = false }
zerocopy = { version = "0.8.50", default-features = false }
[workspace.lints.clippy]
# Any clippy lint (group) in alphabetical order:
@@ -115,6 +124,7 @@ style = "deny"
suspicious = "deny"
# Individual Lints
absolute_paths = "deny"
assertions_on_result_states = "deny"
if_not_else = "deny"
manual_string_new = "deny"

View File

@@ -59,10 +59,10 @@ based on the [Rust VMM](https://github.com/rust-vmm) crates.
### Architectures
Cloud Hypervisor supports the `x86-64`, `AArch64` and `riscv64`
architectures, with functionality varying across these platforms. The
functionality differences between `x86-64` and `AArch64` are documented
in [#1125](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/1125).
Cloud Hypervisor's main supported architectures are `x86-64` and `AArch64`,
with functionality varying across these platforms. The functionality
differences between `x86-64` and `AArch64` are documented in
[#1125](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/1125).
The `riscv64` architecture support is experimental and offers limited
functionality. For more details and instructions, please refer to [riscv
documentation](docs/riscv.md).
@@ -203,7 +203,7 @@ To build the kernel:
```shell
# Clone the Cloud Hypervisor Linux branch
$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.12.8 linux-cloud-hypervisor
$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.16.9 linux-cloud-hypervisor
$ pushd linux-cloud-hypervisor
$ make ch_defconfig
# Do native build of the x86-64 kernel

View File

@@ -3,24 +3,26 @@
// SPDX-License-Identifier: Apache-2.0
//
use std::io::{Read, Write};
use std::io::{self, Read, Write};
use std::os::unix::io::RawFd;
use std::{num, str};
use thiserror::Error;
use vmm_sys_util::errno;
use vmm_sys_util::sock_ctrl_msg::ScmSocket;
#[derive(Debug, Error)]
pub enum Error {
#[error("Error writing to or reading from HTTP socket")]
Socket(#[source] std::io::Error),
Socket(#[source] io::Error),
#[error("Error sending file descriptors")]
SocketSendFds(#[source] vmm_sys_util::errno::Error),
SocketSendFds(#[source] errno::Error),
#[error("Error parsing HTTP status code")]
StatusCodeParsing(#[source] std::num::ParseIntError),
StatusCodeParsing(#[source] num::ParseIntError),
#[error("HTTP output is missing protocol statement")]
MissingProtocol,
#[error("Error parsing HTTP Content-Length field")]
ContentLengthParsing(#[source] std::num::ParseIntError),
ContentLengthParsing(#[source] num::ParseIntError),
#[error("Server responded with error {0:?}: {1:?}")]
ServerResponse(
StatusCode,
@@ -100,7 +102,7 @@ fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
if count == 0 {
break;
}
res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap());
res.push_str(str::from_utf8(&bytes[0..count]).unwrap());
// End of headers
if let Some(o) = res.find("\r\n\r\n") {

View File

@@ -25,6 +25,10 @@ uuid = { workspace = true }
vm-memory = { workspace = true, features = ["backend-bitmap", "backend-mmap"] }
vmm-sys-util = { workspace = true, features = ["with-serde"] }
[dev-dependencies]
proptest = "1.0.0"
serde_json = { workspace = true }
[target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies]
fdt_parser = { version = "0.1.5", package = "fdt" }
vm-fdt = { workspace = true }

188
arch/src/aarch64/cache.rs Normal file
View File

@@ -0,0 +1,188 @@
// Copyright 2020 Arm Limited (or its affiliates). All rights reserved.
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Portions Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the THIRD-PARTY file.
use std::fs;
use std::path::Path;
use log::warn;
#[derive(Copy, Clone)]
pub enum CacheLevel {
/// L1 data cache
L1D = 0,
/// L1 instruction cache
L1I = 1,
/// L2 cache
L2 = 2,
/// L3 cache
L3 = 3,
}
/// NOTE: cache size file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/size".
pub fn get_cache_size(cache_level: CacheLevel) -> u32 {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
match cache_level {
CacheLevel::L1D => file_directory += "/index0/size",
CacheLevel::L1I => file_directory += "/index1/size",
CacheLevel::L2 => file_directory += "/index2/size",
CacheLevel::L3 => file_directory += "/index3/size",
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
// The content of the file is as simple as a size, like: "32K"
let src = src.trim();
let src_digits: u32 = src[0..src.len() - 1].parse().unwrap();
let src_unit = &src[src.len() - 1..];
src_digits
* match src_unit {
"K" => 1u32 << 10,
"M" => 1u32 << 20,
"G" => 1u32 << 30,
_ => 1,
}
} else {
0
}
}
/// NOTE: coherency_line_size file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size".
pub fn get_cache_coherency_line_size(cache_level: CacheLevel) -> u32 {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
match cache_level {
CacheLevel::L1D => file_directory += "/index0/coherency_line_size",
CacheLevel::L1I => file_directory += "/index1/coherency_line_size",
CacheLevel::L2 => file_directory += "/index2/coherency_line_size",
CacheLevel::L3 => file_directory += "/index3/coherency_line_size",
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
src.trim().parse::<u32>().unwrap()
} else {
0
}
}
/// NOTE: number_of_sets file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/number_of_sets".
pub fn get_cache_number_of_sets(cache_level: CacheLevel) -> u32 {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
match cache_level {
CacheLevel::L1D => file_directory += "/index0/number_of_sets",
CacheLevel::L1I => file_directory += "/index1/number_of_sets",
CacheLevel::L2 => file_directory += "/index2/number_of_sets",
CacheLevel::L3 => file_directory += "/index3/number_of_sets",
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
src.trim().parse::<u32>().unwrap()
} else {
0
}
}
/// NOTE: shared_cpu_list file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list".
pub fn get_cache_shared(cache_level: CacheLevel) -> bool {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
let mut result = true;
match cache_level {
CacheLevel::L1D | CacheLevel::L1I => result = false,
CacheLevel::L2 => file_directory += "/index2/shared_cpu_list",
CacheLevel::L3 => file_directory += "/index3/shared_cpu_list",
}
if !result {
return false;
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
let src = src.trim();
if src.is_empty() {
result = false;
} else {
result = src.contains('-') || src.contains(',');
}
} else {
result = false;
}
result
}
#[derive(Default, Copy, Clone, Debug)]
pub struct CacheTopologyInfo {
pub l1_d_cache_size: u32,
pub l1_d_cache_line_size: u32,
pub l1_d_cache_sets: u32,
pub l1_i_cache_size: u32,
pub l1_i_cache_line_size: u32,
pub l1_i_cache_sets: u32,
pub l2_cache_size: u32,
pub l2_cache_line_size: u32,
pub l2_cache_sets: u32,
pub l3_cache_size: u32,
pub l3_cache_line_size: u32,
pub l3_cache_sets: u32,
pub l2_cache_shared: bool,
pub l3_cache_shared: bool,
}
/// Reads cache topology information from sysfs for cpu0.
pub fn read_cache_topology() -> Option<CacheTopologyInfo> {
let cache_path = Path::new("/sys/devices/system/cpu/cpu0/cache");
if !cache_path.exists() {
warn!("Cache topology information is not available in sysfs.");
return None;
}
let mut info = CacheTopologyInfo {
l1_d_cache_size: get_cache_size(CacheLevel::L1D),
l1_d_cache_line_size: get_cache_coherency_line_size(CacheLevel::L1D),
l1_d_cache_sets: get_cache_number_of_sets(CacheLevel::L1D),
l1_i_cache_size: get_cache_size(CacheLevel::L1I),
l1_i_cache_line_size: get_cache_coherency_line_size(CacheLevel::L1I),
l1_i_cache_sets: get_cache_number_of_sets(CacheLevel::L1I),
l2_cache_size: get_cache_size(CacheLevel::L2),
l2_cache_line_size: get_cache_coherency_line_size(CacheLevel::L2),
l2_cache_sets: get_cache_number_of_sets(CacheLevel::L2),
l3_cache_size: get_cache_size(CacheLevel::L3),
l3_cache_line_size: get_cache_coherency_line_size(CacheLevel::L3),
l3_cache_sets: get_cache_number_of_sets(CacheLevel::L3),
l2_cache_shared: false,
l3_cache_shared: false,
};
if info.l2_cache_size != 0 {
info.l2_cache_shared = get_cache_shared(CacheLevel::L2);
}
if info.l3_cache_size != 0 {
info.l3_cache_shared = get_cache_shared(CacheLevel::L3);
}
Some(info)
}

View File

@@ -9,22 +9,24 @@
use std::collections::HashMap;
use std::ffi::CStr;
use std::fmt::Debug;
use std::path::Path;
use std::hash::BuildHasher;
use std::sync::{Arc, Mutex};
use std::{cmp, fs, result, str};
use std::{cmp, result, str};
use byteorder::{BigEndian, ByteOrder};
use fdt_parser::node::FdtNode;
use hypervisor::arch::aarch64::gic::Vgic;
use hypervisor::arch::aarch64::regs::{
AARCH64_ARCH_TIMER_HYP_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ,
AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_PMU_IRQ,
};
use log::{debug, info, warn};
use log::{debug, info};
use thiserror::Error;
use vm_fdt::{FdtWriter, FdtWriterResult};
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion};
use super::super::{DeviceType, GuestMemoryMmap, InitramfsConfig};
use super::cache::{CacheTopologyInfo, read_cache_topology};
use super::layout::{
GIC_V2M_COMPATIBLE, GICV2M_SPI_BASE, GICV2M_SPI_NUM, IRQ_BASE, MEM_32BIT_DEVICES_SIZE,
MEM_32BIT_DEVICES_START, MEM_PCI_IO_SIZE, MEM_PCI_IO_START, PCI_HIGH_BASE,
@@ -88,124 +90,9 @@ pub enum Error {
}
type Result<T> = result::Result<T, Error>;
#[derive(Copy, Clone)]
pub enum CacheLevel {
/// L1 data cache
L1D = 0,
/// L1 instruction cache
L1I = 1,
/// L2 cache
L2 = 2,
/// L3 cache
L3 = 3,
}
/// NOTE: cache size file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/size".
pub fn get_cache_size(cache_level: CacheLevel) -> u32 {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
match cache_level {
CacheLevel::L1D => file_directory += "/index0/size",
CacheLevel::L1I => file_directory += "/index1/size",
CacheLevel::L2 => file_directory += "/index2/size",
CacheLevel::L3 => file_directory += "/index3/size",
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
// The content of the file is as simple as a size, like: "32K"
let src = src.trim();
let src_digits: u32 = src[0..src.len() - 1].parse().unwrap();
let src_unit = &src[src.len() - 1..];
src_digits
* match src_unit {
"K" => 1024,
"M" => 1024u32.pow(2),
"G" => 1024u32.pow(3),
_ => 1,
}
} else {
0
}
}
/// NOTE: coherency_line_size file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size".
pub fn get_cache_coherency_line_size(cache_level: CacheLevel) -> u32 {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
match cache_level {
CacheLevel::L1D => file_directory += "/index0/coherency_line_size",
CacheLevel::L1I => file_directory += "/index1/coherency_line_size",
CacheLevel::L2 => file_directory += "/index2/coherency_line_size",
CacheLevel::L3 => file_directory += "/index3/coherency_line_size",
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
src.trim().parse::<u32>().unwrap()
} else {
0
}
}
/// NOTE: number_of_sets file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/number_of_sets".
pub fn get_cache_number_of_sets(cache_level: CacheLevel) -> u32 {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
match cache_level {
CacheLevel::L1D => file_directory += "/index0/number_of_sets",
CacheLevel::L1I => file_directory += "/index1/number_of_sets",
CacheLevel::L2 => file_directory += "/index2/number_of_sets",
CacheLevel::L3 => file_directory += "/index3/number_of_sets",
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
src.trim().parse::<u32>().unwrap()
} else {
0
}
}
/// NOTE: shared_cpu_list file directory example,
/// "/sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list".
pub fn get_cache_shared(cache_level: CacheLevel) -> bool {
let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string();
let mut result = true;
match cache_level {
CacheLevel::L1D | CacheLevel::L1I => result = false,
CacheLevel::L2 => file_directory += "/index2/shared_cpu_list",
CacheLevel::L3 => file_directory += "/index3/shared_cpu_list",
}
if !result {
return false;
}
let file_path = Path::new(&file_directory);
if file_path.exists() {
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
let src = src.trim();
if src.is_empty() {
result = false;
} else {
result = src.contains('-') || src.contains(',');
}
} else {
result = false;
}
result
}
/// Creates the flattened device tree for this aarch64 VM.
#[allow(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
#[expect(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
vcpu_mpidr: &[u64],
@@ -287,63 +174,24 @@ fn create_cpu_nodes(
threads_per_core as u32 * cores_per_die as u32 * dies_per_package as u32 * packages as u32;
// Add cache info.
// L1 Data Cache Info.
let mut l1_d_cache_size: u32 = 0;
let mut l1_d_cache_line_size: u32 = 0;
let mut l1_d_cache_sets: u32 = 0;
// L1 Instruction Cache Info.
let mut l1_i_cache_size: u32 = 0;
let mut l1_i_cache_line_size: u32 = 0;
let mut l1_i_cache_sets: u32 = 0;
// L2 Cache Info.
let mut l2_cache_size: u32 = 0;
let mut l2_cache_line_size: u32 = 0;
let mut l2_cache_sets: u32 = 0;
// L3 Cache Info.
let mut l3_cache_size: u32 = 0;
let mut l3_cache_line_size: u32 = 0;
let mut l3_cache_sets: u32 = 0;
// Cache Shared Info.
let mut l2_cache_shared: bool = false;
let mut l3_cache_shared: bool = false;
let cache_path = Path::new("/sys/devices/system/cpu/cpu0/cache");
let cache_exist: bool = cache_path.exists();
if cache_exist {
// L1 Data Cache Info.
l1_d_cache_size = get_cache_size(CacheLevel::L1D);
l1_d_cache_line_size = get_cache_coherency_line_size(CacheLevel::L1D);
l1_d_cache_sets = get_cache_number_of_sets(CacheLevel::L1D);
// L1 Instruction Cache Info.
l1_i_cache_size = get_cache_size(CacheLevel::L1I);
l1_i_cache_line_size = get_cache_coherency_line_size(CacheLevel::L1I);
l1_i_cache_sets = get_cache_number_of_sets(CacheLevel::L1I);
// L2 Cache Info.
l2_cache_size = get_cache_size(CacheLevel::L2);
l2_cache_line_size = get_cache_coherency_line_size(CacheLevel::L2);
l2_cache_sets = get_cache_number_of_sets(CacheLevel::L2);
// L3 Cache Info.
l3_cache_size = get_cache_size(CacheLevel::L3);
l3_cache_line_size = get_cache_coherency_line_size(CacheLevel::L3);
l3_cache_sets = get_cache_number_of_sets(CacheLevel::L3);
// Cache Shared Info.
if l2_cache_size != 0 {
l2_cache_shared = get_cache_shared(CacheLevel::L2);
}
if l3_cache_size != 0 {
l3_cache_shared = get_cache_shared(CacheLevel::L3);
}
} else {
warn!("cache sysfs system does not exist.");
}
let cache_info = read_cache_topology();
let cache_exist = cache_info.is_some();
let CacheTopologyInfo {
l1_d_cache_size,
l1_d_cache_line_size,
l1_d_cache_sets,
l1_i_cache_size,
l1_i_cache_line_size,
l1_i_cache_sets,
l2_cache_size,
l2_cache_line_size,
l2_cache_sets,
l3_cache_size,
l3_cache_line_size,
l3_cache_sets,
l2_cache_shared,
l3_cache_shared,
} = cache_info.unwrap_or_default();
// Arm boot protocol requires a minimal Device Tree
// https://docs.kernel.org/arch/arm64/booting.html
@@ -879,7 +727,7 @@ fn create_fw_cfg_node<T: DeviceInfoForFdt + Clone + Debug>(
Ok(())
}
fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
fdt: &mut FdtWriter,
dev_info: &HashMap<(DeviceType, String), T, S>,
) -> FdtWriterResult<()> {
@@ -1145,7 +993,7 @@ pub fn print_fdt(dtb: &[u8]) {
}
}
fn print_node(node: fdt_parser::node::FdtNode<'_, '_>, n_spaces: usize) {
fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) {
debug!("{:indent$}{}/", "", node.name, indent = n_spaces);
for property in node.properties() {
let name = property.name;

View File

@@ -2,6 +2,8 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
/// Module for cache info.
pub mod cache;
/// Module for the flattened device tree.
pub mod fdt;
/// Layout for this aarch64 system.
@@ -11,6 +13,7 @@ pub mod uefi;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::BuildHasher;
use std::sync::{Arc, Mutex};
use hypervisor::arch::aarch64::gic::Vgic;
@@ -121,8 +124,8 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
}
/// Configures the system and should be called once per vm before starting vcpu threads.
#[allow(clippy::too_many_arguments)]
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
#[expect(clippy::too_many_arguments)]
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
vcpu_mpidr: &[u64],

View File

@@ -9,14 +9,17 @@
//! Supported platforms: x86_64, aarch64, riscv64.
use std::collections::BTreeMap;
use std::str::FromStr;
use std::sync::Arc;
use std::{fmt, result};
use serde::de::{IntoDeserializer, value};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use vm_memory::bitmap::AtomicBitmap;
type GuestMemoryMmap = vm_memory::GuestMemoryMmap<vm_memory::bitmap::AtomicBitmap>;
type GuestRegionMmap = vm_memory::GuestRegionMmap<vm_memory::bitmap::AtomicBitmap>;
type GuestMemoryMmap = vm_memory::GuestMemoryMmap<AtomicBitmap>;
type GuestRegionMmap = vm_memory::GuestRegionMmap<AtomicBitmap>;
/// Type for returning error code.
#[derive(Debug, Error)]
@@ -33,11 +36,13 @@ pub enum Error {
#[error("The memory map table extends past the end of guest memory")]
MemmapTablePastRamEnd,
#[error("Error writing memory map table to guest memory")]
MemmapTableSetup,
MemmapTableSetup(#[source] vm_memory::GuestMemoryError),
#[error("Error generating memory map table")]
MemmapTableGeneration,
#[error("The hvm_start_info structure extends past the end of guest memory")]
StartInfoPastRamEnd,
#[error("Error writing hvm_start_info to guest memory")]
StartInfoSetup,
StartInfoSetup(#[source] vm_memory::GuestMemoryError),
#[error("Failed to compute initramfs address")]
InitramfsAddress,
#[error("Error writing module entry to guest memory")]
@@ -53,6 +58,26 @@ pub enum Error {
/// Type for returning public functions outcome.
pub type Result<T> = result::Result<T, Error>;
// If the target_arch is x86_64 we import CpuProfile from the x86_64 module, otherwise we
// declare it here with only "host" as a selectable CPU profile. This trick is useful to prevent
// excessive conditional compilation throughout the codebase.
#[cfg(not(target_arch = "x86_64"))]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
/// A [`CpuProfile`] is a mechanism for ensuring live migration compatibility
/// between host's with potentially different CPU models.
pub enum CpuProfile {
#[default]
Host,
}
// Note that this trait impl is architecture agnostic and may thus reside here.
impl FromStr for CpuProfile {
type Err = value::Error;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
Self::deserialize(s.into_deserializer())
}
}
/// Type for memory region types.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum RegionType {
@@ -100,8 +125,9 @@ pub mod x86_64;
#[cfg(target_arch = "x86_64")]
pub use x86_64::{
_NSIG, CpuidConfig, CpuidFeatureEntry, EntryPoint, arch_memory_regions, configure_system,
configure_vcpu, generate_common_cpuid, generate_ram_ranges, get_host_cpu_phys_bits,
initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE, layout::CMDLINE_START, regs,
configure_vcpu, cpu_profile::CpuProfile, generate_common_cpuid, generate_ram_ranges,
get_host_cpu_phys_bits, initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE,
layout::CMDLINE_START, regs,
};
/// Safe wrapper for `sysconf(_SC_PAGESIZE)`.

View File

@@ -61,7 +61,7 @@ pub enum Error {
type Result<T> = result::Result<T, Error>;
/// Creates the flattened device tree for this riscv64 VM.
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
@@ -71,6 +71,7 @@ pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHash
aia_device: &Arc<Mutex<dyn Vaia>>,
initrd: &Option<InitramfsConfig>,
pci_space_info: &[PciSpaceInfo],
timebase_frequency: u32,
) -> FdtWriterResult<Vec<u8>> {
// Allocate stuff necessary for the holding the blob.
let mut fdt = FdtWriter::new()?;
@@ -86,7 +87,7 @@ pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHash
// Properties
fdt.property_u32("#address-cells", ADDRESS_CELLS)?;
fdt.property_u32("#size-cells", SIZE_CELLS)?;
create_cpu_nodes(&mut fdt, num_vcpu, isa_string)?;
create_cpu_nodes(&mut fdt, num_vcpu, isa_string, timebase_frequency)?;
create_memory_node(&mut fdt, guest_mem)?;
create_chosen_node(&mut fdt, cmdline, initrd)?;
create_aia_node(&mut fdt, aia_device)?;
@@ -110,14 +111,17 @@ pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Res
}
// Following are the auxiliary function for creating the different nodes that we append to our FDT.
fn create_cpu_nodes(fdt: &mut FdtWriter, num_cpus: u32, isa_string: &str) -> FdtWriterResult<()> {
fn create_cpu_nodes(
fdt: &mut FdtWriter,
num_cpus: u32,
isa_string: &str,
timebase_frequency: u32,
) -> FdtWriterResult<()> {
// See https://elixir.bootlin.com/linux/v6.10/source/Documentation/devicetree/bindings/riscv/cpus.yaml
let cpus = fdt.begin_node("cpus")?;
// As per documentation, on RISC-V 64-bit systems value should be set to 1.
fdt.property_u32("#address-cells", 0x01)?;
fdt.property_u32("#size-cells", 0x0)?;
// TODO: Retrieve CPU frequency from cpu timer regs
let timebase_frequency: u32 = 0x989680;
fdt.property_u32("timebase-frequency", timebase_frequency)?;
for cpu_index in 0..num_cpus {
@@ -231,8 +235,8 @@ fn create_aia_node(fdt: &mut FdtWriter, aia_device: &Arc<Mutex<dyn Vaia>>) -> Fd
fdt.property_u32("#interrupt-cells", 0u32)?;
fdt.property_null("interrupt-controller")?;
fdt.property_null("msi-controller")?;
// TODO complete num-ids
fdt.property_u32("riscv,num-ids", 2047u32)?;
let imsic_num_ids = aia_device.lock().unwrap().imsic_num_ids();
fdt.property_u32("riscv,num-ids", imsic_num_ids)?;
fdt.property_u32("phandle", AIA_IMSIC_PHANDLE)?;
let mut irq_cells = Vec::new();

View File

@@ -160,7 +160,6 @@ fn isa_string_from_host() -> Result<String, Error> {
}
/// Configures the system and should be called once per vm before starting vcpu threads.
#[allow(clippy::too_many_arguments)]
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
@@ -169,6 +168,7 @@ pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::Bui
initrd: &Option<super::InitramfsConfig>,
pci_space_info: &[PciSpaceInfo],
aia_device: &Arc<Mutex<dyn Vaia>>,
timebase_frequency: u32,
) -> super::Result<()> {
let isa_string = isa_string_from_host()?;
let fdt_final = fdt::create_fdt(
@@ -180,6 +180,7 @@ pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::Bui
aia_device,
initrd,
pci_space_info,
timebase_frequency,
)
.map_err(|_| Error::SetupFdt)?;

View File

@@ -7,7 +7,7 @@ use std::os::fd::AsFd;
use std::result;
use thiserror::Error;
use vm_memory::{GuestAddress, GuestMemory};
use vm_memory::{Bytes, GuestAddress, GuestMemory};
/// Errors thrown while loading UEFI binary
#[derive(Debug, Error)]

View File

@@ -0,0 +1,190 @@
// Copyright © 2026 Cyberus Technology GmbH
//
// SPDX-License-Identifier: Apache-2.0
//
//! This module contains types associated with adjusting CPUID entries according
//! to a selected CPU profile.
use std::ops::RangeInclusive;
use hypervisor::arch::x86::CpuIdEntry;
use log::error;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::x86_64::{CpuidReg, deserialize_u32_hex, serialize_u32_hex};
/// Parameters for inspecting CPUID definitions.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct CpuidParameters {
/// The leaf (EAX) parameter used with the CPUID instruction
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
pub leaf: u32,
/// The sub-leaf (ECX) parameter used with the CPUID instruction
#[serde(
serialize_with = "serialize_range_hex",
deserialize_with = "deserialize_range_hex"
)]
pub sub_leaf: RangeInclusive<u32>,
/// The register we are interested in inspecting which gets filled by the CPUID instruction
pub register: CpuidReg,
}
// Only used for (de-)serialization
#[derive(Debug, Serialize, Deserialize)]
struct ProvisionalRangeInclusive {
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
start: u32,
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
end: u32,
}
fn serialize_range_hex<S: serde::Serializer>(
input: &RangeInclusive<u32>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let provisional = ProvisionalRangeInclusive {
start: *input.start(),
end: *input.end(),
};
provisional.serialize(serializer)
}
fn deserialize_range_hex<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<RangeInclusive<u32>, D::Error> {
let ProvisionalRangeInclusive { start, end } =
ProvisionalRangeInclusive::deserialize(deserializer)?;
Ok(start..=end)
}
/// Used for adjusting an entire cpuid output register (EAX, EBX, ECX or EDX).
///
/// Instances of this struct typically adjust CPUID according to the following
/// formula: `cpuid_reg_value = (self.mask & cpuid_reg_value) | self.replacements`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CpuidOutputRegisterAdjustments {
/// Packs values to be placed into the given CPUID output register.
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
pub replacements: u32,
/// Used to zero out the area `replacements` occupy. This mask is not necessarily !replacements, as replacements
/// may pack values of different types that occupy varying ranges of bits.
///
/// Bit ranges within a CPUID output register that are **not** supposed to be replaced/overwritten should be set in
/// this mask.
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
pub mask: u32,
}
/// Error type indicating that expected CPUID entries could not be found.
///
/// This type does not record which entries could not be found as we do not
/// expect this to be actionable at runtime. Instead we encourage logging such
/// violations when and where they are detected.
#[derive(Debug, Error)]
#[error("Required CPUID entries not found")]
pub struct MissingCpuidEntriesError;
impl CpuidOutputRegisterAdjustments {
/// Adjust the given `cpuid_output_register` by retaining and replacing values according to `self`.
fn adjust(self, cpuid_output_register: &mut u32) {
*cpuid_output_register &= self.mask;
*cpuid_output_register |= self.replacements;
}
/// Adjust `cpuid` according to the given `adjustments`.
///
/// The returned vector of cpuid entries covers the same CPUID (sub-) leaves as the given `cpuid` input,
/// but values without matching [`CpuidParameters`] are zeroed out.
///
/// # Errors
///
/// An error is returned if an entry cannot be found for an adjustment describing non-zero replacements.
pub(super) fn adjust_cpuid_entries(
mut cpuid: Vec<CpuIdEntry>,
adjustments: &[(CpuidParameters, Self)],
) -> Result<Vec<CpuIdEntry>, MissingCpuidEntriesError> {
for entry in &mut cpuid {
for (reg, reg_value) in [
(CpuidReg::EAX, &mut entry.eax),
(CpuidReg::EBX, &mut entry.ebx),
(CpuidReg::ECX, &mut entry.ecx),
(CpuidReg::EDX, &mut entry.edx),
] {
// Lookup the adjustment corresponding to the entry's function/leaf and index/sub-leaf for each of the register.
let register_adjustments: Option<CpuidOutputRegisterAdjustments> =
adjustments.iter().find_map(|(param, adjustment)| {
((param.leaf == entry.function)
&& param.sub_leaf.contains(&entry.index)
&& (param.register == reg))
.then_some(*adjustment)
});
match register_adjustments {
Some(adjustment) => adjustment.adjust(reg_value),
None => {
// No matching cpuid parameters were found. We thus set the value of the register to 0.
*reg_value = 0;
}
}
}
}
Self::expected_entries_found(&cpuid, adjustments)?;
Ok(cpuid)
}
/// Check that we found every value that was supposed to be replaced with something else than 0
///
/// IMPORTANT: This function assumes that the given `cpuid` has already been adjusted with the
/// provided `adjustments`.
fn expected_entries_found(
cpuid: &[CpuIdEntry],
adjustments: &[(CpuidParameters, Self)],
) -> Result<(), MissingCpuidEntriesError> {
let mut missing_entry = false;
for (param, adjustment) in adjustments {
if adjustment.replacements == 0 {
continue;
}
if !cpuid.iter().any(|entry| {
(entry.function == param.leaf) && (param.sub_leaf.contains(&entry.index))
}) {
error!(
"cannot adjust CPU profile. No entry found matching the required parameters: {param:?}"
);
missing_entry = true;
}
}
if missing_entry {
Err(MissingCpuidEntriesError)
} else {
Ok(())
}
}
}
/// Data describing CPUID adjustments related to a CPU Profile.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuidProfileData {
/// Adjustments necessary to become compatible with the desired target.
pub adjustments: Vec<(CpuidParameters, CpuidOutputRegisterAdjustments)>,
}

View File

@@ -0,0 +1,880 @@
// Copyright © 2026 Cyberus Technology GmbH
//
// SPDX-License-Identifier: Apache-2.0
//
use hypervisor::CpuVendor;
use hypervisor::arch::x86::CpuIdEntry;
use crate::x86_64::cpu_profile::cpuid_adjustments::{
CpuidOutputRegisterAdjustments, CpuidProfileData, MissingCpuidEntriesError,
};
use crate::x86_64::{AMX_TILECFG_BIT, AMX_TILEDATA_BIT, CpuidReg};
/// Mask indicating availability of the AMX TILECFG state component
const TILECFG_MASK: u32 = 1_u32 << AMX_TILECFG_BIT;
/// Mask indicating availability of the AMX TILEDATA state component
const TILEDATA_MASK: u32 = 1_u32 << AMX_TILEDATA_BIT;
pub mod cpuid_adjustments;
// TODO: Auto generate the CpuProfile enum with a build script once we introduce user facing CPU profiles.
/// A [`CpuProfile`] is a mechanism for ensuring live migration compatibility
/// between hosts with potentially different CPU models.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum CpuProfile {
#[default]
Host,
}
impl CpuProfile {
/// Adjust `cpuid` to the chosen CPU profile.
///
/// The CPUID data obtained from the hypervisor is thus downgraded to the selected profile.
///
/// This method does **not** perform any compatibility checks beyond
/// ensuring that all expected (sub) leaves required by the CPU profile are present.
///
/// The caller is responsible for ensuring compatibility of `cpuid` by the time it is
/// utilized.
///
/// If Intel AMX is not desired, then passing `amx = false` will permit missing (sub)-leaves
/// that are **purely AMX related**.
///
/// The Host profile guarantees that `cpuid` is returned without any modifications.
pub(in crate::x86_64) fn adjust_cpuid(
&self,
cpuid: Vec<CpuIdEntry>,
amx: bool,
cpu_vendor: CpuVendor,
) -> Result<Vec<CpuIdEntry>, MissingCpuidEntriesError> {
let Some(cpuid_profile_data) = self.cpuid_data() else {
return Ok(cpuid);
};
adjust_cpuid(cpuid_profile_data, cpuid, amx, cpu_vendor)
}
/// Obtain CPUID adjustment data related to the CPU profile.
fn cpuid_data(&self) -> Option<CpuidProfileData> {
// TODO: Auto generate this through a build script once
// we introduce actual CPU profiles.
match self {
CpuProfile::Host => None,
}
}
}
/// See [`CpuProfile::adjust_cpuid`](CpuProfile::adjust_cpuid)
fn adjust_cpuid(
CpuidProfileData { mut adjustments }: CpuidProfileData,
cpuid: Vec<CpuIdEntry>,
amx: bool,
cpu_vendor: CpuVendor,
) -> Result<Vec<CpuIdEntry>, MissingCpuidEntriesError> {
if (!amx) && matches!(cpu_vendor, CpuVendor::Intel) {
let amx_tilecfg_leaf = u32::from(AMX_TILECFG_BIT);
let amx_tiledata_leaf = u32::from(AMX_TILEDATA_BIT);
// In this case we invalidate tile state components and zero out all other purely AMX related leaves
// in order to maximize our chances of finding all required (sub) leaves.
for adj in adjustments.iter_mut() {
if adj.0.sub_leaf.start() != adj.0.sub_leaf.end() {
continue;
}
let sub_leaf = *adj.0.sub_leaf.start();
let leaf = adj.0.leaf;
if (leaf == 0xd) && (sub_leaf == 0) && (adj.0.register == CpuidReg::EAX) {
adj.1.mask &= !(TILECFG_MASK | TILEDATA_MASK);
adj.1.replacements &= !(TILECFG_MASK | TILEDATA_MASK);
}
if (leaf == 0xd) && (sub_leaf == 1) && (adj.0.register == CpuidReg::ECX) {
adj.1.mask &= !(TILECFG_MASK | TILEDATA_MASK);
adj.1.replacements &= !(TILECFG_MASK | TILEDATA_MASK);
}
if (leaf == 0xd) && ((sub_leaf == amx_tilecfg_leaf) || (sub_leaf == amx_tiledata_leaf))
{
adj.1.mask = 0;
adj.1.replacements = 0;
}
// Tile Information (purely AMX related).
if leaf == 0x1d {
adj.1.mask = 0;
adj.1.replacements = 0;
}
// TMUL information (purely AMX related)
if leaf == 0x1e {
adj.1.mask = 0;
adj.1.replacements = 0;
}
}
}
CpuidOutputRegisterAdjustments::adjust_cpuid_entries(cpuid, &adjustments)
}
#[cfg(test)]
mod unit_tests {
use proptest::prelude::*;
use super::{CpuIdEntry, CpuVendor, CpuidProfileData, CpuidReg, adjust_cpuid};
use crate::x86_64::cpu_profile::cpuid_adjustments::{
CpuidOutputRegisterAdjustments, CpuidParameters,
};
use crate::x86_64::cpu_profile::{TILECFG_MASK, TILEDATA_MASK};
// Note that the tests for adjust_cpuid within this module tend to use much simpler inputs
// than what it will be called with at runtime within Cloud hypervisor. We do this here in order
// to keep each test focused on the behavioral aspect under test.
/// Helper function that returns adjustments tied to purely AMX related leaves.
fn amx_related_adjustments() -> Vec<(CpuidParameters, CpuidOutputRegisterAdjustments)> {
let amx_adjustments_json = r#"
[
[
{
"leaf": "0xd",
"sub_leaf": {
"start": "0x11",
"end": "0x11"
},
"register": "EAX"
},
{
"replacements": "0x40",
"mask": "0x0"
}
],
[
{
"leaf": "0xd",
"sub_leaf": {
"start": "0x11",
"end": "0x11"
},
"register": "EBX"
},
{
"replacements": "0xac0",
"mask": "0x0"
}
],
[
{
"leaf": "0xd",
"sub_leaf": {
"start": "0x11",
"end": "0x11"
},
"register": "ECX"
},
{
"replacements": "0x2",
"mask": "0x0"
}
],
[
{
"leaf": "0xd",
"sub_leaf": {
"start": "0x12",
"end": "0x12"
},
"register": "EAX"
},
{
"replacements": "0x2000",
"mask": "0x0"
}
],
[
{
"leaf": "0xd",
"sub_leaf": {
"start": "0x12",
"end": "0x12"
},
"register": "EBX"
},
{
"replacements": "0xb00",
"mask": "0x0"
}
],
[
{
"leaf": "0xd",
"sub_leaf": {
"start": "0x12",
"end": "0x12"
},
"register": "ECX"
},
{
"replacements": "0x6",
"mask": "0x0"
}
],
[
{
"leaf": "0x1d",
"sub_leaf": {
"start": "0x0",
"end": "0x0"
},
"register": "EAX"
},
{
"replacements": "0x1",
"mask": "0x0"
}
],
[
{
"leaf": "0x1d",
"sub_leaf": {
"start": "0x1",
"end": "0x1"
},
"register": "EAX"
},
{
"replacements": "0x4002000",
"mask": "0x0"
}
],
[
{
"leaf": "0x1d",
"sub_leaf": {
"start": "0x1",
"end": "0x1"
},
"register": "EBX"
},
{
"replacements": "0x80040",
"mask": "0x0"
}
],
[
{
"leaf": "0x1d",
"sub_leaf": {
"start": "0x1",
"end": "0x1"
},
"register": "ECX"
},
{
"replacements": "0x10",
"mask": "0x0"
}
],
[
{
"leaf": "0x1e",
"sub_leaf": {
"start": "0x0",
"end": "0x0"
},
"register": "EAX"
},
{
"replacements": "0x0",
"mask": "0x0"
}
],
[
{
"leaf": "0x1e",
"sub_leaf": {
"start": "0x0",
"end": "0x0"
},
"register": "EBX"
},
{
"replacements": "0x4010",
"mask": "0x0"
}
]
]"#;
serde_json::from_str(amx_adjustments_json).unwrap()
}
// Randonly generate three CPUID entries and construct some simple adjustments which we apply
// through the `adjust_cpuid` function and assert that our expectations are met.
proptest! {
#[test]
fn adjust_cpuid_simple_adjustments(
leaf0 in any::<u32>(),
leaf1 in any::<u32>(),
leaf2 in any::<u32>(),
a in any::<u32>(),
b in any::<u32>(),
c in any::<u32>(),
d in any::<u32>(),
) {
// Ensure that we have distinct leaves in this test
let mut leaves = [leaf0, leaf1, leaf2];
leaves.sort_unstable();
for (l, i) in leaves.iter_mut().zip([0, 1, 2]) {
*l = l.wrapping_add(i);
}
let [leaf0, leaf1, leaf2] = leaves;
// The following leaves have some special handling that we test in later
// more specialized tests. In this simple test we want to avoid them.
let leaves_with_special_handling = { [0xd, 0x1d, 0x1e] };
let transform_leaf = |leaf: u32| {
if leaves_with_special_handling.contains(&leaf) {
// Ensures that we get a leaf different from any of the leaves that have special handling
leaf | 0x1000
} else {
leaf
}
};
let leaf0 = transform_leaf(leaf0);
let leaf1 = transform_leaf(leaf1);
let leaf2 = transform_leaf(leaf2);
// The leaves should still be distinct
assert!(leaf0 != leaf1);
assert!(leaf0 != leaf2);
assert!(leaf1 != leaf2);
// We have now setup leaves to be used in this test
// Let's now construct some simple adjustments
// mask retaining bits 0,1,2 and 3
let first_four_bits_mask = 15;
// Retain the first four bits of the register and overwrite the remaining bits with the value "42"
let adjustment_u = CpuidOutputRegisterAdjustments {
mask: first_four_bits_mask,
replacements: 42 << 4,
};
let assert_adjustment_u = |new_value: u32, old_value: u32| {
assert_eq!(new_value & first_four_bits_mask, (old_value & first_four_bits_mask));
// Recall that we placed the value 42 into bits 31:4
assert_eq!(new_value >> 4, 42);
};
// Set bits 0 and 28 and zero out the rest
let adjustment_v = CpuidOutputRegisterAdjustments {
replacements: 1 | (1 << 28),
mask: 0
};
let assert_adjustment_v = |new_value: u32| {
assert_eq!(new_value, 1 | (1 << 28));
};
// Make adjustment_u apply to EAX of leaf0 and EBX of leaf1.
//
// Make adjustment_v apply to EDX of leaf0 and ECX of leaf1.
//
// We do not specify any adjustment for leaf2.
let cpuid_profile_data = CpuidProfileData {
adjustments: vec![
(
CpuidParameters {
leaf: leaf0,
sub_leaf: 0..=0,
register: CpuidReg::EAX,
},
adjustment_u,
),
(
CpuidParameters {
leaf: leaf0,
sub_leaf: 0..=0,
register: CpuidReg::EDX,
},
adjustment_v,
),
(
CpuidParameters {
leaf: leaf1,
sub_leaf: 0..=0,
register: CpuidReg::EBX,
},
adjustment_u,
),
(
CpuidParameters {
leaf: leaf1,
sub_leaf: 0..=0,
register: CpuidReg::ECX,
},
adjustment_v,
),
],
};
// Construct cpuid entries consisting of leaves leaf0, leaf1 and leaf2.
// The registers eax, ebx, ecx, edx are populated with the randomly generated values `a`, `b`, `c` and `d`
// and we do not consider sub-leaves in this test.
let cpuid = vec![
CpuIdEntry {
function: leaf0,
index: 0,
flags: 0,
eax: a,
ebx: b,
ecx: c,
edx: d,
},
CpuIdEntry {
function: leaf1,
index: 0,
flags: 0,
eax: a,
ebx: b,
ecx: c,
edx: d,
},
CpuIdEntry {
function: leaf2,
index: 0,
flags: 0,
eax: a,
ebx: b,
ecx: c,
edx: d,
},
];
// Check that the output of `adjust_cpuid` contains the same CPUID leaves as the
// `cpuid` vector we started with.
let expected_num_entries = cpuid.len();
let mut found_entry_count = 0;
let adjusted_cpuid =
adjust_cpuid(cpuid_profile_data, cpuid, false, CpuVendor::Intel).unwrap();
// Iterate through our adjusted entries and assert that our expectations are met.
for entry in adjusted_cpuid {
let CpuIdEntry {
function,
index,
flags,
eax,
ebx,
ecx,
edx,
} = entry;
if function == leaf0 {
found_entry_count += 1;
assert_adjustment_u(eax, a);
assert_adjustment_v(edx);
// ebx and ecx should be zeroed out
assert_eq!(ebx, 0);
assert_eq!(ecx, 0);
}
if function == leaf1 {
found_entry_count += 1;
assert_adjustment_u(ebx, b);
assert_adjustment_v(ecx);
// eax and edx should be zeroed out
assert_eq!(eax, 0);
assert_eq!(edx, 0);
}
if function == leaf2 {
found_entry_count += 1;
// All registers should be zeroed out
assert_eq!(eax, ebx);
assert_eq!(ebx, ecx);
assert_eq!(ecx, edx);
assert_eq!(edx, 0);
}
// Index and flags should not be altered. Since these were both
// always 0 for all leaves in the original `cpuid` that should
// remain the case.
assert_eq!(index, 0);
assert_eq!(flags, 0);
}
assert_eq!(expected_num_entries, found_entry_count);
}
}
// Check that adjust_cpuid follows the prescribed adjustments on
// specified subleaf ranges
#[test]
fn adjust_cpuid_works_with_subleaf_ranges() {
// As in the real runtime case the Topology enumeration leaves should not be altered
// by CPU profiles. In this test we thus define adjustment's that do not alter non-reserved
// bits for the 0x1f leaf and its sub-leaves.
let adjustments_json = r#"
[
[
{
"leaf": "0x1f",
"sub_leaf": {
"start": "0x0",
"end": "0xffffffff"
},
"register": "EAX"
},
{
"replacements": "0x0",
"mask": "0x1f"
}
],
[
{
"leaf": "0x1f",
"sub_leaf": {
"start": "0x0",
"end": "0xffffffff"
},
"register": "EBX"
},
{
"replacements": "0x0",
"mask": "0xffff"
}
],
[
{
"leaf": "0x1f",
"sub_leaf": {
"start": "0x0",
"end": "0xffffffff"
},
"register": "ECX"
},
{
"replacements": "0x0",
"mask": "0xffff"
}
],
[
{
"leaf": "0x1f",
"sub_leaf": {
"start": "0x0",
"end": "0xffffffff"
},
"register": "EDX"
},
{
"replacements": "0x0",
"mask": "0xffffffff"
}
]
]"#;
let cpuid_profile_data = CpuidProfileData {
adjustments: serde_json::from_str(adjustments_json).unwrap(),
};
let cpuid = vec![
CpuIdEntry {
function: 0x1f,
index: 0,
flags: 1,
eax: 0x00000001,
ebx: 0x00000002,
ecx: 0x00000100,
edx: 0x00000000,
},
CpuIdEntry {
function: 0x1f,
index: 1,
flags: 1,
eax: 0x00000004,
ebx: 0x00000008,
ecx: 0x00000201,
edx: 0x00000006,
},
];
let adjusted_cpuid =
adjust_cpuid(cpuid_profile_data, cpuid.clone(), false, CpuVendor::Intel).unwrap();
// Check that cpuid was indeed not altered
for entry in cpuid {
let adjusted_entry = adjusted_cpuid
.iter()
.find(|e| (e.function == entry.function) && (e.index == entry.index))
.unwrap();
assert_eq!(entry.eax, adjusted_entry.eax);
assert_eq!(entry.ebx, adjusted_entry.ebx);
assert_eq!(entry.ecx, adjusted_entry.ecx);
assert_eq!(entry.edx, adjusted_entry.edx);
}
}
#[test]
fn adjust_cpuid_fails_on_missing_entries() {
let cpuid = vec![CpuIdEntry {
function: 0x0,
index: 0x0,
eax: 0x20,
ebx: 0x756e6547,
ecx: 0x6c65746e,
edx: 0x49656e69,
flags: 0,
}];
let cpuid_profile_data = CpuidProfileData {
adjustments: vec![(
CpuidParameters {
leaf: 0x1,
sub_leaf: 0x0..=0x0,
register: CpuidReg::EAX,
},
CpuidOutputRegisterAdjustments {
replacements: 0x000806f8,
mask: 0,
},
)],
};
let _ =
adjust_cpuid(cpuid_profile_data, cpuid.clone(), false, CpuVendor::Intel).unwrap_err();
// Also check this for a purely AMX related leaves which has special handling
let _ = adjust_cpuid(
CpuidProfileData {
adjustments: amx_related_adjustments(),
},
cpuid,
true,
CpuVendor::Intel,
)
.unwrap_err();
}
// Check that if `amx = false` then AMX related leaves are zeroed out
#[test]
fn adjust_cpuid_no_amx_zeros_amx_leaves() {
// Not AMX related
let leaf_zero = CpuIdEntry {
function: 0x0,
index: 0x0,
eax: 0x20,
ebx: 0x756e6547,
ecx: 0x6c65746e,
edx: 0x49656e69,
flags: 0,
};
let cpuid = vec![
leaf_zero,
// State components base leaf extracted from granite rapids the AMX related state component bits are set
CpuIdEntry {
function: 0xd,
index: 0x0,
flags: 1,
eax: 0x000602e7,
ebx: 0x00002b00,
ecx: 0x00002b00,
edx: 0x00000000,
},
// TILECFG state
CpuIdEntry {
function: 0xd,
index: 0x11,
flags: 1,
eax: 0x00000040,
ebx: 0x00000ac0,
ecx: 0x00000002,
edx: 0x00000000,
},
// TILEDATA state
CpuIdEntry {
function: 0xd,
index: 0x12,
flags: 1,
eax: 0x00002000,
ebx: 0x00000b00,
ecx: 0x00000006,
edx: 0x00000000,
},
// Tile information base leaf
CpuIdEntry {
function: 0x1d,
index: 0x0,
flags: 1,
eax: 0x00000001,
ebx: 0x00000000,
ecx: 0x00000000,
edx: 0x00000000,
},
// Tile Palette 1
CpuIdEntry {
function: 0x1d,
index: 0x1,
flags: 1,
eax: 0x04002000,
ebx: 0x00080040,
ecx: 0x00000010,
edx: 0x00000000,
},
// TMUL information base leaf
CpuIdEntry {
function: 0x1e,
index: 0x0,
flags: 1,
eax: 0x00000000,
ebx: 0x00004010,
ecx: 0x00000000,
edx: 0x00000000,
},
];
let adjustments: Vec<(CpuidParameters, CpuidOutputRegisterAdjustments)> =
amx_related_adjustments()
.into_iter()
// leave leaf 0 untouched
.chain([
(
CpuidParameters {
leaf: 0x0,
sub_leaf: 0x0..=0x0,
register: CpuidReg::EAX,
},
CpuidOutputRegisterAdjustments {
replacements: 0,
mask: u32::MAX,
},
),
(
CpuidParameters {
leaf: 0x0,
sub_leaf: 0x0..=0x0,
register: CpuidReg::EBX,
},
CpuidOutputRegisterAdjustments {
replacements: 0,
mask: u32::MAX,
},
),
(
CpuidParameters {
leaf: 0x0,
sub_leaf: 0x0..=0x0,
register: CpuidReg::ECX,
},
CpuidOutputRegisterAdjustments {
replacements: 0,
mask: u32::MAX,
},
),
(
CpuidParameters {
leaf: 0x0,
sub_leaf: 0x0..=0x0,
register: CpuidReg::EDX,
},
CpuidOutputRegisterAdjustments {
replacements: 0,
mask: u32::MAX,
},
),
])
// Keep EAX of leaf 0xd so we see that the AMX-related state component bits get unset, regardless of what
// the adjustment says
.chain([(
CpuidParameters {
leaf: 0xd,
sub_leaf: 0x0..=0x0,
register: CpuidReg::EAX,
},
CpuidOutputRegisterAdjustments {
mask: u32::MAX,
replacements: 0,
},
)])
.collect();
let adjusted_cpuid = adjust_cpuid(
CpuidProfileData { adjustments },
cpuid.clone(),
false,
CpuVendor::Intel,
)
.unwrap();
// Check that leaf zero is left untouched as expected
{
let adjusted_leaf_zero = adjusted_cpuid
.iter()
.find(|entry| entry.function == 0x0)
.unwrap();
assert_eq!(adjusted_leaf_zero.eax, leaf_zero.eax);
assert_eq!(adjusted_leaf_zero.ebx, leaf_zero.ebx);
assert_eq!(adjusted_leaf_zero.ecx, leaf_zero.ecx);
assert_eq!(adjusted_leaf_zero.edx, leaf_zero.edx);
}
// Check that the TILECFG and TILEDATA state bits are now zeroed ut
{
let state_cmp_base_leaf = adjusted_cpuid
.iter()
.find(|entry| (entry.function == 0xd) && (entry.index == 0x0))
.unwrap();
// EAX should not have been zeroed out in its entirety
assert!(state_cmp_base_leaf.eax != 0);
// The TILECFG state bit should be unset
assert_eq!(state_cmp_base_leaf.eax & TILECFG_MASK, 0);
// The TILEDATA state bit should be unset
assert_eq!(state_cmp_base_leaf.eax & TILEDATA_MASK, 0);
}
// Since all remaining entries we placed in `cpuid` are purely AMX related we now
// expect them to be zeroed out
for entry in adjusted_cpuid {
if entry.function == 0 || (entry.function == 0xd && entry.index == 0x0) {
continue;
}
assert_eq!(entry.eax, 0);
assert_eq!(entry.ebx, 0);
assert_eq!(entry.ecx, 0);
assert_eq!(entry.edx, 0);
}
}
// Check that if `amx = false` then missing purely AMX related leaves
// do not lead to failure
#[test]
fn adjust_cpuid_no_amx_missing_amx_leaves_accepted() {
let cpuid = vec![CpuIdEntry {
function: 0x0,
index: 0x0,
eax: 0x20,
ebx: 0x756e6547,
ecx: 0x6c65746e,
edx: 0x49656e69,
flags: 0,
}];
let _ = adjust_cpuid(
CpuidProfileData {
adjustments: amx_related_adjustments(),
},
cpuid,
false,
CpuVendor::Intel,
)
.unwrap();
}
}

View File

@@ -0,0 +1,82 @@
// Copyright © 2026 Cyberus Technology GmbH
//
// SPDX-License-Identifier: Apache-2.0
//
use std::result;
use serde::{Deserialize, Deserializer, Serializer, de};
/// Serializes the given `input` as a hex string (starting with "0x").
///
/// As an example if `input:=5` then this function will feed the given
/// `serializer` the string "0x5".
pub(crate) fn serialize_u32_hex<S: Serializer>(
input: &u32,
serializer: S,
) -> result::Result<S::Ok, S::Error> {
serializer.serialize_str(&format!("{input:#x}"))
}
/// Deserializes a u32 from a hex string representation.
pub(crate) fn deserialize_u32_hex<'de, D: Deserializer<'de>>(
deserializer: D,
) -> result::Result<u32, D::Error> {
let hex: &str = <&str>::deserialize(deserializer)?;
u32::from_str_radix(hex.strip_prefix("0x").unwrap_or(""), 16).map_err(|_| {
<D::Error as de::Error>::custom(format!("{hex} is not a hex encoded 32 bit integer"))
})
}
#[cfg(test)]
mod unit_tests {
use proptest::prelude::*;
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
struct TestStruct {
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
foo: u32,
#[serde(
serialize_with = "serialize_u32_hex",
deserialize_with = "deserialize_u32_hex"
)]
bar: u32,
}
// Check that our hex serializers satisfy the two following invariants
// 1. Serialization followed by deserialization is the identity.
// 2. Values of type u32 are serialized to strings starting with "0x" and then
// a sub-string where all characters are ascii hex digits (with the letters [a-f] always in lowercase).
proptest! {
#[test]
fn hex_serialization_works(foo in any::<u32>(), bar in any::<u32>()) {
let t = TestStruct { foo , bar };
let t_string = serde_json::to_string(&t).unwrap();
let t_deserialized = serde_json::from_str(&t_string).unwrap();
prop_assert_eq!(t, t_deserialized);
let t_json = serde_json::to_value(t).unwrap();
let check_str_invariants = |value: &str| {
prop_assert!(value.starts_with("0x"));
prop_assert!(value.as_bytes()[2..].iter().all(u8::is_ascii_hexdigit));
prop_assert!(!value.as_bytes()[2..].iter().any(u8::is_ascii_uppercase));
Ok(())
};
let foo_str = t_json.get("foo").unwrap().as_str().unwrap();
let bar_str = t_json.get("bar").unwrap().as_str().unwrap();
check_str_invariants(foo_str)?;
check_str_invariants(bar_str)?;
}
}
}

View File

@@ -7,6 +7,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
pub mod cpu_profile;
pub mod interrupts;
pub mod layout;
pub mod regs;
@@ -14,13 +15,14 @@ pub mod regs;
#[cfg(feature = "tdx")]
pub mod tdx;
mod helpers;
mod mpspec;
mod mptable;
mod smbios;
use std::arch::x86_64;
use std::mem;
use helpers::{deserialize_u32_hex, serialize_u32_hex};
use hypervisor::arch::x86::{CPUID_FLAG_VALID_INDEX, CpuIdEntry};
use hypervisor::{CpuVendor, HypervisorCpuError, HypervisorError};
use linux_loader::loader::bootparam::{boot_params, setup_header};
@@ -28,13 +30,16 @@ use linux_loader::loader::elf::start_info::{
hvm_memmap_table_entry, hvm_modlist_entry, hvm_start_info,
};
use log::{debug, error, info};
pub use smbios::{SmbiosChassisConfig, SmbiosConfig, SmbiosSystem};
use thiserror::Error;
use vm_memory::{
Address, Bytes, GuestAddress, GuestAddressSpace, GuestMemory, GuestMemoryAtomic,
GuestMemoryRegion,
};
use vmm_sys_util::fam;
use crate::{GuestMemoryMmap, InitramfsConfig, RegionType};
use crate::x86_64::cpu_profile::cpuid_adjustments::MissingCpuidEntriesError;
use crate::{CpuProfile, GuestMemoryMmap, InitramfsConfig, RegionType};
// While modern architectures support more than 255 CPUs via x2APIC,
// legacy devices such as mptable support at most 254 CPUs.
@@ -55,6 +60,9 @@ const AMX_INT8: u8 = 25; // AMX tile computation on 8-bit integers
const AMX_FP16: u8 = 21; // AMX tile computation on fp16 numbers
const AMX_COMPLEX: u8 = 8; // AMX tile computation on complex numbers
const AMX_TILECFG_BIT: u8 = 17; // AMX tile cfg state component bit
const AMX_TILEDATA_BIT: u8 = 18; // AMX tile data state component bit
// KVM feature bits
#[cfg(feature = "tdx")]
const KVM_FEATURE_CLOCKSOURCE_BIT: u8 = 0;
@@ -93,6 +101,7 @@ pub struct CpuidConfig {
#[cfg(feature = "tdx")]
pub tdx: bool,
pub amx: bool,
pub profile: CpuProfile,
}
#[derive(Debug, Error)]
@@ -135,16 +144,32 @@ pub enum Error {
/// Error populating CPUID with KVM HyperV emulation details
#[error("Error populating CPUID with KVM HyperV emulation details")]
CpuidKvmHyperV(#[source] vmm_sys_util::fam::Error),
CpuidKvmHyperV(#[source] fam::Error),
/// Error populating CPUID with CPU identification
#[error("Error populating CPUID with CPU identification")]
CpuidIdentification(#[source] vmm_sys_util::fam::Error),
CpuidIdentification(#[source] fam::Error),
/// Error checking CPUID compatibility
#[error("Error checking CPUID compatibility")]
CpuidCheckCompatibility,
/// Error checking if CPUID is compatible with profile
#[error(
"The selected CPU profile cannot be utilized because the host's CPUID entries are not compatible with the profile"
)]
CpuProfileCpuidIncompatibility,
/// Error because TDX cannot be enabled when a custom (non host) CPU profile has been selected
#[error("TDX cannot be enabled when a custom CPU profile has been selected")]
CpuProfileTdxIncompatibility,
/// Error when trying to apply a CPU profile because a necessary CPUID entry was not found
#[error(
"The selected CPU profile cannot be utilized because a necessary CPUID entry was not found"
)]
MissingExpectedCpuidEntry(#[source] MissingCpuidEntriesError),
// Error writing EBDA address
#[error("Error writing EBDA address")]
EbdaSetup(#[source] vm_memory::GuestMemoryError),
@@ -190,7 +215,7 @@ pub fn get_max_x2apic_id(topology: (u16, u16, u16, u16)) -> u32 {
)
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum CpuidReg {
EAX,
EBX,
@@ -502,11 +527,27 @@ impl CpuidFeatureEntry {
features
}
// The function returns `Error` (a.k.a. "incompatible"), when the CPUID features from `src_vm_cpuid`
// is not a subset of those of the `dest_vm_cpuid`.
/// The function returns `Error` (a.k.a. "incompatible"), when the CPUID features from `src_vm_cpuid`
/// is not a subset of those of the `dest_vm_cpuid`.
pub fn check_cpuid_compatibility(
src_vm_cpuid: &[CpuIdEntry],
dest_vm_cpuid: &[CpuIdEntry],
) -> Result<(), Error> {
Self::check_cpuid_compatibility_with_descriptions(
src_vm_cpuid,
"source VM",
dest_vm_cpuid,
"destination VM",
)
}
/// Similar to `check_cpuid_compatibility`, but with the possibility to change
/// the description of the source and destination for logging purposes.
fn check_cpuid_compatibility_with_descriptions(
src_vm_cpuid: &[CpuIdEntry],
src_description: &str,
dest_vm_cpuid: &[CpuIdEntry],
dest_description: &str,
) -> Result<(), Error> {
let feature_entry_list = &Self::checked_feature_entry_list();
let src_vm_features = Self::get_features_from_cpuid(src_vm_cpuid, feature_entry_list);
@@ -532,8 +573,8 @@ impl CpuidFeatureEntry {
};
if !entry_compatible {
error!(
"Detected incompatible CPUID entry: leaf={:#02x} (subleaf={:#02x}), register='{:?}', \
compatible_check='{:?}', source VM feature='{:#04x}', destination VM feature'{:#04x}'.",
"Detected incompatible CPUID entry: leaf={:#04x} (subleaf={:#04x}), register='{:?}', \
compatible_check='{:?}', {src_description} feature='{:#04x}', {dest_description} feature='{:#04x}'.",
entry.function,
entry.index,
entry.feature_reg,
@@ -555,6 +596,15 @@ impl CpuidFeatureEntry {
}
}
/// Generate the CPUID entries intended for every vCPU.
///
/// ## CPU profiles
///
/// This function takes the CPU profile given in `config` into account and returns compatible CPUID entries
/// if possible.
///
/// An error is returned when the CPUID entries obtained from the hypervisor do not satisfy the requirements
/// to apply the selected CPU profile.
pub fn generate_common_cpuid(
hypervisor: &dyn hypervisor::Hypervisor,
config: &CpuidConfig,
@@ -577,9 +627,86 @@ pub fn generate_common_cpuid(
}
info!(
"Generating guest CPUID for with physical address size: {}",
"Generating guest CPUID with physical address size: {}",
config.phys_bits
);
// Supported CPUID
let mut cpuid = hypervisor
.get_supported_cpuid()
.map_err(Error::CpuidGetSupported)?;
let is_non_host_profile = !matches!(config.profile, CpuProfile::Host);
#[cfg(feature = "tdx")]
if config.tdx {
if is_non_host_profile {
// TDX is not supported by CPU profiles other than host for the time being.
return Err(Error::CpuProfileTdxIncompatibility.into());
}
common_cpuid_tdx_configuration(&mut cpuid, hypervisor)?;
}
// Copy CPU identification string
for i in 0x8000_0002..=0x8000_0004 {
cpuid.retain(|c| c.function != i);
// SAFETY: call cpuid with valid leaves
#[allow(unused_unsafe)]
let leaf = unsafe { x86_64::__cpuid(i) };
cpuid.push(CpuIdEntry {
function: i,
eax: leaf.eax,
ebx: leaf.ebx,
ecx: leaf.ecx,
edx: leaf.edx,
..Default::default()
});
}
let cpuid_profile = if is_non_host_profile {
let cpuid_profile = config
.profile
.adjust_cpuid(cpuid.clone(), config.amx, hypervisor.get_cpu_vendor())
.map_err(Error::MissingExpectedCpuidEntry)?;
required_common_cpuid_updates(
cpuid_profile,
config,
#[cfg(feature = "kvm")]
hypervisor.hypervisor_type(),
)
} else {
Vec::new()
};
let cpuid_host = required_common_cpuid_updates(
cpuid,
config,
#[cfg(feature = "kvm")]
hypervisor.hypervisor_type(),
);
// If we want to apply a CPU profile we need to check that it remains compatible with `cpuid_host`
if is_non_host_profile {
CpuidFeatureEntry::check_cpuid_compatibility_with_descriptions(
&cpuid_profile,
"CPU Profile",
&cpuid_host,
"Host VM",
)
.map_err(|_| Error::CpuProfileCpuidIncompatibility)?;
Ok(cpuid_profile)
} else {
Ok(cpuid_host)
}
}
/// Apply updates to common CPUID (not vCPU specific) that are necessary regardless of
/// the chosen CPU profile.
fn required_common_cpuid_updates(
mut cpuid: Vec<CpuIdEntry>,
config: &CpuidConfig,
#[cfg(feature = "kvm")] hypervisor_type: hypervisor::HypervisorType,
) -> Vec<CpuIdEntry> {
#[allow(unused_mut)]
let mut cpuid_patches = vec![
// Patch hypervisor bit
@@ -605,10 +732,7 @@ pub fn generate_common_cpuid(
];
#[cfg(feature = "kvm")]
if matches!(
hypervisor.hypervisor_type(),
hypervisor::HypervisorType::Kvm
) {
if matches!(hypervisor_type, hypervisor::HypervisorType::Kvm) {
// Patch tsc deadline timer bit
cpuid_patches.push(CpuidPatch {
function: 1,
@@ -621,24 +745,8 @@ pub fn generate_common_cpuid(
});
}
// Supported CPUID
let mut cpuid = hypervisor
.get_supported_cpuid()
.map_err(Error::CpuidGetSupported)?;
CpuidPatch::patch_cpuid(&mut cpuid, &cpuid_patches);
#[cfg(feature = "tdx")]
let tdx_capabilities = if config.tdx {
let caps = hypervisor
.tdx_capabilities()
.map_err(Error::TdxCapabilities)?;
info!("TDX capabilities {caps:#?}");
Some(caps)
} else {
None
};
// Update some existing CPUID
for entry in cpuid.as_mut_slice().iter_mut() {
#[allow(unused_unsafe)]
@@ -653,25 +761,6 @@ pub fn generate_common_cpuid(
entry.edx &= !(1 << AMX_COMPLEX);
}
}
0xd =>
{
#[cfg(feature = "tdx")]
if let Some(caps) = &tdx_capabilities {
let xcr0_mask: u64 = 0x82ff;
let xss_mask: u64 = !xcr0_mask;
if entry.index == 0 {
entry.eax &= (caps.xfam_fixed0 as u32) & (xcr0_mask as u32);
entry.eax |= (caps.xfam_fixed1 as u32) & (xcr0_mask as u32);
entry.edx &= ((caps.xfam_fixed0 & xcr0_mask) >> 32) as u32;
entry.edx |= ((caps.xfam_fixed1 & xcr0_mask) >> 32) as u32;
} else if entry.index == 1 {
entry.ecx &= (caps.xfam_fixed0 as u32) & (xss_mask as u32);
entry.ecx |= (caps.xfam_fixed1 as u32) & (xss_mask as u32);
entry.edx &= ((caps.xfam_fixed0 & xss_mask) >> 32) as u32;
entry.edx |= ((caps.xfam_fixed1 & xss_mask) >> 32) as u32;
}
}
}
// Tile Information (purely AMX related).
0x1d if !config.amx => {
entry.eax = 0;
@@ -694,10 +783,10 @@ pub fn generate_common_cpuid(
&& entry.ecx == 0
&& entry.edx == 0
// SAFETY: cpuid called with valid leaves
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
&& unsafe { x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
{
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
let leaf = unsafe { x86_64::__cpuid(0x8000_0005) };
entry.eax = leaf.eax;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
@@ -710,18 +799,20 @@ pub fn generate_common_cpuid(
&& entry.ecx == 0
&& entry.edx == 0
// SAFETY: cpuid called with valid leaves
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
&& unsafe { x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
{
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) };
let leaf = unsafe { x86_64::__cpuid(0x8000_0006) };
entry.eax = leaf.eax;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
entry.edx = leaf.edx;
}
// Set CPU physical bits
// Set CPU physical bits and guest physical bits
0x8000_0008 => {
entry.eax = (entry.eax & 0xffff_ff00) | (config.phys_bits as u32 & 0xff);
entry.eax = (entry.eax & 0xff00_ff00)
| (config.phys_bits as u32 & 0xff)
| ((config.phys_bits as u32 & 0xff) << 16);
}
0x4000_0001 => {
// Enable KVM_FEATURE_MSI_EXT_DEST_ID. This allows the guest to target
@@ -743,22 +834,6 @@ pub fn generate_common_cpuid(
}
}
// Copy CPU identification string
for i in 0x8000_0002..=0x8000_0004 {
cpuid.retain(|c| c.function != i);
// SAFETY: call cpuid with valid leaves
#[allow(unused_unsafe)]
let leaf = unsafe { std::arch::x86_64::__cpuid(i) };
cpuid.push(CpuIdEntry {
function: i,
eax: leaf.eax,
ebx: leaf.ebx,
ecx: leaf.ecx,
edx: leaf.edx,
..Default::default()
});
}
if config.kvm_hyperv {
// Remove conflicting entries
cpuid.retain(|c| c.function != 0x4000_0000);
@@ -786,16 +861,31 @@ pub fn generate_common_cpuid(
});
cpuid.push(CpuIdEntry {
function: 0x4000_0003,
eax: (1 << 1) // AccessPartitionReferenceCounter
eax: (1 << 0) // AccessVpRunTimeReg
| (1 << 1) // AccessPartitionReferenceCounter
| (1 << 2) // AccessSynicRegs
| (1 << 3) // AccessSyntheticTimerRegs
| (1 << 9), // AccessPartitionReferenceTsc
edx: 1 << 3, // CPU dynamic partitioning
| (1 << 4) // AccessIntrCtrlRegs (APIC access MSRs / VP Assist EOI)
| (1 << 5) // AccessHypercallMsrs
| (1 << 6) // AccessVpIndex
| (1 << 9) // AccessPartitionReferenceTsc
| (1 << 11), // AccessFrequencyMsrs (TSC/APIC frequency MSRs)
edx: (1 << 3) // CPU dynamic partitioning
| (1 << 4) // FastHypercall (XMM register hypercall input)
| (1 << 8), // ExtendedGvaRangesForFlushVirtualAddressList
..Default::default()
});
cpuid.push(CpuIdEntry {
function: 0x4000_0004,
eax: 1 << 5, // Recommend relaxed timing
// Recommendation hints to Hyper-V-aware guests. Bit semantics per
// Microsoft Hypervisor Top-Level Functional Specification 7.4.5.
eax: (1 << 1) // LocalTlbFlushRecommended
| (1 << 2) // RemoteTlbFlushRecommended
| (1 << 3) // ApicAccessRecommended (VP Assist page MSR EOI/ICR/TPR)
| (1 << 5) // RelaxedTimingRecommended
| (1 << 9) // DeprecatingAeoiRecommended (keeps APICv on with SynIC)
| (1 << 10), // ClusterIpiRecommended (HvCallSendSyntheticClusterIpi)
ebx: 0xfff, // Suggested spinlock retry attempts before trapping to host
..Default::default()
});
for i in 0x4000_0005..=0x4000_000a {
@@ -806,10 +896,39 @@ pub fn generate_common_cpuid(
}
}
Ok(cpuid)
cpuid
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "tdx")]
fn common_cpuid_tdx_configuration(
cpuid: &mut [CpuIdEntry],
hypervisor: &dyn hypervisor::Hypervisor,
) -> super::Result<()> {
let caps = hypervisor
.tdx_capabilities()
.map_err(Error::TdxCapabilities)?;
info!("TDX capabilities {caps:#?}");
for entry in cpuid.iter_mut().filter(|entry| entry.function == 0xd) {
let xcr0_mask: u64 = 0x82ff;
let xss_mask: u64 = !xcr0_mask;
if entry.index == 0 {
entry.eax &= (caps.xfam_fixed0 as u32) & (xcr0_mask as u32);
entry.eax |= (caps.xfam_fixed1 as u32) & (xcr0_mask as u32);
entry.edx &= ((caps.xfam_fixed0 & xcr0_mask) >> 32) as u32;
entry.edx |= ((caps.xfam_fixed1 & xcr0_mask) >> 32) as u32;
} else if entry.index == 1 {
entry.ecx &= (caps.xfam_fixed0 as u32) & (xss_mask as u32);
entry.ecx |= (caps.xfam_fixed1 as u32) & (xss_mask as u32);
entry.edx &= ((caps.xfam_fixed0 & xss_mask) >> 32) as u32;
entry.edx |= ((caps.xfam_fixed1 & xss_mask) >> 32) as u32;
}
}
Ok(())
}
#[expect(clippy::too_many_arguments)]
pub fn configure_vcpu(
vcpu: &dyn hypervisor::Vcpu,
id: u32,
@@ -865,9 +984,7 @@ pub fn configure_vcpu(
// Need to check that the TSC doesn't vary with dynamic frequency
#[allow(unused_unsafe)]
// SAFETY: cpuid called with valid leaves
if unsafe { std::arch::x86_64::__cpuid(0x8000_0007) }.edx & (1u32 << INVARIANT_TSC_EDX_BIT)
> 0
{
if unsafe { x86_64::__cpuid(0x8000_0007) }.edx & (1u32 << INVARIANT_TSC_EDX_BIT) > 0 {
CpuidPatch::set_cpuid_reg(&mut cpuid, 0x4000_0000, None, CpuidReg::EAX, 0x4000_0010);
cpuid.retain(|c| c.function != 0x4000_0010);
cpuid.push(CpuIdEntry {
@@ -949,7 +1066,7 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
/// * `cmdline_addr` - Address in `guest_mem` where the kernel command line was loaded.
/// * `cmdline_size` - Size of the kernel command line in bytes including the null terminator.
/// * `num_cpus` - Number of virtual CPUs the guest will have.
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
pub fn configure_system(
guest_mem: &GuestMemoryMmap,
cmdline_addr: GuestAddress,
@@ -958,9 +1075,7 @@ pub fn configure_system(
_num_cpus: u32,
setup_header: Option<setup_header>,
rsdp_addr: Option<GuestAddress>,
serial_number: Option<&str>,
uuid: Option<&str>,
oem_strings: Option<&[&str]>,
smbios: Option<&SmbiosConfig>,
topology: Option<(u16, u16, u16, u16)>,
) -> super::Result<()> {
// Write EBDA address to location where ACPICA expects to find it
@@ -968,8 +1083,7 @@ pub fn configure_system(
.write_obj((layout::EBDA_START.0 >> 4) as u16, layout::EBDA_POINTER)
.map_err(Error::EbdaSetup)?;
let size = smbios::setup_smbios(guest_mem, serial_number, uuid, oem_strings)
.map_err(Error::SmbiosSetup)?;
let size = smbios::setup_smbios(guest_mem, smbios).map_err(Error::SmbiosSetup)?;
// Place the MP table after the SMIOS table aligned to 16 bytes
let offset = GuestAddress(layout::SMBIOS_START).unchecked_add(size);
@@ -1039,8 +1153,9 @@ pub fn generate_ram_ranges(guest_mem: &GuestMemoryMmap) -> super::Result<Vec<Ram
// Generate the first usable physical memory range before the gap. The e820 map
// should only report memory above 1MiB.
let first_ram_range = {
let (first_region_start, first_region_end) =
ram_regions.first().ok_or(super::Error::MemmapTableSetup)?;
let (first_region_start, first_region_end) = ram_regions
.first()
.ok_or(super::Error::MemmapTableGeneration)?;
let high_ram_start = layout::HIGH_RAM_START.raw_value();
let mem_32bit_reserved_start = layout::MEM_32BIT_RESERVED_START.raw_value();
@@ -1053,7 +1168,7 @@ pub fn generate_ram_ranges(guest_mem: &GuestMemoryMmap) -> super::Result<Vec<Ram
high_ram_start: 0x{high_ram_start:08x}, mem_32bit_reserved_start: 0x{mem_32bit_reserved_start:08x}"
);
return Err(super::Error::MemmapTableSetup);
return Err(super::Error::MemmapTableGeneration);
}
info!(
@@ -1156,7 +1271,7 @@ fn configure_pvh(
guest_mem
.checked_offset(
memmap_start_addr,
mem::size_of::<hvm_memmap_table_entry>() * start_info.memmap_entries as usize,
size_of::<hvm_memmap_table_entry>() * start_info.memmap_entries as usize,
)
.ok_or(super::Error::MemmapTablePastRamEnd)?;
@@ -1164,9 +1279,9 @@ fn configure_pvh(
for memmap_entry in memmap {
guest_mem
.write_obj(memmap_entry, memmap_start_addr)
.map_err(|_| super::Error::MemmapTableSetup)?;
.map_err(super::Error::MemmapTableSetup)?;
memmap_start_addr =
memmap_start_addr.unchecked_add(mem::size_of::<hvm_memmap_table_entry>() as u64);
memmap_start_addr.unchecked_add(size_of::<hvm_memmap_table_entry>() as u64);
}
// The hvm_start_info struct itself must be stored at PVH_START_INFO
@@ -1175,13 +1290,13 @@ fn configure_pvh(
let start_info_addr = layout::PVH_INFO_START;
guest_mem
.checked_offset(start_info_addr, mem::size_of::<hvm_start_info>())
.checked_offset(start_info_addr, size_of::<hvm_start_info>())
.ok_or(super::Error::StartInfoPastRamEnd)?;
// Write the start_info struct to guest memory.
guest_mem
.write_obj(start_info, start_info_addr)
.map_err(|_| super::Error::StartInfoSetup)?;
.map_err(super::Error::StartInfoSetup)?;
Ok(())
}
@@ -1254,7 +1369,7 @@ fn configure_32bit_entry(
let zero_page_addr = layout::ZERO_PAGE_START;
guest_mem
.checked_offset(zero_page_addr, mem::size_of::<boot_params>())
.checked_offset(zero_page_addr, size_of::<boot_params>())
.ok_or(super::Error::ZeroPagePastRamEnd)?;
guest_mem
.write_obj(params, zero_page_addr)
@@ -1312,27 +1427,15 @@ pub fn initramfs_load_addr(
Ok(aligned_addr)
}
pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
pub fn get_host_cpu_phys_bits(_hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
// SAFETY: call cpuid with valid leaves
#[allow(unused_unsafe)]
unsafe {
let leaf = x86_64::__cpuid(0x8000_0000);
// Detect and handle AMD SME (Secure Memory Encryption) properly.
// Some physical address bits may become reserved when the feature is enabled.
// See AMD64 Architecture Programmer's Manual Volume 2, Section 7.10.1
let reduced = if leaf.eax >= 0x8000_001f
&& matches!(hypervisor.get_cpu_vendor(), CpuVendor::AMD)
&& x86_64::__cpuid(0x8000_001f).eax & 0x1 != 0
{
(x86_64::__cpuid(0x8000_001f).ebx >> 6) & 0x3f
} else {
0
};
if leaf.eax >= 0x8000_0008 {
let leaf = x86_64::__cpuid(0x8000_0008);
((leaf.eax & 0xff) - reduced) as u8
(leaf.eax & 0xff) as u8
} else {
36
}
@@ -1514,8 +1617,6 @@ mod unit_tests {
Some(layout::RSDP_POINTER),
None,
None,
None,
None,
);
config_err.unwrap_err();
@@ -1538,8 +1639,6 @@ mod unit_tests {
None,
None,
None,
None,
None,
)
.unwrap();
@@ -1567,8 +1666,6 @@ mod unit_tests {
None,
None,
None,
None,
None,
)
.unwrap();
@@ -1582,8 +1679,6 @@ mod unit_tests {
None,
None,
None,
None,
None,
)
.unwrap();
}

View File

@@ -3,35 +3,36 @@
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
#![allow(non_camel_case_types)]
use std::os::raw;
use vm_memory::ByteValued;
pub const MP_PROCESSOR: ::std::os::raw::c_uint = 0;
pub const MP_BUS: ::std::os::raw::c_uint = 1;
pub const MP_IOAPIC: ::std::os::raw::c_uint = 2;
pub const MP_INTSRC: ::std::os::raw::c_uint = 3;
pub const MP_LINTSRC: ::std::os::raw::c_uint = 4;
pub const CPU_ENABLED: ::std::os::raw::c_uint = 1;
pub const CPU_BOOTPROCESSOR: ::std::os::raw::c_uint = 2;
pub const MPC_APIC_USABLE: ::std::os::raw::c_uint = 1;
pub const MP_IRQDIR_DEFAULT: ::std::os::raw::c_uint = 0;
pub const MP_PROCESSOR: raw::c_uint = 0;
pub const MP_BUS: raw::c_uint = 1;
pub const MP_IOAPIC: raw::c_uint = 2;
pub const MP_INTSRC: raw::c_uint = 3;
pub const MP_LINTSRC: raw::c_uint = 4;
pub const CPU_ENABLED: raw::c_uint = 1;
pub const CPU_BOOTPROCESSOR: raw::c_uint = 2;
pub const MPC_APIC_USABLE: raw::c_uint = 1;
pub const MP_IRQDIR_DEFAULT: raw::c_uint = 0;
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpf_intel {
pub signature: [::std::os::raw::c_uchar; 4usize],
pub physptr: ::std::os::raw::c_uint,
pub length: ::std::os::raw::c_uchar,
pub specification: ::std::os::raw::c_uchar,
pub checksum: ::std::os::raw::c_uchar,
pub feature1: ::std::os::raw::c_uchar,
pub feature2: ::std::os::raw::c_uchar,
pub feature3: ::std::os::raw::c_uchar,
pub feature4: ::std::os::raw::c_uchar,
pub feature5: ::std::os::raw::c_uchar,
pub signature: [raw::c_uchar; 4usize],
pub physptr: raw::c_uint,
pub length: raw::c_uchar,
pub specification: raw::c_uchar,
pub checksum: raw::c_uchar,
pub feature1: raw::c_uchar,
pub feature2: raw::c_uchar,
pub feature3: raw::c_uchar,
pub feature4: raw::c_uchar,
pub feature5: raw::c_uchar,
}
const _: () = assert!(::core::mem::size_of::<mpf_intel>() == 16);
const _: () = assert!(size_of::<mpf_intel>() == 16);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
@@ -41,24 +42,24 @@ unsafe impl ByteValued for mpf_intel {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_table {
pub signature: [::std::os::raw::c_uchar; 4usize],
pub length: ::std::os::raw::c_ushort,
pub spec: ::std::os::raw::c_uchar,
pub checksum: ::std::os::raw::c_uchar,
pub oem: [::std::os::raw::c_uchar; 8usize],
pub productid: [::std::os::raw::c_uchar; 12usize],
pub oemptr: ::std::os::raw::c_uint,
pub oemsize: ::std::os::raw::c_ushort,
pub oemcount: ::std::os::raw::c_ushort,
pub lapic: ::std::os::raw::c_uint,
pub reserved: ::std::os::raw::c_uint,
pub signature: [raw::c_uchar; 4usize],
pub length: raw::c_ushort,
pub spec: raw::c_uchar,
pub checksum: raw::c_uchar,
pub oem: [raw::c_uchar; 8usize],
pub productid: [raw::c_uchar; 12usize],
pub oemptr: raw::c_uint,
pub oemsize: raw::c_ushort,
pub oemcount: raw::c_ushort,
pub lapic: raw::c_uint,
pub reserved: raw::c_uint,
}
const _: () = {
assert!(::core::mem::size_of::<mpc_table>() == 4 + 2 + 1 + 1 + 8 + 12 + 4 + 2 + 2 + 4 + 4);
assert!(::core::mem::size_of::<::std::os::raw::c_uint>() == 4);
assert!(::core::mem::size_of::<::std::os::raw::c_ushort>() == 2);
assert!(::core::mem::size_of::<::std::os::raw::c_uchar>() == 1);
assert!(size_of::<mpc_table>() == 4 + 2 + 1 + 1 + 8 + 12 + 4 + 2 + 2 + 4 + 4);
assert!(size_of::<raw::c_uint>() == 4);
assert!(size_of::<raw::c_ushort>() == 2);
assert!(size_of::<raw::c_uchar>() == 1);
};
// SAFETY: all members of this struct are plain integers
@@ -70,16 +71,16 @@ unsafe impl ByteValued for mpc_table {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_cpu {
pub type_: ::std::os::raw::c_uchar,
pub apicid: ::std::os::raw::c_uchar,
pub apicver: ::std::os::raw::c_uchar,
pub cpuflag: ::std::os::raw::c_uchar,
pub cpufeature: ::std::os::raw::c_uint,
pub featureflag: ::std::os::raw::c_uint,
pub reserved: [::std::os::raw::c_uint; 2usize],
pub type_: raw::c_uchar,
pub apicid: raw::c_uchar,
pub apicver: raw::c_uchar,
pub cpuflag: raw::c_uchar,
pub cpufeature: raw::c_uint,
pub featureflag: raw::c_uint,
pub reserved: [raw::c_uint; 2usize],
}
const _: () = assert!(::core::mem::size_of::<mpc_cpu>() == 20);
const _: () = assert!(size_of::<mpc_cpu>() == 20);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
@@ -89,12 +90,12 @@ unsafe impl ByteValued for mpc_cpu {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_bus {
pub type_: ::std::os::raw::c_uchar,
pub busid: ::std::os::raw::c_uchar,
pub bustype: [::std::os::raw::c_uchar; 6usize],
pub type_: raw::c_uchar,
pub busid: raw::c_uchar,
pub bustype: [raw::c_uchar; 6usize],
}
const _: () = assert!(::core::mem::size_of::<mpc_bus>() == 8);
const _: () = assert!(size_of::<mpc_bus>() == 8);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
@@ -104,14 +105,14 @@ unsafe impl ByteValued for mpc_bus {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_ioapic {
pub type_: ::std::os::raw::c_uchar,
pub apicid: ::std::os::raw::c_uchar,
pub apicver: ::std::os::raw::c_uchar,
pub flags: ::std::os::raw::c_uchar,
pub apicaddr: ::std::os::raw::c_uint,
pub type_: raw::c_uchar,
pub apicid: raw::c_uchar,
pub apicver: raw::c_uchar,
pub flags: raw::c_uchar,
pub apicaddr: raw::c_uint,
}
const _: () = assert!(::core::mem::size_of::<mpc_ioapic>() == 8);
const _: () = assert!(size_of::<mpc_ioapic>() == 8);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
@@ -121,39 +122,39 @@ unsafe impl ByteValued for mpc_ioapic {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_intsrc {
pub type_: ::std::os::raw::c_uchar,
pub irqtype: ::std::os::raw::c_uchar,
pub irqflag: ::std::os::raw::c_ushort,
pub srcbus: ::std::os::raw::c_uchar,
pub srcbusirq: ::std::os::raw::c_uchar,
pub dstapic: ::std::os::raw::c_uchar,
pub dstirq: ::std::os::raw::c_uchar,
pub type_: raw::c_uchar,
pub irqtype: raw::c_uchar,
pub irqflag: raw::c_ushort,
pub srcbus: raw::c_uchar,
pub srcbusirq: raw::c_uchar,
pub dstapic: raw::c_uchar,
pub dstirq: raw::c_uchar,
}
const _: () = assert!(::core::mem::size_of::<mpc_intsrc>() == 8);
const _: () = assert!(size_of::<mpc_intsrc>() == 8);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
// would be nowhere for them to exist.
unsafe impl ByteValued for mpc_intsrc {}
pub const MP_IRQ_SOURCE_TYPES_MP_INT: ::std::os::raw::c_uint = 0;
pub const MP_IRQ_SOURCE_TYPES_MP_NMI: ::std::os::raw::c_uint = 1;
pub const MP_IRQ_SOURCE_TYPES_MP_EXT_INT: ::std::os::raw::c_uint = 3;
pub const MP_IRQ_SOURCE_TYPES_MP_INT: raw::c_uint = 0;
pub const MP_IRQ_SOURCE_TYPES_MP_NMI: raw::c_uint = 1;
pub const MP_IRQ_SOURCE_TYPES_MP_EXT_INT: raw::c_uint = 3;
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_lintsrc {
pub type_: ::std::os::raw::c_uchar,
pub irqtype: ::std::os::raw::c_uchar,
pub irqflag: ::std::os::raw::c_ushort,
pub srcbusid: ::std::os::raw::c_uchar,
pub srcbusirq: ::std::os::raw::c_uchar,
pub destapic: ::std::os::raw::c_uchar,
pub destapiclint: ::std::os::raw::c_uchar,
pub type_: raw::c_uchar,
pub irqtype: raw::c_uchar,
pub irqflag: raw::c_ushort,
pub srcbusid: raw::c_uchar,
pub srcbusirq: raw::c_uchar,
pub destapic: raw::c_uchar,
pub destapiclint: raw::c_uchar,
}
const _: () = assert!(::core::mem::size_of::<mpc_lintsrc>() == 8);
const _: () = assert!(size_of::<mpc_lintsrc>() == 8);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
@@ -163,14 +164,14 @@ unsafe impl ByteValued for mpc_lintsrc {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_oemtable {
pub signature: [::std::os::raw::c_uchar; 4usize],
pub length: ::std::os::raw::c_ushort,
pub rev: ::std::os::raw::c_uchar,
pub checksum: ::std::os::raw::c_uchar,
pub mpc: [::std::os::raw::c_uchar; 8usize],
pub signature: [raw::c_uchar; 4usize],
pub length: raw::c_ushort,
pub rev: raw::c_uchar,
pub checksum: raw::c_uchar,
pub mpc: [raw::c_uchar; 8usize],
}
const _: () = assert!(::core::mem::size_of::<mpc_oemtable>() == 16);
const _: () = assert!(size_of::<mpc_oemtable>() == 16);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there

View File

@@ -5,7 +5,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
use std::{mem, result, slice};
use std::result;
use libc::c_uchar;
use log::{info, warn};
@@ -101,11 +101,8 @@ const CPU_FEATURE_APIC: u32 = 0x200;
const CPU_FEATURE_FPU: u32 = 0x001;
fn compute_checksum<T: Copy + ByteValued>(v: &T) -> u8 {
let v: *const T = v;
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
let v_slice = unsafe { slice::from_raw_parts(v.cast(), mem::size_of::<T>()) };
let mut checksum: u8 = 0;
for i in v_slice.iter() {
for i in v.as_slice().iter() {
checksum = checksum.wrapping_add(*i);
}
checksum
@@ -117,13 +114,13 @@ fn mpf_intel_compute_checksum(v: &mpspec::mpf_intel) -> u8 {
}
fn compute_mp_size(num_cpus: u32) -> usize {
mem::size_of::<MpfIntelWrapper>()
+ mem::size_of::<MpcTableWrapper>()
+ mem::size_of::<MpcCpuWrapper>() * (num_cpus as usize)
+ mem::size_of::<MpcIoapicWrapper>()
+ mem::size_of::<MpcBusWrapper>()
+ mem::size_of::<MpcIntsrcWrapper>() * 16
+ mem::size_of::<MpcLintsrcWrapper>() * 2
size_of::<MpfIntelWrapper>()
+ size_of::<MpcTableWrapper>()
+ size_of::<MpcCpuWrapper>() * (num_cpus as usize)
+ size_of::<MpcIoapicWrapper>()
+ size_of::<MpcBusWrapper>()
+ size_of::<MpcIntsrcWrapper>() * 16
+ size_of::<MpcLintsrcWrapper>() * 2
}
/// Performs setup of the MP table for the given `num_cpus`.
@@ -170,7 +167,7 @@ pub fn setup_mptable(
{
let mut mpf_intel = MpfIntelWrapper(mpspec::mpf_intel::default());
let size = mem::size_of::<MpfIntelWrapper>() as u64;
let size = size_of::<MpfIntelWrapper>() as u64;
mpf_intel.0.signature = *SMP_MAGIC_IDENT;
mpf_intel.0.length = 1;
mpf_intel.0.specification = 4;
@@ -184,10 +181,10 @@ pub fn setup_mptable(
// We set the location of the mpc_table here but we can't fill it out until we have the length
// of the entire table later.
let table_base = base_mp;
base_mp = base_mp.unchecked_add(mem::size_of::<MpcTableWrapper>() as u64);
base_mp = base_mp.unchecked_add(size_of::<MpcTableWrapper>() as u64);
{
let size = mem::size_of::<MpcCpuWrapper>();
let size = size_of::<MpcCpuWrapper>();
for cpu_id in 0..num_cpus {
let mut mpc_cpu = MpcCpuWrapper(mpspec::mpc_cpu::default());
mpc_cpu.0.type_ = mpspec::MP_PROCESSOR as u8;
@@ -208,7 +205,7 @@ pub fn setup_mptable(
}
}
{
let size = mem::size_of::<MpcBusWrapper>();
let size = size_of::<MpcBusWrapper>();
let mut mpc_bus = MpcBusWrapper(mpspec::mpc_bus::default());
mpc_bus.0.type_ = mpspec::MP_BUS as u8;
mpc_bus.0.busid = 0;
@@ -219,7 +216,7 @@ pub fn setup_mptable(
checksum = checksum.wrapping_add(compute_checksum(&mpc_bus.0));
}
{
let size = mem::size_of::<MpcIoapicWrapper>();
let size = size_of::<MpcIoapicWrapper>();
let mut mpc_ioapic = MpcIoapicWrapper(mpspec::mpc_ioapic::default());
mpc_ioapic.0.type_ = mpspec::MP_IOAPIC as u8;
mpc_ioapic.0.apicid = ioapicid;
@@ -233,7 +230,7 @@ pub fn setup_mptable(
}
// Per kvm_setup_default_irq_routing() in kernel
for i in 0..16 {
let size = mem::size_of::<MpcIntsrcWrapper>();
let size = size_of::<MpcIntsrcWrapper>();
let mut mpc_intsrc = MpcIntsrcWrapper(mpspec::mpc_intsrc::default());
mpc_intsrc.0.type_ = mpspec::MP_INTSRC as u8;
mpc_intsrc.0.irqtype = mpspec::MP_IRQ_SOURCE_TYPES_MP_INT as u8;
@@ -248,7 +245,7 @@ pub fn setup_mptable(
checksum = checksum.wrapping_add(compute_checksum(&mpc_intsrc.0));
}
{
let size = mem::size_of::<MpcLintsrcWrapper>();
let size = size_of::<MpcLintsrcWrapper>();
let mut mpc_lintsrc = MpcLintsrcWrapper(mpspec::mpc_lintsrc::default());
mpc_lintsrc.0.type_ = mpspec::MP_LINTSRC as u8;
mpc_lintsrc.0.irqtype = mpspec::MP_IRQ_SOURCE_TYPES_MP_EXT_INT as u8;
@@ -263,7 +260,7 @@ pub fn setup_mptable(
checksum = checksum.wrapping_add(compute_checksum(&mpc_lintsrc.0));
}
{
let size = mem::size_of::<MpcLintsrcWrapper>();
let size = size_of::<MpcLintsrcWrapper>();
let mut mpc_lintsrc = MpcLintsrcWrapper(mpspec::mpc_lintsrc::default());
mpc_lintsrc.0.type_ = mpspec::MP_LINTSRC as u8;
mpc_lintsrc.0.irqtype = mpspec::MP_IRQ_SOURCE_TYPES_MP_NMI as u8;
@@ -308,11 +305,11 @@ mod unit_tests {
fn table_entry_size(type_: u8) -> usize {
match type_ as u32 {
mpspec::MP_PROCESSOR => mem::size_of::<MpcCpuWrapper>(),
mpspec::MP_BUS => mem::size_of::<MpcBusWrapper>(),
mpspec::MP_IOAPIC => mem::size_of::<MpcIoapicWrapper>(),
mpspec::MP_INTSRC => mem::size_of::<MpcIntsrcWrapper>(),
mpspec::MP_LINTSRC => mem::size_of::<MpcLintsrcWrapper>(),
mpspec::MP_PROCESSOR => size_of::<MpcCpuWrapper>(),
mpspec::MP_BUS => size_of::<MpcBusWrapper>(),
mpspec::MP_IOAPIC => size_of::<MpcIoapicWrapper>(),
mpspec::MP_INTSRC => size_of::<MpcIntsrcWrapper>(),
mpspec::MP_LINTSRC => size_of::<MpcLintsrcWrapper>(),
_ => panic!("unrecognized mpc table entry type: {type_}"),
}
}
@@ -405,7 +402,7 @@ mod unit_tests {
.unwrap();
let mut entry_offset = mpc_offset
.checked_add(mem::size_of::<MpcTableWrapper>() as GuestUsize)
.checked_add(size_of::<MpcTableWrapper>() as GuestUsize)
.unwrap();
let mut cpu_count = 0;
while entry_offset < mpc_end {

View File

@@ -6,7 +6,7 @@
// Portions Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
use std::{mem, result};
use std::result;
use hypervisor::arch::x86::gdt::{gdt_entry, segment_from_gdt};
use hypervisor::arch::x86::regs::CR0_PE;
@@ -134,7 +134,7 @@ fn write_gdt_table(table: &[u64], guest_mem: &GuestMemoryMmap) -> Result<()> {
let boot_gdt_addr = BOOT_GDT_START;
for (index, entry) in table.iter().enumerate() {
let addr = guest_mem
.checked_offset(boot_gdt_addr, index * mem::size_of::<u64>())
.checked_offset(boot_gdt_addr, index * size_of::<u64>())
.ok_or(Error::CheckGdtAddr)?;
guest_mem.write_obj(*entry, addr).map_err(Error::WriteGdt)?;
}
@@ -170,11 +170,11 @@ pub fn configure_segments_and_sregs(
// Write segments
write_gdt_table(&gdt_table[..], mem)?;
sregs.gdt.base = BOOT_GDT_START.raw_value();
sregs.gdt.limit = mem::size_of_val(&gdt_table) as u16 - 1;
sregs.gdt.limit = size_of_val(&gdt_table) as u16 - 1;
write_idt_value(0, mem)?;
sregs.idt.base = BOOT_IDT_START.raw_value();
sregs.idt.limit = mem::size_of::<u64>() as u16 - 1;
sregs.idt.limit = size_of::<u64>() as u16 - 1;
sregs.cs = code_seg;
sregs.ds = data_seg;

View File

@@ -6,7 +6,7 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::{mem, result, slice};
use std::result;
use thiserror::Error;
use uuid::Uuid;
@@ -28,39 +28,74 @@ pub enum Error {
Clear,
/// Failure to write SMBIOS entrypoint structure
#[error("Failure to write SMBIOS entrypoint structure")]
WriteSmbiosEp,
WriteSmbiosEp(#[source] vm_memory::GuestMemoryError),
/// Failure to write additional data to memory
#[error("Failure to write additional data to memory")]
WriteData,
WriteData(#[source] vm_memory::GuestMemoryError),
/// Failure to parse uuid, uuid format may be error
#[error("Failure to parse uuid: {1}")]
ParseUuid(#[source] uuid::Error, String),
/// SMBIOS string index overflow (u8 limit reached).
#[error("SMBIOS string index overflow (u8 limit reached: {})", u8::MAX)]
TooManyStrings,
}
pub type Result<T> = result::Result<T, Error>;
// Constants sourced from SMBIOS Spec 3.2.0.
// Constants sourced from SMBIOS Spec 3.9.0.
const SM3_MAGIC_IDENT: &[u8; 5usize] = b"_SM3_";
const BIOS_INFORMATION: u8 = 0;
const SYSTEM_INFORMATION: u8 = 1;
const OEM_STRINGS: u8 = 11;
const SYSTEM_ENCLOSURE: u8 = 3;
const END_OF_TABLE: u8 = 127;
const SYSTEM_WAKE_UP_TYPE_UNKNOWN: u8 = 0x02;
const CHASSIS_TYPE_UNKNOWN: u8 = 0x02;
const CHASSIS_STATE_UNKNOWN: u8 = 0x02;
const CHASSIS_SECURITY_STATUS_NONE: u8 = 0x03;
const PCI_SUPPORTED: u64 = 1 << 7;
const IS_VIRTUAL_MACHINE: u8 = 1 << 4;
pub const DEFAULT_SYSTEM_MANUFACTURER: &str = "Cloud Hypervisor";
pub const DEFAULT_SYSTEM_PRODUCT_NAME: &str = "cloud-hypervisor";
fn compute_checksum<T: Copy>(v: &T) -> u8 {
let v: *const T = v;
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
let v_slice = unsafe { slice::from_raw_parts(v.cast(), mem::size_of::<T>()) };
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SmbiosConfig {
pub system: Option<SmbiosSystem>,
pub chassis: Option<SmbiosChassisConfig>,
pub oem_strings: Box<[String]>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SmbiosSystem {
pub manufacturer: Option<String>,
pub product_name: Option<String>,
pub version: Option<String>,
pub serial_number: Option<String>,
pub uuid: Option<String>,
pub sku_number: Option<String>,
pub family: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SmbiosChassisConfig {
pub asset_tag: Option<String>,
}
impl SmbiosConfig {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
fn compute_checksum<T: Copy + ByteValued>(v: &T) -> u8 {
let mut checksum: u8 = 0;
for i in v_slice.iter() {
for i in v.as_slice().iter() {
checksum = checksum.wrapping_add(*i);
}
(!checksum).wrapping_add(1)
}
#[repr(C)]
#[repr(packed)]
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct Smbios30Entrypoint {
signature: [u8; 5usize],
@@ -75,8 +110,7 @@ struct Smbios30Entrypoint {
physptr: u64,
}
#[repr(C)]
#[repr(packed)]
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosBiosInfo {
r#type: u8,
@@ -92,8 +126,7 @@ struct SmbiosBiosInfo {
characteristics_ext2: u8,
}
#[repr(C)]
#[repr(packed)]
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosSysInfo {
r#type: u8,
@@ -109,8 +142,7 @@ struct SmbiosSysInfo {
family: u8,
}
#[repr(C)]
#[repr(packed)]
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosOemStrings {
r#type: u8,
@@ -119,8 +151,34 @@ struct SmbiosOemStrings {
count: u8,
}
#[repr(C)]
#[repr(packed)]
/// SMBIOS Chassis Table (Type 3) as defined in DMTF SMBIOS 3.9.0:
/// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf
/// Note: trailing fields are omitted, so this structure is not complete.
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosChassis {
r#type: u8,
length: u8,
handle: u16,
manufacturer: u8,
chassis_type: u8,
version: u8,
serial_number: u8,
asset_tag: u8,
bootup_state: u8,
power_supply_state: u8,
thermal_state: u8,
security_status: u8,
oem_defined: u32,
height: u8,
number_of_power_cords: u8,
contained_element_count: u8,
contained_element_record_length: u8,
// followed by contained element records (optional, variable-length)
// followed by sku_number: u8, rack_type: u8, rack_height: u8
}
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosEndOfTable {
r#type: u8,
@@ -137,6 +195,8 @@ unsafe impl ByteValued for SmbiosSysInfo {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for SmbiosOemStrings {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for SmbiosChassis {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for SmbiosEndOfTable {}
fn write_and_incr<T: ByteValued>(
@@ -144,9 +204,9 @@ fn write_and_incr<T: ByteValued>(
val: T,
mut curptr: GuestAddress,
) -> Result<GuestAddress> {
mem.write_obj(val, curptr).map_err(|_| Error::WriteData)?;
mem.write_obj(val, curptr).map_err(Error::WriteData)?;
curptr = curptr
.checked_add(mem::size_of::<T>() as u64)
.checked_add(size_of::<T>() as u64)
.ok_or(Error::NotEnoughMemory)?;
Ok(curptr)
}
@@ -163,14 +223,155 @@ fn write_string(
Ok(curptr)
}
pub fn setup_smbios(
fn write_opt_string(
mem: &GuestMemoryMmap,
serial_number: Option<&str>,
uuid: Option<&str>,
oem_strings: Option<&[&str]>,
) -> Result<u64> {
s: Option<&str>,
cur: GuestAddress,
) -> Result<GuestAddress> {
if let Some(v) = s {
write_string(mem, v, cur)
} else {
Ok(cur)
}
}
fn write_string_terminator(
mem: &GuestMemoryMmap,
cur: GuestAddress,
has_strings: bool,
) -> Result<GuestAddress> {
// SMBIOS DSP0134 §6.1.3: if all string-reference fields are 0, follow the
// formatted section with two null bytes (empty string-set).
if has_strings {
write_and_incr(mem, 0u8, cur)
} else {
let cur = write_and_incr(mem, 0u8, cur)?;
write_and_incr(mem, 0u8, cur)
}
}
/// Allocate the next string index for an SMBIOS string-set.
///
/// Per SMBIOS DSP0134, index `0` means "no string", so valid indices run from
/// `1` to `255`. Returns `0` when `present` is `false`. Otherwise returns the
/// current value of `*next` and advances it by one. Fails with
/// [`Error::TooManyStrings`] once all 255 indices have been used: `next`
/// starts at `1`, so it can only be `0` here after wrapping past `255`.
fn alloc_index(next: &mut u8, present: bool) -> Result<u8> {
if !present {
return Ok(0);
}
let idx = *next;
if idx == 0 {
return Err(Error::TooManyStrings);
}
*next = next.wrapping_add(1);
Ok(idx)
}
fn write_type1_system(
mem: &GuestMemoryMmap,
curptr: &mut GuestAddress,
handle: &mut u16,
system: Option<&SmbiosSystem>,
) -> Result<()> {
*handle += 1;
let manufacturer = system
.and_then(|s| s.manufacturer.as_deref())
.unwrap_or(DEFAULT_SYSTEM_MANUFACTURER);
let product = system
.and_then(|s| s.product_name.as_deref())
.unwrap_or(DEFAULT_SYSTEM_PRODUCT_NAME);
let version = system.and_then(|s| s.version.as_deref());
let serial = system.and_then(|s| s.serial_number.as_deref());
let uuid = system.and_then(|s| s.uuid.as_deref());
let sku = system.and_then(|s| s.sku_number.as_deref());
let family = system.and_then(|s| s.family.as_deref());
let uuid_number = uuid
.map(Uuid::parse_str)
.transpose()
.map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))?
.unwrap_or(Uuid::nil());
let mut next = 1u8;
let manufacturer_idx = alloc_index(&mut next, true)?;
let product_idx = alloc_index(&mut next, true)?;
let version_idx = alloc_index(&mut next, version.is_some())?;
let serial_idx = alloc_index(&mut next, serial.is_some())?;
let sku_idx = alloc_index(&mut next, sku.is_some())?;
let family_idx = alloc_index(&mut next, family.is_some())?;
let sys = SmbiosSysInfo {
r#type: SYSTEM_INFORMATION,
length: size_of::<SmbiosSysInfo>() as u8,
handle: *handle,
manufacturer: manufacturer_idx,
product_name: product_idx,
version: version_idx,
serial_number: serial_idx,
uuid: uuid_number.to_bytes_le(),
wake_up_type: SYSTEM_WAKE_UP_TYPE_UNKNOWN,
sku: sku_idx,
family: family_idx,
};
*curptr = write_and_incr(mem, sys, *curptr)?;
*curptr = write_string(mem, manufacturer, *curptr)?;
*curptr = write_string(mem, product, *curptr)?;
*curptr = write_opt_string(mem, version, *curptr)?;
*curptr = write_opt_string(mem, serial, *curptr)?;
*curptr = write_opt_string(mem, sku, *curptr)?;
*curptr = write_opt_string(mem, family, *curptr)?;
*curptr = write_and_incr(mem, 0u8, *curptr)?;
Ok(())
}
fn write_type3_chassis(
mem: &GuestMemoryMmap,
curptr: &mut GuestAddress,
handle: &mut u16,
chassis: &SmbiosChassisConfig,
) -> Result<()> {
*handle += 1;
let asset_tag = chassis.asset_tag.as_deref();
let mut next = 1u8;
let asset_idx = alloc_index(&mut next, asset_tag.is_some())?;
let ch = SmbiosChassis {
r#type: SYSTEM_ENCLOSURE,
length: size_of::<SmbiosChassis>() as u8,
handle: *handle,
manufacturer: 0,
chassis_type: CHASSIS_TYPE_UNKNOWN,
version: 0,
serial_number: 0,
asset_tag: asset_idx,
bootup_state: CHASSIS_STATE_UNKNOWN,
power_supply_state: CHASSIS_STATE_UNKNOWN,
thermal_state: CHASSIS_STATE_UNKNOWN,
security_status: CHASSIS_SECURITY_STATUS_NONE,
contained_element_count: 0,
contained_element_record_length: 0,
..Default::default()
};
*curptr = write_and_incr(mem, ch, *curptr)?;
*curptr = write_opt_string(mem, asset_tag, *curptr)?;
*curptr = write_string_terminator(mem, *curptr, asset_tag.is_some())?;
Ok(())
}
pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Result<u64> {
let system = smbios.and_then(|cfg| cfg.system.as_ref());
let chassis = smbios.and_then(|cfg| cfg.chassis.as_ref());
let oem_strings: &[String] = smbios.map_or(&[], |cfg| &cfg.oem_strings);
let physptr = GuestAddress(SMBIOS_START)
.checked_add(mem::size_of::<Smbios30Entrypoint>() as u64)
.checked_add(size_of::<Smbios30Entrypoint>() as u64)
.ok_or(Error::NotEnoughMemory)?;
let mut curptr = physptr;
let mut handle = 0;
@@ -179,7 +380,7 @@ pub fn setup_smbios(
handle += 1;
let smbios_biosinfo = SmbiosBiosInfo {
r#type: BIOS_INFORMATION,
length: mem::size_of::<SmbiosBiosInfo>() as u8,
length: size_of::<SmbiosBiosInfo>() as u8,
handle,
vendor: 1, // First string written in this section
version: 2, // Second string written in this section
@@ -193,39 +394,18 @@ pub fn setup_smbios(
curptr = write_and_incr(mem, 0u8, curptr)?;
}
{
handle += 1;
write_type1_system(mem, &mut curptr, &mut handle, system)?;
let uuid_number = uuid
.map(Uuid::parse_str)
.transpose()
.map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))?
.unwrap_or(Uuid::nil());
let smbios_sysinfo = SmbiosSysInfo {
r#type: SYSTEM_INFORMATION,
length: mem::size_of::<SmbiosSysInfo>() as u8,
handle,
manufacturer: 1, // First string written in this section
product_name: 2, // Second string written in this section
serial_number: serial_number.map(|_| 3).unwrap_or_default(), // 3rd string
uuid: uuid_number.to_bytes_le(), // set uuid
..Default::default()
};
curptr = write_and_incr(mem, smbios_sysinfo, curptr)?;
curptr = write_string(mem, "Cloud Hypervisor", curptr)?;
curptr = write_string(mem, "cloud-hypervisor", curptr)?;
if let Some(serial_number) = serial_number {
curptr = write_string(mem, serial_number, curptr)?;
}
curptr = write_and_incr(mem, 0u8, curptr)?;
if let Some(chassis) = chassis {
write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?;
}
if let Some(oem_strings) = oem_strings {
if !oem_strings.is_empty() {
handle += 1;
let smbios_oemstrings = SmbiosOemStrings {
r#type: OEM_STRINGS,
length: mem::size_of::<SmbiosOemStrings>() as u8,
length: size_of::<SmbiosOemStrings>() as u8,
handle,
count: oem_strings.len() as u8,
};
@@ -236,14 +416,14 @@ pub fn setup_smbios(
curptr = write_string(mem, s, curptr)?;
}
curptr = write_and_incr(mem, 0u8, curptr)?;
curptr = write_string_terminator(mem, curptr, true)?;
}
{
handle += 1;
let smbios_end = SmbiosEndOfTable {
r#type: END_OF_TABLE,
length: mem::size_of::<SmbiosEndOfTable>() as u8,
length: size_of::<SmbiosEndOfTable>() as u8,
handle,
};
curptr = write_and_incr(mem, smbios_end, curptr)?;
@@ -254,7 +434,7 @@ pub fn setup_smbios(
{
let mut smbios_ep = Smbios30Entrypoint {
signature: *SM3_MAGIC_IDENT,
length: mem::size_of::<Smbios30Entrypoint>() as u8,
length: size_of::<Smbios30Entrypoint>() as u8,
// SMBIOS rev 3.2.0
majorver: 0x03,
minorver: 0x02,
@@ -266,43 +446,261 @@ pub fn setup_smbios(
};
smbios_ep.checksum = compute_checksum(&smbios_ep);
mem.write_obj(smbios_ep, GuestAddress(SMBIOS_START))
.map_err(|_| Error::WriteSmbiosEp)?;
.map_err(Error::WriteSmbiosEp)?;
}
Ok(curptr.unchecked_offset_from(physptr) + std::mem::size_of::<Smbios30Entrypoint>() as u64)
Ok(curptr.unchecked_offset_from(physptr) + size_of::<Smbios30Entrypoint>() as u64)
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn struct_size() {
assert_eq!(
mem::size_of::<Smbios30Entrypoint>(),
0x18usize,
concat!("Size of: ", stringify!(Smbios30Entrypoint))
);
assert_eq!(
mem::size_of::<SmbiosBiosInfo>(),
0x14usize,
concat!("Size of: ", stringify!(SmbiosBiosInfo))
);
assert_eq!(
mem::size_of::<SmbiosSysInfo>(),
0x1busize,
concat!("Size of: ", stringify!(SmbiosSysInfo))
);
/// Collects all strings after a SMBIOS structure, stopping at the double-NUL terminator and returns next addr.
fn read_string_set(mem: &GuestMemoryMmap, addr: GuestAddress) -> (Vec<String>, GuestAddress) {
let mut cur = addr;
let read_byte = |addr: GuestAddress| -> u8 { mem.read_obj(addr).unwrap() };
// SMBIOS string-set: NUL-terminated strings, terminated by an extra NUL.
// Empty string-set is exactly "\0\0".
if read_byte(cur) == 0 {
let next = cur.checked_add(1).unwrap();
assert_eq!(read_byte(next), 0);
return (Vec::new(), next.checked_add(1).unwrap());
}
let mut strings = Vec::new();
loop {
let mut bytes = Vec::new();
loop {
let b = read_byte(cur);
cur = cur.checked_add(1).unwrap();
if b == 0 {
break;
}
bytes.push(b);
}
strings.push(String::from_utf8(bytes).unwrap());
// If the next byte is NUL, that's the extra terminator.
if read_byte(cur) == 0 {
cur = cur.checked_add(1).unwrap();
break;
}
}
(strings, cur)
}
#[test]
fn entrypoint_checksum() {
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
setup_smbios(&mem, None, None, None).unwrap();
setup_smbios(&mem, None).unwrap();
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
assert_eq!(compute_checksum(&smbios_ep), 0);
}
#[test]
fn entrypoint_struct_size() {
assert_eq!(
size_of::<Smbios30Entrypoint>(),
0x18usize,
concat!("Size of: ", stringify!(Smbios30Entrypoint))
);
assert_eq!(
size_of::<SmbiosBiosInfo>(),
0x14usize,
concat!("Size of: ", stringify!(SmbiosBiosInfo))
);
assert_eq!(
size_of::<SmbiosSysInfo>(),
0x1busize,
concat!("Size of: ", stringify!(SmbiosSysInfo))
);
}
#[test]
fn smbios_chassis_empty_string_set_has_double_null() {
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
let smbios = SmbiosConfig {
chassis: Some(SmbiosChassisConfig::default()),
..Default::default()
};
setup_smbios(&mem, Some(&smbios)).unwrap();
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
let mut cur = GuestAddress(smbios_ep.physptr);
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
cur = cur.checked_add(bios.length as u64).unwrap();
let (_, next) = read_string_set(&mem, cur);
cur = next;
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
cur = cur.checked_add(sys.length as u64).unwrap();
let (_, next) = read_string_set(&mem, cur);
cur = next;
let chassis: SmbiosChassis = mem.read_obj(cur).unwrap();
cur = cur.checked_add(chassis.length as u64).unwrap();
// SMBIOS DSP0134 §6.1.3: empty string-set ends with double NUL.
let b0: u8 = mem.read_obj(cur).unwrap();
let b1: u8 = mem.read_obj(cur.checked_add(1).unwrap()).unwrap();
assert_eq!(b0, 0);
assert_eq!(b1, 0);
cur = cur.checked_add(2).unwrap();
let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap();
assert_eq!(end.r#type, END_OF_TABLE);
}
#[test]
fn smbios_chassis_oem_strings_layout() {
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
let smbios = SmbiosConfig {
chassis: Some(SmbiosChassisConfig {
asset_tag: Some("rack1".to_string()),
}),
oem_strings: ["o1".to_string(), "o2".to_string()].into(),
..Default::default()
};
setup_smbios(&mem, Some(&smbios)).unwrap();
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
let mut cur = GuestAddress(smbios_ep.physptr);
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
cur = cur.checked_add(bios.length as u64).unwrap();
let (_, next) = read_string_set(&mem, cur);
cur = next;
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
cur = cur.checked_add(sys.length as u64).unwrap();
let (_, next) = read_string_set(&mem, cur);
cur = next;
let chassis: SmbiosChassis = mem.read_obj(cur).unwrap();
assert_eq!(chassis.r#type, SYSTEM_ENCLOSURE);
assert_eq!(chassis.asset_tag, 1);
cur = cur.checked_add(chassis.length as u64).unwrap();
let (chassis_strings, next) = read_string_set(&mem, cur);
assert_eq!(chassis_strings, vec!["rack1"]);
cur = next;
let oem: SmbiosOemStrings = mem.read_obj(cur).unwrap();
assert_eq!(oem.r#type, OEM_STRINGS);
assert_eq!(oem.count, 2);
cur = cur.checked_add(oem.length as u64).unwrap();
let (oem_strings, next) = read_string_set(&mem, cur);
assert_eq!(oem_strings, vec!["o1", "o2"]);
cur = next;
let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap();
assert_eq!(end.r#type, END_OF_TABLE);
}
#[test]
fn smbios_strings_terminators_default() {
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
setup_smbios(&mem, None).unwrap();
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
let mut cur = GuestAddress(smbios_ep.physptr);
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
assert_eq!(bios.r#type, BIOS_INFORMATION);
cur = cur.checked_add(bios.length as u64).unwrap();
let (bios_strings, next) = read_string_set(&mem, cur);
assert_eq!(bios_strings, vec!["cloud-hypervisor", "0"]);
cur = next;
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
assert_eq!(sys.r#type, SYSTEM_INFORMATION);
assert_eq!(sys.manufacturer, 1);
assert_eq!(sys.product_name, 2);
assert_eq!(sys.version, 0);
assert_eq!(sys.serial_number, 0);
assert_eq!(sys.sku, 0);
assert_eq!(sys.family, 0);
cur = cur.checked_add(sys.length as u64).unwrap();
let (sys_strings, next) = read_string_set(&mem, cur);
assert_eq!(
sys_strings,
vec![DEFAULT_SYSTEM_MANUFACTURER, DEFAULT_SYSTEM_PRODUCT_NAME]
);
cur = next;
let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap();
assert_eq!(end.r#type, END_OF_TABLE);
}
#[test]
fn smbios_strings_too_many() {
let mut next = 1u8;
for _ in 0..255 {
alloc_index(&mut next, true).unwrap();
}
let err = alloc_index(&mut next, true).unwrap_err();
assert!(matches!(err, Error::TooManyStrings));
}
#[test]
fn smbios_uuid_invalid_rejected() {
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
let smbios = SmbiosConfig {
system: Some(SmbiosSystem {
uuid: Some("not-a-uuid".to_string()),
..Default::default()
}),
..Default::default()
};
let err = setup_smbios(&mem, Some(&smbios)).unwrap_err();
assert!(matches!(err, Error::ParseUuid(_, _)));
}
#[test]
fn smbios_uuid_written_le() {
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
let uuid_str = "00112233-4455-6677-8899-aabbccddeeff";
let smbios = SmbiosConfig {
system: Some(SmbiosSystem {
uuid: Some(uuid_str.to_string()),
..Default::default()
}),
..Default::default()
};
setup_smbios(&mem, Some(&smbios)).unwrap();
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
let mut cur = GuestAddress(smbios_ep.physptr);
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
cur = cur.checked_add(bios.length as u64).unwrap();
let (_, next) = read_string_set(&mem, cur);
cur = next;
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
assert_eq!(sys.uuid, Uuid::parse_str(uuid_str).unwrap().to_bytes_le());
}
#[test]
fn smbios_write_fails_with_too_small_memory() {
let mem = GuestMemoryMmap::from_ranges(&[(
GuestAddress(SMBIOS_START),
size_of::<Smbios30Entrypoint>(),
)])
.unwrap();
let err = setup_smbios(&mem, None).unwrap_err();
assert!(matches!(err, Error::WriteData(_)));
}
}

View File

@@ -2,7 +2,8 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::io::{self, Read, Seek, SeekFrom};
use std::slice;
use std::str::FromStr;
use log::{debug, info};
@@ -15,11 +16,11 @@ use crate::GuestMemoryMmap;
#[derive(Error, Debug)]
pub enum TdvfError {
#[error("Failed read TDVF descriptor")]
ReadDescriptor(#[source] std::io::Error),
ReadDescriptor(#[source] io::Error),
#[error("Failed read TDVF descriptor offset")]
ReadDescriptorOffset(#[source] std::io::Error),
ReadDescriptorOffset(#[source] io::Error),
#[error("Failed read GUID table")]
ReadGuidTable(#[source] std::io::Error),
ReadGuidTable(#[source] io::Error),
#[error("Invalid descriptor signature")]
InvalidDescriptorSignature,
#[error("Invalid descriptor size")]
@@ -162,10 +163,7 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
let mut descriptor: TdvfDescriptor = Default::default();
// SAFETY: we read exactly the size of the descriptor header
file.read_exact(unsafe {
std::slice::from_raw_parts_mut(
(&raw mut descriptor).cast(),
std::mem::size_of::<TdvfDescriptor>(),
)
slice::from_raw_parts_mut((&raw mut descriptor).cast(), size_of::<TdvfDescriptor>())
})
.map_err(TdvfError::ReadDescriptor)?;
@@ -174,8 +172,7 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
}
if descriptor.length as usize
!= std::mem::size_of::<TdvfDescriptor>()
+ std::mem::size_of::<TdvfSection>() * descriptor.num_sections as usize
!= size_of::<TdvfDescriptor>() + size_of::<TdvfSection>() * descriptor.num_sections as usize
{
return Err(TdvfError::InvalidDescriptorSize);
}
@@ -189,9 +186,9 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
// SAFETY: we read exactly the advertised sections
file.read_exact(unsafe {
std::slice::from_raw_parts_mut(
slice::from_raw_parts_mut(
sections.as_mut_ptr().cast(),
descriptor.num_sections as usize * std::mem::size_of::<TdvfSection>(),
descriptor.num_sections as usize * size_of::<TdvfSection>(),
)
})
.map_err(TdvfError::ReadDescriptor)?;
@@ -305,7 +302,7 @@ fn align_hob(v: u64) -> u64 {
impl TdHob {
fn update_offset<T>(&mut self) {
self.current_offset = align_hob(self.current_offset + std::mem::size_of::<T>() as u64);
self.current_offset = align_hob(self.current_offset + size_of::<T>() as u64);
}
pub fn start(offset: u64) -> TdHob {
@@ -322,7 +319,7 @@ impl TdHob {
// Write end
let end = HobHeader {
r#type: HobType::EndOfHobList,
length: std::mem::size_of::<HobHeader>() as u16,
length: size_of::<HobHeader>() as u16,
reserved: 0,
};
info!("Writing HOB end {:x} {:x?}", self.current_offset, end);
@@ -335,7 +332,7 @@ impl TdHob {
let handoff = HobHandoffInfoTable {
header: HobHeader {
r#type: HobType::Handoff,
length: std::mem::size_of::<HobHandoffInfoTable>() as u16,
length: size_of::<HobHandoffInfoTable>() as u16,
reserved: 0,
},
version: 0x9,
@@ -362,7 +359,7 @@ impl TdHob {
let resource_descriptor = HobResourceDescriptor {
header: HobHeader {
r#type: HobType::ResourceDescriptor,
length: std::mem::size_of::<HobResourceDescriptor>() as u16,
length: size_of::<HobResourceDescriptor>() as u16,
reserved: 0,
},
owner: EfiGuid::default(),
@@ -439,8 +436,7 @@ impl TdHob {
// We already know the HobGuidType size is 8 bytes multiple, but we
// need the total size to be 8 bytes multiple. That is why the ACPI
// table size must be 8 bytes multiple as well.
let length = std::mem::size_of::<HobGuidType>() as u16
+ align_hob(table_content.len() as u64) as u16;
let length = size_of::<HobGuidType>() as u16 + align_hob(table_content.len() as u64) as u16;
let hob_guid_type = HobGuidType {
header: HobHeader {
r#type: HobType::GuidExtension,
@@ -462,7 +458,7 @@ impl TdHob {
);
mem.write_obj(hob_guid_type, GuestAddress(self.current_offset))
.map_err(TdvfError::GuestMemoryWriteHob)?;
let current_offset = self.current_offset + std::mem::size_of::<HobGuidType>() as u64;
let current_offset = self.current_offset + size_of::<HobGuidType>() as u64;
// In case the table is quite large, let's make sure we can handle
// retrying until everything has been correctly copied.
@@ -493,7 +489,7 @@ impl TdHob {
guid_type: HobGuidType {
header: HobHeader {
r#type: HobType::GuidExtension,
length: std::mem::size_of::<TdPayload>() as u16,
length: size_of::<TdPayload>() as u16,
reserved: 0,
},
// HOB_PAYLOAD_INFO_GUID
@@ -526,7 +522,7 @@ mod unit_tests {
#[test]
#[ignore]
fn test_parse_tdvf_sections() {
let mut f = std::fs::File::open("tdvf.fd").unwrap();
let mut f = File::open("tdvf.fd").unwrap();
let (sections, _) = parse_tdvf_sections(&mut f).unwrap();
for section in sections {
eprintln!("{section:x?}");

View File

@@ -8,18 +8,19 @@ version = "0.1.0"
[features]
default = []
io_uring = ["dep:io-uring"]
test-utils = []
[dependencies]
bitflags = { workspace = true }
byteorder = { workspace = true }
crc-any = "2.5.0"
crc-any = "3.0.0"
flate2 = "1.1"
io-uring = { version = "0.7.12", optional = true }
libc = { workspace = true }
log = { workspace = true }
remain = "0.2.15"
serde = { workspace = true, features = ["derive"] }
smallvec = "1.15.1"
smallvec = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
virtio-bindings = { workspace = true }
@@ -31,6 +32,7 @@ vm-memory = { workspace = true, features = [
] }
vm-virtio = { path = "../vm-virtio" }
vmm-sys-util = { workspace = true }
zerocopy = { workspace = true, features = ["derive"] }
zstd = "0.13"
[dev-dependencies]

269
block/src/aligned_buffer.rs Normal file
View File

@@ -0,0 +1,269 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::os::unix::fs::FileExt;
use std::{io, slice};
/// RAII aligned heap buffer for O_DIRECT I/O.
///
/// Handles the alignment math for offset and length, allocating a buffer
/// that satisfies O_DIRECT constraints. The caller's logical data lives
/// at `as_slice()`/`as_mut_slice()` (accounting for head padding when the
/// requested offset is not alignment-aligned). The full aligned region is
/// used internally for pread/pwrite via `FileExt`.
pub(crate) struct AlignedBuffer {
ptr: *mut u8,
layout: Layout,
head_pad: usize,
user_len: usize,
aligned_len: usize,
aligned_offset: u64,
}
impl AlignedBuffer {
/// Create a new aligned buffer for I/O at `offset` of `len` bytes with
/// the given `alignment` requirement.
///
/// When offset and length are already aligned, `head_pad == 0` and the
/// full buffer equals the user's logical portion (no overhead).
pub fn new(offset: u64, len: usize, alignment: usize) -> io::Result<Self> {
if alignment == 0 || !alignment.is_power_of_two() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"alignment must be a non-zero power of two",
));
}
let mask = alignment as u64 - 1;
let aligned_offset = offset & !mask;
let head_pad = (offset - aligned_offset) as usize;
let min_len = head_pad
.checked_add(len)
.ok_or_else(|| io::Error::other("aligned buffer length overflow"))?;
let aligned_len = if min_len == 0 {
0
} else {
let remainder = min_len % alignment;
if remainder == 0 {
min_len
} else {
min_len
.checked_add(alignment - remainder)
.ok_or_else(|| io::Error::other("aligned buffer length overflow"))?
}
};
// alloc_zeroed is UB on a zero-sized layout, so round the allocation
// up to one alignment unit for the zero-length case. The padding is
// never exposed: as_slice/full_slice report aligned_len/user_len (0).
let layout = Layout::from_size_align(aligned_len.max(alignment), alignment)
.map_err(|e| io::Error::other(format!("invalid aligned layout: {e}")))?;
// SAFETY: layout has non-zero size.
let ptr = unsafe { alloc_zeroed(layout) };
if ptr.is_null() {
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"aligned allocation failed",
));
}
Ok(AlignedBuffer {
ptr,
layout,
head_pad,
user_len: len,
aligned_len,
aligned_offset,
})
}
/// The caller's logical portion of the buffer (read-only).
pub fn as_slice(&self) -> &[u8] {
// SAFETY: ptr is valid for layout.size() bytes; head_pad + user_len <= layout.size().
unsafe { slice::from_raw_parts(self.ptr.add(self.head_pad), self.user_len) }
}
/// The caller's logical portion of the buffer (mutable).
pub fn as_mut_slice(&mut self) -> &mut [u8] {
// SAFETY: ptr is valid for layout.size() bytes; head_pad + user_len <= layout.size().
unsafe { slice::from_raw_parts_mut(self.ptr.add(self.head_pad), self.user_len) }
}
fn full_slice(&self) -> &[u8] {
// SAFETY: ptr is valid for layout.size() bytes; aligned_len <= layout.size().
unsafe { slice::from_raw_parts(self.ptr, self.aligned_len) }
}
fn full_mut_slice(&mut self) -> &mut [u8] {
// SAFETY: ptr is valid for layout.size() bytes; aligned_len <= layout.size().
unsafe { slice::from_raw_parts_mut(self.ptr, self.aligned_len) }
}
/// Read into the buffer from `f`, tolerating a short read at EOF.
///
/// Returns the number of caller-logical bytes now valid in `as_slice()`,
/// accounting for head padding and any short read.
pub fn read_from(&mut self, f: &impl FileExt) -> io::Result<usize> {
let mut total = 0usize;
while total < self.aligned_len {
let offset = self
.aligned_offset
.checked_add(total as u64)
.ok_or_else(|| io::Error::other("aligned buffer offset overflow"))?;
match f.read_at(&mut self.full_mut_slice()[total..], offset) {
Ok(0) => break,
Ok(n) => total += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(total.saturating_sub(self.head_pad).min(self.user_len))
}
/// Write the full aligned region from this buffer to `f`.
pub fn write_to(&self, f: &impl FileExt) -> io::Result<()> {
f.write_all_at(self.full_slice(), self.aligned_offset)
}
}
impl Drop for AlignedBuffer {
fn drop(&mut self) {
// SAFETY: ptr was allocated by alloc_zeroed with self.layout.
unsafe { dealloc(self.ptr, self.layout) };
}
}
// SAFETY: The buffer is a plain heap allocation with no interior references.
unsafe impl Send for AlignedBuffer {}
#[cfg(test)]
mod tests {
use std::io::Write;
use std::os::unix::fs::FileExt;
use vmm_sys_util::tempfile::TempFile;
use super::*;
fn create_pattern_file(size: usize) -> TempFile {
let tf = TempFile::new().unwrap();
let pattern: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
tf.as_file().write_all(&pattern).unwrap();
tf.as_file().sync_all().unwrap();
tf
}
#[test]
fn test_read_aligned() {
let size = 4096usize;
let tf = create_pattern_file(size);
let alignment = 512;
let mut abuf = AlignedBuffer::new(0, size, alignment).unwrap();
abuf.read_from(tf.as_file()).unwrap();
let expected: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
assert_eq!(abuf.as_slice(), &expected[..]);
}
#[test]
fn test_zero_len_is_noop() {
let tf = create_pattern_file(512);
let mut abuf = AlignedBuffer::new(100, 0, 512).unwrap();
abuf.read_from(tf.as_file()).unwrap();
abuf.write_to(tf.as_file()).unwrap();
assert!(abuf.as_slice().is_empty());
assert!(abuf.as_mut_slice().is_empty());
}
#[test]
fn test_read_unaligned_offset() {
let file_size = 8192usize;
let tf = create_pattern_file(file_size);
let alignment = 512;
let offset = 100u64;
let len = 200usize;
let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap();
abuf.read_from(tf.as_file()).unwrap();
let expected: Vec<u8> = (offset as usize..offset as usize + len)
.map(|i| (i % 251) as u8)
.collect();
assert_eq!(abuf.as_slice(), &expected[..]);
}
#[test]
fn test_write_aligned() {
let size = 4096usize;
let tf = create_pattern_file(size);
let alignment = 512;
let data: Vec<u8> = (0..size).map(|i| ((i + 1) % 251) as u8).collect();
let mut abuf = AlignedBuffer::new(0, size, alignment).unwrap();
abuf.as_mut_slice().copy_from_slice(&data);
abuf.write_to(tf.as_file()).unwrap();
let mut readback = vec![0u8; size];
tf.as_file().read_exact_at(&mut readback, 0).unwrap();
assert_eq!(readback, data);
}
#[test]
fn test_write_unaligned_offset_rmw() {
let file_size = 8192usize;
let tf = create_pattern_file(file_size);
let alignment = 512;
let offset = 100u64;
let len = 200usize;
let data: Vec<u8> = (0..len).map(|i| ((i + 1) % 239) as u8).collect();
let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap();
abuf.read_from(tf.as_file()).unwrap();
abuf.as_mut_slice().copy_from_slice(&data);
abuf.write_to(tf.as_file()).unwrap();
let mut whole = vec![0u8; file_size];
tf.as_file().read_exact_at(&mut whole, 0).unwrap();
let before: Vec<u8> = (0..offset as usize).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[..offset as usize], &before[..]);
assert_eq!(&whole[offset as usize..offset as usize + len], &data[..]);
let after_start = offset as usize + len;
let after: Vec<u8> = (after_start..file_size).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[after_start..], &after[..]);
}
#[test]
fn test_4096_alignment() {
let file_size = 16384usize;
let tf = create_pattern_file(file_size);
let alignment = 4096;
let offset = 4096u64;
let len = 4096usize;
let data: Vec<u8> = (0..len).map(|i| ((i + 1) % 239) as u8).collect();
let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap();
abuf.read_from(tf.as_file()).unwrap();
abuf.as_mut_slice().copy_from_slice(&data);
abuf.write_to(tf.as_file()).unwrap();
let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap();
abuf.read_from(tf.as_file()).unwrap();
assert_eq!(abuf.as_slice(), &data[..]);
let mut whole = vec![0u8; file_size];
tf.as_file().read_exact_at(&mut whole, 0).unwrap();
let before: Vec<u8> = (0..offset as usize).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[..offset as usize], &before[..]);
let after_start = offset as usize + len;
let after: Vec<u8> = (after_start..file_size).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[after_start..], &after[..]);
}
}

402
block/src/aligned_file.rs Normal file
View File

@@ -0,0 +1,402 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::{File, Metadata};
use std::io;
use std::os::fd::{AsFd, BorrowedFd};
use std::os::unix::fs::FileExt;
use std::os::unix::io::{AsRawFd, RawFd};
use vmm_sys_util::file_traits::FileSync;
use vmm_sys_util::seek_hole::SeekHole;
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
use crate::aligned_buffer::AlignedBuffer;
use crate::{SECTOR_SIZE, probe_direct_alignment};
/// True when `buf_ptr`/`len`/`offset` already satisfy `alignment`
/// (`alignment == 0` means no O_DIRECT, so everything is "aligned").
fn is_aligned(alignment: usize, buf_ptr: usize, len: usize, offset: u64) -> bool {
alignment == 0
|| (buf_ptr.is_multiple_of(alignment)
&& len.is_multiple_of(alignment)
&& offset.is_multiple_of(alignment as u64))
}
/// A `File` that transparently satisfies O_DIRECT alignment requirements.
///
/// `alignment == 0` means no O_DIRECT (all I/O passes straight through).
/// For unaligned requests under O_DIRECT, I/O is bounced through an
/// `AlignedBuffer` (read-modify-write for writes).
#[derive(Debug)]
pub struct AlignedFile {
file: File,
alignment: usize,
}
impl AlignedFile {
/// Wrap `file`, querying the O_DIRECT block alignment when `direct_io`.
pub fn new(file: File, direct_io: bool) -> Self {
let alignment = if direct_io {
probe_direct_alignment(file.as_raw_fd()).unwrap_or(SECTOR_SIZE) as usize
} else {
0
};
AlignedFile { file, alignment }
}
pub fn alignment(&self) -> usize {
self.alignment
}
pub fn file(&self) -> &File {
&self.file
}
pub fn file_mut(&mut self) -> &mut File {
&mut self.file
}
pub fn try_clone(&self) -> io::Result<Self> {
Ok(AlignedFile {
file: self.file.try_clone()?,
alignment: self.alignment,
})
}
pub fn set_len(&self, size: u64) -> io::Result<()> {
self.file.set_len(size)
}
pub fn metadata(&self) -> io::Result<Metadata> {
self.file.metadata()
}
pub fn sync_all(&self) -> io::Result<()> {
self.file.sync_all()
}
pub fn sync_data(&self) -> io::Result<()> {
self.file.sync_data()
}
pub fn is_direct(&self) -> bool {
self.alignment != 0
}
pub fn is_writable(&self) -> bool {
// SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor
let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return false;
}
let access_mode = flags & libc::O_ACCMODE;
access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR
}
/// Wrap `file` with an explicit alignment, bypassing the probe. Used by
/// tests to force the bounce/RMW path without a real O_DIRECT fd.
#[cfg(test)]
pub fn with_alignment(file: File, alignment: usize) -> Self {
AlignedFile { file, alignment }
}
/// Read `len` bytes at `offset` through an aligned bounce buffer.
pub(crate) fn read_unaligned(
&self,
offset: u64,
len: usize,
scatter: impl FnOnce(&[u8]) -> io::Result<()>,
) -> io::Result<usize> {
let mut abuf = AlignedBuffer::new(offset, len, self.alignment)?;
let n = abuf.read_from(&self.file)?;
scatter(&abuf.as_slice()[..n])?;
Ok(n)
}
/// Write `len` bytes at `offset` through an aligned bounce buffer.
pub(crate) fn write_unaligned(
&self,
offset: u64,
len: usize,
gather: impl FnOnce(&mut [u8]) -> io::Result<()>,
) -> io::Result<usize> {
let mut abuf = AlignedBuffer::new(offset, len, self.alignment)?;
abuf.read_from(&self.file)?; // RMW: preserve head/tail padding
gather(abuf.as_mut_slice())?;
abuf.write_to(&self.file)?;
Ok(len)
}
}
impl FileExt for AlignedFile {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
if is_aligned(self.alignment, buf.as_ptr() as usize, buf.len(), offset) {
return self.file.read_at(buf, offset);
}
self.read_unaligned(offset, buf.len(), |data| {
buf[..data.len()].copy_from_slice(data);
Ok(())
})
}
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
if is_aligned(self.alignment, buf.as_ptr() as usize, buf.len(), offset) {
return self.file.write_at(buf, offset);
}
self.write_unaligned(offset, buf.len(), |dst| {
dst.copy_from_slice(buf);
Ok(())
})
}
}
impl WriteZeroesAt for AlignedFile {
fn write_zeroes_at(&mut self, offset: u64, length: usize) -> io::Result<usize> {
self.file.write_zeroes_at(offset, length)
}
}
impl PunchHole for AlignedFile {
fn punch_hole(&mut self, offset: u64, length: u64) -> io::Result<()> {
self.file.punch_hole(offset, length)
}
}
impl FileSync for AlignedFile {
fn fsync(&mut self) -> io::Result<()> {
self.file.fsync()
}
}
impl SeekHole for AlignedFile {
fn seek_hole(&mut self, offset: u64) -> io::Result<Option<u64>> {
self.file.seek_hole(offset)
}
fn seek_data(&mut self, offset: u64) -> io::Result<Option<u64>> {
self.file.seek_data(offset)
}
}
impl Clone for AlignedFile {
fn clone(&self) -> Self {
self.try_clone().expect("AlignedFile cloning failed")
}
}
impl AsRawFd for AlignedFile {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl AsFd for AlignedFile {
fn as_fd(&self) -> BorrowedFd<'_> {
self.file.as_fd()
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use std::os::unix::fs::FileExt;
use vmm_sys_util::tempfile::TempFile;
use super::*;
fn pattern_file(size: usize) -> TempFile {
let tf = TempFile::new().unwrap();
let p: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
tf.as_file().write_all(&p).unwrap();
tf.as_file().sync_all().unwrap();
tf
}
fn forced(file: File, alignment: usize) -> AlignedFile {
AlignedFile { file, alignment }
}
#[test]
fn new_probes_alignment_and_accessors() {
let tf = pattern_file(8192);
// Not O_DIRECT, so new() falls back to SECTOR_SIZE (512).
let mut af = AlignedFile::new(tf.as_file().try_clone().unwrap(), true);
assert_eq!(af.alignment(), 512);
let _ = af.file();
let _ = af.file_mut();
let _ = af.try_clone().unwrap();
let plain = AlignedFile::new(tf.as_file().try_clone().unwrap(), false);
assert_eq!(plain.alignment(), 0);
}
#[test]
fn read_unaligned_offset_matches_contents() {
let tf = pattern_file(8192);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let mut buf = vec![0u8; 200];
assert_eq!(af.read_at(&mut buf, 100).unwrap(), 200);
let want: Vec<u8> = (100..300).map(|i| (i % 251) as u8).collect();
assert_eq!(buf, want);
}
#[test]
fn read_unaligned_short_at_eof() {
let tf = pattern_file(100);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let mut buf = vec![0u8; 200];
assert_eq!(af.read_at(&mut buf, 10).unwrap(), 90);
}
#[test]
fn write_unaligned_offset_is_rmw() {
let tf = pattern_file(8192);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let data: Vec<u8> = (0..200).map(|i| ((i + 1) % 239) as u8).collect();
assert_eq!(af.write_at(&data, 100).unwrap(), 200);
let mut whole = vec![0u8; 8192];
tf.as_file().read_exact_at(&mut whole, 0).unwrap();
let before: Vec<u8> = (0..100).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[..100], &before[..]);
assert_eq!(&whole[100..300], &data[..]);
let after: Vec<u8> = (300..8192).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[300..], &after[..]);
}
#[test]
fn aligned_passthrough_roundtrip() {
let tf = pattern_file(4096);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let mut buf = vec![0u8; 512];
assert_eq!(af.read_at(&mut buf, 512).unwrap(), 512);
let want: Vec<u8> = (512..1024).map(|i| (i % 251) as u8).collect();
assert_eq!(buf, want);
}
#[test]
fn no_alignment_is_plain_passthrough() {
let tf = pattern_file(100);
let af = forced(tf.as_file().try_clone().unwrap(), 0);
let mut buf = vec![0u8; 50];
assert_eq!(af.read_at(&mut buf, 10).unwrap(), 50);
}
#[test]
fn test_unaligned_read_beyond_eof_returns_zero() {
let tf = pattern_file(100);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let mut buf = vec![0u8; 16];
assert_eq!(af.read_at(&mut buf, 200).unwrap(), 0);
}
#[test]
fn test_unaligned_write_extends_at_eof() {
let file_size = 100usize;
let tf = pattern_file(file_size);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let data = b"xyz";
assert_eq!(af.write_at(data, file_size as u64).unwrap(), data.len());
let mut readback = vec![0u8; file_size + data.len()];
tf.as_file().read_exact_at(&mut readback, 0).unwrap();
let expected_prefix: Vec<u8> = (0..file_size).map(|i| (i % 251) as u8).collect();
assert_eq!(&readback[..file_size], &expected_prefix[..]);
assert_eq!(&readback[file_size..], data);
}
#[test]
fn test_empty_unaligned_io_is_noop() {
let tf = pattern_file(100);
let af = forced(tf.as_file().try_clone().unwrap(), 512);
let mut read_buf = [];
assert_eq!(af.read_at(&mut read_buf, 1).unwrap(), 0);
assert_eq!(af.write_at(&[], 1).unwrap(), 0);
}
#[test]
fn read_unaligned_scatters_in_a_single_copy() {
let file = pattern_file(8192);
let aligned_file = forced(file.as_file().try_clone().unwrap(), 512);
let mut out = vec![0u8; 200];
let n = aligned_file
.read_unaligned(100, 200, |data| {
out.copy_from_slice(data);
Ok(())
})
.unwrap();
assert_eq!(n, 200);
let want: Vec<u8> = (100..300).map(|i| (i % 251) as u8).collect();
assert_eq!(out, want);
}
#[test]
fn read_unaligned_closure_short_at_eof() {
let file = pattern_file(100);
let aligned_file = forced(file.as_file().try_clone().unwrap(), 512);
let mut seen = 0usize;
let n = aligned_file
.read_unaligned(10, 200, |data| {
seen = data.len();
Ok(())
})
.unwrap();
assert_eq!(n, 90);
assert_eq!(seen, 90);
}
#[test]
fn write_unaligned_gather_is_rmw() {
let file = pattern_file(8192);
let aligned_file = forced(file.as_file().try_clone().unwrap(), 512);
let data: Vec<u8> = (0..200).map(|i| ((i + 1) % 239) as u8).collect();
let n = aligned_file
.write_unaligned(100, 200, |buf| {
buf.copy_from_slice(&data);
Ok(())
})
.unwrap();
assert_eq!(n, 200);
let mut whole = vec![0u8; 8192];
file.as_file().read_exact_at(&mut whole, 0).unwrap();
let before: Vec<u8> = (0..100).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[..100], &before[..]);
assert_eq!(&whole[100..300], &data[..]);
let after: Vec<u8> = (300..8192).map(|i| (i % 251) as u8).collect();
assert_eq!(&whole[300..], &after[..]);
}
#[test]
fn read_unaligned_propagates_closure_error() {
let file = pattern_file(8192);
let aligned_file = forced(file.as_file().try_clone().unwrap(), 512);
let err = aligned_file
.read_unaligned(100, 200, |_| {
Err(io::Error::new(io::ErrorKind::InvalidInput, "boom"))
})
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn write_unaligned_propagates_closure_error() {
let file = pattern_file(8192);
let aligned_file = forced(file.as_file().try_clone().unwrap(), 512);
let err = aligned_file
.write_unaligned(100, 200, |_| {
Err(io::Error::new(io::ErrorKind::InvalidInput, "boom"))
})
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
}

View File

@@ -1,90 +0,0 @@
// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::io;
use vm_memory::GuestAddress;
/// Owns an aligned bounce buffer used when a guest descriptor's host VA
/// does not meet the disk backend's alignment requirement.
#[derive(Debug)]
pub struct AlignedOperation {
data_addr: GuestAddress,
aligned_ptr: *mut u8,
size: usize,
layout: Layout,
}
impl AlignedOperation {
/// Allocate a zero-initialized buffer of `size` bytes aligned to
/// `alignment`. Returns `InvalidInput` if `size` is zero;
/// `alignment` must be a power of two and not exceed `isize::MAX`
/// after rounding up.
pub fn new(data_addr: GuestAddress, size: usize, alignment: usize) -> io::Result<Self> {
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"AlignedOperation requires a non-zero size",
));
}
let layout = Layout::from_size_align(size, alignment)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
// SAFETY: size is non-zero (checked above) and Layout::from_size_align
// rejects alignments that are not a power of two or that overflow.
let aligned_ptr = unsafe { alloc_zeroed(layout) };
if aligned_ptr.is_null() {
return Err(io::Error::last_os_error());
}
Ok(Self {
data_addr,
aligned_ptr,
size,
layout,
})
}
/// Gets the raw pointer to the aligned buffer.
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.aligned_ptr
}
/// Returns the aligned buffer as a slice.
pub fn as_bytes(&self) -> &[u8] {
// SAFETY: `new` allocates `size` bytes via alloc_zeroed (so they
// are initialized) and AlignedOperation owns the buffer
// exclusively.
unsafe { std::slice::from_raw_parts(self.aligned_ptr, self.size) }
}
/// Returns the aligned buffer as a mutable slice.
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
// SAFETY: same invariant as as_bytes; &mut self rules out other
// simultaneous borrows.
unsafe { std::slice::from_raw_parts_mut(self.aligned_ptr, self.size) }
}
/// Returns the guest address for this op.
pub fn data_addr(&self) -> GuestAddress {
self.data_addr
}
}
impl Drop for AlignedOperation {
fn drop(&mut self) {
// SAFETY: `new` is the only constructor, and it stores a pointer
// returned by `alloc_zeroed` paired with the exact `layout` used
// for that allocation. Ownership has not escaped (the type is
// neither `Clone` nor `Copy`).
unsafe {
dealloc(self.aligned_ptr, self.layout);
}
}
}
// SAFETY: AlignedOperation owns its heap allocation exclusively (no Clone/
// Copy, no shared aliases) and the allocation's lifetime is tied to the
// value's. Moving an AlignedOperation between threads transfers that
// ownership — the same rationale Box<T> uses for its Send impl.
unsafe impl Send for AlignedOperation {}

View File

@@ -1,112 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::marker::PhantomData;
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use thiserror::Error;
use vmm_sys_util::eventfd::EventFd;
use crate::{BatchRequest, SECTOR_SIZE};
#[derive(Error, Debug)]
pub enum DiskFileError {
/// Failed getting disk file size.
#[error("Failed getting disk file size")]
Size(#[source] std::io::Error),
/// Failed creating a new AsyncIo.
#[error("Failed creating a new AsyncIo")]
NewAsyncIo(#[source] std::io::Error),
/// Unsupported operation.
#[error("Unsupported operation")]
Unsupported,
/// Resize failed
#[error("Resize failed")]
ResizeError(#[source] std::io::Error),
#[error("Failed cloning disk file")]
Clone(#[source] std::io::Error),
}
pub type DiskFileResult<T> = std::result::Result<T, DiskFileError>;
/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding disk file.
///
/// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations
/// by some disk implementations, which wrap the effective [`File`]
/// in an `Arc<Mutex<T>>`, making the use of [`BorrowedFd`] impossible.
///
/// [`BorrowedFd`]: std::os::fd::BorrowedFd
#[derive(Copy, Clone, Debug)]
pub struct BorrowedDiskFd<'fd> {
raw_fd: RawFd,
_lifetime: PhantomData<&'fd OwnedFd>,
}
impl BorrowedDiskFd<'_> {
pub(super) fn new(raw_fd: RawFd) -> Self {
Self {
raw_fd,
_lifetime: PhantomData,
}
}
}
impl AsRawFd for BorrowedDiskFd<'_> {
fn as_raw_fd(&self) -> RawFd {
self.raw_fd
}
}
#[derive(Error, Debug)]
pub enum AsyncIoError {
/// Failed vectored reading from file.
#[error("Failed vectored reading from file")]
ReadVectored(#[source] std::io::Error),
/// Failed vectored writing to file.
#[error("Failed vectored writing to file")]
WriteVectored(#[source] std::io::Error),
/// Failed synchronizing file.
#[error("Failed synchronizing file")]
Fsync(#[source] std::io::Error),
/// Failed punching hole.
#[error("Failed punching hole")]
PunchHole(#[source] std::io::Error),
/// Failed writing zeroes.
#[error("Failed writing zeroes")]
WriteZeroes(#[source] std::io::Error),
/// Failed submitting batch requests.
#[error("Failed submitting batch requests")]
SubmitBatchRequests(#[source] std::io::Error),
}
pub type AsyncIoResult<T> = std::result::Result<T, AsyncIoError>;
pub trait AsyncIo: Send {
fn notifier(&self) -> &EventFd;
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()>;
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()>;
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()>;
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
fn next_completed_request(&mut self) -> Option<(u64, i32)>;
fn batch_requests_enabled(&self) -> bool {
false
}
fn submit_batch_requests(&mut self, _batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
Ok(())
}
fn alignment(&self) -> u64 {
SECTOR_SIZE
}
}

View File

@@ -20,10 +20,10 @@ use log::info;
use crate::block_io_uring_is_supported;
use crate::disk_file::AsyncFullDiskFile;
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::fixed_vhd_disk::FixedVhdDisk;
use crate::qcow_disk::QcowDisk;
use crate::raw_disk::{RawBackend, RawDisk};
use crate::vhdx_sync::VhdxDiskSync;
use crate::formats::qcow::QcowDisk;
use crate::formats::raw::{RawBackend, RawDisk};
use crate::formats::vhd::VhdDisk;
use crate::formats::vhdx::VhdxDisk;
use crate::{
ImageType, block_aio_is_supported, detect_image_type, open_disk_image, preallocate_disk,
};
@@ -114,7 +114,7 @@ fn open_vhdx(
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
info!("Opening VHDX disk file with synchronous backend");
Ok(Box::new(
VhdxDiskSync::new(file).map_err(|e| e.with_path(options.path))?,
VhdxDisk::new(file, options.direct).map_err(|e| e.with_path(options.path))?,
))
}
@@ -127,7 +127,7 @@ fn open_fixed_vhd(
if io_uring_supported() {
info!("Opening fixed VHD disk file with io_uring backend");
return Ok(Box::new(
FixedVhdDisk::new(file, true).map_err(|e| e.with_path(options.path))?,
VhdDisk::new(file, true, options.direct).map_err(|e| e.with_path(options.path))?,
));
}
info!("io_uring runtime probe failed for fixed VHD, using synchronous backend");
@@ -135,7 +135,7 @@ fn open_fixed_vhd(
info!("Opening fixed VHD disk file with synchronous backend");
Ok(Box::new(
FixedVhdDisk::new(file, false).map_err(|e| e.with_path(options.path))?,
VhdDisk::new(file, false, options.direct).map_err(|e| e.with_path(options.path))?,
))
}
@@ -151,7 +151,11 @@ fn open_raw(
if !options.disable_io_uring {
if io_uring_supported() {
info!("Opening RAW disk file with io_uring backend");
return Ok(Box::new(RawDisk::new(file, RawBackend::IoUring)));
return Ok(Box::new(RawDisk::new(
file,
RawBackend::IoUring,
options.direct,
)));
}
info!("io_uring runtime probe failed for RAW, trying next backend");
}
@@ -159,13 +163,21 @@ fn open_raw(
if !options.disable_aio {
if aio_supported() {
info!("Opening RAW disk file with AIO backend");
return Ok(Box::new(RawDisk::new(file, RawBackend::Aio)));
return Ok(Box::new(RawDisk::new(
file,
RawBackend::Aio,
options.direct,
)));
}
info!("AIO runtime probe failed for RAW, using synchronous backend");
}
info!("Opening RAW disk file with synchronous backend");
Ok(Box::new(RawDisk::new(file, RawBackend::Sync)))
Ok(Box::new(RawDisk::new(
file,
RawBackend::Sync,
options.direct,
)))
}
fn open_qcow2(
@@ -205,13 +217,12 @@ fn open_qcow2(
#[cfg(test)]
mod unit_tests {
use std::io::Write;
use std::path::Path;
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::qcow::{QcowFile, RawFile};
use crate::formats::qcow;
fn default_options(path: &Path) -> DiskOpenOptions<'_> {
DiskOpenOptions {
@@ -247,12 +258,9 @@ mod unit_tests {
#[test]
fn detect_qcow2_image() {
let tmp = TempFile::new().unwrap();
{
let raw = RawFile::new(tmp.as_file().try_clone().unwrap(), false);
let mut qcow = QcowFile::new(raw, 3, 100 * 1024 * 1024, true).unwrap();
qcow.flush().unwrap();
}
let tmp = qcow::QcowTempDisk::new(100 * 1024 * 1024, None, false, true, false)
.unwrap()
.into_tempfile();
let path = tmp.as_path().to_owned();
let options = default_options(&path);
let opened = open_disk(&options).unwrap();

View File

@@ -1,99 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::io::{AsRawFd, RawFd};
use crate::BlockBackend;
use crate::vhd::VhdFooter;
#[derive(Debug)]
pub struct FixedVhd {
file: File,
size: u64,
position: u64,
}
impl FixedVhd {
pub fn new(mut file: File) -> std::io::Result<Self> {
let footer = VhdFooter::new(&mut file)?;
Ok(Self {
file,
size: footer.current_size(),
position: 0,
})
}
}
impl AsRawFd for FixedVhd {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl Read for FixedVhd {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self.file.read(buf) {
Ok(r) => {
self.position = self.position.checked_add(r.try_into().unwrap()).unwrap();
Ok(r)
}
Err(e) => Err(e),
}
}
}
impl Write for FixedVhd {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self.file.write(buf) {
Ok(r) => {
self.position = self.position.checked_add(r.try_into().unwrap()).unwrap();
Ok(r)
}
Err(e) => Err(e),
}
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.sync_all()
}
}
impl Seek for FixedVhd {
fn seek(&mut self, newpos: SeekFrom) -> std::io::Result<u64> {
match self.file.seek(newpos) {
Ok(pos) => {
self.position = pos;
Ok(pos)
}
Err(e) => Err(e),
}
}
}
impl BlockBackend for FixedVhd {
fn logical_size(&self) -> Result<u64, crate::Error> {
Ok(self.size)
}
/// Returns the physical size of the underlying file.
fn physical_size(&self) -> Result<u64, crate::Error> {
self.file
.metadata()
.map(|m| m.len())
.map_err(crate::Error::GetFileMetadata)
}
}
impl Clone for FixedVhd {
fn clone(&self) -> Self {
Self {
file: self.file.try_clone().expect("FixedVhd cloning failed"),
size: self.size,
position: self.position,
}
}
}

View File

@@ -1,101 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::os::unix::io::RawFd;
use vmm_sys_util::eventfd::EventFd;
use crate::BatchRequest;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
use crate::error::BlockResult;
use crate::raw_async::RawFileAsync;
pub struct FixedVhdAsync {
raw_file_async: RawFileAsync,
size: u64,
}
impl FixedVhdAsync {
pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> BlockResult<Self> {
let raw_file_async = RawFileAsync::new(fd, ring_depth)?;
Ok(FixedVhdAsync {
raw_file_async,
size,
})
}
}
impl AsyncIo for FixedVhdAsync {
fn notifier(&self) -> &EventFd {
self.raw_file_async.notifier()
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
if offset as u64 >= self.size {
return Err(AsyncIoError::ReadVectored(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid offset {}, can't be larger than file size {}",
offset, self.size
),
)));
}
self.raw_file_async.read_vectored(offset, iovecs, user_data)
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
if offset as u64 >= self.size {
return Err(AsyncIoError::WriteVectored(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid offset {}, can't be larger than file size {}",
offset, self.size
),
)));
}
self.raw_file_async
.write_vectored(offset, iovecs, user_data)
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.raw_file_async.fsync(user_data)
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
self.raw_file_async.next_completed_request()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::PunchHole(std::io::Error::other(
"punch_hole not supported for fixed VHD",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
"write_zeroes not supported for fixed VHD",
)))
}
fn batch_requests_enabled(&self) -> bool {
true
}
fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
self.raw_file_async.submit_batch_requests(batch_request)
}
}

View File

@@ -1,88 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::os::unix::io::RawFd;
use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
use crate::raw_sync::RawFileSync;
pub struct FixedVhdSync {
raw_file_sync: RawFileSync,
size: u64,
}
impl FixedVhdSync {
pub fn new(fd: RawFd, size: u64) -> std::io::Result<Self> {
Ok(FixedVhdSync {
raw_file_sync: RawFileSync::new(fd),
size,
})
}
}
impl AsyncIo for FixedVhdSync {
fn notifier(&self) -> &EventFd {
self.raw_file_sync.notifier()
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
if offset as u64 >= self.size {
return Err(AsyncIoError::ReadVectored(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid offset {}, can't be larger than file size {}",
offset, self.size
),
)));
}
self.raw_file_sync.read_vectored(offset, iovecs, user_data)
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
if offset as u64 >= self.size {
return Err(AsyncIoError::WriteVectored(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid offset {}, can't be larger than file size {}",
offset, self.size
),
)));
}
self.raw_file_sync.write_vectored(offset, iovecs, user_data)
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.raw_file_sync.fsync(user_data)
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
self.raw_file_sync.next_completed_request()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::PunchHole(std::io::Error::other(
"punch_hole not supported for fixed VHD",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
"write_zeroes not supported for fixed VHD",
)))
}
}

13
block/src/formats/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Disk format implementations.
//!
//! Each format lives in its own submodule with a `DiskFile` wrapper,
//! format specific internals, and sync/async I/O workers.
pub mod qcow;
pub mod raw;
pub mod vhd;
pub mod vhdx;

View File

@@ -6,23 +6,25 @@
//! Thread safe backing file readers for QCOW2 images.
use std::fs::File;
use std::io;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::os::unix::fs::FileExt;
use std::sync::Arc;
use super::decoder::Decoder;
use super::metadata::{BackingRead, ClusterReadMapping, QcowMetadata};
use super::parser::{BackingFile, BackingKind, Error as QcowError};
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::qcow::decoder::Decoder;
use crate::qcow::metadata::{BackingRead, ClusterReadMapping, QcowMetadata};
use crate::qcow::{BackingFile, BackingKind, Error as QcowError};
use crate::qcow_common::{decompress_cluster, pread_alloc, pread_exact};
use crate::formats::qcow::common::decompress_cluster;
/// Raw backing file using pread64 on a duplicated fd.
/// Raw backing file using position-independent reads on a duplicated fd.
pub(crate) struct RawBacking {
pub(crate) fd: OwnedFd,
pub(crate) file: File,
pub(crate) virtual_size: u64,
}
// SAFETY: The only I/O operation is pread64 which is position independent
// SAFETY: The only I/O operation is read_at which is position independent
// and safe for concurrent use from multiple threads.
unsafe impl Sync for RawBacking {}
@@ -34,9 +36,9 @@ impl BackingRead for RawBacking {
}
let available = (self.virtual_size - address) as usize;
if available >= buf.len() {
pread_exact(self.fd.as_raw_fd(), buf, address)
self.file.read_exact_at(buf, address)
} else {
pread_exact(self.fd.as_raw_fd(), &mut buf[..available], address)?;
self.file.read_exact_at(&mut buf[..available], address)?;
buf[available..].fill(0);
Ok(())
}
@@ -51,14 +53,14 @@ impl BackingRead for RawBacking {
/// are handled recursively via the optional `backing_file` field.
pub(crate) struct Qcow2Backing {
pub(crate) metadata: Arc<QcowMetadata>,
pub(crate) data_fd: OwnedFd,
pub(crate) data_file: File,
pub(crate) backing_file: Option<Arc<dyn BackingRead>>,
pub(crate) cluster_size: u64,
pub(crate) decoder: Arc<dyn Decoder>,
}
// SAFETY: All reads go through QcowMetadata which uses RwLock
// and pread64 which is position independent and thread safe.
// and read_exact_at which is position independent and thread safe.
unsafe impl Sync for Qcow2Backing {}
impl BackingRead for Qcow2Backing {
@@ -79,8 +81,6 @@ impl BackingRead for Qcow2Backing {
}
impl Qcow2Backing {
/// Resolve cluster mappings via metadata then read allocated clusters
/// with pread64.
fn read_clusters(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
let total_len = buf.len();
let has_backing = self.backing_file.is_some();
@@ -100,8 +100,7 @@ impl Qcow2Backing {
offset: host_offset,
length,
} => {
pread_exact(
self.data_fd.as_raw_fd(),
self.data_file.read_exact_at(
&mut buf[buf_offset..buf_offset + length as usize],
host_offset,
)?;
@@ -113,8 +112,8 @@ impl Qcow2Backing {
cluster_offset,
length,
} => {
let compressed =
pread_alloc(self.data_fd.as_raw_fd(), host_offset, compressed_size)?;
let mut compressed = vec![0u8; compressed_size];
self.data_file.read_exact_at(&mut compressed, host_offset)?;
let decompressed = decompress_cluster(
&compressed,
self.cluster_size as usize,
@@ -147,7 +146,7 @@ impl Drop for Qcow2Backing {
}
/// Construct a thread safe backing file reader.
pub fn shared_backing_from(bf: BackingFile) -> BlockResult<Arc<dyn BackingRead>> {
pub(super) fn shared_backing_from(bf: BackingFile) -> BlockResult<Arc<dyn BackingRead>> {
let (kind, virtual_size) = bf.into_kind();
let dup_fd = |fd: BorrowedFd<'_>| -> BlockResult<OwnedFd> {
@@ -162,23 +161,19 @@ pub fn shared_backing_from(bf: BackingFile) -> BlockResult<Arc<dyn BackingRead>>
match kind {
BackingKind::Raw(raw_file) => {
let fd = dup_fd(raw_file.as_fd())?;
Ok(Arc::new(RawBacking { fd, virtual_size }))
let file = File::from(dup_fd(raw_file.as_fd())?);
Ok(Arc::new(RawBacking { file, virtual_size }))
}
BackingKind::Qcow { inner, backing } => {
let data_fd = dup_fd(inner.raw_file.as_fd())?;
let data_file = File::from(dup_fd(inner.raw_file.as_fd())?);
let metadata = Arc::new(QcowMetadata::new(*inner));
Ok(Arc::new(Qcow2Backing {
cluster_size: metadata.cluster_size(),
decoder: metadata.decoder(),
metadata,
data_fd,
data_file,
backing_file: backing.map(|bf| shared_backing_from(*bf)).transpose()?,
}))
}
#[cfg(test)]
BackingKind::QcowFile(_) => {
unreachable!("QcowFile variant is only used by set_backing_file() in tests")
}
}
}

View File

@@ -0,0 +1,161 @@
// Copyright © 2021 Intel Corporation
//
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! Shared helpers for QCOW2 sync and async backends.
use std::io;
use super::decoder::Decoder;
/// Decompress a full QCOW2 cluster from compressed data.
///
/// Returns a `cluster_size` byte buffer with the decompressed cluster
/// content. Fails if the decoder does not produce exactly `cluster_size`
/// bytes.
pub(super) fn decompress_cluster(
compressed: &[u8],
cluster_size: usize,
decoder: &dyn Decoder,
) -> io::Result<Vec<u8>> {
let mut decompressed = vec![0u8; cluster_size];
let n = decoder
.decode(compressed, &mut decompressed)
.map_err(|_| io::Error::from_raw_os_error(libc::EIO))?;
if n != cluster_size {
return Err(io::Error::from_raw_os_error(libc::EIO));
}
Ok(decompressed)
}
#[cfg(test)]
pub(crate) mod unit_tests {
use std::fs::File;
use std::io::Write;
use std::os::unix::fs::FileExt;
use flate2::Compression;
use flate2::write::DeflateEncoder;
use super::super::decoder::ZlibDecoder;
use super::decompress_cluster;
const COMPRESSED_FLAG: u64 = 1 << 62;
const CLUSTER_USED_FLAG: u64 = 1 << 63;
const COMPRESSED_SECTOR_SIZE: u64 = 512;
const HEADER_CLUSTER_BITS_OFFSET: u64 = 20;
const HEADER_L1_SIZE_OFFSET: u64 = 36;
const HEADER_L1_TABLE_OFFSET: u64 = 40;
const L1_L2_ADDR_MASK: u64 = 0x00ff_ffff_ffff_fe00;
fn make_compressed_l2_entry(host_offset: u64, compressed_len: usize, cluster_bits: u32) -> u64 {
let compressed_size_shift = 62 - (cluster_bits - 8);
let intra_sector_offset = host_offset & (COMPRESSED_SECTOR_SIZE - 1);
let total_bytes = compressed_len as u64 + intra_sector_offset;
let nsectors = total_bytes.div_ceil(COMPRESSED_SECTOR_SIZE);
let addr_part = host_offset & ((1 << compressed_size_shift) - 1);
let size_part = (nsectors - 1) << compressed_size_shift;
COMPRESSED_FLAG | size_part | addr_part
}
/// Compress every allocated cluster in a QCOW2 image file in place.
pub fn compress_allocated_clusters(file: &mut File) {
let mut buf4 = [0u8; 4];
file.read_exact_at(&mut buf4, HEADER_CLUSTER_BITS_OFFSET)
.unwrap();
let cluster_bits = u32::from_be_bytes(buf4);
let cluster_size = 1u64 << cluster_bits;
file.read_exact_at(&mut buf4, HEADER_L1_SIZE_OFFSET)
.unwrap();
let l1_size = u32::from_be_bytes(buf4);
let mut buf8 = [0u8; 8];
file.read_exact_at(&mut buf8, HEADER_L1_TABLE_OFFSET)
.unwrap();
let l1_table_offset = u64::from_be_bytes(buf8);
let entries_per_l2 = cluster_size / 8;
let mut append_offset = file.metadata().unwrap().len();
append_offset = (append_offset + 511) & !511;
for l1_idx in 0..l1_size as u64 {
let l1_entry_offset = l1_table_offset + l1_idx * 8;
file.read_exact_at(&mut buf8, l1_entry_offset).unwrap();
let l1_entry = u64::from_be_bytes(buf8);
let l2_table_addr = l1_entry & L1_L2_ADDR_MASK;
if l2_table_addr == 0 {
continue;
}
for l2_idx in 0..entries_per_l2 {
let l2_entry_offset = l2_table_addr + l2_idx * 8;
file.read_exact_at(&mut buf8, l2_entry_offset).unwrap();
let l2_entry = u64::from_be_bytes(buf8);
if l2_entry & CLUSTER_USED_FLAG == 0 || l2_entry & COMPRESSED_FLAG != 0 {
continue;
}
let host_cluster_addr = l2_entry & L1_L2_ADDR_MASK;
if host_cluster_addr == 0 {
continue;
}
let mut cluster_data = vec![0u8; cluster_size as usize];
file.read_exact_at(&mut cluster_data, host_cluster_addr)
.unwrap();
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&cluster_data).unwrap();
let compressed = encoder.finish().unwrap();
file.write_all_at(&compressed, append_offset).unwrap();
let padded_len = (compressed.len() + 511) & !511;
if padded_len > compressed.len() {
let padding = vec![0u8; padded_len - compressed.len()];
file.write_all_at(&padding, append_offset + compressed.len() as u64)
.unwrap();
}
let new_entry =
make_compressed_l2_entry(append_offset, compressed.len(), cluster_bits);
file.write_all_at(&new_entry.to_be_bytes(), l2_entry_offset)
.unwrap();
append_offset += padded_len as u64;
}
}
file.flush().unwrap();
}
#[test]
fn test_decompress_cluster() {
let cluster_size = 65536;
let original: Vec<u8> = (0..=255).cycle().take(cluster_size).collect();
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&original).unwrap();
let compressed = encoder.finish().unwrap();
let result = decompress_cluster(&compressed, cluster_size, &ZlibDecoder {}).unwrap();
assert_eq!(result, original);
}
#[test]
fn test_decompress_cluster_corrupt_input() {
let corrupt = vec![0xffu8; 64];
let err = decompress_cluster(&corrupt, 65536, &ZlibDecoder {}).unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::EIO));
}
}

View File

@@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::{io, result};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -11,12 +13,12 @@ pub enum Error {
#[error("Zlib unexpected status: {0:?}")]
ZlibUnexpectedStatus(flate2::Status),
#[error("Zstd decompress error")]
ZstdDecompress(#[source] std::io::Error),
ZstdDecompress(#[source] io::Error),
#[error("Zstd: failed to fill buffer")]
ZstdFillBuffer(#[source] std::io::Error),
ZstdFillBuffer(#[source] io::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub(super) type Result<T> = result::Result<T, Error>;
/// Generic trait for decoding zlib/zstd formats
pub trait Decoder: Send + Sync {
@@ -24,7 +26,7 @@ pub trait Decoder: Send + Sync {
}
#[derive(Default)]
pub struct ZlibDecoder {}
pub(super) struct ZlibDecoder {}
impl Decoder for ZlibDecoder {
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
@@ -43,7 +45,7 @@ impl Decoder for ZlibDecoder {
}
#[derive(Default)]
pub struct ZstdDecoder {}
pub(super) struct ZstdDecoder {}
impl Decoder for ZstdDecoder {
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {

View File

@@ -9,17 +9,18 @@
//! QCOW2 header parsing, validation, and creation.
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::io::{Read, Seek, SeekFrom, Write};
use std::mem::size_of;
use std::os::unix::fs::FileExt;
use std::str::FromStr;
use bitflags::bitflags;
use vmm_sys_util::file_traits::FileSync;
use zerocopy::big_endian::{U32 as BeU32, U64 as BeU64};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
use super::decoder::{Decoder, ZlibDecoder, ZstdDecoder};
use super::qcow_raw_file::BeUint;
use super::raw_file::RawFile;
use super::{Error, Result, div_round_up_u32, div_round_up_u64};
use super::parser::{Error, Result};
use super::util::{div_round_up_u32, div_round_up_u64};
use crate::aligned_file::AlignedFile;
use crate::error::{BlockError, BlockErrorKind, BlockResult};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -207,30 +208,115 @@ pub struct QcowHeader {
pub backing_file: Option<BackingFileConfig>,
}
/// On-disk layout of the bare qcow2 header shared by v2 and v3 (72 bytes).
#[repr(C)]
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
struct RawHeaderV2 {
magic: BeU32,
version: BeU32,
backing_file_offset: BeU64,
backing_file_size: BeU32,
cluster_bits: BeU32,
size: BeU64,
crypt_method: BeU32,
l1_size: BeU32,
l1_table_offset: BeU64,
refcount_table_offset: BeU64,
refcount_table_clusters: BeU32,
nb_snapshots: BeU32,
snapshots_offset: BeU64,
}
impl RawHeaderV2 {
fn from_header(header: &QcowHeader) -> Self {
Self {
magic: BeU32::new(header.magic),
version: BeU32::new(header.version),
backing_file_offset: BeU64::new(header.backing_file_offset),
backing_file_size: BeU32::new(header.backing_file_size),
cluster_bits: BeU32::new(header.cluster_bits),
size: BeU64::new(header.size),
crypt_method: BeU32::new(header.crypt_method),
l1_size: BeU32::new(header.l1_size),
l1_table_offset: BeU64::new(header.l1_table_offset),
refcount_table_offset: BeU64::new(header.refcount_table_offset),
refcount_table_clusters: BeU32::new(header.refcount_table_clusters),
nb_snapshots: BeU32::new(header.nb_snapshots),
snapshots_offset: BeU64::new(header.snapshots_offset),
}
}
}
/// On-disk layout of the fields v3 adds after the bare header (32 bytes).
#[repr(C)]
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
struct RawHeaderV3Tail {
incompatible_features: BeU64,
compatible_features: BeU64,
autoclear_features: BeU64,
refcount_order: BeU32,
header_size: BeU32,
}
impl RawHeaderV3Tail {
fn from_header(header: &QcowHeader) -> Self {
Self {
incompatible_features: BeU64::new(header.incompatible_features),
compatible_features: BeU64::new(header.compatible_features),
autoclear_features: BeU64::new(header.autoclear_features),
refcount_order: BeU32::new(header.refcount_order),
header_size: BeU32::new(header.header_size),
}
}
}
#[repr(C)]
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
struct ExtensionHeader {
extension_type: BeU32,
length: BeU32,
}
impl ExtensionHeader {
fn end() -> Self {
Self {
extension_type: BeU32::new(HEADER_EXT_END),
length: BeU32::ZERO,
}
}
}
impl QcowHeader {
/// Read header extensions, optionally collecting feature names for error reporting.
pub(super) fn read_header_extensions(
f: &mut RawFile,
f: &AlignedFile,
header: &mut QcowHeader,
mut feature_table: Option<&mut Vec<(u8, String)>>,
) -> Result<()> {
// Extensions start directly after the header
f.seek(SeekFrom::Start(header.header_size as u64))
.map_err(Error::ReadingHeader)?;
// Extensions start directly after the header.
let mut offset = header.header_size as u64;
loop {
let ext_type = u32::read_be(f).map_err(Error::ReadingHeader)?;
let mut field = [0u8; size_of::<ExtensionHeader>()];
f.read_exact_at(&mut field, offset)
.map_err(Error::ReadingHeader)?;
offset += field.len() as u64;
let extension =
ExtensionHeader::read_from_bytes(&field).expect("buffer covers extension header");
let ext_type = extension.extension_type.get();
if ext_type == HEADER_EXT_END {
break;
}
let ext_length = u32::read_be(f).map_err(Error::ReadingHeader)?;
let ext_length = extension.length.get();
match ext_type {
HEADER_EXT_BACKING_FORMAT => {
let mut format_bytes = vec![0u8; ext_length as usize];
f.read_exact(&mut format_bytes)
f.read_exact_at(&mut format_bytes, offset)
.map_err(Error::ReadingHeader)?;
offset += format_bytes.len() as u64;
let format_str = String::from_utf8(format_bytes)
.map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?;
if let Some(backing_file) = &mut header.backing_file {
@@ -240,9 +326,11 @@ impl QcowHeader {
HEADER_EXT_FEATURE_NAME_TABLE if feature_table.is_some() => {
const FEATURE_NAME_ENTRY_SIZE: usize = 1 + 1 + 46; // type + bit + name
let mut data = vec![0u8; ext_length as usize];
f.read_exact(&mut data).map_err(Error::ReadingHeader)?;
f.read_exact_at(&mut data, offset)
.map_err(Error::ReadingHeader)?;
offset += data.len() as u64;
let table = feature_table.as_mut().unwrap();
for entry in data.chunks_exact(FEATURE_NAME_ENTRY_SIZE) {
for entry in data.as_chunks::<FEATURE_NAME_ENTRY_SIZE>().0 {
if entry[0] == FEAT_TYPE_INCOMPATIBLE {
let bit_number = entry[1];
let name_bytes = &entry[2..];
@@ -254,88 +342,116 @@ impl QcowHeader {
}
_ => {
// Skip unknown extension
f.seek(SeekFrom::Current(ext_length as i64))
.map_err(Error::ReadingHeader)?;
offset += ext_length as u64;
}
}
// Skip to the next 8 byte boundary
let padding = (8 - (ext_length % 8)) % 8;
f.seek(SeekFrom::Current(padding as i64))
.map_err(Error::ReadingHeader)?;
offset += padding as u64;
}
Ok(())
}
/// Creates a QcowHeader from a reference to a file.
pub fn new(f: &mut RawFile) -> Result<QcowHeader> {
f.rewind().map_err(Error::ReadingHeader)?;
let magic = u32::read_be(f).map_err(Error::ReadingHeader)?;
pub fn new(f: &AlignedFile) -> Result<QcowHeader> {
// The bare header fits in V3_BARE_HEADER_SIZE plus the optional
// compression field. Read it once, then decode each region as a typed
// view whose layout matches the on-disk header.
let mut buf = [0u8; V3_BARE_HEADER_SIZE as usize + size_of::<u64>()];
f.read_exact_at(&mut buf, 0).map_err(Error::ReadingHeader)?;
// `buf` is always larger than the views, and the views are unaligned,
// so the casts cannot fail.
let (v2, tail) = RawHeaderV2::ref_from_prefix(&buf).expect("buffer covers the v2 header");
let magic = v2.magic.get();
if magic != QCOW_MAGIC {
return Err(Error::InvalidMagic);
}
// Reads the next u32 from the file.
fn read_u32_be(f: &mut RawFile) -> Result<u32> {
u32::read_be(f).map_err(Error::ReadingHeader)
}
// Reads the next u64 from the file.
fn read_u64_be(f: &mut RawFile) -> Result<u64> {
u64::read_be(f).map_err(Error::ReadingHeader)
}
let version = read_u32_be(f)?;
let version = v2.version.get();
let mut header = QcowHeader {
magic,
version,
backing_file_offset: read_u64_be(f)?,
backing_file_size: read_u32_be(f)?,
cluster_bits: read_u32_be(f)?,
size: read_u64_be(f)?,
crypt_method: read_u32_be(f)?,
l1_size: read_u32_be(f)?,
l1_table_offset: read_u64_be(f)?,
refcount_table_offset: read_u64_be(f)?,
refcount_table_clusters: read_u32_be(f)?,
nb_snapshots: read_u32_be(f)?,
snapshots_offset: read_u64_be(f)?,
incompatible_features: if version == 2 { 0 } else { read_u64_be(f)? },
compatible_features: if version == 2 { 0 } else { read_u64_be(f)? },
autoclear_features: if version == 2 { 0 } else { read_u64_be(f)? },
refcount_order: if version == 2 {
DEFAULT_REFCOUNT_ORDER
} else {
read_u32_be(f)?
},
header_size: if version == 2 {
V2_BARE_HEADER_SIZE
} else {
read_u32_be(f)?
},
backing_file_offset: v2.backing_file_offset.get(),
backing_file_size: v2.backing_file_size.get(),
cluster_bits: v2.cluster_bits.get(),
size: v2.size.get(),
crypt_method: v2.crypt_method.get(),
l1_size: v2.l1_size.get(),
l1_table_offset: v2.l1_table_offset.get(),
refcount_table_offset: v2.refcount_table_offset.get(),
refcount_table_clusters: v2.refcount_table_clusters.get(),
nb_snapshots: v2.nb_snapshots.get(),
snapshots_offset: v2.snapshots_offset.get(),
incompatible_features: 0,
compatible_features: 0,
autoclear_features: 0,
refcount_order: DEFAULT_REFCOUNT_ORDER,
header_size: V2_BARE_HEADER_SIZE,
compression_type: CompressionType::Zlib,
backing_file: None,
};
if version == 3 && header.header_size > V3_BARE_HEADER_SIZE {
let raw_compression_type = read_u64_be(f)? >> (64 - 8);
header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB {
Ok(CompressionType::Zlib)
} else if raw_compression_type == COMPRESSION_TYPE_ZSTD {
Ok(CompressionType::Zstd)
} else {
Err(Error::UnsupportedCompressionType)
}?;
if version != 2 {
let (v3, rest) =
RawHeaderV3Tail::ref_from_prefix(tail).expect("buffer covers the v3 header");
header.incompatible_features = v3.incompatible_features.get();
header.compatible_features = v3.compatible_features.get();
header.autoclear_features = v3.autoclear_features.get();
header.refcount_order = v3.refcount_order.get();
header.header_size = v3.header_size.get();
if version == 3 && header.header_size > V3_BARE_HEADER_SIZE {
let (compression, _) =
BeU64::ref_from_prefix(rest).expect("buffer covers the compression field");
let raw_compression_type = compression.get() >> (64 - 8);
header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB {
Ok(CompressionType::Zlib)
} else if raw_compression_type == COMPRESSION_TYPE_ZSTD {
Ok(CompressionType::Zstd)
} else {
Err(Error::UnsupportedCompressionType)
}?;
}
}
if header.backing_file_size > MAX_BACKING_FILE_SIZE {
return Err(Error::BackingFileTooLong(header.backing_file_size as usize));
}
if header.backing_file_offset == 0 && header.backing_file_size != 0 {
return Err(Error::BackingFileSizeWithoutOffset(
header.backing_file_size,
));
}
if header.backing_file_offset != 0 && header.backing_file_size == 0 {
return Err(Error::BackingFileOffsetWithoutSize(
header.backing_file_offset,
));
}
if header.backing_file_offset != 0 {
f.seek(SeekFrom::Start(header.backing_file_offset))
.map_err(Error::ReadingHeader)?;
let cluster_size = 1u64
.checked_shl(header.cluster_bits)
.ok_or(Error::InvalidClusterSize)?;
if header.backing_file_offset < u64::from(header.header_size) {
return Err(Error::BackingFileOverlapsHeader(
header.backing_file_offset,
header.backing_file_size,
header.header_size,
));
}
if header.backing_file_offset >= cluster_size
|| header.backing_file_offset + u64::from(header.backing_file_size) > cluster_size
{
return Err(Error::BackingFileOutsideFirstCluster(
header.backing_file_offset,
header.backing_file_size,
cluster_size,
));
}
let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize];
f.read_exact(&mut backing_file_name_bytes)
f.read_exact_at(&mut backing_file_name_bytes, header.backing_file_offset)
.map_err(Error::ReadingHeader)?;
let path = String::from_utf8(backing_file_name_bytes)
.map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?;
@@ -448,78 +564,65 @@ impl QcowHeader {
})
}
/// Write the header to `file`.
pub fn write_to<F: Write + Seek>(&self, file: &mut F) -> Result<()> {
// Writes the next u32 to the file.
fn write_u32_be<F: Write>(f: &mut F, value: u32) -> Result<()> {
u32::write_be(f, value).map_err(Error::WritingHeader)
}
// Writes the next u64 to the file.
fn write_u64_be<F: Write>(f: &mut F, value: u64) -> Result<()> {
u64::write_be(f, value).map_err(Error::WritingHeader)
}
write_u32_be(file, self.magic)?;
write_u32_be(file, self.version)?;
write_u64_be(file, self.backing_file_offset)?;
write_u32_be(file, self.backing_file_size)?;
write_u32_be(file, self.cluster_bits)?;
write_u64_be(file, self.size)?;
write_u32_be(file, self.crypt_method)?;
write_u32_be(file, self.l1_size)?;
write_u64_be(file, self.l1_table_offset)?;
write_u64_be(file, self.refcount_table_offset)?;
write_u32_be(file, self.refcount_table_clusters)?;
write_u32_be(file, self.nb_snapshots)?;
write_u64_be(file, self.snapshots_offset)?;
/// Write the header to `f`.
pub fn write_to(&self, f: &AlignedFile) -> Result<()> {
// Build the header in memory, then write it in one positional write.
let mut buf = Vec::new();
let v2 = RawHeaderV2::from_header(self);
buf.extend_from_slice(v2.as_bytes());
if self.version == 3 {
write_u64_be(file, self.incompatible_features)?;
write_u64_be(file, self.compatible_features)?;
write_u64_be(file, self.autoclear_features)?;
write_u32_be(file, self.refcount_order)?;
write_u32_be(file, self.header_size)?;
let v3 = RawHeaderV3Tail::from_header(self);
buf.extend_from_slice(v3.as_bytes());
if self.header_size > V3_BARE_HEADER_SIZE {
write_u64_be(file, 0)?; // no compression
let compression_type = match &self.compression_type {
CompressionType::Zlib => COMPRESSION_TYPE_ZLIB,
CompressionType::Zstd => COMPRESSION_TYPE_ZSTD,
};
let compression_type = BeU64::new(compression_type << (64 - 8));
buf.extend_from_slice(compression_type.as_bytes());
}
write_u32_be(file, 0)?; // header extension type: end of header extension area
write_u32_be(file, 0)?; // length of header extension data: 0
let end_extension = ExtensionHeader::end();
buf.extend_from_slice(end_extension.as_bytes());
}
f.write_all_at(&buf, 0).map_err(Error::WritingHeader)?;
if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) {
if self.backing_file_offset > 0 {
file.seek(SeekFrom::Start(self.backing_file_offset))
.map_err(Error::WritingHeader)?;
}
write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?;
let offset = if self.backing_file_offset > 0 {
self.backing_file_offset
} else {
buf.len() as u64
};
f.write_all_at(backing_file_path.as_bytes(), offset)
.map_err(Error::WritingHeader)?;
}
// Set the file length by seeking and writing a zero to the last byte. This avoids needing
// a `File` instead of anything that implements seek as the `file` argument.
// Zeros out the l1 and refcount table clusters.
// Set the file length by writing a zero to the last byte. This also
// zeros the l1 and refcount table clusters.
let cluster_size = 0x01u64 << self.cluster_bits;
let refcount_blocks_size = u64::from(self.refcount_table_clusters) * cluster_size;
file.seek(SeekFrom::Start(
f.write_all_at(
&[0u8],
self.refcount_table_offset + refcount_blocks_size - 2,
))
)
.map_err(Error::WritingHeader)?;
file.write(&[0u8]).map_err(Error::WritingHeader)?;
Ok(())
}
/// Write only the incompatible_features field to the file at its fixed offset.
fn write_incompatible_features<F: Seek + Write>(&self, file: &mut F) -> BlockResult<()> {
fn write_incompatible_features(&self, file: &AlignedFile) -> BlockResult<()> {
if self.version != 3 {
return Ok(());
}
file.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64))
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?;
u64::write_be(file, self.incompatible_features)
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?;
file.write_all_at(
&self.incompatible_features.to_be_bytes(),
V2_BARE_HEADER_SIZE as u64,
)
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?;
Ok(())
}
@@ -527,11 +630,7 @@ impl QcowHeader {
///
/// When `dirty` is true, sets the bit to indicate the image is in use.
/// When `dirty` is false, clears the bit to indicate a clean shutdown.
pub fn set_dirty_bit<F: Seek + Write + FileSync>(
&mut self,
file: &mut F,
dirty: bool,
) -> BlockResult<()> {
pub fn set_dirty_bit(&mut self, file: &mut AlignedFile, dirty: bool) -> BlockResult<()> {
if self.version == 3 {
if dirty {
self.incompatible_features |= IncompatFeatures::DIRTY.bits();
@@ -549,7 +648,7 @@ impl QcowHeader {
///
/// This marks the image as corrupted. Once set, the image can only be
/// opened read-only until repaired.
pub fn set_corrupt_bit<F: Seek + Write + FileSync>(&mut self, file: &mut F) -> BlockResult<()> {
pub fn set_corrupt_bit(&mut self, file: &mut AlignedFile) -> BlockResult<()> {
if self.version == 3 {
self.incompatible_features |= IncompatFeatures::CORRUPT.bits();
self.write_incompatible_features(file)?;
@@ -568,15 +667,11 @@ impl QcowHeader {
///
/// These bits indicate features that can be safely disabled when modified
/// by software that doesn't understand them.
pub fn clear_autoclear_features<F: Seek + Write + FileSync>(
&mut self,
file: &mut F,
) -> Result<()> {
pub fn clear_autoclear_features(&mut self, file: &mut AlignedFile) -> Result<()> {
if self.version == 3 && self.autoclear_features != 0 {
self.autoclear_features = 0;
file.seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET))
file.write_all_at(&0u64.to_be_bytes(), AUTOCLEAR_FEATURES_OFFSET)
.map_err(Error::WritingHeader)?;
u64::write_be(file, 0).map_err(Error::WritingHeader)?;
file.fsync().map_err(Error::SyncingHeader)?;
}
Ok(())

View File

@@ -17,11 +17,12 @@
//! operations upgrade to a write lock.
use std::cmp::min;
use std::io::{self, Seek};
use std::mem;
use std::os::unix::fs::FileExt;
use std::sync::{Arc, RwLock};
use std::{io, mem};
use libc::{EINVAL, EIO};
use vmm_sys_util::write_zeroes::WriteZeroesAt;
use super::decoder::Decoder;
use super::qcow_raw_file::QcowRawFile;
@@ -29,7 +30,7 @@ use super::refcount::RefCount;
use super::util::{
div_round_up_u64, l1_entry_make, l2_entry_compressed_cluster_layout, l2_entry_is_compressed,
l2_entry_is_empty, l2_entry_is_zero, l2_entry_make_std, l2_entry_make_zero,
l2_entry_std_cluster_addr,
l2_entry_make_zero_plain, l2_entry_std_cluster_addr,
};
use super::vec_cache::{CacheMap, Cacheable, VecCache};
use super::{QcowHeader, refcount};
@@ -40,7 +41,7 @@ use super::{QcowHeader, refcount};
/// the actual data I/O using its own per queue file descriptor without
/// holding the metadata lock.
#[derive(Debug)]
pub enum ClusterReadMapping {
pub(super) enum ClusterReadMapping {
/// The cluster is not allocated and the guest should see zeros.
/// This covers both truly unallocated clusters where the L1 or L2
/// entry is zero and clusters with the ZERO flag set.
@@ -75,7 +76,7 @@ pub enum ClusterReadMapping {
/// the actual data I/O using its own per queue file descriptor without
/// holding the metadata lock.
#[derive(Debug)]
pub enum ClusterWriteMapping {
pub(super) enum ClusterWriteMapping {
/// The write target is at the given host file offset.
/// This covers both already allocated clusters and freshly allocated ones.
/// The offset is the exact byte position combining cluster base and
@@ -93,7 +94,7 @@ pub(crate) trait BackingRead: Send + Sync {
/// Action that the caller must perform after deallocate_bytes.
#[derive(Debug)]
pub enum DeallocAction {
pub(super) enum DeallocAction {
/// Punch a hole at the given host file offset for a full cluster.
PunchHole { host_offset: u64, length: u64 },
/// Write zeros at the given host file offset for a partial cluster.
@@ -114,7 +115,7 @@ pub enum DeallocAction {
/// L1 to L2 lookup, which completes under a shared read lock. Only
/// cluster allocation, L2 cache eviction and resize take the exclusive
/// write lock, so contention stays low and queues scale.
pub struct QcowMetadata {
pub(super) struct QcowMetadata {
inner: RwLock<QcowState>,
decoder: Arc<dyn Decoder>,
}
@@ -159,7 +160,7 @@ impl QcowMetadata {
///
/// The has_backing_file flag indicates whether a backing file exists,
/// needed to distinguish zero versus backing for unallocated clusters.
pub fn map_clusters_for_read(
pub(super) fn map_clusters_for_read(
&self,
address: u64,
total_length: usize,
@@ -227,7 +228,7 @@ impl QcowMetadata {
/// unallocated and a backing file exists, the caller should have already
/// read the backing cluster data and pass it here. If None, the new
/// cluster is zeroed.
pub fn map_cluster_for_write(
pub(super) fn map_cluster_for_write(
&self,
address: u64,
backing_data: Option<Vec<u8>>,
@@ -236,7 +237,7 @@ impl QcowMetadata {
inner.map_write(address, backing_data)
}
pub fn flush(&self) -> io::Result<()> {
pub(super) fn flush(&self) -> io::Result<()> {
let mut inner = self.inner.write().unwrap();
inner.sync_caches()?;
let mut unref = mem::take(&mut inner.unref_clusters);
@@ -246,7 +247,7 @@ impl QcowMetadata {
/// Flushes dirty metadata caches and clears the dirty bit for
/// clean shutdown.
pub fn shutdown(&self) {
pub(super) fn shutdown(&self) {
let mut inner = self.inner.write().unwrap();
let _ = inner.sync_caches();
let QcowState {
@@ -264,21 +265,22 @@ impl QcowMetadata {
/// clusters beyond the new size and risks data loss.
///
/// Returns an error if the new size is smaller than the current size.
pub fn resize(&self, new_size: u64) -> io::Result<()> {
pub(super) fn resize(&self, new_size: u64) -> io::Result<()> {
let mut inner = self.inner.write().unwrap();
inner.resize(new_size)
}
/// Deallocates a range of bytes. Full clusters are deallocated via metadata.
/// Partial clusters need the caller to write zeros. This method returns a
/// list of actions the caller should take.
/// If `zero_marker` is true, full-cluster deallocation records a logical
/// zero instead of an empty entry where backing data could otherwise be
/// exposed. Partial clusters need the caller to write zeros. This method
/// returns a list of actions the caller should take.
pub(crate) fn deallocate_bytes(
&self,
address: u64,
length: usize,
sparse: bool,
virtual_size: u64,
cluster_size: u64,
zero_marker: bool,
backing_file: Option<&dyn BackingRead>,
) -> io::Result<Vec<DeallocAction>> {
if address.checked_add(length as u64).is_none() {
@@ -287,7 +289,8 @@ impl QcowMetadata {
let mut inner = self.inner.write().unwrap();
let mut actions = Vec::new();
let file_end = virtual_size;
let file_end = inner.header.size;
let cluster_size = inner.raw_file.cluster_size();
let remaining_in_file = file_end.saturating_sub(address);
let write_count = min(length as u64, remaining_in_file) as usize;
@@ -301,7 +304,11 @@ impl QcowMetadata {
);
if count == cluster_size as usize {
let punch_offset = inner.deallocate_cluster(curr_addr, sparse)?;
let punch_offset = inner.deallocate_cluster(
curr_addr,
sparse,
zero_marker && backing_file.is_some(),
)?;
if let Some(host_offset) = punch_offset {
actions.push(DeallocAction::PunchHole {
host_offset,
@@ -332,18 +339,36 @@ impl QcowMetadata {
Ok(actions)
}
pub fn virtual_size(&self) -> u64 {
pub(super) fn virtual_size(&self) -> u64 {
self.inner.read().unwrap().header.size
}
pub fn cluster_size(&self) -> u64 {
pub(super) fn cluster_size(&self) -> u64 {
self.inner.read().unwrap().raw_file.cluster_size()
}
/// Returns the shared decoder matching the image compression type.
pub fn decoder(&self) -> Arc<dyn Decoder> {
pub(super) fn decoder(&self) -> Arc<dyn Decoder> {
Arc::clone(&self.decoder)
}
#[cfg(test)]
pub fn header(&self) -> QcowHeader {
self.inner.read().unwrap().header.clone()
}
#[cfg(test)]
pub fn cluster_refcount(&self, address: u64) -> io::Result<u64> {
let mut inner = self.inner.write().unwrap();
let QcowState {
refcounts,
raw_file,
..
} = &mut *inner;
refcounts
.get_cluster_refcount(raw_file, address)
.map_err(|e| io::Error::other(format!("get_cluster_refcount: {e}")))
}
}
impl QcowState {
@@ -406,18 +431,12 @@ impl QcowState {
has_backing_file,
)))
} else if l2_entry_is_zero(l2_entry) {
// Match original QcowFile::file_read semantics where zero flagged
// entries fall through to backing file when one exists or return
// zeros otherwise.
Ok(Some(self.unallocated_read_mapping(
address,
count,
has_backing_file,
)))
Ok(Some(ClusterReadMapping::Zero {
length: count as u64,
}))
} else {
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
let cluster_size = self.raw_file.cluster_size();
if cluster_addr & (cluster_size - 1) != 0 {
if !self.is_refcount_addressable_cluster_offset(cluster_addr) {
// Fall through to write lock path which sets the corrupt bit
return Ok(None);
}
@@ -469,17 +488,12 @@ impl QcowState {
length: count,
})
} else if l2_entry_is_zero(l2_entry) {
// Match original QcowFile::file_read semantics where zero flagged
// entries fall through to backing file when one exists or return
// zeros otherwise.
Ok(self.unallocated_read_mapping(address, count, has_backing_file))
Ok(ClusterReadMapping::Zero {
length: count as u64,
})
} else {
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
let cluster_size = self.raw_file.cluster_size();
if cluster_addr & (cluster_size - 1) != 0 {
self.set_corrupt_bit_best_effort();
return Err(io::Error::from_raw_os_error(EIO));
}
self.reject_invalid_cluster_offset(cluster_addr)?;
let intra_offset = self.raw_file.cluster_offset(address);
Ok(ClusterReadMapping::Allocated {
offset: cluster_addr + intra_offset,
@@ -506,19 +520,6 @@ impl QcowState {
}
}
/// Maps a single cluster region for a sequential read.
pub(crate) fn map_cluster_read(
&mut self,
address: u64,
count: usize,
has_backing_file: bool,
) -> io::Result<ClusterReadMapping> {
match self.try_map_read(address, count, has_backing_file)? {
Some(mapping) => Ok(mapping),
None => self.map_read_with_populate(address, count, has_backing_file),
}
}
/// Write path mapping. Always called under write lock.
fn map_write(
&mut self,
@@ -547,10 +548,10 @@ impl QcowState {
let decompressed_cluster = self.decompress_l2_cluster(l2_entry)?;
let cluster_addr = self.append_data_cluster(None)?;
self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?;
self.raw_file
let nwritten = self
.raw_file
.file_mut()
.seek(io::SeekFrom::Start(cluster_addr))?;
let nwritten = io::Write::write(self.raw_file.file_mut(), &decompressed_cluster)?;
.write_at(&decompressed_cluster, cluster_addr)?;
if nwritten != decompressed_cluster.len() {
self.set_corrupt_bit_best_effort();
return Err(io::Error::from_raw_os_error(EIO));
@@ -558,16 +559,16 @@ impl QcowState {
self.deallocate_compressed_cluster(l2_entry)?;
cluster_addr
} else if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) {
let cluster_addr = self.append_data_cluster(backing_data)?;
let cluster_addr = if l2_entry_is_zero(l2_entry) {
self.append_zeroed_data_cluster()?
} else {
self.append_data_cluster(backing_data)?
};
self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?;
cluster_addr
} else {
// Already allocated - validate alignment
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
if cluster_addr & (self.raw_file.cluster_size() - 1) != 0 {
self.set_corrupt_bit_best_effort();
return Err(io::Error::from_raw_os_error(EIO));
}
self.reject_invalid_cluster_offset(cluster_addr)?;
cluster_addr
};
@@ -592,16 +593,26 @@ impl QcowState {
(address / self.raw_file.cluster_size()) % self.l2_entries
}
fn is_refcount_addressable_cluster_offset(&self, cluster_addr: u64) -> bool {
cluster_addr & (self.raw_file.cluster_size() - 1) == 0
&& cluster_addr <= self.refcounts.max_valid_cluster_offset()
}
fn reject_invalid_cluster_offset(&mut self, cluster_addr: u64) -> io::Result<()> {
if self.is_refcount_addressable_cluster_offset(cluster_addr) {
Ok(())
} else {
self.set_corrupt_bit_best_effort();
Err(io::Error::from_raw_os_error(EIO))
}
}
// -- Cache and allocation operations requiring exclusive access --
/// Populates the L2 cache for read operations without allocation.
fn cache_l2_cluster(&mut self, l1_index: usize, l2_addr_disk: u64) -> io::Result<()> {
if !self.l2_cache.contains_key(l1_index) {
let cluster_size = self.raw_file.cluster_size();
if l2_addr_disk & (cluster_size - 1) != 0 {
self.set_corrupt_bit_best_effort();
return Err(io::Error::from_raw_os_error(EIO));
}
self.reject_invalid_cluster_offset(l2_addr_disk)?;
let l2_table =
VecCache::from_vec(self.raw_file.read_pointer_cluster(l2_addr_disk, None)?);
let l1_table = &self.l1_table;
@@ -629,11 +640,7 @@ impl QcowState {
self.l1_table[l1_index] = new_addr;
VecCache::new(self.l2_entries as usize)
} else {
let cluster_size = self.raw_file.cluster_size();
if l2_addr_disk & (cluster_size - 1) != 0 {
self.set_corrupt_bit_best_effort();
return Err(io::Error::from_raw_os_error(EIO));
}
self.reject_invalid_cluster_offset(l2_addr_disk)?;
VecCache::from_vec(self.raw_file.read_pointer_cluster(l2_addr_disk, None)?)
};
let l1_table = &self.l1_table;
@@ -683,6 +690,17 @@ impl QcowState {
Ok(new_addr)
}
/// Allocates a data cluster and zeroes it without building a cluster-sized buffer.
fn append_zeroed_data_cluster(&mut self) -> io::Result<u64> {
let new_addr = self.get_new_cluster(None)?;
let cluster_size = self.raw_file.cluster_size() as usize;
self.raw_file
.file_mut()
.write_zeroes_at(new_addr, cluster_size)?;
self.set_cluster_refcount_track_freed(new_addr, 1)?;
Ok(new_addr)
}
/// Updates the L1 and L2 tables to point to a new cluster address.
fn update_cluster_addr(
&mut self,
@@ -737,7 +755,6 @@ impl QcowState {
self.header.size = new_size;
self.raw_file.file_mut().rewind()?;
self.header
.write_to(self.raw_file.file_mut())
.map_err(|e| io::Error::other(format!("failed to write header during resize: {e}")))?;
@@ -757,7 +774,7 @@ impl QcowState {
let new_l1_clusters = div_round_up_u64(new_l1_bytes, cluster_size);
// Allocate contiguous clusters at file end for new L1 table
let file_size = self.raw_file.file_mut().seek(io::SeekFrom::End(0))?;
let file_size = self.raw_file.physical_size()?;
let new_l1_offset = self.raw_file.cluster_address(file_size + cluster_size - 1);
let new_file_end = new_l1_offset + new_l1_clusters * cluster_size;
@@ -793,7 +810,6 @@ impl QcowState {
self.header.l1_size = new_l1_size;
self.header.l1_table_offset = new_l1_offset;
self.raw_file.file_mut().rewind()?;
self.header
.write_to(self.raw_file.file_mut())
.map_err(|e| io::Error::other(format!("failed to write header during resize: {e}")))?;
@@ -821,13 +837,15 @@ impl QcowState {
/// If sparse is true, fully deallocates and returns the host offset if
/// the underlying storage should be punched after the refcount dropped
/// to zero. If sparse is false, uses the zero flag optimization when
/// possible.
/// possible. If `zero_marker` is true, empty entries are replaced with
/// logical-zero entries so reads do not fall through to backing data.
///
/// Returns None if no host punch_hole is needed.
pub(super) fn deallocate_cluster(
&mut self,
address: u64,
sparse: bool,
zero_marker: bool,
) -> io::Result<Option<u64>> {
if address >= self.header.size {
return Err(io::Error::from_raw_os_error(EINVAL));
@@ -839,25 +857,44 @@ impl QcowState {
None => return Err(io::Error::from_raw_os_error(EINVAL)),
};
let l2_index = self.l2_table_index(address) as usize;
let dealloc_entry = if zero_marker {
l2_entry_make_zero_plain()
} else {
0
};
if l2_addr_disk == 0 {
if zero_marker {
if let Some(new_addr) = self.cache_l2_cluster_alloc(l1_index, l2_addr_disk)? {
self.set_cluster_refcount_track_freed(new_addr, 1)?;
}
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
}
return Ok(None);
}
self.cache_l2_cluster(l1_index, l2_addr_disk)?;
let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index];
if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) {
if l2_entry_is_empty(l2_entry) {
if zero_marker {
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
}
return Ok(None);
}
// Compressed entries may use bit 0 as part of their layout, so they
// must be classified before zero-flagged standard entries.
if l2_entry_is_compressed(l2_entry) {
self.deallocate_compressed_cluster(l2_entry)?;
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0;
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
return Ok(None);
}
if l2_entry_is_zero(l2_entry) {
return Ok(None);
}
let cluster_addr = l2_entry_std_cluster_addr(l2_entry);
self.reject_invalid_cluster_offset(cluster_addr)?;
let refcount = self
.refcounts
.get_cluster_refcount(&mut self.raw_file, cluster_addr)
@@ -877,7 +914,7 @@ impl QcowState {
if sparse {
let new_refcount = refcount - 1;
self.set_cluster_refcount_track_freed(cluster_addr, new_refcount)?;
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0;
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
if new_refcount == 0 {
self.unref_clusters.push(cluster_addr);
return Ok(Some(cluster_addr));
@@ -886,7 +923,7 @@ impl QcowState {
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = l2_entry_make_zero(cluster_addr);
} else {
self.set_cluster_refcount_track_freed(cluster_addr, refcount - 1)?;
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0;
self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry;
}
Ok(None)
}
@@ -1013,11 +1050,10 @@ impl QcowState {
fn decompress_l2_cluster(&mut self, l2_entry: u64) -> io::Result<Vec<u8>> {
let (compressed_addr, compressed_size) =
l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits);
let mut compressed = vec![0u8; compressed_size];
self.raw_file
.file_mut()
.seek(io::SeekFrom::Start(compressed_addr))?;
let mut compressed = vec![0u8; compressed_size];
io::Read::read_exact(self.raw_file.file_mut(), &mut compressed)?;
.read_exact_at(&mut compressed, compressed_addr)?;
let decoder = self.header.get_decoder();
let cluster_size = self.raw_file.cluster_size() as usize;
let mut decompressed = vec![0u8; cluster_size];

View File

@@ -2,21 +2,56 @@
//
// SPDX-License-Identifier: Apache-2.0
//! QCOW2 disk image format.
//!
//! Provides [`QcowDisk`], the `DiskFile` wrapper for QCOW2 images
//! with backing file and compression support.
mod backing;
mod common;
mod decoder;
mod engine_sync;
#[cfg(feature = "io_uring")]
mod engine_uring;
mod header;
mod metadata;
mod parser;
mod qcow_raw_file;
mod refcount;
mod util;
mod vec_cache;
use std::fs::File;
use std::os::unix::io::AsRawFd;
#[cfg(any(test, feature = "test-utils"))]
use std::path::Path;
use std::sync::Arc;
use std::{fmt, io};
pub use parser::{
BackingFileConfig, CompressionType, Error, ImageType, IncompatFeatures, MissingFeatureError,
QcowHeader,
};
#[cfg(any(test, feature = "test-utils"))]
use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap};
#[cfg(any(test, feature = "test-utils"))]
use vmm_sys_util::tempfile::TempFile;
use self::backing::shared_backing_from;
use self::engine_sync::QcowSync;
#[cfg(feature = "io_uring")]
use self::engine_uring::QcowAsync;
use self::metadata::{BackingRead, QcowMetadata};
use self::parser::{MAX_NESTING_DEPTH, parse_qcow};
use self::qcow_raw_file::QcowRawFile;
use crate::aligned_file::AlignedFile;
#[cfg(any(test, feature = "test-utils"))]
use crate::async_io::GuestMemoryTarget;
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
use crate::disk_file;
#[cfg(any(test, feature = "test-utils"))]
use crate::disk_file::AsyncDiskFile;
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::qcow::backing::shared_backing_from;
use crate::qcow::metadata::{BackingRead, QcowMetadata};
use crate::qcow::qcow_raw_file::QcowRawFile;
use crate::qcow::{MAX_NESTING_DEPTH, RawFile, parse_qcow};
#[cfg(feature = "io_uring")]
use crate::qcow_async::QcowAsync;
use crate::qcow_sync::QcowSync;
/// Unified DiskFile wrapper for QCOW2 disk images.
///
@@ -67,7 +102,7 @@ impl QcowDisk {
}
let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 };
let raw_file = RawFile::new(file, direct_io);
let raw_file = AlignedFile::new(file, direct_io);
let (inner, backing_file, sparse) = parse_qcow(raw_file, max_nesting_depth, sparse)
.map_err(|e| {
let e = if !backing_files && matches!(e.kind(), BlockErrorKind::Overflow) {
@@ -86,6 +121,123 @@ impl QcowDisk {
use_io_uring,
})
}
/// Synchronous write convenience for tests and benchmarks.
#[cfg(any(test, feature = "test-utils"))]
pub fn write_all_at(&self, offset: u64, data: &[u8]) {
let mut async_io = self.create_async_io(1).unwrap();
let mem =
Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), data.len())]).unwrap());
mem.write_slice(data, GuestAddress(0)).unwrap();
let range = [(GuestAddress(0), data.len() as u32)];
let target = GuestMemoryTarget::new(Arc::clone(&mem), &range).unwrap();
async_io
.write_from_memory(offset as libc::off_t, target, 0)
.unwrap();
while async_io.next_completed_request().is_some() {}
}
/// Synchronous read convenience for tests and benchmarks.
#[cfg(test)]
pub fn read_all_at(&self, offset: u64, len: usize) -> Vec<u8> {
let mut async_io = self.create_async_io(1).unwrap();
let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), len)]).unwrap());
let range = [(GuestAddress(0), len as u32)];
let target = GuestMemoryTarget::new(Arc::clone(&mem), &range).unwrap();
async_io
.read_to_memory(offset as libc::off_t, target, 0)
.unwrap();
while async_io.next_completed_request().is_some() {}
let mut buf = vec![0u8; len];
mem.read_slice(&mut buf, GuestAddress(0)).unwrap();
buf
}
#[cfg(test)]
fn metadata(&self) -> &QcowMetadata {
&self.metadata
}
}
/// Writes a fresh qcow2 layout into `file`
#[cfg(any(test, feature = "test-utils"))]
pub(crate) fn create_image(
file: &File,
virtual_size: u64,
backing_config: Option<&BackingFileConfig>,
) -> BlockResult<()> {
let path = backing_config.map(|cfg| cfg.path.as_str());
let mut header = QcowHeader::create_for_size_and_path(3, virtual_size, path)
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
if let Some(cfg) = backing_config
&& let Some(backing_file) = &mut header.backing_file
{
backing_file.format = cfg.format;
}
let raw = AlignedFile::new(
file.try_clone()
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?,
false,
);
header
.write_to(&raw)
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let (inner, _backing, _sparse) = parse_qcow(raw, MAX_NESTING_DEPTH, true)?;
// Flush dirty caches and clear the dirty bit
QcowMetadata::new(inner).shutdown();
Ok(())
}
/// Helper struct to create a new qcow2 image in a temporary file.
#[cfg(any(test, feature = "test-utils"))]
pub struct QcowTempDisk {
tmp: TempFile,
disk: QcowDisk,
}
#[cfg(any(test, feature = "test-utils"))]
impl QcowTempDisk {
/// Creates a new qcow2 image in a temporary file with optional
/// backing file. Flags are passed to QcowDisk::new.
pub fn new(
virtual_size: u64,
backing_config: Option<&BackingFileConfig>,
direct_io: bool,
sparse: bool,
use_io_uring: bool,
) -> BlockResult<Self> {
let tmp = TempFile::new().map_err(io::Error::from)?;
create_image(tmp.as_file(), virtual_size, backing_config)?;
let file = tmp
.as_file()
.try_clone()
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?;
let disk = QcowDisk::new(
file,
direct_io,
backing_config.is_some(),
sparse,
use_io_uring,
)?;
Ok(Self { tmp, disk })
}
pub fn path(&self) -> &Path {
self.tmp.as_path()
}
pub fn as_file(&self) -> &File {
self.tmp.as_file()
}
pub fn disk(&self) -> &QcowDisk {
&self.disk
}
/// Drops the disk handle and returns the underlying TempFile.
pub fn into_tempfile(self) -> TempFile {
self.tmp
}
}
impl Drop for QcowDisk {
@@ -189,22 +341,17 @@ impl disk_file::AsyncDiskFile for QcowDisk {
#[cfg(test)]
mod unit_tests {
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::async_io::AsyncIo;
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize};
use crate::qcow::{QcowFile, RawFile};
const TEST_SIZE: u64 = 0x5566_7788;
fn make_qcow_file() -> File {
let temp_file = TempFile::new().unwrap();
{
let raw = RawFile::new(temp_file.as_file().try_clone().unwrap(), false);
QcowFile::new(raw, 3, TEST_SIZE, true).unwrap();
}
temp_file.into_file()
QcowTempDisk::new(TEST_SIZE, None, false, true, false)
.unwrap()
.into_tempfile()
.into_file()
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,557 @@
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::fmt::Debug;
use std::io::{self, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd};
use std::os::unix::fs::FileExt;
use byteorder::{BigEndian, WriteBytesExt};
use vmm_sys_util::write_zeroes::WriteZeroesAt;
use crate::aligned_file::AlignedFile;
// Type aliases for the refcount read/write function pointers
type RefcountReader = fn(&mut AlignedFile, u64, usize) -> io::Result<Vec<u64>>;
type RefcountWriter = fn(&mut AlignedFile, u64, &[u64]) -> io::Result<()>;
/// Big-endian file access trait.
pub(super) trait BeUint: Sized + Copy {
fn from_be_slice(bytes: &[u8]) -> u64;
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()>;
}
impl BeUint for u8 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
bytes[0] as u64
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u8(val)
}
}
impl BeUint for u16 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
u16::from_be_bytes([bytes[0], bytes[1]]) as u64
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u16::<BigEndian>(val)
}
}
impl BeUint for u32 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u32::<BigEndian>(val)
}
}
impl BeUint for u64 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u64::<BigEndian>(val)
}
}
/// Read byte-aligned refcounts.
fn read_refcount<T: BeUint>(
file: &mut AlignedFile,
offset: u64,
count: usize,
) -> io::Result<Vec<u64>> {
let bytes_per_entry = size_of::<T>();
let mut data = vec![0u8; count * bytes_per_entry];
file.read_exact_at(&mut data, offset)?;
Ok(data
.chunks_exact(bytes_per_entry)
.map(T::from_be_slice)
.collect())
}
/// Write byte-aligned refcounts.
fn write_refcount<T: BeUint + TryFrom<u64>>(
file: &mut AlignedFile,
offset: u64,
table: &[u64],
) -> io::Result<()>
where
<T as TryFrom<u64>>::Error: Debug,
{
let bytes_per_entry = size_of::<T>();
let mut buffer = Vec::with_capacity(table.len() * bytes_per_entry);
for &val in table {
let converted = T::try_from(val).expect("refcount values are validated on increment");
T::write_be(&mut buffer, converted)?;
}
file.write_all_at(&buffer, offset)
}
/// Read sub-byte refcounts. Bit 0 is the least significant bit.
fn read_refcount_subbyte<const BITS: usize>(
file: &mut AlignedFile,
offset: u64,
count: usize,
) -> io::Result<Vec<u64>> {
const { assert!(BITS == 1 || BITS == 2 || BITS == 4) };
let entries_per_byte = 8 / BITS;
let mask = (1u64 << BITS) - 1;
let bytes_needed = count.div_ceil(entries_per_byte);
let mut bytes = vec![0u8; bytes_needed];
file.read_exact_at(&mut bytes, offset)?;
let mut table = vec![0u64; count];
for (i, val) in table.iter_mut().enumerate() {
let byte_idx = i / entries_per_byte;
let bit_offset = (i % entries_per_byte) * BITS;
*val = (bytes[byte_idx] as u64 >> bit_offset) & mask;
}
Ok(table)
}
/// Write sub-byte refcounts. Bit 0 is the least significant bit.
fn write_refcount_subbyte<const BITS: usize>(
file: &mut AlignedFile,
offset: u64,
table: &[u64],
) -> io::Result<()> {
const { assert!(BITS == 1 || BITS == 2 || BITS == 4) };
let entries_per_byte = 8 / BITS;
let mask = (1u64 << BITS) - 1;
let mut buffer = Vec::with_capacity(table.len().div_ceil(entries_per_byte));
for chunk in table.chunks(entries_per_byte) {
let mut byte = 0u8;
for (i, &val) in chunk.iter().enumerate() {
let bit_offset = i * BITS;
byte |= ((val & mask) << bit_offset) as u8;
}
buffer.push(byte);
}
file.write_all_at(&buffer, offset)
}
/// A qcow file. Allows reading/writing clusters and appending clusters.
#[derive(Debug)]
pub(super) struct QcowRawFile {
file: AlignedFile,
cluster_size: u64,
cluster_mask: u64,
refcount_block_entries: u64,
read_refcount_fn: RefcountReader,
write_refcount_fn: RefcountWriter,
}
impl QcowRawFile {
/// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not
/// a power of two or refcount_bits is invalid.
pub(super) fn from(file: AlignedFile, cluster_size: u64, refcount_bits: u64) -> Option<Self> {
if !cluster_size.is_power_of_two() {
return None;
}
let (read_refcount_fn, write_refcount_fn): (RefcountReader, RefcountWriter) =
match refcount_bits {
1 => (read_refcount_subbyte::<1>, write_refcount_subbyte::<1>),
2 => (read_refcount_subbyte::<2>, write_refcount_subbyte::<2>),
4 => (read_refcount_subbyte::<4>, write_refcount_subbyte::<4>),
8 => (read_refcount::<u8>, write_refcount::<u8>),
16 => (read_refcount::<u16>, write_refcount::<u16>),
32 => (read_refcount::<u32>, write_refcount::<u32>),
64 => (read_refcount::<u64>, write_refcount::<u64>),
_ => return None,
};
// For sub-byte refcounts (1,2,4 bits), entries pack multiple per byte
let refcount_block_entries = cluster_size * 8 / refcount_bits;
Some(QcowRawFile {
file,
cluster_size,
cluster_mask: cluster_size - 1,
refcount_block_entries,
read_refcount_fn,
write_refcount_fn,
})
}
/// Reads `count` 64 bit offsets and returns them as a vector.
/// `mask` optionally `&`s out some of the bits on the file.
pub(super) fn read_pointer_table(
&mut self,
offset: u64,
count: u64,
mask: Option<u64>,
) -> io::Result<Vec<u64>> {
let mut bytes = vec![0u8; count as usize * size_of::<u64>()];
self.file.read_exact_at(&mut bytes, offset)?;
let m = mask.unwrap_or(u64::MAX);
let table = bytes
.as_chunks::<{ size_of::<u64>() }>()
.0
.iter()
.map(|c| u64::from_be_bytes(*c) & m)
.collect();
Ok(table)
}
/// Reads a cluster's worth of 64 bit offsets and returns them as a vector.
/// `mask` optionally `&`s out some of the bits on the file.
pub(super) fn read_pointer_cluster(
&mut self,
offset: u64,
mask: Option<u64>,
) -> io::Result<Vec<u64>> {
let count = self.cluster_size / size_of::<u64>() as u64;
self.read_pointer_table(offset, count, mask)
}
/// Writes a pointer table to `offset` in the file.
/// Entries are computed on-the-fly by the callback.
///
/// The callback may perform metadata I/O on this `QcowRawFile`, so all
/// entries are materialized before the final positional write.
pub(super) fn write_pointer_table<'a, T: Copy + 'a>(
&mut self,
offset: u64,
entries: impl Iterator<Item = &'a T>,
mut f: impl FnMut(&mut QcowRawFile, T) -> io::Result<u64>,
) -> io::Result<()> {
let mut buffer = Vec::with_capacity(entries.size_hint().0 * size_of::<u64>());
for addr in entries {
let entry = f(self, *addr)?;
buffer.extend_from_slice(&entry.to_be_bytes());
}
self.file.write_all_at(&buffer, offset)
}
/// Writes a pointer table directly without transforming values.
///
/// Uses the same materialize-then-write path as `write_pointer_table`.
pub(super) fn write_pointer_table_direct<'a>(
&mut self,
offset: u64,
entries: impl Iterator<Item = &'a u64>,
) -> io::Result<()> {
let mut buffer = Vec::with_capacity(entries.size_hint().0 * size_of::<u64>());
for &entry in entries {
buffer.extend_from_slice(&entry.to_be_bytes());
}
self.file.write_all_at(&buffer, offset)
}
/// Read a refcount block from the file and returns a Vec containing the block.
/// Always returns a cluster's worth of data.
#[inline]
pub(super) fn read_refcount_block(&mut self, offset: u64) -> io::Result<Vec<u64>> {
(self.read_refcount_fn)(&mut self.file, offset, self.refcount_block_entries as usize)
}
/// Writes a refcount block to the file.
#[inline]
pub(super) fn write_refcount_block(&mut self, offset: u64, table: &[u64]) -> io::Result<()> {
(self.write_refcount_fn)(&mut self.file, offset, table)
}
/// Allocates a new cluster at the end of the current file, return the address.
pub(super) fn add_cluster_end(
&mut self,
max_valid_cluster_offset: u64,
) -> io::Result<Option<u64>> {
// Determine where the new end of the file should be and set_len, which
// translates to truncate(2).
let file_end: u64 = self.physical_size()?;
let new_cluster_address: u64 = (file_end + self.cluster_size - 1) & !self.cluster_mask;
if new_cluster_address > max_valid_cluster_offset {
return Ok(None);
}
self.file.set_len(new_cluster_address + self.cluster_size)?;
Ok(Some(new_cluster_address))
}
/// Returns a reference to the underlying file.
pub(super) fn file(&self) -> &AlignedFile {
&self.file
}
/// Returns a mutable reference to the underlying file.
pub(super) fn file_mut(&mut self) -> &mut AlignedFile {
&mut self.file
}
/// Returns the size of the file's clusters.
pub(super) fn cluster_size(&self) -> u64 {
self.cluster_size
}
/// Returns the offset of `address` within a cluster.
pub(super) fn cluster_offset(&self, address: u64) -> u64 {
address & self.cluster_mask
}
/// Returns the base address of the cluster containing `address`.
pub(super) fn cluster_address(&self, address: u64) -> u64 {
address & !self.cluster_mask
}
/// Zeros out a cluster in the file.
pub(super) fn zero_cluster(&mut self, address: u64) -> io::Result<()> {
let cluster_size = self.cluster_size as usize;
self.file.write_all_zeroes_at(address, cluster_size)?;
Ok(())
}
/// Writes
pub(super) fn write_cluster(&mut self, address: u64, data: &[u8]) -> io::Result<()> {
let cluster_size = self.cluster_size as usize;
self.file.write_all_at(&data[0..cluster_size], address)
}
pub(super) fn physical_size(&self) -> io::Result<u64> {
self.file.metadata().map(|m| m.len())
}
}
impl Clone for QcowRawFile {
fn clone(&self) -> Self {
QcowRawFile {
file: self.file.try_clone().expect("QcowRawFile cloning failed"),
cluster_size: self.cluster_size,
cluster_mask: self.cluster_mask,
refcount_block_entries: self.refcount_block_entries,
read_refcount_fn: self.read_refcount_fn,
write_refcount_fn: self.write_refcount_fn,
}
}
}
impl AsRawFd for QcowRawFile {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl AsFd for QcowRawFile {
fn as_fd(&self) -> BorrowedFd<'_> {
self.file.as_fd()
}
}
#[cfg(test)]
mod unit_tests {
use std::io::Read;
use std::os::unix::fs::FileExt;
use vmm_sys_util::tempfile::TempFile;
use super::*;
fn be_bytes(entries: &[u64]) -> Vec<u8> {
let mut v = Vec::with_capacity(size_of_val(entries));
for e in entries {
v.extend_from_slice(&e.to_be_bytes());
}
v
}
fn find_all(haystack: &[u8], needle: &[u8]) -> Vec<usize> {
haystack
.windows(needle.len())
.enumerate()
.filter(|(_, w)| *w == needle)
.map(|(i, _)| i)
.collect()
}
const CLUSTER_SIZE: u64 = 0x10000; // 64 KiB
const TARGET_OFFSET: u64 = 0x1000; // where the table must be written
const FAR_OFFSET: u64 = 0x9000; // where the callback reads (refcount block)
const FILE_LEN: u64 = 0x40000; // 256 KiB filler so all offsets are valid
fn make_qcow_raw() -> (TempFile, QcowRawFile) {
make_qcow_raw_bits(16)
}
fn make_qcow_raw_bits(refcount_bits: u64) -> (TempFile, QcowRawFile) {
let temp_file = TempFile::new().unwrap();
temp_file.as_file().set_len(FILE_LEN).unwrap();
let file = temp_file.as_file().try_clone().unwrap();
let raw = AlignedFile::new(file, false);
let qcow_raw =
QcowRawFile::from(raw, CLUSTER_SIZE, refcount_bits).expect("QcowRawFile::from");
(temp_file, qcow_raw)
}
#[test]
fn write_pointer_table_lands_at_offset_despite_callback_seek() {
let (temp_file, mut qcow) = make_qcow_raw();
let entries: Vec<u64> = vec![0x1111_2222_3333_4444u64; 8]; // 64 bytes
qcow.write_pointer_table(TARGET_OFFSET, entries.iter(), |q, addr| {
let _ = q.read_refcount_block(FAR_OFFSET)?;
Ok(addr)
})
.expect("write_pointer_table");
let expected = be_bytes(&entries);
let mut verify = temp_file.as_file().try_clone().unwrap();
let mut whole = Vec::new();
verify.read_to_end(&mut whole).unwrap();
let found_at = find_all(&whole, &expected);
let mut at_target = vec![0u8; expected.len()];
verify.read_exact_at(&mut at_target, TARGET_OFFSET).unwrap();
assert_eq!(
at_target, expected,
"pointer table did NOT land at TARGET_OFFSET {TARGET_OFFSET:#x}; \
found matching bytes at {found_at:x?}"
);
}
#[test]
fn write_pointer_table_direct_lands_at_offset() {
let (temp_file, mut qcow) = make_qcow_raw();
let entries: Vec<u64> = vec![0xAAAA_BBBB_CCCC_DDDDu64; 8];
qcow.write_pointer_table_direct(TARGET_OFFSET, entries.iter())
.expect("write_pointer_table_direct");
let expected = be_bytes(&entries);
let verify = temp_file.as_file().try_clone().unwrap();
let mut at_target = vec![0u8; expected.len()];
verify.read_exact_at(&mut at_target, TARGET_OFFSET).unwrap();
assert_eq!(
at_target, expected,
"write_pointer_table_direct did not land at {TARGET_OFFSET:#x}"
);
}
#[test]
fn read_pointer_table_round_trips() {
let (_temp_file, mut qcow) = make_qcow_raw();
let entries: Vec<u64> = vec![
0x0000_0000_0000_0000,
0x0011_2233_4455_6677,
0x8899_aabb_ccdd_eeff,
0xffff_ffff_ffff_ffff,
];
qcow.write_pointer_table_direct(TARGET_OFFSET, entries.iter())
.expect("write_pointer_table_direct");
let read_back = qcow
.read_pointer_table(TARGET_OFFSET, entries.len() as u64, None)
.expect("read_pointer_table");
assert_eq!(read_back, entries);
}
#[test]
fn read_pointer_table_applies_mask() {
let (_temp_file, mut qcow) = make_qcow_raw();
let entries: Vec<u64> = vec![0xffff_ffff_ffff_ffffu64; 4];
let mask = 0x00ff_ffff_ffff_fe00u64;
qcow.write_pointer_table_direct(TARGET_OFFSET, entries.iter())
.expect("write_pointer_table_direct");
let read_back = qcow
.read_pointer_table(TARGET_OFFSET, entries.len() as u64, Some(mask))
.expect("read_pointer_table");
assert!(read_back.iter().all(|&e| e == mask));
}
#[test]
fn write_cluster_then_zero_cluster_round_trips() {
let (temp_file, mut qcow) = make_qcow_raw();
let cluster_size = CLUSTER_SIZE as usize;
let data: Vec<u8> = (0..cluster_size).map(|i| (i % 251) as u8).collect();
qcow.write_cluster(CLUSTER_SIZE, &data)
.expect("write_cluster");
let verify = temp_file.as_file().try_clone().unwrap();
let mut buf = vec![0u8; cluster_size];
verify.read_exact_at(&mut buf, CLUSTER_SIZE).unwrap();
assert_eq!(buf, data);
qcow.zero_cluster(CLUSTER_SIZE).expect("zero_cluster");
verify.read_exact_at(&mut buf, CLUSTER_SIZE).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn refcount_block_round_trips() {
let (_temp_file, mut qcow) = make_qcow_raw_bits(16);
let count = qcow.refcount_block_entries as usize;
let table: Vec<u64> = (0..count).map(|i| (i % 251) as u64).collect();
qcow.write_refcount_block(TARGET_OFFSET, &table)
.expect("write_refcount_block");
let read_back = qcow
.read_refcount_block(TARGET_OFFSET)
.expect("read_refcount_block");
assert_eq!(read_back, table);
}
#[test]
fn refcount_block_subbyte_round_trips() {
let (_temp_file, mut qcow) = make_qcow_raw_bits(4);
let count = qcow.refcount_block_entries as usize;
let table: Vec<u64> = (0..count).map(|i| (i % 16) as u64).collect();
qcow.write_refcount_block(TARGET_OFFSET, &table)
.expect("write_refcount_block");
let read_back = qcow
.read_refcount_block(TARGET_OFFSET)
.expect("read_refcount_block");
assert_eq!(read_back, table);
}
#[test]
fn add_cluster_end_appends_aligned_cluster() {
let (_temp_file, mut qcow) = make_qcow_raw();
let before = qcow.physical_size().unwrap();
let addr = qcow
.add_cluster_end(u64::MAX)
.expect("add_cluster_end")
.expect("a cluster was allocated");
assert_eq!(addr % CLUSTER_SIZE, 0);
assert!(addr >= before);
assert_eq!(qcow.physical_size().unwrap(), addr + CLUSTER_SIZE);
}
#[test]
fn add_cluster_end_respects_max_offset() {
let (_temp_file, mut qcow) = make_qcow_raw();
assert!(qcow.add_cluster_end(0).unwrap().is_none());
}
}

View File

@@ -4,13 +4,13 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::io;
use std::{io, result};
use libc::EINVAL;
use thiserror::Error;
use crate::qcow::qcow_raw_file::QcowRawFile;
use crate::qcow::vec_cache::{CacheMap, Cacheable, VecCache};
use super::qcow_raw_file::QcowRawFile;
use super::vec_cache::{CacheMap, Cacheable, VecCache};
#[derive(Debug, Error)]
pub enum Error {
@@ -41,11 +41,11 @@ pub enum Error {
},
}
pub type Result<T> = std::result::Result<T, Error>;
pub(super) type Result<T> = result::Result<T, Error>;
/// Represents the refcount entries for an open qcow file.
#[derive(Clone, Debug)]
pub struct RefCount {
pub(super) struct RefCount {
ref_table: VecCache<u64>,
refcount_table_offset: u64,
refblock_cache: CacheMap<VecCache<u64>>,
@@ -62,7 +62,7 @@ impl RefCount {
/// `refcount_block_entries` indicates the number of refcounts in each refcount block.
/// `refcount_bits` is the number of bits per refcount (1, 2, 4, 8, 16, 32, or 64).
/// Each refcount table entry points to a refcount block.
pub fn new(
pub(super) fn new(
raw_file: &mut QcowRawFile,
refcount_table_offset: u64,
refcount_table_entries: u64,
@@ -95,12 +95,12 @@ impl RefCount {
}
/// Returns the number of refcounts per block.
pub fn refcounts_per_block(&self) -> u64 {
pub(super) fn refcounts_per_block(&self) -> u64 {
self.refcount_block_entries
}
/// Returns the maximum valid cluster offset in the raw file for this refcount table.
pub fn max_valid_cluster_offset(&self) -> u64 {
pub(super) fn max_valid_cluster_offset(&self) -> u64 {
self.max_valid_cluster_offset
}
@@ -109,7 +109,7 @@ impl RefCount {
/// allocate a cluster or read the required one and call this function again with the cluster.
/// On success, an optional address of a dropped cluster is returned. The dropped cluster can
/// be reused for other purposes.
pub fn set_cluster_refcount(
pub(super) fn set_cluster_refcount(
&mut self,
raw_file: &mut QcowRawFile,
cluster_address: u64,
@@ -167,14 +167,14 @@ impl RefCount {
/// Flush the dirty refcount blocks. This must be done before flushing the table that points to
/// the blocks.
pub fn flush_blocks(&mut self, raw_file: &mut QcowRawFile) -> io::Result<()> {
pub(super) fn flush_blocks(&mut self, raw_file: &mut QcowRawFile) -> io::Result<()> {
// Write out all dirty L2 tables.
for (table_index, block) in self.refblock_cache.iter_mut().filter(|(_k, v)| v.dirty()) {
let addr = self.ref_table[*table_index];
if addr != 0 {
raw_file.write_refcount_block(addr, block.get_values())?;
} else {
return Err(std::io::Error::from_raw_os_error(EINVAL));
return Err(io::Error::from_raw_os_error(EINVAL));
}
block.mark_clean();
}
@@ -183,7 +183,7 @@ impl RefCount {
/// Flush the refcount table that keeps the address of the refcounts blocks.
/// Returns true if the table changed since the previous `flush_table()` call.
pub fn flush_table(&mut self, raw_file: &mut QcowRawFile) -> io::Result<bool> {
pub(super) fn flush_table(&mut self, raw_file: &mut QcowRawFile) -> io::Result<bool> {
if self.ref_table.dirty() {
raw_file
.write_pointer_table_direct(self.refcount_table_offset, self.ref_table.iter())?;
@@ -195,7 +195,7 @@ impl RefCount {
}
/// Gets the refcount for a cluster with the given address.
pub fn get_cluster_refcount(
pub(super) fn get_cluster_refcount(
&mut self,
raw_file: &mut QcowRawFile,
address: u64,
@@ -224,41 +224,6 @@ impl RefCount {
Ok(self.refblock_cache.get(table_index).unwrap()[block_index])
}
/// Returns the refcount table for this file. This is only useful for debugging.
pub fn ref_table(&self) -> &[u64] {
self.ref_table.get_values()
}
/// Returns the refcounts stored in the given block.
pub fn refcount_block(
&mut self,
raw_file: &mut QcowRawFile,
table_index: usize,
) -> Result<Option<&[u64]>> {
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;
if block_addr_disk == 0 {
return Ok(None);
}
if !self.refblock_cache.contains_key(table_index) {
let table = VecCache::from_vec(
raw_file
.read_refcount_block(block_addr_disk)
.map_err(Error::ReadingRefCounts)?,
);
// TODO(dgreid) - closure needs to return an error.
let ref_table = &self.ref_table;
self.refblock_cache
.insert(table_index, table, |index, evicted| {
raw_file.write_refcount_block(ref_table[index], evicted.get_values())
})
.map_err(Error::EvictingRefCounts)?;
}
// The index must exist as it was just inserted if it didn't already.
Ok(Some(
self.refblock_cache.get(table_index).unwrap().get_values(),
))
}
// Gets the address of the refcount block and the index into the block for the given address.
fn get_refcount_index(&self, address: u64) -> (usize, usize) {
let block_index = (address / self.cluster_size) % self.refcount_block_entries;

View File

@@ -63,6 +63,11 @@ pub(super) fn l2_entry_make_zero(cluster_addr: u64) -> u64 {
(cluster_addr & L2_TABLE_OFFSET_MASK) | CLUSTER_USED_FLAG | ZERO_FLAG
}
/// Make L2 entry for an unallocated cluster that reads as logical zeros.
pub(super) fn l2_entry_make_zero_plain() -> u64 {
ZERO_FLAG
}
/// Make L1 entry with optional flags.
pub(super) fn l1_entry_make(cluster_addr: u64, refcount_is_one: bool) -> u64 {
(cluster_addr & L1_TABLE_OFFSET_MASK) | (refcount_is_one as u64 * CLUSTER_USED_FLAG)

View File

@@ -12,21 +12,21 @@ use std::slice::SliceIndex;
/// Trait that allows for checking if an implementor is dirty. Useful for types that are cached so
/// it can be checked if they need to be committed to disk.
pub trait Cacheable {
pub(super) trait Cacheable {
/// Used to check if the item needs to be written out or if it can be discarded.
fn dirty(&self) -> bool;
}
#[derive(Clone, Debug)]
/// Represents a vector that implements the `Cacheable` trait so it can be held in a cache.
pub struct VecCache<T: 'static + Copy + Default> {
pub(super) struct VecCache<T: 'static + Copy + Default> {
vec: Box<[T]>,
dirty: bool,
}
impl<T: 'static + Copy + Default> VecCache<T> {
/// Creates a `VecCache` that can hold `count` elements.
pub fn new(count: usize) -> VecCache<T> {
pub(super) fn new(count: usize) -> VecCache<T> {
VecCache {
vec: vec![Default::default(); count].into_boxed_slice(),
dirty: true,
@@ -34,14 +34,14 @@ impl<T: 'static + Copy + Default> VecCache<T> {
}
/// Creates a `VecCache` from the passed in `vec`.
pub fn from_vec(vec: Vec<T>) -> VecCache<T> {
pub(super) fn from_vec(vec: Vec<T>) -> VecCache<T> {
VecCache {
vec: vec.into_boxed_slice(),
dirty: false,
}
}
pub fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[T]>>::Output>
pub(super) fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[T]>>::Output>
where
I: SliceIndex<[T]>,
{
@@ -49,17 +49,17 @@ impl<T: 'static + Copy + Default> VecCache<T> {
}
/// Gets a reference to the underlying vector.
pub fn get_values(&self) -> &[T] {
pub(super) fn get_values(&self) -> &[T] {
&self.vec
}
/// Mark this cache element as clean.
pub fn mark_clean(&mut self) {
pub(super) fn mark_clean(&mut self) {
self.dirty = false;
}
/// Returns the number of elements in the vector.
pub fn len(&self) -> usize {
pub(super) fn len(&self) -> usize {
self.vec.len()
}
@@ -68,7 +68,7 @@ impl<T: 'static + Copy + Default> VecCache<T> {
/// No-op if `new_len <= self.len()`. Allocates a new buffer, copies
/// existing data, and fills new elements with default values.
/// Marks the cache as dirty.
pub fn extend(&mut self, new_len: usize) {
pub(super) fn extend(&mut self, new_len: usize) {
if new_len <= self.vec.len() {
return;
}
@@ -109,37 +109,37 @@ impl<T: 'static + Copy + Default> Deref for VecCache<T> {
}
#[derive(Clone, Debug)]
pub struct CacheMap<T: Cacheable> {
pub(super) struct CacheMap<T: Cacheable> {
capacity: usize,
map: HashMap<usize, T>,
}
impl<T: Cacheable> CacheMap<T> {
pub fn new(capacity: usize) -> Self {
pub(super) fn new(capacity: usize) -> Self {
CacheMap {
capacity,
map: HashMap::with_capacity(capacity),
}
}
pub fn contains_key(&self, key: usize) -> bool {
pub(super) fn contains_key(&self, key: usize) -> bool {
self.map.contains_key(&key)
}
pub fn get(&self, index: usize) -> Option<&T> {
pub(super) fn get(&self, index: usize) -> Option<&T> {
self.map.get(&index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
pub(super) fn get_mut(&mut self, index: usize) -> Option<&mut T> {
self.map.get_mut(&index)
}
pub fn iter_mut(&mut self) -> IterMut<'_, usize, T> {
pub(super) fn iter_mut(&mut self) -> IterMut<'_, usize, T> {
self.map.iter_mut()
}
// Check if the refblock cache is full and we need to evict.
pub fn insert<F>(&mut self, index: usize, block: T, write_callback: F) -> io::Result<()>
pub(super) fn insert<F>(&mut self, index: usize, block: T, write_callback: F) -> io::Result<()>
where
F: FnOnce(usize, T) -> io::Result<()>,
{

View File

@@ -0,0 +1,159 @@
// Copyright © 2023 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//
// Copyright © 2023 Crusoe Energy Systems LLC
//
use std::os::unix::io::AsRawFd;
use vmm_sys_util::eventfd::EventFd;
use super::{operation_is_aligned, run_unaligned_operation};
use crate::async_io::{
AioDataIo, AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult,
};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::sparse::{punch_hole, write_zeroes};
use crate::{AlignedFile, is_block_device};
pub(super) struct RawAio {
raw_file: AlignedFile,
data_io: AioDataIo,
alignment: u64,
is_block_device: bool,
}
impl RawAio {
pub(super) fn new(raw_file: AlignedFile, queue_depth: u32) -> BlockResult<Self> {
let data_io =
AioDataIo::new(queue_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let is_block_device = is_block_device(raw_file.as_raw_fd());
let alignment = raw_file.alignment() as u64;
Ok(RawAio {
raw_file,
data_io,
alignment,
is_block_device,
})
}
}
impl AsyncIo for RawAio {
fn notifier(&self) -> &EventFd {
self.data_io.notifier()
}
fn alignment(&self) -> u64 {
self.alignment
}
fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> {
let is_read = op.is_read();
if operation_is_aligned(&op, self.alignment) {
let fd = self.raw_file.as_raw_fd();
return self.data_io.submit_operation(fd, op).map_err(|e| {
if is_read {
AsyncIoError::ReadVectored(e)
} else {
AsyncIoError::WriteVectored(e)
}
});
}
let result = run_unaligned_operation(&self.raw_file, &mut op)?;
self.data_io
.inject_completion(AsyncIoCompletion::from_operation(op, result));
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
let fd = self.raw_file.as_raw_fd();
if let Some(user_data) = user_data {
self.data_io
.submit_fsync(fd, user_data)
.map_err(AsyncIoError::Fsync)?;
} else {
// SAFETY: FFI call with a valid fd
unsafe { libc::fsync(fd) };
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.data_io.next_completion()
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Linux AIO has no IOCB command for fallocate, so perform the
// operation synchronously and signal completion via the completion
// list, matching the pattern used by the sync backend (RawSync).
punch_hole(
self.raw_file.as_raw_fd(),
self.is_block_device,
offset,
length,
)
.map_err(AsyncIoError::PunchHole)?;
self.data_io
.inject_completion(AsyncIoCompletion::new(user_data, 0, None));
Ok(())
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Same as punch_hole().
write_zeroes(
self.raw_file.as_raw_fd(),
self.is_block_device,
offset,
length,
)
.map_err(AsyncIoError::WriteZeroes)?;
self.data_io
.inject_completion(AsyncIoCompletion::new(user_data, 0, None));
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::formats::raw::tests;
#[test]
fn test_punch_hole() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io =
RawAio::new(AlignedFile::new(file.try_clone().unwrap(), false), 128).unwrap();
tests::test_punch_hole(&mut async_io, &mut file);
}
#[test]
fn test_write_zeroes() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io =
RawAio::new(AlignedFile::new(file.try_clone().unwrap(), false), 128).unwrap();
tests::test_write_zeroes(&mut async_io, &mut file);
}
#[test]
fn test_punch_hole_multiple_operations() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io =
RawAio::new(AlignedFile::new(file.try_clone().unwrap(), false), 128).unwrap();
tests::test_punch_hole_multiple_operations(&mut async_io, &mut file);
}
}

View File

@@ -0,0 +1,181 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::collections::VecDeque;
use std::io;
use std::os::unix::io::AsRawFd;
use vmm_sys_util::eventfd::EventFd;
use super::{operation_is_aligned, run_unaligned_operation};
use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult};
use crate::sparse::{punch_hole, write_zeroes};
use crate::{AlignedFile, is_block_device};
pub(crate) struct RawSync {
raw_file: AlignedFile,
eventfd: EventFd,
completion_list: VecDeque<AsyncIoCompletion>,
alignment: u64,
is_block_device: bool,
}
impl RawSync {
pub(crate) fn new(raw_file: AlignedFile) -> Self {
let is_block_device = is_block_device(raw_file.as_raw_fd());
let alignment = raw_file.alignment() as u64;
RawSync {
raw_file,
eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"),
completion_list: VecDeque::new(),
alignment,
is_block_device,
}
}
}
impl AsyncIo for RawSync {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn alignment(&self) -> u64 {
self.alignment
}
fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> {
let is_read = op.is_read();
let result = if operation_is_aligned(&op, self.alignment) {
let fd = self.raw_file.as_raw_fd();
let offset = op.offset();
let iovecs = op.iovecs();
let result = if is_read {
// SAFETY: the memory pointed to by `iovecs` is backed by the op,
// and valid for the kernel to write to by construction of
// AsyncIoOperation.
unsafe {
libc::preadv(
fd as libc::c_int,
iovecs.as_ptr(),
iovecs.len() as libc::c_int,
offset,
)
}
} else {
// SAFETY: the memory pointed to by `iovecs` is backed by the op,
// and valid for the kernel to read from by construction of
// AsyncIoOperation.
unsafe {
libc::pwritev(
fd as libc::c_int,
iovecs.as_ptr(),
iovecs.len() as libc::c_int,
offset,
)
}
};
if result < 0 {
let error = io::Error::last_os_error();
return Err(if is_read {
AsyncIoError::ReadVectored(error)
} else {
AsyncIoError::WriteVectored(error)
});
}
result as i32
} else {
run_unaligned_operation(&self.raw_file, &mut op)?
};
self.completion_list
.push_back(AsyncIoCompletion::from_operation(op, result));
self.eventfd.write(1).unwrap();
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
// SAFETY: FFI call
let result = unsafe { libc::fsync(self.raw_file.as_raw_fd() as libc::c_int) };
if result < 0 {
return Err(AsyncIoError::Fsync(io::Error::last_os_error()));
}
if let Some(user_data) = user_data {
self.completion_list
.push_back(AsyncIoCompletion::new(user_data, result, None));
self.eventfd.write(1).unwrap();
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.completion_list.pop_front()
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
punch_hole(
self.raw_file.as_raw_fd(),
self.is_block_device,
offset,
length,
)
.map_err(AsyncIoError::PunchHole)?;
self.completion_list
.push_back(AsyncIoCompletion::new(user_data, 0, None));
self.eventfd.write(1).unwrap();
Ok(())
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
write_zeroes(
self.raw_file.as_raw_fd(),
self.is_block_device,
offset,
length,
)
.map_err(AsyncIoError::WriteZeroes)?;
self.completion_list
.push_back(AsyncIoCompletion::new(user_data, 0, None));
self.eventfd.write(1).unwrap();
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::formats::raw::tests;
#[test]
fn test_punch_hole() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawSync::new(AlignedFile::new(file.try_clone().unwrap(), false));
tests::test_punch_hole(&mut async_io, &mut file);
}
#[test]
fn test_write_zeroes() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawSync::new(AlignedFile::new(file.try_clone().unwrap(), false));
tests::test_write_zeroes(&mut async_io, &mut file);
}
#[test]
fn test_punch_hole_multiple_operations() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawSync::new(AlignedFile::new(file.try_clone().unwrap(), false));
tests::test_punch_hole_multiple_operations(&mut async_io, &mut file);
}
}

View File

@@ -0,0 +1,162 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::os::unix::io::AsRawFd;
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
use vmm_sys_util::eventfd::EventFd;
use super::{operation_is_aligned, run_unaligned_operation};
use crate::async_io::{
AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, UringDataIo,
};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::sparse::{blkdiscard, blkzeroout};
use crate::{AlignedFile, is_block_device};
pub(crate) struct RawAsync {
raw_file: AlignedFile,
data_io: UringDataIo,
alignment: u64,
is_block_device: bool,
}
impl RawAsync {
pub(crate) fn new(raw_file: AlignedFile, ring_depth: u32) -> BlockResult<Self> {
let data_io =
UringDataIo::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let is_block_device = is_block_device(raw_file.as_raw_fd());
let alignment = raw_file.alignment() as u64;
Ok(RawAsync {
raw_file,
data_io,
alignment,
is_block_device,
})
}
}
impl AsyncIo for RawAsync {
fn notifier(&self) -> &EventFd {
self.data_io.notifier()
}
fn alignment(&self) -> u64 {
self.alignment
}
fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> {
let is_read = op.is_read();
if operation_is_aligned(&op, self.alignment) {
let fd = self.raw_file.as_raw_fd();
return self.data_io.submit_operation(fd, op).map_err(|e| {
if is_read {
AsyncIoError::ReadVectored(e)
} else {
AsyncIoError::WriteVectored(e)
}
});
}
let result = run_unaligned_operation(&self.raw_file, &mut op)?;
self.data_io
.inject_completion(AsyncIoCompletion::from_operation(op, result));
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
let fd = self.raw_file.as_raw_fd();
if let Some(user_data) = user_data {
self.data_io
.submit_fsync(fd, user_data)
.map_err(AsyncIoError::Fsync)?;
} else {
// SAFETY: FFI call with a valid fd
unsafe { libc::fsync(fd) };
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.data_io.next_completion()
}
fn batch_requests_enabled(&self) -> bool {
true
}
fn submit_batch_requests(&mut self, batch_request: Vec<AsyncIoOperation>) -> AsyncIoResult<()> {
if self.alignment != 0 {
let mut aligned_batch = Vec::with_capacity(batch_request.len());
for mut op in batch_request {
if operation_is_aligned(&op, self.alignment) {
aligned_batch.push(op);
} else {
let result = run_unaligned_operation(&self.raw_file, &mut op)?;
self.data_io
.inject_completion(AsyncIoCompletion::from_operation(op, result));
}
}
if aligned_batch.is_empty() {
return Ok(());
}
return self
.data_io
.submit_batch(self.raw_file.as_raw_fd(), aligned_batch)
.map_err(AsyncIoError::SubmitBatchRequests);
}
self.data_io
.submit_batch(self.raw_file.as_raw_fd(), batch_request)
.map_err(AsyncIoError::SubmitBatchRequests)
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Some block devices don't support fallocate(). Use ioctl instead. The assumption is that
// this happens rarely and we don't need to introduce unnecessary complexity by submitting
// a fallocate request, reaping ENOTSUPP in the completion routine, and reissuing the
// request with an ioctl.
if self.is_block_device {
blkdiscard(self.raw_file.as_raw_fd(), offset, length)
.map_err(AsyncIoError::PunchHole)?;
// Deliver the completion through the normal io_uring path by
// queuing a NOP carrying `user_data`. The registered eventfd will
// fire when it completes, just like any other request.
return self
.data_io
.submit_nop(user_data)
.map_err(AsyncIoError::PunchHole);
}
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
self.data_io
.submit_fallocate(self.raw_file.as_raw_fd(), offset, length, mode, user_data)
.map_err(AsyncIoError::PunchHole)
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Same rationale as punch_hole().
if self.is_block_device {
blkzeroout(self.raw_file.as_raw_fd(), offset, length)
.map_err(AsyncIoError::WriteZeroes)?;
return self
.data_io
.submit_nop(user_data)
.map_err(AsyncIoError::WriteZeroes);
}
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
self.data_io
.submit_fallocate(self.raw_file.as_raw_fd(), offset, length, mode, user_data)
.map_err(AsyncIoError::WriteZeroes)
}
}

View File

@@ -2,6 +2,11 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! Raw disk image format.
//!
//! Provides [`RawDisk`], the `DiskFile` wrapper for flat disk images
//! with no metadata or copy on write layer.
use std::fs::File;
use std::io;
use std::os::unix::fs::FileTypeExt;
@@ -9,13 +14,22 @@ use std::os::unix::io::AsRawFd;
use log::warn;
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use self::engine_aio::RawAio;
use self::engine_sync::RawSync;
#[cfg(feature = "io_uring")]
use crate::raw_async::RawFileAsync;
use crate::raw_async_aio::RawFileAsyncAio;
use crate::raw_sync::RawFileSync;
use crate::{DiskTopology, disk_file, probe_sparse_support, query_device_size};
use self::engine_uring::RawAsync;
use crate::async_io::{
AsyncIo, AsyncIoError, AsyncIoOperation, AsyncIoResult, BorrowedDiskFd, DiskFileError,
};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::{AlignedFile, DiskTopology, disk_file, probe_sparse_support, query_device_size};
mod engine_aio;
pub(crate) mod engine_sync;
#[cfg(feature = "io_uring")]
pub(crate) mod engine_uring;
#[cfg(test)]
mod tests;
/// Selects which async I/O backend a `RawDisk` uses.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -39,11 +53,16 @@ pub enum RawBackend {
pub struct RawDisk {
file: File,
backend: RawBackend,
direct: bool,
}
impl RawDisk {
pub fn new(file: File, backend: RawBackend) -> Self {
Self { file, backend }
pub fn new(file: File, backend: RawBackend, direct: bool) -> Self {
Self {
file,
backend,
direct,
}
}
}
@@ -124,25 +143,60 @@ impl disk_file::AsyncDiskFile for RawDisk {
Ok(Box::new(RawDisk {
file,
backend: self.backend,
direct: self.direct,
}))
}
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
let file = self
.file
.try_clone()
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?;
let raw_file = AlignedFile::new(file, self.direct);
match self.backend {
RawBackend::Sync => Ok(Box::new(RawFileSync::new(self.file.as_raw_fd()))),
RawBackend::Sync => Ok(Box::new(RawSync::new(raw_file))),
#[cfg(feature = "io_uring")]
RawBackend::IoUring => Ok(Box::new(RawFileAsync::new(
self.file.as_raw_fd(),
ring_depth,
)?)),
RawBackend::Aio => Ok(Box::new(RawFileAsyncAio::new(
self.file.as_raw_fd(),
ring_depth,
)?)),
RawBackend::IoUring => Ok(Box::new(RawAsync::new(raw_file, ring_depth)?)),
RawBackend::Aio => Ok(Box::new(RawAio::new(raw_file, ring_depth)?)),
}
}
}
/// True when `op` satisfies `alignment` and can go straight to the kernel.
fn operation_is_aligned(op: &AsyncIoOperation, alignment: u64) -> bool {
if alignment == 0 {
return true;
}
if !(op.offset() as u64).is_multiple_of(alignment) {
return false;
}
op.iovecs().iter().all(|iov| {
(iov.iov_base as u64).is_multiple_of(alignment)
&& (iov.iov_len as u64).is_multiple_of(alignment)
})
}
/// Runs an unaligned O_DIRECT operation synchronously through `aligned_file`.
fn run_unaligned_operation(
aligned_file: &AlignedFile,
op: &mut AsyncIoOperation,
) -> AsyncIoResult<i32> {
let offset = op.offset() as u64;
let total_len = op.total_len();
if op.is_read() {
let n = aligned_file
.read_unaligned(offset, total_len, |data| op.write_bytes_at(0, data))
.map_err(AsyncIoError::ReadVectored)?;
Ok(n as i32)
} else {
let n = aligned_file
.write_unaligned(offset, total_len, |data| op.read_bytes_at(0, data))
.map_err(AsyncIoError::WriteVectored)?;
Ok(n as i32)
}
}
#[cfg(test)]
mod unit_tests {
use std::fs::File;
@@ -164,7 +218,7 @@ mod unit_tests {
#[test]
fn new_sync_returns_correct_size() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Sync);
let disk = RawDisk::new(file, RawBackend::Sync, false);
assert_eq!(disk.logical_size().unwrap(), TEST_SIZE);
}
@@ -200,14 +254,14 @@ mod unit_tests {
#[test]
fn sync_backend_disables_batch_requests() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Sync);
let disk = RawDisk::new(file, RawBackend::Sync, false);
assert_sync_backend(&disk);
}
#[test]
fn aio_backend_disables_batch_requests() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Aio);
let disk = RawDisk::new(file, RawBackend::Aio, false);
assert_aio_backend(&disk);
}
@@ -215,7 +269,7 @@ mod unit_tests {
#[test]
fn io_uring_backend_enables_batch_requests() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::IoUring);
let disk = RawDisk::new(file, RawBackend::IoUring, false);
assert_io_uring_backend(&disk);
}
@@ -227,14 +281,14 @@ mod unit_tests {
#[test]
fn try_clone_preserves_sync_backend() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Sync);
let disk = RawDisk::new(file, RawBackend::Sync, false);
assert_try_clone(&disk, RawBackend::Sync);
}
#[test]
fn try_clone_preserves_aio_backend() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Aio);
let disk = RawDisk::new(file, RawBackend::Aio, false);
assert_try_clone(&disk, RawBackend::Aio);
}
@@ -242,14 +296,14 @@ mod unit_tests {
#[test]
fn try_clone_preserves_io_uring_backend() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::IoUring);
let disk = RawDisk::new(file, RawBackend::IoUring, false);
assert_try_clone(&disk, RawBackend::IoUring);
}
#[test]
fn resize_changes_file_size() {
let file = make_raw_file();
let mut disk = RawDisk::new(file, RawBackend::Aio);
let mut disk = RawDisk::new(file, RawBackend::Aio, false);
let new_size = TEST_SIZE * 2;
disk.resize(new_size).unwrap();
assert_eq!(disk.logical_size().unwrap(), new_size);
@@ -258,7 +312,7 @@ mod unit_tests {
#[test]
fn physical_size_reports_allocated_blocks() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Aio);
let disk = RawDisk::new(file, RawBackend::Aio, false);
// Sparse file: physical size is less than logical size.
assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap());
}

View File

@@ -1,5 +1,7 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! Shared test helpers for [`AsyncIo`] backends.
@@ -13,6 +15,11 @@ use std::io::{Read, Seek, SeekFrom, Write};
use crate::async_io::{AsyncIo, AsyncIoError};
fn next_completion(async_io: &mut dyn AsyncIo) -> (u64, i32) {
let completion = async_io.next_completed_request().expect("No completion");
(completion.user_data, completion.result)
}
/// Tests punching a hole in the middle of a 4 MB file and verifying data
/// integrity around the hole.
pub fn test_punch_hole(async_io: &mut dyn AsyncIo, file: &mut File) {
@@ -27,7 +34,7 @@ pub fn test_punch_hole(async_io: &mut dyn AsyncIo, file: &mut File) {
async_io.punch_hole(offset, length, 1).unwrap();
// Check completion
let (user_data, result) = async_io.next_completed_request().unwrap();
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 1);
assert_eq!(result, 0);
@@ -84,7 +91,7 @@ pub fn test_write_zeroes(async_io: &mut dyn AsyncIo, file: &mut File) {
write_zeroes_result.unwrap();
// Check completion
let (user_data, result) = async_io.next_completed_request().unwrap();
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 2);
assert_eq!(result, 0);
@@ -134,15 +141,15 @@ pub fn test_punch_hole_multiple_operations(async_io: &mut dyn AsyncIo, file: &mu
.unwrap();
// Check all completions
let (user_data, result) = async_io.next_completed_request().unwrap();
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 10);
assert_eq!(result, 0);
let (user_data, result) = async_io.next_completed_request().unwrap();
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 11);
assert_eq!(result, 0);
let (user_data, result) = async_io.next_completed_request().unwrap();
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 12);
assert_eq!(result, 0);

View File

@@ -0,0 +1,58 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0
use std::io;
use vmm_sys_util::eventfd::EventFd;
use crate::AlignedFile;
use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult};
use crate::formats::raw::engine_sync::RawSync;
pub(super) struct FixedVhdSync {
raw_file_sync: RawSync,
size: u64,
}
impl FixedVhdSync {
pub(super) fn new(raw_file: AlignedFile, size: u64) -> Self {
FixedVhdSync {
raw_file_sync: RawSync::new(raw_file),
size,
}
}
}
impl AsyncIo for FixedVhdSync {
fn notifier(&self) -> &EventFd {
self.raw_file_sync.notifier()
}
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> {
op.validate_bounds(self.size)?;
self.raw_file_sync.submit_data_operation(op)
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.raw_file_sync.fsync(user_data)
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.raw_file_sync.next_completed_request()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::PunchHole(io::Error::other(
"punch_hole not supported for fixed VHD",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(io::Error::other(
"write_zeroes not supported for fixed VHD",
)))
}
}

View File

@@ -0,0 +1,73 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0
use std::io;
use vmm_sys_util::eventfd::EventFd;
use crate::AlignedFile;
use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult};
use crate::error::BlockResult;
use crate::formats::raw::engine_uring::RawAsync;
pub(super) struct FixedVhdAsync {
raw_file_async: RawAsync,
size: u64,
}
impl FixedVhdAsync {
pub(super) fn new(raw_file: AlignedFile, ring_depth: u32, size: u64) -> BlockResult<Self> {
let raw_file_async = RawAsync::new(raw_file, ring_depth)?;
Ok(FixedVhdAsync {
raw_file_async,
size,
})
}
}
impl AsyncIo for FixedVhdAsync {
fn notifier(&self) -> &EventFd {
self.raw_file_async.notifier()
}
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> {
op.validate_bounds(self.size)?;
self.raw_file_async.submit_data_operation(op)
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.raw_file_async.fsync(user_data)
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.raw_file_async.next_completed_request()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::PunchHole(io::Error::other(
"punch_hole not supported for fixed VHD",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(io::Error::other(
"write_zeroes not supported for fixed VHD",
)))
}
fn batch_requests_enabled(&self) -> bool {
true
}
fn submit_batch_requests(&mut self, batch_request: Vec<AsyncIoOperation>) -> AsyncIoResult<()> {
for op in &batch_request {
op.validate_bounds(self.size)?;
}
self.raw_file_async.submit_batch_requests(batch_request)
}
}

View File

@@ -0,0 +1,59 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io;
use std::os::unix::io::{AsRawFd, RawFd};
use super::footer::VhdFooter;
#[derive(Debug)]
pub(super) struct FixedVhd {
file: File,
size: u64,
}
impl FixedVhd {
pub(super) fn new(mut file: File) -> io::Result<Self> {
let footer = VhdFooter::new(&mut file)?;
Ok(Self {
file,
size: footer.current_size(),
})
}
pub(crate) fn file(&self) -> &File {
&self.file
}
}
impl AsRawFd for FixedVhd {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl FixedVhd {
pub(crate) fn logical_size(&self) -> Result<u64, crate::Error> {
Ok(self.size)
}
/// Returns the physical size of the underlying file.
pub(crate) fn physical_size(&self) -> Result<u64, crate::Error> {
self.file
.metadata()
.map(|m| m.len())
.map_err(crate::Error::GetFileMetadata)
}
}
impl Clone for FixedVhd {
fn clone(&self) -> Self {
Self {
file: self.file.try_clone().expect("FixedVhd cloning failed"),
size: self.size,
}
}
}

View File

@@ -3,12 +3,17 @@
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{Seek, SeekFrom};
use std::io;
use std::os::unix::fs::FileExt;
use crate::{DiskTopology, read_aligned_block_size};
use crate::{AlignedFile, query_device_size};
// Production code uses: cookie, file_format_version, data_offset,
// current_size, disk_type. The remaining fields are parsed for VHD
// spec completeness and exercised only by unit tests.
#[derive(Clone, Copy)]
pub struct VhdFooter {
#[cfg_attr(not(test), expect(dead_code))]
pub(super) struct VhdFooter {
cookie: u64,
features: u32,
file_format_version: u32,
@@ -27,17 +32,14 @@ pub struct VhdFooter {
}
impl VhdFooter {
pub fn new(file: &mut File) -> std::io::Result<VhdFooter> {
let blocksize = DiskTopology::probe(file)?.logical_block_size as usize;
// Place the cursor in the last block of the file
file.seek(SeekFrom::End(0 - (blocksize as i64)))?;
// Read in the last block
let data = read_aligned_block_size(file)?;
// We only care about the last sector
let offset = blocksize - 512;
let sector = &data[offset..];
pub(super) fn new(file: &mut File) -> io::Result<VhdFooter> {
let aligned = AlignedFile::new(file.try_clone()?, true);
let size = query_device_size(file)?.0;
let footer_offset = size.checked_sub(512).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "file too small for VHD footer")
})?;
let mut sector = [0u8; 512];
aligned.read_exact_at(&mut sector, footer_offset)?;
Ok(VhdFooter {
cookie: u64::from_be_bytes(sector[0..8].try_into().unwrap()),
@@ -58,55 +60,65 @@ impl VhdFooter {
})
}
pub fn cookie(&self) -> u64 {
pub(super) fn cookie(&self) -> u64 {
self.cookie
}
#[cfg(test)]
pub fn features(&self) -> u32 {
self.features
}
pub fn file_format_version(&self) -> u32 {
pub(super) fn file_format_version(&self) -> u32 {
self.file_format_version
}
pub fn data_offset(&self) -> u64 {
pub(super) fn data_offset(&self) -> u64 {
self.data_offset
}
#[cfg(test)]
pub fn time_stamp(&self) -> u32 {
self.time_stamp
}
#[cfg(test)]
pub fn creator_application(&self) -> u32 {
self.creator_application
}
#[cfg(test)]
pub fn creator_version(&self) -> u32 {
self.creator_version
}
#[cfg(test)]
pub fn creator_host_os(&self) -> u32 {
self.creator_host_os
}
#[cfg(test)]
pub fn original_size(&self) -> u64 {
self.original_size
}
pub fn current_size(&self) -> u64 {
pub(super) fn current_size(&self) -> u64 {
self.current_size
}
#[cfg(test)]
pub fn disk_geometry(&self) -> u32 {
self.disk_geometry
}
pub fn disk_type(&self) -> u32 {
pub(super) fn disk_type(&self) -> u32 {
self.disk_type
}
#[cfg(test)]
pub fn checksum(&self) -> u32 {
self.checksum
}
#[cfg(test)]
pub fn unique_id(&self) -> u128 {
self.unique_id
}
#[cfg(test)]
pub fn saved_state(&self) -> u8 {
self.saved_state
}
}
/// Determine image type through file parsing.
pub fn is_fixed_vhd(f: &mut File) -> std::io::Result<bool> {
pub fn is_fixed_vhd(f: &mut File) -> io::Result<bool> {
let footer = VhdFooter::new(f)?;
// "conectix" => 0x636f6e6563746978

View File

@@ -1,28 +1,45 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0
//! Fixed VHD disk image format.
//!
//! Provides [`VhdDisk`], the `DiskFile` wrapper for fixed size VHD
//! images.
mod engine_sync;
#[cfg(feature = "io_uring")]
mod engine_uring;
mod fixed;
mod footer;
use std::fs::File;
use std::io;
use std::os::unix::io::AsRawFd;
pub use footer::is_fixed_vhd;
use log::warn;
use self::engine_sync::FixedVhdSync;
#[cfg(feature = "io_uring")]
use self::engine_uring::FixedVhdAsync;
use self::fixed::FixedVhd;
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
use crate::disk_file::DiskSize;
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::fixed_vhd::FixedVhd;
#[cfg(feature = "io_uring")]
use crate::fixed_vhd_async::FixedVhdAsync;
use crate::fixed_vhd_sync::FixedVhdSync;
use crate::{BlockBackend, Error, disk_file};
use crate::{AlignedFile, DiskTopology, Error, disk_file};
#[derive(Debug)]
pub struct FixedVhdDisk {
pub struct VhdDisk {
inner: FixedVhd,
use_io_uring: bool,
direct: bool,
}
impl FixedVhdDisk {
pub fn new(file: File, use_io_uring: bool) -> BlockResult<Self> {
impl VhdDisk {
pub fn new(file: File, use_io_uring: bool, direct: bool) -> BlockResult<Self> {
#[cfg(not(feature = "io_uring"))]
if use_io_uring {
return Err(BlockError::new(
@@ -36,11 +53,12 @@ impl FixedVhdDisk {
Ok(Self {
inner: FixedVhd::new(file).map_err(|e| BlockError::from(e).with_op(ErrorOp::Open))?,
use_io_uring,
direct,
})
}
}
impl disk_file::DiskSize for FixedVhdDisk {
impl disk_file::DiskSize for VhdDisk {
fn logical_size(&self) -> BlockResult<u64> {
self.inner
.logical_size()
@@ -48,7 +66,7 @@ impl disk_file::DiskSize for FixedVhdDisk {
}
}
impl disk_file::PhysicalSize for FixedVhdDisk {
impl disk_file::PhysicalSize for VhdDisk {
fn physical_size(&self) -> BlockResult<u64> {
self.inner.physical_size().map_err(|e| match e {
Error::GetFileMetadata(io) => {
@@ -59,17 +77,24 @@ impl disk_file::PhysicalSize for FixedVhdDisk {
}
}
impl disk_file::DiskFd for FixedVhdDisk {
impl disk_file::DiskFd for VhdDisk {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.inner.as_raw_fd())
}
}
impl disk_file::Geometry for FixedVhdDisk {}
impl disk_file::Geometry for VhdDisk {
fn topology(&self) -> DiskTopology {
DiskTopology::probe(self.inner.file()).unwrap_or_else(|_| {
warn!("Unable to get device topology. Using default topology");
DiskTopology::default()
})
}
}
impl disk_file::SparseCapable for FixedVhdDisk {}
impl disk_file::SparseCapable for VhdDisk {}
impl disk_file::Resizable for FixedVhdDisk {
impl disk_file::Resizable for VhdDisk {
fn resize(&mut self, _size: u64) -> BlockResult<()> {
Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
@@ -79,27 +104,28 @@ impl disk_file::Resizable for FixedVhdDisk {
}
}
impl disk_file::DiskFile for FixedVhdDisk {}
impl disk_file::DiskFile for VhdDisk {}
impl disk_file::AsyncDiskFile for FixedVhdDisk {
impl disk_file::AsyncDiskFile for VhdDisk {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
Ok(Box::new(FixedVhdDisk {
Ok(Box::new(VhdDisk {
inner: self.inner.clone(),
use_io_uring: self.use_io_uring,
direct: self.direct,
}))
}
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
let size = self.logical_size()?;
let file = self.inner.file().try_clone().map_err(|e| {
BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e)).with_op(ErrorOp::Open)
})?;
let raw_file = AlignedFile::new(file, self.direct);
if self.use_io_uring {
#[cfg(feature = "io_uring")]
{
return Ok(Box::new(FixedVhdAsync::new(
self.inner.as_raw_fd(),
ring_depth,
size,
)?));
return Ok(Box::new(FixedVhdAsync::new(raw_file, ring_depth, size)?));
}
#[cfg(not(feature = "io_uring"))]
@@ -107,12 +133,7 @@ impl disk_file::AsyncDiskFile for FixedVhdDisk {
}
let _ = ring_depth;
Ok(Box::new(
FixedVhdSync::new(self.inner.as_raw_fd(), size).map_err(|e| {
BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e))
.with_op(ErrorOp::Open)
})?,
))
Ok(Box::new(FixedVhdSync::new(raw_file, size)))
}
}
@@ -124,7 +145,7 @@ mod unit_tests {
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::async_io::AsyncIo;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoOperation, OwnedIoBuffer};
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize, Resizable};
/// Minimal fixed VHD footer (disk type = 2, current_size = 0x11223344).
@@ -160,7 +181,7 @@ mod unit_tests {
#[test]
fn new_sync_returns_correct_size() {
let file = make_vhd_file();
let disk = FixedVhdDisk::new(file, false).unwrap();
let disk = VhdDisk::new(file, false, false).unwrap();
assert_eq!(disk.logical_size().unwrap(), 0x1122_3344);
}
@@ -169,14 +190,14 @@ mod unit_tests {
assert_eq!(io.batch_requests_enabled(), expect_batch);
}
fn assert_async_io(disk: &FixedVhdDisk, expect_batch: bool) {
fn assert_async_io(disk: &VhdDisk, expect_batch: bool) {
assert_async_io_from_dyn(disk, expect_batch);
}
#[test]
fn sync_backend_disables_batch_requests() {
let file = make_vhd_file();
let disk = FixedVhdDisk::new(file, false).unwrap();
let disk = VhdDisk::new(file, false, false).unwrap();
assert_async_io(&disk, false);
}
@@ -184,14 +205,103 @@ mod unit_tests {
#[test]
fn io_uring_backend_enables_batch_requests() {
let file = make_vhd_file();
let disk = FixedVhdDisk::new(file, true).unwrap();
let disk = VhdDisk::new(file, true, false).unwrap();
assert_async_io(&disk, true);
}
#[test]
fn sync_rejects_read_straddling_logical_size() {
let file = TempFile::new().unwrap().into_file();
file.set_len(0x2000).unwrap();
let mut sync_io =
FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000);
let op = AsyncIoOperation::read_to_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1);
assert!(matches!(
sync_io.submit_data_operation(op),
Err(AsyncIoError::ReadVectored(_))
));
}
#[test]
fn sync_rejects_write_straddling_logical_size() {
let file = TempFile::new().unwrap().into_file();
file.set_len(0x2000).unwrap();
let mut sync_io =
FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000);
let op =
AsyncIoOperation::write_from_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1);
assert!(matches!(
sync_io.submit_data_operation(op),
Err(AsyncIoError::WriteVectored(_))
));
}
#[test]
fn sync_accepts_operation_exactly_filling_logical_size() {
let file = TempFile::new().unwrap().into_file();
file.set_len(0x2000).unwrap();
let mut sync_io =
FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000);
// end == size: boundary must be accepted
let op = AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 0x1000]), 1);
sync_io.submit_data_operation(op).unwrap();
}
#[test]
fn sync_accepts_operation_at_last_byte() {
let file = TempFile::new().unwrap().into_file();
file.set_len(0x2000).unwrap();
let mut sync_io =
FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000);
// end = 0xFFF + 1 = 0x1000 == size: boundary must be accepted
let op = AsyncIoOperation::read_to_vec(0xFFF, OwnedIoBuffer::from_vec(vec![0; 1]), 1);
sync_io.submit_data_operation(op).unwrap();
}
#[cfg(feature = "io_uring")]
#[test]
fn io_uring_batch_rejects_request_straddling_logical_size() {
let file = TempFile::new().unwrap().into_file();
file.set_len(0x2000).unwrap();
let mut async_io = FixedVhdAsync::new(
AlignedFile::new(file.try_clone().unwrap(), false),
8,
0x1000,
)
.unwrap();
let op = AsyncIoOperation::read_to_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1);
assert!(matches!(
async_io.submit_batch_requests(vec![op]),
Err(AsyncIoError::ReadVectored(_))
));
}
#[cfg(feature = "io_uring")]
#[test]
fn io_uring_rejects_single_op_straddling_logical_size() {
let file = TempFile::new().unwrap().into_file();
file.set_len(0x2000).unwrap();
let mut async_io = FixedVhdAsync::new(
AlignedFile::new(file.try_clone().unwrap(), false),
8,
0x1000,
)
.unwrap();
let op = AsyncIoOperation::read_to_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1);
assert!(matches!(
async_io.submit_data_operation(op),
Err(AsyncIoError::ReadVectored(_))
));
}
#[test]
fn try_clone_preserves_sync_dispatch() {
let file = make_vhd_file();
let disk = FixedVhdDisk::new(file, false).unwrap();
let disk = VhdDisk::new(file, false, false).unwrap();
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), false);
}
@@ -200,7 +310,7 @@ mod unit_tests {
#[test]
fn try_clone_preserves_io_uring_dispatch() {
let file = make_vhd_file();
let disk = FixedVhdDisk::new(file, true).unwrap();
let disk = VhdDisk::new(file, true, false).unwrap();
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), true);
}
@@ -208,14 +318,14 @@ mod unit_tests {
#[test]
fn resize_returns_error() {
let file = make_vhd_file();
let mut disk = FixedVhdDisk::new(file, false).unwrap();
let mut disk = VhdDisk::new(file, false, false).unwrap();
assert!(disk.resize(0x2000_0000).is_err());
}
#[test]
fn physical_size_includes_footer() {
let file = make_vhd_file();
let disk = FixedVhdDisk::new(file, false).unwrap();
let disk = VhdDisk::new(file, false, false).unwrap();
// Data region (0x1122_3344) + VHD footer (0x200).
assert_eq!(disk.physical_size().unwrap(), 0x1122_3344 + 0x200);
}

View File

@@ -2,29 +2,29 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Seek, SeekFrom};
use std::mem::size_of;
use std::os::unix::fs::FileExt;
use std::{io, result};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use byteorder::{ByteOrder, LittleEndian};
use remain::sorted;
use thiserror::Error;
use crate::vhdx::vhdx_header::RegionTableEntry;
use crate::vhdx::vhdx_metadata::DiskSpec;
use super::header::RegionTableEntry;
use super::metadata::DiskSpec;
use crate::aligned_file::AlignedFile;
// Payload BAT Entry States
pub const PAYLOAD_BLOCK_NOT_PRESENT: u64 = 0;
pub const PAYLOAD_BLOCK_UNDEFINED: u64 = 1;
pub const PAYLOAD_BLOCK_ZERO: u64 = 2;
pub const PAYLOAD_BLOCK_UNMAPPED: u64 = 3;
pub const PAYLOAD_BLOCK_FULLY_PRESENT: u64 = 6;
pub const PAYLOAD_BLOCK_PARTIALLY_PRESENT: u64 = 7;
pub(super) const PAYLOAD_BLOCK_NOT_PRESENT: u64 = 0;
pub(super) const PAYLOAD_BLOCK_UNDEFINED: u64 = 1;
pub(super) const PAYLOAD_BLOCK_ZERO: u64 = 2;
pub(super) const PAYLOAD_BLOCK_UNMAPPED: u64 = 3;
pub(super) const PAYLOAD_BLOCK_FULLY_PRESENT: u64 = 6;
pub(super) const PAYLOAD_BLOCK_PARTIALLY_PRESENT: u64 = 7;
// Mask for the BAT state
pub const BAT_STATE_BIT_MASK: u64 = 0x07;
pub(super) const BAT_STATE_BIT_MASK: u64 = 0x07;
// Mask for the offset within the file in units of 1 MB
pub const BAT_FILE_OFF_MASK: u64 = 0xFFFFFFFFFFF00000;
pub(super) const BAT_FILE_OFF_MASK: u64 = 0xFFFFFFFFFFF00000;
#[sorted]
#[derive(Error, Debug)]
@@ -39,15 +39,15 @@ pub enum VhdxBatError {
WriteBat(#[source] io::Error),
}
pub type Result<T> = std::result::Result<T, VhdxBatError>;
pub(super) type Result<T> = result::Result<T, VhdxBatError>;
#[derive(Default, Clone, Debug)]
pub struct BatEntry(pub u64);
pub(super) struct BatEntry(pub u64);
impl BatEntry {
// Read all BAT entries presented on the disk and insert them to a vector
pub fn collect_bat_entries(
f: &mut File,
pub(super) fn collect_bat_entries(
f: &AlignedFile,
disk_spec: &DiskSpec,
bat_entry: &RegionTableEntry,
) -> Result<Vec<BatEntry>> {
@@ -63,14 +63,10 @@ impl BatEntry {
let mut bat: Vec<BatEntry> = Vec::with_capacity(bat_entry.length as usize);
let offset = bat_entry.file_offset;
for i in 0..entry_count {
f.seek(SeekFrom::Start(offset + i * size_of::<u64>() as u64))
let mut entry = [0u8; size_of::<u64>()];
f.read_exact_at(&mut entry, offset + i * size_of::<u64>() as u64)
.map_err(VhdxBatError::ReadBat)?;
let bat_entry = BatEntry(
f.read_u64::<LittleEndian>()
.map_err(VhdxBatError::ReadBat)?,
);
bat.insert(i as usize, bat_entry);
bat.insert(i as usize, BatEntry(LittleEndian::read_u64(&entry)));
}
Ok(bat)
@@ -83,14 +79,12 @@ impl BatEntry {
}
// Routine for writing BAT entries to the disk
pub fn write_bat_entries(
f: &mut File,
pub(super) fn write_bat_entries(
f: &AlignedFile,
bat_offset: u64,
bat_entries: &[BatEntry],
) -> Result<()> {
for i in 0..bat_entries.len() as u64 {
f.seek(SeekFrom::Start(bat_offset + i * size_of::<u64>() as u64))
.map_err(VhdxBatError::WriteBat)?;
let bat_entry = match bat_entries.get(i as usize) {
Some(entry) => entry.0,
None => {
@@ -98,7 +92,9 @@ impl BatEntry {
}
};
f.write_u64::<LittleEndian>(bat_entry)
let mut buf = [0u8; size_of::<u64>()];
LittleEndian::write_u64(&mut buf, bat_entry);
f.write_all_at(&buf, bat_offset + i * size_of::<u64>() as u64)
.map_err(VhdxBatError::WriteBat)?;
}
Ok(())

View File

@@ -0,0 +1,208 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0
use std::collections::VecDeque;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::sync::{Arc, Mutex};
use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult};
use crate::formats::vhdx::Vhdx;
pub(super) struct VhdxSync {
vhdx_file: Arc<Mutex<Vhdx>>,
eventfd: EventFd,
completion_list: VecDeque<AsyncIoCompletion>,
size: u64,
}
impl VhdxSync {
pub(super) fn new(vhdx_file: Arc<Mutex<Vhdx>>, size: u64) -> Self {
VhdxSync {
vhdx_file,
eventfd: EventFd::new(libc::EFD_NONBLOCK)
.expect("Failed creating EventFd for VhdxSync"),
completion_list: VecDeque::new(),
size,
}
}
fn read_operation(&mut self, op: &mut AsyncIoOperation) -> AsyncIoResult<usize> {
let offset = op.offset();
let mut buf = vec![0u8; op.total_len()];
let mut vhdx = self.vhdx_file.lock().unwrap();
vhdx.seek(SeekFrom::Start(offset as u64))
.map_err(AsyncIoError::ReadVectored)?;
let result = vhdx.read(&mut buf).map_err(AsyncIoError::ReadVectored)?;
drop(vhdx);
op.write_bytes_at(0, &buf[..result])
.map_err(AsyncIoError::ReadVectored)?;
Ok(result)
}
fn write_operation(&mut self, op: &AsyncIoOperation) -> AsyncIoResult<usize> {
let offset = op.offset();
let mut buf = vec![0u8; op.total_len()];
op.read_bytes_at(0, &mut buf)
.map_err(AsyncIoError::WriteVectored)?;
let mut vhdx = self.vhdx_file.lock().unwrap();
vhdx.seek(SeekFrom::Start(offset as u64))
.map_err(AsyncIoError::WriteVectored)?;
let result = vhdx.write(&buf).map_err(AsyncIoError::WriteVectored)?;
Ok(result)
}
}
impl AsyncIo for VhdxSync {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> {
op.validate_bounds(self.size)?;
let is_read = op.is_read();
let mut op = op;
let result = if is_read {
self.read_operation(&mut op)?
} else {
self.write_operation(&op)?
};
self.completion_list
.push_back(AsyncIoCompletion::from_operation(op, result as i32));
self.eventfd.write(1).unwrap();
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.vhdx_file
.lock()
.unwrap()
.flush()
.map_err(AsyncIoError::Fsync)?;
if let Some(user_data) = user_data {
self.completion_list
.push_back(AsyncIoCompletion::new(user_data, 0, None));
self.eventfd.write(1).unwrap();
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.completion_list.pop_front()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::PunchHole(io::Error::other(
"punch_hole not supported for VHDX",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(io::Error::other(
"write_zeroes not supported for VHDX",
)))
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::sync::{Arc, Mutex};
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoOperation, OwnedIoBuffer};
use crate::formats::vhdx::Vhdx;
use crate::formats::vhdx::test_util::create_dynamic_vhdx;
fn make_vhdx_sync(tf: &TempFile) -> (VhdxSync, u64) {
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let vhdx = Vhdx::new(file, false).unwrap();
let size = vhdx.virtual_disk_size();
let sync = VhdxSync::new(Arc::new(Mutex::new(vhdx)), size);
(sync, size)
}
/// Builds a `VhdxSync` from a fresh 1 MiB dynamic VHDX, or `None`
/// if `qemu-img` is unavailable to generate one.
fn setup() -> Option<(VhdxSync, u64)> {
let tf = create_dynamic_vhdx(1)?;
Some(make_vhdx_sync(&tf))
}
#[test]
fn sync_rejects_read_straddling_logical_size() {
let Some((mut sync, size)) = setup() else {
eprintln!("skipping: qemu-img unavailable");
return;
};
let op = AsyncIoOperation::read_to_vec(
(size - 512) as i64,
OwnedIoBuffer::from_vec(vec![0u8; 1024]),
1,
);
assert!(matches!(
sync.submit_data_operation(op),
Err(AsyncIoError::ReadVectored(_))
));
}
#[test]
fn sync_rejects_write_straddling_logical_size() {
let Some((mut sync, size)) = setup() else {
eprintln!("skipping: qemu-img unavailable");
return;
};
let op = AsyncIoOperation::write_from_vec(
(size - 512) as i64,
OwnedIoBuffer::from_vec(vec![0u8; 1024]),
1,
);
assert!(matches!(
sync.submit_data_operation(op),
Err(AsyncIoError::WriteVectored(_))
));
}
#[test]
fn sync_accepts_operation_exactly_filling_logical_size() {
let Some((mut sync, size)) = setup() else {
eprintln!("skipping: qemu-img unavailable");
return;
};
let op =
AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0u8; size as usize]), 1);
sync.submit_data_operation(op).unwrap();
}
#[test]
fn sync_accepts_operation_at_last_sector() {
let Some((mut sync, size)) = setup() else {
eprintln!("skipping: qemu-img unavailable");
return;
};
// VHDX operates in 512-byte sectors; read exactly the last sector.
let op = AsyncIoOperation::read_to_vec(
(size - 512) as i64,
OwnedIoBuffer::from_vec(vec![0u8; 512]),
1,
);
sync.submit_data_operation(op).unwrap();
}
}

View File

@@ -3,14 +3,16 @@
// SPDX-License-Identifier: Apache-2.0
use std::collections::btree_map::BTreeMap;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::mem::size_of;
use std::os::unix::fs::FileExt;
use std::{io, result};
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt};
use byteorder::{ByteOrder, LittleEndian};
use remain::sorted;
use thiserror::Error;
use uuid::Uuid;
use zerocopy::{FromBytes, Immutable, IntoBytes};
use crate::aligned_file::AlignedFile;
const VHDX_SIGN: u64 = 0x656C_6966_7864_6876; // "vhdxfile"
const HEADER_SIGN: u32 = 0x6461_6568; // "head"
@@ -19,7 +21,7 @@ const REGION_SIGN: u32 = 0x6967_6572; // "regi"
const FILE_START: u64 = 0; // The first element
const HEADER_1_START: u64 = 64 * 1024; // Header 1 start in Bytes
const HEADER_2_START: u64 = 128 * 1024; // Header 2 start in Bytes
pub const REGION_TABLE_1_START: u64 = 192 * 1024; // Region 1 start in Bytes
pub(super) const REGION_TABLE_1_START: u64 = 192 * 1024; // Region 1 start in Bytes
const REGION_TABLE_2_START: u64 = 256 * 1024; // Region 2 start in Bytes
const HEADER_SIZE: u64 = 4 * 1024; // Each header is 64 KiB, but only first 4 kiB contains info
@@ -27,8 +29,13 @@ const REGION_SIZE: u64 = 64 * 1024; // Each region size is 64 KiB
const REGION_ENTRY_REQUIRED: u32 = 1;
const BAT_GUID: &str = "2DC27766-F623-4200-9D64-115E9BFD4A08"; // BAT GUID
const MDR_GUID: &str = "8B7CA206-4790-4B9A-B8FE-575F050F886E"; // Metadata GUID
// VHDX stores GUIDs using little-endian GUID byte order.
const BAT_GUID: [u8; 16] = [
0x66, 0x77, 0xc2, 0x2d, 0x23, 0xf6, 0x00, 0x42, 0x9d, 0x64, 0x11, 0x5e, 0x9b, 0xfd, 0x4a, 0x08,
];
const MDR_GUID: [u8; 16] = [
0x06, 0xa2, 0x7c, 0x8b, 0x90, 0x47, 0x9a, 0x4b, 0xb8, 0xfe, 0x57, 0x5f, 0x05, 0x0f, 0x88, 0x6e,
];
#[sorted]
#[derive(Error, Debug)]
@@ -39,7 +46,7 @@ pub enum VhdxHeaderError {
DuplicateBATEntry,
#[error("Metadata region entry is not unique")]
DuplicateMDREntry,
#[error("Checksum doesn't match for")]
#[error("Checksum doesn't match for {0}")]
InvalidChecksum(String),
#[error("Invalid entry count")]
InvalidEntryCount,
@@ -47,8 +54,6 @@ pub enum VhdxHeaderError {
InvalidHeaderSign,
#[error("Not a valid VHDx region")]
InvalidRegionSign,
#[error("Couldn't parse Uuid for region entry {0}")]
InvalidUuid(#[source] uuid::Error),
#[error("Not a VHDx file")]
InvalidVHDXSign,
#[error("No valid header found")]
@@ -60,46 +65,39 @@ pub enum VhdxHeaderError {
#[error("Failed to read headers {0}")]
ReadHeader(#[source] io::Error),
#[error("Failed to read metadata {0}")]
ReadMetadata(#[source] std::io::Error),
ReadMetadata(#[source] io::Error),
#[error("Failed to read region table entries {0}")]
ReadRegionTableEntries(#[source] io::Error),
#[error("Failed to read region table header {0}")]
ReadRegionTableHeader(#[source] io::Error),
#[error("Failed to read region entries")]
RegionEntryCollectionFailed,
#[error("Region entry file offset ({0}) and length ({1}) overflow u64")]
RegionEntryOverflow(u64 /* start */, usize /* length */),
#[error("Overlapping regions found")]
RegionOverlap,
#[error("Reserved region has non-zero value")]
ReservedIsNonZero,
#[error("Failed to seek in File Type Identifier {0}")]
SeekFileTypeIdentifier(#[source] io::Error),
#[error("Failed to seek in headers {0}")]
SeekHeader(#[source] io::Error),
#[error("Failed to seek in region table entries {0}")]
SeekRegionTableEntries(#[source] io::Error),
#[error("Failed to seek in region table header {0}")]
SeekRegionTableHeader(#[source] io::Error),
#[error("We do not recognize this entry")]
UnrecognizedRegionEntry,
#[error("Failed to write header {0}")]
WriteHeader(#[source] io::Error),
}
pub type Result<T> = std::result::Result<T, VhdxHeaderError>;
pub(super) type Result<T> = result::Result<T, VhdxHeaderError>;
#[derive(Clone, Debug)]
pub struct FileTypeIdentifier {
pub(super) struct FileTypeIdentifier {
pub _signature: u64,
}
impl FileTypeIdentifier {
/// Reads the File Type Identifier structure from a reference VHDx file
pub fn new(f: &mut File) -> Result<FileTypeIdentifier> {
f.seek(SeekFrom::Start(FILE_START))
.map_err(VhdxHeaderError::SeekFileTypeIdentifier)?;
let _signature = f
.read_u64::<LittleEndian>()
pub(super) fn new(f: &AlignedFile) -> Result<FileTypeIdentifier> {
let mut buf = [0u8; size_of::<u64>()];
f.read_exact_at(&mut buf, FILE_START)
.map_err(VhdxHeaderError::ReadFileTypeIdentifier)?;
let _signature = LittleEndian::read_u64(&buf);
if _signature != VHDX_SIGN {
return Err(VhdxHeaderError::InvalidVHDXSign);
}
@@ -109,8 +107,8 @@ impl FileTypeIdentifier {
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct Header {
#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)]
pub(super) struct Header {
pub signature: u32,
pub checksum: u32,
pub sequence_number: u64,
@@ -125,17 +123,14 @@ pub struct Header {
impl Header {
/// Reads the Header structure from a reference VHDx file
pub fn new(f: &mut File, start: u64) -> Result<Header> {
pub(super) fn new(f: &AlignedFile, start: u64) -> Result<Header> {
// Read the whole header into a buffer. We will need it for
// calculating checksum.
let mut buffer = [0; HEADER_SIZE as usize];
f.seek(SeekFrom::Start(start))
.map_err(VhdxHeaderError::SeekHeader)?;
f.read_exact(&mut buffer)
f.read_exact_at(&mut buffer, start)
.map_err(VhdxHeaderError::ReadHeader)?;
// SAFETY: buffer is of correct size and has been successfully filled.
let header: Header = unsafe { *(buffer.as_ptr().cast()) };
let header = Header::read_from_prefix(&buffer).unwrap().0;
if header.signature != HEADER_SIGN {
return Err(VhdxHeaderError::InvalidHeaderSign);
}
@@ -148,32 +143,26 @@ impl Header {
Ok(header)
}
/// Converts the header structure into a buffer
fn write_to_buffer(&self, buffer: &mut [u8; HEADER_SIZE as usize]) {
// SAFETY: self is a valid header.
let reference =
unsafe { std::slice::from_raw_parts((&raw const *self).cast(), HEADER_SIZE as usize) };
*buffer = reference.try_into().unwrap();
}
/// Creates and returns new updated header from the provided current header
fn update_header(
f: &mut File,
f: &AlignedFile,
current_header: &Header,
change_data_guid: bool,
mut file_write_guid: u128,
file_write_guid: u128,
start: u64,
) -> Result<Header> {
let mut buffer = [0u8; HEADER_SIZE as usize];
let mut data_write_guid = current_header.data_write_guid;
let data_write_guid = if change_data_guid {
Uuid::new_v4().as_u128()
} else {
current_header.data_write_guid
};
if change_data_guid {
data_write_guid = Uuid::new_v4().as_u128();
}
if file_write_guid == 0 {
file_write_guid = current_header.file_write_guid;
}
let file_write_guid = if file_write_guid == 0 {
current_header.file_write_guid
} else {
file_write_guid
};
let mut new_header = Header {
signature: current_header.signature,
@@ -188,20 +177,19 @@ impl Header {
log_offset: current_header.log_offset,
};
new_header.write_to_buffer(&mut buffer);
new_header.write_to_prefix(&mut buffer).unwrap();
new_header.checksum = calculate_checksum(&mut buffer, size_of::<u32>());
new_header.write_to_buffer(&mut buffer);
new_header.write_to_prefix(&mut buffer).unwrap();
f.seek(SeekFrom::Start(start))
.map_err(VhdxHeaderError::SeekHeader)?;
f.write(&buffer).map_err(VhdxHeaderError::WriteHeader)?;
f.write_all_at(&buffer, start)
.map_err(VhdxHeaderError::WriteHeader)?;
Ok(new_header)
}
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, FromBytes)]
struct RegionTableHeader {
pub signature: u32,
pub checksum: u32,
@@ -211,17 +199,14 @@ struct RegionTableHeader {
impl RegionTableHeader {
/// Reads the Region Table Header structure from a reference VHDx file
pub fn new(f: &mut File, start: u64) -> Result<RegionTableHeader> {
pub(crate) fn new(f: &AlignedFile, start: u64) -> Result<RegionTableHeader> {
// Read the whole header into a buffer. We will need it for calculating
// checksum.
let mut buffer = [0u8; REGION_SIZE as usize];
f.seek(SeekFrom::Start(start))
.map_err(VhdxHeaderError::SeekRegionTableHeader)?;
f.read_exact(&mut buffer)
f.read_exact_at(&mut buffer, start)
.map_err(VhdxHeaderError::ReadRegionTableHeader)?;
// SAFETY: buffer is of correct size and has been successfully filled.
let region_table_header: RegionTableHeader = unsafe { *(buffer.as_ptr().cast()) };
let region_table_header = RegionTableHeader::read_from_prefix(&buffer).unwrap().0;
if region_table_header.signature != REGION_SIGN {
return Err(VhdxHeaderError::InvalidRegionSign);
}
@@ -243,7 +228,13 @@ impl RegionTableHeader {
}
}
pub struct RegionInfo {
/// Returns `true` if the half-open byte ranges `[a_start, a_end)` and
/// `[b_start, b_end)` overlap.
fn ranges_overlap(a_start: u64, a_end: u64, b_start: u64, b_end: u64) -> bool {
a_start < b_end && b_start < a_end
}
pub(super) struct RegionInfo {
pub bat_entry: RegionTableEntry,
pub mdr_entry: RegionTableEntry,
pub region_entries: BTreeMap<u64, u64>,
@@ -252,7 +243,7 @@ pub struct RegionInfo {
impl RegionInfo {
/// Collect all entries in a BTreeMap from the Region Table and identifies
/// BAT and metadata regions
pub fn new(f: &mut File, region_start: u64, entry_count: u32) -> Result<RegionInfo> {
pub(super) fn new(f: &AlignedFile, region_start: u64, entry_count: u32) -> Result<RegionInfo> {
let mut bat_entry: Option<RegionTableEntry> = None;
let mut mdr_entry: Option<RegionTableEntry> = None;
@@ -260,31 +251,34 @@ impl RegionInfo {
let mut region_entries = BTreeMap::new();
let mut buffer = [0; REGION_SIZE as usize];
// Seek after the Region Table Header
f.seek(SeekFrom::Start(
// Read after the Region Table Header
f.read_exact_at(
&mut buffer,
region_start + size_of::<RegionTableHeader>() as u64,
))
.map_err(VhdxHeaderError::SeekRegionTableEntries)?;
f.read_exact(&mut buffer)
.map_err(VhdxHeaderError::ReadRegionTableEntries)?;
)
.map_err(VhdxHeaderError::ReadRegionTableEntries)?;
for _ in 0..entry_count {
let entry =
RegionTableEntry::new(&buffer[offset..offset + size_of::<RegionTableEntry>()])?;
let entry = RegionTableEntry::read_from_bytes(
&buffer[offset..offset + size_of::<RegionTableEntry>()],
)
.unwrap();
offset += size_of::<RegionTableEntry>();
let start = entry.file_offset;
let end = start + entry.length as u64;
let end = start.checked_add(entry.length as u64).ok_or(
VhdxHeaderError::RegionEntryOverflow(start, entry.length as usize),
)?;
for (region_ent_start, region_ent_end) in region_entries.iter() {
if !((start >= *region_ent_start) || (end <= *region_ent_end)) {
if ranges_overlap(start, end, *region_ent_start, *region_ent_end) {
return Err(VhdxHeaderError::RegionOverlap);
}
}
region_entries.insert(entry.file_offset, entry.file_offset + entry.length as u64);
region_entries.insert(start, end);
if entry.guid == Uuid::parse_str(BAT_GUID).map_err(VhdxHeaderError::InvalidUuid)? {
if entry.guid == BAT_GUID {
if bat_entry.is_none() {
bat_entry = Some(entry);
continue;
@@ -292,7 +286,7 @@ impl RegionInfo {
return Err(VhdxHeaderError::DuplicateBATEntry);
}
if entry.guid == Uuid::parse_str(MDR_GUID).map_err(VhdxHeaderError::InvalidUuid)? {
if entry.guid == MDR_GUID {
if mdr_entry.is_none() {
mdr_entry = Some(entry);
continue;
@@ -326,28 +320,14 @@ impl RegionInfo {
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct RegionTableEntry {
pub guid: Uuid,
#[derive(Clone, Copy, Debug, FromBytes)]
pub(super) struct RegionTableEntry {
guid: [u8; 16],
pub file_offset: u64,
pub length: u32,
pub required: u32,
}
impl RegionTableEntry {
/// Reads one Region Entry from a Region Table index that starts from 0
pub fn new(buffer: &[u8]) -> Result<RegionTableEntry> {
assert!(buffer.len() == std::mem::size_of::<RegionTableEntry>());
// SAFETY: the assertion above makes sure the buffer size is correct.
let mut region_table_entry: RegionTableEntry = unsafe { *(buffer.as_ptr().cast()) };
let uuid = crate::vhdx::uuid_from_guid(buffer);
region_table_entry.guid = uuid;
Ok(region_table_entry)
}
}
enum HeaderNo {
First,
Second,
@@ -355,7 +335,7 @@ enum HeaderNo {
/// Contains the information from the header of a VHDx file
#[derive(Clone, Debug)]
pub struct VhdxHeader {
pub(super) struct VhdxHeader {
_file_type_identifier: FileTypeIdentifier,
header_1: Header,
header_2: Header,
@@ -365,7 +345,7 @@ pub struct VhdxHeader {
impl VhdxHeader {
/// Creates a VhdxHeader from a reference to a file
pub fn new(f: &mut File) -> Result<VhdxHeader> {
pub(super) fn new(f: &AlignedFile) -> Result<VhdxHeader> {
Ok(VhdxHeader {
_file_type_identifier: FileTypeIdentifier::new(f)?,
header_1: Header::new(f, HEADER_1_START)?,
@@ -402,7 +382,7 @@ impl VhdxHeader {
/// current one. Returns both headers as a tuple sequenced the way it was
/// received from the parameter list.
fn update_header(
f: &mut File,
f: &AlignedFile,
header_1: Result<Header>,
header_2: Result<Header>,
guid: u128,
@@ -425,7 +405,7 @@ impl VhdxHeader {
// Update the provided headers according to the spec
fn update_headers(
f: &mut File,
f: &AlignedFile,
header_1: Result<Header>,
header_2: Result<Header>,
guid: u128,
@@ -435,14 +415,14 @@ impl VhdxHeader {
VhdxHeader::update_header(f, Ok(header_1), Ok(header_2), guid)
}
pub fn update(&mut self, f: &mut File) -> Result<()> {
pub(super) fn update(&mut self, f: &AlignedFile) -> Result<()> {
let headers = VhdxHeader::update_headers(f, Ok(self.header_1), Ok(self.header_2), 0)?;
self.header_1 = headers.0;
self.header_2 = headers.1;
Ok(())
}
pub fn region_entry_count(&self) -> u32 {
pub(super) fn region_entry_count(&self) -> u32 {
self.region_table_1.entry_count
}
}
@@ -466,3 +446,144 @@ fn calculate_checksum(buffer: &mut [u8], csum_offset: usize) -> u32 {
new_csum
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::FileExt;
use vmm_sys_util::tempfile::TempFile;
use zerocopy::{FromBytes, IntoBytes};
use super::{
BAT_GUID, HEADER_SIGN, Header, MDR_GUID, REGION_TABLE_1_START, RegionInfo,
RegionTableHeader, VhdxHeaderError, ranges_overlap,
};
use crate::aligned_file::AlignedFile;
#[test]
fn test_header_bytes_round_trip() {
let header = Header {
signature: HEADER_SIGN,
checksum: 0x1122_3344,
sequence_number: 0x0102_0304_0506_0708,
file_write_guid: 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100,
data_write_guid: 0x1f1e_1d1c_1b1a_1918_1716_1514_1312_1110,
log_guid: 0x2f2e_2d2c_2b2a_2928_2726_2524_2322_2120,
log_version: 0xabcd,
version: 0x0001,
log_length: 0x0010_0000,
log_offset: 0x0000_0100_0000_0000,
};
let bytes = header.as_bytes();
assert_eq!(&bytes[0..4], &header.signature.to_le_bytes()[..]);
assert_eq!(&bytes[8..16], &header.sequence_number.to_le_bytes()[..]);
assert_eq!(&bytes[16..32], &header.file_write_guid.to_le_bytes()[..]);
assert_eq!(&bytes[64..66], &header.log_version.to_le_bytes()[..]);
assert_eq!(&bytes[72..80], &header.log_offset.to_le_bytes()[..]);
let parsed = Header::read_from_bytes(bytes).unwrap();
assert_eq!({ parsed.signature }, { header.signature });
assert_eq!({ parsed.checksum }, { header.checksum });
assert_eq!({ parsed.sequence_number }, { header.sequence_number });
assert_eq!({ parsed.file_write_guid }, { header.file_write_guid });
assert_eq!({ parsed.data_write_guid }, { header.data_write_guid });
assert_eq!({ parsed.log_guid }, { header.log_guid });
assert_eq!({ parsed.log_version }, { header.log_version });
assert_eq!({ parsed.version }, { header.version });
assert_eq!({ parsed.log_length }, { header.log_length });
assert_eq!({ parsed.log_offset }, { header.log_offset });
}
#[test]
fn test_ranges_overlap() {
// (new [start,end), existing [s,e), expected overlap)
let cases: &[(u64, u64, u64, u64, bool)] = &[
// Genuine overlaps — all of these must be detected.
(0, 10, 0, 10, true), // identical
(2, 8, 0, 10, true), // new fully inside existing
(0, 20, 5, 10, true), // new fully contains existing
(5, 15, 0, 10, true), // partial, new starts inside existing
(0, 8, 5, 15, true), // partial, new starts before existing
// Non-overlapping — must not be flagged.
(0, 5, 10, 20, false), // disjoint, new before existing
(30, 40, 10, 20, false), // disjoint, new after existing
(0, 10, 10, 20, false), // touching at the boundary (half-open)
];
for &(a_start, a_end, b_start, b_end, expected) in cases {
assert_eq!(
ranges_overlap(a_start, a_end, b_start, b_end),
expected,
"[{a_start},{a_end}) vs [{b_start},{b_end})"
);
// Overlap is symmetric.
assert_eq!(
ranges_overlap(b_start, b_end, a_start, a_end),
expected,
"symmetry: [{b_start},{b_end}) vs [{a_start},{a_end})"
);
}
}
/// Builds the 32-byte on-disk region table entry for `guid` describing
/// the region `[file_offset, file_offset + length)`.
fn region_entry(guid: [u8; 16], file_offset: u64, length: u32) -> [u8; 32] {
let mut e = [0u8; 32];
e[0..16].copy_from_slice(&guid);
e[16..24].copy_from_slice(&file_offset.to_le_bytes());
e[24..28].copy_from_slice(&length.to_le_bytes());
// `required` (e[28..32]) left zero.
e
}
#[test]
fn test_region_info_rejects_overlapping_regions() {
// BAT region [1 MiB, 3 MiB) and metadata region [2 MiB, 4 MiB) overlap
// on [2 MiB, 3 MiB); per [MS-VHDX] all region objects must be
// non-overlapping, so this image must be rejected.
const MIB: u64 = 1024 * 1024;
let region_start = REGION_TABLE_1_START;
let entries_at = region_start + size_of::<RegionTableHeader>() as u64;
let temp = TempFile::new().unwrap();
let f = temp.into_file();
f.set_len(entries_at + 64 * 1024).unwrap();
f.write_all_at(&region_entry(BAT_GUID, MIB, (2 * MIB) as u32), entries_at)
.unwrap();
f.write_all_at(
&region_entry(MDR_GUID, 2 * MIB, (2 * MIB) as u32),
entries_at + 32,
)
.unwrap();
let af = AlignedFile::new(f, false);
let res = RegionInfo::new(&af, region_start, 2);
assert!(
matches!(res, Err(VhdxHeaderError::RegionOverlap)),
"expected RegionOverlap for an overlapping region table"
);
}
#[test]
fn test_region_info_rejects_overflowing_region() {
// A region whose file offset plus length wraps past u64::MAX must be
// rejected rather than silently producing a small end offset that
// could mask a genuine overlap.
let region_start = REGION_TABLE_1_START;
let entries_at = region_start + size_of::<RegionTableHeader>() as u64;
let temp = TempFile::new().unwrap();
let f = temp.into_file();
f.set_len(entries_at + 64 * 1024).unwrap();
f.write_all_at(&region_entry(BAT_GUID, u64::MAX, 0x1000), entries_at)
.unwrap();
let af = AlignedFile::new(f, false);
let res = RegionInfo::new(&af, region_start, 1);
assert!(
matches!(res, Err(VhdxHeaderError::RegionEntryOverflow(..))),
"expected RegionEntryOverflow for a wrapping region entry"
);
}
}

View File

@@ -2,14 +2,15 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::unix::fs::FileExt;
use std::{io, result};
use remain::sorted;
use thiserror::Error;
use crate::vhdx::vhdx_bat::{self, BatEntry, VhdxBatError};
use crate::vhdx::vhdx_metadata::{self, DiskSpec};
use super::bat::{self, BatEntry, VhdxBatError};
use super::metadata::{self, DiskSpec};
use crate::aligned_file::AlignedFile;
const SECTOR_SIZE: u64 = 512;
@@ -32,7 +33,7 @@ pub enum VhdxIoError {
WriteBat(#[source] VhdxBatError),
}
pub type Result<T> = std::result::Result<T, VhdxIoError>;
pub(super) type Result<T> = result::Result<T, VhdxIoError>;
macro_rules! align {
($n:expr, $align:expr) => {{ $n.div_ceil($align) * $align }};
@@ -50,7 +51,7 @@ struct Sector {
impl Sector {
/// Translate sector index and count of data in file to actual offsets and
/// BAT index.
pub fn new(
pub(crate) fn new(
disk_spec: &DiskSpec,
bat: &[BatEntry],
sector_index: u64,
@@ -74,7 +75,7 @@ impl Sector {
return Err(VhdxIoError::InvalidBatIndex);
}
};
sector.file_offset = bat_entry & vhdx_bat::BAT_FILE_OFF_MASK;
sector.file_offset = bat_entry & bat::BAT_FILE_OFF_MASK;
if sector.file_offset != 0 {
sector.file_offset += sector.block_offset;
}
@@ -85,8 +86,8 @@ impl Sector {
/// VHDx IO read routine: requires relative sector index and count for the
/// requested data.
pub fn read(
f: &mut File,
pub(super) fn read(
f: &AlignedFile,
buf: &mut [u8],
disk_spec: &DiskSpec,
bat: &[BatEntry],
@@ -108,21 +109,20 @@ pub fn read(
}
};
match bat_entry & vhdx_bat::BAT_STATE_BIT_MASK {
vhdx_bat::PAYLOAD_BLOCK_NOT_PRESENT
| vhdx_bat::PAYLOAD_BLOCK_UNDEFINED
| vhdx_bat::PAYLOAD_BLOCK_UNMAPPED
| vhdx_bat::PAYLOAD_BLOCK_ZERO => {}
vhdx_bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
f.seek(SeekFrom::Start(sector.file_offset))
.map_err(VhdxIoError::ReadSectorBlock)?;
f.read_exact(
match bat_entry & bat::BAT_STATE_BIT_MASK {
bat::PAYLOAD_BLOCK_NOT_PRESENT
| bat::PAYLOAD_BLOCK_UNDEFINED
| bat::PAYLOAD_BLOCK_UNMAPPED
| bat::PAYLOAD_BLOCK_ZERO => {}
bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
f.read_exact_at(
&mut buf
[read_count..(read_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
sector.file_offset,
)
.map_err(VhdxIoError::ReadSectorBlock)?;
}
vhdx_bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => {
bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => {
return Err(VhdxIoError::UnsupportedMode);
}
_ => {
@@ -138,8 +138,8 @@ pub fn read(
/// VHDx IO write routine: requires relative sector index and count for the
/// requested data.
pub fn write(
f: &mut File,
pub(super) fn write(
f: &AlignedFile,
buf: &[u8],
disk_spec: &mut DiskSpec,
bat_offset: u64,
@@ -162,49 +162,48 @@ pub fn write(
}
};
match bat_entry & vhdx_bat::BAT_STATE_BIT_MASK {
vhdx_bat::PAYLOAD_BLOCK_NOT_PRESENT
| vhdx_bat::PAYLOAD_BLOCK_UNDEFINED
| vhdx_bat::PAYLOAD_BLOCK_UNMAPPED
| vhdx_bat::PAYLOAD_BLOCK_ZERO => {
let file_offset =
align!(disk_spec.image_size, vhdx_metadata::BLOCK_SIZE_MIN as u64);
match bat_entry & bat::BAT_STATE_BIT_MASK {
bat::PAYLOAD_BLOCK_NOT_PRESENT
| bat::PAYLOAD_BLOCK_UNDEFINED
| bat::PAYLOAD_BLOCK_UNMAPPED
| bat::PAYLOAD_BLOCK_ZERO => {
let file_offset = align!(disk_spec.image_size, metadata::BLOCK_SIZE_MIN as u64);
let new_size = file_offset
.checked_add(disk_spec.block_size as u64)
.ok_or(VhdxIoError::InvalidDiskSize)?;
f.set_len(new_size).map_err(VhdxIoError::ResizeFile)?;
f.file()
.set_len(new_size)
.map_err(VhdxIoError::ResizeFile)?;
disk_spec.image_size = new_size;
let new_bat_entry = file_offset
| (vhdx_bat::PAYLOAD_BLOCK_FULLY_PRESENT & vhdx_bat::BAT_STATE_BIT_MASK);
let new_bat_entry =
file_offset | (bat::PAYLOAD_BLOCK_FULLY_PRESENT & bat::BAT_STATE_BIT_MASK);
bat[sector.bat_index as usize] = BatEntry(new_bat_entry);
BatEntry::write_bat_entries(f, bat_offset, bat).map_err(VhdxIoError::WriteBat)?;
if file_offset < vhdx_metadata::BLOCK_SIZE_MIN as u64 {
if file_offset < metadata::BLOCK_SIZE_MIN as u64 {
break;
}
f.seek(SeekFrom::Start(file_offset))
.map_err(VhdxIoError::ReadSectorBlock)?;
f.write_all(
f.write_all_at(
&buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
file_offset,
)
.map_err(VhdxIoError::ReadSectorBlock)?;
}
vhdx_bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
if sector.file_offset < vhdx_metadata::BLOCK_SIZE_MIN as u64 {
bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
if sector.file_offset < metadata::BLOCK_SIZE_MIN as u64 {
break;
}
f.seek(SeekFrom::Start(sector.file_offset))
.map_err(VhdxIoError::ReadSectorBlock)?;
f.write_all(
f.write_all_at(
&buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
sector.file_offset,
)
.map_err(VhdxIoError::ReadSectorBlock)?;
}
vhdx_bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => {
bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => {
return Err(VhdxIoError::UnsupportedMode);
}
_ => {

View File

@@ -2,16 +2,16 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::mem::size_of;
use std::os::unix::fs::FileExt;
use std::{io, result};
use byteorder::{LittleEndian, ReadBytesExt};
use byteorder::{ByteOrder, LittleEndian};
use remain::sorted;
use thiserror::Error;
use uuid::Uuid;
use zerocopy::FromBytes;
use crate::vhdx::vhdx_header::RegionTableEntry;
use super::header::RegionTableEntry;
use crate::aligned_file::AlignedFile;
const METADATA_SIGN: u64 = 0x6174_6164_6174_656D;
const METADATA_ENTRY_SIZE: usize = 32;
@@ -21,19 +21,31 @@ const METADATA_TABLE_MAX_SIZE: usize = METADATA_ENTRY_SIZE * (METADATA_MAX_ENTRI
const METADATA_FLAGS_IS_REQUIRED: u32 = 0x04;
pub const BLOCK_SIZE_MIN: u32 = 1 << 20; // 1 MiB
pub(super) const BLOCK_SIZE_MIN: u32 = 1 << 20; // 1 MiB
const BLOCK_SIZE_MAX: u32 = 256 << 20; // 256 MiB
const MAX_SECTORS_PER_BLOCK: u64 = 1 << 23;
const BLOCK_HAS_PARENT: u32 = 0x02; // Has a parent or a backing file
// GUID for known metadata items
const METADATA_FILE_PARAMETER: &str = "CAA16737-FA36-4D43-B3B6-33F0AA44E76B";
const METADATA_VIRTUAL_DISK_SIZE: &str = "2FA54224-CD1B-4876-B211-5DBED83BF4B8";
const METADATA_VIRTUAL_DISK_ID: &str = "BECA12AB-B2E6-4523-93EF-C309E000C746";
const METADATA_LOGICAL_SECTOR_SIZE: &str = "8141BF1D-A96F-4709-BA47-F233A8FAAB5F";
const METADATA_PHYSICAL_SECTOR_SIZE: &str = "CDA348C7-445D-4471-9CC9-E9885251C556";
const METADATA_PARENT_LOCATOR: &str = "A8D35F2D-B30B-454D-ABF7-D3D84834AB0C";
const METADATA_FILE_PARAMETER: [u8; 16] = [
0x37, 0x67, 0xa1, 0xca, 0x36, 0xfa, 0x43, 0x4d, 0xb3, 0xb6, 0x33, 0xf0, 0xaa, 0x44, 0xe7, 0x6b,
];
const METADATA_VIRTUAL_DISK_SIZE: [u8; 16] = [
0x24, 0x42, 0xa5, 0x2f, 0x1b, 0xcd, 0x76, 0x48, 0xb2, 0x11, 0x5d, 0xbe, 0xd8, 0x3b, 0xf4, 0xb8,
];
const METADATA_VIRTUAL_DISK_ID: [u8; 16] = [
0xab, 0x12, 0xca, 0xbe, 0xe6, 0xb2, 0x23, 0x45, 0x93, 0xef, 0xc3, 0x09, 0xe0, 0x00, 0xc7, 0x46,
];
const METADATA_LOGICAL_SECTOR_SIZE: [u8; 16] = [
0x1d, 0xbf, 0x41, 0x81, 0x6f, 0xa9, 0x09, 0x47, 0xba, 0x47, 0xf2, 0x33, 0xa8, 0xfa, 0xab, 0x5f,
];
const METADATA_PHYSICAL_SECTOR_SIZE: [u8; 16] = [
0xc7, 0x48, 0xa3, 0xcd, 0x5d, 0x44, 0x71, 0x44, 0x9c, 0xc9, 0xe9, 0x88, 0x52, 0x51, 0xc5, 0x56,
];
const METADATA_PARENT_LOCATOR: [u8; 16] = [
0x2d, 0x5f, 0xd3, 0xa8, 0x0b, 0xb3, 0x4d, 0x45, 0xab, 0xf7, 0xd3, 0xd8, 0x48, 0x34, 0xab, 0x0c,
];
const METADATA_FILE_PARAMETER_PRESENT: u16 = 0x01;
const METADATA_VIRTUAL_DISK_SIZE_PRESENT: u16 = 0x02;
@@ -69,8 +81,6 @@ pub enum VhdxMetadataError {
InvalidMetadataSign,
#[error("Invalid physical sector size")]
InvalidPhysicalSectorSize,
#[error("Invalid UUID")]
InvalidUuid(#[source] uuid::Error),
#[error("Invalid value")]
InvalidValue,
#[error("Not all required metadata found")]
@@ -83,10 +93,10 @@ pub enum VhdxMetadataError {
UnsupportedFlag,
}
pub type Result<T> = std::result::Result<T, VhdxMetadataError>;
pub(super) type Result<T> = result::Result<T, VhdxMetadataError>;
#[derive(Default, Clone, Debug)]
pub struct DiskSpec {
pub(super) struct DiskSpec {
pub disk_id: u128,
pub image_size: u64,
pub block_size: u32,
@@ -102,17 +112,18 @@ pub struct DiskSpec {
impl DiskSpec {
/// Parse all metadata from the provided file and store info in DiskSpec
/// structure.
pub fn new(f: &mut File, metadata_region: &RegionTableEntry) -> Result<DiskSpec> {
pub(super) fn new(f: &AlignedFile, metadata_region: &RegionTableEntry) -> Result<DiskSpec> {
let mut disk_spec = DiskSpec::default();
let mut metadata_presence: u16 = 0;
let mut offset = 0;
let metadata = f.metadata().map_err(VhdxMetadataError::ReadMetadata)?;
let metadata = f
.file()
.metadata()
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.image_size = metadata.len();
let mut buffer = [0u8; METADATA_TABLE_MAX_SIZE];
f.seek(SeekFrom::Start(metadata_region.file_offset))
.map_err(VhdxMetadataError::ReadMetadata)?;
f.read_exact(&mut buffer)
f.read_exact_at(&mut buffer, metadata_region.file_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
let metadata_header =
@@ -123,18 +134,13 @@ impl DiskSpec {
let metadata_entry =
MetadataTableEntry::new(&buffer[offset..offset + size_of::<MetadataTableEntry>()])?;
f.seek(SeekFrom::Start(
metadata_region.file_offset + metadata_entry.offset as u64,
))
.map_err(VhdxMetadataError::ReadMetadata)?;
let item_offset = metadata_region.file_offset + metadata_entry.offset as u64;
if metadata_entry.item_id
== Uuid::parse_str(METADATA_FILE_PARAMETER)
.map_err(VhdxMetadataError::InvalidUuid)?
{
disk_spec.block_size = f
.read_u32::<LittleEndian>()
if metadata_entry.item_id == METADATA_FILE_PARAMETER {
let mut item = [0u8; 2 * size_of::<u32>()];
f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.block_size = LittleEndian::read_u32(&item[0..4]);
// MUST be at least 1 MiB and not greater than 256 MiB
if disk_spec.block_size < BLOCK_SIZE_MIN || disk_spec.block_size > BLOCK_SIZE_MAX {
@@ -146,50 +152,40 @@ impl DiskSpec {
return Err(VhdxMetadataError::InvalidBlockSize);
}
let bits = f
.read_u32::<LittleEndian>()
.map_err(VhdxMetadataError::ReadMetadata)?;
let bits = LittleEndian::read_u32(&item[4..8]);
disk_spec.has_parent = bits & BLOCK_HAS_PARENT != 0;
metadata_presence |= METADATA_FILE_PARAMETER_PRESENT;
} else if metadata_entry.item_id
== Uuid::parse_str(METADATA_VIRTUAL_DISK_SIZE)
.map_err(VhdxMetadataError::InvalidUuid)?
{
disk_spec.virtual_disk_size = f
.read_u64::<LittleEndian>()
} else if metadata_entry.item_id == METADATA_VIRTUAL_DISK_SIZE {
let mut item = [0u8; size_of::<u64>()];
f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.virtual_disk_size = LittleEndian::read_u64(&item);
metadata_presence |= METADATA_VIRTUAL_DISK_SIZE_PRESENT;
} else if metadata_entry.item_id
== Uuid::parse_str(METADATA_VIRTUAL_DISK_ID)
.map_err(VhdxMetadataError::InvalidUuid)?
{
disk_spec.disk_id = f
.read_u128::<LittleEndian>()
} else if metadata_entry.item_id == METADATA_VIRTUAL_DISK_ID {
let mut item = [0u8; size_of::<u128>()];
f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.disk_id = LittleEndian::read_u128(&item);
metadata_presence |= METADATA_VIRTUAL_DISK_ID_PRESENT;
} else if metadata_entry.item_id
== Uuid::parse_str(METADATA_LOGICAL_SECTOR_SIZE)
.map_err(VhdxMetadataError::InvalidUuid)?
{
disk_spec.logical_sector_size = f
.read_u32::<LittleEndian>()
} else if metadata_entry.item_id == METADATA_LOGICAL_SECTOR_SIZE {
let mut item = [0u8; size_of::<u32>()];
f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.logical_sector_size = LittleEndian::read_u32(&item);
if !(disk_spec.logical_sector_size == 512 || disk_spec.logical_sector_size == 4096)
{
return Err(VhdxMetadataError::InvalidLogicalSectorSize);
}
metadata_presence |= METADATA_LOGICAL_SECTOR_SIZE_PRESENT;
} else if metadata_entry.item_id
== Uuid::parse_str(METADATA_PHYSICAL_SECTOR_SIZE)
.map_err(VhdxMetadataError::InvalidUuid)?
{
disk_spec.physical_sector_size = f
.read_u32::<LittleEndian>()
} else if metadata_entry.item_id == METADATA_PHYSICAL_SECTOR_SIZE {
let mut item = [0u8; size_of::<u32>()];
f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.physical_sector_size = LittleEndian::read_u32(&item);
if !(disk_spec.physical_sector_size == 512
|| disk_spec.physical_sector_size == 4096)
{
@@ -197,10 +193,7 @@ impl DiskSpec {
}
metadata_presence |= METADATA_PHYSICAL_SECTOR_SIZE_PRESENT;
} else if metadata_entry.item_id
== Uuid::parse_str(METADATA_PARENT_LOCATOR)
.map_err(VhdxMetadataError::InvalidUuid)?
{
} else if metadata_entry.item_id == METADATA_PARENT_LOCATOR {
metadata_presence |= METADATA_PARENT_LOCATOR_PRESENT;
} else {
return Err(VhdxMetadataError::InvalidMetadataItem);
@@ -268,7 +261,7 @@ impl DiskSpec {
}
#[repr(C, packed)]
#[derive(Default, Debug, Clone, Copy)]
#[derive(Default, Debug, Clone, Copy, FromBytes)]
struct MetadataTableHeader {
signature: u64,
reserved: u16,
@@ -277,10 +270,8 @@ struct MetadataTableHeader {
}
impl MetadataTableHeader {
pub fn new(buffer: &[u8]) -> Result<MetadataTableHeader> {
assert!(buffer.len() == std::mem::size_of::<MetadataTableHeader>());
// SAFETY: the assertion above makes sure the buffer size is correct.
let metadata_table_header: MetadataTableHeader = unsafe { *(buffer.as_ptr().cast()) };
pub(crate) fn new(buffer: &[u8]) -> Result<MetadataTableHeader> {
let metadata_table_header = MetadataTableHeader::read_from_bytes(buffer).unwrap();
if metadata_table_header.signature != METADATA_SIGN {
return Err(VhdxMetadataError::InvalidMetadataSign);
@@ -299,9 +290,9 @@ impl MetadataTableHeader {
}
#[repr(C, packed)]
#[derive(Default, Debug, Clone, Copy)]
pub struct MetadataTableEntry {
item_id: Uuid,
#[derive(Default, Debug, Clone, Copy, FromBytes)]
pub(super) struct MetadataTableEntry {
item_id: [u8; 16],
offset: u32,
length: u32,
flag_bits: u32,
@@ -311,12 +302,7 @@ pub struct MetadataTableEntry {
impl MetadataTableEntry {
/// Parse one metadata entry from the buffer
fn new(buffer: &[u8]) -> Result<MetadataTableEntry> {
assert!(buffer.len() == std::mem::size_of::<MetadataTableEntry>());
// SAFETY: the assertion above makes sure the buffer size is correct.
let mut metadata_table_entry: MetadataTableEntry = unsafe { *(buffer.as_ptr().cast()) };
let uuid = crate::vhdx::uuid_from_guid(buffer);
metadata_table_entry.item_id = uuid;
let metadata_table_entry = MetadataTableEntry::read_from_bytes(buffer).unwrap();
if metadata_table_entry.length > METADATA_LENGTH_MAX {
return Err(VhdxMetadataError::InvalidMetadataLength);

View File

@@ -0,0 +1,118 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0
//! VHDX disk format support.
//!
//! Provides [`VhdxDisk`], the `DiskFile` wrapper for dynamic VHDX
//! images.
mod bat;
mod engine_sync;
mod header;
mod io;
mod metadata;
mod parser;
#[cfg(test)]
mod test_util;
use std::fs::File;
use std::io::Error as IoError;
use std::os::fd::AsRawFd;
use std::sync::{Arc, Mutex};
pub use parser::{Vhdx, VhdxError};
use self::engine_sync::VhdxSync;
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::{Error, disk_file};
#[derive(Debug)]
pub struct VhdxDisk {
// FIXME: The Mutex serializes all VHDX I/O operations across queues, which
// is necessary for correctness but eliminates any parallelism benefit from
// multiqueue. Vhdx::clone() shares the underlying file description across
// threads, so concurrent I/O from multiple queues races on the file offset
// causing data corruption.
//
// A proper fix would require restructuring the VHDX I/O path so that data
// operations can proceed in parallel with independent file descriptors.
vhdx_file: Arc<Mutex<Vhdx>>,
}
impl VhdxDisk {
pub fn new(f: File, direct_io: bool) -> BlockResult<Self> {
Ok(VhdxDisk {
vhdx_file: Arc::new(Mutex::new(Vhdx::new(f, direct_io).map_err(|e| {
let kind = match &e {
VhdxError::NotVhdx(_)
| VhdxError::ParseVhdxHeader(_)
| VhdxError::ParseVhdxMetadata(_)
| VhdxError::ParseVhdxRegionEntry(_) => BlockErrorKind::InvalidFormat,
VhdxError::ReadBatEntry(_) => BlockErrorKind::CorruptImage,
VhdxError::ReadFailed(_) | VhdxError::WriteFailed(_) => BlockErrorKind::Io,
};
BlockError::new(kind, e).with_op(ErrorOp::Open)
})?)),
})
}
}
impl disk_file::DiskSize for VhdxDisk {
fn logical_size(&self) -> BlockResult<u64> {
Ok(self.vhdx_file.lock().unwrap().virtual_disk_size())
}
}
impl disk_file::PhysicalSize for VhdxDisk {
fn physical_size(&self) -> BlockResult<u64> {
self.vhdx_file
.lock()
.unwrap()
.physical_size()
.map_err(|e| match e {
Error::GetFileMetadata(io) => {
BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io))
}
_ => unreachable!("unexpected error from Vhdx::physical_size(): {e}"),
})
}
}
impl disk_file::DiskFd for VhdxDisk {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.vhdx_file.lock().unwrap().as_raw_fd())
}
}
impl disk_file::Geometry for VhdxDisk {}
impl disk_file::SparseCapable for VhdxDisk {}
impl disk_file::Resizable for VhdxDisk {
fn resize(&mut self, _size: u64) -> BlockResult<()> {
Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
DiskFileError::ResizeError(IoError::other("resize not supported for VHDX")),
)
.with_op(ErrorOp::Resize))
}
}
impl disk_file::DiskFile for VhdxDisk {}
impl disk_file::AsyncDiskFile for VhdxDisk {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
Ok(Box::new(VhdxDisk {
vhdx_file: Arc::clone(&self.vhdx_file),
}))
}
fn create_async_io(&self, _ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
let size = self.vhdx_file.lock().unwrap().virtual_disk_size();
Ok(Box::new(VhdxSync::new(Arc::clone(&self.vhdx_file), size)))
}
}

View File

@@ -4,24 +4,20 @@
use std::collections::btree_map::BTreeMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::{
Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, Write,
};
use std::os::fd::{AsRawFd, RawFd};
use std::result;
use byteorder::{BigEndian, ByteOrder};
use remain::sorted;
use thiserror::Error;
use uuid::Uuid;
use crate::BlockBackend;
use crate::vhdx::vhdx_bat::{BatEntry, VhdxBatError};
use crate::vhdx::vhdx_header::{RegionInfo, RegionTableEntry, VhdxHeader, VhdxHeaderError};
use crate::vhdx::vhdx_io::VhdxIoError;
use crate::vhdx::vhdx_metadata::{DiskSpec, VhdxMetadataError};
mod vhdx_bat;
mod vhdx_header;
mod vhdx_io;
mod vhdx_metadata;
use super::bat::{BatEntry, VhdxBatError};
use super::header::{self, RegionInfo, RegionTableEntry, VhdxHeader, VhdxHeaderError};
use super::io::{self, VhdxIoError};
use super::metadata::{DiskSpec, VhdxMetadataError};
use crate::aligned_file::AlignedFile;
#[sorted]
#[derive(Error, Debug)]
@@ -42,11 +38,11 @@ pub enum VhdxError {
WriteFailed(#[source] VhdxIoError),
}
pub type Result<T> = std::result::Result<T, VhdxError>;
pub(super) type Result<T> = result::Result<T, VhdxError>;
#[derive(Debug)]
pub struct Vhdx {
file: File,
aligned: AlignedFile,
vhdx_header: VhdxHeader,
region_entries: BTreeMap<u64, u64>,
bat_entry: RegionTableEntry,
@@ -60,12 +56,14 @@ pub struct Vhdx {
impl Vhdx {
/// Parse the Vhdx header, BAT, and metadata from a file and store info
// in Vhdx structure.
pub fn new(mut file: File) -> Result<Vhdx> {
let vhdx_header = VhdxHeader::new(&mut file).map_err(VhdxError::ParseVhdxHeader)?;
pub fn new(file: File, direct_io: bool) -> Result<Vhdx> {
let aligned = AlignedFile::new(file, direct_io);
let vhdx_header = VhdxHeader::new(&aligned).map_err(VhdxError::ParseVhdxHeader)?;
let collected_entries = RegionInfo::new(
&mut file,
vhdx_header::REGION_TABLE_1_START,
&aligned,
header::REGION_TABLE_1_START,
vhdx_header.region_entry_count(),
)
.map_err(VhdxError::ParseVhdxRegionEntry)?;
@@ -74,12 +72,12 @@ impl Vhdx {
let mdr_entry = collected_entries.mdr_entry;
let disk_spec =
DiskSpec::new(&mut file, &mdr_entry).map_err(VhdxError::ParseVhdxMetadata)?;
let bat_entries = BatEntry::collect_bat_entries(&mut file, &disk_spec, &bat_entry)
DiskSpec::new(&aligned, &mdr_entry).map_err(VhdxError::ParseVhdxMetadata)?;
let bat_entries = BatEntry::collect_bat_entries(&aligned, &disk_spec, &bat_entry)
.map_err(VhdxError::ReadBatEntry)?;
Ok(Vhdx {
file,
aligned,
vhdx_header,
region_entries: collected_entries.region_entries,
bat_entry,
@@ -99,12 +97,12 @@ impl Vhdx {
impl Read for Vhdx {
/// Wrapper function to satisfy Read trait implementation for VHDx disk.
/// Convert the offset to sector index and buffer length to sector count.
fn read(&mut self, buf: &mut [u8]) -> std::result::Result<usize, std::io::Error> {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64);
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
let result = vhdx_io::read(
&mut self.file,
let result = io::read(
&self.aligned,
buf,
&self.disk_spec,
&self.bat_entries,
@@ -112,7 +110,7 @@ impl Read for Vhdx {
sector_count,
)
.map_err(|e| {
std::io::Error::other(format!(
IoError::other(format!(
"Failed reading {sector_count} sectors from VHDx at index {sector_index}: {e}"
))
})?;
@@ -124,25 +122,25 @@ impl Read for Vhdx {
}
impl Write for Vhdx {
fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
self.file.flush()
fn flush(&mut self) -> IoResult<()> {
self.aligned.file_mut().flush()
}
/// Wrapper function to satisfy Write trait implementation for VHDx disk.
/// Convert the offset to sector index and buffer length to sector count.
fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, std::io::Error> {
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64);
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
if self.first_write {
self.first_write = false;
self.vhdx_header
.update(&mut self.file)
.map_err(|e| std::io::Error::other(format!("Failed to update VHDx header: {e}")))?;
.update(&self.aligned)
.map_err(|e| IoError::other(format!("Failed to update VHDx header: {e}")))?;
}
let result = vhdx_io::write(
&mut self.file,
let result = io::write(
&self.aligned,
buf,
&mut self.disk_spec,
self.bat_entry.file_offset,
@@ -151,7 +149,7 @@ impl Write for Vhdx {
sector_count,
)
.map_err(|e| {
std::io::Error::other(format!(
IoError::other(format!(
"Failed writing {sector_count} sectors on VHDx at index {sector_index}: {e}"
))
})?;
@@ -165,7 +163,7 @@ impl Write for Vhdx {
impl Seek for Vhdx {
/// Wrapper function to satisfy Seek trait implementation for VHDx disk.
/// Updates the offset field in the Vhdx struct.
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
let new_offset: Option<u64> = match pos {
SeekFrom::Start(off) => Some(off),
SeekFrom::End(off) => {
@@ -194,20 +192,17 @@ impl Seek for Vhdx {
return Ok(o);
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
Err(IoError::new(
IoErrorKind::InvalidData,
"Failed seek operation",
))
}
}
impl BlockBackend for Vhdx {
fn logical_size(&self) -> std::result::Result<u64, crate::Error> {
Ok(self.virtual_disk_size())
}
fn physical_size(&self) -> std::result::Result<u64, crate::Error> {
self.file
impl Vhdx {
pub(crate) fn physical_size(&self) -> result::Result<u64, crate::Error> {
self.aligned
.file()
.metadata()
.map(|m| m.len())
.map_err(crate::Error::GetFileMetadata)
@@ -217,7 +212,7 @@ impl BlockBackend for Vhdx {
impl Clone for Vhdx {
fn clone(&self) -> Self {
Vhdx {
file: self.file.try_clone().unwrap(),
aligned: self.aligned.try_clone().unwrap(),
vhdx_header: self.vhdx_header.clone(),
region_entries: self.region_entries.clone(),
bat_entry: self.bat_entry,
@@ -232,18 +227,82 @@ impl Clone for Vhdx {
impl AsRawFd for Vhdx {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
self.aligned.file().as_raw_fd()
}
}
pub(crate) fn uuid_from_guid(buf: &[u8]) -> Uuid {
// The first 3 fields of UUID are stored in Big Endian format, and
// the last 8 bytes are stored as byte array. Therefore, we read the
// first 3 fields in Big Endian format instead of Little Endian.
Uuid::from_fields_le(
BigEndian::read_u32(&buf[0..4]),
BigEndian::read_u16(&buf[4..6]),
BigEndian::read_u16(&buf[6..8]),
buf[8..16].try_into().unwrap(),
)
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
use crate::formats::vhdx::test_util::create_dynamic_vhdx;
/// An unaligned sector write under a forced O_DIRECT alignment must go
/// through `AlignedFile`'s read-modify-write bounce (the data block and the
/// BAT update both land at unaligned host offsets) and read back intact.
#[test]
fn unaligned_write_is_rmw() {
let Some(tf) = create_dynamic_vhdx(16) else {
eprintln!("skipping unaligned_write_is_rmw: qemu-img unavailable");
return;
};
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
// Force a non-zero alignment so all of vhdx's positioned I/O exercises
// the bounce/RMW path even though the tempfile is not really O_DIRECT.
vhdx.aligned = AlignedFile::with_alignment(vhdx.aligned.file().try_clone().unwrap(), 512);
let sector = vhdx.disk_spec.logical_sector_size as usize;
let data: Vec<u8> = (0..sector).map(|i| ((i + 1) % 251) as u8).collect();
// Write at virtual offset 0 (allocates a new data block + rewrites BAT).
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.write(&data).unwrap(), data.len());
vhdx.flush().unwrap();
// Read it back through a fresh, forced-alignment handle.
let mut readback = vec![0u8; sector];
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len());
assert_eq!(readback, data);
}
#[test]
fn header_update_survives_reopen() {
let Some(tf) = create_dynamic_vhdx(16) else {
eprintln!("skipping header_update_survives_reopen: qemu-img unavailable");
return;
};
let data = [0xa5u8; 512];
{
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.write(&data).unwrap(), data.len());
vhdx.flush().unwrap();
}
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
let mut readback = [0u8; 512];
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len());
assert_eq!(readback, data);
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Shared test helpers for VHDX image tests.
use std::process::Command;
use vmm_sys_util::tempfile::TempFile;
/// Generate a small dynamic VHDX with `qemu-img`. Returns `None` (and the
/// test is skipped) when `qemu-img` is unavailable, e.g. in minimal CI.
pub(crate) fn create_dynamic_vhdx(size_mib: u64) -> Option<TempFile> {
let tf = TempFile::new().unwrap();
let path = tf.as_path();
let status = Command::new("qemu-img")
.args(["create", "-f", "vhdx", "-o", "subformat=dynamic"])
.arg(path)
.arg(format!("{size_mib}M"))
.status();
match status {
Ok(s) if s.success() => Some(tf),
_ => None,
}
}

184
block/src/io/async_io.rs Normal file
View File

@@ -0,0 +1,184 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::marker::PhantomData;
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
mod aio_data_io;
mod common;
mod completion;
mod guest_memory_target;
mod operation;
mod owned_io_buffer;
#[cfg(feature = "io_uring")]
mod uring_data_io;
use std::{io, result};
pub use aio_data_io::AioDataIo;
pub use completion::AsyncIoCompletion;
pub use guest_memory_target::GuestMemoryTarget;
pub use operation::AsyncIoOperation;
pub use owned_io_buffer::OwnedIoBuffer;
use thiserror::Error;
#[cfg(feature = "io_uring")]
pub use uring_data_io::UringDataIo;
use vmm_sys_util::eventfd::EventFd;
use crate::SECTOR_SIZE;
#[derive(Error, Debug)]
pub enum DiskFileError {
/// Failed getting disk file size.
#[error("Failed getting disk file size")]
Size(#[source] io::Error),
/// Failed creating a new AsyncIo.
#[error("Failed creating a new AsyncIo")]
NewAsyncIo(#[source] io::Error),
/// Unsupported operation.
#[error("Unsupported operation")]
Unsupported,
/// Resize failed
#[error("Resize failed")]
ResizeError(#[source] io::Error),
#[error("Failed cloning disk file")]
Clone(#[source] io::Error),
}
pub type DiskFileResult<T> = result::Result<T, DiskFileError>;
/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding disk file.
///
/// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations
/// by some disk implementations, which wrap the effective [`File`]
/// in an `Arc<Mutex<T>>`, making the use of [`BorrowedFd`] impossible.
///
/// [`BorrowedFd`]: std::os::fd::BorrowedFd
#[derive(Copy, Clone, Debug)]
pub struct BorrowedDiskFd<'fd> {
raw_fd: RawFd,
_lifetime: PhantomData<&'fd OwnedFd>,
}
impl BorrowedDiskFd<'_> {
pub(crate) fn new(raw_fd: RawFd) -> Self {
Self {
raw_fd,
_lifetime: PhantomData,
}
}
}
impl AsRawFd for BorrowedDiskFd<'_> {
fn as_raw_fd(&self) -> RawFd {
self.raw_fd
}
}
#[derive(Error, Debug)]
pub enum AsyncIoError {
/// Failed vectored reading from file.
#[error("Failed vectored reading from file")]
ReadVectored(#[source] io::Error),
/// Failed vectored writing to file.
#[error("Failed vectored writing to file")]
WriteVectored(#[source] io::Error),
/// Failed synchronizing file.
#[error("Failed synchronizing file")]
Fsync(#[source] io::Error),
/// Failed punching hole.
#[error("Failed punching hole")]
PunchHole(#[source] io::Error),
/// Failed writing zeroes.
#[error("Failed writing zeroes")]
WriteZeroes(#[source] io::Error),
/// Failed submitting batch requests.
#[error("Failed submitting batch requests")]
SubmitBatchRequests(#[source] io::Error),
}
pub type AsyncIoResult<T> = result::Result<T, AsyncIoError>;
pub trait AsyncIo: Send {
fn notifier(&self) -> &EventFd;
/// Submits one owned data operation.
///
/// Takes ownership of `op`.
/// Implementations that complete asynchronously must retain it until its
/// completion is returned.
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()>;
/// Submits a read from `offset` into guest memory.
fn read_to_memory(
&mut self,
offset: libc::off_t,
target: GuestMemoryTarget,
user_data: u64,
) -> AsyncIoResult<()> {
self.submit_data_operation(AsyncIoOperation::read_to_memory(offset, target, user_data))
}
/// Submits a write to `offset` from guest memory.
fn write_from_memory(
&mut self,
offset: libc::off_t,
target: GuestMemoryTarget,
user_data: u64,
) -> AsyncIoResult<()> {
self.submit_data_operation(AsyncIoOperation::write_from_memory(
offset, target, user_data,
))
}
/// Submits a read from `offset` into an owned host-memory buffer.
fn read_to_vec(
&mut self,
offset: libc::off_t,
buffer: OwnedIoBuffer,
user_data: u64,
) -> AsyncIoResult<()> {
self.submit_data_operation(AsyncIoOperation::read_to_vec(offset, buffer, user_data))
}
/// Submits a write to `offset` from an owned host-memory buffer.
fn write_from_vec(
&mut self,
offset: libc::off_t,
buffer: OwnedIoBuffer,
user_data: u64,
) -> AsyncIoResult<()> {
self.submit_data_operation(AsyncIoOperation::write_from_vec(offset, buffer, user_data))
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()>;
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
/// Returns the next owned completion, if one is available.
///
/// Read completions from owned host-memory buffers return that buffer here.
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion>;
fn batch_requests_enabled(&self) -> bool {
false
}
/// Submits a batch of owned data operations.
///
/// Backends either accept the whole batch for eventual completion or return
/// an error before taking ownership of any operation.
fn submit_batch_requests(&mut self, batch_request: Vec<AsyncIoOperation>) -> AsyncIoResult<()> {
if batch_request.is_empty() {
Ok(())
} else {
Err(AsyncIoError::SubmitBatchRequests(io::Error::other(
"batch requests are not supported by this backend",
)))
}
}
fn alignment(&self) -> u64 {
SECTOR_SIZE
}
}

View File

@@ -0,0 +1,235 @@
// Copyright © 2023 Intel Corporation
//
// Copyright © 2023 Crusoe Energy Systems LLC
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::collections::{HashMap, VecDeque};
use std::os::fd::{AsRawFd, RawFd};
use std::{io, slice};
use log::warn;
use vmm_sys_util::aio;
use vmm_sys_util::eventfd::EventFd;
use super::common::{duplicate_user_data_error, errno_result, validate_batch};
use super::{AsyncIoCompletion, AsyncIoOperation};
/// Retained Linux AIO queue for owned async data I/O operations.
pub struct AioDataIo {
// Keep this before `in_flight`: Rust drops fields in declaration order, so
// dropping the context destroys kernel AIO state before retained
// operations release the buffers referenced by their iovecs.
ctx: aio::IoContext,
// The `EventFd` for completion signals.
eventfd: EventFd,
// `in_flight` tracks every user_data value accepted by the kernel. Owned
// data operations store `Some(op)` so their iovecs and backing buffers
// remain valid until completion; metadata operations store `None`.
in_flight: HashMap<u64, Option<AsyncIoOperation>>,
// `completions` holds locally produced completions and kernel events that
// have been fetched but not yet returned to the caller.
completions: VecDeque<AsyncIoCompletion>,
}
impl AioDataIo {
/// Creates a Linux AIO context and its completion eventfd.
pub fn new(queue_depth: u32) -> io::Result<Self> {
Ok(Self {
ctx: aio::IoContext::new(queue_depth)?,
eventfd: EventFd::new(libc::EFD_NONBLOCK)?,
in_flight: HashMap::new(),
completions: VecDeque::new(),
})
}
/// Returns the eventfd signaled when completions are available.
pub fn notifier(&self) -> &EventFd {
&self.eventfd
}
#[allow(unused_unsafe)]
fn submit_iocbs(ctx: &aio::IoContext, iocbs: &[&mut aio::IoControlBlock]) -> io::Result<usize> {
// SAFETY: vmm_sys_util currently marks IoContext::submit safe, but
// io_submit consumes raw pointers asynchronously. Callers must ensure
// all iovec and buffer memory referenced by each iocb remains valid
// until completion or failed submission.
unsafe { ctx.submit(iocbs) }
}
/// Submits one owned read or write operation to the queue.
///
/// Submission failures are converted into injected completions so callers
/// can observe every accepted request through the normal completion path.
pub fn submit_operation(&mut self, fd: RawFd, op: AsyncIoOperation) -> io::Result<()> {
validate_batch(
|user_data| self.in_flight.contains_key(&user_data),
slice::from_ref(&op),
)?;
let user_data = op.user_data();
let iovecs = op.iovecs();
let opcode = if op.is_read() {
aio::IOCB_CMD_PREADV
} else {
aio::IOCB_CMD_PWRITEV
};
let mut iocb = aio::IoControlBlock {
aio_fildes: fd.as_raw_fd() as u32,
aio_lio_opcode: opcode as u16,
aio_buf: iovecs.as_ptr() as u64,
aio_nbytes: iovecs.len() as u64,
aio_offset: op.offset(),
aio_data: user_data,
aio_flags: aio::IOCB_FLAG_RESFD,
aio_resfd: self.eventfd.as_raw_fd() as u32,
..Default::default()
};
self.in_flight.insert(user_data, Some(op));
let result = match Self::submit_iocbs(&self.ctx, &[&mut iocb]) {
Ok(1) => return Ok(()),
Ok(_) => -libc::EAGAIN,
Err(e) => errno_result(&e),
};
let buffer = self
.in_flight
.remove(&user_data)
.flatten()
.and_then(AsyncIoOperation::into_completion_buffer);
self.inject_completion(AsyncIoCompletion::new(user_data, result, buffer));
Ok(())
}
/// Submits an fsync operation carrying `user_data`.
pub fn submit_fsync(&mut self, fd: RawFd, user_data: u64) -> io::Result<()> {
if self.in_flight.contains_key(&user_data) {
return Err(duplicate_user_data_error(user_data));
}
let mut iocb = aio::IoControlBlock {
aio_fildes: fd.as_raw_fd() as u32,
aio_lio_opcode: aio::IOCB_CMD_FSYNC as u16,
aio_data: user_data,
aio_flags: aio::IOCB_FLAG_RESFD,
aio_resfd: self.eventfd.as_raw_fd() as u32,
..Default::default()
};
self.in_flight.insert(user_data, None);
let result = match Self::submit_iocbs(&self.ctx, &[&mut iocb]) {
Ok(1) => return Ok(()),
Ok(_) => -libc::EAGAIN,
Err(e) => errno_result(&e),
};
self.in_flight.remove(&user_data);
self.inject_completion(AsyncIoCompletion::new(user_data, result, None));
Ok(())
}
/// Injects a completion that did not come from a kernel AIO event.
///
/// The notifier is signaled so callers can drain it with
/// [`Self::next_completion`].
pub fn inject_completion(&mut self, completion: AsyncIoCompletion) {
self.completions.push_back(completion);
self.eventfd.write(1).unwrap();
}
/// Returns the next kernel or injected completion if one is available.
///
/// Consuming a kernel completion returns ownership of any buffer retained
/// by the corresponding operation.
pub fn next_completion(&mut self) -> Option<AsyncIoCompletion> {
if self.completions.is_empty() {
let mut events = [aio::IoEvent::default(); 32];
let rc = match self.ctx.get_events(0, &mut events, None) {
Ok(rc) => rc,
Err(e) => {
warn!("Linux AIO get_events failed: {e}");
return None;
}
};
for event in &events[..rc] {
self.completions.push_back(AsyncIoCompletion::new(
event.data,
event.res as i32,
self.in_flight
.remove(&event.data)
.flatten()
.and_then(AsyncIoOperation::into_completion_buffer),
));
}
}
self.completions.pop_front()
}
}
#[cfg(test)]
mod tests {
use std::io::{self, Write};
use std::os::fd::AsRawFd;
use std::thread::sleep;
use std::time::Duration;
use vmm_sys_util::tempfile::TempFile;
use super::AioDataIo;
use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, OwnedIoBuffer};
fn wait_for_completion(data_io: &mut AioDataIo) -> AsyncIoCompletion {
for _ in 0..1000 {
if let Some(completion) = data_io.next_completion() {
return completion;
}
sleep(Duration::from_millis(1));
}
panic!("timed out waiting for Linux AIO completion");
}
#[test]
fn aio_rejects_duplicate_user_data_for_metadata_ops() {
let mut file = TempFile::new().unwrap().into_file();
file.write_all(&[0xa5; 512]).unwrap();
let fd = file.as_raw_fd();
let mut data_io = AioDataIo::new(8).unwrap();
data_io
.submit_operation(
fd,
AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), 7),
)
.unwrap();
assert_eq!(
data_io.submit_fsync(fd, 7).unwrap_err().kind(),
io::ErrorKind::AlreadyExists
);
let completion = wait_for_completion(&mut data_io);
assert_eq!(completion.user_data, 7);
assert_eq!(completion.result, 512);
assert_eq!(
completion.buffer.unwrap().as_slice(),
[0xa5; 512].as_slice()
);
}
#[test]
fn aio_injected_completion_uses_completion_path() {
let mut data_io = AioDataIo::new(8).unwrap();
data_io.inject_completion(AsyncIoCompletion::new(9, -libc::EIO, None));
let completion = data_io.next_completion().unwrap();
assert_eq!(completion.user_data, 9);
assert_eq!(completion.result, -libc::EIO);
assert!(completion.buffer.is_none());
assert!(data_io.next_completion().is_none());
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! Helpers used by both aio and uring async io.
use std::collections::HashSet;
use std::io;
use super::AsyncIoOperation;
/// Converts an I/O error into the negative errno form used in completions.
pub(super) fn errno_result(error: &io::Error) -> i32 {
-error.raw_os_error().unwrap_or(libc::EIO)
}
/// Builds the error returned when a new request reuses in-flight `user_data`.
pub(super) fn duplicate_user_data_error(user_data: u64) -> io::Error {
io::Error::new(
io::ErrorKind::AlreadyExists,
format!("duplicate async I/O user_data {user_data}"),
)
}
/// Validates that a batch has unique `user_data` not already in flight.
pub(super) fn validate_batch<F>(mut is_in_flight: F, batch: &[AsyncIoOperation]) -> io::Result<()>
where
F: FnMut(u64) -> bool,
{
let mut seen = HashSet::with_capacity(batch.len());
for op in batch {
let user_data = op.user_data();
if is_in_flight(user_data) || !seen.insert(user_data) {
return Err(duplicate_user_data_error(user_data));
}
}
Ok(())
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use super::{AsyncIoOperation, OwnedIoBuffer};
/// Completion returned by an owned async I/O backend.
///
/// The completion carries the caller provided `user_data`, the result,
/// and any owned buffer that can now be dropped.
#[derive(Debug)]
pub struct AsyncIoCompletion {
/// Caller provided identifier associated with the submitted operation.
pub user_data: u64,
/// I/O result reported by the backend.
///
/// Successful operations report a non-negative byte count. Failed
/// operations report a negative errno value.
pub result: i32,
/// The backing buffer that can now be dropped or re-used.
pub buffer: Option<OwnedIoBuffer>,
}
impl AsyncIoCompletion {
/// Creates a completion from its parts.
pub fn new(user_data: u64, result: i32, buffer: Option<OwnedIoBuffer>) -> Self {
Self {
user_data,
result,
buffer,
}
}
/// Creates a completion by consuming the operation that just completed.
///
/// This returns ownership of any completion buffer carried by the
/// operation.
pub fn from_operation(op: AsyncIoOperation, result: i32) -> Self {
let user_data = op.user_data();
Self::new(user_data, result, op.into_completion_buffer())
}
}

View File

@@ -0,0 +1,241 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::cmp::min;
use std::fmt;
use std::sync::Arc;
use smallvec::SmallVec;
use vm_memory::bitmap::Bitmap;
use vm_memory::{Address, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryMmap};
trait GuestMemoryTargetOwner: Send + Sync {
fn iovec_for_range(
&self,
addr: GuestAddress,
len: usize,
) -> Result<libc::iovec, GuestMemoryError>;
fn write_guest_slice(&self, buf: &[u8], addr: GuestAddress) -> Result<(), GuestMemoryError>;
fn read_guest_slice(&self, buf: &mut [u8], addr: GuestAddress) -> Result<(), GuestMemoryError>;
}
impl<B> GuestMemoryTargetOwner for GuestMemoryMmap<B>
where
B: Bitmap + Send + Sync + 'static,
{
fn iovec_for_range(
&self,
addr: GuestAddress,
len: usize,
) -> Result<libc::iovec, GuestMemoryError> {
let slice = self.get_slice(addr, len)?;
let guard = slice.ptr_guard_mut();
Ok(libc::iovec {
iov_base: guard.as_ptr().cast(),
iov_len: len,
})
}
fn write_guest_slice(&self, buf: &[u8], addr: GuestAddress) -> Result<(), GuestMemoryError> {
<Self as Bytes<GuestAddress>>::write_slice(self, buf, addr)
}
fn read_guest_slice(&self, buf: &mut [u8], addr: GuestAddress) -> Result<(), GuestMemoryError> {
<Self as Bytes<GuestAddress>>::read_slice(self, buf, addr)
}
}
/// Retains a guest-memory Arc and the validated ranges used for I/O.
///
/// Keeping the guest memory arc with the ranges guarantees that the iovecs
/// remain valid for as long as Self is alive. The iovecs are also shared with
/// the kernel and must be stable.
pub struct GuestMemoryTarget {
owner: Arc<dyn GuestMemoryTargetOwner>,
ranges: SmallVec<[(GuestAddress, usize); 1]>,
iovecs: Vec<libc::iovec>,
}
// SAFETY: GuestMemoryTarget owns an Arc to the guest memory backing and
// holds its iovecs in a heap allocation, so moving the target leaves the
// iovec addresses (and the host pointers they reference) stable.
unsafe impl Send for GuestMemoryTarget {}
impl GuestMemoryTarget {
/// Creates a new `GuestMemoryTarget`.
///
/// The memory Arc is retained for the life of `Self`, making this
/// appropriate for asynchronous I/O operations on the specified ranges.
pub fn new<B>(
mem: Arc<GuestMemoryMmap<B>>,
ranges: &[(GuestAddress, u32)],
) -> Result<Self, GuestMemoryError>
where
B: Bitmap + Send + Sync + 'static,
{
let retained_ranges: SmallVec<[(GuestAddress, usize); 1]> = ranges
.iter()
.copied()
.filter(|&(_, len)| len != 0)
.map(|(addr, len)| {
let len = len as usize;
mem.get_slice(addr, len)?;
Ok((addr, len))
})
.collect::<Result<SmallVec<[_; 1]>, GuestMemoryError>>()?;
// iovec_for_range cannot fail: each range was just validated by
// get_slice above and the Arc keeps the mapping alive.
let iovecs: Vec<libc::iovec> = retained_ranges
.iter()
.map(|&(addr, len)| {
mem.iovec_for_range(addr, len)
.expect("range validated above and retained by owner Arc")
})
.collect();
Ok(Self {
owner: mem,
ranges: retained_ranges,
iovecs,
})
}
/// Returns the raw iovecs to be passed to the kernel for asynchronous I/O.
pub(super) fn iovecs(&self) -> &[libc::iovec] {
&self.iovecs
}
/// Returns the total length of the ranges specified at creation.
pub fn total_len(&self) -> usize {
self.ranges.iter().map(|(_, len)| len).sum()
}
pub(crate) fn write_bytes_at(&self, start: usize, data: &[u8]) -> Result<(), GuestMemoryError> {
self.for_each_range(start, data.len(), |addr, offset, len| {
self.owner
.write_guest_slice(&data[offset..offset + len], addr)
})
}
pub(crate) fn read_bytes_at(
&self,
start: usize,
data: &mut [u8],
) -> Result<(), GuestMemoryError> {
self.for_each_range(start, data.len(), |addr, offset, len| {
self.owner
.read_guest_slice(&mut data[offset..offset + len], addr)
})
}
pub(crate) fn fill_zeroes_at(&self, start: usize, len: usize) -> Result<(), GuestMemoryError> {
let zeroes = [0u8; 4096];
self.for_each_range(start, len, |addr, _, mut len| {
let mut offset = 0usize;
while len > 0 {
let count = min(len, zeroes.len());
let addr = addr
.checked_add(offset as u64)
.ok_or(GuestMemoryError::InvalidGuestAddress(addr))?;
self.owner.write_guest_slice(&zeroes[..count], addr)?;
offset += count;
len -= count;
}
Ok(())
})
}
fn for_each_range<F>(&self, start: usize, len: usize, mut f: F) -> Result<(), GuestMemoryError>
where
F: FnMut(GuestAddress, usize, usize) -> Result<(), GuestMemoryError>,
{
self.validate_range(start, len)?;
let mut copied = 0usize;
let mut pos = 0usize;
for &(addr, range_len) in self.ranges.iter() {
let range_end = pos + range_len;
if range_end <= start || copied == len {
pos = range_end;
continue;
}
let range_start = start.saturating_sub(pos);
let count = min(range_len - range_start, len - copied);
let addr = addr
.checked_add(range_start as u64)
.ok_or(GuestMemoryError::InvalidGuestAddress(addr))?;
f(addr, copied, count)?;
copied += count;
if copied == len {
break;
}
pos = range_end;
}
if copied != len {
return Err(GuestMemoryError::PartialBuffer {
expected: len,
completed: copied,
});
}
Ok(())
}
fn validate_range(&self, start: usize, len: usize) -> Result<(), GuestMemoryError> {
let total_len = self.total_len();
if start <= total_len
&& let Some(end) = start.checked_add(len)
&& end <= total_len
{
return Ok(());
}
Err(GuestMemoryError::PartialBuffer {
expected: len,
completed: total_len.saturating_sub(start).min(len),
})
}
}
impl fmt::Debug for GuestMemoryTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("GuestMemoryTarget");
debug.field("ranges", &self.ranges.len());
debug
.field("iovecs", &self.iovecs.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use vm_memory::{GuestAddress, GuestMemoryMmap};
use super::GuestMemoryTarget;
#[test]
fn iovecs_survive_move() {
// The iovec array must live on the heap so its address stays valid
// after the GuestMemoryTarget (and the AsyncIoOperation that owns it)
// is moved into an in-flight map. Capture the addresses before the
// move and confirm they still match afterwards.
let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 4096)]).unwrap());
let target = GuestMemoryTarget::new(mem, &[(GuestAddress(0), 512)]).unwrap();
let iovec_ptr_before = target.iovecs().as_ptr() as usize;
let iov_base_before = target.iovecs()[0].iov_base as usize;
let moved = Box::new(target);
assert_eq!(moved.iovecs().as_ptr() as usize, iovec_ptr_before);
assert_eq!(moved.iovecs()[0].iov_base as usize, iov_base_before);
assert_eq!(moved.iovecs().len(), 1);
}
}

View File

@@ -0,0 +1,308 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::io;
use std::ops::Range;
use super::{AsyncIoError, AsyncIoResult, GuestMemoryTarget, OwnedIoBuffer};
/// A single async IO operation.
///
/// Each operation owns or retains the memory target for the duration of the
/// operation so backends can submit it to the kernel or copy through safe helper
/// methods.
#[derive(Debug)]
pub enum AsyncIoOperation {
/// Read from disk into guest memory.
ReadToMemory {
/// Disk offset for the operation.
offset: libc::off_t,
/// Guest-memory destination.
target: GuestMemoryTarget,
/// Caller-provided completion identifier.
user_data: u64,
},
/// Write from guest memory to disk.
WriteFromMemory {
/// Disk offset for the operation.
offset: libc::off_t,
/// Guest-memory source.
target: GuestMemoryTarget,
/// Caller-provided completion identifier.
user_data: u64,
},
/// Read from disk into an owned host-memory buffer.
ReadToVec {
/// Disk offset for the operation.
offset: libc::off_t,
/// Owned destination buffer.
buffer: OwnedIoBuffer,
/// Caller-provided completion identifier.
user_data: u64,
},
/// Write from an owned host-memory buffer to disk.
WriteFromVec {
/// Disk offset for the operation.
offset: libc::off_t,
/// Owned source buffer.
buffer: OwnedIoBuffer,
/// Caller-provided completion identifier.
user_data: u64,
},
}
impl AsyncIoOperation {
/// Creates an operation that reads from disk into guest memory.
pub fn read_to_memory(offset: libc::off_t, target: GuestMemoryTarget, user_data: u64) -> Self {
Self::ReadToMemory {
offset,
target,
user_data,
}
}
/// Creates an operation that writes from guest memory to disk.
pub fn write_from_memory(
offset: libc::off_t,
target: GuestMemoryTarget,
user_data: u64,
) -> Self {
Self::WriteFromMemory {
offset,
target,
user_data,
}
}
/// Creates an operation that reads from disk into an owned buffer.
pub fn read_to_vec(offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64) -> Self {
Self::ReadToVec {
offset,
buffer,
user_data,
}
}
/// Creates an operation that writes from an owned buffer to disk.
pub fn write_from_vec(offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64) -> Self {
Self::WriteFromVec {
offset,
buffer,
user_data,
}
}
/// Returns the value provided at construction.
pub fn user_data(&self) -> u64 {
match self {
Self::ReadToMemory { user_data, .. }
| Self::WriteFromMemory { user_data, .. }
| Self::ReadToVec { user_data, .. }
| Self::WriteFromVec { user_data, .. } => *user_data,
}
}
/// Returns the disk offset for this operation.
pub fn offset(&self) -> libc::off_t {
match self {
Self::ReadToMemory { offset, .. }
| Self::WriteFromMemory { offset, .. }
| Self::ReadToVec { offset, .. }
| Self::WriteFromVec { offset, .. } => *offset,
}
}
/// Updates the disk offset for this operation.
pub fn set_offset(&mut self, new_offset: libc::off_t) {
match self {
Self::ReadToMemory { offset, .. }
| Self::WriteFromMemory { offset, .. }
| Self::ReadToVec { offset, .. }
| Self::WriteFromVec { offset, .. } => *offset = new_offset,
}
}
/// Returns whether this operation reads from disk.
pub fn is_read(&self) -> bool {
matches!(self, Self::ReadToMemory { .. } | Self::ReadToVec { .. })
}
/// Rejects an operation whose byte range falls outside a disk of `size` bytes.
///
/// Returns the read/write-specific `AsyncIoError` variant, carrying an
/// `InvalidData` error, when the offset overflows or `offset + len`
/// exceeds `size`.
pub(crate) fn validate_bounds(&self, size: u64) -> AsyncIoResult<()> {
let bounds_error = || {
let error = io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid request offset {} and length {}, can't exceed file size {}",
self.offset(),
self.total_len(),
size
),
);
if self.is_read() {
AsyncIoError::ReadVectored(error)
} else {
AsyncIoError::WriteVectored(error)
}
};
let offset = u64::try_from(self.offset()).map_err(|_| bounds_error())?;
let len = u64::try_from(self.total_len()).map_err(|_| bounds_error())?;
let end = offset.checked_add(len).ok_or_else(bounds_error)?;
if end > size {
return Err(bounds_error());
}
Ok(())
}
/// Returns the retained iovec array for kernel submission.
///
/// The iovec pointers are valid while this operation is alive.
pub fn iovecs(&self) -> &[libc::iovec] {
match self {
Self::ReadToMemory { target, .. } | Self::WriteFromMemory { target, .. } => {
target.iovecs()
}
Self::ReadToVec { buffer, .. } | Self::WriteFromVec { buffer, .. } => buffer.iovecs(),
}
}
/// Returns the total number of bytes described by the operation iovecs.
pub fn total_len(&self) -> usize {
match self {
Self::ReadToMemory { target, .. } | Self::WriteFromMemory { target, .. } => {
target.total_len()
}
Self::ReadToVec { buffer, .. } | Self::WriteFromVec { buffer, .. } => {
buffer.total_len()
}
}
}
fn checked_range(total_len: usize, start: usize, len: usize) -> io::Result<Range<usize>> {
if start <= total_len
&& let Some(end) = start.checked_add(len)
&& end <= total_len
{
return Ok(start..end);
}
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"async I/O buffer range out of bounds",
))
}
/// Copies bytes into a read operation at `start`.
pub(crate) fn write_bytes_at(&mut self, start: usize, data: &[u8]) -> io::Result<()> {
match self {
Self::ReadToMemory { target, .. } => {
target.write_bytes_at(start, data).map_err(io::Error::other)
}
Self::ReadToVec { buffer, .. } => {
let range = Self::checked_range(buffer.total_len(), start, data.len())?;
buffer.as_mut_slice()[range].copy_from_slice(data);
Ok(())
}
Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot write into a write operation",
)),
}
}
/// Fills a read operation with zeroes at `start`.
pub(crate) fn fill_zeroes_at(&mut self, start: usize, len: usize) -> io::Result<()> {
match self {
Self::ReadToMemory { target, .. } => {
target.fill_zeroes_at(start, len).map_err(io::Error::other)
}
Self::ReadToVec { buffer, .. } => {
let range = Self::checked_range(buffer.total_len(), start, len)?;
buffer.as_mut_slice()[range].fill(0);
Ok(())
}
Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot write into a write operation",
)),
}
}
/// Copies bytes out of a write operation at `start`.
pub(crate) fn read_bytes_at(&self, start: usize, data: &mut [u8]) -> io::Result<()> {
match self {
Self::WriteFromMemory { target, .. } => {
target.read_bytes_at(start, data).map_err(io::Error::other)
}
Self::WriteFromVec { buffer, .. } => {
let range = Self::checked_range(buffer.total_len(), start, data.len())?;
data.copy_from_slice(&buffer.as_slice()[range]);
Ok(())
}
Self::ReadToMemory { .. } | Self::ReadToVec { .. } => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot read from a read operation",
)),
}
}
/// Consumes the operation and returns the buffer needed by its completion.
///
/// Only `ReadToVec` operations return a buffer because callers need the
/// data they read.
pub fn into_completion_buffer(self) -> Option<OwnedIoBuffer> {
match self {
Self::ReadToVec { buffer, .. } => Some(buffer),
Self::ReadToMemory { .. }
| Self::WriteFromMemory { .. }
| Self::WriteFromVec { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn read_op(offset: libc::off_t, len: usize) -> AsyncIoOperation {
AsyncIoOperation::read_to_vec(offset, OwnedIoBuffer::from_vec(vec![0u8; len]), 0)
}
fn write_op(offset: libc::off_t, len: usize) -> AsyncIoOperation {
AsyncIoOperation::write_from_vec(offset, OwnedIoBuffer::from_vec(vec![0u8; len]), 0)
}
#[test]
fn accepts_operation_exactly_filling_size() {
read_op(0, 512).validate_bounds(512).unwrap();
}
#[test]
fn rejects_read_straddling_size() {
assert!(matches!(
read_op(256, 512).validate_bounds(512),
Err(AsyncIoError::ReadVectored(_))
));
}
#[test]
fn rejects_write_straddling_size() {
assert!(matches!(
write_op(256, 512).validate_bounds(512),
Err(AsyncIoError::WriteVectored(_))
));
}
#[test]
fn rejects_offset_at_size() {
assert!(read_op(512, 1).validate_bounds(512).is_err());
}
}

View File

@@ -0,0 +1,174 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::{fmt, io, slice};
// Storage owned by an async I/O request for host-memory buffers.
//
// `Vec` is used when ordinary vector storage is sufficient. `Aligned` is used
// when the backend requires an alignment that a normal `Vec` cannot
// guarantee.
enum OwnedIoBufferStorage {
// Buffer backed by a standard `Vec<u8>`.
Vec(Vec<u8>),
// Buffer backed by an explicitly aligned allocation.
Aligned {
// Pointer returned by `alloc_zeroed` for `layout`.
ptr: *mut u8,
// Layout used to allocate and deallocate `ptr`.
layout: Layout,
// Logical buffer length exposed to I/O.
len: usize,
},
}
// SAFETY: OwnedIoBufferStorage owns its allocation exclusively. Moving it to
// another thread transfers that ownership.
unsafe impl Send for OwnedIoBufferStorage {}
impl OwnedIoBufferStorage {
fn new(len: usize, alignment: usize) -> io::Result<Self> {
if alignment <= 1 {
return Ok(Self::Vec(vec![0; len]));
}
let alloc_len = len.max(1).next_multiple_of(alignment);
let layout = Layout::from_size_align(alloc_len, alignment)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
// SAFETY: layout has non-zero size because alloc_len is at least 1.
let ptr = unsafe { alloc_zeroed(layout) };
if ptr.is_null() {
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"alloc_zeroed returned null",
));
}
Ok(Self::Aligned { ptr, layout, len })
}
fn as_mut_ptr(&mut self) -> *mut u8 {
match self {
Self::Vec(buf) => buf.as_mut_ptr(),
Self::Aligned { ptr, .. } => *ptr,
}
}
fn as_slice(&self) -> &[u8] {
match self {
Self::Vec(buf) => buf.as_slice(),
Self::Aligned { ptr, len, .. } => {
// SAFETY: alloc_zeroed initialized `len` bytes at `ptr` and the
// allocation is owned by Self.
unsafe { slice::from_raw_parts(*ptr, *len) }
}
}
}
fn as_mut_slice(&mut self) -> &mut [u8] {
match self {
Self::Vec(buf) => buf.as_mut_slice(),
Self::Aligned { ptr, len, .. } => {
// SAFETY: alloc_zeroed initialized `len` bytes at `ptr`,
// &mut self ensures unique access, and the allocation is
// owned by Self.
unsafe { slice::from_raw_parts_mut(*ptr, *len) }
}
}
}
}
impl Drop for OwnedIoBufferStorage {
fn drop(&mut self) {
if let Self::Aligned { ptr, layout, .. } = self {
// SAFETY: ptr was allocated by alloc_zeroed with this layout and is
// solely owned by Self.
unsafe { dealloc(*ptr, *layout) };
}
}
}
impl fmt::Debug for OwnedIoBufferStorage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Vec(buf) => f.debug_tuple("Vec").field(&buf.len()).finish(),
Self::Aligned { len, layout, .. } => f
.debug_struct("Aligned")
.field("len", len)
.field("layout", layout)
.finish(),
}
}
}
/// Owns host-memory buffer storage and the iovec array that points into it.
///
/// The retained iovec is valid for as long as this value is alive.
/// When used for Async I/O this struct must remain valid for the duration of the op.
#[derive(Debug)]
pub struct OwnedIoBuffer {
storage: OwnedIoBufferStorage,
iovecs: Vec<libc::iovec>,
}
// SAFETY: OwnedIoBuffer owns the storage referenced by its single iovec, and moving the buffer
// keeps the allocation address stable.
unsafe impl Send for OwnedIoBuffer {}
impl OwnedIoBuffer {
/// Creates a zeroed buffer with the requested logical length and alignment.
///
/// An alignment of 0 or 1 uses ordinary `Vec` storage. Larger alignments use an explicitly
/// aligned allocation whose allocated size may be rounded up while the exposed slice length
/// remains `len`.
pub fn new(len: usize, alignment: usize) -> io::Result<Self> {
let mut storage = OwnedIoBufferStorage::new(len, alignment)?;
let iovec = libc::iovec {
iov_base: storage.as_mut_ptr().cast(),
iov_len: len,
};
Ok(Self {
storage,
iovecs: vec![iovec],
})
}
/// Creates an owned I/O buffer from an existing `Vec<u8>`.
///
/// The generated iovec covers the full vector length and remains valid
/// until the OwnedIoBuffer is dropped.
pub fn from_vec(mut buf: Vec<u8>) -> Self {
let iovec = libc::iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
};
Self {
storage: OwnedIoBufferStorage::Vec(buf),
iovecs: vec![iovec],
}
}
/// Returns the logical buffer contents.
pub fn as_slice(&self) -> &[u8] {
self.storage.as_slice()
}
/// Returns the logical buffer contents mutably.
pub fn as_mut_slice(&mut self) -> &mut [u8] {
self.storage.as_mut_slice()
}
/// Returns the retained iovec array for kernel submission.
///
/// The iovec pointers remain valid while this buffer is alive.
pub fn iovecs(&self) -> &[libc::iovec] {
&self.iovecs
}
/// Returns the total number of bytes described by the retained iovecs.
pub fn total_len(&self) -> usize {
self.iovecs.iter().map(|iov| iov.iov_len).sum()
}
}

View File

@@ -0,0 +1,426 @@
// Copyright © 2021 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::collections::{HashMap, VecDeque};
use std::os::fd::{AsRawFd, RawFd};
use std::{io, mem};
use io_uring::{IoUring, opcode, squeue, types};
use log::{error, warn};
use vmm_sys_util::eventfd::EventFd;
use super::common::{duplicate_user_data_error, validate_batch};
use super::{AsyncIoCompletion, AsyncIoOperation};
/// `io_uring` wrapper for async I/O.
///
/// Holds the `IoUring` and its `EventFd`. Tracks ops that are pending.
pub struct UringDataIo {
io_uring: IoUring,
// The `EventFd` for completion signals.
eventfd: EventFd,
// `in_flight` tracks every user_data value accepted by the kernel. Owned
// data operations store `Some(op)` so their iovecs and backing buffers
// remain valid until completion; metadata operations store `None`.
in_flight: HashMap<u64, Option<AsyncIoOperation>>,
// `injected` holds locally produced completions so synchronous failures
// and short-circuited requests use the same drain path as kernel CQEs.
injected: VecDeque<AsyncIoCompletion>,
// `needs_submit_retry` is set when SQEs have been published to the ring,
// but the submit syscall failed before confirming kernel ownership.
needs_submit_retry: bool,
}
impl UringDataIo {
/// Creates an io_uring queue and registers its completion eventfd.
pub fn new(ring_depth: u32) -> io::Result<Self> {
let io_uring = IoUring::new(ring_depth)?;
let eventfd = EventFd::new(libc::EFD_NONBLOCK)?;
io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?;
Ok(Self {
io_uring,
eventfd,
in_flight: HashMap::new(),
injected: VecDeque::new(),
needs_submit_retry: false,
})
}
/// Returns the eventfd signaled when completions are available.
pub fn notifier(&self) -> &EventFd {
&self.eventfd
}
/// Submits one owned read or write operation to the queue.
pub fn submit_operation(&mut self, fd: RawFd, op: AsyncIoOperation) -> io::Result<()> {
self.submit_batch(fd, vec![op])
}
fn reserve_user_data(&mut self, user_data: u64) -> io::Result<()> {
if self.in_flight.contains_key(&user_data) {
return Err(duplicate_user_data_error(user_data));
}
self.in_flight.insert(user_data, None);
Ok(())
}
fn submit_kernel_entry(&mut self, user_data: u64, entry: &squeue::Entry) -> io::Result<()> {
self.reserve_user_data(user_data)?;
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: the entry has no caller-owned buffer. `user_data` is retained
// in `in_flight` until the CQE is consumed.
if let Err(e) = unsafe { sq.push(entry) } {
self.in_flight.remove(&user_data);
return Err(io::Error::other(format!("Submission queue is full: {e:?}")));
}
sq.sync();
match submitter.submit() {
Ok(_) => self.needs_submit_retry = false,
Err(e) => {
self.needs_submit_retry = true;
warn!("io_uring submit failed after SQE was published: {e}");
self.eventfd.write(1).unwrap();
}
}
Ok(())
}
/// Submits a batch of owned read and write operations.
///
/// If the io_uring submission queue cannot accept the whole batch, each
/// operation is completed locally with `-EAGAIN` so callers can observe
/// every request through the normal completion path.
pub fn submit_batch(&mut self, fd: RawFd, batch: Vec<AsyncIoOperation>) -> io::Result<()> {
if batch.is_empty() {
return Ok(());
}
validate_batch(|user_data| self.in_flight.contains_key(&user_data), &batch)?;
let (submitter, mut sq, _) = self.io_uring.split();
let available = sq.capacity() - sq.len();
if batch.len() > available {
// Not enough space for the batch.
// Drop sq, which will re-publish an unmodified tail pointer
drop(sq);
for op in batch {
self.injected
.push_back(AsyncIoCompletion::from_operation(op, -libc::EAGAIN));
}
self.eventfd.write(1).unwrap();
return Ok(());
}
let mut signal_completion = false;
let mut batch = batch.into_iter();
while let Some(op) = batch.next() {
let user_data = op.user_data();
let entry = Self::build_entry(fd, &op);
self.in_flight.insert(user_data, Some(op));
// SAFETY: the SQ capacity was just checked. Every iovec's pointer is retained in
// self.in_flight before the SQ tail is advanced by sync or drop. in_flight only
// drops the memory after a completion.
if let Err(e) = unsafe { sq.push(&entry) } {
Self::handle_push_failure(
&mut self.in_flight,
&mut self.injected,
user_data,
batch.by_ref(),
&e,
);
signal_completion = true;
break;
}
}
sq.sync();
match submitter.submit() {
Ok(_) => self.needs_submit_retry = false,
Err(e) => {
self.needs_submit_retry = true;
warn!("io_uring submit failed after SQEs were published: {e}");
signal_completion = true;
}
}
if signal_completion {
self.eventfd.write(1).unwrap();
}
Ok(())
}
#[cold]
fn handle_push_failure(
in_flight: &mut HashMap<u64, Option<AsyncIoOperation>>,
injected: &mut VecDeque<AsyncIoCompletion>,
user_data: u64,
remaining: impl Iterator<Item = AsyncIoOperation>,
error: &squeue::PushError,
) {
// Since capacity was just checked, this should only happen if the ring
// state changed unexpectedly. Keep all affected operations memory safe
// by returning local completions through the normal path.
let op = in_flight
.remove(&user_data)
.flatten()
.expect("pending operation missing after failed push");
injected.push_back(AsyncIoCompletion::from_operation(op, -libc::EAGAIN));
for op in remaining {
injected.push_back(AsyncIoCompletion::from_operation(op, -libc::EAGAIN));
}
warn!("io_uring submission queue became full after capacity check: {error:?}");
}
fn build_entry(fd: RawFd, op: &AsyncIoOperation) -> squeue::Entry {
let iovecs = op.iovecs();
let fd = types::Fd(fd);
if op.is_read() {
opcode::Readv::new(fd, iovecs.as_ptr(), iovecs.len() as u32)
.offset(op.offset() as u64)
.build()
.user_data(op.user_data())
} else {
opcode::Writev::new(fd, iovecs.as_ptr(), iovecs.len() as u32)
.offset(op.offset() as u64)
.build()
.user_data(op.user_data())
}
}
/// Submits an io_uring NOP carrying `user_data`.
pub fn submit_nop(&mut self, user_data: u64) -> io::Result<()> {
self.submit_kernel_entry(user_data, &opcode::Nop::new().build().user_data(user_data))
}
/// Submits an fsync operation carrying `user_data`.
pub fn submit_fsync(&mut self, fd: RawFd, user_data: u64) -> io::Result<()> {
self.submit_kernel_entry(
user_data,
&opcode::Fsync::new(types::Fd(fd))
.build()
.user_data(user_data),
)
}
/// Submits a fallocate operation carrying `user_data`.
pub fn submit_fallocate(
&mut self,
fd: RawFd,
offset: u64,
length: u64,
mode: i32,
user_data: u64,
) -> io::Result<()> {
self.submit_kernel_entry(
user_data,
&opcode::Fallocate::new(types::Fd(fd), length)
.offset(offset)
.mode(mode)
.build()
.user_data(user_data),
)
}
/// Injects a completion that did not come from a kernel CQE.
///
/// The notifier is signaled so callers can drain it with
/// [`Self::next_completion`].
pub fn inject_completion(&mut self, completion: AsyncIoCompletion) {
self.injected.push_back(completion);
self.eventfd.write(1).unwrap();
}
/// Returns the next kernel or injected completion if one is available.
///
/// Consuming a kernel completion returns ownership of any buffer retained
/// by the corresponding operation.
pub fn next_completion(&mut self) -> Option<AsyncIoCompletion> {
if self.needs_submit_retry {
match self.io_uring.submitter().submit() {
Ok(_) => self.needs_submit_retry = false,
Err(e) => warn!("io_uring retry submit failed for retained SQEs: {e}"),
}
}
if let Some(entry) = self.io_uring.completion().next() {
let user_data = entry.user_data();
return Some(AsyncIoCompletion::new(
user_data,
entry.result(),
self.in_flight
.remove(&user_data)
.flatten()
.and_then(AsyncIoOperation::into_completion_buffer),
));
}
self.injected.pop_front()
}
}
impl Drop for UringDataIo {
fn drop(&mut self) {
// Closing the ring fd does not cancel io_uring ops that have started.
// Wait for CQEs before releasing retained iovecs.
if self.needs_submit_retry {
if let Err(e) = self.io_uring.submitter().submit() {
warn!("io_uring drain submit failed for retained SQEs: {e}");
}
self.needs_submit_retry = false;
}
let max_drain_iterations = self.in_flight.len().saturating_mul(2);
let mut drain_iterations = 0;
while !self.in_flight.is_empty() {
if drain_iterations == max_drain_iterations {
error!(
"io_uring drain abandoned with {} operations still in flight after {} drain iterations",
self.in_flight.len(),
drain_iterations
);
// Keep retained buffers mapped if the ring cannot be drained.
mem::forget(mem::take(&mut self.in_flight));
break;
}
drain_iterations += 1;
if let Some(entry) = self.io_uring.completion().next() {
self.in_flight.remove(&entry.user_data());
continue;
}
// No completion ready: block in the kernel until at least one is.
if let Err(e) = self.io_uring.submitter().submit_and_wait(1) {
if e.kind() == io::ErrorKind::Interrupted {
continue;
}
error!(
"io_uring drain abandoned with {} operations still in flight: {e}",
self.in_flight.len()
);
// Keep retained buffers mapped if the ring cannot be drained.
mem::forget(mem::take(&mut self.in_flight));
break;
}
}
}
}
#[cfg(test)]
mod tests {
use std::io;
use std::os::fd::AsRawFd;
use std::thread::sleep;
use std::time::Duration;
use vmm_sys_util::tempfile::TempFile;
use super::UringDataIo;
use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, OwnedIoBuffer};
fn wait_for_completion(data_io: &mut UringDataIo) -> AsyncIoCompletion {
for _ in 0..1000 {
if let Some(completion) = data_io.next_completion() {
return completion;
}
sleep(Duration::from_millis(1));
}
panic!("timed out waiting for io_uring completion");
}
#[test]
fn uring_rejects_duplicate_user_data_for_metadata_ops() {
let file = TempFile::new().unwrap().into_file();
file.set_len(512).unwrap();
let fd = file.as_raw_fd();
let mut data_io = UringDataIo::new(8).unwrap();
data_io
.submit_operation(
fd,
AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), 7),
)
.unwrap();
assert_eq!(
data_io.submit_fsync(fd, 7).unwrap_err().kind(),
io::ErrorKind::AlreadyExists
);
assert_eq!(
data_io.submit_nop(7).unwrap_err().kind(),
io::ErrorKind::AlreadyExists
);
assert_eq!(
data_io
.submit_fallocate(fd, 0, 512, 0, 7)
.unwrap_err()
.kind(),
io::ErrorKind::AlreadyExists
);
let completion = wait_for_completion(&mut data_io);
assert_eq!(completion.user_data, 7);
assert_eq!(completion.result, 512);
}
#[test]
fn uring_drop_drains_in_flight_operations() {
let file = TempFile::new().unwrap().into_file();
file.set_len(8192).unwrap();
let fd = file.as_raw_fd();
let mut data_io = UringDataIo::new(8).unwrap();
for user_data in 0..4 {
data_io
.submit_operation(
fd,
AsyncIoOperation::read_to_vec(
0,
OwnedIoBuffer::from_vec(vec![0; 512]),
user_data,
),
)
.unwrap();
}
drop(data_io);
}
#[test]
fn uring_queue_full_batch_completes_each_operation() {
let file = TempFile::new().unwrap().into_file();
let fd = file.as_raw_fd();
let mut data_io = UringDataIo::new(1).unwrap();
let available = {
let (_, sq, _) = data_io.io_uring.split();
sq.capacity() - sq.len()
};
let batch_len = available + 1;
let batch: Vec<_> = (0..batch_len as u64)
.map(|user_data| {
AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), user_data)
})
.collect();
data_io.submit_batch(fd, batch).unwrap();
let mut completed = Vec::new();
while let Some(completion) = data_io.next_completion() {
assert_eq!(completion.result, -libc::EAGAIN);
assert!(completion.buffer.is_some());
completed.push(completion.user_data);
}
completed.sort_unstable();
assert_eq!(completed, (0..batch_len as u64).collect::<Vec<_>>());
}
}

View File

@@ -34,7 +34,7 @@ pub enum LockError {
}
/// Commands for use with [`fcntl`].
#[allow(non_camel_case_types)]
#[expect(non_camel_case_types)]
enum FcntlArg<'a> {
/// Set an OFD lock from the given lock description.
F_OFD_SETLK(&'a libc::flock),
@@ -201,20 +201,23 @@ pub fn try_acquire_lock<Fd: AsRawFd>(
) -> Result<(), LockError> {
let flock = get_flock(lock_type, granularity);
let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock));
match res {
0 => Ok(()),
-1 => {
let io_error = io::Error::last_os_error();
let errno = io_error.raw_os_error().unwrap();
match errno {
// See man page for error code:
// <https://man7.org/linux/man-pages/man2/fcntl.2.html>
libc::EAGAIN | libc::EACCES => Err(LockError::AlreadyLocked),
_ => Err(LockError::Io(io_error)),
loop {
let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock));
match res {
0 => return Ok(()),
-1 => {
let io_error = io::Error::last_os_error();
let errno = io_error.raw_os_error().unwrap();
match errno {
// See man page for error code:
// <https://man7.org/linux/man-pages/man2/fcntl.2.html>
libc::EAGAIN | libc::EACCES => return Err(LockError::AlreadyLocked),
libc::EINTR => continue,
_ => return Err(LockError::Io(io_error)),
}
}
val => panic!("Unexpected return value from fcntl(): {val}"),
}
val => panic!("Unexpected return value from fcntl(): {val}"),
}
}

12
block/src/io/mod.rs Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Shared I/O infrastructure for all disk format backends.
//!
//! Contains the async I/O trait, request handling, and file locking
//! helpers.
pub mod async_io;
pub mod fcntl;
pub mod request;

View File

@@ -6,10 +6,13 @@
//
// Copyright © 2020 Intel Corporation
//
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::io::{Read, Seek, SeekFrom, Write};
use std::mem;
use std::os::unix::fs::FileExt;
use std::sync::Arc;
use std::time::Instant;
use log::{error, warn};
@@ -24,10 +27,13 @@ use vm_memory::{
Address as _, Bytes as _, GuestAddress, GuestMemory as _, GuestMemoryError,
GuestMemoryLoadGuard,
};
use vm_virtio::{AccessPlatform, Translatable as _};
use vm_virtio::AccessPlatform;
use vm_virtio::checked_descriptor::DescriptorChainExt;
use vmm_sys_util::file_traits::FileSync;
use crate::aligned_operation::AlignedOperation;
use crate::async_io::AsyncIo;
use crate::async_io::{
AsyncIo, AsyncIoCompletion, AsyncIoOperation, GuestMemoryTarget, OwnedIoBuffer,
};
use crate::{Error, ExecuteError, request_type, sector};
const SECTOR_SHIFT: u8 = 9;
@@ -36,7 +42,7 @@ pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT;
/// Maximum number of segments per DISCARD or WRITE_ZEROES request.
pub const MAX_DISCARD_WRITE_ZEROES_SEG: u32 = 1;
/// Size and field offsets within `struct virtio_blk_discard_write_zeroes`.
const DISCARD_WZ_SEG_SIZE: u32 = mem::size_of::<virtio_blk_discard_write_zeroes>() as u32;
const DISCARD_WZ_SEG_SIZE: u32 = size_of::<virtio_blk_discard_write_zeroes>() as u32;
const DISCARD_WZ_MAX_PAYLOAD: u32 = DISCARD_WZ_SEG_SIZE * MAX_DISCARD_WRITE_ZEROES_SEG;
const DISCARD_WZ_SECTOR_OFFSET: u64 =
mem::offset_of!(virtio_blk_discard_write_zeroes, sector) as u64;
@@ -56,18 +62,12 @@ pub enum RequestType {
}
pub const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32;
pub struct BatchRequest {
pub offset: libc::off_t,
pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
pub user_data: u64,
pub request_type: RequestType,
}
pub struct ExecuteAsync {
// `true` if the execution will complete asynchronously
pub async_complete: bool,
// request need to be batched for submission if any
pub batch_request: Option<BatchRequest>,
pub batch_request: Option<AsyncIoOperation>,
}
#[derive(Debug)]
@@ -77,7 +77,6 @@ pub struct Request {
data_descriptors: SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]>,
status_addr: GuestAddress,
pub writeback: bool,
aligned_operations: SmallVec<[AlignedOperation; DEFAULT_DESCRIPTOR_VEC_SIZE]>,
start: Instant,
}
@@ -87,10 +86,11 @@ impl Request {
access_platform: Option<&dyn AccessPlatform>,
) -> Result<Request, Error> {
let hdr_desc = desc_chain
.next()
.ok_or(Error::DescriptorChainTooShort)
.inspect_err(|_| {
.next_checked(access_platform)
.map_err(|addr| Error::GuestMemory(GuestMemoryError::InvalidGuestAddress(addr)))?
.ok_or_else(|| {
error!("Missing head descriptor");
Error::DescriptorChainTooShort
})?;
// The head contains the request type which MUST be readable.
@@ -98,10 +98,7 @@ impl Request {
return Err(Error::UnexpectedWriteOnlyDescriptor);
}
let hdr_desc_addr = hdr_desc
.addr()
.translate_gva(access_platform, hdr_desc.len() as usize)
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?;
let hdr_desc_addr = hdr_desc.addr();
let mut req = Request {
request_type: request_type(desc_chain.memory(), hdr_desc_addr)?,
@@ -109,16 +106,16 @@ impl Request {
data_descriptors: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE),
status_addr: GuestAddress(0),
writeback: true,
aligned_operations: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE),
start: Instant::now(),
};
let status_desc;
let mut desc = desc_chain
.next()
.ok_or(Error::DescriptorChainTooShort)
.inspect_err(|_| {
.next_checked(access_platform)
.map_err(|addr| Error::GuestMemory(GuestMemoryError::InvalidGuestAddress(addr)))?
.ok_or_else(|| {
error!("Only head descriptor present: request = {req:?}");
Error::DescriptorChainTooShort
})?;
if desc.has_next() {
@@ -140,17 +137,15 @@ impl Request {
return Err(Error::UnexpectedReadOnlyDescriptor);
}
req.data_descriptors.push((
desc.addr()
.translate_gva(access_platform, desc.len() as usize)
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?,
desc.len(),
));
req.data_descriptors.push((desc.addr(), desc.len()));
desc = desc_chain
.next()
.ok_or(Error::DescriptorChainTooShort)
.inspect_err(|_| {
.next_checked(access_platform)
.map_err(|addr| {
Error::GuestMemory(GuestMemoryError::InvalidGuestAddress(addr))
})?
.ok_or_else(|| {
error!("DescriptorChain corrupted: request = {req:?}");
Error::DescriptorChainTooShort
})?;
}
status_desc = desc;
@@ -168,19 +163,16 @@ impl Request {
return Err(Error::UnexpectedReadOnlyDescriptor);
}
if status_desc.len() < 1 {
if status_desc.is_empty() {
return Err(Error::DescriptorLengthTooSmall);
}
req.status_addr = status_desc
.addr()
.translate_gva(access_platform, status_desc.len() as usize)
.map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?;
req.status_addr = status_desc.addr();
Ok(req)
}
pub fn execute<T: Seek + Read + Write, B: Bitmap + 'static>(
pub fn execute<T: FileExt + FileSync, B: Bitmap + 'static>(
&self,
disk: &mut T,
disk_nsectors: u64,
@@ -189,32 +181,35 @@ impl Request {
) -> Result<u32, ExecuteError> {
self.check_data_bounds(disk_nsectors)?;
disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT))
.map_err(ExecuteError::Seek)?;
let mut offset = self.sector << SECTOR_SHIFT;
let mut len = 0;
for (data_addr, data_len) in &self.data_descriptors {
match self.request_type {
RequestType::In => {
let mut buf = vec![0u8; *data_len as usize];
disk.read_exact(&mut buf).map_err(ExecuteError::ReadExact)?;
disk.read_exact_at(&mut buf, offset)
.map_err(ExecuteError::ReadExact)?;
mem.read_exact_volatile_from(
*data_addr,
&mut buf.as_slice(),
*data_len as usize,
)
.map_err(ExecuteError::Read)?;
offset += u64::from(*data_len);
len += data_len;
}
RequestType::Out => {
let mut buf: Vec<u8> = Vec::new();
mem.write_all_volatile_to(*data_addr, &mut buf, *data_len as usize)
.map_err(ExecuteError::Write)?;
disk.write_all(&buf).map_err(ExecuteError::WriteAll)?;
disk.write_all_at(&buf, offset)
.map_err(ExecuteError::WriteAll)?;
if !self.writeback {
disk.flush().map_err(ExecuteError::Flush)?;
disk.fsync().map_err(ExecuteError::Flush)?;
}
offset += u64::from(*data_len);
}
RequestType::Flush => disk.flush().map_err(ExecuteError::Flush)?,
RequestType::Flush => disk.fsync().map_err(ExecuteError::Flush)?,
RequestType::GetDeviceId => {
if (*data_len as usize) < serial.len() {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
@@ -234,9 +229,9 @@ impl Request {
Ok(len)
}
pub fn execute_async<B: Bitmap + 'static>(
pub fn execute_async<B: Bitmap + Send + Sync + 'static>(
&mut self,
mem: &vm_memory::GuestMemoryMmap<B>,
mem: Arc<vm_memory::GuestMemoryMmap<B>>,
disk_nsectors: u64,
disk_image: &mut dyn AsyncIo,
serial: &[u8],
@@ -250,56 +245,6 @@ impl Request {
self.check_data_bounds(disk_nsectors)?;
let mut iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
SmallVec::with_capacity(self.data_descriptors.len());
for &(data_addr, data_len) in &self.data_descriptors {
let _: u32 = data_len; // compiler-checked documentation
const _: () = assert!(
core::mem::size_of::<u32>() <= core::mem::size_of::<usize>(),
"unsupported platform"
);
if data_len == 0 {
continue;
}
let data_len = data_len as usize;
let origin_ptr = mem
.get_slice(data_addr, data_len)
.map_err(ExecuteError::GetHostAddress)?;
assert!(origin_ptr.len() >= data_len);
let origin_ptr = origin_ptr.ptr_guard_mut();
// O_DIRECT requires buffer addresses to be aligned to the
// backend device's logical block size. In case it's not properly
// aligned, an intermediate buffer is created with the correct
// alignment, and a copy from/to the origin buffer is performed,
// depending on the type of operation.
let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(alignment) {
origin_ptr.as_ptr().cast()
} else {
let mut aligned_op = AlignedOperation::new(data_addr, data_len, alignment as usize)
.map_err(ExecuteError::TemporaryBufferAllocation)?;
// We need to perform the copy beforehand in case we're writing
// data out.
if request_type == RequestType::Out {
mem.read_slice(aligned_op.as_bytes_mut(), data_addr)
.map_err(ExecuteError::Read)?;
}
let aligned_ptr = aligned_op.as_mut_ptr();
self.aligned_operations.push(aligned_op);
aligned_ptr.cast()
};
let iovec = libc::iovec {
iov_base,
iov_len: data_len as libc::size_t,
};
iovecs.push(iovec);
}
let mut ret = ExecuteAsync {
async_complete: true,
batch_request: None,
@@ -307,37 +252,52 @@ impl Request {
// Queue operations expected to be submitted.
match request_type {
RequestType::In => {
for (data_addr, data_len) in &self.data_descriptors {
mem.get_slice(*data_addr, *data_len as usize)
.map_err(ExecuteError::GetHostAddress)?
.bitmap()
.mark_dirty(0, *data_len as usize);
}
self.mark_read_dirty(&mem)?;
let op = self.build_data_operation(mem, offset, alignment, user_data)?;
if disk_image.batch_requests_enabled() {
ret.batch_request = Some(BatchRequest {
offset,
iovecs,
user_data,
request_type,
});
ret.batch_request = Some(op);
} else {
disk_image
.read_vectored(offset, &iovecs, user_data)
.map_err(ExecuteError::AsyncRead)?;
match op {
AsyncIoOperation::ReadToMemory {
offset,
target,
user_data,
} => disk_image
.read_to_memory(offset, target, user_data)
.map_err(ExecuteError::AsyncRead)?,
AsyncIoOperation::ReadToVec {
offset,
buffer,
user_data,
} => disk_image
.read_to_vec(offset, buffer, user_data)
.map_err(ExecuteError::AsyncRead)?,
_ => unreachable!("unexpected read operation"),
}
}
}
RequestType::Out => {
let op = self.build_data_operation(mem, offset, alignment, user_data)?;
if disk_image.batch_requests_enabled() {
ret.batch_request = Some(BatchRequest {
offset,
iovecs,
user_data,
request_type,
});
ret.batch_request = Some(op);
} else {
disk_image
.write_vectored(offset, &iovecs, user_data)
.map_err(ExecuteError::AsyncWrite)?;
match op {
AsyncIoOperation::WriteFromMemory {
offset,
target,
user_data,
} => disk_image
.write_from_memory(offset, target, user_data)
.map_err(ExecuteError::AsyncWrite)?,
AsyncIoOperation::WriteFromVec {
offset,
buffer,
user_data,
} => disk_image
.write_from_vec(offset, buffer, user_data)
.map_err(ExecuteError::AsyncWrite)?,
_ => unreachable!("unexpected write operation"),
}
}
}
RequestType::Flush => {
@@ -472,11 +432,6 @@ impl Request {
});
}
let wz_offset = wz_sector * SECTOR_SIZE;
if wz_offset == 0 && disable_sector0_writes {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
}
let top = wz_sector
.checked_add(wz_num_sectors as u64)
.ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?;
@@ -484,6 +439,11 @@ impl Request {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
}
let wz_offset = wz_sector * SECTOR_SIZE;
if wz_offset == 0 && disable_sector0_writes {
return Err(ExecuteError::BadRequest(Error::InvalidOffset));
}
let wz_length = (wz_num_sectors as u64) * SECTOR_SIZE;
if wz_flags & VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP != 0 {
@@ -502,17 +462,141 @@ impl Request {
Ok(ret)
}
// Builds a read or write operation for IO to or from `mem`.
fn build_data_operation<B: Bitmap + Send + Sync + 'static>(
&self,
mem: Arc<vm_memory::GuestMemoryMmap<B>>,
offset: libc::off_t,
alignment: u64,
user_data: u64,
) -> Result<AsyncIoOperation, ExecuteError> {
if self.guest_memory_is_aligned(&mem, alignment)? {
let target = GuestMemoryTarget::new(mem, &self.data_descriptors)
.map_err(ExecuteError::GetHostAddress)?;
return Ok(match self.request_type {
RequestType::In => AsyncIoOperation::read_to_memory(offset, target, user_data),
RequestType::Out => AsyncIoOperation::write_from_memory(offset, target, user_data),
_ => unreachable!("unexpected data operation type"),
});
}
// The guest-memory buffers are unaligned, so use an aligned bounce buffer.
let mut buffer = OwnedIoBuffer::new(self.data_len(), alignment as usize)
.map_err(ExecuteError::TemporaryBufferAllocation)?;
if self.request_type == RequestType::Out {
self.copy_guest_to_buffer(&mem, buffer.as_mut_slice())?;
}
Ok(match self.request_type {
RequestType::In => AsyncIoOperation::read_to_vec(offset, buffer, user_data),
RequestType::Out => AsyncIoOperation::write_from_vec(offset, buffer, user_data),
_ => unreachable!("unexpected data operation type"),
})
}
// Checks whether `self.data_descriptors` are aligned to `alignment`.
fn guest_memory_is_aligned<B: Bitmap + 'static>(
&self,
mem: &vm_memory::GuestMemoryMmap<B>,
alignment: u64,
) -> Result<bool, ExecuteError> {
if alignment <= 1 {
return Ok(true);
}
for &(data_addr, data_len) in &self.data_descriptors {
let _: u32 = data_len;
const _: () = assert!(
size_of::<u32>() <= size_of::<usize>(),
"unsupported platform"
);
if data_len == 0 {
continue;
}
let data_len = data_len as usize;
let origin_ptr = mem
.get_slice(data_addr, data_len)
.map_err(ExecuteError::GetHostAddress)?;
let origin_ptr = origin_ptr.ptr_guard_mut();
if !(origin_ptr.as_ptr() as u64).is_multiple_of(alignment)
|| !(origin_ptr.len() as u64).is_multiple_of(alignment)
{
return Ok(false);
}
}
Ok(true)
}
// Returns the sum of the lengths of `self.data_descriptors`.
fn data_len(&self) -> usize {
self.data_descriptors
.iter()
.map(|(_, len)| *len as usize)
.sum()
}
// Marks guest-memory read destinations dirty before submitting async IO.
fn mark_read_dirty<B: Bitmap + 'static>(
&self,
mem: &vm_memory::GuestMemoryMmap<B>,
) -> Result<(), ExecuteError> {
for (data_addr, data_len) in &self.data_descriptors {
mem.get_slice(*data_addr, *data_len as usize)
.map_err(ExecuteError::GetHostAddress)?
.bitmap()
.mark_dirty(0, *data_len as usize);
}
Ok(())
}
// Copies guest descriptor contents into a contiguous host buffer.
fn copy_guest_to_buffer<B: Bitmap + 'static>(
&self,
mem: &vm_memory::GuestMemoryMmap<B>,
buffer: &mut [u8],
) -> Result<(), ExecuteError> {
let mut offset = 0usize;
for (data_addr, data_len) in &self.data_descriptors {
let data_len = *data_len as usize;
mem.read_slice(&mut buffer[offset..offset + data_len], *data_addr)
.map_err(ExecuteError::Read)?;
offset += data_len;
}
Ok(())
}
// Copies a host completion buffer back into guest descriptors.
fn copy_buffer_to_guest<B: Bitmap + 'static>(
&self,
mem: &vm_memory::GuestMemoryMmap<B>,
buffer: &[u8],
) -> Result<(), Error> {
let mut buffer_offset = 0usize;
for (data_addr, data_len) in &self.data_descriptors {
if buffer_offset >= buffer.len() {
break;
}
let data_len = (*data_len as usize).min(buffer.len() - buffer_offset);
mem.write_slice(&buffer[buffer_offset..buffer_offset + data_len], *data_addr)
.map_err(Error::GuestMemory)?;
buffer_offset += data_len;
}
Ok(())
}
pub fn complete_async<B: Bitmap + 'static>(
&mut self,
mem: &vm_memory::GuestMemoryMmap<B>,
completion: &mut AsyncIoCompletion,
) -> Result<(), Error> {
for aligned_op in self.aligned_operations.drain(..) {
// We need to perform the copy after the data has been read inside
// the aligned buffer in case we're reading data in.
if self.request_type == RequestType::In {
mem.write_slice(aligned_op.as_bytes(), aligned_op.data_addr())
.map_err(Error::GuestMemory)?;
}
if self.request_type == RequestType::In
&& completion.result > 0
&& let Some(buffer) = completion.buffer.take()
{
let len = (completion.result as usize).min(buffer.as_slice().len());
self.copy_buffer_to_guest(mem, &buffer.as_slice()[..len])?;
}
Ok(())
@@ -560,7 +644,10 @@ impl Request {
if total_bytes == 0 {
return Ok(());
}
let total_sectors = total_bytes.div_ceil(SECTOR_SIZE);
if !total_bytes.is_multiple_of(SECTOR_SIZE) {
return Err(ExecuteError::BadRequest(Error::InvalidDataLength));
}
let total_sectors = total_bytes / SECTOR_SIZE;
let end_sector = self
.sector
.checked_add(total_sectors)
@@ -571,3 +658,62 @@ impl Request {
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
use std::sync::Arc;
use vm_memory::GuestMemoryMmap;
use vmm_sys_util::eventfd::EventFd;
use super::*;
use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoOperation, AsyncIoResult};
struct PanicAsyncIo(EventFd);
impl AsyncIo for PanicAsyncIo {
fn notifier(&self) -> &EventFd {
&self.0
}
fn submit_data_operation(&mut self, _: AsyncIoOperation) -> AsyncIoResult<()> {
unreachable!()
}
fn fsync(&mut self, _: Option<u64>) -> AsyncIoResult<()> {
unreachable!()
}
fn punch_hole(&mut self, _: u64, _: u64, _: u64) -> AsyncIoResult<()> {
unreachable!()
}
fn write_zeroes(&mut self, _: u64, _: u64, _: u64) -> AsyncIoResult<()> {
unreachable!()
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
None
}
}
#[test]
fn write_zeroes_rejects_sector_arithmetic_overflow() {
let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 4096)]).unwrap());
mem.write_slice(&(u64::MAX - 100).to_le_bytes(), GuestAddress(0))
.unwrap();
mem.write_slice(&1000u32.to_le_bytes(), GuestAddress(8))
.unwrap();
let mut request = Request {
request_type: RequestType::WriteZeroes,
sector: 0,
data_descriptors: SmallVec::from_slice(&[(GuestAddress(0), DISCARD_WZ_SEG_SIZE)]),
status_addr: GuestAddress(0),
writeback: true,
start: Instant::now(),
};
let mut disk = PanicAsyncIo(EventFd::new(0).unwrap());
let Err(ExecuteError::BadRequest(Error::InvalidOffset)) =
request.execute_async(mem, 1024, &mut disk, &[], false, 0)
else {
panic!("expected BadRequest(InvalidOffset)");
};
}
}

View File

@@ -8,75 +8,52 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
mod aligned_operation;
pub mod async_io;
pub mod disk_file;
pub mod error;
pub mod factory;
pub mod fcntl;
pub mod fixed_vhd;
#[cfg(feature = "io_uring")]
/// Enabled with the `"io_uring"` feature
pub mod fixed_vhd_async;
pub mod fixed_vhd_disk;
pub mod fixed_vhd_sync;
pub mod qcow;
#[cfg(feature = "io_uring")]
pub(crate) mod qcow_async;
pub(crate) mod qcow_common;
pub mod qcow_disk;
pub(crate) mod qcow_sync;
#[cfg(feature = "io_uring")]
pub(crate) mod raw_async;
pub(crate) mod raw_async_aio;
#[cfg(test)]
mod raw_async_io_tests;
pub mod raw_disk;
pub(crate) mod raw_sync;
mod request;
pub mod vhd;
pub mod vhdx;
pub mod vhdx_sync;
use std::alloc::{Layout, alloc_zeroed};
use std::collections::VecDeque;
#[path = "io/mod.rs"]
mod io_impl;
pub use io_impl::{async_io, fcntl, request};
pub(crate) mod aligned_buffer;
pub mod aligned_file;
pub mod formats;
mod sparse;
use std::fmt::{self, Debug};
use std::fs::{File, OpenOptions};
use std::io::{self, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};
use std::os::linux::fs::MetadataExt;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::io::AsRawFd;
use std::os::unix::fs::{FileExt, FileTypeExt};
use std::os::unix::io::{AsRawFd, RawFd};
use std::path::Path;
use std::str::FromStr;
use std::{cmp, mem, result};
use std::{cmp, io, mem, result};
pub use aligned_operation::AlignedOperation;
pub use aligned_file::AlignedFile;
use formats::qcow;
#[cfg(feature = "io_uring")]
use io_uring::{IoUring, Probe, opcode};
use libc::{
FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE, S_IFBLK, S_IFMT, ioctl,
};
use log::{debug, info, warn};
pub use request::{BatchRequest, ExecuteAsync, MAX_DISCARD_WRITE_ZEROES_SEG, Request, RequestType};
pub use request::{ExecuteAsync, MAX_DISCARD_WRITE_ZEROES_SEG, Request, RequestType};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
pub use sparse::{BLKDISCARD, BLKZEROOUT};
use thiserror::Error;
use virtio_bindings::virtio_blk::*;
use vm_memory::bitmap::Bitmap;
use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError};
use vmm_sys_util::eventfd::EventFd;
use vmm_sys_util::{aio, ioctl_io_nr, ioctl_ior_nr};
use crate::async_io::{AsyncIoError, AsyncIoResult};
use crate::async_io::AsyncIoError;
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::request::{DEFAULT_DESCRIPTOR_VEC_SIZE, SECTOR_SIZE};
use crate::vhdx::VhdxError;
use crate::formats::vhdx::VhdxError;
use crate::request::SECTOR_SIZE;
#[derive(Error, Debug)]
pub enum Error {
#[error("Guest gave us bad memory addresses")]
GuestMemory(#[source] GuestMemoryError),
#[error("Guest gave us offsets that would have overflowed a usize")]
#[error("Guest address {0:?} with sector offset {1} would overflow a usize")]
CheckedOffset(GuestAddress, usize /* sector offset */),
#[error("Guest gave us a write only descriptor that protocol says to read from")]
UnexpectedWriteOnlyDescriptor,
@@ -87,17 +64,17 @@ pub enum Error {
#[error("Guest gave us a descriptor that was too short to use")]
DescriptorLengthTooSmall,
#[error("Failed to detect image type")]
DetectImageType(#[source] std::io::Error),
DetectImageType(#[source] io::Error),
#[error("Failure in fixed vhd")]
FixedVhdError(#[source] std::io::Error),
FixedVhdError(#[source] io::Error),
#[error("Getting a block's metadata failed")]
GetFileMetadata(#[source] std::io::Error),
GetFileMetadata(#[source] io::Error),
#[error("The requested operation would cause a seek beyond disk end")]
InvalidOffset,
#[error("Request data length is not a multiple of the 512-byte sector size")]
InvalidDataLength,
#[error("Failure in qcow")]
QcowError(#[source] qcow::Error),
#[error("Failure in raw file")]
RawFileError(#[source] std::io::Error),
#[error("The requested operation does not support multiple descriptors")]
TooManyDescriptors,
#[error("Request contains too many segments ({0}, max {MAX_DISCARD_WRITE_ZEROES_SEG})")]
@@ -150,8 +127,6 @@ pub enum ExecuteError {
ReadExact(#[source] io::Error),
#[error("Can't execute an operation other than `read` or `get_id` on a read-only device")]
ReadOnly,
#[error("Failed to seek")]
Seek(#[source] io::Error),
#[error("Failed to write")]
Write(#[source] GuestMemoryError),
#[error("Failed to write_all")]
@@ -186,7 +161,6 @@ impl ExecuteError {
ExecuteError::Read(_) => VIRTIO_BLK_S_IOERR,
ExecuteError::ReadExact(_) => VIRTIO_BLK_S_IOERR,
ExecuteError::ReadOnly => VIRTIO_BLK_S_IOERR,
ExecuteError::Seek(_) => VIRTIO_BLK_S_IOERR,
ExecuteError::Write(_) => VIRTIO_BLK_S_IOERR,
ExecuteError::WriteAll(_) => VIRTIO_BLK_S_IOERR,
ExecuteError::Unsupported(_) => VIRTIO_BLK_S_UNSUPP,
@@ -332,26 +306,82 @@ pub fn block_io_uring_is_supported() -> bool {
}
}
/// Returns `true` iff `fd` refers to a block device.
///
/// Returns `false` if the `fstat()` probe itself fails. Callers that need to
/// distinguish "not a block device" from "couldn't tell" should fall back to
/// regular-file behaviour, which is what every current caller already does.
pub(crate) fn is_block_device(fd: RawFd) -> bool {
// SAFETY: `libc::stat` is POD; zero-initialization is a valid bit pattern
// and `fstat` overwrites every field it cares about on success.
let mut stat: libc::stat = unsafe { mem::zeroed() };
// SAFETY: FFI call with a valid fd and a valid out-pointer.
let ret = unsafe { libc::fstat(fd, &mut stat) };
ret == 0 && stat.st_mode & S_IFMT == S_IFBLK
}
/// Returns the kernel reported direct I/O alignment for `fd`, or `None`
/// when `fd` was not opened with O_DIRECT.
///
/// When O_DIRECT is set, uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain
/// the exact memory and offset alignment the kernel requires for direct I/O on
/// this specific fd. Unlike `fstatvfs().f_bsize`, which only returns the
/// filesystem's preferred I/O block size, `STATX_DIOALIGN` reports the true per
/// fd direct I/O constraint accounting for the filesystem, underlying block
/// device, and any stacking such as loop or device mapper. Falls back to
/// [`SECTOR_SIZE`] when the kernel does not report a value.
pub(crate) fn probe_direct_alignment(fd: RawFd) -> Option<u64> {
// SAFETY: fcntl(F_GETFL) is always safe on a valid fd.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0 || (flags & libc::O_DIRECT) == 0 {
return None;
}
// The libc crate does not expose statx / STATX_DIOALIGN on all targets,
// for example musl, so define the constant and a minimal repr(C) struct
// locally and invoke the syscall directly.
const STATX_DIOALIGN: u32 = 0x2000;
// Minimal statx layout, only the needed fields, everything else is
// padding.
#[repr(C)]
struct Statx {
stx_mask: u32,
_pad: [u8; 148],
stx_dio_mem_align: u32,
stx_dio_offset_align: u32,
_pad2: [u8; 96],
}
let mut stx = mem::MaybeUninit::<Statx>::zeroed();
// SAFETY: FFI syscall with valid fd and correctly sized buffer.
let ret = unsafe {
libc::syscall(
libc::SYS_statx,
fd,
c"".as_ptr(),
libc::AT_EMPTY_PATH,
STATX_DIOALIGN,
stx.as_mut_ptr(),
)
};
if ret == 0 {
// SAFETY: statx succeeded, the struct is fully initialized.
let stx = unsafe { stx.assume_init() };
if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 {
return Some(cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64);
}
}
debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}");
Some(SECTOR_SIZE)
}
/// Probe whether the file/device supports punch hole and zero range
pub fn probe_sparse_support(file: &File) -> bool {
let fd = file.as_raw_fd();
let is_block_device = {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
// SAFETY: FFI call with valid fd and buffer
let ret = unsafe { libc::fstat(fd, stat.as_mut_ptr()) };
if ret != 0 {
warn!(
"Failed to stat file descriptor for sparse probe: {}",
io::Error::last_os_error()
);
return false;
}
// SAFETY: stat result is valid at this point
unsafe { (*stat.as_ptr()).st_mode & S_IFMT == S_IFBLK }
};
if is_block_device {
if is_block_device(fd) {
probe_block_device_sparse_support(fd)
} else {
probe_file_sparse_support(fd)
@@ -452,110 +482,6 @@ pub fn preallocate_disk<P: AsRef<Path>>(file: &File, path: P) {
}
}
pub trait AsyncAdaptor {
fn read_vectored_sync(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
eventfd: &EventFd,
completion_list: &mut VecDeque<(u64, i32)>,
) -> AsyncIoResult<()>
where
Self: Read + Seek,
{
// Convert libc::iovec into IoSliceMut
let mut slices: SmallVec<[IoSliceMut; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
SmallVec::with_capacity(iovecs.len());
for iovec in iovecs.iter() {
// SAFETY: on Linux IoSliceMut wraps around libc::iovec
slices.push(IoSliceMut::new(unsafe {
std::mem::transmute::<libc::iovec, &mut [u8]>(*iovec)
}));
}
let result = {
// Move the cursor to the right offset
self.seek(SeekFrom::Start(offset as u64))
.map_err(AsyncIoError::ReadVectored)?;
let mut r = 0;
for b in slices.iter_mut() {
r += self.read(b).map_err(AsyncIoError::ReadVectored)?;
}
r
};
completion_list.push_back((user_data, result as i32));
eventfd.write(1).unwrap();
Ok(())
}
fn write_vectored_sync(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
eventfd: &EventFd,
completion_list: &mut VecDeque<(u64, i32)>,
) -> AsyncIoResult<()>
where
Self: Write + Seek,
{
// Convert libc::iovec into IoSlice
let mut slices: SmallVec<[IoSlice; DEFAULT_DESCRIPTOR_VEC_SIZE]> =
SmallVec::with_capacity(iovecs.len());
for iovec in iovecs.iter() {
// SAFETY: on Linux IoSlice wraps around libc::iovec
slices.push(IoSlice::new(unsafe {
std::mem::transmute::<libc::iovec, &mut [u8]>(*iovec)
}));
}
let result = {
// Move the cursor to the right offset
self.seek(SeekFrom::Start(offset as u64))
.map_err(AsyncIoError::WriteVectored)?;
let mut r = 0;
for b in slices.iter() {
r += self.write(b).map_err(AsyncIoError::WriteVectored)?;
}
r
};
completion_list.push_back((user_data, result as i32));
eventfd.write(1).unwrap();
Ok(())
}
fn fsync_sync(
&mut self,
user_data: Option<u64>,
eventfd: &EventFd,
completion_list: &mut VecDeque<(u64, i32)>,
) -> AsyncIoResult<()>
where
Self: Write,
{
let result: i32 = {
// Flush
self.flush().map_err(AsyncIoError::Fsync)?;
0
};
if let Some(user_data) = user_data {
completion_list.push_back((user_data, result));
eventfd.write(1).unwrap();
}
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ImageType {
FixedVhd,
@@ -599,23 +525,6 @@ impl FromStr for ImageType {
const QCOW_MAGIC: u32 = 0x5146_49fb;
const VHDX_SIGN: u64 = 0x656C_6966_7864_6876;
/// Read a block into memory aligned by the source block size (needed for O_DIRECT)
pub fn read_aligned_block_size(f: &mut File) -> std::io::Result<Vec<u8>> {
let blocksize = DiskTopology::probe(f)?.logical_block_size as usize;
// SAFETY: We are allocating memory that is naturally aligned (size = alignment) and we meet
// requirements for safety from Vec::from_raw_parts() as we are using the global allocator
// and transferring ownership of the memory.
let mut data = unsafe {
Vec::from_raw_parts(
alloc_zeroed(Layout::from_size_align_unchecked(blocksize, blocksize)),
blocksize,
blocksize,
)
};
f.read_exact(&mut data)?;
Ok(data)
}
/// Open a disk image file, returning a [`BlockError`] with path context
/// on failure.
pub fn open_disk_image(path: &Path, options: &OpenOptions) -> BlockResult<File> {
@@ -628,13 +537,16 @@ pub fn open_disk_image(path: &Path, options: &OpenOptions) -> BlockResult<File>
/// Determine image type through file parsing.
pub fn detect_image_type(f: &mut File) -> BlockResult<ImageType> {
let block = read_aligned_block_size(f)
let aligned = AlignedFile::new(f.try_clone()?, true);
let mut block = vec![0u8; aligned.alignment()];
aligned
.read_exact_at(&mut block, 0)
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?;
// Check 4 first bytes to get the header value and determine the image type
let image_type = if u32::from_be_bytes(block[0..4].try_into().unwrap()) == QCOW_MAGIC {
ImageType::Qcow2
} else if vhd::is_fixed_vhd(f)
} else if formats::vhd::is_fixed_vhd(f)
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?
{
ImageType::FixedVhd
@@ -647,17 +559,6 @@ pub fn detect_image_type(f: &mut File) -> BlockResult<ImageType> {
Ok(image_type)
}
pub trait BlockBackend: Read + Write + Seek + Send + Debug {
/// Returns the logical disk size a guest will see.
///
/// For raw formats, this is equal to [`Self::physical_size`]. For file formats
/// that wrap disk images in a container (e.g. QCOW2), this refers to the
/// effective size that the guest will see.
fn logical_size(&self) -> Result<u64, Error>;
/// Returns the physical size of the underlying file.
fn physical_size(&self) -> Result<u64, Error>;
}
#[derive(Debug)]
pub struct DiskTopology {
pub logical_block_size: u64,
@@ -721,21 +622,8 @@ enum BlockSize {
}
impl DiskTopology {
fn is_block_device(f: &File) -> std::io::Result<bool> {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
// SAFETY: FFI call with a valid fd and buffer
let ret = unsafe { libc::fstat(f.as_raw_fd(), stat.as_mut_ptr()) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
// SAFETY: stat is valid at this point
let is_block = unsafe { (*stat.as_ptr()).st_mode & S_IFMT == S_IFBLK };
Ok(is_block)
}
// libc::ioctl() takes different types on different architectures
fn query_block_size(f: &File, block_size_type: BlockSize) -> std::io::Result<u64> {
fn query_block_size(f: &File, block_size_type: BlockSize) -> io::Result<u64> {
let mut block_size = 0;
// SAFETY: FFI call with correct arguments
let ret = unsafe {
@@ -751,72 +639,18 @@ impl DiskTopology {
)
};
if ret != 0 {
return Err(std::io::Error::last_os_error());
return Err(io::Error::last_os_error());
}
Ok(block_size)
}
/// Query the O_DIRECT alignment requirement for a regular file.
///
/// Uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain the exact
/// memory and offset alignment the kernel requires for direct I/O on
/// this specific file. Unlike `fstatvfs().f_bsize`, which only returns
/// the filesystem's preferred I/O block size, `STATX_DIOALIGN` reports
/// the true per-file DIO constraints accounting for the filesystem,
/// underlying block device, and any stacking (loop, dm, etc.).
fn query_file_alignment(f: &File) -> u64 {
// The libc crate does not expose statx / STATX_DIOALIGN on all
// targets (e.g. musl), so define the constant and a minimal repr(C)
// struct locally and invoke the syscall directly.
const STATX_DIOALIGN: u32 = 0x2000;
// Minimal statx layout, only the needed fields,
// everything else is padding.
#[repr(C)]
struct Statx {
stx_mask: u32,
_pad: [u8; 148],
stx_dio_mem_align: u32,
stx_dio_offset_align: u32,
_pad2: [u8; 96],
}
let mut stx = mem::MaybeUninit::<Statx>::zeroed();
// SAFETY: FFI syscall with valid fd and correctly sized buffer.
let ret = unsafe {
libc::syscall(
libc::SYS_statx,
f.as_raw_fd(),
c"".as_ptr(),
libc::AT_EMPTY_PATH,
STATX_DIOALIGN,
stx.as_mut_ptr(),
)
};
if ret == 0 {
// SAFETY: statx succeeded, the struct is fully initialized.
let stx = unsafe { stx.assume_init() };
if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 {
let align = cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64;
debug!("statx(STATX_DIOALIGN) returned alignment {align}");
return align;
}
}
debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}");
SECTOR_SIZE
}
pub fn probe(f: &File) -> std::io::Result<Self> {
if !Self::is_block_device(f)? {
pub fn probe(f: &File) -> io::Result<Self> {
if !is_block_device(f.as_raw_fd()) {
// For regular files opened with O_DIRECT, the logical block size
// must reflect the filesystem DIO alignment so the guest issues
// correctly sized I/O.
// SAFETY: fcntl(F_GETFL) is always safe on a valid fd.
let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) };
if flags >= 0 && (flags & libc::O_DIRECT) != 0 {
let alignment = Self::query_file_alignment(f);
if let Some(alignment) = probe_direct_alignment(f.as_raw_fd()) {
return Ok(DiskTopology {
logical_block_size: alignment,
physical_block_size: alignment,
@@ -842,7 +676,7 @@ mod unit_tests {
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::{ptr, slice};
use std::{mem, ptr, slice};
use vmm_sys_util::tempfile::TempFile;
@@ -973,6 +807,25 @@ mod unit_tests {
assert!(physical > 0);
}
// A mode-0 fallocate() is not eagerly accounted in st_blocks on every
// filesystem: zfs reserves the range but accounts blocks lazily at
// transaction-group commit, and FUSE-based filesystems such as virtiofs
// report preallocated files as sparse. Identify those by filesystem type
// so a skipped physical-size check always names a proven platform
// limitation instead of being inferred from the value under test.
fn fs_defers_fallocate_block_accounting(f: &File) -> bool {
// SAFETY: a zeroed statfs is a valid output buffer for fstatfs and it
// is only read after the call succeeds.
let mut sfs: libc::statfs = unsafe { mem::zeroed() };
// SAFETY: the fd is valid and sfs outlives the call.
let ret = unsafe { libc::fstatfs(f.as_raw_fd(), &mut sfs) };
assert_eq!(ret, 0, "fstatfs failed: {}", io::Error::last_os_error());
// ZFS_SUPER_MAGIC and FUSE_SUPER_MAGIC (statfs(2)), as untyped
// literals because the width and signedness of f_type differ
// between libc targets.
matches!(sfs.f_type, 0x2fc1_2fc1 | 0x6573_5546)
}
#[test]
fn test_query_device_size_sparse_file_punch_hole() {
let temp_file = TempFile::new().unwrap();
@@ -989,11 +842,25 @@ mod unit_tests {
size,
)
};
assert_eq!(ret, 0, "fallocate failed: {}", io::Error::last_os_error());
if ret != 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EOPNOTSUPP) {
eprintln!("Skipping test: fallocate() is not supported: {err}");
return;
}
panic!("fallocate failed: {err}");
}
f.sync_all().unwrap();
let (log_before, phys_before) = query_device_size(f).unwrap();
assert_eq!(log_before, size as u64);
if fs_defers_fallocate_block_accounting(f) {
eprintln!(
"Skipping physical size checks: the filesystem defers \
fallocate() block accounting"
);
return;
}
assert_eq!(phys_before, size as u64);
// Punch a hole in the middle 512 KiB
@@ -1006,7 +873,16 @@ mod unit_tests {
size / 2,
)
};
assert_eq!(ret, 0, "punch hole failed: {}", io::Error::last_os_error());
if ret != 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EOPNOTSUPP) {
eprintln!(
"Skipping punch-hole checks: FALLOC_FL_PUNCH_HOLE is not supported: {err}"
);
return;
}
panic!("punch hole failed: {err}");
}
f.sync_all().unwrap();
let (logical, physical) = query_device_size(f).unwrap();
@@ -1019,7 +895,7 @@ mod unit_tests {
#[test]
fn test_query_device_size_rejects_char_device() {
let f = std::fs::File::open("/dev/zero").unwrap();
let f = File::open("/dev/zero").unwrap();
let err = query_device_size(&f).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,369 +0,0 @@
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::fmt::Debug;
use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
use std::mem::size_of;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use vmm_sys_util::write_zeroes::WriteZeroes;
use super::RawFile;
// Type aliases for the refcount read/write function pointers
type RefcountReader = fn(&mut RawFile, usize) -> io::Result<Vec<u64>>;
type RefcountWriter = fn(&mut RawFile, &[u64]) -> io::Result<()>;
/// Big-endian file access trait.
pub(super) trait BeUint: Sized + Copy {
fn from_be_slice(bytes: &[u8]) -> u64;
fn read_be<R: Read>(r: &mut R) -> io::Result<Self>;
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()>;
}
impl BeUint for u8 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
bytes[0] as u64
}
#[inline(always)]
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
r.read_u8()
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u8(val)
}
}
impl BeUint for u16 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
u16::from_be_bytes([bytes[0], bytes[1]]) as u64
}
#[inline(always)]
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
r.read_u16::<BigEndian>()
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u16::<BigEndian>(val)
}
}
impl BeUint for u32 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
#[inline(always)]
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
r.read_u32::<BigEndian>()
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u32::<BigEndian>(val)
}
}
impl BeUint for u64 {
#[inline(always)]
fn from_be_slice(bytes: &[u8]) -> u64 {
u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}
#[inline(always)]
fn read_be<R: Read>(r: &mut R) -> io::Result<Self> {
r.read_u64::<BigEndian>()
}
#[inline(always)]
fn write_be<W: Write>(w: &mut W, val: Self) -> io::Result<()> {
w.write_u64::<BigEndian>(val)
}
}
/// Read byte-aligned refcounts.
fn read_refcount<T: BeUint>(file: &mut RawFile, count: usize) -> io::Result<Vec<u64>> {
let bytes_per_entry = size_of::<T>();
let mut data = vec![0u8; count * bytes_per_entry];
file.read_exact(&mut data)?;
Ok(data
.chunks_exact(bytes_per_entry)
.map(T::from_be_slice)
.collect())
}
/// Write byte-aligned refcounts.
fn write_refcount<T: BeUint + TryFrom<u64>>(file: &mut RawFile, table: &[u64]) -> io::Result<()>
where
<T as TryFrom<u64>>::Error: Debug,
{
let bytes_per_entry = size_of::<T>();
let mut buffer = BufWriter::with_capacity(table.len() * bytes_per_entry, file);
for &val in table {
let converted = T::try_from(val).expect("refcount values are validated on increment");
T::write_be(&mut buffer, converted)?;
}
buffer.flush()
}
/// Read sub-byte refcounts. Bit 0 is the least significant bit.
fn read_refcount_subbyte<const BITS: usize>(
file: &mut RawFile,
count: usize,
) -> io::Result<Vec<u64>> {
const { assert!(BITS == 1 || BITS == 2 || BITS == 4) };
let entries_per_byte = 8 / BITS;
let mask = (1u64 << BITS) - 1;
let bytes_needed = count.div_ceil(entries_per_byte);
let mut bytes = vec![0u8; bytes_needed];
file.read_exact(&mut bytes)?;
let mut table = vec![0u64; count];
for (i, val) in table.iter_mut().enumerate() {
let byte_idx = i / entries_per_byte;
let bit_offset = (i % entries_per_byte) * BITS;
*val = (bytes[byte_idx] as u64 >> bit_offset) & mask;
}
Ok(table)
}
/// Write sub-byte refcounts. Bit 0 is the least significant bit.
fn write_refcount_subbyte<const BITS: usize>(file: &mut RawFile, table: &[u64]) -> io::Result<()> {
const { assert!(BITS == 1 || BITS == 2 || BITS == 4) };
let entries_per_byte = 8 / BITS;
let mask = (1u64 << BITS) - 1;
let mut buffer = BufWriter::with_capacity(table.len().div_ceil(entries_per_byte), file);
for chunk in table.chunks(entries_per_byte) {
let mut byte = 0u8;
for (i, &val) in chunk.iter().enumerate() {
let bit_offset = i * BITS;
byte |= ((val & mask) << bit_offset) as u8;
}
buffer.write_u8(byte)?;
}
buffer.flush()
}
/// A qcow file. Allows reading/writing clusters and appending clusters.
#[derive(Debug)]
pub struct QcowRawFile {
file: RawFile,
cluster_size: u64,
cluster_mask: u64,
refcount_block_entries: u64,
read_refcount_fn: RefcountReader,
write_refcount_fn: RefcountWriter,
}
impl QcowRawFile {
/// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not
/// a power of two or refcount_bits is invalid.
pub fn from(file: RawFile, cluster_size: u64, refcount_bits: u64) -> Option<Self> {
if !cluster_size.is_power_of_two() {
return None;
}
let (read_refcount_fn, write_refcount_fn): (RefcountReader, RefcountWriter) =
match refcount_bits {
1 => (read_refcount_subbyte::<1>, write_refcount_subbyte::<1>),
2 => (read_refcount_subbyte::<2>, write_refcount_subbyte::<2>),
4 => (read_refcount_subbyte::<4>, write_refcount_subbyte::<4>),
8 => (read_refcount::<u8>, write_refcount::<u8>),
16 => (read_refcount::<u16>, write_refcount::<u16>),
32 => (read_refcount::<u32>, write_refcount::<u32>),
64 => (read_refcount::<u64>, write_refcount::<u64>),
_ => return None,
};
// For sub-byte refcounts (1,2,4 bits), entries pack multiple per byte
let refcount_block_entries = cluster_size * 8 / refcount_bits;
Some(QcowRawFile {
file,
cluster_size,
cluster_mask: cluster_size - 1,
refcount_block_entries,
read_refcount_fn,
write_refcount_fn,
})
}
/// Reads `count` 64 bit offsets and returns them as a vector.
/// `mask` optionally `&`s out some of the bits on the file.
pub fn read_pointer_table(
&mut self,
offset: u64,
count: u64,
mask: Option<u64>,
) -> io::Result<Vec<u64>> {
let mut table = vec![0; count as usize];
self.file.seek(SeekFrom::Start(offset))?;
self.file.read_u64_into::<BigEndian>(&mut table)?;
if let Some(m) = mask {
for ptr in &mut table {
*ptr &= m;
}
}
Ok(table)
}
/// Reads a cluster's worth of 64 bit offsets and returns them as a vector.
/// `mask` optionally `&`s out some of the bits on the file.
pub fn read_pointer_cluster(&mut self, offset: u64, mask: Option<u64>) -> io::Result<Vec<u64>> {
let count = self.cluster_size / size_of::<u64>() as u64;
self.read_pointer_table(offset, count, mask)
}
/// Internal helper for creating a buffered writer for pointer tables.
#[inline]
fn setup_pointer_table_writer<T>(
&mut self,
offset: u64,
entries: &impl Iterator<Item = T>,
) -> io::Result<BufWriter<RawFile>> {
self.file.seek(SeekFrom::Start(offset))?;
let my_file = self.file.try_clone()?;
let capacity = entries.size_hint().0 * size_of::<u64>();
Ok(BufWriter::with_capacity(capacity, my_file))
}
/// Writes a pointer table to `offset` in the file.
/// Entries are computed on-the-fly by the callback.
pub fn write_pointer_table<'a, T: Copy + 'a>(
&mut self,
offset: u64,
entries: impl Iterator<Item = &'a T>,
mut f: impl FnMut(&mut QcowRawFile, T) -> io::Result<u64>,
) -> io::Result<()> {
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
for addr in entries {
let entry = f(self, *addr)?;
u64::write_be(&mut buffer, entry)?;
}
buffer.flush()?;
Ok(())
}
/// Writes a pointer table directly without transforming values.
pub fn write_pointer_table_direct<'a>(
&mut self,
offset: u64,
entries: impl Iterator<Item = &'a u64>,
) -> io::Result<()> {
let mut buffer = self.setup_pointer_table_writer(offset, &entries)?;
for &entry in entries {
u64::write_be(&mut buffer, entry)?;
}
buffer.flush()?;
Ok(())
}
/// Read a refcount block from the file and returns a Vec containing the block.
/// Always returns a cluster's worth of data.
#[inline]
pub fn read_refcount_block(&mut self, offset: u64) -> io::Result<Vec<u64>> {
self.file.seek(SeekFrom::Start(offset))?;
(self.read_refcount_fn)(&mut self.file, self.refcount_block_entries as usize)
}
/// Writes a refcount block to the file.
#[inline]
pub fn write_refcount_block(&mut self, offset: u64, table: &[u64]) -> io::Result<()> {
self.file.seek(SeekFrom::Start(offset))?;
(self.write_refcount_fn)(&mut self.file, table)
}
/// Allocates a new cluster at the end of the current file, return the address.
pub fn add_cluster_end(&mut self, max_valid_cluster_offset: u64) -> io::Result<Option<u64>> {
// Determine where the new end of the file should be and set_len, which
// translates to truncate(2).
let file_end: u64 = self.file.seek(SeekFrom::End(0))?;
let new_cluster_address: u64 = (file_end + self.cluster_size - 1) & !self.cluster_mask;
if new_cluster_address > max_valid_cluster_offset {
return Ok(None);
}
self.file.set_len(new_cluster_address + self.cluster_size)?;
Ok(Some(new_cluster_address))
}
/// Returns a reference to the underlying file.
pub fn file(&self) -> &RawFile {
&self.file
}
/// Returns a mutable reference to the underlying file.
pub fn file_mut(&mut self) -> &mut RawFile {
&mut self.file
}
/// Returns the size of the file's clusters.
pub fn cluster_size(&self) -> u64 {
self.cluster_size
}
/// Returns the offset of `address` within a cluster.
pub fn cluster_offset(&self, address: u64) -> u64 {
address & self.cluster_mask
}
/// Returns the base address of the cluster containing `address`.
pub fn cluster_address(&self, address: u64) -> u64 {
address & !self.cluster_mask
}
/// Zeros out a cluster in the file.
pub fn zero_cluster(&mut self, address: u64) -> io::Result<()> {
let cluster_size = self.cluster_size as usize;
self.file.seek(SeekFrom::Start(address))?;
self.file.write_zeroes(cluster_size)?;
Ok(())
}
/// Writes
pub fn write_cluster(&mut self, address: u64, data: &[u8]) -> io::Result<()> {
let cluster_size = self.cluster_size as usize;
self.file.seek(SeekFrom::Start(address))?;
self.file.write_all(&data[0..cluster_size])
}
pub fn physical_size(&self) -> Result<u64, std::io::Error> {
self.file.metadata().map(|m| m.len())
}
}
impl Clone for QcowRawFile {
fn clone(&self) -> Self {
QcowRawFile {
file: self.file.try_clone().expect("QcowRawFile cloning failed"),
cluster_size: self.cluster_size,
cluster_mask: self.cluster_mask,
refcount_block_entries: self.refcount_block_entries,
read_refcount_fn: self.read_refcount_fn,
write_refcount_fn: self.write_refcount_fn,
}
}
}
impl AsRawFd for QcowRawFile {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl AsFd for QcowRawFile {
fn as_fd(&self) -> BorrowedFd<'_> {
self.file.as_fd()
}
}

View File

@@ -1,406 +0,0 @@
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Portions Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
//
// Copyright © 2020 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::fs::{File, Metadata};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::fd::{AsFd, BorrowedFd};
use std::os::unix::io::{AsRawFd, RawFd};
use std::slice;
use vmm_sys_util::file_traits::FileSync;
use vmm_sys_util::seek_hole::SeekHole;
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
use crate::{BlockBackend, query_device_size};
#[derive(Debug)]
pub struct RawFile {
file: File,
alignment: usize,
position: u64,
direct_io: bool,
}
const BLK_ALIGNMENTS: [usize; 2] = [512, 4096];
fn is_valid_alignment(fd: RawFd, alignment: usize) -> bool {
let layout = Layout::from_size_align(alignment, alignment).unwrap();
// SAFETY: layout has non-zero size
let ptr = unsafe { alloc_zeroed(layout) };
assert!(!ptr.is_null());
// SAFETY: FFI call
let ret = unsafe { ::libc::pread(fd, ptr.cast(), alignment, alignment.try_into().unwrap()) };
// SAFETY: ptr was allocated by alloc_zeroed with layout
unsafe { dealloc(ptr, layout) };
ret >= 0
}
impl RawFile {
pub fn new(file: File, direct_io: bool) -> Self {
// Assume no alignment restrictions if we aren't using O_DIRECT.
let mut alignment = 0;
if direct_io {
for align in &BLK_ALIGNMENTS {
if is_valid_alignment(file.as_raw_fd(), *align) {
alignment = *align;
break;
}
}
}
RawFile {
file,
alignment,
position: 0,
direct_io,
}
}
fn round_up(&self, offset: u64) -> u64 {
let align: u64 = self.alignment.try_into().unwrap();
offset.div_ceil(align) * align
}
fn round_down(&self, offset: u64) -> u64 {
let align: u64 = self.alignment.try_into().unwrap();
(offset / align) * align
}
fn is_aligned(&self, buf: &[u8]) -> bool {
if self.alignment == 0 {
return true;
}
let align64: u64 = self.alignment.try_into().unwrap();
self.position.is_multiple_of(align64)
&& (buf.as_ptr() as usize).is_multiple_of(self.alignment)
&& buf.len().is_multiple_of(self.alignment)
}
pub fn set_len(&self, size: u64) -> std::io::Result<()> {
self.file.set_len(size)
}
pub fn metadata(&self) -> std::io::Result<Metadata> {
self.file.metadata()
}
pub fn try_clone(&self) -> std::io::Result<RawFile> {
Ok(RawFile {
file: self.file.try_clone().expect("RawFile cloning failed"),
alignment: self.alignment,
position: self.position,
direct_io: self.direct_io,
})
}
pub fn sync_all(&self) -> std::io::Result<()> {
self.file.sync_all()
}
pub fn sync_data(&self) -> std::io::Result<()> {
self.file.sync_data()
}
pub fn is_direct(&self) -> bool {
self.direct_io
}
pub fn alignment(&self) -> usize {
self.alignment
}
/// Returns true if the file was opened with write access.
pub fn is_writable(&self) -> bool {
// SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor
let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return false;
}
let access_mode = flags & libc::O_ACCMODE;
access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR
}
}
impl Read for RawFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.is_aligned(buf) {
match self.file.read(buf) {
Ok(r) => {
self.position = self.position.checked_add(r.try_into().unwrap()).unwrap();
Ok(r)
}
Err(e) => Err(e),
}
} else {
let rounded_pos: u64 = self.round_down(self.position);
let file_offset: usize = self
.position
.checked_sub(rounded_pos)
.unwrap()
.try_into()
.unwrap();
let buf_len: usize = buf.len();
let rounded_len: usize = self
.round_up(
file_offset
.checked_add(buf_len)
.unwrap()
.try_into()
.unwrap(),
)
.try_into()
.unwrap();
let layout = Layout::from_size_align(rounded_len, self.alignment).unwrap();
// SAFETY: layout has non-zero size
let tmp_ptr = unsafe { alloc_zeroed(layout) };
if tmp_ptr.is_null() {
return Err(io::Error::last_os_error());
}
// SAFETY: tmp_ptr is valid and at least rounded_len long
let tmp_buf = unsafe { slice::from_raw_parts_mut(tmp_ptr, rounded_len) };
// This can eventually replaced with read_at once its interface
// has been stabilized.
// SAFETY: FFI call. All parameters are valid.
let ret = unsafe {
::libc::pread64(
self.file.as_raw_fd(),
tmp_buf.as_mut_ptr().cast(),
tmp_buf.len(),
rounded_pos.try_into().unwrap(),
)
};
if ret < 0 {
// SAFETY: tmp_ptr was allocated by alloc_zeroed with layout
unsafe { dealloc(tmp_ptr, layout) };
return Err(io::Error::last_os_error());
}
let read: usize = ret.try_into().unwrap();
if read < file_offset {
// SAFETY: tmp_ptr was allocated by alloc_zeroed with layout
unsafe { dealloc(tmp_ptr, layout) };
return Ok(0);
}
let mut to_copy = read - file_offset;
if to_copy > buf_len {
to_copy = buf_len;
}
buf.copy_from_slice(&tmp_buf[file_offset..(file_offset + buf_len)]);
// SAFETY: tmp_ptr was allocated by alloc_zeroed with layout
unsafe { dealloc(tmp_ptr, layout) };
self.seek(SeekFrom::Current(to_copy.try_into().unwrap()))
.unwrap();
Ok(to_copy)
}
}
}
impl Write for RawFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.is_aligned(buf) {
match self.file.write(buf) {
Ok(r) => {
self.position = self.position.checked_add(r.try_into().unwrap()).unwrap();
Ok(r)
}
Err(e) => Err(e),
}
} else {
let rounded_pos: u64 = self.round_down(self.position);
let file_offset: usize = self
.position
.checked_sub(rounded_pos)
.unwrap()
.try_into()
.unwrap();
let buf_len: usize = buf.len();
let rounded_len: usize = self
.round_up(
file_offset
.checked_add(buf_len)
.unwrap()
.try_into()
.unwrap(),
)
.try_into()
.unwrap();
let layout = Layout::from_size_align(rounded_len, self.alignment).unwrap();
// SAFETY: layout has non-zero size
let tmp_ptr = unsafe { alloc_zeroed(layout) };
if tmp_ptr.is_null() {
return Err(io::Error::last_os_error());
}
// SAFETY: tmp_ptr is at least rounded_len long
let tmp_buf = unsafe { slice::from_raw_parts_mut(tmp_ptr, rounded_len) };
// This can eventually replaced with read_at once its interface
// has been stabilized.
// SAFETY: FFI call
let ret = unsafe {
::libc::pread64(
self.file.as_raw_fd(),
tmp_buf.as_mut_ptr().cast(),
tmp_buf.len(),
rounded_pos.try_into().unwrap(),
)
};
if ret < 0 {
// SAFETY: tmp_ptr was allocated by alloc_zeroed with layout
unsafe { dealloc(tmp_ptr, layout) };
return Err(io::Error::last_os_error());
}
tmp_buf[file_offset..(file_offset + buf_len)].copy_from_slice(buf);
// This can eventually replaced with write_at once its interface
// has been stabilized.
// SAFETY: FFI call
let ret = unsafe {
::libc::pwrite64(
self.file.as_raw_fd(),
tmp_buf.as_ptr().cast(),
tmp_buf.len(),
rounded_pos.try_into().unwrap(),
)
};
// SAFETY: tmp_ptr was allocated by alloc_zeroed with layout
unsafe { dealloc(tmp_ptr, layout) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
let written: usize = ret.try_into().unwrap();
if written < file_offset {
Ok(0)
} else {
let mut to_seek = written - file_offset;
if to_seek > buf_len {
to_seek = buf_len;
}
self.seek(SeekFrom::Current(to_seek.try_into().unwrap()))
.unwrap();
Ok(to_seek)
}
}
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.sync_all()
}
}
impl Seek for RawFile {
fn seek(&mut self, newpos: SeekFrom) -> std::io::Result<u64> {
match self.file.seek(newpos) {
Ok(pos) => {
self.position = pos;
Ok(pos)
}
Err(e) => Err(e),
}
}
}
impl WriteZeroesAt for RawFile {
fn write_zeroes_at(&mut self, offset: u64, length: usize) -> std::io::Result<usize> {
self.file.write_zeroes_at(offset, length)
}
}
impl PunchHole for RawFile {
fn punch_hole(&mut self, offset: u64, length: u64) -> std::io::Result<()> {
self.file.punch_hole(offset, length)
}
}
impl FileSync for RawFile {
fn fsync(&mut self) -> std::io::Result<()> {
self.file.fsync()
}
}
impl SeekHole for RawFile {
fn seek_hole(&mut self, offset: u64) -> std::io::Result<Option<u64>> {
match self.file.seek_hole(offset) {
Ok(pos) => {
if let Some(p) = pos {
self.position = p;
}
Ok(pos)
}
Err(e) => Err(e),
}
}
fn seek_data(&mut self, offset: u64) -> std::io::Result<Option<u64>> {
match self.file.seek_data(offset) {
Ok(pos) => {
if let Some(p) = pos {
self.position = p;
}
Ok(pos)
}
Err(e) => Err(e),
}
}
}
impl BlockBackend for RawFile {
fn logical_size(&self) -> std::result::Result<u64, crate::Error> {
Ok(query_device_size(&self.file)
.map_err(crate::Error::RawFileError)?
.0)
}
fn physical_size(&self) -> std::result::Result<u64, crate::Error> {
Ok(query_device_size(&self.file)
.map_err(crate::Error::RawFileError)?
.1)
}
}
impl Clone for RawFile {
fn clone(&self) -> Self {
RawFile {
file: self.file.try_clone().expect("RawFile cloning failed"),
alignment: self.alignment,
position: self.position,
direct_io: self.direct_io,
}
}
}
impl AsRawFd for RawFile {
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
impl AsFd for RawFile {
fn as_fd(&self) -> BorrowedFd<'_> {
self.file.as_fd()
}
}

View File

@@ -1,446 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! Shared helpers for QCOW2 sync and async backends.
//!
//! Position-independent I/O (`pread_exact`, `pwrite_all`) and iovec
//! scatter/gather helpers used by both `qcow_sync` and `qcow_async`.
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::cmp::min;
use std::os::fd::RawFd;
use std::{io, ptr, slice};
use crate::qcow::decoder::Decoder;
// -- Position independent I/O helpers --
//
// Duplicated file descriptors share the kernel file description and thus the
// file position. Using seek then read from multiple queues races on that
// shared position. pread64 and pwrite64 are atomic and never touch the position.
/// Read exactly the requested bytes at offset, looping on short reads.
pub fn pread_exact(fd: RawFd, buf: &mut [u8], offset: u64) -> io::Result<()> {
let mut total = 0usize;
while total < buf.len() {
// SAFETY: buf and fd are valid for the lifetime of the call.
let ret = unsafe {
libc::pread64(
fd,
buf[total..].as_mut_ptr().cast(),
buf.len() - total,
(offset + total as u64) as libc::off_t,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
if ret == 0 {
return Err(io::Error::from(io::ErrorKind::UnexpectedEof));
}
total += ret as usize;
}
Ok(())
}
/// Allocate a buffer and pread exactly `len` bytes at `offset`.
pub fn pread_alloc(fd: RawFd, offset: u64, len: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; len];
pread_exact(fd, &mut buf, offset)?;
Ok(buf)
}
/// Decompress a full QCOW2 cluster from compressed data.
///
/// Returns a `cluster_size` byte buffer with the decompressed cluster
/// content. Fails if the decoder does not produce exactly `cluster_size`
/// bytes.
pub fn decompress_cluster(
compressed: &[u8],
cluster_size: usize,
decoder: &dyn Decoder,
) -> io::Result<Vec<u8>> {
let mut decompressed = vec![0u8; cluster_size];
let n = decoder
.decode(compressed, &mut decompressed)
.map_err(|_| io::Error::from_raw_os_error(libc::EIO))?;
if n != cluster_size {
return Err(io::Error::from_raw_os_error(libc::EIO));
}
Ok(decompressed)
}
/// Write all bytes to fd at offset, looping on short writes.
pub fn pwrite_all(fd: RawFd, buf: &[u8], offset: u64) -> io::Result<()> {
let mut total = 0usize;
while total < buf.len() {
// SAFETY: buf and fd are valid for the lifetime of the call.
let ret = unsafe {
libc::pwrite64(
fd,
buf[total..].as_ptr().cast(),
buf.len() - total,
(offset + total as u64) as libc::off_t,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
if ret == 0 {
return Err(io::Error::other("pwrite64 wrote 0 bytes"));
}
total += ret as usize;
}
Ok(())
}
/// RAII wrapper for an aligned heap buffer required by O_DIRECT.
pub struct AlignedBuf {
ptr: *mut u8,
layout: Layout,
}
impl AlignedBuf {
pub fn new(size: usize, alignment: usize) -> io::Result<Self> {
let size = size.max(1).next_multiple_of(alignment);
let layout = Layout::from_size_align(size, alignment)
.map_err(|e| io::Error::other(format!("invalid aligned layout: {e}")))?;
// SAFETY: layout has non-zero size.
let ptr = unsafe { alloc_zeroed(layout) };
if ptr.is_null() {
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"aligned allocation failed",
));
}
Ok(AlignedBuf { ptr, layout })
}
pub fn as_mut_slice(&mut self, len: usize) -> &mut [u8] {
let len = len.min(self.layout.size());
// SAFETY: ptr is valid for layout.size() bytes; len <= layout.size().
unsafe { slice::from_raw_parts_mut(self.ptr, len) }
}
pub fn as_slice(&self, len: usize) -> &[u8] {
let len = len.min(self.layout.size());
// SAFETY: ptr is valid for layout.size() bytes; len <= layout.size().
unsafe { slice::from_raw_parts(self.ptr, len) }
}
#[cfg(test)]
pub fn layout(&self) -> &Layout {
&self.layout
}
#[cfg(test)]
pub fn ptr(&self) -> *const u8 {
self.ptr
}
}
impl Drop for AlignedBuf {
fn drop(&mut self) {
// SAFETY: ptr was allocated by alloc_zeroed with self.layout.
unsafe { dealloc(self.ptr, self.layout) };
}
}
/// Read into `buf` via an aligned bounce buffer when O_DIRECT requires it.
pub fn aligned_pread(fd: RawFd, buf: &mut [u8], offset: u64, alignment: usize) -> io::Result<()> {
if alignment == 0
|| ((buf.as_ptr() as usize).is_multiple_of(alignment)
&& buf.len().is_multiple_of(alignment)
&& (offset as usize).is_multiple_of(alignment))
{
return pread_exact(fd, buf, offset);
}
let aligned_offset = offset & !(alignment as u64 - 1);
let head = (offset - aligned_offset) as usize;
let aligned_len = (head + buf.len()).next_multiple_of(alignment);
let mut bounce = AlignedBuf::new(aligned_len, alignment)?;
pread_exact(fd, bounce.as_mut_slice(aligned_len), aligned_offset)?;
buf.copy_from_slice(&bounce.as_slice(aligned_len)[head..head + buf.len()]);
Ok(())
}
/// Write `buf` via an aligned bounce buffer when O_DIRECT requires it.
pub fn aligned_pwrite(fd: RawFd, buf: &[u8], offset: u64, alignment: usize) -> io::Result<()> {
if alignment == 0
|| ((buf.as_ptr() as usize).is_multiple_of(alignment)
&& buf.len().is_multiple_of(alignment)
&& (offset as usize).is_multiple_of(alignment))
{
return pwrite_all(fd, buf, offset);
}
let aligned_offset = offset & !(alignment as u64 - 1);
let head = (offset - aligned_offset) as usize;
let aligned_len = (head + buf.len()).next_multiple_of(alignment);
let mut bounce = AlignedBuf::new(aligned_len, alignment)?;
// Read-modify-write: read the existing aligned region, overlay our data.
pread_exact(fd, bounce.as_mut_slice(aligned_len), aligned_offset)?;
bounce.as_mut_slice(aligned_len)[head..head + buf.len()].copy_from_slice(buf);
pwrite_all(fd, bounce.as_slice(aligned_len), aligned_offset)
}
// -- iovec helper functions --
//
// Operate on the iovec array as a flat byte stream.
/// Copy data into iovecs starting at the given byte offset.
///
/// # Safety
/// Caller must ensure iovecs point to valid, writable memory of sufficient size.
pub unsafe fn scatter_to_iovecs(iovecs: &[libc::iovec], start: usize, data: &[u8]) {
let mut remaining = data;
let mut pos = 0usize;
for iov in iovecs {
let iov_end = pos + iov.iov_len;
if iov_end <= start || remaining.is_empty() {
pos = iov_end;
continue;
}
let iov_start = start.saturating_sub(pos);
let available = iov.iov_len - iov_start;
let count = min(available, remaining.len());
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
unsafe {
let dst = iov.iov_base.cast::<u8>().add(iov_start);
ptr::copy_nonoverlapping(remaining.as_ptr(), dst, count);
}
remaining = &remaining[count..];
if remaining.is_empty() {
break;
}
pos = iov_end;
}
}
/// Zero fill iovecs starting at the given byte offset for the given length.
///
/// # Safety
/// Caller must ensure iovecs point to valid, writable memory of sufficient size.
pub unsafe fn zero_fill_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) {
let mut remaining = len;
let mut pos = 0usize;
for iov in iovecs {
let iov_end = pos + iov.iov_len;
if iov_end <= start || remaining == 0 {
pos = iov_end;
continue;
}
let iov_start = start.saturating_sub(pos);
let available = iov.iov_len - iov_start;
let count = min(available, remaining);
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
unsafe {
let dst = iov.iov_base.cast::<u8>().add(iov_start);
ptr::write_bytes(dst, 0, count);
}
remaining -= count;
if remaining == 0 {
break;
}
pos = iov_end;
}
}
/// Gather bytes from iovecs starting at the given byte offset into `dst`.
///
/// # Safety
/// Caller must ensure iovecs point to valid, readable memory of sufficient size.
pub unsafe fn gather_from_iovecs_into(iovecs: &[libc::iovec], start: usize, dst: &mut [u8]) {
let len = dst.len();
let mut written = 0usize;
let mut pos = 0usize;
for iov in iovecs {
let iov_end = pos + iov.iov_len;
if iov_end <= start || written == len {
pos = iov_end;
continue;
}
let iov_start = start.saturating_sub(pos);
let available = iov.iov_len - iov_start;
let count = min(available, len - written);
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
unsafe {
let src = iov.iov_base.cast::<u8>().add(iov_start);
ptr::copy_nonoverlapping(src, dst.as_mut_ptr().add(written), count);
}
written += count;
if written == len {
break;
}
pos = iov_end;
}
}
/// Gather bytes from iovecs starting at the given byte offset into a Vec.
///
/// # Safety
/// Caller must ensure iovecs point to valid, readable memory of sufficient size.
pub unsafe fn gather_from_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) -> Vec<u8> {
let mut result = vec![0u8; len];
// SAFETY: caller guarantees iovecs are valid; result has len bytes.
unsafe { gather_from_iovecs_into(iovecs, start, &mut result) };
result
}
#[cfg(test)]
pub(crate) mod unit_tests {
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::FileExt;
use std::os::unix::io::AsRawFd;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use flate2::Compression;
use flate2::write::DeflateEncoder;
use vmm_sys_util::tempfile::TempFile;
use super::{decompress_cluster, pread_alloc};
use crate::qcow::decoder::ZlibDecoder;
const COMPRESSED_FLAG: u64 = 1 << 62;
const CLUSTER_USED_FLAG: u64 = 1 << 63;
const COMPRESSED_SECTOR_SIZE: u64 = 512;
const HEADER_CLUSTER_BITS_OFFSET: u64 = 20;
const HEADER_L1_SIZE_OFFSET: u64 = 36;
const HEADER_L1_TABLE_OFFSET: u64 = 40;
const L1_L2_ADDR_MASK: u64 = 0x00ff_ffff_ffff_fe00;
fn make_compressed_l2_entry(host_offset: u64, compressed_len: usize, cluster_bits: u32) -> u64 {
let compressed_size_shift = 62 - (cluster_bits - 8);
let intra_sector_offset = host_offset & (COMPRESSED_SECTOR_SIZE - 1);
let total_bytes = compressed_len as u64 + intra_sector_offset;
let nsectors = total_bytes.div_ceil(COMPRESSED_SECTOR_SIZE);
let addr_part = host_offset & ((1 << compressed_size_shift) - 1);
let size_part = (nsectors - 1) << compressed_size_shift;
COMPRESSED_FLAG | size_part | addr_part
}
/// Compress every allocated cluster in a QCOW2 image file in place.
///
/// Walks L1 -> L2 tables, compresses each standard cluster with raw
/// deflate, appends the compressed payload at the end of the file,
/// and rewrites the L2 entry with the compressed layout.
pub fn compress_allocated_clusters(file: &mut File) {
file.seek(SeekFrom::Start(HEADER_CLUSTER_BITS_OFFSET))
.unwrap();
let cluster_bits = file.read_u32::<BigEndian>().unwrap();
let cluster_size = 1u64 << cluster_bits;
file.seek(SeekFrom::Start(HEADER_L1_SIZE_OFFSET)).unwrap();
let l1_size = file.read_u32::<BigEndian>().unwrap();
file.seek(SeekFrom::Start(HEADER_L1_TABLE_OFFSET)).unwrap();
let l1_table_offset = file.read_u64::<BigEndian>().unwrap();
let entries_per_l2 = cluster_size / 8;
let mut append_offset = file.seek(SeekFrom::End(0)).unwrap();
append_offset = (append_offset + 511) & !511;
for l1_idx in 0..l1_size as u64 {
let l1_entry_offset = l1_table_offset + l1_idx * 8;
file.seek(SeekFrom::Start(l1_entry_offset)).unwrap();
let l1_entry = file.read_u64::<BigEndian>().unwrap();
let l2_table_addr = l1_entry & L1_L2_ADDR_MASK;
if l2_table_addr == 0 {
continue;
}
for l2_idx in 0..entries_per_l2 {
let l2_entry_offset = l2_table_addr + l2_idx * 8;
file.seek(SeekFrom::Start(l2_entry_offset)).unwrap();
let l2_entry = file.read_u64::<BigEndian>().unwrap();
if l2_entry & CLUSTER_USED_FLAG == 0 || l2_entry & COMPRESSED_FLAG != 0 {
continue;
}
let host_cluster_addr = l2_entry & L1_L2_ADDR_MASK;
if host_cluster_addr == 0 {
continue;
}
let mut cluster_data = vec![0u8; cluster_size as usize];
file.seek(SeekFrom::Start(host_cluster_addr)).unwrap();
file.read_exact(&mut cluster_data).unwrap();
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&cluster_data).unwrap();
let compressed = encoder.finish().unwrap();
file.seek(SeekFrom::Start(append_offset)).unwrap();
file.write_all(&compressed).unwrap();
// The L2 entry encodes the compressed size in units of
// 512 byte sectors. The reader decodes the sector count
// back and computes: nsectors * 512 - (addr & 511).
// Because addr is 512 aligned, this yields nsectors * 512
// which rounds up to the next sector boundary. The file
// must contain enough bytes for that rounded up pread.
let padded_len = (compressed.len() + 511) & !511;
if padded_len > compressed.len() {
let padding = vec![0u8; padded_len - compressed.len()];
file.write_all(&padding).unwrap();
}
let new_entry =
make_compressed_l2_entry(append_offset, compressed.len(), cluster_bits);
file.seek(SeekFrom::Start(l2_entry_offset)).unwrap();
file.write_u64::<BigEndian>(new_entry).unwrap();
append_offset += padded_len as u64;
}
}
file.flush().unwrap();
}
#[test]
fn test_pread_alloc() {
let temp = TempFile::new().unwrap();
let file = temp.as_file();
let data: Vec<u8> = (0..=255).cycle().take(4096).collect();
file.write_all_at(&data, 0).unwrap();
let buf = pread_alloc(file.as_raw_fd(), 0, 4096).unwrap();
assert_eq!(buf, data);
let buf = pread_alloc(file.as_raw_fd(), 100, 200).unwrap();
assert_eq!(buf, &data[100..300]);
pread_alloc(file.as_raw_fd(), 4000, 200).unwrap_err();
}
#[test]
fn test_decompress_cluster() {
let cluster_size = 65536;
let original: Vec<u8> = (0..=255).cycle().take(cluster_size).collect();
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&original).unwrap();
let compressed = encoder.finish().unwrap();
let result = decompress_cluster(&compressed, cluster_size, &ZlibDecoder {}).unwrap();
assert_eq!(result, original);
}
#[test]
fn test_decompress_cluster_corrupt_input() {
let corrupt = vec![0xffu8; 64];
let err = decompress_cluster(&corrupt, 65536, &ZlibDecoder {}).unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::EIO));
}
}

View File

@@ -1,286 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::io::Error;
use std::os::unix::io::{AsRawFd, RawFd};
use io_uring::{IoUring, opcode, types};
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
use crate::{BatchRequest, RequestType, SECTOR_SIZE};
pub struct RawFileAsync {
fd: RawFd,
io_uring: IoUring,
eventfd: EventFd,
alignment: u64,
}
impl RawFileAsync {
pub fn new(fd: RawFd, ring_depth: u32) -> BlockResult<Self> {
let io_uring =
IoUring::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let eventfd =
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
// Register the io_uring eventfd that will notify when something in
// the completion queue is ready.
io_uring
.submitter()
.register_eventfd(eventfd.as_raw_fd())
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
Ok(RawFileAsync {
fd,
io_uring,
eventfd,
alignment: SECTOR_SIZE,
})
}
}
impl AsyncIo for RawFileAsync {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn alignment(&self) -> u64 {
self.alignment
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Readv::new(types::Fd(self.fd), iovecs.as_ptr(), iovecs.len() as u32)
.offset(offset.try_into().unwrap())
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::ReadVectored(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter.submit().map_err(AsyncIoError::ReadVectored)?;
Ok(())
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Writev::new(types::Fd(self.fd), iovecs.as_ptr(), iovecs.len() as u32)
.offset(offset.try_into().unwrap())
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::WriteVectored(Error::other(format!(
"Submission queue is full: {e:?}"
)))
})?;
};
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter.submit().map_err(AsyncIoError::WriteVectored)?;
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
if let Some(user_data) = user_data {
let (submitter, mut sq, _) = self.io_uring.split();
// SAFETY: we know the file descriptor is valid.
unsafe {
sq.push(
&opcode::Fsync::new(types::Fd(self.fd))
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::Fsync(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter.submit().map_err(AsyncIoError::Fsync)?;
} else {
// SAFETY: FFI call with a valid fd
unsafe { libc::fsync(self.fd) };
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
self.io_uring
.completion()
.next()
.map(|entry| (entry.user_data(), entry.result()))
}
fn batch_requests_enabled(&self) -> bool {
true
}
fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> {
if !self.batch_requests_enabled() {
return Ok(());
}
let (submitter, mut sq, _) = self.io_uring.split();
let mut submitted = false;
// Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially
// successful push.
if batch_request.len() > sq.capacity() - sq.len() {
return Err(AsyncIoError::SubmitBatchRequests(Error::other(
"io_uring submission queue is full",
)));
}
for req in batch_request {
match req.request_type {
RequestType::In => {
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Readv::new(
types::Fd(self.fd),
req.iovecs.as_ptr(),
req.iovecs.len() as u32,
)
.offset(req.offset as u64)
.build()
.user_data(req.user_data),
)
.map_err(|e| {
AsyncIoError::ReadVectored(Error::other(format!(
"Submission queue is full: {e:?}"
)))
})?;
};
submitted = true;
}
RequestType::Out => {
// SAFETY: we know the file descriptor is valid and we
// relied on vm-memory to provide the buffer address.
unsafe {
sq.push(
&opcode::Writev::new(
types::Fd(self.fd),
req.iovecs.as_ptr(),
req.iovecs.len() as u32,
)
.offset(req.offset as u64)
.build()
.user_data(req.user_data),
)
.map_err(|e| {
AsyncIoError::WriteVectored(Error::other(format!(
"Submission queue is full: {e:?}"
)))
})?;
};
submitted = true;
}
_ => {
unreachable!("Unexpected batch request type: {:?}", req.request_type)
}
}
}
// Only submit if we actually queued something
if submitted {
// Update the submission queue and submit new operations to the
// io_uring instance.
sq.sync();
submitter
.submit()
.map_err(AsyncIoError::SubmitBatchRequests)?;
}
Ok(())
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
let (submitter, mut sq, _) = self.io_uring.split();
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
// SAFETY: The file descriptor is known to be valid.
unsafe {
sq.push(
&opcode::Fallocate::new(types::Fd(self.fd), length)
.offset(offset)
.mode(mode)
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::PunchHole(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
sq.sync();
submitter.submit().map_err(AsyncIoError::PunchHole)?;
Ok(())
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
let (submitter, mut sq, _) = self.io_uring.split();
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
// SAFETY: The file descriptor is known to be valid.
unsafe {
sq.push(
&opcode::Fallocate::new(types::Fd(self.fd), length)
.offset(offset)
.mode(mode)
.build()
.user_data(user_data),
)
.map_err(|e| {
AsyncIoError::WriteZeroes(Error::other(format!("Submission queue is full: {e:?}")))
})?;
};
sq.sync();
submitter.submit().map_err(AsyncIoError::WriteZeroes)?;
Ok(())
}
}

View File

@@ -1,218 +0,0 @@
// Copyright © 2023 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//
// Copyright © 2023 Crusoe Energy Systems LLC
//
use std::collections::VecDeque;
use std::os::unix::io::{AsRawFd, RawFd};
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
use vmm_sys_util::aio;
use vmm_sys_util::eventfd::EventFd;
use crate::SECTOR_SIZE;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
use crate::error::{BlockError, BlockErrorKind, BlockResult};
pub struct RawFileAsyncAio {
fd: RawFd,
ctx: aio::IoContext,
eventfd: EventFd,
alignment: u64,
completion_list: VecDeque<(u64, i32)>,
}
impl RawFileAsyncAio {
pub fn new(fd: RawFd, queue_depth: u32) -> BlockResult<Self> {
let eventfd =
EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
let ctx =
aio::IoContext::new(queue_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?;
Ok(RawFileAsyncAio {
fd,
ctx,
eventfd,
alignment: SECTOR_SIZE,
completion_list: VecDeque::new(),
})
}
}
impl AsyncIo for RawFileAsyncAio {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn alignment(&self) -> u64 {
self.alignment
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
let iocbs = [&mut aio::IoControlBlock {
aio_fildes: self.fd.as_raw_fd() as u32,
aio_lio_opcode: aio::IOCB_CMD_PREADV as u16,
aio_buf: iovecs.as_ptr() as u64,
aio_nbytes: iovecs.len() as u64,
aio_offset: offset,
aio_data: user_data,
aio_flags: aio::IOCB_FLAG_RESFD,
aio_resfd: self.eventfd.as_raw_fd() as u32,
..Default::default()
}];
let _ = self
.ctx
.submit(&iocbs[..])
.map_err(AsyncIoError::ReadVectored)?;
Ok(())
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
let iocbs = [&mut aio::IoControlBlock {
aio_fildes: self.fd.as_raw_fd() as u32,
aio_lio_opcode: aio::IOCB_CMD_PWRITEV as u16,
aio_buf: iovecs.as_ptr() as u64,
aio_nbytes: iovecs.len() as u64,
aio_offset: offset,
aio_data: user_data,
aio_flags: aio::IOCB_FLAG_RESFD,
aio_resfd: self.eventfd.as_raw_fd() as u32,
..Default::default()
}];
let _ = self
.ctx
.submit(&iocbs[..])
.map_err(AsyncIoError::WriteVectored)?;
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
if let Some(user_data) = user_data {
let iocbs = [&mut aio::IoControlBlock {
aio_fildes: self.fd.as_raw_fd() as u32,
aio_lio_opcode: aio::IOCB_CMD_FSYNC as u16,
aio_data: user_data,
aio_flags: aio::IOCB_FLAG_RESFD,
aio_resfd: self.eventfd.as_raw_fd() as u32,
..Default::default()
}];
let _ = self.ctx.submit(&iocbs[..]).map_err(AsyncIoError::Fsync)?;
} else {
// SAFETY: FFI call with a valid fd
unsafe { libc::fsync(self.fd) };
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
if self.completion_list.is_empty() {
// Drain pending AIO completions batched into the same queue.
let mut events = [aio::IoEvent::default(); 32];
let rc = self.ctx.get_events(0, &mut events, None).unwrap();
for event in &events[..rc] {
self.completion_list
.push_back((event.data, event.res as i32));
}
}
self.completion_list.pop_front()
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Linux AIO has no IOCB command for fallocate, so perform the operation
// synchronously and signal completion via the completion list, matching
// the pattern used by the sync backend (RawFileSync).
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
// SAFETY: FFI call with valid arguments
let result = unsafe {
libc::fallocate(
self.fd as libc::c_int,
mode,
offset as libc::off_t,
length as libc::off_t,
)
};
if result < 0 {
return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error()));
}
self.completion_list.push_back((user_data, result));
self.eventfd.write(1).unwrap();
Ok(())
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
// Linux AIO has no IOCB command for fallocate, so perform the operation
// synchronously and signal completion via the completion list, matching
// the pattern used by the sync backend (RawFileSync).
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
// SAFETY: FFI call with valid arguments
let result = unsafe {
libc::fallocate(
self.fd as libc::c_int,
mode,
offset as libc::off_t,
length as libc::off_t,
)
};
if result < 0 {
return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error()));
}
self.completion_list.push_back((user_data, result));
self.eventfd.write(1).unwrap();
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
use std::os::unix::io::AsRawFd;
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::raw_async_io_tests;
#[test]
fn test_punch_hole() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
raw_async_io_tests::test_punch_hole(&mut async_io, &mut file);
}
#[test]
fn test_write_zeroes() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
raw_async_io_tests::test_write_zeroes(&mut async_io, &mut file);
}
#[test]
fn test_punch_hole_multiple_operations() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap();
raw_async_io_tests::test_punch_hole_multiple_operations(&mut async_io, &mut file);
}
}

View File

@@ -1,187 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::collections::VecDeque;
use std::os::unix::io::RawFd;
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
use vmm_sys_util::eventfd::EventFd;
use crate::SECTOR_SIZE;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult};
pub struct RawFileSync {
fd: RawFd,
eventfd: EventFd,
completion_list: VecDeque<(u64, i32)>,
alignment: u64,
}
impl RawFileSync {
pub fn new(fd: RawFd) -> Self {
RawFileSync {
fd,
eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"),
completion_list: VecDeque::new(),
alignment: SECTOR_SIZE,
}
}
}
impl AsyncIo for RawFileSync {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn alignment(&self) -> u64 {
self.alignment
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
// SAFETY: FFI call with valid arguments
let result = unsafe {
libc::preadv(
self.fd as libc::c_int,
iovecs.as_ptr(),
iovecs.len() as libc::c_int,
offset,
)
};
if result < 0 {
return Err(AsyncIoError::ReadVectored(std::io::Error::last_os_error()));
}
self.completion_list.push_back((user_data, result as i32));
self.eventfd.write(1).unwrap();
Ok(())
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
// SAFETY: FFI call with valid arguments
let result = unsafe {
libc::pwritev(
self.fd as libc::c_int,
iovecs.as_ptr(),
iovecs.len() as libc::c_int,
offset,
)
};
if result < 0 {
return Err(AsyncIoError::WriteVectored(std::io::Error::last_os_error()));
}
self.completion_list.push_back((user_data, result as i32));
self.eventfd.write(1).unwrap();
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
// SAFETY: FFI call
let result = unsafe { libc::fsync(self.fd as libc::c_int) };
if result < 0 {
return Err(AsyncIoError::Fsync(std::io::Error::last_os_error()));
}
if let Some(user_data) = user_data {
self.completion_list.push_back((user_data, result));
self.eventfd.write(1).unwrap();
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
self.completion_list.pop_front()
}
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
// SAFETY: FFI call with valid arguments
let result = unsafe {
libc::fallocate(
self.fd as libc::c_int,
mode,
offset as libc::off_t,
length as libc::off_t,
)
};
if result < 0 {
return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error()));
}
self.completion_list.push_back((user_data, result));
self.eventfd.write(1).unwrap();
Ok(())
}
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
// SAFETY: FFI call with valid arguments
let result = unsafe {
libc::fallocate(
self.fd as libc::c_int,
mode,
offset as libc::off_t,
length as libc::off_t,
)
};
if result < 0 {
return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error()));
}
self.completion_list.push_back((user_data, result));
self.eventfd.write(1).unwrap();
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
use std::os::unix::io::AsRawFd;
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::raw_async_io_tests;
#[test]
fn test_punch_hole() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawFileSync::new(file.as_raw_fd());
raw_async_io_tests::test_punch_hole(&mut async_io, &mut file);
}
#[test]
fn test_write_zeroes() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawFileSync::new(file.as_raw_fd());
raw_async_io_tests::test_write_zeroes(&mut async_io, &mut file);
}
#[test]
fn test_punch_hole_multiple_operations() {
let temp_file = TempFile::new().unwrap();
let mut file = temp_file.into_file();
let mut async_io = RawFileSync::new(file.as_raw_fd());
raw_async_io_tests::test_punch_hole_multiple_operations(&mut async_io, &mut file);
}
}

104
block/src/sparse.rs Normal file
View File

@@ -0,0 +1,104 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
// Helpers for issuing `BLKDISCARD` / `BLKZEROOUT` ioctls on block devices,
// and the `punch_hole` / `write_zeroes` dispatchers used by the raw I/O
// backends.
//
// The kernel ioctl numbers and argument layout are stable userspace ABI
// (see `include/uapi/linux/fs.h`):
//
// ```c
// #define BLKDISCARD _IO(0x12, 119) /* arg: const __u64 range[2] = { start, len } */
// #define BLKZEROOUT _IO(0x12, 127) /* arg: const __u64 range[2] = { start, len } */
// ```
//
// The kernel does `copy_from_user(range, arg, sizeof(range))`, i.e. it reads
// 16 bytes through the single pointer it is given, so we must pass a single
// `__u64[2]` array rather than two separate `*const u64` pointers.
use std::io;
use std::os::unix::io::RawFd;
use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE};
// `_IO(0x12, 119)` — issue a discard request to a block device.
pub const BLKDISCARD: libc::c_ulong = 0x1277;
// `_IO(0x12, 127)` — write zeroes to a range of a block device, with a
// kernel-side fallback to writing zero pages when the hardware has no native
// `WRITE_ZEROES`.
pub const BLKZEROOUT: libc::c_ulong = 0x127f;
// Issue a `BLK*` range ioctl with proper `[start, len]` argument.
fn blk_range_ioctl(fd: RawFd, request: libc::c_ulong, offset: u64, length: u64) -> io::Result<()> {
let range: [u64; 2] = [offset, length];
// SAFETY: `fd` is a valid block-device fd owned by the caller; `&range`
// is a 16-byte array matching the kernel's expected `__u64[2]` layout
// and lives for the duration of the call.
let ret = unsafe { libc::ioctl(fd, request as _, &range) };
if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
// Discard (TRIM/UNMAP) the byte range `[offset, offset + length)` on the
// block device referenced by `fd`.
pub(crate) fn blkdiscard(fd: RawFd, offset: u64, length: u64) -> io::Result<()> {
blk_range_ioctl(fd, BLKDISCARD, offset, length)
}
// Zero the byte range `[offset, offset + length)` on the block device
// referenced by `fd`. The kernel falls back to writing explicit zero pages
// when the device has no hardware `WRITE_ZEROES`.
pub(crate) fn blkzeroout(fd: RawFd, offset: u64, length: u64) -> io::Result<()> {
blk_range_ioctl(fd, BLKZEROOUT, offset, length)
}
// Punch a hole in `fd` over the byte range `[offset, offset + length)`.
//
// On block devices the kernel rejects `fallocate(PUNCH_HOLE)` (notably ZFS
// zvols), so route through `BLKDISCARD` instead. On regular files use
// `fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)`.
pub(crate) fn punch_hole(fd: RawFd, is_blkdev: bool, offset: u64, length: u64) -> io::Result<()> {
if is_blkdev {
blkdiscard(fd, offset, length)
} else {
fallocate(
fd,
FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
offset,
length,
)
}
}
// Zero the byte range `[offset, offset + length)` in `fd`.
//
// Uses `BLKZEROOUT` on block devices (see [`punch_hole`] for the rationale)
// and `fallocate(FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE)` on regular
// files.
pub(crate) fn write_zeroes(fd: RawFd, is_blkdev: bool, offset: u64, length: u64) -> io::Result<()> {
if is_blkdev {
blkzeroout(fd, offset, length)
} else {
fallocate(
fd,
FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE,
offset,
length,
)
}
}
fn fallocate(fd: RawFd, mode: libc::c_int, offset: u64, length: u64) -> io::Result<()> {
// SAFETY: FFI call with a valid fd; fallocate touches no userspace memory.
let ret = unsafe { libc::fallocate(fd, mode, offset as libc::off_t, length as libc::off_t) };
if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

View File

@@ -1,180 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::collections::VecDeque;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::sync::{Arc, Mutex};
use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFileError};
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::vhdx::{Vhdx, VhdxError};
use crate::{AsyncAdaptor, BlockBackend, Error, disk_file};
#[derive(Debug)]
pub struct VhdxDiskSync {
// FIXME: The Mutex serializes all VHDX I/O operations across queues, which
// is necessary for correctness but eliminates any parallelism benefit from
// multiqueue. Vhdx::clone() shares the underlying file description across
// threads, so concurrent I/O from multiple queues races on the file offset
// causing data corruption.
//
// A proper fix would require restructuring the VHDX I/O path so that data
// operations can proceed in parallel with independent file descriptors.
vhdx_file: Arc<Mutex<Vhdx>>,
}
impl VhdxDiskSync {
pub fn new(f: File) -> BlockResult<Self> {
Ok(VhdxDiskSync {
vhdx_file: Arc::new(Mutex::new(Vhdx::new(f).map_err(|e| {
let kind = match &e {
VhdxError::NotVhdx(_)
| VhdxError::ParseVhdxHeader(_)
| VhdxError::ParseVhdxMetadata(_)
| VhdxError::ParseVhdxRegionEntry(_) => BlockErrorKind::InvalidFormat,
VhdxError::ReadBatEntry(_) => BlockErrorKind::CorruptImage,
VhdxError::ReadFailed(_) | VhdxError::WriteFailed(_) => BlockErrorKind::Io,
};
BlockError::new(kind, e).with_op(ErrorOp::Open)
})?)),
})
}
}
impl disk_file::DiskSize for VhdxDiskSync {
fn logical_size(&self) -> BlockResult<u64> {
Ok(self.vhdx_file.lock().unwrap().virtual_disk_size())
}
}
impl disk_file::PhysicalSize for VhdxDiskSync {
fn physical_size(&self) -> BlockResult<u64> {
self.vhdx_file
.lock()
.unwrap()
.physical_size()
.map_err(|e| match e {
Error::GetFileMetadata(io) => {
BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io))
}
_ => unreachable!("unexpected error from Vhdx::physical_size(): {e}"),
})
}
}
impl disk_file::DiskFd for VhdxDiskSync {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.vhdx_file.lock().unwrap().as_raw_fd())
}
}
impl disk_file::Geometry for VhdxDiskSync {}
impl disk_file::SparseCapable for VhdxDiskSync {}
impl disk_file::Resizable for VhdxDiskSync {
fn resize(&mut self, _size: u64) -> BlockResult<()> {
Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
DiskFileError::ResizeError(std::io::Error::other("resize not supported for VHDX")),
)
.with_op(ErrorOp::Resize))
}
}
impl disk_file::DiskFile for VhdxDiskSync {}
impl disk_file::AsyncDiskFile for VhdxDiskSync {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
Ok(Box::new(VhdxDiskSync {
vhdx_file: Arc::clone(&self.vhdx_file),
}))
}
fn create_async_io(&self, _ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
Ok(Box::new(VhdxSync::new(Arc::clone(&self.vhdx_file))))
}
}
pub struct VhdxSync {
vhdx_file: Arc<Mutex<Vhdx>>,
eventfd: EventFd,
completion_list: VecDeque<(u64, i32)>,
}
impl VhdxSync {
pub fn new(vhdx_file: Arc<Mutex<Vhdx>>) -> Self {
VhdxSync {
vhdx_file,
eventfd: EventFd::new(libc::EFD_NONBLOCK)
.expect("Failed creating EventFd for VhdxSync"),
completion_list: VecDeque::new(),
}
}
}
impl AsyncAdaptor for Vhdx {}
impl AsyncIo for VhdxSync {
fn notifier(&self) -> &EventFd {
&self.eventfd
}
fn read_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
self.vhdx_file.lock().unwrap().read_vectored_sync(
offset,
iovecs,
user_data,
&self.eventfd,
&mut self.completion_list,
)
}
fn write_vectored(
&mut self,
offset: libc::off_t,
iovecs: &[libc::iovec],
user_data: u64,
) -> AsyncIoResult<()> {
self.vhdx_file.lock().unwrap().write_vectored_sync(
offset,
iovecs,
user_data,
&self.eventfd,
&mut self.completion_list,
)
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
self.vhdx_file.lock().unwrap().fsync_sync(
user_data,
&self.eventfd,
&mut self.completion_list,
)
}
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
self.completion_list.pop_front()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::PunchHole(std::io::Error::other(
"punch_hole not supported for VHDX",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(std::io::Error::other(
"write_zeroes not supported for VHDX",
)))
}
}

1
clippy.toml Normal file
View File

@@ -0,0 +1 @@
absolute-paths-allowed-crates = ["arch"]

View File

@@ -8,15 +8,13 @@ homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
license = "Apache-2.0 AND BSD-3-Clause"
name = "cloud-hypervisor"
rust-version.workspace = true
version = "52.0.0"
version = "53.0.0"
[dependencies]
anyhow = { workspace = true }
api_client = { path = "../api_client" }
clap = { workspace = true, features = ["string"] }
dhat = { workspace = true, optional = true }
env_logger = { workspace = true }
epoll = { workspace = true }
event_monitor = { path = "../event_monitor" }
hypervisor = { path = "../hypervisor" }
jiff = { workspace = true }
@@ -26,10 +24,10 @@ option_parser = { path = "../option_parser" }
seccompiler = { workspace = true }
serde_json = { workspace = true }
signal-hook = { workspace = true }
signal-hook-registry = { workspace = true }
thiserror = { workspace = true }
tpm = { path = "../tpm" }
tracer = { path = "../tracer" }
vm-memory = { workspace = true }
vm-migration = { path = "../vm-migration" }
vmm = { path = "../vmm" }
vmm-sys-util = { workspace = true }
zbus = { version = "5.15.0", optional = true }
@@ -50,13 +48,13 @@ default = ["io_uring", "kvm"]
dhat-heap = ["dhat", "vmm/dhat-heap"] # For heap profiling
fw_cfg = ["vmm/fw_cfg"]
guest_debug = ["vmm/guest_debug"]
igvm = ["mshv", "vmm/igvm"]
igvm = ["vmm/igvm"]
io_uring = ["vmm/io_uring"]
ivshmem = ["vmm/ivshmem"]
kvm = ["vmm/kvm"]
mshv = ["vmm/mshv"]
pvmemcontrol = ["vmm/pvmemcontrol"]
sev_snp = ["igvm", "mshv", "vmm/sev_snp"]
sev_snp = ["igvm", "vmm/sev_snp"]
tdx = ["vmm/tdx"]
tracing = ["tracer/tracing", "vmm/tracing"]

View File

@@ -7,10 +7,10 @@
#[path = "../test_util.rs"]
mod test_util;
use std::io::Read;
use std::io::{self, Read};
use std::marker::PhantomData;
use std::os::unix::net::UnixStream;
use std::process;
use std::{error, fs, iter, num, process};
use api_client::{
Error as ApiClientError, simple_api_command, simple_api_command_with_fds,
@@ -22,13 +22,14 @@ use clap::{Arg, ArgMatches, Command};
use log::error;
use option_parser::{ByteSized, ByteSizedParseError};
use thiserror::Error;
use vmm::config::RestoreConfig;
use vmm::api;
use vmm::config::{self, RestoreConfig};
use vmm::vm_config::{
DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig,
UserDeviceConfig, VdpaConfig, VsockConfig,
};
#[cfg(feature = "dbus_api")]
use zbus::{proxy, zvariant::Optional};
use zbus::{blocking::Connection, proxy, zvariant::Optional};
type ApiResult = Result<(), Error>;
@@ -40,39 +41,41 @@ enum Error {
#[error("dbus api client error")]
DBusApiClient(#[source] zbus::Error),
#[error("Error parsing CPU count")]
InvalidCpuCount(#[source] std::num::ParseIntError),
InvalidCpuCount(#[source] num::ParseIntError),
#[error("Error parsing memory size")]
InvalidMemorySize(#[source] ByteSizedParseError),
#[error("Error parsing balloon size")]
InvalidBalloonSize(#[source] ByteSizedParseError),
#[error("Error parsing device syntax")]
AddDeviceConfig(#[source] vmm::config::Error),
AddDeviceConfig(#[source] config::Error),
#[error("Error parsing disk syntax")]
AddDiskConfig(#[source] vmm::config::Error),
AddDiskConfig(#[source] config::Error),
#[error("Error parsing filesystem syntax")]
AddFsConfig(#[source] vmm::config::Error),
AddFsConfig(#[source] config::Error),
#[error("Error parsing generic vhost-user syntax")]
AddGenericVhostUserConfig(#[source] vmm::config::Error),
AddGenericVhostUserConfig(#[source] config::Error),
#[error("Error parsing persistent memory syntax")]
AddPmemConfig(#[source] vmm::config::Error),
AddPmemConfig(#[source] config::Error),
#[error("Error parsing network syntax")]
AddNetConfig(#[source] vmm::config::Error),
AddNetConfig(#[source] config::Error),
#[error("Error parsing user device syntax")]
AddUserDeviceConfig(#[source] vmm::config::Error),
AddUserDeviceConfig(#[source] config::Error),
#[error("Error parsing vDPA device syntax")]
AddVdpaConfig(#[source] vmm::config::Error),
AddVdpaConfig(#[source] config::Error),
#[error("Error parsing vsock syntax")]
AddVsockConfig(#[source] vmm::config::Error),
AddVsockConfig(#[source] config::Error),
#[error("Error parsing restore syntax")]
Restore(#[source] vmm::config::Error),
Restore(#[source] config::Error),
#[error("Error reading from stdin")]
ReadingStdin(#[source] std::io::Error),
ReadingStdin(#[source] io::Error),
#[error("Error reading from file")]
ReadingFile(#[source] std::io::Error),
ReadingFile(#[source] io::Error),
#[error("Invalid disk size")]
InvalidDiskSize(#[source] ByteSizedParseError),
#[error("Error parsing receive migration configuration")]
ReceiveMigrationConfig(#[from] api::VmReceiveMigrationConfigError),
#[error("Error parsing send migration configuration")]
SendMigrationConfig(#[from] vmm::api::VmSendMigrationConfigError),
SendMigrationConfig(#[from] api::VmSendMigrationConfigError),
}
enum TargetApi<'a> {
@@ -122,9 +125,9 @@ trait DBusApi1 {
impl<'a> DBusApi1ProxyBlocking<'a> {
fn new_connection(name: &'a str, path: &'a str, system_bus: bool) -> Result<Self, zbus::Error> {
let connection = if system_bus {
zbus::blocking::Connection::system()?
Connection::system()?
} else {
zbus::blocking::Connection::session()?
Connection::session()?
};
Self::builder(&connection)
@@ -369,14 +372,14 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu
.map_err(Error::HttpApiClient)
}
Some("add-device") => {
let device_config = add_device_config(
let (device_config, fds) = add_device_config(
matches
.subcommand_matches("add-device")
.unwrap()
.get_one::<String>("device_config")
.unwrap(),
)?;
simple_api_command(socket, "PUT", "add-device", Some(&device_config))
simple_api_command_with_fds(socket, "PUT", "add-device", Some(&device_config), &fds)
.map_err(Error::HttpApiClient)
}
Some("remove-device") => {
@@ -534,7 +537,7 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu
.unwrap()
.get_one::<String>("receive_migration_config")
.unwrap(),
);
)?;
simple_api_command(
socket,
"PUT",
@@ -607,7 +610,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>)
proxy.api_vm_resize_zone(&resize_zone)
}
Some("add-device") => {
let device_config = add_device_config(
let (device_config, _fds) = add_device_config(
matches
.subcommand_matches("add-device")
.unwrap()
@@ -753,7 +756,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>)
.unwrap()
.get_one::<String>("receive_migration_config")
.unwrap(),
);
)?;
proxy.api_vm_receive_migration(&receive_migration_data)
}
Some("create") => {
@@ -803,7 +806,7 @@ fn resize_config(
None
};
let resize = vmm::api::VmResizeData {
let resize = api::VmResizeData {
desired_vcpus,
desired_ram,
desired_balloon,
@@ -813,7 +816,7 @@ fn resize_config(
}
fn resize_disk_config(id: &str, size: &str) -> Result<String, Error> {
let resize_disk = vmm::api::VmResizeDiskData {
let resize_disk = api::VmResizeDiskData {
id: id.to_owned(),
desired_size: size.parse::<ByteSized>().map_err(Error::InvalidDiskSize)?.0,
};
@@ -822,7 +825,7 @@ fn resize_disk_config(id: &str, size: &str) -> Result<String, Error> {
}
fn resize_zone_config(id: &str, size: &str) -> Result<String, Error> {
let resize_zone = vmm::api::VmResizeZoneData {
let resize_zone = api::VmResizeZoneData {
id: id.to_owned(),
desired_ram: size
.parse::<ByteSized>()
@@ -833,11 +836,21 @@ fn resize_zone_config(id: &str, size: &str) -> Result<String, Error> {
Ok(serde_json::to_string(&resize_zone).unwrap())
}
fn add_device_config(config: &str) -> Result<String, Error> {
let device_config = DeviceConfig::parse(config).map_err(Error::AddDeviceConfig)?;
fn add_device_config(config: &str) -> Result<(String, Vec<i32>), Error> {
let mut device_config = DeviceConfig::parse(config).map_err(Error::AddDeviceConfig)?;
// DeviceConfig is modified on purpose here by taking the file
// descriptor out. Keeping it and sending it over to the server side
// process would not make any sense since the file descriptor may be
// represented with different values.
let fds = device_config
.fd
.take()
.map(|fd| vec![fd])
.unwrap_or_default();
let device_config = serde_json::to_string(&device_config).unwrap();
Ok(device_config)
Ok((device_config, fds))
}
fn add_user_device_config(config: &str) -> Result<String, Error> {
@@ -848,7 +861,7 @@ fn add_user_device_config(config: &str) -> Result<String, Error> {
}
fn remove_device_config(id: &str) -> String {
let remove_device_data = vmm::api::VmRemoveDeviceData { id: id.to_owned() };
let remove_device_data = api::VmRemoveDeviceData { id: id.to_owned() };
serde_json::to_string(&remove_device_data).unwrap()
}
@@ -910,7 +923,7 @@ fn add_vsock_config(config: &str) -> Result<String, Error> {
}
fn snapshot_config(url: &str) -> String {
let snapshot_config = vmm::api::VmSnapshotConfig {
let snapshot_config = api::VmSnapshotConfig {
destination_url: String::from(url),
};
@@ -934,24 +947,22 @@ fn restore_config(config: &str) -> Result<(String, Vec<i32>), Error> {
}
fn coredump_config(destination_url: &str) -> String {
let coredump_config = vmm::api::VmCoredumpData {
let coredump_config = api::VmCoredumpData {
destination_url: String::from(destination_url),
};
serde_json::to_string(&coredump_config).unwrap()
}
fn receive_migration_data(url: &str) -> String {
let receive_migration_data = vmm::api::VmReceiveMigrationData {
receiver_url: url.to_owned(),
};
serde_json::to_string(&receive_migration_data).unwrap()
fn receive_migration_data(config: &str) -> Result<String, Error> {
let receive_migration_data =
api::VmReceiveMigrationData::parse(config).map_err(Error::ReceiveMigrationConfig)?;
Ok(serde_json::to_string(&receive_migration_data).unwrap())
}
fn send_migration_data(config: &str) -> Result<String, Error> {
let send_migration_data =
vmm::api::VmSendMigrationData::parse(config).map_err(Error::SendMigrationConfig)?;
api::VmSendMigrationData::parse(config).map_err(Error::SendMigrationConfig)?;
let send_migration_config = serde_json::to_string(&send_migration_data).unwrap();
Ok(send_migration_config)
}
@@ -959,11 +970,11 @@ fn send_migration_data(config: &str) -> Result<String, Error> {
fn create_data(path: &str) -> Result<String, Error> {
let mut data = String::default();
if path == "-" {
std::io::stdin()
io::stdin()
.read_to_string(&mut data)
.map_err(Error::ReadingStdin)?;
} else {
data = std::fs::read_to_string(path).map_err(Error::ReadingFile)?;
data = fs::read_to_string(path).map_err(Error::ReadingFile)?;
}
Ok(data)
@@ -1014,28 +1025,20 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
.arg(Arg::new("disk_config").index(1).help(DiskConfig::SYNTAX)),
Command::new("add-fs")
.about("Add virtio-fs backed fs device")
.arg(
Arg::new("fs_config")
.index(1)
.help(vmm::vm_config::FsConfig::SYNTAX),
),
.arg(Arg::new("fs_config").index(1).help(FsConfig::SYNTAX)),
Command::new("add-generic-vhost-user")
.about("Add generic vhost-user device")
.arg(
Arg::new("generic_vhost_user_config")
.index(1)
.help(vmm::vm_config::GenericVhostUserConfig::SYNTAX),
.help(GenericVhostUserConfig::SYNTAX),
),
Command::new("add-net")
.about("Add network device")
.arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)),
Command::new("add-pmem")
.about("Add persistent memory device")
.arg(
Arg::new("pmem_config")
.index(1)
.help(vmm::vm_config::PmemConfig::SYNTAX),
),
.arg(Arg::new("pmem_config").index(1).help(PmemConfig::SYNTAX)),
Command::new("add-user-device")
.about("Add userspace device")
.arg(
@@ -1069,7 +1072,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
.arg(
Arg::new("receive_migration_config")
.index(1)
.help("<receiver_url>"),
.help(api::VmReceiveMigrationData::SYNTAX),
),
Command::new("remove-device")
.about("Remove VFIO and PCI device")
@@ -1127,6 +1130,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
.arg(
Arg::new("restore_config")
.index(1)
.required(true)
.help(RestoreConfig::SYNTAX),
),
Command::new("resume").about("Resume the VM"),
@@ -1135,7 +1139,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
.arg(
Arg::new("send_migration_config")
.index(1)
.help(vmm::api::VmSendMigrationData::SYNTAX),
.help(api::VmSendMigrationData::SYNTAX),
),
Command::new("shutdown").about("Shutdown the VM"),
Command::new("shutdown-vmm").about("Shutdown the VMM"),
@@ -1144,6 +1148,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> {
.arg(
Arg::new("snapshot_config")
.index(1)
.required(true)
.help("<destination_url>"),
),
]
@@ -1217,7 +1222,7 @@ fn main() {
if let Err(top_error) = target_api.do_command(&matches) {
// Helper to join strings with a newline.
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
fn join_strs(mut acc: String, next: String) -> String {
if !acc.is_empty() {
acc.push('\n');
@@ -1237,7 +1242,7 @@ fn main() {
fn server_api_error_display_modifier(
level: usize,
indention: usize,
error: &(dyn std::error::Error + 'static),
error: &(dyn error::Error + 'static),
) -> Option<String> {
if let Some(api_client::Error::ServerResponse(status_code, body)) =
error.downcast_ref::<api_client::Error>()
@@ -1245,7 +1250,7 @@ fn main() {
let body = body.as_ref().map_or("", |body| body.as_str());
// Retrieve the list of error messages back.
let lines: Vec<&str> = match serde_json::from_str(body) {
let lines: Vec<String> = match serde_json::from_str(body) {
Ok(json) => json,
Err(e) => {
return Some(format!(
@@ -1257,7 +1262,8 @@ fn main() {
let error_status = format!("Server responded with {status_code:?}");
// Prepend the error status line to the lines iter.
let lines = std::iter::once(error_status.as_str()).chain(lines);
let lines =
iter::once(error_status.as_str()).chain(lines.iter().map(|s| s.as_str()));
let error_msg_multiline = lines
.enumerate()
.map(|(index, error_msg)| (index + level, error_msg))
@@ -1275,7 +1281,7 @@ fn main() {
None
}
let top_error: &dyn std::error::Error = &top_error;
let top_error: &dyn error::Error = &top_error;
cloud_hypervisor::cli_print_error_chain(
top_error,
"ch-remote",
@@ -1319,4 +1325,17 @@ mod unit_tests {
assert_args_sorted(|| command.get_arguments());
}
}
#[test]
fn test_error_deserialization() {
let body = r#"["Error from API","The VM could not be snapshotted","Cannot send VM snapshot","Failed to send migratable component snapshot","Destination is not a directory: \"/tmp/ch.dump\""]"#;
let lines: Result<Vec<String>, _> = serde_json::from_str(body);
assert!(lines.is_ok());
let lines = lines.unwrap();
assert_eq!(lines.len(), 5);
assert_eq!(
lines[4],
r#"Destination is not a directory: "/tmp/ch.dump""#
);
}
}

View File

@@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
use std::error::Error;
use std::iter;
use log::error;
@@ -28,7 +29,7 @@ pub fn cli_print_error_chain<'a>(
eprintln!(" {top_error}");
} else {
eprintln!("chain of errors:");
std::iter::successors(Some(top_error), |sub_error| {
iter::successors(Some(top_error), |sub_error| {
// Dereference necessary to mitigate rustc compiler bug.
// See <https://github.com/rust-lang/rust/issues/141673>
(*sub_error).source()

View File

@@ -3,11 +3,13 @@
// SPDX-License-Identifier: Apache-2.0
//
use std::io::Write;
use std::io::{self, Write};
use std::str::FromStr;
use std::sync::Mutex;
use std::time::Instant;
use std::{mem, process, thread};
use jiff::tz::TimeZone;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -20,30 +22,82 @@ pub enum Error {
UnknownToken(String),
}
/// Which time source a date/time field should be read from.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Zone {
Utc,
Local,
}
/// An individual broken-down date/time field.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum TimeField {
Year,
Month,
Day,
Hour,
Minute,
Second,
Micros,
/// Timezone offset like `-08:00` (always `+00:00` for `Zone::Utc`).
Offset,
}
enum Token {
Literal(String),
BootTime,
/// Wallclock using RFC 3339 formatting.
WallClock,
/// UTC glog-style timestamp (e.g. `0521 08:02:15.542701`).
Glog,
/// Local-time glog-style timestamp (e.g. `0521 08:02:15.542701`).
LocalGlog,
Pid,
Tid,
Thread,
/// Full level word (e.g. `INFO`).
Level,
/// Single-letter level character, glog style (e.g. `I`).
LevelChar,
Location,
Msg,
/// A broken-down date/time field from either UTC or local wallclock.
Time(TimeField, Zone),
}
impl FromStr for Token {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Detect `local`-prefixed variants for the broken-down time fields.
let (name, zone) = match s.strip_prefix("local") {
Some(rest) => (rest, Zone::Local),
None => (s, Zone::Utc),
};
match name {
"year" => return Ok(Self::Time(TimeField::Year, zone)),
"month" => return Ok(Self::Time(TimeField::Month, zone)),
"day" => return Ok(Self::Time(TimeField::Day, zone)),
"hour" => return Ok(Self::Time(TimeField::Hour, zone)),
"minute" => return Ok(Self::Time(TimeField::Minute, zone)),
"second" => return Ok(Self::Time(TimeField::Second, zone)),
"micros" => return Ok(Self::Time(TimeField::Micros, zone)),
"offset" => return Ok(Self::Time(TimeField::Offset, zone)),
_ => {}
}
// Fall back to tokens that don't take a `local` prefix.
match s {
"boottime" => Ok(Self::BootTime),
"wallclock" => Ok(Self::WallClock),
"glog" => Ok(Self::Glog),
"localglog" => Ok(Self::LocalGlog),
"pid" => Ok(Self::Pid),
"tid" => Ok(Self::Tid),
"thread" => Ok(Self::Thread),
"level" => Ok(Self::Level),
"levelchar" => Ok(Self::LevelChar),
"location" => Ok(Self::Location),
"msg" => Ok(Self::Msg),
_ => Err(Error::UnknownToken(s.to_string())),
@@ -51,6 +105,34 @@ impl FromStr for Token {
}
}
/// Convert a `log::Level` to its glog single-letter abbreviation.
fn level_char(level: log::Level) -> char {
match level {
log::Level::Error => 'E',
log::Level::Warn => 'W',
log::Level::Info => 'I',
log::Level::Debug => 'D',
log::Level::Trace => 'T',
}
}
fn write_time_field<W: Write + ?Sized>(
out: &mut W,
field: TimeField,
zoned: &jiff::Zoned,
) -> io::Result<()> {
match field {
TimeField::Year => write!(out, "{:04}", zoned.year()),
TimeField::Month => write!(out, "{:02}", zoned.month()),
TimeField::Day => write!(out, "{:02}", zoned.day()),
TimeField::Hour => write!(out, "{:02}", zoned.hour()),
TimeField::Minute => write!(out, "{:02}", zoned.minute()),
TimeField::Second => write!(out, "{:02}", zoned.second()),
TimeField::Micros => write!(out, "{:06}", zoned.subsec_nanosecond() / 1000),
TimeField::Offset => write!(out, "{}", zoned.strftime("%:z")),
}
}
fn parse_format(fmt: &str) -> Result<Vec<Token>, Error> {
let mut tokens = Vec::new();
let mut literal = String::new();
@@ -66,7 +148,7 @@ fn parse_format(fmt: &str) -> Result<Vec<Token>, Error> {
}
if !literal.is_empty() {
tokens.push(Token::Literal(std::mem::take(&mut literal)));
tokens.push(Token::Literal(mem::take(&mut literal)));
}
let mut name = String::new();
@@ -105,6 +187,9 @@ pub struct Logger {
start: Instant,
pid: u32,
tokens: Vec<Token>,
// Saving the timezone when Logger is constructed avoids potential seccomp violations when the
// internal libc timezone cache expires as the affected thread is unpredictable.
local_tz: TimeZone,
}
impl Logger {
@@ -112,8 +197,9 @@ impl Logger {
Ok(Self {
output: Mutex::new(output),
start: Instant::now(),
pid: std::process::id(),
pid: process::id(),
tokens: parse_format(format)?,
local_tz: TimeZone::try_system().unwrap_or(TimeZone::UTC),
})
}
}
@@ -129,6 +215,10 @@ impl log::Log for Logger {
}
let duration_s = Instant::now().duration_since(self.start).as_secs_f32();
// Compute the wallclock timestamps lazily, but at most once per record so
// that multiple `{hour}`/`{minute}`/`{second}`/etc. fields stay coherent.
let mut zoned_utc: Option<jiff::Zoned> = None;
let mut zoned_local: Option<jiff::Zoned> = None;
let mut out = self.output.lock().unwrap();
for token in &self.tokens {
let _ = match token {
@@ -136,7 +226,20 @@ impl log::Log for Logger {
// 10: 6 decimal places + sep => whole seconds in range `0..=999` properly aligned
Token::BootTime => write!(&mut *out, "{duration_s:>10.6?}"),
Token::WallClock => {
write!(out, "{:.6}", jiff::Timestamp::now())
let zoned = zoned_utc
.get_or_insert_with(|| jiff::Timestamp::now().to_zoned(TimeZone::UTC));
write!(&mut *out, "{:.6}", zoned.timestamp())
}
Token::Glog => {
let zoned = zoned_utc
.get_or_insert_with(|| jiff::Timestamp::now().to_zoned(TimeZone::UTC));
write!(&mut *out, "{}", zoned.strftime("%m%d %H:%M:%S%.6f"))
}
Token::LocalGlog => {
let zoned = zoned_local.get_or_insert_with(|| {
jiff::Timestamp::now().to_zoned(self.local_tz.clone())
});
write!(&mut *out, "{}", zoned.strftime("%m%d %H:%M:%S%.6f"))
}
Token::Pid => write!(&mut *out, "{}", self.pid),
// SAFETY: gettid(2) always succeeds
@@ -144,14 +247,25 @@ impl log::Log for Logger {
Token::Thread => write!(
&mut *out,
"{}",
std::thread::current().name().unwrap_or("anonymous")
thread::current().name().unwrap_or("anonymous")
),
Token::Level => write!(&mut *out, "{}", record.level()),
Token::LevelChar => write!(&mut *out, "{}", level_char(record.level())),
Token::Location => match (record.file(), record.line()) {
(Some(file), Some(line)) => write!(&mut *out, "{file}:{line}"),
_ => write!(&mut *out, "{}", record.target()),
},
Token::Msg => write!(&mut *out, "{}", record.args()),
Token::Time(field, zone) => {
let zoned = match zone {
Zone::Utc => zoned_utc
.get_or_insert_with(|| jiff::Timestamp::now().to_zoned(TimeZone::UTC)),
Zone::Local => zoned_local.get_or_insert_with(|| {
jiff::Timestamp::now().to_zoned(self.local_tz.clone())
}),
};
write_time_field(&mut *out, *field, zoned)
}
};
}
let _ = out.write_all(b"\r\n");
@@ -198,12 +312,32 @@ mod tests {
Token::Literal(s) => format!("L({s})"),
Token::BootTime => "B".to_string(),
Token::WallClock => "W".to_string(),
Token::Glog => "G".to_string(),
Token::LocalGlog => "LG".to_string(),
Token::Pid => "P".to_string(),
Token::Tid => "I".to_string(),
Token::Thread => "T".to_string(),
Token::Level => "V".to_string(),
Token::LevelChar => "VC".to_string(),
Token::Location => "O".to_string(),
Token::Msg => "M".to_string(),
Token::Time(field, zone) => {
let z = match zone {
Zone::Utc => "u",
Zone::Local => "l",
};
let f = match field {
TimeField::Year => "Y",
TimeField::Month => "Mo",
TimeField::Day => "D",
TimeField::Hour => "H",
TimeField::Minute => "Mi",
TimeField::Second => "S",
TimeField::Micros => "U",
TimeField::Offset => "Z",
};
format!("T({z}:{f})")
}
})
.collect::<Vec<_>>()
.join("|")
@@ -224,12 +358,12 @@ mod tests {
#[test]
fn parse_all_known_tokens() {
let tokens = parse_format(
"[{boottime}] {wallclock} {pid}/{tid} <{thread}> {level} {location} -- {msg}",
"[{boottime}] {wallclock} {glog} {localglog} {pid}/{tid} <{thread}> {level} {levelchar} {location} -- {msg}",
)
.unwrap();
assert_eq!(
render(&tokens),
"L([)|B|L(] )|W|L( )|P|L(/)|I|L( <)|T|L(> )|V|L( )|O|L( -- )|M"
"L([)|B|L(] )|W|L( )|G|L( )|LG|L( )|P|L(/)|I|L( <)|T|L(> )|V|L( )|VC|L( )|O|L( -- )|M"
);
}
@@ -377,6 +511,134 @@ mod tests {
assert!(out.ends_with('Z'), "got: {out}");
}
#[test]
fn logger_glog_style_output() {
// `{levelchar}{localglog}` => glog-style header like `I0521 08:02:15.542701`.
let buf = SharedBuffer::default();
let logger = Logger::new(Box::new(buf.clone()), "{levelchar}{localglog}").unwrap();
logger.log(
&log::Record::builder()
.args(format_args!(""))
.level(log::Level::Info)
.target("t")
.build(),
);
let out = buf.contents();
let out = out.trim();
// `IMMDD HH:MM:SS.uuuuuu` => 21 chars.
assert_eq!(out.len(), 21, "got: {out}");
assert_eq!(&out[0..1], "I", "got: {out}");
assert_eq!(&out[5..6], " ", "got: {out}");
assert_eq!(&out[8..9], ":", "got: {out}");
assert_eq!(&out[11..12], ":", "got: {out}");
assert_eq!(&out[14..15], ".", "got: {out}");
// Every non-separator character is an ASCII digit.
for (i, ch) in out.chars().enumerate() {
if [0, 5, 8, 11, 14].contains(&i) {
continue;
}
assert!(ch.is_ascii_digit(), "non-digit at {i}: got {out}");
}
}
#[test]
fn logger_glog_utc_output_shape() {
// `{glog}` alone produces `MMDD HH:MM:SS.uuuuuu` (20 chars).
let buf = SharedBuffer::default();
let logger = Logger::new(Box::new(buf.clone()), "{glog}").unwrap();
logger.log(
&log::Record::builder()
.args(format_args!(""))
.level(log::Level::Info)
.target("t")
.build(),
);
let out = buf.contents();
let out = out.trim();
assert_eq!(out.len(), 20, "got: {out}");
assert_eq!(&out[4..5], " ", "got: {out}");
assert_eq!(&out[7..8], ":", "got: {out}");
assert_eq!(&out[10..11], ":", "got: {out}");
assert_eq!(&out[13..14], ".", "got: {out}");
}
#[test]
fn parse_utc_time_fields() {
let tokens =
parse_format("{year}-{month}-{day}T{hour}:{minute}:{second}.{micros}{offset}").unwrap();
assert_eq!(
render(&tokens),
"T(u:Y)|L(-)|T(u:Mo)|L(-)|T(u:D)|L(T)|T(u:H)|L(:)|T(u:Mi)|L(:)|T(u:S)|L(.)|T(u:U)|T(u:Z)"
);
}
#[test]
fn parse_local_time_fields() {
let tokens = parse_format(
"{localyear}-{localmonth}-{localday}T{localhour}:{localminute}:{localsecond}.{localmicros}{localoffset}",
)
.unwrap();
assert_eq!(
render(&tokens),
"T(l:Y)|L(-)|T(l:Mo)|L(-)|T(l:D)|L(T)|T(l:H)|L(:)|T(l:Mi)|L(:)|T(l:S)|L(.)|T(l:U)|T(l:Z)"
);
}
#[test]
fn logger_utc_offset_is_zero() {
let buf = SharedBuffer::default();
let logger = Logger::new(Box::new(buf.clone()), "{offset}").unwrap();
logger.log(
&log::Record::builder()
.args(format_args!(""))
.level(log::Level::Info)
.target("t")
.build(),
);
assert_eq!(buf.contents().trim(), "+00:00");
}
#[test]
fn logger_utc_year_matches_jiff() {
let buf = SharedBuffer::default();
let logger = Logger::new(Box::new(buf.clone()), "{year}").unwrap();
logger.log(
&log::Record::builder()
.args(format_args!(""))
.level(log::Level::Info)
.target("t")
.build(),
);
let year: i32 = buf.contents().trim().parse().expect("year is numeric");
assert!(year >= 2024, "got: {year}");
}
#[test]
fn logger_levelchar_per_level() {
for (level, expected) in [
(log::Level::Error, "E"),
(log::Level::Warn, "W"),
(log::Level::Info, "I"),
(log::Level::Debug, "D"),
(log::Level::Trace, "T"),
] {
let buf = SharedBuffer::default();
let logger = Logger::new(Box::new(buf.clone()), "{levelchar}").unwrap();
logger.log(
&log::Record::builder()
.args(format_args!(""))
.level(level)
.target("t")
.build(),
);
assert_eq!(buf.contents().trim(), expected);
}
}
#[test]
fn logger_pid_token() {
let buf = SharedBuffer::default();
@@ -392,7 +654,7 @@ mod tests {
let out = buf.contents();
let out = out.trim();
assert_eq!(out, std::process::id().to_string(), "got: {out}");
assert_eq!(out, process::id().to_string(), "got: {out}");
}
#[test]

View File

@@ -7,10 +7,13 @@ mod logger;
#[cfg(test)]
mod test_util;
use std::fs::File;
use std::fs::{self, File};
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::path::Path;
#[cfg(feature = "guest_debug")]
use std::path::PathBuf;
use std::sync::mpsc::channel;
use std::{env, io};
use std::{any, cmp, env, io, num, process, str, thread};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
use event_monitor::event;
@@ -19,13 +22,16 @@ use log::{LevelFilter, error, info, warn};
use option_parser::OptionParser;
use seccompiler::SeccompAction;
use signal_hook::consts::SIGSYS;
use signal_hook::low_level;
use thiserror::Error;
use vmm::api::ApiAction;
use vm_migration::protocol;
#[cfg(feature = "dbus_api")]
use vmm::api::dbus::{DBusApiOptions, dbus_api_graceful_shutdown};
use vmm::api::http::http_api_graceful_shutdown;
use vmm::config::{RestoreConfig, VmParams};
use vmm::api::{self, ApiAction};
use vmm::config::{self, RestoreConfig, VmParams};
use vmm::landlock::{Landlock, LandlockError};
use vmm::vm::Vm;
use vmm::vm_config;
#[cfg(feature = "fw_cfg")]
use vmm::vm_config::FwCfgConfig;
@@ -34,14 +40,49 @@ use vmm::vm_config::IvshmemConfig;
use vmm::vm_config::{
BalloonConfig, ConsoleConfig, DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig,
LandlockConfig, NetConfig, NumaConfig, PciSegmentConfig, PlatformConfig, PmemConfig,
RateLimiterGroupConfig, RngConfig, SerialConfig, TpmConfig, UserDeviceConfig, VdpaConfig,
VmConfig, VsockConfig,
RateLimiterGroupConfig, RngConfig, RtcConfig, SerialConfig, TpmConfig, UserDeviceConfig,
VdpaConfig, VmConfig, VsockConfig,
};
use vmm_sys_util::eventfd::EventFd;
use vmm_sys_util::signal::block_signal;
use crate::logger::Logger;
// Linux exposes seccomp's SIGSYS payload via the siginfo_t layout; this struct mirrors the
// fields we need so the handler can read the syscall and arch.
#[repr(C)]
struct SeccompSiginfo {
si_signo: libc::c_int,
si_errno: libc::c_int,
si_code: libc::c_int,
_pad0: libc::c_int,
si_call_addr: *mut libc::c_void,
si_syscall: libc::c_int,
si_arch: libc::c_uint,
}
fn handle_sigsys(info: &libc::siginfo_t) {
// SAFETY: The handler only reads the provided siginfo pointer, writes a
// diagnostic message, and then delegates to the default SIGSYS handler.
unsafe {
let current_thread = thread::current();
let thread_name = current_thread.name().unwrap_or("<unknown>");
let tid = libc::syscall(libc::SYS_gettid) as i64;
let info = &*(info as *const libc::siginfo_t as *const SeccompSiginfo);
eprintln!(
concat!(
"\n==== Possible seccomp violation ====\n",
"Syscall number: {} (arch: {:#x}, tid: {}, thread: {})\n",
"Try running with `strace -ff` to identify the cause and open an issue: ",
"https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new",
),
info.si_syscall, info.si_arch, tid, thread_name,
);
low_level::emulate_default_handler(SIGSYS).unwrap();
}
}
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
@@ -49,32 +90,32 @@ static ALLOC: dhat::Alloc = dhat::Alloc;
#[derive(Error, Debug)]
enum Error {
#[error("Failed to create API EventFd")]
CreateApiEventFd(#[source] std::io::Error),
CreateApiEventFd(#[source] io::Error),
#[cfg(feature = "guest_debug")]
#[error("Failed to create Debug EventFd")]
CreateDebugEventFd(#[source] std::io::Error),
CreateDebugEventFd(#[source] io::Error),
#[error("Failed to create exit EventFd")]
CreateExitEventFd(#[source] std::io::Error),
CreateExitEventFd(#[source] io::Error),
#[error("Failed to open hypervisor interface (is hypervisor interface available?)")]
CreateHypervisor(#[source] hypervisor::HypervisorError),
#[error("Failed to start the VMM thread")]
StartVmmThread(#[source] vmm::Error),
#[error("Error parsing config")]
ParsingConfig(#[source] vmm::config::Error),
ParsingConfig(#[source] config::Error),
#[error("Error creating VM")]
VmCreate(#[source] vmm::api::ApiError),
VmCreate(#[source] api::ApiError),
#[error("Error booting VM")]
VmBoot(#[source] vmm::api::ApiError),
VmBoot(#[source] api::ApiError),
#[error("Error restoring VM")]
VmRestore(#[source] vmm::api::ApiError),
VmRestore(#[source] api::ApiError),
#[error("Error parsing restore")]
ParsingRestore(#[source] vmm::config::Error),
ParsingRestore(#[source] config::Error),
#[error("Failed to join on VMM thread: {0:?}")]
ThreadJoin(std::boxed::Box<dyn std::any::Any + std::marker::Send>),
ThreadJoin(Box<dyn any::Any + Send>),
#[error("VMM thread exited with error")]
VmmThread(#[source] vmm::Error),
#[error("Error parsing --api-socket")]
ParsingApiSocket(#[source] std::num::ParseIntError),
ParsingApiSocket(#[source] num::ParseIntError),
#[error("Error parsing --event-monitor")]
ParsingEventMonitor(#[source] option_parser::OptionParserError),
#[cfg(feature = "dbus_api")]
@@ -86,7 +127,7 @@ enum Error {
#[error("Error parsing --event-monitor: path or fd required")]
BareEventMonitor,
#[error("Error doing event monitor I/O")]
EventMonitorIo(#[source] std::io::Error),
EventMonitorIo(#[source] io::Error),
#[error("Event monitor thread failed")]
EventMonitorThread(#[source] vmm::Error),
#[cfg(feature = "guest_debug")]
@@ -96,7 +137,7 @@ enum Error {
#[error("Error parsing --gdb: path required")]
BareGdb,
#[error("Error creating log file")]
LogFileCreation(#[source] std::io::Error),
LogFileCreation(#[source] io::Error),
#[error("Error parsing logger format")]
LoggerFormat(#[source] logger::Error),
#[error("Error setting up logger")]
@@ -112,13 +153,13 @@ enum Error {
#[derive(Error, Debug)]
enum FdTableError {
#[error("Failed to create event fd")]
CreateEventFd(#[source] std::io::Error),
CreateEventFd(#[source] io::Error),
#[error("Failed to obtain file limit")]
GetRLimit(#[source] std::io::Error),
GetRLimit(#[source] io::Error),
#[error("Error calling fcntl with F_GETFD")]
GetFd(#[source] std::io::Error),
GetFd(#[source] io::Error),
#[error("Failed to duplicate file handle")]
Dup2(#[source] std::io::Error),
Dup2(#[source] io::Error),
}
fn prepare_default_values() -> (String, String, String) {
@@ -303,7 +344,12 @@ fn get_cli_options_sorted(
.group("logging"),
Arg::new("log-format")
.long("log-format")
.help("Log format. Available tokens: {boottime}, {wallclock}, {pid}, {tid}, {thread}, {level}, {location}, {msg}")
.help(
"Log format. Common tokens: {boottime}, {wallclock}, {glog}, \
{localglog}, {thread}, {level}, {location}, {msg}. See \
docs/logging.md for the full list (per-field date/time tokens, \
local-time variants, glog level letter).",
)
.num_args(1)
.default_value(logger::DEFAULT_FORMAT)
.group("logging"),
@@ -316,7 +362,7 @@ fn get_cli_options_sorted(
hotplug_method=acpi|virtio-mem,\
hotplug_size=<hotpluggable_memory_size>,\
hotplugged_size=<hotplugged_memory_size>,\
prefault=on|off,thp=on|off\"",
prefault=on|off,reserve=on|off,thp=on|off\"",
)
.default_value(default_memory)
.group("vm-config"),
@@ -330,7 +376,7 @@ fn get_cli_options_sorted(
host_numa_node=<node_id>,\
id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,\
hotplugged_size=<hotplugged_memory_size>,\
prefault=on|off\"",
prefault=on|off,reserve=on|off\"",
)
.num_args(1..)
.action(ArgAction::Append)
@@ -399,10 +445,16 @@ fn get_cli_options_sorted(
.help(RngConfig::SYNTAX)
.default_value(default_rng)
.group("vm-config"),
Arg::new("rtc")
.long("rtc")
.help(RtcConfig::SYNTAX)
.num_args(0..=1)
.default_missing_value("")
.group("vm-config"),
Arg::new("seccomp")
.long("seccomp")
.num_args(1)
.value_parser(["true", "false", "log"])
.value_parser(["true", "false", "log", "errno"])
.default_value("true"),
Arg::new("serial")
.long("serial")
@@ -514,13 +566,12 @@ fn start_vmm(
_ => LevelFilter::Trace,
};
let log_file: Box<dyn std::io::Write + Send> = if let Some(ref file) =
cmd_arguments.get_one::<String>("log-file")
{
Box::new(std::fs::File::create(std::path::Path::new(file)).map_err(Error::LogFileCreation)?)
} else {
Box::new(std::io::stderr())
};
let log_file: Box<dyn io::Write + Send> =
if let Some(ref file) = cmd_arguments.get_one::<String>("log-file") {
Box::new(File::create(Path::new(file)).map_err(Error::LogFileCreation)?)
} else {
Box::new(io::stderr())
};
let format = cmd_arguments.get_one::<String>("log-format").unwrap();
let logger = Logger::new(log_file, format).map_err(Error::LoggerFormat)?;
@@ -537,6 +588,7 @@ fn start_vmm(
"true" => SeccompAction::Trap,
"false" => SeccompAction::Allow,
"log" => SeccompAction::Log,
"errno" => SeccompAction::Errno(libc::EPERM as u32),
val => {
// The user providing an invalid value will be rejected
panic!("Invalid parameter {val} for \"--seccomp\" flag");
@@ -547,20 +599,13 @@ fn start_vmm(
};
if seccomp_action == SeccompAction::Trap {
// SAFETY: We only using signal_hook for managing signals and only execute signal
// SAFETY: We only use signal_hook for managing signals and only execute signal
// handler safe functions (writing to stderr) and manipulating signals.
unsafe {
signal_hook::low_level::register(signal_hook::consts::SIGSYS, || {
eprintln!(
"\n==== Possible seccomp violation ====\n\
Try running with `strace -ff` to identify the cause and open an issue: \
https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new"
);
signal_hook::low_level::emulate_default_handler(SIGSYS).unwrap();
})
signal_hook_registry::register_sigaction(SIGSYS, handle_sigsys)
.map_err(|e| error!("Error adding SIGSYS signal handler: {e}"))
.ok();
}
.map_err(|e| error!("Error adding SIGSYS signal handler: {e}"))
.ok();
}
// SAFETY: Trivially safe.
@@ -571,7 +616,7 @@ fn start_vmm(
// Before we start any threads, mask the signals we'll be
// installing handlers for, to make sure they only ever run on the
// dedicated signal handling thread we'll start in a bit.
for sig in &vmm::vm::Vm::HANDLED_SIGNALS {
for sig in &Vm::HANDLED_SIGNALS {
if let Err(e) = block_signal(*sig) {
error!("Error blocking signals: {e}");
}
@@ -594,7 +639,7 @@ fn start_vmm(
parser.parse(gdb_config).map_err(Error::ParsingGdb)?;
if parser.is_set("path") {
Some(std::path::PathBuf::from(parser.get("path").unwrap()))
Some(PathBuf::from(parser.get("path").unwrap()))
} else {
return Err(Error::BareGdb);
}
@@ -630,7 +675,7 @@ fn start_vmm(
Ok(Some(unsafe { File::from_raw_fd(fd) }))
} else if parser.is_set("path") {
Ok(Some(
std::fs::OpenOptions::new()
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
@@ -679,7 +724,6 @@ fn start_vmm(
monitor,
&seccomp_action,
landlock_enable,
hypervisor.hypervisor_type(),
exit_evt.try_clone().unwrap(),
)
.map_err(Error::EventMonitorThread)?;
@@ -725,18 +769,18 @@ fn start_vmm(
// Create and boot the VM based off the VM config we just built.
let sender = api_request_sender.clone();
vmm::api::VmCreate
api::VmCreate
.send(
api_evt.try_clone().unwrap(),
api_request_sender,
Box::new(vm_config),
)
.map_err(Error::VmCreate)?;
vmm::api::VmBoot
api::VmBoot
.send(api_evt.try_clone().unwrap(), sender, ())
.map_err(Error::VmBoot)?;
} else if let Some(restore_params) = cmd_arguments.get_one::<String>("restore") {
vmm::api::VmRestore
api::VmRestore
.send(
api_evt.try_clone().unwrap(),
api_request_sender,
@@ -817,7 +861,7 @@ fn expand_fdtable() -> Result<(), FdTableError> {
let table_size = if limits.rlim_cur == libc::RLIM_INFINITY {
4096
} else {
std::cmp::min(limits.rlim_cur, 4096) as libc::c_int
cmp::min(limits.rlim_cur, 4096) as libc::c_int
};
// The first 3 handles are stdin, stdout, stderr. We don't want to touch
@@ -851,12 +895,18 @@ fn expand_fdtable() -> Result<(), FdTableError> {
}
fn main() {
#[cfg(feature = "tdx")]
compile_error!("Feature 'tdx' is broken.");
#[cfg(all(feature = "tdx", feature = "sev_snp"))]
compile_error!("Feature 'tdx' and 'sev_snp' are mutually exclusive.");
#[cfg(all(feature = "sev_snp", not(target_arch = "x86_64")))]
compile_error!("Feature 'sev_snp' needs target 'x86_64'");
#[cfg(all(feature = "fw_cfg", target_arch = "riscv64"))]
compile_error!("Feature 'fw_cfg' needs targets 'x86_64' or 'aarch64'");
#[cfg(all(feature = "igvm", not(any(feature = "kvm", feature = "mshv"))))]
compile_error!("Feature 'igvm' needs feature 'kvm' or 'mshv'");
#[cfg(all(feature = "sev_snp", not(any(feature = "kvm", feature = "mshv"))))]
compile_error!("Feature 'sev_snp' needs feature 'kvm' or 'mshv'");
#[cfg(feature = "dhat-heap")]
let _profiler = dhat::Profiler::new_heap();
@@ -870,6 +920,11 @@ fn main() {
if cmd_arguments.get_flag("version") {
println!("{} {}", env!("CARGO_BIN_NAME"), env!("BUILD_VERSION"));
let migration_protocol_versions = protocol::supported_protocol_versions()
.map(|version| version.to_string())
.collect::<Vec<_>>()
.join(", ");
println!("Migration Protocol Versions: {migration_protocol_versions}");
if cmd_arguments.get_count("v") != 0 {
println!("Enabled features: {:?}", vmm::feature_list());
@@ -886,14 +941,20 @@ fn main() {
Ok(p) => p,
Err(top_error) => {
cloud_hypervisor::cli_print_error_chain(&top_error, "Cloud Hypervisor", |_, _, _| None);
std::process::exit(1);
process::exit(1);
}
};
let vmm_result = start_vmm(&cmd_arguments, &api_socket_path, api_socket_fd);
if let Some(ref p) = api_socket_path {
let _ = std::fs::remove_file(p);
// Remove the socket only when we actually ran (Ok): a failed start may mean
// another instance already holds the path, and removing it would clobber
// that live socket. A stale socket left by a crash is cleared under the lock
// on the next start.
if vmm_result.is_ok()
&& let Some(ref api_socket_path) = api_socket_path
{
let _ = fs::remove_file(api_socket_path);
}
let exit_code = match vmm_result {
@@ -910,7 +971,7 @@ fn main() {
#[cfg(feature = "dhat-heap")]
drop(_profiler);
std::process::exit(exit_code);
process::exit(exit_code);
}
#[cfg(test)]
@@ -975,6 +1036,7 @@ mod unit_tests {
features: CpuFeatures::default(),
nested: true,
core_scheduling: CoreScheduling::Vm,
profile: Default::default(),
},
memory: MemoryConfig {
size: 536_870_912,
@@ -986,6 +1048,7 @@ mod unit_tests {
hugepages: false,
hugepage_size: None,
prefault: false,
reserve: false,
zones: None,
thp: true,
},
@@ -1039,6 +1102,7 @@ mod unit_tests {
iommu: false,
numa: None,
watchdog: false,
rtc: None,
#[cfg(feature = "guest_debug")]
gdb: false,
pci_segments: None,
@@ -1693,12 +1757,12 @@ mod unit_tests {
"--serial",
"null",
"--console",
"tty,pci_segment=1,pci_device_id=7",
"tty,pci_segment=0,pci_device_id=7",
],
r#"{
"payload": {"kernel": "/path/to/kernel"},
"serial": {"mode": "Null"},
"console": {"mode": "Tty", "iommu": false, "pci_segment": 1, "pci_device_id": 7}
"console": {"mode": "Tty", "iommu": false, "pci_segment": 0, "pci_device_id": 7}
}"#,
true,
),

Some files were not shown because too many files have changed in this diff Show More