Compare commits

..

13 Commits
v53.0 ... v51.2

Author SHA1 Message Date
Bo Chen
aa208c147e build: Release v51.2
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-05-14 14:47:42 -07:00
Bo Chen
3453eb6e86 misc: Fix various clippy issues
Assisted-by: Claude:Opus-4.7
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-05-14 14:47:42 -07:00
Bo Chen
e2cd6ff8c9 block, hypervisor: Fix cargo +nightly fmt issues
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-05-14 14:47:42 -07:00
Dylan Reid
3ba8e92c6a block: raw_async: reject batch atomically when SQ lacks capacity
submit_batch_requests pushed each BatchRequest into the io_uring SQ in
turn and used `?` to bail on the first push failure.
Leaving the initial SQEs visible to the kernel — but submitter.submit()
was never called, and every other call site in this file gates submit()
behind a preceding sq.push() that now also fails on the full ring.

This could allow a guest to DoS it's own queue or worse if the buffer is
freed early.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit ee315d2e7c)
2026-05-14 14:47:42 -07:00
Dylan Reid
7796c49afd block: AlignedOperation owns its bounce buffer via Drop
The bounce buffer for an unaligned descriptor was allocated in
execute_async and leaked on error paths, even though, for the sync
case the kernel already had a pointer to the buffer.

Clean this up by moving ownership of the buffer to the
AlignedOperation type. To make it actually safe, stop stashing a
guest memory pointer for the duration of the op. Instead, save the
guest address and pass guest memory back to the complete function.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit 1b8c92dd5e3c0c58316826486ce5ee30eeb71407))
[backport: adapted to stable/v51.x; v51.x has no block/src/request.rs
 split, so the new aligned_operation module is added next to
 block/src/lib.rs and the in tree struct, alloc, free path is
 replaced in place.]
Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
2026-05-14 14:47:42 -07:00
Dylan Reid
68feea7cbe virtio-devices: block: track non-batch inflight reqs immediately
For non-batch backends execute_async submits the kernel I/O inline
before returning. An early return while processing before inserting in
inflight_requests, meant the request went untracked, the local batch
list was never appended to inflight_requests, even though the request is
pending in the kernel.

To track it, insert into self.inflight_requests as soon as execute_async
returns Ok. The completion path's find_inflight_request now matches the
orphan and the bounce buffer is freed only after the kernel signals it
is done.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit fa8acbd712)
2026-05-14 14:47:42 -07:00
Dylan Reid
3bf94535d5 virtio-devices: block: reject duplicate in-flight head_index
A malicious or buggy guest can violate virtio by making the same
descriptor head available twice before the first chain has been placed
on the used ring. The submit path pushed both chains onto the
VecDeque-backed inflight_requests keyed by head_index, and on completion
find_inflight_request() returned the first linear match. That Request's
complete_async() freed its bounce buffer while the other chain's
io_uring op was still targeting it, producing a use-after-free the
kernel could then scribble into.

Signed-off-by: Dylan Reid <dgreid@fb.com>
(cherry picked from commit 544fa4aa76)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
2e7ff20a11 virtio-devices: block: handle corrupted requests with NEEDS_RESET
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit 8b60b38281)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
7696bcc71f virtio-devices: net: handle corrupted requests with NEEDS_RESET
A buggy or malicious guest may write an inappropriate value into
virtqueue's next_avail field. This will result in an error
when iterating over the queue:

863837ef86/virtio-queue/src/queue.rs (L708)

but this error is (logged and) ignored if pop_descriptor_chain()
is used:

863837ef86/virtio-queue/src/queue.rs (L583)

A reasonable approach, implemented here, is to mark the device as
NEEDS_RESET and ignore further queue events until the guest
reinitializes the device.

How this patch was tested:

Linux kernel was patched to trigger a bad next_avail when the
virtqueue queue counter reaches 5000:

--------------- START OF LINUX KERNEL PATCH ----------
$ git diff
diff --git a/drivers/virtio/virtio_ring.c b/drivers/virtio/virtio_ring.c
index b784aab668670..989f2a0c64a77 100644
--- a/drivers/virtio/virtio_ring.c
+++ b/drivers/virtio/virtio_ring.c
@@ -15,6 +15,9 @@
 #include <linux/spinlock.h>
 #include <xen/xen.h>

+
+void virtqueue_kick_always(struct virtqueue *vq);
+
 #ifdef DEBUG
 /* For development, we want to crash whenever the ring is screwed. */
 #define BAD_RING(_vq, fmt, args...)                            \
@@ -677,6 +680,12 @@ static inline int virtqueue_add_split(
                   struct virtqueue *_vq,
         * new available array entries. */
        virtio_wmb(vq->weak_barriers);
        vq->split.avail_idx_shadow++;
+       {
+        if ((vq->split.avail_idx_shadow % 100) == 0)
+            printk(KERN_ERR "avail idx: %d",
+                  (int)vq->split.avail_idx_shadow);
+               if (vq->split.avail_idx_shadow == 5000)
+               vq->split.avail_idx_shadow = 0;
+       }
        vq->split.vring.avail->idx = cpu_to_virtio16(_vq->vdev,
                                      vq->split.avail_idx_shadow);
        vq->num_added++;
@@ -689,6 +698,11 @@ static inline int virtqueue_add_split(
                  struct virtqueue *_vq,
        if (unlikely(vq->num_added == (1 << 16) - 1))
                virtqueue_kick(_vq);

+       {
+               if (unlikely(vq->split.avail_idx_shadow == 0))
+                       virtqueue_kick_always(_vq);
+       }
+
        return 0;

 unmap_release:
@@ -2515,6 +2529,11 @@ bool virtqueue_kick(struct virtqueue *vq)
 }
 EXPORT_SYMBOL_GPL(virtqueue_kick);

+void virtqueue_kick_always(struct virtqueue *vq)
+{
+       virtqueue_kick_prepare(vq);
+       virtqueue_notify(vq);
+}
 /**
  * virtqueue_get_buf_ctx - get the next used buffer
  * @_vq: the struct virtqueue we're talking about.
--------------- END OF LINUX KERNEL PATCH ----------

Then the kernel was booted, and the host pinged until the
nic became unresponsive:

ping -i 0.002 192.168.4.1

Device status was confirmed using

cat /sys/class/net/eth0/device/status

(it was 0x4f).

Then the device was re-initialized:

DEV_NAME=$(basename $(readlink -f /sys/class/net/eth0/device))
echo $DEV_NAME | tee /sys/bus/virtio/drivers/virtio_net/unbind
echo $DEV_NAME | tee /sys/bus/virtio/drivers/virtio_net/bind
ip link set eth0 up

At this point networking became healthly again.

Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit 563303b50a)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
e97524ecf6 virtio-devices: wire driver_status to EpollHandler
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit b5053ae4de)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
38dbcc2f44 virtio-devices: switch driver_status to Arc<AtomicU8>
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit 21bd3ae916)
2026-05-14 14:47:42 -07:00
Peter Oskolkov
fdd682c0b0 virtio-devices: introduce ActivationContext for device activation
Signed-off-by: Peter Oskolkov <posk@google.com>
(cherry picked from commit f77c6ef78b)
2026-05-14 14:47:42 -07:00
Bo Chen
9503e1ade9 build: Release v51.1
Signed-off-by: Bo Chen <bchen@crusoe.ai>
2026-02-22 21:12:53 +00:00
387 changed files with 30797 additions and 69460 deletions

View File

@@ -1,38 +1,3 @@
[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

@@ -1,25 +0,0 @@
# https://editorconfig.org/
#
# Hints for editors to assist with correct formatting as you type.
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
indent_size = 4
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
# Recommendation, not enforced.
max_line_length = 80
[Makefile]
indent_style = tab
# Inherited as default
# [*.sh]
# indent_size = 4
[{Cargo.lock,*.md,*.toml,*.yml,*.yaml}]
indent_size = 2

View File

@@ -8,7 +8,6 @@ updates:
interval: weekly
allow:
- dependency-name: "acpi_tables"
- dependency-name: "iommufd-ioctls"
- dependency-name: "kvm-bindings"
- dependency-name: "kvm-ioctls"
- dependency-name: "linux-loader"
@@ -38,14 +37,8 @@ updates:
interval: weekly
allow:
- dependency-type: all
cooldown:
default-days: 7
semver-major-days: 14
semver-minor-days: 7
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"

16
.github/workflows/audit.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Cloud Hypervisor Dependency Audit
on:
pull_request:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
jobs:
security_audit:
name: Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/audit@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

77
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: Cloud Hypervisor Build
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust:
- stable
- beta
- nightly
- "1.89.0"
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
steps:
- name: Code checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install musl-gcc
run: sudo apt install -y musl-tools
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- name: Build (default features)
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)
run: cargo build --locked --bin cloud-hypervisor --features "guest_debug"
- name: Build (default features + pvmemcontrol)
run: cargo build --locked --bin cloud-hypervisor --features "pvmemcontrol"
- name: Build (default features + fw_cfg)
run: cargo build --locked --bin cloud-hypervisor --features "fw_cfg"
- name: Build (default features + ivshmem)
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 (igvm)
run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "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)"

View File

@@ -1,890 +0,0 @@
name: CI
on: [pull_request, merge_group]
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
preflight:
name: preflight
runs-on: ubuntu-latest
outputs:
full: ${{ steps.classify.outputs.full }}
rust: ${{ steps.changes.outputs.rust }}
cargo: ${{ steps.changes.outputs.cargo }}
openapi: ${{ steps.changes.outputs.openapi }}
dockerfile: ${{ steps.changes.outputs.dockerfile }}
shell: ${{ steps.changes.outputs.shell }}
ci: ${{ steps.changes.outputs.ci }}
docs: ${{ steps.changes.outputs.docs }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: changes
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
filters: |
rust:
- '**/*.rs'
- 'build.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- 'rust-toolchain.toml'
cargo:
- '**/Cargo.toml'
- '**/Cargo.lock'
openapi:
- 'vmm/src/api/openapi/**'
dockerfile:
- 'resources/Dockerfile'
shell:
- '**/*.sh'
- 'scripts/**'
ci:
- '.github/workflows/**'
docs:
- 'docs/**'
- '**/*.md'
- '.github/ISSUE_TEMPLATE/**'
- 'LICENSES/**'
- 'CODEOWNERS'
- id: classify
name: Classify changes
run: |
set -eufo pipefail
full=false
if [[ "${{ steps.changes.outputs.rust }}" == "true" \
|| "${{ steps.changes.outputs.dockerfile }}" == "true" \
|| "${{ steps.changes.outputs.shell }}" == "true" \
|| "${{ steps.changes.outputs.ci }}" == "true" ]]; then
full=true
fi
echo "full=$full" >> "$GITHUB_OUTPUT"
echo "full=$full"
dco:
name: dco
needs: [preflight]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Check DCO
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eufo pipefail
pip3 install -U dco-check
dco-check -e "49699333+dependabot[bot]@users.noreply.github.com"
gitlint:
name: gitlint
needs: [preflight]
# PR-only: gitlint needs GITHUB_BASE_REF, unset on merge_group.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
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 }}
fetch-depth: 0
- name: Set up Python 3.10
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade gitlint
- name: Lint git commit messages
run: |
gitlint --commits "origin/$GITHUB_BASE_REF.."
lychee:
name: lychee
needs: [preflight]
if: needs.preflight.outputs.docs == 'true' || needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get changed files in PR
id: changed-files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
base_sha: ${{ github.event.pull_request.base.sha }}
- name: Verify Changed Files
run: |
set -eufo pipefail
echo "--- tj-actions/changed-files Outputs ---"
echo "any_changed: ${{ steps.changed-files.outputs.any_changed }}"
echo "all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}"
echo "added_files: ${{ steps.changed-files.outputs.added_files }}"
echo "modified_files: ${{ steps.changed-files.outputs.modified_files }}"
echo "deleted_files: ${{ steps.changed-files.outputs.deleted_files }}"
echo "renamed_files: ${{ steps.changed-files.outputs.renamed_files }}"
echo "----------------------------------------"
if [ -n "${{ steps.changed-files.outputs.all_changed_files }}" ]; then
echo "Detected changes: all_changed_files output is NOT empty."
else
echo "No changes detected: all_changed_files output IS empty."
fi
- name: Link Availability Check (Diff Only)
if: ${{ steps.changed-files.outputs.all_changed_files != '' }}
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
args: --verbose --config .lychee.toml ${{ steps.changed-files.outputs.all_changed_files }}
failIfEmpty: false
fail: true
taplo:
name: taplo
needs: [preflight]
if: needs.preflight.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v7
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
run: sudo apt-get update && sudo apt-get -yqq install build-essential libssl-dev
- name: Install taplo
run: cargo install taplo-cli --locked
- name: Check formatting
run: taplo fmt --check
audit:
name: audit
needs: [preflight]
if: needs.preflight.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/audit@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
shlint:
name: shlint
needs: [preflight]
if: needs.preflight.outputs.shell == 'true' || needs.preflight.outputs.ci == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Run the shell script checkers
uses: luizm/action-sh-checker@883217215b11c1fabbf00eb1a9a041f62d74c744 # v0.10.0
env:
SHFMT_OPTS: -i 4 -d
SHELLCHECK_OPTS: -x --source-path scripts
hadolint:
name: hadolint
needs: [preflight]
if: needs.preflight.outputs.dockerfile == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Lint Dockerfile
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: ./resources/Dockerfile
format: tty
no-fail: false
verbose: true
failure-threshold: info
reuse:
name: reuse
needs: [preflight]
if: needs.preflight.outputs.full == 'true' || needs.preflight.outputs.cargo == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v6
formatting:
name: formatting
needs: [preflight]
if: needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
rust: [nightly]
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-musl
env:
RUSTFLAGS: -D warnings
steps:
- name: Code checkout
uses: actions/checkout@v7
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
components: rustfmt
- name: Formatting (rustfmt)
run: cargo fmt --all -- --check
- name: Formatting (fuzz) (rustfmt)
run: cargo fmt --all --manifest-path fuzz/Cargo.toml -- --check
package-consistency:
name: package-consistency
needs: [preflight]
if: needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install dependencies
run: sudo apt install -y python3
- name: Install Rust toolchain stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Check Rust VMM Package Consistency of root Workspace
run: python3 scripts/package-consistency-check.py github.com/rust-vmm
- name: Check Rust VMM Package Consistency of fuzz Workspace
run: |
set -eufo pipefail
pushd fuzz
python3 ../scripts/package-consistency-check.py github.com/rust-vmm
popd
fuzz-build:
name: fuzz-build
needs: [preflight]
if: needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
rust: [nightly]
target: [x86_64-unknown-linux-gnu]
env:
RUSTFLAGS: -D warnings
steps:
- name: Code checkout
uses: actions/checkout@v7
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- name: Install Cargo fuzz
run: cargo install cargo-fuzz
- name: Fuzz Build
run: cargo fuzz build
- name: Fuzz Check
run: cargo fuzz check
openapi:
name: openapi
needs: [preflight]
if: needs.preflight.outputs.openapi == 'true'
runs-on: ubuntu-latest
container: openapitools/openapi-generator-cli
steps:
- uses: actions/checkout@v7
- name: Validate OpenAPI
run: |
/usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml
typos:
name: typos
needs: [preflight]
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
quality:
name: quality
needs: [preflight]
if: needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
# Beta clippy is non-blocking; continue-on-error below keeps the
# aggregated needs.quality.result green when only beta fails.
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
rust:
- beta
- stable
target:
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
include:
- rust: beta
experimental: true
- rust: stable
experimental: false
steps:
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
components: clippy
- name: Bisectability Check (default features)
if: ${{ github.event_name == 'pull_request' && matrix.target == 'x86_64-unknown-linux-gnu' }}
run: |
set -eufo pipefail
commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }})
for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done
git checkout ${{ github.sha }}
- name: Clippy (kvm)
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" -- -D warnings
- name: Clippy (mshv)
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" -- -D warnings
- name: Clippy (mshv + kvm)
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,kvm" -- -D warnings
- name: Clippy (default features)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples -- -D warnings
- name: Clippy (default features + guest_debug)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "guest_debug" -- -D warnings
- name: Clippy (default features + pvmemcontrol)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "pvmemcontrol" -- -D warnings
- name: Clippy (default features + tracing)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "tracing" -- -D warnings
- name: Clippy (default features + fw_cfg)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "fw_cfg" -- -D warnings
- name: Clippy (default features + ivshmem)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "ivshmem" -- -D warnings
- 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 "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 "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 "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
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,igvm,sev_snp,fw_cfg" -- -D warnings
- name: Clippy (default features + sev_snp + igvm + fw_cfg)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "sev_snp,igvm,fw_cfg" -- -D warnings
- name: Check build did not modify any files
run: test -z "$(git status --porcelain)"
build:
name: build
needs: [preflight]
if: needs.preflight.outputs.full == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust:
- stable
- beta
- nightly
- "1.89.0" # MSRV — keep quoted.
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
steps:
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install musl-gcc
run: sudo apt install -y musl-tools
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- name: Build (default features)
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 + dbus_api)
run: cargo build --locked --bin cloud-hypervisor --features "dbus_api"
- name: Build (default features + guest_debug)
run: cargo build --locked --bin cloud-hypervisor --features "guest_debug"
- name: Build (default features + pvmemcontrol)
run: cargo build --locked --bin cloud-hypervisor --features "pvmemcontrol"
- name: Build (default features + fw_cfg)
run: cargo build --locked --bin cloud-hypervisor --features "fw_cfg"
- name: Build (default features + ivshmem)
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 (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 (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:
name: integration-x86-64-pr
needs: [preflight, dco, quality, build]
if: >-
needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success'
timeout-minutes: 80
env:
# Our runner has 16 cores (nproc).
# We limit parallelism only to avoid exhausting disk space and memory
# resources, not to save CPU resources.
PARALLEL_INTEGRATION_TESTS_NUM: 12
runs-on: garm-jammy-16
steps:
- name: Code checkout
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: Prepare for VDPA
run: scripts/prepare_vdpa.sh
- name: Run unit tests
run: scripts/dev_cli.sh tests --unit --libc gnu
- name: Load openvswitch module
run: sudo modprobe openvswitch
- name: Run integration tests
timeout-minutes: 60
run: scripts/dev_cli.sh tests --integration --libc gnu
# MQ-only: the 3 matrix entries that integration-x86-64-pr does not cover.
integration-x86-64-mq:
name: integration-x86-64-mq
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: 80
env:
# Our runner has 16 cores (nproc).
# We limit parallelism only to avoid exhausting disk space and memory
# resources, not to save CPU resources.
PARALLEL_INTEGRATION_TESTS_NUM: 12
strategy:
fail-fast: false
matrix:
include:
- {runner: garm-jammy, libc: musl}
- {runner: garm-jammy-amd, libc: gnu}
- {runner: garm-jammy-amd, libc: musl}
# format() because `${{ matrix.runner }}-16` is not valid in runs-on.
runs-on: ${{ format('{0}-16', matrix.runner) }}
steps:
- name: Code checkout
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: Prepare for VDPA
run: scripts/prepare_vdpa.sh
- name: Run unit tests
run: scripts/dev_cli.sh tests --unit --libc ${{ matrix.libc }}
- name: Load openvswitch module
run: sudo modprobe openvswitch
- name: Run integration tests
timeout-minutes: 60
run: scripts/dev_cli.sh tests --integration --libc ${{ matrix.libc }}
integration-arm64:
name: integration-arm64
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: 120
env:
# Our runner has 80 cores (nproc).
# We limit parallelism only to avoid exhausting disk space and memory
# resources, not to save CPU resources.
PARALLEL_INTEGRATION_TESTS_NUM: 25
runs-on: bookworm-arm64
steps:
# arm64 runner user is "runner" (vfio's is "github-runner").
- name: Fix workspace permissions
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Run unit tests (musl)
run: scripts/dev_cli.sh tests --unit --libc musl
- name: Load openvswitch module
run: sudo modprobe openvswitch
- name: Run integration tests (musl)
timeout-minutes: 60
run: scripts/dev_cli.sh tests --integration --libc musl
- name: Install Azure CLI
run: |
set -eufo pipefail
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
echo "deb [arch=arm64] https://packages.microsoft.com/repos/azure-cli/ bookworm main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
sudo apt update
sudo apt install -y azure-cli
- name: Download Windows image
shell: bash
run: |
set -eufo pipefail
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-25h2-6.raw.gz
cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/"
pushd "$HOME/workloads"
if sha1sum "$IMG_BASENAME.sha1" --check; then
exit
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
timeout-minutes: 30
run: scripts/dev_cli.sh tests --integration-windows --libc musl
integration-vfio:
name: integration-vfio
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: vfio-nvidia
env:
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
steps:
# vfio-nvidia runner user is "github-runner" (not "runner" like arm64).
- name: Fix workspace permissions
run: sudo chown -R github-runner:github-runner "${GITHUB_WORKSPACE}"
- name: Code checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Run VFIO integration tests
timeout-minutes: 25
run: scripts/dev_cli.sh tests --integration-vfio
# Most tests are failing with musl, see #6790
# - name: Run VFIO integration tests for musl
# timeout-minutes: 25
# run: scripts/dev_cli.sh tests --integration-vfio --libc musl
integration-windows:
name: integration-windows
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: garm-jammy-16
steps:
- name: Code checkout
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: Install Azure CLI
run: |
set -eufo pipefail
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
sudo apt update
sudo apt install -y azure-cli
- name: Download Windows image
run: |
set -eufo pipefail
mkdir $HOME/workloads
az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2025-amd64-1.raw" --name windows-server-2025-amd64-1.raw --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
- name: Run Windows guest integration tests
timeout-minutes: 15
run: scripts/dev_cli.sh tests --integration-windows
- name: Run Windows guest integration tests for musl
timeout-minutes: 15
run: scripts/dev_cli.sh tests --integration-windows --libc musl
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'
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@v7
with:
fetch-depth: 0
- 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-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
- integration-sev-snp
- integration-vfio
- integration-mshv-x86-64
- integration-windows
- integration-x86-64-mq
- integration-x86-64-pr
- openapi
- package-consistency
- preflight
- quality
- reuse
- shlint
- taplo
- typos
if: always()
runs-on: ubuntu-latest
steps:
- name: Verify all dependencies succeeded or were skipped
env:
NEEDS_JSON: ${{ toJson(needs) }}
run: |
set -eufo pipefail
echo "$NEEDS_JSON" | jq .
# success or skipped = pass; failure or cancelled = red.
echo "$NEEDS_JSON" | jq -e '
to_entries
| map(select(.value.result != "success" and .value.result != "skipped"))
| length == 0
' >/dev/null

20
.github/workflows/dco.yaml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: DCO
on: [pull_request, merge_group]
jobs:
check:
name: DCO Check ("Signed-off-by")
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.x
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Check DCO
if: ${{ github.event_name == 'pull_request' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip3 install -U dco-check
dco-check -e "49699333+dependabot[bot]@users.noreply.github.com"

View File

@@ -14,106 +14,52 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
main:
runs-on: ubuntu-latest
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Code checkout
uses: actions/checkout@v7
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Login to ghcr
if: ${{ github.event_name == 'push' }}
uses: docker/login-action@v4
uses: docker/login-action@v3
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
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# generate Docker tags based on the following events/attributes
tags: |
type=raw,value=20260522-0
type=raw,value=20251114-0
type=sha
- name: Login to ghcr
uses: docker/login-action@v4
- name: Build and push
if: ${{ github.event_name == 'push' }}
uses: docker/build-push-action@v6
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
file: ./resources/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
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: Build only
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v6
with:
file: ./resources/Dockerfile
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

32
.github/workflows/formatting.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Cloud Hypervisor Code Formatting
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Code Formatting
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- nightly
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-musl
env:
RUSTFLAGS: -D warnings
steps:
- name: Code checkout
uses: actions/checkout@v6
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
components: rustfmt
- name: Formatting (rustfmt)
run: cargo fmt --all -- --check
- name: Formatting (fuzz) (rustfmt)
run: cargo fmt --all --manifest-path fuzz/Cargo.toml -- --check

32
.github/workflows/fuzz-build.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Cloud Hypervisor Cargo Fuzz Build
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Cargo Fuzz Build
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- nightly
target:
- x86_64-unknown-linux-gnu
env:
RUSTFLAGS: -D warnings
steps:
- name: Code checkout
uses: actions/checkout@v6
- name: Install Rust toolchain (${{ matrix.rust }})
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- name: Install Cargo fuzz
run: cargo install cargo-fuzz
- name: Fuzz Build
run: cargo fuzz build
- name: Fuzz Check
run: cargo fuzz check

25
.github/workflows/gitlint.yaml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Commit messages check
on:
pull_request:
jobs:
gitlint:
name: Check commit messages
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Set up Python 3.10
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade gitlint
- name: Lint git commit messages
run: |
gitlint --commits origin/$GITHUB_BASE_REF..

25
.github/workflows/hadolint.yaml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Lint Dockerfile
on:
push:
paths:
- resources/Dockerfile
pull_request:
paths:
- resources/Dockerfile
jobs:
hadolint:
name: Run Hadolint Dockerfile Linter
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Lint Dockerfile
uses: hadolint/hadolint-action@master
with:
dockerfile: ./resources/Dockerfile
format: tty
no-fail: false
verbose: true
failure-threshold: info

View File

@@ -0,0 +1,54 @@
name: Cloud Hypervisor Tests (ARM64)
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
timeout-minutes: 120
name: Tests (ARM64)
runs-on: bookworm-arm64
steps:
- name: Fix workspace permissions
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
- name: Code checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run unit tests (musl)
run: scripts/dev_cli.sh tests --unit --libc musl
- name: Load openvswitch module
run: sudo modprobe openvswitch
- name: Run integration tests (musl)
timeout-minutes: 60
run: scripts/dev_cli.sh tests --integration --libc musl
- name: Install Azure CLI
if: ${{ github.event_name != 'pull_request' }}
run: |
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
echo "deb [arch=arm64] https://packages.microsoft.com/repos/azure-cli/ bookworm main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
sudo apt update
sudo apt install -y azure-cli
- name: Download Windows image
if: ${{ github.event_name != 'pull_request' }}
shell: bash
run: |
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
cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/"
pushd "$HOME/workloads"
if sha1sum "$IMG_BASENAME.sha1" --check; then
exit
fi
popd
mkdir -p "$HOME/workloads"
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
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 30
run: scripts/dev_cli.sh tests --integration-windows --libc musl

View File

@@ -7,26 +7,16 @@ on:
jobs:
build:
name: Tests (Metrics)
runs-on: garm-jammy-16
runs-on: bare-metal-9950x
env:
METRICS_PUBLISH_KEY: ${{ secrets.METRICS_PUBLISH_KEY }}
steps:
- name: Code checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
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_,block_qcow2 -- --report-file /root/workloads/metrics.json
run: scripts/dev_cli.sh tests --metrics -- -- --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

@@ -0,0 +1,25 @@
name: Cloud Hypervisor Tests (Rate-Limiter)
on: [merge_group, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Tests (Rate-Limiter)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'bare-metal-9950x' }}
env:
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
steps:
- name: Code checkout
if: ${{ github.event_name != 'pull_request' }}
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run rate-limiter integration tests
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 20
run: scripts/dev_cli.sh tests --integration-rate-limiter
- name: Skipping build for PR
if: ${{ github.event_name == 'pull_request' }}
run: echo "Skipping build for PR"

33
.github/workflows/integration-vfio.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Cloud Hypervisor Tests (VFIO)
on: [merge_group, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Tests (VFIO)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'vfio-nvidia' }}
env:
AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }}
steps:
- name: Fix workspace permissions
if: ${{ github.event_name != 'pull_request' }}
run: sudo chown -R runner:runner ${GITHUB_WORKSPACE}
- name: Code checkout
if: ${{ github.event_name != 'pull_request' }}
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run VFIO integration tests
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 15
run: scripts/dev_cli.sh tests --integration-vfio
# Most tests are failing with musl see #6790
# - name: Run VFIO integration tests for musl
# if: ${{ github.event_name != 'pull_request' }}
# timeout-minutes: 15
# run: scripts/dev_cli.sh tests --integration-vfio --libc musl
- name: Skipping build for PR
if: ${{ github.event_name == 'pull_request' }}
run: echo "Skipping build for PR"

View File

@@ -0,0 +1,50 @@
name: Cloud Hypervisor Tests (Windows Guest)
on: [merge_group, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Tests (Windows Guest)
runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'garm-jammy-16' }}
steps:
- name: Code checkout
if: ${{ github.event_name != 'pull_request' }}
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Docker
if: ${{ github.event_name != 'pull_request' }}
run: |
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: Install Azure CLI
if: ${{ github.event_name != 'pull_request' }}
run: |
sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg
curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
sudo apt update
sudo apt install -y azure-cli
- name: Download Windows image
if: ${{ github.event_name != 'pull_request' }}
run: |
mkdir $HOME/workloads
az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2022-amd64-2.raw" --name windows-server-2022-amd64-2.raw --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}"
- name: Run Windows guest integration tests
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 15
run: scripts/dev_cli.sh tests --integration-windows
- name: Run Windows guest integration tests for musl
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 15
run: scripts/dev_cli.sh tests --integration-windows --libc musl
- name: Skipping build for PR
if: ${{ github.event_name == 'pull_request' }}
run: echo "Skipping build for PR"

View File

@@ -0,0 +1,52 @@
name: Cloud Hypervisor Tests (x86-64)
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
runner: ['garm-jammy', "garm-jammy-amd"]
libc: ["musl", 'gnu']
name: Tests (x86-64)
runs-on: ${{ github.event_name == 'pull_request' && !(matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') && 'ubuntu-latest' || format('{0}-16', matrix.runner) }}
steps:
- name: Code checkout
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Docker
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
run: |
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: Prepare for VDPA
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
run: scripts/prepare_vdpa.sh
- name: Run unit tests
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
run: scripts/dev_cli.sh tests --unit --libc ${{ matrix.libc }}
- name: Load openvswitch module
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
run: sudo modprobe openvswitch
- name: Run integration tests
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
timeout-minutes: 40
run: scripts/dev_cli.sh tests --integration --libc ${{ matrix.libc }}
- name: Run live-migration integration tests
if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }}
timeout-minutes: 20
run: scripts/dev_cli.sh tests --integration-live-migration --libc ${{ matrix.libc }}
- name: Skipping build for PR
if: ${{ github.event_name == 'pull_request' && matrix.runner != 'garm-jammy' && matrix.libc != 'gnu' }}
run: echo "Skipping build for PR"

45
.github/workflows/lychee.yaml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Link Check (lychee)
on: pull_request
jobs:
link_check:
name: Link Check
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
with:
# Fetch the entire history so git diff can compare against the base branch
fetch-depth: 0
- name: Get changed files in PR
id: changed-files
uses: tj-actions/changed-files@v47 # Using a dedicated action for robustness
with:
# Compare the HEAD of the PR with the merge-base (where the PR branches off)
base_sha: ${{ github.event.pull_request.base.sha }}
# NEW STEP: Print all changed-files outputs for verification
- name: Verify Changed Files
run: |
echo "--- tj-actions/changed-files Outputs ---"
echo "any_changed: ${{ steps.changed-files.outputs.any_changed }}"
echo "all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}"
echo "added_files: ${{ steps.changed-files.outputs.added_files }}"
echo "modified_files: ${{ steps.changed-files.outputs.modified_files }}"
echo "deleted_files: ${{ steps.changed-files.outputs.deleted_files }}"
echo "renamed_files: ${{ steps.changed-files.outputs.renamed_files }}"
echo "----------------------------------------"
# This will also show if the all_changed_files string is empty or not
if [ -n "${{ steps.changed-files.outputs.all_changed_files }}" ]; then
echo "Detected changes: all_changed_files output is NOT empty."
else
echo "No changes detected: all_changed_files output IS empty."
fi
- name: Link Availability Check (Diff Only)
# MODIFIED: Only run lychee if the 'all_changed_files' output is not an empty string
if: ${{ steps.changed-files.outputs.all_changed_files != '' }}
uses: lycheeverse/lychee-action@master
with:
# Pass the space-separated list of changed files to lychee
args: --verbose --config .lychee.toml ${{ steps.changed-files.outputs.all_changed_files }}
failIfEmpty: false
fail: true

248
.github/workflows/mshv-infra.yaml vendored Normal file
View File

@@ -0,0 +1,248 @@
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: string
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 -e
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 -e
# Extract vCPU count from SKU (e.g., "Standard_D2s_v3" => 2)
vcpu=$(echo "$SKU" | sed -n 's/^Standard_[A-Za-z]\+\([0-9]\+\).*/\1/p')
if [[ -z "$vcpu" ]]; then
echo "Cannot extract vCPU count from SKU: $SKU"
exit 1
fi
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
usage=$(az vm list-usage --location "$location" --query "[?name.value=='$family'] | [0]" -o json)
current=$(echo "$usage" | jq -r '.currentValue')
limit=$(echo "$usage" | jq -r '.limit')
if [[ $((limit - current)) -ge $vcpu ]]; 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 -e
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 -e
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 -e
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 -e
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 -e
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 -e
ssh -i ~/.ssh/${KEY} -o StrictHostKeyChecking=no ${USERNAME}@${PRIVATE_IP} << EOF
set -e
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

130
.github/workflows/mshv-integration.yaml vendored Normal file
View File

@@ -0,0 +1,130 @@
name: Cloud Hypervisor Tests (MSHV) (x86_64)
on: [pull_request_target, merge_group]
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 -e
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} << EOF
sudo dmesg
EOF
- 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 -e
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."

14
.github/workflows/openapi.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Cloud Hypervisor OpenAPI Validation
on: [pull_request, merge_group]
jobs:
Validate:
runs-on: ubuntu-latest
container: openapitools/openapi-generator-cli
steps:
- uses: actions/checkout@v6
- name: Validate OpenAPI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
/usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml

View File

@@ -0,0 +1,32 @@
name: Cloud Hypervisor Consistency
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Rust VMM Consistency Check
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install dependencies
run: sudo apt install -y python3
- name: Install Rust toolchain stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Check Rust VMM Package Consistency of root Workspace
run: python3 scripts/package-consistency-check.py github.com/rust-vmm
- name: Check Rust VMM Package Consistency of fuzz Workspace
run: |
pushd fuzz
python3 ../scripts/package-consistency-check.py github.com/rust-vmm
popd

View File

@@ -0,0 +1,30 @@
name: Cloud Hypervisor RISC-V 64-bit kvm build Preview
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Cargo
runs-on: riscv64-qemu-host
strategy:
fail-fast: false
steps:
- name: Code checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust toolchain
run: /opt/scripts/exec-in-qemu.sh rustup default 1.89.0
- name: Build test (kvm)
run: /opt/scripts/exec-in-qemu.sh cargo build --locked --no-default-features --features "kvm" -p cloud-hypervisor
- name: Clippy test (kvm)
run: /opt/scripts/exec-in-qemu.sh cargo clippy --locked --no-default-features --features "kvm" -p cloud-hypervisor
- name: Check no files were modified
run: test -z "$(git status --porcelain)"

View File

@@ -0,0 +1,39 @@
name: Cloud Hypervisor RISC-V 64-bit Preview
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Cargo
runs-on: riscv64-qemu-host
strategy:
fail-fast: false
matrix:
module:
- hypervisor
- arch
- vm-allocator
- devices
steps:
- name: Code checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust toolchain
run: /opt/scripts/exec-in-qemu.sh rustup default 1.89.0
- name: Build ${{ matrix.module }} Module (kvm)
run: /opt/scripts/exec-in-qemu.sh cargo build --locked -p ${{ matrix.module }} --no-default-features --features "kvm"
- name: Clippy ${{ matrix.module }} Module (kvm)
run: /opt/scripts/exec-in-qemu.sh cargo clippy --locked -p ${{ matrix.module }} --no-default-features --features "kvm" -- -D warnings
- name: Test ${{ matrix.module }} Module (kvm)
run: /opt/scripts/exec-in-qemu.sh cargo test --locked -p ${{ matrix.module }} --no-default-features --features "kvm"
- name: Check no files were modified
run: test -z "$(git status --porcelain)"

170
.github/workflows/quality.yaml vendored Normal file
View File

@@ -0,0 +1,170 @@
name: Cloud Hypervisor Quality Checks
on: [pull_request, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
build:
name: Quality (clippy)
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
rust:
- beta
- stable
target:
- aarch64-unknown-linux-gnu
- aarch64-unknown-linux-musl
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
include:
- rust: beta
experimental: true
- rust: stable
experimental: false
steps:
- name: Code checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust toolchain (${{ matrix.rust }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
components: clippy
- name: Bisectability Check (default features)
if: ${{ github.event_name == 'pull_request' && matrix.target == 'x86_64-unknown-linux-gnu' }}
run: |
set -e
commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }})
for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done
git checkout ${{ github.sha }}
- name: Clippy (kvm)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm" -- -D warnings
- name: Clippy (mshv)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv" -- -D warnings
- name: Clippy (mshv + kvm)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,kvm" -- -D warnings
- name: Clippy (default features)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples -- -D warnings
- name: Clippy (default features + guest_debug)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "guest_debug" -- -D warnings
- name: Clippy (default features + pvmemcontrol)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "pvmemcontrol" -- -D warnings
- name: Clippy (default features + tracing)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "tracing" -- -D warnings
- name: Clippy (default features + fw_cfg)
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
command: clippy
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "fw_cfg" -- -D warnings
- name: Clippy (default features + ivshmem)
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --tests --examples --features "ivshmem" -- -D warnings
- name: Clippy (sev_snp)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "sev_snp" -- -D warnings
- name: Clippy (igvm)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "igvm" -- -D warnings
- name: Clippy (kvm + tdx)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
uses: houseabsolute/actions-rust-cross@v1
with:
command: clippy
cross-version: 3e0957637b49b1bbced23ad909170650c5b70635
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
args: --locked --all --all-targets --no-default-features --tests --examples --features "tdx,kvm" -- -D warnings
- name: Check build did not modify any files
run: test -z "$(git status --porcelain)"
typos:
if: github.event_name == 'pull_request'
name: Typos / Spellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Executes "typos ."
- uses: crate-ci/typos@v1.43.5

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install musl-gcc
if: contains(matrix.platform.target, 'musl')
run: sudo apt install -y musl-tools
@@ -54,7 +54,7 @@ jobs:
cp target/${{ matrix.platform.target }}/release/ch-remote ./${{ matrix.platform.name_ch_remote }}
- name: Upload Release Artifacts
if: github.event_name == 'create' && github.event.ref_type == 'tag'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: Artifacts for ${{ matrix.platform.target }}
path: |
@@ -80,13 +80,13 @@ jobs:
github.event_name == 'create' && github.event.ref_type == 'tag' &&
matrix.platform.target == 'x86_64-unknown-linux-gnu'
id: upload-release-cloud-hypervisor-vendored-sources
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
path: cloud-hypervisor-${{ github.event.ref }}.tar.xz
name: cloud-hypervisor-${{ github.event.ref }}.tar.xz
- name: Create GitHub Release
if: github.event_name == 'create' && github.event.ref_type == 'tag'
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
draft: true
files: |

12
.github/workflows/reuse.yaml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: REUSE Compliance Check
on: [push, pull_request]
jobs:
reuse:
name: REUSE Compliance Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v6

20
.github/workflows/shlint.yaml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Shell scripts check
on:
pull_request:
merge_group:
push:
branches:
- main
jobs:
sh-checker:
name: Check shell scripts
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run the shell script checkers
uses: luizm/action-sh-checker@master
env:
SHFMT_OPTS: -i 4 -d
SHELLCHECK_OPTS: -x --source-path scripts

21
.github/workflows/taplo.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Cargo.toml Formatting (taplo)
on:
pull_request:
paths:
- '**/Cargo.toml'
jobs:
cargo_toml_format:
name: Cargo.toml Formatting
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
run: sudo apt-get update && sudo apt-get -yqq install build-essential libssl-dev
- name: Install taplo
run: cargo install taplo-cli --locked
- name: Check formatting
run: taplo fmt --check

11
.gitignore vendored
View File

@@ -1,13 +1,10 @@
/build
/.cargo
/target
**/*.rs.bk
**/Cargo.lock
**/rusty-tags.vi
/.agents
/.cargo
/.claude
/.codex
/.vscode
/build
/rpm/SOURCES
/target
/.vscode
/vendor
__pycache__

View File

@@ -3,34 +3,34 @@ verbose = "info"
exclude_path = [".lychee.toml"]
exclude = [
# Availability of links below should be manually verified.
# Page for intel TDX support, returns 403 while querying.
'^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html',
# Page for TPM, returns 403 while querying.
'^https://trustedcomputinggroup.org/wp-content/uploads/PC-Client-Specific-Platform-TPM-Profile-for-TPM-2p0-v1p05p_r14_pub.pdf',
# GitHub user smibarber referenced in `CREDITS.md` no longer exist
'^https://github.com/smibarber',
# OSDev has added bot protection and accesses my result in 403 Forbidden.
'^https://wiki.osdev.org',
# Exclude all pages with $ in the URL since $XXX is a variable
"\\$.*",
# Exclude local files
"file://.*",
# ARM documentation returns 403 Forbidden for automated CI checks.
'^http://infocenter\.arm\.com',
'^https://developer\.arm\.com',
# Ignore internal/unsupported protocols seen in logs
'^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',
# Availability of links below should be manually verified.
# Page for intel TDX support, returns 403 while querying.
'^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html',
# Page for TPM, returns 403 while querying.
'^https://trustedcomputinggroup.org/wp-content/uploads/PC-Client-Specific-Platform-TPM-Profile-for-TPM-2p0-v1p05p_r14_pub.pdf',
# GitHub user smibarber referenced in `CREDITS.md` no longer exist
'^https://github.com/smibarber',
# OSDev has added bot protection and accesses my result in 403 Forbidden.
'^https://wiki.osdev.org',
# Exclude all pages with $ in the URL since $XXX is a variable
"\\$.*",
# Exclude local files
"file://.*",
# ARM documentation returns 403 Forbidden for automated CI checks.
'^http://infocenter\.arm\.com',
'^https://developer\.arm\.com',
# Ignore internal/unsupported protocols seen in logs
'^tcp://192\.168\.1\.10',
]
# Exclude loopback addresses
exclude_loopback = true
max_retries = 3
retry_wait_time = 5

View File

@@ -7,6 +7,6 @@ Files: docs/*.md *.md
Copyright: 2024
License: CC-BY-4.0
Files: scripts/* test_data/* *.toml .git* .editorconfig fuzz/Cargo.lock fuzz/.gitignore resources/linux-config-* vmm/src/api/openapi/cloud-hypervisor.yaml CODEOWNERS Cargo.lock
Files: scripts/* test_data/* *.toml .git* fuzz/Cargo.lock fuzz/.gitignore resources/linux-config-* vmm/src/api/openapi/cloud-hypervisor.yaml CODEOWNERS Cargo.lock
Copyright: 2024
License: Apache-2.0

View File

@@ -1,6 +1,5 @@
include = ["**/Cargo.toml"]
[formatting]
indent_string = " " # 2 spaces: keep in sync with .editorconfig
reorder_arrays = true
reorder_keys = true

View File

@@ -2,8 +2,8 @@
[files]
extend-exclude = [
"hypervisor/src/kvm/x86_64/mod.rs",
"resources/linux-config-*",
"hypervisor/src/kvm/x86_64/mod.rs",
"resources/linux-config-*",
]
[default.extend-words]

View File

@@ -1,79 +0,0 @@
## For Humans
This is a compact [AGENTS.md](https://agents.md/) file for Cloud Hypervisor.
It is meant to help automated coding agents make useful changes that stay safe,
reviewable, and compatible with the project's normal engineering constraints.
## For LLMs
### Project Context
- 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!
- Respect `.editorconfig` when editing files, in addition to any
language-specific formatter required by `CONTRIBUTING.md`.
### Change Guidelines
- Prefer correctness, safety, and readability over micro-optimizations. Keep
changes small, reviewable, and aligned with the existing crate/module
boundaries. Avoid speculative changes and unrelated refactoring.
- For API, config, migration, device model, or hypervisor boundary changes,
consider the effect on all architectures and all backends. Changes to one
backend can be okay if the other backend still functions properly and could
be extended or modified later.
- Follow Rust best practices and the style already present in the touched code.
- Avoid new dependencies unless the benefit is clear and local alternatives are
not enough.
- Preserve existing behavior unless the requested change explicitly needs a
behavior change; refactors must preserve behavior. Call out compatibility or
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
- Prefer safe Rust. If `unsafe` is necessary, keep it narrow, add a `SAFETY:`
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.
### Build and Test Notes
- Some workspace members require the `kvm` feature to build or test correctly.
When a default build failure looks feature-related, retry the narrow command
with `--features kvm` before widening the diagnosis.
- Prefer narrow crate/test commands while iterating, then broaden verification
when the touched surface justifies it.
- Formatting currently needs nightly-only rustfmt features; use
`cargo +nightly fmt --all`.
- Add targeted unit tests for bug fixes and non-trivial logic where practical.
Keep test scaffolding minimal and focused.
- Integration tests live in `./cloud-hypervisor/tests/` and are normally driven
by `./scripts/dev_cli.sh` / `./scripts/run_integration_tests_*.sh`. They need
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.
### Commit and Patch Formatting
- Follow the rules in `CONTRIBUTING.md`, including reviewable commit structure,
valid component prefixes, 72-column commit messages, and a `Signed-off-by`
trailer.
- Lines in a commit message that are allowed to exceed the 72-column limit are
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. 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,52 +11,17 @@ 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
## Coding Style & Code Comments
We use the [Rust Style] guide and enforce formatting and linting in CI,
including `rustfmt`, `clippy`, and other common Rust quality checks, for every
pull request. We adapt to best practices, new lints and new tooling as the
ecosystem evolves.
Code should **speak for itself** (for example, by using descriptive identifiers)
and be **easy to read and maintain**. Beyond the conventions and tooling
described above, contributors have _some_ room to apply their own style and
preferred structure. Maintainers may still suggest refactorings where they
believe readability, consistency, or maintainability can be improved.
For new code, add documentation and comments where they **provide additional value**:
* **Rustdoc** explains the API to its users.
* **Inline comments** explain the code the reader, especially *why* it is
written that way.
* **Commit messages** explain the broader context of a change (for more
information on commit messages, see below).
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
We follow the [Rust Style](https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src)
convention and enforce it through the Continuous Integration (CI) process calling into `rustfmt`,
`clippy`, and other well-known code quality tool of the ecosystem for each submitted Pull Request (PR).
## Basic Checks
```sh
# We currently rely on nightly-only formatting features
cargo +nightly fmt --all
cargo +nightly fmt --all
cargo check --all-targets --tests
cargo clippy --all-targets --tests
# Please note that this will not execute integration tests.
@@ -71,7 +36,7 @@ gitlint --commits "HEAD~3..HEAD"
_Caution: These tests are taking a long time to complete (40+ mins) and need special setup._
```sh
bash ./scripts/dev_cli.sh tests --integration -- --test-filter '<optionally filter test by name pattern>'
bash ./scripts/dev_cli.sh tests --integration -- --test-filter '<optionally filter test by name pattern>'
```
### Setup Commit Hook
@@ -93,65 +58,42 @@ commit you make.
## Certificate of Origin
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://www.kernel.org/doc/Documentation/process/submitting-patches.rst)
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://web.archive.org/web/20230406041855/https://01.org/community/signed-process)
used by the Linux kernel project.
## Patch format & Git Commit Hygiene
## Patch format
_We use **Patch** as synonym for **Commit**._
Beside the signed-off-by footer, we expect each patch to comply with the following format:
We require patches to:
```
<component>: Change summary
- Have a `Signed-off-by: Name <email>` footer
- Follow the pattern: \
```
<component>: Change summary
More detailed explanation of your changes: Why and how.
Wrap it to 72 characters.
See http://chris.beams.io/posts/git-commit/
for some more good pieces of advice.
More detailed explanation of your changes: Why and how.
Wrap it to 72 characters.
See http://chris.beams.io/posts/git-commit/
for some more good pieces of advice.
Signed-off-by: <contributor@foo.com>
```
Signed-off-by: <contributor@foo.com>
```
Valid components are listed in `TitleStartsWithComponent.py`. In short, each
cargo workspace member is a valid component as well as `build`, `ci`, `docs` and
`misc`.
Example patch:
For example:
```
vm-virtio: Reset underlying device on driver request
If the driver triggers a reset by writing zero into the status register
then reset the underlying device if supported. A device reset also
requires resetting various aspects of the queue.
In order to be able to do a subsequent reactivate it is required to
reclaim certain resources (interrupt and queue EventFDs.) If a device
reset is requested by the driver but the underlying device does not
support it then generate an error as the driver would not be able to
configure it anyway.
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
```
### Git Commit History
We value a clean, **reviewable** commit history. Each commit should represent
a self-contained, logical step that guides reviewers clearly from A to B.
Avoid patterns like `init A -> init B -> fix A` or \
`init design A -> revert A -> use design B`. Commits must be independently
reviewable - don't leave "fix previous commit" or earlier design attempts in
the history.
Intermediate work-in-progress changes are acceptable only if a subsequent
commit in the same series cleans them up (e.g. a temporary `#[allow(unused)]`
removed in the next commit).
## Pull requests
Cloud Hypervisor uses the “fork-and-pull” development model. Follow these steps if
@@ -162,14 +104,10 @@ you want to merge your changes to `cloud-hypervisor`:
1. Within your fork, create a branch for your contribution.
1. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/)
against the main branch of the Cloud Hypervisor repository.
1. Each commit must comply with the Commit Hygiene guidelines above.
1. A pull request should address a single component or concern to keep review
focused and approvals straightforward.
1. To update your pull request amend existing commits whenever applicable and
then push the new changes to your pull request branch.
1. Once the pull request is approved it can be integrated.
Please squash any changes done during review already into the corresponding
commits instead of pushing `<component>: addressing review for A`-style commits.
## Issue tracking
If you have a problem, please let us know. We recommend using
@@ -185,83 +123,26 @@ comments or by adding the `Fixes` keyword to your commit message:
```
serial: Set terminal in raw mode
In order to have proper output from the serial, we need to setup the
terminal in raw mode. When the VM is shutting down, it is also the
VMM responsibility to set the terminal back into canonical mode if we
don't want to get any weird behavior from the shell.
Fixes #88
Signed-off-by: Sebastien Boeuf <sebastien.boeuf@intel.com>
```
Then, after the corresponding PR is merged, GitHub will automatically close that issue when parsing the
[commit message](https://help.github.com/articles/closing-issues-via-commit-messages/).
## AI/LLM Assistance & Generated Code
## AI Generated Code
We recommend **a careful and conservative approach** to LLM usage, guided by
sound engineering judgment. Please use AI/LLM-assisted tooling thoughtfully and
responsibly to ensure efficient use of limited project resources, particularly
in code review and long-term maintenance. Our primary goals are to avoid
ambiguity in license compliance and to keep contributions clear and easy to
review.
Our policy is to decline any contributions known to contain contents
generated or derived from using Large Language Models (LLMs). This
includes ChatGPT, Gemini, Claude, Copilot and similar tools.
Or in other words: please apply common sense and don't blindly accept LLM
suggestions.
This policy can be revisited as LLMs evolve and mature.
### Code Review
We generally recommend doing early coarse-grained reviews using state-of-the-art
LLMs. This can help identify rough edges, copy & paste errors, and typos early
on. This reduces review cycles for human reviewers.
Please **do not** use GitHub Copilot directly in PRs to keep discussions clean.
Instead, ask an LLM of your choice for a review. A convenient way to do this is
- appending `.patch` to the GitHub PR URL
(e.g., `https://github.com/cloud-hypervisor/cloud-hypervisor/pull/1234.patch`)
and pasting it into the LLM of your choice, or
- using a local agent in your terminal, such as `codex` or `claude`.
### Contributions assisted by LLMs
All contributions **must** be submitted by a human contributor. Automated or
bot-driven PRs are not accepted.
You are responsible for every piece of code you submit, and you must understand
both the design and the implementation details. LLMs are useful for prototyping
and generating boilerplate code. However, large or complex logic must be
authored and fully understood by the contributor - LLM output should not be
submitted without careful review and comprehension.
Please disclose LLM use in your commit message and PR description if it
meaningfully contributed to the submitted code. Again, we recommend careful and
conservative use of LLMs, guided by common sense.
Use the following tag to disclose LLM assistance in your commit message:
```
Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]
```
Where:
- ``AGENT_NAME`` is the name of the AI tool or framework
- ``MODEL_VERSION`` is the specific model version used
- ``[TOOL1] [TOOL2]`` are optional specialized analysis tools used
Basic development tools (git, make, editors) should not be listed.
Example:
```
Assisted-by: Claude:Opus-4.6 CodeQL
```
Maintainers reserve the right to request additional clarification or decline
contributions where LLM usage raises concerns. Ultimately, acceptance of any
contribution is at the maintainers' discretion.
The goal is to avoid ambiguity in license compliance and optimize the
use of limited project resources, especially for code review and
maintenance. This policy can be revisited as LLMs evolve and mature.

1199
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@ members = [
"devices",
"event_monitor",
"hypervisor",
"net_gen",
"net_util",
"offload_daemon",
"option_parser",
"pci",
"performance-metrics",
@@ -41,36 +41,27 @@ members = [
"vmm",
]
package.edition = "2024"
# Minimum buildable version:
# Keep in sync with version in .github/workflows/build.yaml
# Policy on MSRV (see #4318):
# Can only be bumped if satisfying any of the following:
# a.) A dependency requires it,
# b.) If we want to use a new feature and that MSRV is at least 6 months old,
# c.) There is a security issue that is addressed by the toolchain update.
package.rust-version = "1.89.0"
resolver = "3"
[workspace.dependencies]
# rust-vmm crates
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"
acpi_tables = "0.2.0"
kvm-bindings = "0.12.1"
kvm-ioctls = "0.22.1"
linux-loader = "0.13.1"
mshv-bindings = "0.6.7"
mshv-ioctls = "0.6.7"
seccompiler = "0.5.0"
vfio-bindings = { version = "0.6.2", 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 }
vfio-bindings = { version = "0.6.0", default-features = false }
vfio-ioctls = { version = "0.5.1", default-features = false }
vfio_user = { version = "0.1.1", default-features = false }
vhost = { version = "0.14.0", default-features = false }
vhost-user-backend = { version = "0.20.0", default-features = false }
virtio-bindings = "0.2.6"
virtio-queue = "0.17.0"
virtio-queue = "0.16.0"
vm-fdt = "0.3.0"
vm-memory = "0.17.1"
vmm-sys-util = "0.15.0"
vm-memory = "0.16.1"
vmm-sys-util = "0.14.0"
# igvm crates
igvm = "0.4.0"
@@ -78,38 +69,28 @@ igvm_defs = "0.4.0"
# serde crates
serde = "1.0.228"
serde_json = "1.0.150"
serde_with = { version = "3.19.0", default-features = false }
serde_json = "1.0.149"
serde_with = { version = "3.16.1", default-features = false }
# other crates
anyhow = "1.0.102"
base64 = "0.22.1"
bitflags = "2.11.1"
anyhow = "1.0.101"
bitflags = "2.11.0"
byteorder = "1.5.0"
cfg-if = "1.0.4"
clap = "4.6.1"
clap = "4.5.59"
dhat = "0.3.3"
dirs = "6.0.0"
env_logger = "0.11.10"
env_logger = "0.11.8"
epoll = "4.4.0"
flume = "0.12.0"
itertools = "0.15.0"
jiff = { version = "0.2", default-features = false, features = ["std"] }
libc = "0.2.186"
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"
itertools = "0.14.0"
libc = "0.2.182"
log = "0.4.29"
signal-hook = "0.4.3"
thiserror = "2.0.18"
uuid = { version = "1.23.2" }
uuid = { version = "1.21.0" }
wait-timeout = "0.2.1"
zerocopy = { version = "0.8.50", default-features = false }
zerocopy = { version = "0.8.39", default-features = false }
[workspace.lints.clippy]
# Any clippy lint (group) in alphabetical order:
@@ -124,7 +105,6 @@ 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'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).
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).
The `riscv64` architecture support is experimental and offers limited
functionality. For more details and instructions, please refer to [riscv
documentation](docs/riscv.md).
@@ -111,25 +111,19 @@ do not wish to use the pre-built binaries.
## Booting Linux
Cloud Hypervisor boots guests in one of two ways. The first is direct
kernel boot, where a kernel image is passed to `--kernel`. The x86-64
kernel must be built with PVH support or be a bzImage. The second is
firmware boot, where a firmware image is passed to `--firmware` and
brings up the guest's normal boot loader.
Cloud Hypervisor supports direct kernel boot (the x86-64 kernel requires the kernel
built with PVH support or a bzImage) or booting via a firmware (either [Rust Hypervisor
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) or an
edk2 UEFI firmware called `CLOUDHV` / `CLOUDHV_EFI`.)
Two firmware options are supported, and which one works best depends
on the guest OS. [Rust Hypervisor
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware)
is a lightweight Rust-based PVH firmware. The edk2 UEFI firmware is
called `CLOUDHV.fd` for x86-64 and `CLOUDHV_EFI.fd` for AArch64.
Prebuilt binaries for both are available at their respective releases
pages, [Rust Hypervisor
Binary builds of the firmware files are available for the latest release of
[Rust Hypervisor
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/latest)
and [our edk2
fork](https://github.com/cloud-hypervisor/edk2/releases/latest).
The edk2 fork carries customizations required to boot AArch64 guests
on cloud-hypervisor. See [docs/uefi.md](docs/uefi.md) for differences
with upstream tianocore/edk2.
repository](https://github.com/cloud-hypervisor/edk2/releases/latest)
The choice of firmware depends on your guest OS choice; some experimentation
may be required.
### Firmware Booting
@@ -203,7 +197,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.16.9 linux-cloud-hypervisor
$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.12.8 linux-cloud-hypervisor
$ pushd linux-cloud-hypervisor
$ make ch_defconfig
# Do native build of the x86-64 kernel

View File

@@ -3,7 +3,6 @@ authors = ["The Cloud Hypervisor Authors"]
edition.workspace = true
license = "Apache-2.0"
name = "api_client"
rust-version.workspace = true
version = "0.1.0"
[dependencies]

View File

@@ -3,26 +3,24 @@
// SPDX-License-Identifier: Apache-2.0
//
use std::io::{self, Read, Write};
use std::io::{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] io::Error),
Socket(#[source] std::io::Error),
#[error("Error sending file descriptors")]
SocketSendFds(#[source] errno::Error),
SocketSendFds(#[source] vmm_sys_util::errno::Error),
#[error("Error parsing HTTP status code")]
StatusCodeParsing(#[source] num::ParseIntError),
StatusCodeParsing(#[source] std::num::ParseIntError),
#[error("HTTP output is missing protocol statement")]
MissingProtocol,
#[error("Error parsing HTTP Content-Length field")]
ContentLengthParsing(#[source] num::ParseIntError),
ContentLengthParsing(#[source] std::num::ParseIntError),
#[error("Server responded with error {0:?}: {1:?}")]
ServerResponse(
StatusCode,
@@ -102,7 +100,7 @@ fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
if count == 0 {
break;
}
res.push_str(str::from_utf8(&bytes[0..count]).unwrap());
res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap());
// End of headers
if let Some(o) = res.find("\r\n\r\n") {

View File

@@ -2,7 +2,6 @@
authors = ["The Chromium OS Authors"]
edition.workspace = true
name = "arch"
rust-version.workspace = true
version = "0.1.0"
[features]
@@ -25,10 +24,6 @@ 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 }

View File

@@ -1,188 +0,0 @@
// 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,24 +9,22 @@
use std::collections::HashMap;
use std::ffi::CStr;
use std::fmt::Debug;
use std::hash::BuildHasher;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::{cmp, result, str};
use std::{cmp, fs, 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};
use log::{debug, info, warn};
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,
@@ -90,9 +88,124 @@ 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.
#[expect(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
#[allow(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
vcpu_mpidr: &[u64],
@@ -174,24 +287,63 @@ 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.
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();
// 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.");
}
// Arm boot protocol requires a minimal Device Tree
// https://docs.kernel.org/arch/arm64/booting.html
@@ -727,7 +879,7 @@ fn create_fw_cfg_node<T: DeviceInfoForFdt + Clone + Debug>(
Ok(())
}
fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
fdt: &mut FdtWriter,
dev_info: &HashMap<(DeviceType, String), T, S>,
) -> FdtWriterResult<()> {
@@ -993,7 +1145,7 @@ pub fn print_fdt(dtb: &[u8]) {
}
}
fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) {
fn print_node(node: fdt_parser::node::FdtNode<'_, '_>, n_spaces: usize) {
debug!("{:indent$}{}/", "", node.name, indent = n_spaces);
for property in node.properties() {
let name = property.name;

View File

@@ -2,8 +2,6 @@
// 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.
@@ -13,7 +11,6 @@ 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;
@@ -124,8 +121,8 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
}
/// Configures the system and should be called once per vm before starting vcpu threads.
#[expect(clippy::too_many_arguments)]
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
#[allow(clippy::too_many_arguments)]
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
vcpu_mpidr: &[u64],

View File

@@ -7,7 +7,7 @@ use std::os::fd::AsFd;
use std::result;
use thiserror::Error;
use vm_memory::{Bytes, GuestAddress, GuestMemory};
use vm_memory::{GuestAddress, GuestMemory};
/// Errors thrown while loading UEFI binary
#[derive(Debug, Error)]

View File

@@ -9,17 +9,14 @@
//! 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<AtomicBitmap>;
type GuestRegionMmap = vm_memory::GuestRegionMmap<AtomicBitmap>;
type GuestMemoryMmap = vm_memory::GuestMemoryMmap<vm_memory::bitmap::AtomicBitmap>;
type GuestRegionMmap = vm_memory::GuestRegionMmap<vm_memory::bitmap::AtomicBitmap>;
/// Type for returning error code.
#[derive(Debug, Error)]
@@ -36,13 +33,11 @@ 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(#[source] vm_memory::GuestMemoryError),
#[error("Error generating memory map table")]
MemmapTableGeneration,
MemmapTableSetup,
#[error("The hvm_start_info structure extends past the end of guest memory")]
StartInfoPastRamEnd,
#[error("Error writing hvm_start_info to guest memory")]
StartInfoSetup(#[source] vm_memory::GuestMemoryError),
StartInfoSetup,
#[error("Failed to compute initramfs address")]
InitramfsAddress,
#[error("Error writing module entry to guest memory")]
@@ -58,26 +53,6 @@ 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 {
@@ -125,9 +100,8 @@ pub mod x86_64;
#[cfg(target_arch = "x86_64")]
pub use x86_64::{
_NSIG, CpuidConfig, CpuidFeatureEntry, EntryPoint, arch_memory_regions, configure_system,
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,
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,
};
/// 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.
#[expect(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
@@ -71,7 +71,6 @@ 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()?;
@@ -87,7 +86,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, timebase_frequency)?;
create_cpu_nodes(&mut fdt, num_vcpu, isa_string)?;
create_memory_node(&mut fdt, guest_mem)?;
create_chosen_node(&mut fdt, cmdline, initrd)?;
create_aia_node(&mut fdt, aia_device)?;
@@ -111,17 +110,14 @@ 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,
timebase_frequency: u32,
) -> FdtWriterResult<()> {
fn create_cpu_nodes(fdt: &mut FdtWriter, num_cpus: u32, isa_string: &str) -> 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 {
@@ -235,8 +231,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")?;
let imsic_num_ids = aia_device.lock().unwrap().imsic_num_ids();
fdt.property_u32("riscv,num-ids", imsic_num_ids)?;
// TODO complete num-ids
fdt.property_u32("riscv,num-ids", 2047u32)?;
fdt.property_u32("phandle", AIA_IMSIC_PHANDLE)?;
let mut irq_cells = Vec::new();

View File

@@ -160,6 +160,7 @@ 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,
@@ -168,7 +169,6 @@ 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,7 +180,6 @@ 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::{Bytes, GuestAddress, GuestMemory};
use vm_memory::{GuestAddress, GuestMemory};
/// Errors thrown while loading UEFI binary
#[derive(Debug, Error)]

View File

@@ -1,190 +0,0 @@
// 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

@@ -1,880 +0,0 @@
// 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

@@ -1,82 +0,0 @@
// 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,7 +7,6 @@
// 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;
@@ -15,14 +14,13 @@ 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};
@@ -30,16 +28,13 @@ 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::x86_64::cpu_profile::cpuid_adjustments::MissingCpuidEntriesError;
use crate::{CpuProfile, GuestMemoryMmap, InitramfsConfig, RegionType};
use crate::{GuestMemoryMmap, InitramfsConfig, RegionType};
// While modern architectures support more than 255 CPUs via x2APIC,
// legacy devices such as mptable support at most 254 CPUs.
@@ -60,9 +55,6 @@ 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;
@@ -101,7 +93,6 @@ pub struct CpuidConfig {
#[cfg(feature = "tdx")]
pub tdx: bool,
pub amx: bool,
pub profile: CpuProfile,
}
#[derive(Debug, Error)]
@@ -144,32 +135,16 @@ pub enum Error {
/// Error populating CPUID with KVM HyperV emulation details
#[error("Error populating CPUID with KVM HyperV emulation details")]
CpuidKvmHyperV(#[source] fam::Error),
CpuidKvmHyperV(#[source] vmm_sys_util::fam::Error),
/// Error populating CPUID with CPU identification
#[error("Error populating CPUID with CPU identification")]
CpuidIdentification(#[source] fam::Error),
CpuidIdentification(#[source] vmm_sys_util::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),
@@ -215,7 +190,7 @@ pub fn get_max_x2apic_id(topology: (u16, u16, u16, u16)) -> u32 {
)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[derive(Copy, Clone, Debug)]
pub enum CpuidReg {
EAX,
EBX,
@@ -527,27 +502,11 @@ 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);
@@ -573,8 +532,8 @@ impl CpuidFeatureEntry {
};
if !entry_compatible {
error!(
"Detected incompatible CPUID entry: leaf={:#04x} (subleaf={:#04x}), register='{:?}', \
compatible_check='{:?}', {src_description} feature='{:#04x}', {dest_description} feature='{:#04x}'.",
"Detected incompatible CPUID entry: leaf={:#02x} (subleaf={:#02x}), register='{:?}', \
compatible_check='{:?}', source VM feature='{:#04x}', destination VM feature'{:#04x}'.",
entry.function,
entry.index,
entry.feature_reg,
@@ -596,15 +555,6 @@ 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,
@@ -627,86 +577,9 @@ pub fn generate_common_cpuid(
}
info!(
"Generating guest CPUID with physical address size: {}",
"Generating guest CPUID for 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
@@ -732,7 +605,10 @@ fn required_common_cpuid_updates(
];
#[cfg(feature = "kvm")]
if matches!(hypervisor_type, hypervisor::HypervisorType::Kvm) {
if matches!(
hypervisor.hypervisor_type(),
hypervisor::HypervisorType::Kvm
) {
// Patch tsc deadline timer bit
cpuid_patches.push(CpuidPatch {
function: 1,
@@ -745,8 +621,24 @@ fn required_common_cpuid_updates(
});
}
// 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)]
@@ -761,6 +653,25 @@ fn required_common_cpuid_updates(
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;
@@ -778,15 +689,12 @@ fn required_common_cpuid_updates(
// Copy host L1 cache details if not populated by KVM
0x8000_0005
if entry.eax == 0
&& entry.ebx == 0
&& entry.ecx == 0
&& entry.edx == 0
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0
// SAFETY: cpuid called with valid leaves
&& unsafe { x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>
{
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { x86_64::__cpuid(0x8000_0005) };
let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) };
entry.eax = leaf.eax;
entry.ebx = leaf.ebx;
entry.ecx = leaf.ecx;
@@ -794,25 +702,20 @@ fn required_common_cpuid_updates(
}
// Copy host L2 cache details if not populated by KVM
0x8000_0006
if entry.eax == 0
&& entry.ebx == 0
&& entry.ecx == 0
&& entry.edx == 0
if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0
// SAFETY: cpuid called with valid leaves
&& unsafe { x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
&& unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>
{
// SAFETY: cpuid called with valid leaves
let leaf = unsafe { x86_64::__cpuid(0x8000_0006) };
let leaf = unsafe { std::arch::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 and guest physical bits
// Set CPU physical bits
0x8000_0008 => {
entry.eax = (entry.eax & 0xff00_ff00)
| (config.phys_bits as u32 & 0xff)
| ((config.phys_bits as u32 & 0xff) << 16);
entry.eax = (entry.eax & 0xffff_ff00) | (config.phys_bits as u32 & 0xff);
}
0x4000_0001 => {
// Enable KVM_FEATURE_MSI_EXT_DEST_ID. This allows the guest to target
@@ -834,6 +737,22 @@ fn required_common_cpuid_updates(
}
}
// 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);
@@ -861,31 +780,16 @@ fn required_common_cpuid_updates(
});
cpuid.push(CpuIdEntry {
function: 0x4000_0003,
eax: (1 << 0) // AccessVpRunTimeReg
| (1 << 1) // AccessPartitionReferenceCounter
eax: (1 << 1) // AccessPartitionReferenceCounter
| (1 << 2) // AccessSynicRegs
| (1 << 3) // AccessSyntheticTimerRegs
| (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
| (1 << 9), // AccessPartitionReferenceTsc
edx: 1 << 3, // CPU dynamic partitioning
..Default::default()
});
cpuid.push(CpuIdEntry {
function: 0x4000_0004,
// 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
eax: 1 << 5, // Recommend relaxed timing
..Default::default()
});
for i in 0x4000_0005..=0x4000_000a {
@@ -896,39 +800,10 @@ fn required_common_cpuid_updates(
}
}
cpuid
Ok(cpuid)
}
#[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)]
#[allow(clippy::too_many_arguments)]
pub fn configure_vcpu(
vcpu: &dyn hypervisor::Vcpu,
id: u32,
@@ -938,7 +813,6 @@ pub fn configure_vcpu(
cpu_vendor: CpuVendor,
topology: (u16, u16, u16, u16),
nested: bool,
setup_registers: bool,
) -> super::Result<()> {
let x2apic_id = get_x2apic_id(id, Some(topology));
@@ -957,13 +831,11 @@ pub fn configure_vcpu(
entry.ebx &= 0xffffff;
entry.ebx |= x2apic_id << 24;
apic_id_patched = true;
if matches!(cpu_vendor, CpuVendor::Intel) {
if !nested {
// Disable nested virtualization for Intel
entry.ecx &= !(1 << VMX_ECX_BIT);
}
break;
if !nested {
// Disable nested virtualization for Intel
entry.ecx &= !(1 << VMX_ECX_BIT);
}
break;
}
if entry.function == 0x8000_0001 {
if !nested {
@@ -984,7 +856,9 @@ 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 { x86_64::__cpuid(0x8000_0007) }.edx & (1u32 << INVARIANT_TSC_EDX_BIT) > 0 {
if unsafe { std::arch::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 {
@@ -1010,19 +884,17 @@ pub fn configure_vcpu(
regs::setup_msrs(vcpu).map_err(Error::MsrsConfiguration)?;
if let Some((kernel_entry_point, guest_memory)) = boot_setup {
if setup_registers {
regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?;
// CPUs are required (by Intel sdm spec) to boot in x2apic mode if any
// of the apic IDs is larger than 255. Experimentally, the Linux kernel
// does not recognize the last vCPU if x2apic is not enabled when
// there are 256 vCPUs in a flat hierarchy (i.e. max x2apic ID is 255),
// so we need to enable x2apic in this case as well.
let enable_x2_apic_mode = get_max_x2apic_id(topology) > MAX_SUPPORTED_CPUS_LEGACY;
regs::setup_sregs(&guest_memory.memory(), vcpu, enable_x2_apic_mode)
.map_err(Error::SregsConfiguration)?;
}
regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?;
regs::setup_fpu(vcpu).map_err(Error::FpuConfiguration)?;
// CPUs are required (by Intel sdm spec) to boot in x2apic mode if any
// of the apic IDs is larger than 255. Experimentally, the Linux kernel
// does not recognize the last vCPU if x2apic is not enabled when
// there are 256 vCPUs in a flat hierarchy (i.e. max x2apic ID is 255),
// so we need to enable x2apic in this case as well.
let enable_x2_apic_mode = get_max_x2apic_id(topology) > MAX_SUPPORTED_CPUS_LEGACY;
regs::setup_sregs(&guest_memory.memory(), vcpu, enable_x2_apic_mode)
.map_err(Error::SregsConfiguration)?;
}
interrupts::set_lint(vcpu).map_err(|e| Error::LocalIntConfiguration(e.into()))?;
Ok(())
@@ -1066,7 +938,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.
#[expect(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub fn configure_system(
guest_mem: &GuestMemoryMmap,
cmdline_addr: GuestAddress,
@@ -1075,7 +947,9 @@ pub fn configure_system(
_num_cpus: u32,
setup_header: Option<setup_header>,
rsdp_addr: Option<GuestAddress>,
smbios: Option<&SmbiosConfig>,
serial_number: Option<&str>,
uuid: Option<&str>,
oem_strings: Option<&[&str]>,
topology: Option<(u16, u16, u16, u16)>,
) -> super::Result<()> {
// Write EBDA address to location where ACPICA expects to find it
@@ -1083,7 +957,8 @@ 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, smbios).map_err(Error::SmbiosSetup)?;
let size = smbios::setup_smbios(guest_mem, serial_number, uuid, oem_strings)
.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);
@@ -1153,9 +1028,8 @@ 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::MemmapTableGeneration)?;
let (first_region_start, first_region_end) =
ram_regions.first().ok_or(super::Error::MemmapTableSetup)?;
let high_ram_start = layout::HIGH_RAM_START.raw_value();
let mem_32bit_reserved_start = layout::MEM_32BIT_RESERVED_START.raw_value();
@@ -1168,7 +1042,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::MemmapTableGeneration);
return Err(super::Error::MemmapTableSetup);
}
info!(
@@ -1271,7 +1145,7 @@ fn configure_pvh(
guest_mem
.checked_offset(
memmap_start_addr,
size_of::<hvm_memmap_table_entry>() * start_info.memmap_entries as usize,
mem::size_of::<hvm_memmap_table_entry>() * start_info.memmap_entries as usize,
)
.ok_or(super::Error::MemmapTablePastRamEnd)?;
@@ -1279,9 +1153,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(size_of::<hvm_memmap_table_entry>() as u64);
memmap_start_addr.unchecked_add(mem::size_of::<hvm_memmap_table_entry>() as u64);
}
// The hvm_start_info struct itself must be stored at PVH_START_INFO
@@ -1290,13 +1164,13 @@ fn configure_pvh(
let start_info_addr = layout::PVH_INFO_START;
guest_mem
.checked_offset(start_info_addr, size_of::<hvm_start_info>())
.checked_offset(start_info_addr, mem::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(())
}
@@ -1369,7 +1243,7 @@ fn configure_32bit_entry(
let zero_page_addr = layout::ZERO_PAGE_START;
guest_mem
.checked_offset(zero_page_addr, size_of::<boot_params>())
.checked_offset(zero_page_addr, mem::size_of::<boot_params>())
.ok_or(super::Error::ZeroPagePastRamEnd)?;
guest_mem
.write_obj(params, zero_page_addr)
@@ -1427,15 +1301,27 @@ 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) as u8
((leaf.eax & 0xff) - reduced) as u8
} else {
36
}
@@ -1617,6 +1503,8 @@ mod unit_tests {
Some(layout::RSDP_POINTER),
None,
None,
None,
None,
);
config_err.unwrap_err();
@@ -1639,6 +1527,8 @@ mod unit_tests {
None,
None,
None,
None,
None,
)
.unwrap();
@@ -1666,6 +1556,8 @@ mod unit_tests {
None,
None,
None,
None,
None,
)
.unwrap();
@@ -1679,6 +1571,8 @@ mod unit_tests {
None,
None,
None,
None,
None,
)
.unwrap();
}

View File

@@ -3,36 +3,35 @@
// 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.
use std::os::raw;
#![allow(non_camel_case_types)]
use vm_memory::ByteValued;
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;
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;
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpf_intel {
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,
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,
}
const _: () = assert!(size_of::<mpf_intel>() == 16);
const _: () = assert!(::core::mem::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
@@ -42,24 +41,24 @@ unsafe impl ByteValued for mpf_intel {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_table {
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,
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,
}
const _: () = {
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);
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);
};
// SAFETY: all members of this struct are plain integers
@@ -71,16 +70,16 @@ unsafe impl ByteValued for mpc_table {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_cpu {
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],
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],
}
const _: () = assert!(size_of::<mpc_cpu>() == 20);
const _: () = assert!(::core::mem::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
@@ -90,12 +89,12 @@ unsafe impl ByteValued for mpc_cpu {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_bus {
pub type_: raw::c_uchar,
pub busid: raw::c_uchar,
pub bustype: [raw::c_uchar; 6usize],
pub type_: ::std::os::raw::c_uchar,
pub busid: ::std::os::raw::c_uchar,
pub bustype: [::std::os::raw::c_uchar; 6usize],
}
const _: () = assert!(size_of::<mpc_bus>() == 8);
const _: () = assert!(::core::mem::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
@@ -105,14 +104,14 @@ unsafe impl ByteValued for mpc_bus {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_ioapic {
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,
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,
}
const _: () = assert!(size_of::<mpc_ioapic>() == 8);
const _: () = assert!(::core::mem::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
@@ -122,39 +121,39 @@ unsafe impl ByteValued for mpc_ioapic {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_intsrc {
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,
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,
}
const _: () = assert!(size_of::<mpc_intsrc>() == 8);
const _: () = assert!(::core::mem::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: 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;
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;
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_lintsrc {
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,
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,
}
const _: () = assert!(size_of::<mpc_lintsrc>() == 8);
const _: () = assert!(::core::mem::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
@@ -164,14 +163,14 @@ unsafe impl ByteValued for mpc_lintsrc {}
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
pub struct mpc_oemtable {
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],
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],
}
const _: () = assert!(size_of::<mpc_oemtable>() == 16);
const _: () = assert!(::core::mem::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::result;
use std::{mem, result, slice};
use libc::c_uchar;
use log::{info, warn};
@@ -101,8 +101,10 @@ const CPU_FEATURE_APIC: u32 = 0x200;
const CPU_FEATURE_FPU: u32 = 0x001;
fn compute_checksum<T: Copy + ByteValued>(v: &T) -> u8 {
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::<T>()) };
let mut checksum: u8 = 0;
for i in v.as_slice().iter() {
for i in v_slice.iter() {
checksum = checksum.wrapping_add(*i);
}
checksum
@@ -114,13 +116,13 @@ fn mpf_intel_compute_checksum(v: &mpspec::mpf_intel) -> u8 {
}
fn compute_mp_size(num_cpus: u32) -> usize {
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
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
}
/// Performs setup of the MP table for the given `num_cpus`.
@@ -167,7 +169,7 @@ pub fn setup_mptable(
{
let mut mpf_intel = MpfIntelWrapper(mpspec::mpf_intel::default());
let size = size_of::<MpfIntelWrapper>() as u64;
let size = mem::size_of::<MpfIntelWrapper>() as u64;
mpf_intel.0.signature = *SMP_MAGIC_IDENT;
mpf_intel.0.length = 1;
mpf_intel.0.specification = 4;
@@ -181,10 +183,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(size_of::<MpcTableWrapper>() as u64);
base_mp = base_mp.unchecked_add(mem::size_of::<MpcTableWrapper>() as u64);
{
let size = size_of::<MpcCpuWrapper>();
let size = mem::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;
@@ -205,7 +207,7 @@ pub fn setup_mptable(
}
}
{
let size = size_of::<MpcBusWrapper>();
let size = mem::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;
@@ -216,7 +218,7 @@ pub fn setup_mptable(
checksum = checksum.wrapping_add(compute_checksum(&mpc_bus.0));
}
{
let size = size_of::<MpcIoapicWrapper>();
let size = mem::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;
@@ -230,7 +232,7 @@ pub fn setup_mptable(
}
// Per kvm_setup_default_irq_routing() in kernel
for i in 0..16 {
let size = size_of::<MpcIntsrcWrapper>();
let size = mem::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;
@@ -245,7 +247,7 @@ pub fn setup_mptable(
checksum = checksum.wrapping_add(compute_checksum(&mpc_intsrc.0));
}
{
let size = size_of::<MpcLintsrcWrapper>();
let size = mem::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;
@@ -260,7 +262,7 @@ pub fn setup_mptable(
checksum = checksum.wrapping_add(compute_checksum(&mpc_lintsrc.0));
}
{
let size = size_of::<MpcLintsrcWrapper>();
let size = mem::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;
@@ -305,11 +307,11 @@ mod unit_tests {
fn table_entry_size(type_: u8) -> usize {
match type_ as u32 {
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>(),
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>(),
_ => panic!("unrecognized mpc table entry type: {type_}"),
}
}
@@ -402,7 +404,7 @@ mod unit_tests {
.unwrap();
let mut entry_offset = mpc_offset
.checked_add(size_of::<MpcTableWrapper>() as GuestUsize)
.checked_add(mem::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::result;
use std::{mem, 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 * size_of::<u64>())
.checked_offset(boot_gdt_addr, index * mem::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 = size_of_val(&gdt_table) as u16 - 1;
sregs.gdt.limit = mem::size_of_val(&gdt_table) as u16 - 1;
write_idt_value(0, mem)?;
sregs.idt.base = BOOT_IDT_START.raw_value();
sregs.idt.limit = size_of::<u64>() as u16 - 1;
sregs.idt.limit = mem::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::result;
use std::{mem, result, slice};
use thiserror::Error;
use uuid::Uuid;
@@ -28,74 +28,38 @@ pub enum Error {
Clear,
/// Failure to write SMBIOS entrypoint structure
#[error("Failure to write SMBIOS entrypoint structure")]
WriteSmbiosEp(#[source] vm_memory::GuestMemoryError),
WriteSmbiosEp,
/// Failure to write additional data to memory
#[error("Failure to write additional data to memory")]
WriteData(#[source] vm_memory::GuestMemoryError),
WriteData,
/// 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.9.0.
// Constants sourced from SMBIOS Spec 3.2.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";
#[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 {
fn compute_checksum<T: Copy>(v: &T) -> u8 {
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::<T>()) };
let mut checksum: u8 = 0;
for i in v.as_slice().iter() {
for i in v_slice.iter() {
checksum = checksum.wrapping_add(*i);
}
(!checksum).wrapping_add(1)
}
#[repr(C, packed)]
#[repr(C)]
#[repr(packed)]
#[derive(Default, Copy, Clone)]
struct Smbios30Entrypoint {
signature: [u8; 5usize],
@@ -110,7 +74,8 @@ struct Smbios30Entrypoint {
physptr: u64,
}
#[repr(C, packed)]
#[repr(C)]
#[repr(packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosBiosInfo {
r#type: u8,
@@ -126,7 +91,8 @@ struct SmbiosBiosInfo {
characteristics_ext2: u8,
}
#[repr(C, packed)]
#[repr(C)]
#[repr(packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosSysInfo {
r#type: u8,
@@ -142,7 +108,8 @@ struct SmbiosSysInfo {
family: u8,
}
#[repr(C, packed)]
#[repr(C)]
#[repr(packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosOemStrings {
r#type: u8,
@@ -151,34 +118,8 @@ struct SmbiosOemStrings {
count: u8,
}
/// 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)]
#[repr(C)]
#[repr(packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosEndOfTable {
r#type: u8,
@@ -195,8 +136,6 @@ 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>(
@@ -204,9 +143,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(size_of::<T>() as u64)
.checked_add(mem::size_of::<T>() as u64)
.ok_or(Error::NotEnoughMemory)?;
Ok(curptr)
}
@@ -223,155 +162,14 @@ fn write_string(
Ok(curptr)
}
fn write_opt_string(
pub fn setup_smbios(
mem: &GuestMemoryMmap,
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);
serial_number: Option<&str>,
uuid: Option<&str>,
oem_strings: Option<&[&str]>,
) -> Result<u64> {
let physptr = GuestAddress(SMBIOS_START)
.checked_add(size_of::<Smbios30Entrypoint>() as u64)
.checked_add(mem::size_of::<Smbios30Entrypoint>() as u64)
.ok_or(Error::NotEnoughMemory)?;
let mut curptr = physptr;
let mut handle = 0;
@@ -380,7 +178,7 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
handle += 1;
let smbios_biosinfo = SmbiosBiosInfo {
r#type: BIOS_INFORMATION,
length: size_of::<SmbiosBiosInfo>() as u8,
length: mem::size_of::<SmbiosBiosInfo>() as u8,
handle,
vendor: 1, // First string written in this section
version: 2, // Second string written in this section
@@ -394,18 +192,39 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
curptr = write_and_incr(mem, 0u8, curptr)?;
}
write_type1_system(mem, &mut curptr, &mut handle, system)?;
{
handle += 1;
if let Some(chassis) = chassis {
write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?;
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 !oem_strings.is_empty() {
if let Some(oem_strings) = oem_strings {
handle += 1;
let smbios_oemstrings = SmbiosOemStrings {
r#type: OEM_STRINGS,
length: size_of::<SmbiosOemStrings>() as u8,
length: mem::size_of::<SmbiosOemStrings>() as u8,
handle,
count: oem_strings.len() as u8,
};
@@ -416,14 +235,14 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
curptr = write_string(mem, s, curptr)?;
}
curptr = write_string_terminator(mem, curptr, true)?;
curptr = write_and_incr(mem, 0u8, curptr)?;
}
{
handle += 1;
let smbios_end = SmbiosEndOfTable {
r#type: END_OF_TABLE,
length: size_of::<SmbiosEndOfTable>() as u8,
length: mem::size_of::<SmbiosEndOfTable>() as u8,
handle,
};
curptr = write_and_incr(mem, smbios_end, curptr)?;
@@ -434,7 +253,7 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
{
let mut smbios_ep = Smbios30Entrypoint {
signature: *SM3_MAGIC_IDENT,
length: size_of::<Smbios30Entrypoint>() as u8,
length: mem::size_of::<Smbios30Entrypoint>() as u8,
// SMBIOS rev 3.2.0
majorver: 0x03,
minorver: 0x02,
@@ -446,261 +265,43 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
};
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) + size_of::<Smbios30Entrypoint>() as u64)
Ok(curptr.unchecked_offset_from(physptr) + std::mem::size_of::<Smbios30Entrypoint>() as u64)
}
#[cfg(test)]
mod unit_tests {
use super::*;
/// 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).unwrap();
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
assert_eq!(compute_checksum(&smbios_ep), 0);
}
#[test]
fn entrypoint_struct_size() {
fn struct_size() {
assert_eq!(
size_of::<Smbios30Entrypoint>(),
mem::size_of::<Smbios30Entrypoint>(),
0x18usize,
concat!("Size of: ", stringify!(Smbios30Entrypoint))
);
assert_eq!(
size_of::<SmbiosBiosInfo>(),
mem::size_of::<SmbiosBiosInfo>(),
0x14usize,
concat!("Size of: ", stringify!(SmbiosBiosInfo))
);
assert_eq!(
size_of::<SmbiosSysInfo>(),
mem::size_of::<SmbiosSysInfo>(),
0x1busize,
concat!("Size of: ", stringify!(SmbiosSysInfo))
);
}
#[test]
fn smbios_chassis_empty_string_set_has_double_null() {
fn entrypoint_checksum() {
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();
setup_smbios(&mem, None, None, 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();
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(_)));
assert_eq!(compute_checksum(&smbios_ep), 0);
}
}

View File

@@ -2,8 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::slice;
use std::io::{Read, Seek, SeekFrom};
use std::str::FromStr;
use log::{debug, info};
@@ -16,11 +15,11 @@ use crate::GuestMemoryMmap;
#[derive(Error, Debug)]
pub enum TdvfError {
#[error("Failed read TDVF descriptor")]
ReadDescriptor(#[source] io::Error),
ReadDescriptor(#[source] std::io::Error),
#[error("Failed read TDVF descriptor offset")]
ReadDescriptorOffset(#[source] io::Error),
ReadDescriptorOffset(#[source] std::io::Error),
#[error("Failed read GUID table")]
ReadGuidTable(#[source] io::Error),
ReadGuidTable(#[source] std::io::Error),
#[error("Invalid descriptor signature")]
InvalidDescriptorSignature,
#[error("Invalid descriptor size")]
@@ -163,7 +162,10 @@ 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 {
slice::from_raw_parts_mut((&raw mut descriptor).cast(), size_of::<TdvfDescriptor>())
std::slice::from_raw_parts_mut(
&mut descriptor as *mut _ as *mut u8,
std::mem::size_of::<TdvfDescriptor>(),
)
})
.map_err(TdvfError::ReadDescriptor)?;
@@ -172,7 +174,8 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
}
if descriptor.length as usize
!= size_of::<TdvfDescriptor>() + size_of::<TdvfSection>() * descriptor.num_sections as usize
!= std::mem::size_of::<TdvfDescriptor>()
+ std::mem::size_of::<TdvfSection>() * descriptor.num_sections as usize
{
return Err(TdvfError::InvalidDescriptorSize);
}
@@ -186,9 +189,9 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
// SAFETY: we read exactly the advertised sections
file.read_exact(unsafe {
slice::from_raw_parts_mut(
sections.as_mut_ptr().cast(),
descriptor.num_sections as usize * size_of::<TdvfSection>(),
std::slice::from_raw_parts_mut(
sections.as_mut_ptr() as *mut u8,
descriptor.num_sections as usize * std::mem::size_of::<TdvfSection>(),
)
})
.map_err(TdvfError::ReadDescriptor)?;
@@ -302,7 +305,7 @@ fn align_hob(v: u64) -> u64 {
impl TdHob {
fn update_offset<T>(&mut self) {
self.current_offset = align_hob(self.current_offset + size_of::<T>() as u64);
self.current_offset = align_hob(self.current_offset + std::mem::size_of::<T>() as u64);
}
pub fn start(offset: u64) -> TdHob {
@@ -319,7 +322,7 @@ impl TdHob {
// Write end
let end = HobHeader {
r#type: HobType::EndOfHobList,
length: size_of::<HobHeader>() as u16,
length: std::mem::size_of::<HobHeader>() as u16,
reserved: 0,
};
info!("Writing HOB end {:x} {:x?}", self.current_offset, end);
@@ -332,7 +335,7 @@ impl TdHob {
let handoff = HobHandoffInfoTable {
header: HobHeader {
r#type: HobType::Handoff,
length: size_of::<HobHandoffInfoTable>() as u16,
length: std::mem::size_of::<HobHandoffInfoTable>() as u16,
reserved: 0,
},
version: 0x9,
@@ -359,7 +362,7 @@ impl TdHob {
let resource_descriptor = HobResourceDescriptor {
header: HobHeader {
r#type: HobType::ResourceDescriptor,
length: size_of::<HobResourceDescriptor>() as u16,
length: std::mem::size_of::<HobResourceDescriptor>() as u16,
reserved: 0,
},
owner: EfiGuid::default(),
@@ -436,7 +439,8 @@ 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 = size_of::<HobGuidType>() as u16 + align_hob(table_content.len() as u64) as u16;
let length = std::mem::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,
@@ -458,7 +462,7 @@ impl TdHob {
);
mem.write_obj(hob_guid_type, GuestAddress(self.current_offset))
.map_err(TdvfError::GuestMemoryWriteHob)?;
let current_offset = self.current_offset + size_of::<HobGuidType>() as u64;
let current_offset = self.current_offset + std::mem::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.
@@ -489,7 +493,7 @@ impl TdHob {
guid_type: HobGuidType {
header: HobHeader {
r#type: HobType::GuidExtension,
length: size_of::<TdPayload>() as u16,
length: std::mem::size_of::<TdPayload>() as u16,
reserved: 0,
},
// HOB_PAYLOAD_INFO_GUID
@@ -522,7 +526,7 @@ mod unit_tests {
#[test]
#[ignore]
fn test_parse_tdvf_sections() {
let mut f = File::open("tdvf.fd").unwrap();
let mut f = std::fs::File::open("tdvf.fd").unwrap();
let (sections, _) = parse_tdvf_sections(&mut f).unwrap();
for section in sections {
eprintln!("{section:x?}");

View File

@@ -2,25 +2,23 @@
authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"]
edition.workspace = true
name = "block"
rust-version.workspace = true
version = "0.1.0"
[features]
default = []
io_uring = ["dep:io-uring"]
test-utils = []
[dependencies]
bitflags = { workspace = true }
byteorder = { workspace = true }
crc-any = "3.0.0"
crc-any = "2.5.0"
flate2 = "1.1"
io-uring = { version = "0.7.12", optional = true }
io-uring = { version = "0.7.11", optional = true }
libc = { workspace = true }
log = { workspace = true }
remain = "0.2.15"
serde = { workspace = true, features = ["derive"] }
smallvec = { workspace = true }
smallvec = "1.15.1"
thiserror = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
virtio-bindings = { workspace = true }
@@ -32,11 +30,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]
cfg-if = { workspace = true }
[lints]
workspace = true

View File

@@ -1,269 +0,0 @@
// 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[..]);
}
}

View File

@@ -1,402 +0,0 @@
// 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

@@ -0,0 +1,90 @@
// 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 {}

148
block/src/async_io.rs Normal file
View File

@@ -0,0 +1,148 @@
// 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, DiskTopology};
#[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),
}
pub type DiskFileResult<T> = std::result::Result<T, DiskFileError>;
/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding [`DiskFile`].
///
/// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations
/// by some implementations of [`DiskFile`], 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
}
}
/// Abstraction over the effective [`File`] backing up a block device,
/// with support for synchronous and asynchronous I/O.
///
/// This allows abstracting over raw image formats as well as structured
/// image formats.
pub trait DiskFile: Send {
/// 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(&mut self) -> DiskFileResult<u64>;
/// Returns the physical size of the underlying file.
fn physical_size(&mut self) -> DiskFileResult<u64>;
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>>;
fn topology(&mut self) -> DiskTopology {
DiskTopology::default()
}
fn resize(&mut self, _size: u64) -> DiskFileResult<()> {
Err(DiskFileError::Unsupported)
}
/// Indicates support for sparse operations (punch hole, write zeroes, discard).
/// Override to return true when supported.
fn supports_sparse_operations(&self) -> bool {
false
}
/// Indicates support for zero flag optimization in WRITE_ZEROES. Override
/// to return true when supported.
fn supports_zero_flag(&self) -> bool {
false
}
/// Returns the file descriptor of the underlying disk image file.
///
/// The file descriptor is supposed to be used for `fcntl()` calls but no
/// other operation.
fn fd(&mut self) -> BorrowedDiskFd<'_>;
}
#[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(())
}
}

View File

@@ -1,158 +0,0 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Composable disk capability traits for the block crate.
//!
//! Small traits define individual capabilities:
//!
//! - [`DiskSize`] - reported capacity (logical size)
//! - [`PhysicalSize`] - host allocation size
//! - [`DiskFd`] - backing file descriptor access
//! - [`Geometry`] - sector/cluster geometry (default 512B)
//! - [`SparseCapable`] - sparse and zero flag support
//! - [`Resizable`] - online resize
//!
//! [`DiskFile`] is a supertrait that bundles the universal capabilities
//! (`DiskSize` + `Geometry`). [`FullDiskFile`] adds all optional
//! capabilities. [`AsyncDiskFile`] extends `DiskFile` with async I/O
//! construction for virtio queue workers. [`AsyncFullDiskFile`]
//! combines both axes.
//!
//! ```text
//! DiskFile: DiskSize + Geometry + Sync
//! / \
//! FullDiskFile: AsyncDiskFile:
//! DiskFile + PhysicalSize + DiskFile + Unpin
//! DiskFd + SparseCapable + try_clone, create_async_io
//! Resizable
//! \ /
//! AsyncFullDiskFile: FullDiskFile + AsyncDiskFile
//! ```
//!
//! Readonly accessors take `&self`. Only [`Resizable::resize`] requires
//! `&mut self`. Errors are returned as [`BlockResult`].
use std::fmt::Debug;
use crate::async_io::{AsyncIo, BorrowedDiskFd};
use crate::{BlockResult, DiskTopology};
/// Reported capacity of a disk image.
pub trait DiskSize: Send + Debug {
/// Virtual size of the disk image in bytes (reported capacity).
fn logical_size(&self) -> BlockResult<u64>;
}
/// Host allocation size of a file-backed disk image.
pub trait PhysicalSize: Send + Debug {
/// Actual bytes occupied on the host filesystem.
fn physical_size(&self) -> BlockResult<u64>;
}
/// Backing file descriptor access for disk images backed by a file.
pub trait DiskFd: Send + Debug {
/// Borrows the underlying file descriptor.
fn fd(&self) -> BorrowedDiskFd<'_>;
}
/// Sector and cluster geometry of a disk image.
///
/// Default returns `DiskTopology::default()` (512B logical/physical).
pub trait Geometry: Send + Debug {
/// Returns the disk topology.
fn topology(&self) -> DiskTopology {
DiskTopology::default()
}
}
/// Sparse and zero flag support for thin provisioned disk images.
pub trait SparseCapable: Send + Debug {
/// Indicates support for sparse operations (punch hole, write zeroes, discard).
fn supports_sparse_operations(&self) -> bool {
false
}
/// Indicates support for a metadata level zero flag optimization in
/// virtio `VIRTIO_BLK_T_WRITE_ZEROES` requests. When true, the format
/// can mark regions as reading zeros via a metadata bit rather than
/// writing actual zero bytes to disk.
fn supports_zero_flag(&self) -> bool {
false
}
}
/// Live disk resize support.
///
/// Implementations may return an error if the backend does not
/// support resizing (e.g. fixed size formats).
pub trait Resizable: Send + Debug {
/// Resizes the disk image to the given size in bytes, if the backend supports it.
fn resize(&mut self, size: u64) -> BlockResult<()>;
}
/// Supertrait bundling universal disk capabilities.
///
/// Every disk format implements `DiskSize` and `Geometry`.
/// `Sync` is required so that `Arc<dyn DiskFile>` can be shared
/// across threads for concurrent readonly access.
pub trait DiskFile: DiskSize + Geometry + Sync {}
/// Full capability disk file trait.
///
/// Bundles all optional capabilities on top of [`DiskFile`]:
/// file descriptor access, physical size, sparse operations, and resize.
/// Used by consumers that need feature negotiation without async I/O
/// (e.g. vhost user block).
pub trait FullDiskFile: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable {}
/// Blanket implementation: any type implementing all constituent traits
/// automatically satisfies [`FullDiskFile`].
impl<T: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable> FullDiskFile for T {}
/// Extended disk file trait for virtio queue workers.
///
/// Adds cloning and async I/O construction on top of [`DiskFile`].
/// `Unpin` is required so trait objects can be moved freely.
pub trait AsyncDiskFile: DiskFile + Unpin {
/// Creates an independent handle for a queue worker.
///
/// The clone shares internally reference counted state (e.g.
/// `Arc<Metadata>`) with the original, but owns its own file
/// descriptor and I/O completion resources. Each virtio queue
/// gets one clone so that workers can operate in parallel
/// without contending on I/O state.
///
/// Returns `Box<dyn AsyncDiskFile>` (not `AsyncFullDiskFile`)
/// because clones only serve as data plane handles for queue
/// workers. The original remains the control plane for feature
/// negotiation and configuration.
fn try_clone(&self) -> BlockResult<Box<dyn AsyncDiskFile>>;
/// Constructs a per queue async I/O engine.
///
/// # Arguments
///
/// * `ring_depth` - maximum number of in flight I/O operations.
/// Callers typically pass the virtio queue size. Must be greater
/// than zero. Backends that do not use an async ring (e.g. sync
/// fallback implementations) may ignore this value.
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>>;
}
/// Full capability async disk file trait.
///
/// Combines [`FullDiskFile`] (all optional capabilities) with
/// [`AsyncDiskFile`] (async I/O construction). This is the top level
/// trait for virtio block devices that need both feature negotiation
/// and async queue workers.
///
/// The type narrowing on [`AsyncDiskFile::try_clone`] is intentional:
/// clones only serve as data plane handles for queue workers, while
/// the original `AsyncFullDiskFile` handle remains the control plane
/// for feature negotiation and configuration.
pub trait AsyncFullDiskFile: FullDiskFile + AsyncDiskFile {}
/// Blanket implementation: any type implementing both [`FullDiskFile`]
/// and [`AsyncDiskFile`] automatically satisfies [`AsyncFullDiskFile`].
impl<T: FullDiskFile + AsyncDiskFile> AsyncFullDiskFile for T {}

View File

@@ -1,245 +0,0 @@
// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Unified error handling for the block crate.
//!
//! # Architecture
//!
//! ```text
//! BlockError -- single public error type
//! |-- BlockErrorKind -- small, stable, matchable classification
//! |-- ErrorContext -- optional diagnostic metadata (path, offset, op)
//! +-- source -- format-specific error (boxed)
//! |-- QcowError
//! |-- VhdError / RawError / ...
//! +-- io::Error / etc.
//! ```
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::path::PathBuf;
/// Small, stable classification of block errors.
///
/// Callers match on this for control flow. Adding new format specific
/// errors does not require new variants here.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum BlockErrorKind {
/// An underlying I/O operation failed.
Io,
/// The disk image format is structurally invalid.
InvalidFormat,
/// The disk image requires a feature that is not implemented.
UnsupportedFeature,
/// The image is marked or detected as corrupt.
CorruptImage,
/// An address, offset, or index is outside the valid range.
OutOfBounds,
/// A file or required internal structure could not be found.
NotFound,
/// An internal counter or limit was exceeded.
Overflow,
}
impl Display for BlockErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Io => write!(f, "I/O error"),
Self::InvalidFormat => write!(f, "Invalid format"),
Self::UnsupportedFeature => write!(f, "Unsupported feature"),
Self::CorruptImage => write!(f, "Corrupt image"),
Self::OutOfBounds => write!(f, "Out of bounds"),
Self::NotFound => write!(f, "Not found"),
Self::Overflow => write!(f, "Overflow"),
}
}
}
/// Classification of the operation that was in progress when an error occurred.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorOp {
/// Opening a disk image file.
Open,
/// Detecting the image format.
DetectImageType,
/// Duplicating a backing-file descriptor.
DupBackingFd,
/// Resizing a disk image.
Resize,
}
impl Display for ErrorOp {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Open => write!(f, "open"),
Self::DetectImageType => write!(f, "detect_image_type"),
Self::DupBackingFd => write!(f, "dup_backing_fd"),
Self::Resize => write!(f, "resize"),
}
}
}
/// Optional diagnostic context attached to a [`BlockError`].
#[derive(Debug, Default, Clone)]
pub struct ErrorContext {
pub path: Option<PathBuf>,
pub offset: Option<u64>,
pub op: Option<ErrorOp>,
}
impl Display for ErrorContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
if let Some(path) = &self.path {
write!(f, "path={}", path.display())?;
first = false;
}
if let Some(offset) = self.offset {
if !first {
write!(f, " ")?;
}
write!(f, "offset={offset:#x}")?;
first = false;
}
if let Some(op) = self.op {
if !first {
write!(f, " ")?;
}
write!(f, "op={op}")?;
}
Ok(())
}
}
/// Unified error type for the block crate.
///
/// Pairs a stable [`BlockErrorKind`] classification with an optional
/// boxed source error (format-specific) and optional [`ErrorContext`].
///
/// Display renders kind + context only; the underlying cause is
/// exposed via [`std::error::Error::source()`] for reporters that
/// walk the chain.
#[derive(Debug)]
pub struct BlockError {
kind: BlockErrorKind,
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
ctx: Option<ErrorContext>,
}
impl BlockError {
/// Create a new `BlockError` from a kind and a source error.
pub fn new<E>(kind: BlockErrorKind, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self {
kind,
source: Some(Box::new(source)),
ctx: None,
}
}
/// Create a `BlockError` from just a kind, with no underlying cause.
pub fn from_kind(kind: BlockErrorKind) -> Self {
Self {
kind,
source: None,
ctx: None,
}
}
/// Attach or replace the source error (builder-style).
pub fn with_source<E>(mut self, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
self.source = Some(Box::new(source));
self
}
/// Attach diagnostic context.
pub fn with_ctx(mut self, ctx: ErrorContext) -> Self {
self.ctx = Some(ctx);
self
}
/// Replace the error classification (builder-style).
pub fn with_kind(mut self, kind: BlockErrorKind) -> Self {
self.kind = kind;
self
}
/// Shorthand: attach an operation name.
pub fn with_op(mut self, op: ErrorOp) -> Self {
self.ctx.get_or_insert_with(ErrorContext::default).op = Some(op);
self
}
/// Shorthand: attach a file path.
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
self.ctx.get_or_insert_with(ErrorContext::default).path = Some(path.into());
self
}
/// Shorthand: attach a byte offset.
pub fn with_offset(mut self, offset: u64) -> Self {
self.ctx.get_or_insert_with(ErrorContext::default).offset = Some(offset);
self
}
/// The error classification.
pub fn kind(&self) -> BlockErrorKind {
self.kind
}
/// The diagnostic context, if any.
pub fn context(&self) -> Option<&ErrorContext> {
self.ctx.as_ref()
}
/// Access the underlying source error, if any.
pub fn source_ref(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
self.source.as_deref()
}
/// Try to downcast the source to a concrete type.
pub fn downcast_ref<T: StdError + 'static>(&self) -> Option<&T> {
self.source.as_ref()?.downcast_ref::<T>()
}
/// Consume the error and return the boxed source, if any.
pub fn into_source(self) -> Option<Box<dyn StdError + Send + Sync + 'static>> {
self.source
}
}
impl Display for BlockError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(ctx) = &self.ctx {
write!(f, " ({ctx})")?;
}
Ok(())
}
}
impl StdError for BlockError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn StdError + 'static))
}
}
/// Convenience: wrap an `io::Error` as `BlockErrorKind::Io`.
impl From<io::Error> for BlockError {
fn from(e: io::Error) -> Self {
Self::new(BlockErrorKind::Io, e)
}
}
pub type BlockResult<T> = Result<T, BlockError>;

View File

@@ -1,300 +0,0 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
//! Disk image factory.
//!
//! [`open_disk`] is the single entry point for opening a disk image.
//! It opens the file, detects the image format, probes async I/O
//! support, and constructs the appropriate backend. Callers receive
//! a trait object that is ready for use by virtio queue workers.
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::sync::OnceLock;
use std::{fmt, fs};
use log::info;
#[cfg(feature = "io_uring")]
use crate::block_io_uring_is_supported;
use crate::disk_file::AsyncFullDiskFile;
use crate::error::{BlockError, BlockErrorKind, BlockResult};
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,
};
/// Options for opening a disk image via [`open_disk`].
pub struct DiskOpenOptions<'a> {
pub path: &'a Path,
pub readonly: bool,
pub direct: bool,
pub sparse: bool,
pub backing_files: bool,
pub disable_io_uring: bool,
pub disable_aio: bool,
}
/// Result of [`open_disk`], carrying the detected image type alongside
/// the constructed backend.
pub struct OpenedDisk {
pub image_type: ImageType,
pub disk: Box<dyn AsyncFullDiskFile>,
}
impl fmt::Debug for OpenedDisk {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OpenedDisk")
.field("image_type", &self.image_type)
.finish_non_exhaustive()
}
}
/// Returns true when io_uring is supported on the running kernel.
///
/// The result is cached so the probe runs at most once per process.
#[cfg(feature = "io_uring")]
fn io_uring_supported() -> bool {
static SUPPORTED: OnceLock<bool> = OnceLock::new();
*SUPPORTED.get_or_init(block_io_uring_is_supported)
}
/// Returns true when Linux AIO is supported on the running kernel.
///
/// The result is cached so the probe runs at most once per process.
fn aio_supported() -> bool {
static SUPPORTED: OnceLock<bool> = OnceLock::new();
*SUPPORTED.get_or_init(block_aio_is_supported)
}
/// Open a disk image and construct the appropriate async backend.
///
/// - Opens the file with the requested access mode and flags.
/// - Detects the image format from the file header.
/// - Probes io_uring and Linux AIO support on the running kernel.
/// - Constructs the most capable backend available for the detected
/// format, preferring io_uring over AIO over synchronous fallback.
///
/// The returned [`OpenedDisk`] exposes the detected [`ImageType`] so
/// callers can perform post construction validation (e.g. type mismatch
/// checks, configuration warnings).
pub fn open_disk(options: &DiskOpenOptions<'_>) -> BlockResult<OpenedDisk> {
let mut fs_options = fs::OpenOptions::new();
fs_options.read(true);
fs_options.write(!options.readonly);
if options.direct {
fs_options.custom_flags(libc::O_DIRECT);
}
let mut file = open_disk_image(options.path, &fs_options)?;
let image_type = detect_image_type(&mut file)?;
let disk: Box<dyn AsyncFullDiskFile> = match image_type {
ImageType::FixedVhd => open_fixed_vhd(file, options)?,
ImageType::Raw => open_raw(file, options)?,
ImageType::Qcow2 => open_qcow2(file, options)?,
ImageType::Vhdx => open_vhdx(file, options)?,
ImageType::Unknown => {
return Err(
BlockError::from_kind(BlockErrorKind::UnsupportedFeature).with_path(options.path)
);
}
};
Ok(OpenedDisk { image_type, disk })
}
fn open_vhdx(
file: fs::File,
options: &DiskOpenOptions<'_>,
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
info!("Opening VHDX disk file with synchronous backend");
Ok(Box::new(
VhdxDisk::new(file, options.direct).map_err(|e| e.with_path(options.path))?,
))
}
fn open_fixed_vhd(
file: fs::File,
options: &DiskOpenOptions<'_>,
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
#[cfg(feature = "io_uring")]
if !options.disable_io_uring {
if io_uring_supported() {
info!("Opening fixed VHD disk file with io_uring backend");
return Ok(Box::new(
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");
}
info!("Opening fixed VHD disk file with synchronous backend");
Ok(Box::new(
VhdDisk::new(file, false, options.direct).map_err(|e| e.with_path(options.path))?,
))
}
fn open_raw(
file: fs::File,
options: &DiskOpenOptions<'_>,
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
if !options.readonly && !options.sparse {
preallocate_disk(&file, options.path);
}
#[cfg(feature = "io_uring")]
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,
options.direct,
)));
}
info!("io_uring runtime probe failed for RAW, trying next backend");
}
if !options.disable_aio {
if aio_supported() {
info!("Opening RAW disk file with AIO backend");
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,
options.direct,
)))
}
fn open_qcow2(
file: fs::File,
options: &DiskOpenOptions<'_>,
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
#[cfg(feature = "io_uring")]
if !options.disable_io_uring {
if io_uring_supported() {
info!("Opening QCOW2 disk file with io_uring backend");
return Ok(Box::new(
QcowDisk::new(
file,
options.direct,
options.backing_files,
options.sparse,
true,
)
.map_err(|e| e.with_path(options.path))?,
));
}
info!("io_uring runtime probe failed for QCOW2, using synchronous backend");
}
info!("Opening QCOW2 disk file with synchronous backend");
Ok(Box::new(
QcowDisk::new(
file,
options.direct,
options.backing_files,
options.sparse,
false,
)
.map_err(|e| e.with_path(options.path))?,
))
}
#[cfg(test)]
mod unit_tests {
use std::path::Path;
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::formats::qcow;
fn default_options(path: &Path) -> DiskOpenOptions<'_> {
DiskOpenOptions {
path,
readonly: false,
direct: false,
sparse: false,
backing_files: false,
disable_io_uring: true,
disable_aio: true,
}
}
#[test]
fn nonexistent_path_returns_error() {
let path = Path::new("/tmp/no_such_disk_image.raw");
let options = default_options(path);
match open_disk(&options) {
Err(e) => assert_eq!(e.kind(), BlockErrorKind::Io),
Ok(_) => panic!("expected error for nonexistent path"),
}
}
#[test]
fn detect_raw_image() {
let tmp = TempFile::new().unwrap();
tmp.as_file().set_len(1 << 20).unwrap();
let path = tmp.as_path().to_owned();
let options = default_options(&path);
let opened = open_disk(&options).unwrap();
assert_eq!(opened.image_type, ImageType::Raw);
}
#[test]
fn detect_qcow2_image() {
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();
assert_eq!(opened.image_type, ImageType::Qcow2);
}
#[test]
fn open_readonly() {
let tmp = TempFile::new().unwrap();
tmp.as_file().set_len(1 << 20).unwrap();
let path = tmp.as_path().to_owned();
let mut options = default_options(&path);
options.readonly = true;
let opened = open_disk(&options).unwrap();
assert_eq!(opened.image_type, ImageType::Raw);
}
#[test]
fn sync_fallback_when_async_disabled() {
let tmp = TempFile::new().unwrap();
let size = 1u64 << 20;
tmp.as_file().set_len(size).unwrap();
let path = tmp.as_path().to_owned();
let options = DiskOpenOptions {
path: &path,
readonly: false,
direct: false,
sparse: false,
backing_files: false,
disable_io_uring: true,
disable_aio: true,
};
let opened = open_disk(&options).unwrap();
assert_eq!(opened.image_type, ImageType::Raw);
assert_eq!(opened.disk.logical_size().unwrap(), size);
}
}

View File

@@ -16,7 +16,6 @@
use std::fmt::Debug;
use std::io;
use std::os::fd::{AsRawFd, RawFd};
use std::str::FromStr;
use thiserror::Error;
@@ -34,7 +33,7 @@ pub enum LockError {
}
/// Commands for use with [`fcntl`].
#[expect(non_camel_case_types)]
#[allow(non_camel_case_types)]
enum FcntlArg<'a> {
/// Set an OFD lock from the given lock description.
F_OFD_SETLK(&'a libc::flock),
@@ -141,37 +140,6 @@ impl LockGranularity {
}
}
/// User-facing choice for the lock granularity.
///
/// This allows external management software to create snapshots of the disk
/// image. Without a byte-range lock, some NFS implementations may treat the
/// entire file as exclusively locked and prevent such operations (e.g. NetApp).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum LockGranularityChoice {
/// Byte-range lock covering [0, size).
#[default]
ByteRange,
/// Whole-file lock (l_start=0, l_len=0) - original OFD whole-file lock behavior.
Full,
}
/// Error returned when parsing a [`LockGranularityChoice`] from a string.
#[derive(Error, Debug)]
#[error("Invalid lock granularity value: {0}, expected 'byte-range' or 'full'")]
pub struct LockGranularityParseError(String);
impl FromStr for LockGranularityChoice {
type Err = LockGranularityParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"byte-range" => Ok(LockGranularityChoice::ByteRange),
"full" => Ok(LockGranularityChoice::Full),
_ => Err(LockGranularityParseError(s.to_owned())),
}
}
}
/// Returns a [`struct@libc::flock`] structure for the whole file.
const fn get_flock(lock_type: LockType, granularity: LockGranularity) -> libc::flock {
libc::flock {
@@ -201,23 +169,20 @@ pub fn try_acquire_lock<Fd: AsRawFd>(
) -> Result<(), LockError> {
let flock = get_flock(lock_type, granularity);
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)),
}
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)),
}
val => panic!("Unexpected return value from fcntl(): {val}"),
}
val => panic!("Unexpected return value from fcntl(): {val}"),
}
}

99
block/src/fixed_vhd.rs Normal file
View File

@@ -0,0 +1,99 @@
// 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

@@ -0,0 +1,137 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::os::unix::io::{AsRawFd, RawFd};
use vmm_sys_util::eventfd::EventFd;
use crate::async_io::{
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
};
use crate::fixed_vhd::FixedVhd;
use crate::raw_async::RawFileAsync;
use crate::{BatchRequest, BlockBackend};
pub struct FixedVhdDiskAsync(FixedVhd);
impl FixedVhdDiskAsync {
pub fn new(file: File) -> std::io::Result<Self> {
Ok(Self(FixedVhd::new(file)?))
}
}
impl DiskFile for FixedVhdDiskAsync {
fn logical_size(&mut self) -> DiskFileResult<u64> {
Ok(self.0.logical_size().unwrap())
}
fn physical_size(&mut self) -> DiskFileResult<u64> {
Ok(self.0.physical_size().unwrap())
}
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
Ok(Box::new(
FixedVhdAsync::new(
self.0.as_raw_fd(),
ring_depth,
self.0.logical_size().unwrap(),
)
.map_err(DiskFileError::NewAsyncIo)?,
) as Box<dyn AsyncIo>)
}
fn fd(&mut self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.0.as_raw_fd())
}
}
pub struct FixedVhdAsync {
raw_file_async: RawFileAsync,
size: u64,
}
impl FixedVhdAsync {
pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> std::io::Result<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)
}
}

128
block/src/fixed_vhd_sync.rs Normal file
View File

@@ -0,0 +1,128 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::os::unix::io::{AsRawFd, RawFd};
use vmm_sys_util::eventfd::EventFd;
use crate::BlockBackend;
use crate::async_io::{
AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult,
};
use crate::fixed_vhd::FixedVhd;
use crate::raw_sync::RawFileSync;
pub struct FixedVhdDiskSync(FixedVhd);
impl FixedVhdDiskSync {
pub fn new(file: File) -> std::io::Result<Self> {
Ok(Self(FixedVhd::new(file)?))
}
}
impl DiskFile for FixedVhdDiskSync {
fn logical_size(&mut self) -> DiskFileResult<u64> {
Ok(self.0.logical_size().unwrap())
}
fn physical_size(&mut self) -> DiskFileResult<u64> {
self.0.physical_size().map_err(|e| {
let io_inner = match e {
crate::Error::GetFileMetadata(e) => e,
_ => unreachable!(),
};
DiskFileError::Size(io_inner)
})
}
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
Ok(Box::new(
FixedVhdSync::new(self.0.as_raw_fd(), self.0.logical_size().unwrap())
.map_err(DiskFileError::NewAsyncIo)?,
) as Box<dyn AsyncIo>)
}
fn fd(&mut self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.0.as_raw_fd())
}
}
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",
)))
}
}

View File

@@ -1,13 +0,0 @@
// 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

@@ -1,179 +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
//! Thread safe backing file readers for QCOW2 images.
use std::fs::File;
use std::io;
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::formats::qcow::common::decompress_cluster;
/// Raw backing file using position-independent reads on a duplicated fd.
pub(crate) struct RawBacking {
pub(crate) file: File,
pub(crate) virtual_size: u64,
}
// 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 {}
impl BackingRead for RawBacking {
fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
if address >= self.virtual_size {
buf.fill(0);
return Ok(());
}
let available = (self.virtual_size - address) as usize;
if available >= buf.len() {
self.file.read_exact_at(buf, address)
} else {
self.file.read_exact_at(&mut buf[..available], address)?;
buf[available..].fill(0);
Ok(())
}
}
}
/// QCOW2 image used as a backing file for another QCOW2 image.
///
/// Resolves guest offsets through the QCOW2 cluster mapping (L1/L2
/// tables, refcounts) before reading the underlying data. Read only
/// because backing files never receive writes. Nested backing chains
/// are handled recursively via the optional `backing_file` field.
pub(crate) struct Qcow2Backing {
pub(crate) metadata: Arc<QcowMetadata>,
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 read_exact_at which is position independent and thread safe.
unsafe impl Sync for Qcow2Backing {}
impl BackingRead for Qcow2Backing {
fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
let virtual_size = self.metadata.virtual_size();
if address >= virtual_size {
buf.fill(0);
return Ok(());
}
let available = (virtual_size - address) as usize;
if available < buf.len() {
self.read_clusters(address, &mut buf[..available])?;
buf[available..].fill(0);
return Ok(());
}
self.read_clusters(address, buf)
}
}
impl Qcow2Backing {
fn read_clusters(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
let total_len = buf.len();
let has_backing = self.backing_file.is_some();
let mappings = self
.metadata
.map_clusters_for_read(address, total_len, has_backing)?;
let mut buf_offset = 0usize;
for mapping in mappings {
match mapping {
ClusterReadMapping::Zero { length } => {
buf[buf_offset..buf_offset + length as usize].fill(0);
buf_offset += length as usize;
}
ClusterReadMapping::Allocated {
offset: host_offset,
length,
} => {
self.data_file.read_exact_at(
&mut buf[buf_offset..buf_offset + length as usize],
host_offset,
)?;
buf_offset += length as usize;
}
ClusterReadMapping::Compressed {
host_offset,
compressed_size,
cluster_offset,
length,
} => {
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,
&*self.decoder,
)?;
buf[buf_offset..buf_offset + length]
.copy_from_slice(&decompressed[cluster_offset..cluster_offset + length]);
buf_offset += length;
}
ClusterReadMapping::Backing {
offset: backing_offset,
length,
} => {
self.backing_file.as_ref().unwrap().read_at(
backing_offset,
&mut buf[buf_offset..buf_offset + length as usize],
)?;
buf_offset += length as usize;
}
}
}
Ok(())
}
}
impl Drop for Qcow2Backing {
fn drop(&mut self) {
self.metadata.shutdown();
}
}
/// Construct a thread safe backing file reader.
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> {
fd.try_clone_to_owned().map_err(|e| {
BlockError::new(
BlockErrorKind::Io,
QcowError::BackingFileIo(String::new(), e),
)
.with_op(ErrorOp::DupBackingFd)
})
};
match kind {
BackingKind::Raw(raw_file) => {
let file = File::from(dup_fd(raw_file.as_fd())?);
Ok(Arc::new(RawBacking { file, virtual_size }))
}
BackingKind::Qcow { inner, backing } => {
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_file,
backing_file: backing.map(|bf| shared_backing_from(*bf)).transpose()?,
}))
}
}
}

View File

@@ -1,161 +0,0 @@
// 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));
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,700 +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.
//
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! QCOW2 header parsing, validation, and creation.
use std::fmt::{Display, Formatter, Result as FmtResult};
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::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)]
pub enum ImageType {
Raw,
Qcow2,
}
impl Display for ImageType {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
ImageType::Raw => write!(f, "raw"),
ImageType::Qcow2 => write!(f, "qcow2"),
}
}
}
impl FromStr for ImageType {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"raw" => Ok(ImageType::Raw),
"qcow2" => Ok(ImageType::Qcow2),
_ => Err(Error::UnsupportedBackingFileFormat(s.to_string())),
}
}
}
#[derive(Clone, Debug)]
pub enum CompressionType {
Zlib,
Zstd,
}
#[derive(Debug, Clone)]
pub struct BackingFileConfig {
pub path: String,
// If this is None, we will autodetect it.
pub format: Option<ImageType>,
}
// Maximum data size supported.
pub(super) const MAX_QCOW_FILE_SIZE: u64 = 0x01 << 44; // 16 TB.
// QCOW magic constant that starts the header.
pub(super) const QCOW_MAGIC: u32 = 0x5146_49fb;
// Default to a cluster size of 2^DEFAULT_CLUSTER_BITS
pub(super) const DEFAULT_CLUSTER_BITS: u32 = 16;
// Limit clusters to reasonable sizes. Choose the same limits as qemu. Making the clusters smaller
// increases the amount of overhead for book keeping.
pub(super) const MIN_CLUSTER_BITS: u32 = 9;
pub(super) const MAX_CLUSTER_BITS: u32 = 21;
// The L1 and RefCount table are kept in RAM, only handle files that require less than 35M entries.
// This easily covers 1 TB files. When support for bigger files is needed the assumptions made to
// keep these tables in RAM needs to be thrown out.
pub(super) const MAX_RAM_POINTER_TABLE_SIZE: u64 = 35_000_000;
// 16-bit refcounts.
pub(super) const DEFAULT_REFCOUNT_ORDER: u32 = 4;
pub(super) const V2_BARE_HEADER_SIZE: u32 = 72;
pub(super) const V3_BARE_HEADER_SIZE: u32 = 104;
pub(super) const AUTOCLEAR_FEATURES_OFFSET: u64 = 88;
pub(super) const COMPATIBLE_FEATURES_LAZY_REFCOUNTS: u64 = 1;
// Compression types as defined in https://www.qemu.org/docs/master/interop/qcow2.html
const COMPRESSION_TYPE_ZLIB: u64 = 0; // zlib/deflate <https://www.ietf.org/rfc/rfc1951.txt>
const COMPRESSION_TYPE_ZSTD: u64 = 1; // zstd <http://github.com/facebook/zstd>
// Header extension types
pub(super) const HEADER_EXT_END: u32 = 0x00000000;
// Backing file format name (raw, qcow2)
pub(super) const HEADER_EXT_BACKING_FORMAT: u32 = 0xe2792aca;
// Feature name table
const HEADER_EXT_FEATURE_NAME_TABLE: u32 = 0x6803f857;
// Feature name table entry type incompatible
const FEAT_TYPE_INCOMPATIBLE: u8 = 0;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IncompatFeatures: u64 {
const DIRTY = 1 << 0;
const CORRUPT = 1 << 1;
const DATA_FILE = 1 << 2;
const COMPRESSION = 1 << 3;
const EXTENDED_L2 = 1 << 4;
}
}
impl IncompatFeatures {
/// Features supported by this implementation.
pub(super) const SUPPORTED: IncompatFeatures = IncompatFeatures::DIRTY
.union(IncompatFeatures::CORRUPT)
.union(IncompatFeatures::COMPRESSION);
/// Get the fallback name for a known feature bit.
fn flag_name(bit: u8) -> Option<&'static str> {
Some(match Self::from_bits_truncate(1u64 << bit) {
Self::DIRTY => "dirty bit",
Self::CORRUPT => "corrupt bit",
Self::DATA_FILE => "external data file",
Self::EXTENDED_L2 => "extended L2 entries",
_ => return None,
})
}
}
/// Error type for unsupported incompatible features.
#[derive(Debug, Clone, thiserror::Error)]
pub struct MissingFeatureError {
/// Unsupported feature bits.
features: IncompatFeatures,
/// Feature name table from the qcow2 image.
feature_names: Vec<(u8, String)>,
}
impl MissingFeatureError {
pub(super) fn new(features: IncompatFeatures, feature_names: Vec<(u8, String)>) -> Self {
Self {
features,
feature_names,
}
}
}
impl Display for MissingFeatureError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let names: Vec<String> = (0u8..64)
.filter(|&bit| self.features.bits() & (1u64 << bit) != 0)
.map(|bit| {
// First try the image's feature name table
self.feature_names
.iter()
.find(|(b, _)| *b == bit)
.map(|(_, name)| name.clone())
// Then try hardcoded fallback names
.or_else(|| IncompatFeatures::flag_name(bit).map(|s| s.to_string()))
// Finally, use generic description
.unwrap_or_else(|| format!("unknown feature bit {bit}"))
})
.collect();
write!(f, "Missing features: {}", names.join(", "))
}
}
// The format supports a "header extension area", that crosvm does not use.
const QCOW_EMPTY_HEADER_EXTENSION_SIZE: u32 = 8;
// Defined by the specification
const MAX_BACKING_FILE_SIZE: u32 = 1023;
/// Contains the information from the header of a qcow file.
#[derive(Clone, Debug)]
pub struct QcowHeader {
pub magic: u32,
pub version: u32,
pub backing_file_offset: u64,
pub backing_file_size: u32,
pub cluster_bits: u32,
pub size: u64,
pub crypt_method: u32,
pub l1_size: u32,
pub l1_table_offset: u64,
pub refcount_table_offset: u64,
pub refcount_table_clusters: u32,
pub nb_snapshots: u32,
pub snapshots_offset: u64,
// v3 entries
pub incompatible_features: u64,
pub compatible_features: u64,
pub autoclear_features: u64,
pub refcount_order: u32,
pub header_size: u32,
pub compression_type: CompressionType,
// Post-header entries
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: &AlignedFile,
header: &mut QcowHeader,
mut feature_table: Option<&mut Vec<(u8, String)>>,
) -> Result<()> {
// Extensions start directly after the header.
let mut offset = header.header_size as u64;
loop {
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 = extension.length.get();
match ext_type {
HEADER_EXT_BACKING_FORMAT => {
let mut format_bytes = vec![0u8; ext_length as usize];
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 {
backing_file.format = Some(format_str.parse()?);
}
}
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_at(&mut data, offset)
.map_err(Error::ReadingHeader)?;
offset += data.len() as u64;
let table = feature_table.as_mut().unwrap();
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..];
let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(46);
let name = String::from_utf8_lossy(&name_bytes[..name_len]).to_string();
table.push((bit_number, name));
}
}
}
_ => {
// Skip unknown extension
offset += ext_length as u64;
}
}
// Skip to the next 8 byte boundary
let padding = (8 - (ext_length % 8)) % 8;
offset += padding as u64;
}
Ok(())
}
/// Creates a QcowHeader from a reference to a file.
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);
}
let version = v2.version.get();
let mut header = QcowHeader {
magic,
version,
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 != 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 {
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_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()))?;
header.backing_file = Some(BackingFileConfig { path, format: None });
}
if version == 3 {
// Check for unsupported incompatible features first
let features = IncompatFeatures::from_bits_retain(header.incompatible_features);
let unsupported = features - IncompatFeatures::SUPPORTED;
if !unsupported.is_empty() {
// Read extensions only to get feature names for error reporting
let mut feature_table = Vec::new();
if header.header_size > V3_BARE_HEADER_SIZE {
let _ = Self::read_header_extensions(f, &mut header, Some(&mut feature_table));
}
return Err(Error::UnsupportedFeature(MissingFeatureError::new(
unsupported,
feature_table,
)));
}
// Features OK, now read extensions normally
if header.header_size > V3_BARE_HEADER_SIZE {
Self::read_header_extensions(f, &mut header, None)?;
}
}
Ok(header)
}
pub fn get_decoder(&self) -> Box<dyn Decoder> {
match self.compression_type {
CompressionType::Zlib => Box::new(ZlibDecoder {}),
CompressionType::Zstd => Box::new(ZstdDecoder {}),
}
}
pub fn create_for_size_and_path(
version: u32,
size: u64,
backing_file: Option<&str>,
) -> Result<QcowHeader> {
let header_size = if version == 2 {
V2_BARE_HEADER_SIZE
} else {
V3_BARE_HEADER_SIZE + QCOW_EMPTY_HEADER_EXTENSION_SIZE
};
let cluster_bits: u32 = DEFAULT_CLUSTER_BITS;
let cluster_size: u32 = 0x01 << cluster_bits;
let max_length: usize = (cluster_size - header_size) as usize;
if let Some(path) = backing_file
&& path.len() > max_length
{
return Err(Error::BackingFileTooLong(path.len() - max_length));
}
// L2 blocks are always one cluster long. They contain cluster_size/sizeof(u64) addresses.
let entries_per_cluster: u32 = cluster_size / size_of::<u64>() as u32;
let num_clusters: u32 = div_round_up_u64(size, u64::from(cluster_size)) as u32;
let num_l2_clusters: u32 = div_round_up_u32(num_clusters, entries_per_cluster);
let l1_clusters: u32 = div_round_up_u32(num_l2_clusters, entries_per_cluster);
let header_clusters = div_round_up_u32(size_of::<QcowHeader>() as u32, cluster_size);
Ok(QcowHeader {
magic: QCOW_MAGIC,
version,
backing_file_offset: backing_file.map_or(0, |_| {
header_size
+ if version == 3 {
QCOW_EMPTY_HEADER_EXTENSION_SIZE
} else {
0
}
}) as u64,
backing_file_size: backing_file.map_or(0, |x| x.len()) as u32,
cluster_bits: DEFAULT_CLUSTER_BITS,
size,
crypt_method: 0,
l1_size: num_l2_clusters,
l1_table_offset: u64::from(cluster_size),
// The refcount table is after l1 + header.
refcount_table_offset: u64::from(cluster_size * (l1_clusters + 1)),
refcount_table_clusters: {
// Pre-allocate enough clusters for the entire refcount table as it must be
// continuous in the file. Allocate enough space to refcount all clusters, including
// the refcount clusters.
let max_refcount_clusters = max_refcount_clusters(
DEFAULT_REFCOUNT_ORDER,
cluster_size,
num_clusters + l1_clusters + num_l2_clusters + header_clusters,
) as u32;
// The refcount table needs to store the offset of each refcount cluster.
div_round_up_u32(
max_refcount_clusters * size_of::<u64>() as u32,
cluster_size,
)
},
nb_snapshots: 0,
snapshots_offset: 0,
incompatible_features: 0,
compatible_features: 0,
autoclear_features: 0,
refcount_order: DEFAULT_REFCOUNT_ORDER,
header_size,
compression_type: CompressionType::Zlib,
backing_file: backing_file.map(|path| BackingFileConfig {
path: String::from(path),
format: None,
}),
})
}
/// 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 {
let v3 = RawHeaderV3Tail::from_header(self);
buf.extend_from_slice(v3.as_bytes());
if self.header_size > V3_BARE_HEADER_SIZE {
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());
}
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) {
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 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;
f.write_all_at(
&[0u8],
self.refcount_table_offset + refcount_blocks_size - 2,
)
.map_err(Error::WritingHeader)?;
Ok(())
}
/// Write only the incompatible_features field to the file at its fixed offset.
fn write_incompatible_features(&self, file: &AlignedFile) -> BlockResult<()> {
if self.version != 3 {
return Ok(());
}
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(())
}
/// Set or clear the dirty bit for QCOW2 v3 images.
///
/// 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(&mut self, file: &mut AlignedFile, dirty: bool) -> BlockResult<()> {
if self.version == 3 {
if dirty {
self.incompatible_features |= IncompatFeatures::DIRTY.bits();
} else {
self.incompatible_features &= !IncompatFeatures::DIRTY.bits();
}
self.write_incompatible_features(file)?;
file.fsync()
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
}
Ok(())
}
/// Set the corrupt bit for QCOW2 v3 images.
///
/// This marks the image as corrupted. Once set, the image can only be
/// opened read-only until repaired.
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)?;
file.fsync()
.map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?;
}
Ok(())
}
pub fn is_corrupt(&self) -> bool {
IncompatFeatures::from_bits_truncate(self.incompatible_features)
.contains(IncompatFeatures::CORRUPT)
}
/// Clear all autoclear feature bits for QCOW2 v3 images.
///
/// These bits indicate features that can be safely disabled when modified
/// by software that doesn't understand them.
pub fn clear_autoclear_features(&mut self, file: &mut AlignedFile) -> Result<()> {
if self.version == 3 && self.autoclear_features != 0 {
self.autoclear_features = 0;
file.write_all_at(&0u64.to_be_bytes(), AUTOCLEAR_FEATURES_OFFSET)
.map_err(Error::WritingHeader)?;
file.fsync().map_err(Error::SyncingHeader)?;
}
Ok(())
}
}
pub(super) fn max_refcount_clusters(
refcount_order: u32,
cluster_size: u32,
num_clusters: u32,
) -> u64 {
// Use u64 as the product of the u32 inputs can overflow.
let refcount_bits = 0x01u64 << u64::from(refcount_order);
let cluster_bits = u64::from(cluster_size) * 8;
let for_data = div_round_up_u64(u64::from(num_clusters) * refcount_bits, cluster_bits);
let for_refcounts = div_round_up_u64(for_data * refcount_bits, cluster_bits);
for_data + for_refcounts
}
/// Returns an Error if the given offset doesn't align to a cluster boundary.
pub(super) fn offset_is_cluster_boundary(offset: u64, cluster_bits: u32) -> Result<()> {
if offset & ((0x01 << cluster_bits) - 1) != 0 {
return Err(Error::InvalidOffset(offset));
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,413 +0,0 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// 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};
/// Unified DiskFile wrapper for QCOW2 disk images.
///
/// Holds the in memory QCOW2 metadata, the data file, and an optional
/// backing file. The metadata is wrapped in an `Arc` because
/// [`QcowSync`] and [`QcowAsync`] I/O workers receive a clone when
/// they are created via [`create_async_io`](DiskFile::create_async_io).
/// The backing file is likewise shared with workers through an `Arc`.
///
/// The `sparse` flag controls whether the image advertises discard
/// support to the guest. The `use_io_uring` flag selects between the
/// [`QcowSync`] and [`QcowAsync`] I/O backends. Both are recorded at
/// construction time and propagated through [`try_clone`](DiskFile::try_clone).
pub struct QcowDisk {
metadata: Arc<QcowMetadata>,
backing_file: Option<Arc<dyn BackingRead>>,
sparse: bool,
data_raw_file: QcowRawFile,
use_io_uring: bool,
}
impl fmt::Debug for QcowDisk {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("QcowDisk")
.field("sparse", &self.sparse)
.field("has_backing", &self.backing_file.is_some())
.field("use_io_uring", &self.use_io_uring)
.finish_non_exhaustive()
}
}
impl QcowDisk {
pub fn new(
file: File,
direct_io: bool,
backing_files: bool,
sparse: bool,
use_io_uring: bool,
) -> BlockResult<Self> {
#[cfg(not(feature = "io_uring"))]
if use_io_uring {
return Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
DiskFileError::NewAsyncIo(io::Error::other(
"io_uring requested but feature is not enabled",
)),
));
}
let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 };
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) {
e.with_kind(BlockErrorKind::UnsupportedFeature)
} else {
e
};
e.with_op(ErrorOp::Open)
})?;
let data_raw_file = inner.raw_file.clone();
Ok(QcowDisk {
metadata: Arc::new(QcowMetadata::new(inner)),
backing_file: backing_file.map(shared_backing_from).transpose()?,
sparse,
data_raw_file,
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 {
fn drop(&mut self) {
self.metadata.shutdown();
}
}
impl disk_file::DiskSize for QcowDisk {
fn logical_size(&self) -> BlockResult<u64> {
Ok(self.metadata.virtual_size())
}
}
impl disk_file::PhysicalSize for QcowDisk {
fn physical_size(&self) -> BlockResult<u64> {
Ok(self.data_raw_file.physical_size()?)
}
}
impl disk_file::DiskFd for QcowDisk {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.data_raw_file.as_raw_fd())
}
}
impl disk_file::Geometry for QcowDisk {}
impl disk_file::SparseCapable for QcowDisk {
fn supports_sparse_operations(&self) -> bool {
true
}
fn supports_zero_flag(&self) -> bool {
true
}
}
impl disk_file::Resizable for QcowDisk {
fn resize(&mut self, size: u64) -> BlockResult<()> {
if self.backing_file.is_some() {
return Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
DiskFileError::ResizeError(io::Error::other(
"resize not supported with backing files",
)),
)
.with_op(ErrorOp::Resize));
}
self.metadata.resize(size).map_err(|e| {
BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e))
.with_op(ErrorOp::Resize)
})
}
}
impl disk_file::DiskFile for QcowDisk {}
impl disk_file::AsyncDiskFile for QcowDisk {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
Ok(Box::new(QcowDisk {
metadata: Arc::clone(&self.metadata),
backing_file: self.backing_file.as_ref().map(Arc::clone),
sparse: self.sparse,
data_raw_file: self.data_raw_file.clone(),
use_io_uring: self.use_io_uring,
}))
}
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
if self.use_io_uring {
#[cfg(feature = "io_uring")]
{
return Ok(Box::new(
QcowAsync::new(
Arc::clone(&self.metadata),
self.data_raw_file.clone(),
self.backing_file.as_ref().map(Arc::clone),
self.sparse,
ring_depth,
)
.map_err(|e| {
BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e))
})?,
));
}
#[cfg(not(feature = "io_uring"))]
unreachable!("use_io_uring is set but io_uring feature is not enabled");
}
let _ = ring_depth;
Ok(Box::new(QcowSync::new(
Arc::clone(&self.metadata),
self.data_raw_file.clone(),
self.backing_file.as_ref().map(Arc::clone),
self.sparse,
)))
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
use crate::async_io::AsyncIo;
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize};
const TEST_SIZE: u64 = 0x5566_7788;
fn make_qcow_file() -> File {
QcowTempDisk::new(TEST_SIZE, None, false, true, false)
.unwrap()
.into_tempfile()
.into_file()
}
#[test]
fn new_sync_returns_correct_size() {
let file = make_qcow_file();
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
assert_eq!(disk.logical_size().unwrap(), TEST_SIZE);
}
fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_batch: bool) {
let io: Box<dyn AsyncIo> = disk.create_async_io(128).unwrap();
assert_eq!(io.batch_requests_enabled(), expect_batch);
}
fn assert_async_io(disk: &QcowDisk, expect_batch: bool) {
assert_async_io_from_dyn(disk, expect_batch);
}
#[test]
fn sync_backend_disables_batch_requests() {
let file = make_qcow_file();
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
assert_async_io(&disk, false);
}
#[cfg(feature = "io_uring")]
#[test]
fn io_uring_backend_enables_batch_requests() {
let file = make_qcow_file();
let disk = QcowDisk::new(file, false, false, true, true).unwrap();
assert_async_io(&disk, true);
}
#[test]
fn try_clone_preserves_sync_dispatch() {
let file = make_qcow_file();
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), false);
}
#[cfg(feature = "io_uring")]
#[test]
fn try_clone_preserves_io_uring_dispatch() {
let file = make_qcow_file();
let disk = QcowDisk::new(file, false, false, true, true).unwrap();
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), true);
}
#[test]
fn physical_size_less_than_logical() {
// make_qcow_file() writes no guest data, so the file on disk
// only contains QCOW2 headers and metadata tables.
let file = make_qcow_file();
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,557 +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, 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

@@ -1,84 +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.
//
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
//! Pure helper functions and constants for QCOW2 L1/L2 table entry
//! manipulation and integer arithmetic. Shared across the `qcow` submodules.
/// Nesting depth limit for disk formats that can open other disk files.
pub(crate) const MAX_NESTING_DEPTH: u32 = 10;
// bits 0-8 and 56-63 are reserved.
pub(super) const L1_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00;
pub(super) const L2_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00;
// Flags
pub(super) const ZERO_FLAG: u64 = 1 << 0;
pub(super) const COMPRESSED_FLAG: u64 = 1 << 62;
pub(super) const COMPRESSED_SECTOR_SIZE: u64 = 512;
pub(super) const CLUSTER_USED_FLAG: u64 = 1 << 63;
/// Check if L2 entry is empty (unallocated).
pub(super) fn l2_entry_is_empty(l2_entry: u64) -> bool {
l2_entry == 0
}
/// Check bit 0 - only valid for standard clusters.
pub(super) fn l2_entry_is_zero(l2_entry: u64) -> bool {
l2_entry & ZERO_FLAG != 0
}
/// Check if L2 entry refers to a compressed cluster.
pub(super) fn l2_entry_is_compressed(l2_entry: u64) -> bool {
l2_entry & COMPRESSED_FLAG != 0
}
/// Get file offset and size of compressed cluster data.
pub(super) fn l2_entry_compressed_cluster_layout(l2_entry: u64, cluster_bits: u32) -> (u64, usize) {
let compressed_size_shift = 62 - (cluster_bits - 8);
let compressed_size_mask = (1 << (cluster_bits - 8)) - 1;
let compressed_cluster_addr = l2_entry & ((1 << compressed_size_shift) - 1);
let nsectors = (l2_entry >> compressed_size_shift & compressed_size_mask) + 1;
let compressed_cluster_size = ((nsectors * COMPRESSED_SECTOR_SIZE)
- (compressed_cluster_addr & (COMPRESSED_SECTOR_SIZE - 1)))
as usize;
(compressed_cluster_addr, compressed_cluster_size)
}
/// Get file offset of standard (non-compressed) cluster.
pub(super) fn l2_entry_std_cluster_addr(l2_entry: u64) -> u64 {
l2_entry & L2_TABLE_OFFSET_MASK
}
/// Make L2 entry for standard (non-compressed) cluster.
pub(super) fn l2_entry_make_std(cluster_addr: u64) -> u64 {
(cluster_addr & L2_TABLE_OFFSET_MASK) | CLUSTER_USED_FLAG
}
/// Make L2 entry for preallocated zero cluster.
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)
}
/// Ceiling of the division of `dividend`/`divisor`.
pub(super) fn div_round_up_u32(dividend: u32, divisor: u32) -> u32 {
dividend / divisor + u32::from(!dividend.is_multiple_of(divisor))
}
/// Ceiling of the division of `dividend`/`divisor`.
pub(super) fn div_round_up_u64(dividend: u64, divisor: u64) -> u64 {
dividend / divisor + u64::from(!dividend.is_multiple_of(divisor))
}

View File

@@ -1,159 +0,0 @@
// 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

@@ -1,181 +0,0 @@
// 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

@@ -1,162 +0,0 @@
// 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

@@ -1,319 +0,0 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
//
// 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;
use std::os::unix::io::AsRawFd;
use log::warn;
use self::engine_aio::RawAio;
use self::engine_sync::RawSync;
#[cfg(feature = "io_uring")]
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)]
pub enum RawBackend {
/// Blocking I/O where the caller waits for completion.
Sync,
/// Modern asynchronous I/O using shared submission and completion
/// rings for lower overhead operation dispatch and completion handling.
#[cfg(feature = "io_uring")]
IoUring,
/// Legacy asynchronous I/O where requests are handed to the kernel
/// and completions are collected later.
Aio,
}
/// Unified DiskFile wrapper for raw disk images.
///
/// Owns the underlying file and delegates async I/O creation to the
/// backend selected at construction time via [`RawBackend`].
#[derive(Debug)]
pub struct RawDisk {
file: File,
backend: RawBackend,
direct: bool,
}
impl RawDisk {
pub fn new(file: File, backend: RawBackend, direct: bool) -> Self {
Self {
file,
backend,
direct,
}
}
}
impl disk_file::DiskSize for RawDisk {
fn logical_size(&self) -> BlockResult<u64> {
query_device_size(&self.file)
.map(|(logical_size, _)| logical_size)
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e)))
}
}
impl disk_file::PhysicalSize for RawDisk {
fn physical_size(&self) -> BlockResult<u64> {
query_device_size(&self.file)
.map(|(_, physical_size)| physical_size)
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e)))
}
}
impl disk_file::DiskFd for RawDisk {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.file.as_raw_fd())
}
}
impl disk_file::Geometry for RawDisk {
fn topology(&self) -> DiskTopology {
DiskTopology::probe(&self.file).unwrap_or_else(|_| {
warn!("Unable to get device topology. Using default topology");
DiskTopology::default()
})
}
}
impl disk_file::SparseCapable for RawDisk {
fn supports_sparse_operations(&self) -> bool {
probe_sparse_support(&self.file)
}
}
impl disk_file::Resizable for RawDisk {
fn resize(&mut self, size: u64) -> BlockResult<()> {
let fd_metadata = self
.file
.metadata()
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?;
if fd_metadata.file_type().is_block_device() {
// Block devices cannot be resized via ftruncate; they are resized
// externally (LVM, losetup, etc.). Verify the size matches.
let (actual_size, _) = query_device_size(&self.file)
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?;
if actual_size != size {
return Err(BlockError::new(
BlockErrorKind::Io,
DiskFileError::ResizeError(io::Error::other(format!(
"Block device size {actual_size} does not match requested size {size}"
))),
));
}
Ok(())
} else {
self.file
.set_len(size)
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))
}
}
}
impl disk_file::DiskFile for RawDisk {}
impl disk_file::AsyncDiskFile for RawDisk {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
let file = self
.file
.try_clone()
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?;
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(RawSync::new(raw_file))),
#[cfg(feature = "io_uring")]
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;
use vmm_sys_util::tempfile::TempFile;
use super::*;
use crate::async_io::AsyncIo;
use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize, Resizable};
const TEST_SIZE: u64 = 0x1122_3344;
fn make_raw_file() -> File {
let file: File = TempFile::new().unwrap().into_file();
file.set_len(TEST_SIZE).unwrap();
file
}
#[test]
fn new_sync_returns_correct_size() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::Sync, false);
assert_eq!(disk.logical_size().unwrap(), TEST_SIZE);
}
fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_backend: RawBackend) {
let io: Box<dyn AsyncIo> = disk.create_async_io(128).unwrap();
cfg_if::cfg_if! {
if #[cfg(feature = "io_uring")] {
let expected_batch_requests = expect_backend == RawBackend::IoUring;
} else {
let _ = expect_backend;
let expected_batch_requests = false;
}
}
assert_eq!(io.batch_requests_enabled(), expected_batch_requests);
}
fn assert_sync_backend(disk: &RawDisk) {
assert_eq!(disk.backend, RawBackend::Sync);
assert_async_io_from_dyn(disk, RawBackend::Sync);
}
fn assert_aio_backend(disk: &RawDisk) {
assert_eq!(disk.backend, RawBackend::Aio);
assert_async_io_from_dyn(disk, RawBackend::Aio);
}
#[cfg(feature = "io_uring")]
fn assert_io_uring_backend(disk: &RawDisk) {
assert_eq!(disk.backend, RawBackend::IoUring);
assert_async_io_from_dyn(disk, RawBackend::IoUring);
}
#[test]
fn sync_backend_disables_batch_requests() {
let file = make_raw_file();
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, false);
assert_aio_backend(&disk);
}
#[cfg(feature = "io_uring")]
#[test]
fn io_uring_backend_enables_batch_requests() {
let file = make_raw_file();
let disk = RawDisk::new(file, RawBackend::IoUring, false);
assert_io_uring_backend(&disk);
}
fn assert_try_clone(disk: &RawDisk, expect_backend: RawBackend) {
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), expect_backend);
}
#[test]
fn try_clone_preserves_sync_backend() {
let file = make_raw_file();
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, false);
assert_try_clone(&disk, RawBackend::Aio);
}
#[cfg(feature = "io_uring")]
#[test]
fn try_clone_preserves_io_uring_backend() {
let file = make_raw_file();
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, false);
let new_size = TEST_SIZE * 2;
disk.resize(new_size).unwrap();
assert_eq!(disk.logical_size().unwrap(), new_size);
}
#[test]
fn physical_size_reports_allocated_blocks() {
let file = make_raw_file();
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,169 +0,0 @@
// 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.
//!
//! Each helper takes a `&mut dyn AsyncIo` together with the [`File`] handle
//! that backs the I/O object, so the same logic exercises every backend with
//! only the constructor differing.
use std::fs::File;
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) {
// Write 4MB of data
let data = vec![0xAA; 4 * 1024 * 1024];
file.write_all(&data).unwrap();
file.sync_all().unwrap();
// Punch hole in the middle (1MB at offset 1MB)
let offset = 1024 * 1024;
let length = 1024 * 1024;
async_io.punch_hole(offset, length, 1).unwrap();
// Check completion
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 1);
assert_eq!(result, 0);
// Verify the hole reads as zeros
file.seek(SeekFrom::Start(offset)).unwrap();
let mut read_buf = vec![0; length as usize];
file.read_exact(&mut read_buf).unwrap();
assert!(
read_buf.iter().all(|&b| b == 0),
"Punched hole should read as zeros"
);
// Verify data before hole is intact
file.seek(SeekFrom::Start(0)).unwrap();
let mut read_buf = vec![0; 1024];
file.read_exact(&mut read_buf).unwrap();
assert!(
read_buf.iter().all(|&b| b == 0xAA),
"Data before hole should be intact"
);
// Verify data after hole is intact
file.seek(SeekFrom::Start(offset + length)).unwrap();
let mut read_buf = vec![0; 1024];
file.read_exact(&mut read_buf).unwrap();
assert!(
read_buf.iter().all(|&b| b == 0xAA),
"Data after hole should be intact"
);
}
/// Tests writing zeroes to a 512 KB region inside a 4 MB file and verifying
/// surrounding data is preserved. Gracefully skips when the filesystem does
/// not support `FALLOC_FL_ZERO_RANGE`.
pub fn test_write_zeroes(async_io: &mut dyn AsyncIo, file: &mut File) {
// Write 4MB of data
let data = vec![0xBB; 4 * 1024 * 1024];
file.write_all(&data).unwrap();
file.sync_all().unwrap();
// Write zeros in the middle (512KB at offset 2MB)
let offset = 2 * 1024 * 1024;
let length = 512 * 1024;
let write_zeroes_result = async_io.write_zeroes(offset, length, 2);
// FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs)
// If it fails with ENOTSUP, skip the test
if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result
&& (e.raw_os_error() == Some(libc::EOPNOTSUPP) || e.raw_os_error() == Some(libc::ENOTSUP))
{
eprintln!("Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE");
return;
}
write_zeroes_result.unwrap();
// Check completion
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 2);
assert_eq!(result, 0);
// Verify the zeroed region reads as zeros
file.seek(SeekFrom::Start(offset)).unwrap();
let mut read_buf = vec![0; length as usize];
file.read_exact(&mut read_buf).unwrap();
assert!(
read_buf.iter().all(|&b| b == 0),
"Zeroed region should read as zeros"
);
// Verify data before zeroed region is intact
file.seek(SeekFrom::Start(offset - 1024)).unwrap();
let mut read_buf = vec![0; 1024];
file.read_exact(&mut read_buf).unwrap();
assert!(
read_buf.iter().all(|&b| b == 0xBB),
"Data before zeroed region should be intact"
);
// Verify data after zeroed region is intact
file.seek(SeekFrom::Start(offset + length)).unwrap();
let mut read_buf = vec![0; 1024];
file.read_exact(&mut read_buf).unwrap();
assert!(
read_buf.iter().all(|&b| b == 0xBB),
"Data after zeroed region should be intact"
);
}
/// Tests punching multiple holes in an 8 MB file and verifying each hole
/// independently reads as zeroes.
pub fn test_punch_hole_multiple_operations(async_io: &mut dyn AsyncIo, file: &mut File) {
// Write 8MB of data
let data = vec![0xCC; 8 * 1024 * 1024];
file.write_all(&data).unwrap();
file.sync_all().unwrap();
// Punch multiple holes
async_io.punch_hole(1024 * 1024, 512 * 1024, 10).unwrap();
async_io
.punch_hole(3 * 1024 * 1024, 512 * 1024, 11)
.unwrap();
async_io
.punch_hole(5 * 1024 * 1024, 512 * 1024, 12)
.unwrap();
// Check all completions
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 10);
assert_eq!(result, 0);
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 11);
assert_eq!(result, 0);
let (user_data, result) = next_completion(async_io);
assert_eq!(user_data, 12);
assert_eq!(result, 0);
// Verify all holes read as zeros
file.seek(SeekFrom::Start(1024 * 1024)).unwrap();
let mut read_buf = vec![0; 512 * 1024];
file.read_exact(&mut read_buf).unwrap();
assert!(read_buf.iter().all(|&b| b == 0));
file.seek(SeekFrom::Start(3 * 1024 * 1024)).unwrap();
file.read_exact(&mut read_buf).unwrap();
assert!(read_buf.iter().all(|&b| b == 0));
file.seek(SeekFrom::Start(5 * 1024 * 1024)).unwrap();
file.read_exact(&mut read_buf).unwrap();
assert!(read_buf.iter().all(|&b| b == 0));
}

View File

@@ -1,58 +0,0 @@
// 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

@@ -1,73 +0,0 @@
// 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

@@ -1,59 +0,0 @@
// 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

@@ -1,332 +0,0 @@
// 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::{AlignedFile, DiskTopology, Error, disk_file};
#[derive(Debug)]
pub struct VhdDisk {
inner: FixedVhd,
use_io_uring: bool,
direct: bool,
}
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(
BlockErrorKind::UnsupportedFeature,
DiskFileError::NewAsyncIo(io::Error::other(
"io_uring requested but feature is not enabled",
)),
));
}
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 VhdDisk {
fn logical_size(&self) -> BlockResult<u64> {
self.inner
.logical_size()
.map_err(|e| BlockError::new(BlockErrorKind::Io, e))
}
}
impl disk_file::PhysicalSize for VhdDisk {
fn physical_size(&self) -> BlockResult<u64> {
self.inner.physical_size().map_err(|e| match e {
Error::GetFileMetadata(io) => {
BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io))
}
_ => unreachable!("unexpected error from FixedVhd::physical_size(): {e}"),
})
}
}
impl disk_file::DiskFd for VhdDisk {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.inner.as_raw_fd())
}
}
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 VhdDisk {}
impl disk_file::Resizable for VhdDisk {
fn resize(&mut self, _size: u64) -> BlockResult<()> {
Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
DiskFileError::ResizeError(io::Error::other("resize not supported for fixed VHD")),
)
.with_op(ErrorOp::Resize))
}
}
impl disk_file::DiskFile for VhdDisk {}
impl disk_file::AsyncDiskFile for VhdDisk {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
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(raw_file, ring_depth, size)?));
}
#[cfg(not(feature = "io_uring"))]
unreachable!("use_io_uring is set but io_uring feature is not enabled");
}
let _ = ring_depth;
Ok(Box::new(FixedVhdSync::new(raw_file, size)))
}
}
#[cfg(test)]
mod unit_tests {
use std::fs::File;
use std::io::{Seek, SeekFrom, Write};
use vmm_sys_util::tempfile::TempFile;
use super::*;
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).
fn fixed_vhd_footer() -> &'static [u8] {
&[
0x63, 0x6f, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x78, // cookie
0x00, 0x00, 0x00, 0x02, // features
0x00, 0x01, 0x00, 0x00, // file format version
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // data offset
0x27, 0xa6, 0xa6, 0x5d, // time stamp
0x71, 0x65, 0x6d, 0x75, // creator application
0x00, 0x05, 0x00, 0x03, // creator version
0x57, 0x69, 0x32, 0x6b, // creator host os
0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, // original size
0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, // current size
0x11, 0xe0, 0x10, 0x3f, // disk geometry
0x00, 0x00, 0x00, 0x02, // disk type
0x00, 0x00, 0x00, 0x00, // checksum
0x98, 0x7b, 0xb1, 0xcd, 0x84, 0x14, 0x41, 0xfc, // unique id
0xa4, 0xab, 0xd0, 0x69, 0x45, 0x2b, 0xf2, 0x23, 0x00, // saved state
]
}
fn make_vhd_file() -> File {
let mut file: File = TempFile::new().unwrap().into_file();
let data_size: u64 = 0x1122_3344;
file.set_len(data_size + 0x200).unwrap();
file.seek(SeekFrom::Start(data_size)).unwrap();
file.write_all(fixed_vhd_footer()).unwrap();
file
}
#[test]
fn new_sync_returns_correct_size() {
let file = make_vhd_file();
let disk = VhdDisk::new(file, false, false).unwrap();
assert_eq!(disk.logical_size().unwrap(), 0x1122_3344);
}
fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_batch: bool) {
let io: Box<dyn AsyncIo> = disk.create_async_io(128).unwrap();
assert_eq!(io.batch_requests_enabled(), expect_batch);
}
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 = VhdDisk::new(file, false, false).unwrap();
assert_async_io(&disk, false);
}
#[cfg(feature = "io_uring")]
#[test]
fn io_uring_backend_enables_batch_requests() {
let file = make_vhd_file();
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 = VhdDisk::new(file, false, false).unwrap();
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), false);
}
#[cfg(feature = "io_uring")]
#[test]
fn try_clone_preserves_io_uring_dispatch() {
let file = make_vhd_file();
let disk = VhdDisk::new(file, true, false).unwrap();
let cloned = disk.try_clone().unwrap();
assert_async_io_from_dyn(cloned.as_ref(), true);
}
#[test]
fn resize_returns_error() {
let file = make_vhd_file();
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 = VhdDisk::new(file, false, false).unwrap();
// Data region (0x1122_3344) + VHD footer (0x200).
assert_eq!(disk.physical_size().unwrap(), 0x1122_3344 + 0x200);
}
}

View File

@@ -1,208 +0,0 @@
// 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

@@ -1,118 +0,0 @@
// 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

@@ -1,25 +0,0 @@
// 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,
}
}

View File

@@ -1,184 +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};
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

@@ -1,235 +0,0 @@
// 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

@@ -1,40 +0,0 @@
// 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

@@ -1,42 +0,0 @@
// 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

@@ -1,241 +0,0 @@
// 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);
}
}

Some files were not shown because too many files have changed in this diff Show More