Compare commits

..

7 Commits

Author SHA1 Message Date
Rob Bradford
e01933c8ea build: Release v21.1 (bug fix release)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
Rob Bradford
c6307865eb virtio-devices: Enable F_EVENT_IDX on control queue if negotiated
With the VIRTIO_F_EVENT_IDX handling now conducted inside the
virtio-queue crate it is necessary to activate the functionality on
every queue if it is negotiatated. Otherwise this leads to a failure of
the guest to signal to the host that there is something in the available
queue as the queue's internal state has not been configured correctly.

Fixes: #3829

Signed-off-by: Rob Bradford <robert.bradford@intel.com>
(cherry picked from commit 223d0cf787)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
Yi Wang
6275d4f87e vmm: interrupt: fix msi mask irq causing kernel panic on AMD
When mask a msi irq, we set the entry.masked to be true, so kvm
hypervisor will not pass the gsi to kernel through KVM_SET_GSI_ROUTING
ioctl which update kvm->irq_routing. This will trigger kernel
panic on AMD platform when the gsi is the largest one in kernel
kvm->irqfds.items:

crash> bt
PID: 22218  TASK: ffff951a6ad74980  CPU: 73  COMMAND: "vcpu8"
 #0 [ffffb1ba6707fa40] machine_kexec at ffffffff8565b397
 #1 [ffffb1ba6707fa90] __crash_kexec at ffffffff85788a6d
 #2 [ffffb1ba6707fb58] crash_kexec at ffffffff8578995d
 #3 [ffffb1ba6707fb70] oops_end at ffffffff85623c0d
 #4 [ffffb1ba6707fb90] no_context at ffffffff856692c9
 #5 [ffffb1ba6707fbf8] exc_page_fault at ffffffff85f95b51
 #6 [ffffb1ba6707fc50] asm_exc_page_fault at ffffffff86000ace
    [exception RIP: svm_update_pi_irte+227]
    RIP: ffffffffc0761b53  RSP: ffffb1ba6707fd08  RFLAGS: 00010086
    RAX: ffffb1ba6707fd78  RBX: ffffb1ba66d91000  RCX: 0000000000000001
    RDX: 00003c803f63f1c0  RSI: 000000000000019a  RDI: ffffb1ba66db2ab8
    RBP: 000000000000019a   R8: 0000000000000040   R9: ffff94ca41b82200
    R10: ffffffffffffffcf  R11: 0000000000000001  R12: 0000000000000001
    R13: 0000000000000001  R14: ffffffffffffffcf  R15: 000000000000005f
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
 #7 [ffffb1ba6707fdb8] kvm_irq_routing_update at ffffffffc09f19a1 [kvm]
 #8 [ffffb1ba6707fde0] kvm_set_irq_routing at ffffffffc09f2133 [kvm]
 #9 [ffffb1ba6707fe18] kvm_vm_ioctl at ffffffffc09ef544 [kvm]
    RIP: 00007f143c36488b  RSP: 00007f143a4e04b8  RFLAGS: 00000246
    RAX: ffffffffffffffda  RBX: 00007f05780041d0  RCX: 00007f143c36488b
    RDX: 00007f05780041d0  RSI: 000000004008ae6a  RDI: 0000000000000020
    RBP: 00000000000004e8   R8: 0000000000000008   R9: 00007f05780041e0
    R10: 00007f0578004560  R11: 0000000000000246  R12: 00000000000004e0
    R13: 000000000000001a  R14: 00007f1424001c60  R15: 00007f0578003bc0
    ORIG_RAX: 0000000000000010  CS: 0033  SS: 002b

To solve this problem, move route.disable() before set_gsi_routes() to
remove the gsi from irqfds.items first.

This problem only exists on AMD platform, 'cause on Intel platform
kernel just return when update irte while it only prints a warning on
AMD.

Also, this patch adjusts the order of enable() and set_gsi_routes() in
unmask(), which should do no harm.

Signed-off-by: Yi Wang <wang.yi59@zte.com.cn>
(cherry picked from commit 5375b84e3b)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
Yi Wang
4dfe1ff77a vmm: interrupt: fix msi mask irq causing kernel panic on AMD
When mask a msi irq, we set the entry.masked to be true, so kvm
hypervisor will not pass the gsi to kernel through KVM_SET_GSI_ROUTING
ioctl which update kvm->irq_routing. This will trigger kernel
panic on AMD platform when the gsi is the largest one in kernel
kvm->irqfds.items:

crash> bt
PID: 22218  TASK: ffff951a6ad74980  CPU: 73  COMMAND: "vcpu8"
 #0 [ffffb1ba6707fa40] machine_kexec at ffffffff8565b397
 #1 [ffffb1ba6707fa90] __crash_kexec at ffffffff85788a6d
 #2 [ffffb1ba6707fb58] crash_kexec at ffffffff8578995d
 #3 [ffffb1ba6707fb70] oops_end at ffffffff85623c0d
 #4 [ffffb1ba6707fb90] no_context at ffffffff856692c9
 #5 [ffffb1ba6707fbf8] exc_page_fault at ffffffff85f95b51
 #6 [ffffb1ba6707fc50] asm_exc_page_fault at ffffffff86000ace
    [exception RIP: svm_update_pi_irte+227]
    RIP: ffffffffc0761b53  RSP: ffffb1ba6707fd08  RFLAGS: 00010086
    RAX: ffffb1ba6707fd78  RBX: ffffb1ba66d91000  RCX: 0000000000000001
    RDX: 00003c803f63f1c0  RSI: 000000000000019a  RDI: ffffb1ba66db2ab8
    RBP: 000000000000019a   R8: 0000000000000040   R9: ffff94ca41b82200
    R10: ffffffffffffffcf  R11: 0000000000000001  R12: 0000000000000001
    R13: 0000000000000001  R14: ffffffffffffffcf  R15: 000000000000005f
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
 #7 [ffffb1ba6707fdb8] kvm_irq_routing_update at ffffffffc09f19a1 [kvm]
 #8 [ffffb1ba6707fde0] kvm_set_irq_routing at ffffffffc09f2133 [kvm]
 #9 [ffffb1ba6707fe18] kvm_vm_ioctl at ffffffffc09ef544 [kvm]
    RIP: 00007f143c36488b  RSP: 00007f143a4e04b8  RFLAGS: 00000246
    RAX: ffffffffffffffda  RBX: 00007f05780041d0  RCX: 00007f143c36488b
    RDX: 00007f05780041d0  RSI: 000000004008ae6a  RDI: 0000000000000020
    RBP: 00000000000004e8   R8: 0000000000000008   R9: 00007f05780041e0
    R10: 00007f0578004560  R11: 0000000000000246  R12: 00000000000004e0
    R13: 000000000000001a  R14: 00007f1424001c60  R15: 00007f0578003bc0
    ORIG_RAX: 0000000000000010  CS: 0033  SS: 002b

To solve this problem, move route.disable() before set_gsi_routes() to
remove the gsi from irqfds.items first.

This problem only exists on AMD platform, 'cause on Intel platform
kernel just return when update irte while it only prints a warning on
AMD.

Signed-off-by: Yi Wang <wang.yi59@zte.com.cn>
(cherry picked from commit db9e5e5a87)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
Rob Bradford
bbda07c388 pci: Support DWORD/4-byte writes to the MSI-X control register
The PCI spec does not specify that the access has to be of a specific
size.

Fixes: #3714

Signed-off-by: Rob Bradford <robert.bradford@intel.com>
(cherry picked from commit 9c6e7c4a4b)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
Rob Bradford
366223c9f4 vmm: Ensure that PIO and MMIO exits complete before pausing
As per this kernel documentation:

      For KVM_EXIT_IO, KVM_EXIT_MMIO, KVM_EXIT_OSI, KVM_EXIT_PAPR, KVM_EXIT_XEN,
      KVM_EXIT_EPR, KVM_EXIT_X86_RDMSR and KVM_EXIT_X86_WRMSR the corresponding
      operations are complete (and guest state is consistent) only after userspace
      has re-entered the kernel with KVM_RUN.  The kernel side will first finish
      incomplete operations and then check for pending signals.

      The pending state of the operation is not preserved in state which is
      visible to userspace, thus userspace should ensure that the operation is
      completed before performing a live migration.  Userspace can re-enter the
      guest with an unmasked signal pending or with the immediate_exit field set
      to complete pending operations without allowing any further instructions
      to be executed.

Since we capture the state as part of the pause and override it as part
of the resume we must ensure the state is consistent otherwise we will
lose the results of the MMIO or PIO operation that caused the exit from
which we paused.

Fixes: #3658

Signed-off-by: Rob Bradford <robert.bradford@intel.com>
(cherry picked from commit 507912385a)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
Rob Bradford
375356a097 virtio-devices: Add openat() syscall to seccomp filter
When freeing memory sometimes glibc will attempt to read
"/proc/sys/vm/overcommit_memory" to find out how it should release the
blocks. This happens sporadically with Cloud Hypervisor but has been
seen in use. It is not necessary to add the read() syscall to the list
as it is already included in the virtio devices common set. Similarly
the vCPU and vmm threads already have both these in the allowed list.

Fixes: #3609

Signed-off-by: Rob Bradford <robert.bradford@intel.com>
(cherry picked from commit 53caa565bb)
Signed-off-by: Rob Bradford <robert.bradford@intel.com>
2022-03-11 08:44:05 +00:00
499 changed files with 61718 additions and 144993 deletions

View File

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

@@ -1,77 +1,18 @@
version: 2
updates:
- package-ecosystem: cargo
directories:
- "/"
- "/fuzz"
schedule:
interval: weekly
allow:
- dependency-name: "acpi_tables"
- dependency-name: "iommufd-ioctls"
- dependency-name: "kvm-bindings"
- dependency-name: "kvm-ioctls"
- dependency-name: "linux-loader"
- dependency-name: "micro_http"
- dependency-name: "mshv-bindings"
- dependency-name: "mshv-ioctls"
- dependency-name: "seccompiler"
- dependency-name: "vfio-bindings"
- dependency-name: "vfio-ioctls"
- dependency-name: "vfio_user"
- dependency-name: "vhost"
- dependency-name: "vhost-user-backend"
- dependency-name: "virtio-bindings"
- dependency-name: "virtio-queue"
- dependency-name: "vm-fdt"
- dependency-name: "vm-memory"
- dependency-name: "vmm-sys-util"
groups:
rust-vmm:
patterns:
- "*"
- package-ecosystem: cargo
directories:
- "/"
- "/fuzz"
schedule:
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"
- dependency-name: "micro_http"
- dependency-name: "mshv-bindings"
- dependency-name: "mshv-ioctls"
- dependency-name: "seccompiler"
- dependency-name: "vfio-bindings"
- dependency-name: "vfio-ioctls"
- dependency-name: "vfio_user"
- dependency-name: "vhost"
- dependency-name: "vhost-user-backend"
- dependency-name: "virtio-bindings"
- dependency-name: "virtio-queue"
- dependency-name: "vm-fdt"
- dependency-name: "vm-memory"
- dependency-name: "vmm-sys-util"
groups:
non-rust-vmm:
patterns:
- "*"
# Makes it possible to have another config for the same directory.
# https://github.com/dependabot/dependabot-core/issues/1778#issuecomment-1988140219
target-branch: main
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 1
open-pull-requests-limit: 3
allow:
- dependency-type: direct
- dependency-type: indirect
- package-ecosystem: cargo
directory: "/fuzz"
schedule:
interval: daily
open-pull-requests-limit: 3
allow:
- dependency-type: direct
- dependency-type: indirect

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

@@ -0,0 +1,15 @@
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@v1
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

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

@@ -0,0 +1,56 @@
name: Cloud Hypervisor Build
on: [pull_request, create]
jobs:
build:
if: github.event_name == 'pull_request'
name: Build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust:
- stable
- beta
- nightly
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
steps:
- name: Code checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Install Rust toolchain (${{ matrix.rust }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- name: Debug Check (default features)
run: |
git rev-list origin/main..$GITHUB_SHA | xargs -t -I % sh -c 'git checkout %; cargo check --all --target=${{ matrix.target }}'
git checkout $GITHUB_SHA
- name: Build (default + tdx)
run: cargo rustc --bin cloud-hypervisor --features "tdx" -- -D warnings
- name: Build (acpi,kvm)
run: cargo rustc --bin cloud-hypervisor --no-default-features --features "acpi,kvm" -- -D warnings
- name: Build (kvm)
run: cargo rustc --bin cloud-hypervisor --no-default-features --features "kvm" -- -D warnings
- name: Build (acpi,mshv)
run: cargo rustc --bin cloud-hypervisor --no-default-features --features "acpi,mshv" -- -D warnings
- name: Build (mshv)
run: cargo rustc --bin cloud-hypervisor --no-default-features --features "mshv" -- -D warnings
- name: Release Build (default features)
run: cargo build --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

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

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

View File

@@ -1,119 +1,48 @@
name: Cloud Hypervisor's Docker image update
on:
push:
branches: main
paths: resources/Dockerfile
pull_request:
paths: resources/Dockerfile
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
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@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v1
- name: Login to ghcr
- name: Login to DockerHub
if: ${{ github.event_name == 'push' }}
uses: docker/login-action@v4
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
- name: Build and push
if: ${{ github.event_name == 'push' }}
uses: docker/build-push-action@v2
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' }}
platforms: linux/amd64,linux/arm64
push: true
tags: cloudhypervisor/dev:latest
- 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
- name: Build only
if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v2
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
file: ./resources/Dockerfile
platforms: linux/amd64,linux/arm64
tags: cloudhypervisor/dev:latest
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
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# generate Docker tags based on the following events/attributes
tags: |
type=raw,value=20260522-0
type=sha
- name: Login to ghcr
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- 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: 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 }}

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

@@ -0,0 +1,29 @@
name: Cloud Hypervisor Cargo Fuzz Build
on: [pull_request, create]
jobs:
build:
if: github.event_name == 'pull_request'
name: Cargo Fuzz Build
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- nightly
target:
- x86_64-unknown-linux-gnu
steps:
- name: Code checkout
uses: actions/checkout@v2
- name: Install Rust toolchain (${{ matrix.rust }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- name: Install Cargo fuzz
# Temporary fix for cargo-fuzz on latest nightly: https://github.com/rust-fuzz/cargo-fuzz/issues/276
#run: cargo install cargo-fuzz
run: cargo install --git https://github.com/rust-fuzz/cargo-fuzz --rev b4df3e58f767b5cad8d1aa6753961003f56f3609
- name: Cargo Fuzz Build
run: cargo fuzz build

View File

@@ -1,32 +0,0 @@
name: Cloud Hypervisor Tests (Metrics)
on:
push:
branches:
- main
jobs:
build:
name: Tests (Metrics)
runs-on: garm-jammy-16
env:
METRICS_PUBLISH_KEY: ${{ secrets.METRICS_PUBLISH_KEY }}
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: Run metrics tests
timeout-minutes: 60
run: scripts/dev_cli.sh tests --metrics -- --test-exclude micro_,block_qcow2 -- --report-file /root/workloads/metrics.json
- name: Upload metrics report
run: 'curl -X PUT https://ch-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json'

61
.github/workflows/quality-aarch64.yaml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Cloud Hypervisor Quality Checks
on: [pull_request, create]
jobs:
build:
if: github.event_name == 'pull_request'
name: Quality (clippy, rustfmt)
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
rust:
- stable
target:
- aarch64-unknown-linux-gnu
experimental: [false]
include:
- rust: beta
target: aarch64-unknown-linux-gnu
experimental: true
steps:
- name: Code checkout
uses: actions/checkout@v2
- name: Install Rust toolchain (${{ matrix.rust }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
components: rustfmt, clippy
- name: Formatting (rustfmt)
run: cargo fmt -- --check
- name: Clippy (kvm)
uses: actions-rs/cargo@v1
with:
use-cross: true
command: clippy
args: --target=${{ matrix.target }} --all --no-default-features --features "kvm" -- -D warnings
- name: Clippy (kvm,acpi)
uses: actions-rs/cargo@v1
with:
use-cross: true
command: clippy
args: --target=${{ matrix.target }} --all --no-default-features --features "kvm,acpi" -- -D warnings
- name: Clippy (all features,kvm)
uses: actions-rs/cargo@v1
with:
use-cross: true
command: clippy
args: --target=${{ matrix.target }} --all --no-default-features --features "common,kvm" -- -D warnings
- name: Clippy (default)
uses: actions-rs/cargo@v1
with:
use-cross: true
command: clippy
args: --target=${{ matrix.target }} --all -- -D warnings

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

@@ -0,0 +1,61 @@
name: Cloud Hypervisor Quality Checks
on: [pull_request, create]
jobs:
build:
if: github.event_name == 'pull_request'
name: Quality (clippy, rustfmt)
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
rust:
- stable
target:
- x86_64-unknown-linux-gnu
experimental: [false]
include:
- rust: beta
target: x86_64-unknown-linux-gnu
experimental: true
steps:
- name: Code checkout
uses: actions/checkout@v2
- name: Install Rust toolchain (${{ matrix.rust }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
components: rustfmt, clippy
- name: Formatting (rustfmt)
run: cargo fmt -- --check
- name: Clippy (all features,kvm)
run: cargo clippy --all --all-targets --no-default-features --tests --features "common,kvm" -- -D warnings
- name: Clippy (all features,mshv)
run: cargo clippy --all --all-targets --no-default-features --tests --features "common,mshv" -- -D warnings
- name: Clippy (acpi,kvm)
run: cargo clippy --all --all-targets --no-default-features --tests --features "acpi,kvm" -- -D warnings
- name: Clippy (acpi,kvm,tdx)
run: cargo clippy --all --all-targets --no-default-features --tests --features "acpi,kvm,tdx" -- -D warnings
- name: Clippy (kvm)
run: cargo clippy --all --all-targets --no-default-features --tests --features "kvm" -- -D warnings
- name: Clippy (acpi,mshv)
run: cargo clippy --all --all-targets --no-default-features --tests --features "acpi,mshv" -- -D warnings
- name: Clippy (mshv)
run: cargo clippy --all --all-targets --no-default-features --tests --features "mshv" -- -D warnings
- name: Clippy (integration tests)
run: cargo clippy --all --all-targets --tests -- -D warnings
- name: Check build did not modify any files
run: test -z "$(git status --porcelain)"

View File

@@ -1,95 +1,131 @@
name: Cloud Hypervisor Release
on: [create, merge_group]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
cancel-in-progress: true
env:
GITHUB_TOKEN: ${{ github.token }}
on: [create]
jobs:
release:
if: (github.event_name == 'create' && github.event.ref_type == 'tag') || github.event_name == 'merge_group'
name: Release ${{ matrix.platform.target }}
strategy:
fail-fast: false
matrix:
platform:
- target: x86_64-unknown-linux-gnu
args: --all --release --features mshv
name_ch: cloud-hypervisor
name_ch_remote: ch-remote
- target: x86_64-unknown-linux-musl
args: --all --release --features mshv
name_ch: cloud-hypervisor-static
name_ch_remote: ch-remote-static
- target: aarch64-unknown-linux-musl
args: --all --release
name_ch: cloud-hypervisor-static-aarch64
name_ch_remote: ch-remote-static-aarch64
if: github.event_name == 'create' && github.event.ref_type == 'tag'
name: Release
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v7
- name: Install musl-gcc
if: contains(matrix.platform.target, 'musl')
run: sudo apt install -y musl-tools
uses: actions/checkout@v2
- name: Create release directory
if: |
github.event_name == 'create' && github.event.ref_type == 'tag' &&
matrix.platform.target == 'x86_64-unknown-linux-gnu'
run: rsync -rv --exclude=.git . ../cloud-hypervisor-${{ github.event.ref }}
- name: Build ${{ matrix.platform.target }}
uses: houseabsolute/actions-rust-cross@v1
- name: Install Rust toolchain (x86_64-unknown-linux-gnu)
uses: actions-rs/toolchain@v1
with:
command: build
target: ${{ matrix.platform.target }}
args: ${{ matrix.platform.args }}
strip: true
toolchain: "1.89.0"
- name: Copy Release Binaries
if: github.event_name == 'create' && github.event.ref_type == 'tag'
shell: bash
run: |
cp target/${{ matrix.platform.target }}/release/cloud-hypervisor ./${{ matrix.platform.name_ch }}
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
toolchain: stable
target: x86_64-unknown-linux-gnu
- name: Install Rust toolchain (x86_64-unknown-linux-musl)
uses: actions-rs/toolchain@v1
with:
name: Artifacts for ${{ matrix.platform.target }}
path: |
./${{ matrix.platform.name_ch }}
./${{ matrix.platform.name_ch_remote }}
toolchain: stable
target: x86_64-unknown-linux-musl
- name: Build
run: cargo build --all --release --target=x86_64-unknown-linux-gnu
- name: Static Build
run: cargo build --all --release --target=x86_64-unknown-linux-musl
- name: Strip cloud-hypervisor binaries
run: strip target/*/release/cloud-hypervisor
- name: Install Rust toolchain (aarch64-unknown-linux-musl)
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: aarch64-unknown-linux-musl
override: true
- name: Static Build (AArch64)
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --all --release --target=aarch64-unknown-linux-musl
- name: Vendor
if: |
github.event_name == 'create' && github.event.ref_type == 'tag' &&
matrix.platform.target == 'x86_64-unknown-linux-gnu'
working-directory: ../cloud-hypervisor-${{ github.event.ref }}
run: |
mkdir ../vendor-cargo-home
export CARGO_HOME=$(realpath ../vendor-cargo-home)
mkdir .cargo
cargo vendor > .cargo/config.toml
- name: Create vendored source archive
if: |
github.event_name == 'create' && github.event.ref_type == 'tag' &&
matrix.platform.target == 'x86_64-unknown-linux-gnu'
run: tar cJf cloud-hypervisor-${{ github.event.ref }}.tar.xz ../cloud-hypervisor-${{ github.event.ref }}
- name: Upload cloud-hypervisor vendored source archive
if: |
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
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
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
files: |
./${{ matrix.platform.name_ch }}
./${{ matrix.platform.name_ch_remote }}
./cloud-hypervisor-${{ github.event.ref }}.tar.xz
prerelease: true
- name: Create vendored source archive
working-directory: ../
run: tar cJf cloud-hypervisor-${{ github.event.ref }}.tar.xz cloud-hypervisor-${{ github.event.ref }}
- name: Upload cloud-hypervisor vendored source archive
id: upload-release-cloud-hypervisor-vendored-sources
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ../cloud-hypervisor-${{ github.event.ref }}.tar.xz
asset_name: cloud-hypervisor-${{ github.event.ref }}.tar.xz
asset_content_type: application/x-xz
- name: Upload cloud-hypervisor
id: upload-release-cloud-hypervisor
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: target/x86_64-unknown-linux-gnu/release/cloud-hypervisor
asset_name: cloud-hypervisor
asset_content_type: application/octet-stream
- name: Upload static cloud-hypervisor
id: upload-release-static-cloud-hypervisor
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: target/x86_64-unknown-linux-musl/release/cloud-hypervisor
asset_name: cloud-hypervisor-static
asset_content_type: application/octet-stream
- name: Upload ch-remote
id: upload-release-ch-remote
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: target/x86_64-unknown-linux-gnu/release/ch-remote
asset_name: ch-remote
asset_content_type: application/octet-stream
- name: Upload static-ch-remote
id: upload-release-static-ch-remote
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: target/x86_64-unknown-linux-musl/release/ch-remote
asset_name: ch-remote-static
asset_content_type: application/octet-stream
- name: Upload static AArch64 cloud-hypervisor
id: upload-release-static-aarch64-cloud-hypervisor
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: target/aarch64-unknown-linux-musl/release/cloud-hypervisor
asset_name: cloud-hypervisor-static-aarch64
asset_content_type: application/octet-stream
- name: Upload static AArch64 ch-remote
id: upload-release-static-aarch64-ch-remote
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: target/aarch64-unknown-linux-musl/release/ch-remote
asset_name: ch-remote-static-aarch64
asset_content_type: application/octet-stream

13
.gitignore vendored
View File

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

View File

@@ -1,13 +0,0 @@
[general]
extra-path=scripts/gitlint/rules
regex-style-search=true
ignore=body-max-line-length,body-hard-tab
[ignore-by-author-name]
regex=dependabot
ignore=all
# default 72
[title-max-length]
line-length=72

View File

@@ -1,36 +0,0 @@
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',
]
# Exclude loopback addresses
exclude_loopback = true
max_retries = 3
retry_wait_time = 5

View File

@@ -1,12 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cloud-hypervisor
Upstream-Contact: <>
Source: https://www.cloudhypervisor.org
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
Copyright: 2024
License: Apache-2.0

View File

@@ -1,4 +1 @@
edition = "2024"
group_imports="StdExternalCrate"
imports_granularity="Module"
edition = "2018"

View File

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

View File

@@ -1,28 +0,0 @@
# Configuration for https://github.com/crate-ci/typos
[files]
extend-exclude = [
"hypervisor/src/kvm/x86_64/mod.rs",
"resources/linux-config-*",
]
[default.extend-words]
CLASSE = "CLASSE"
Dake = "Dake"
EXTINT = "EXTINT"
INOUT = "INOUT"
MSIS = "MSIS" # MSIs (Message Signaled Interrupt)
SME = "SME" # Secure Memory Encryption
THR = "THR" # Transmitter Holding Register
TRANSLATER = "TRANSLATER"
ba = "ba"
conectix = "conectix"
liness = "liness"
outout = "outout"
[default.extend-identifiers]
consts = "consts"
fo = "fo"
fpr = "fpr"
# Public Linux API
msg_controllen = "msg_controllen"

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

@@ -1,2 +0,0 @@
# Add the list of code owners here (using their GitHub username)
* @cloud-hypervisor/cloud-hypervisor-reviewers

View File

@@ -2,156 +2,57 @@
Cloud Hypervisor is an open source project licensed under the [Apache v2
License](https://opensource.org/licenses/Apache-2.0) and the [BSD 3
Clause](https://opensource.org/licenses/BSD-3-Clause) license. Individual files
contain details of their licensing and changes to that file are under the same
license unless the contribution changes the license of the file. When importing
code from a third party project (e.g. Firecracker or crosvm) please respect the
license of those projects.
Clause](https://opensource.org/licenses/BSD-3-Clause) license. Contributions
can be made under either license or both. Individual files contain details of
their licensing and changes to that file are under the same license unless the
contribution changes the license of the file. When importing code from a third
party project (e.g. Firecracker or CrosVM) please respect the license of those
projects.
New code should be under the [Apache v2
License](https://opensource.org/licenses/Apache-2.0).
## Coding Style
Cloud Hypervisor's main supported architectures are `x86_64` and `aarch64`,
and the main hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the
most regular exercise, but changes should not make the other supported
architecture and backend combinations worse.
## Coding Style & Code Comments
We use the [Rust Style] guide and enforce formatting and linting in CI,
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
## Basic Checks
```sh
# We currently rely on nightly-only formatting features
cargo +nightly fmt --all
cargo check --all-targets --tests
cargo clippy --all-targets --tests
# Please note that this will not execute integration tests.
cargo test --all-targets --tests
# To lint your last three commits
gitlint --commits "HEAD~3..HEAD"
```
### \[Optional\] Run Integration Tests
_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>'
```
### Setup Commit Hook
Please consider creating the following hook as `.git/hooks/pre-commit` in order
to ensure basic correctness of your code. You can extend this further if you
have specific features that you regularly develop against.
```sh
#!/bin/sh
cargo +nightly fmt --all -- --check || exit 1
cargo check --locked --all-targets --tests || exit 1
cargo clippy --locked --all-targets --tests -- -D warnings || exit 1
```
You will need to `chmod +x .git/hooks/pre-commit` to have it run on every
commit you make.
We follow the [Rust Style](https://github.com/rust-dev-tools/fmt-rfcs/blob/master/guide/guide.md)
convention and enforce it through the Continuous Integration (CI) process calling into `rustfmt`
for each submitted Pull Request (PR).
## 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://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
@@ -159,16 +60,14 @@ you want to merge your changes to `cloud-hypervisor`:
1. Fork the [cloud-hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) project
into your github organization.
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/)
2. Within your fork, create a branch for your contribution.
3. [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. 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.
4. Add reviewers to your pull request and then work with your reviewers to address
any comments and obtain minimum of 2 [maintainers](MAINTAINERS.md) approvals.
To update your pull request amend existing commits whenever applicable and
then push the new changes to your pull request branch.
5. Once the pull request is approved, one of the maintainers will merge it.
## Issue tracking
@@ -185,83 +84,16 @@ 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
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
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.
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.

2626
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +1,89 @@
# Cloud Hypervisor Workspace
#
# The main crate producing the binaries is in `./cloud-hypervisor`.
[package]
name = "cloud-hypervisor"
version = "21.1.0"
authors = ["The Cloud Hypervisor Authors"]
edition = "2018"
default-run = "cloud-hypervisor"
build = "build.rs"
license = "LICENSE-APACHE & LICENSE-BSD-3-Clause"
description = "Open source Virtual Machine Monitor (VMM) that runs on top of KVM"
homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor"
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
strip = true
[profile.profiling]
debug = true
inherits = "release"
strip = false
[dependencies]
anyhow = "1.0.52"
api_client = { path = "api_client" }
clap = { version = "3.0.10", features = ["wrap_help","cargo"] }
epoll = "4.3.1"
event_monitor = { path = "event_monitor" }
hypervisor = { path = "hypervisor" }
libc = "0.2.112"
log = { version = "0.4.14", features = ["std"] }
option_parser = { path = "option_parser" }
seccompiler = "0.2.0"
serde_json = "1.0.75"
signal-hook = "0.3.13"
thiserror = "1.0.30"
vmm = { path = "vmm" }
vmm-sys-util = "0.9.0"
vm-memory = "0.7.0"
[build-dependencies]
clap = { version = "3.0.10", features = ["wrap_help"] }
# List of patched crates
[patch.crates-io]
kvm-bindings = { git = "https://github.com/cloud-hypervisor/kvm-bindings", branch = "ch-v0.5.0", features = ["with-serde", "fam-wrappers"] }
kvm-ioctls = { git = "https://github.com/rust-vmm/kvm-ioctls", branch = "main" }
versionize_derive = { git = "https://github.com/cloud-hypervisor/versionize_derive", branch = "ch" }
[dev-dependencies]
dirs = "4.0.0"
lazy_static= "1.4.0"
net_util = { path = "net_util" }
serde_json = "1.0.75"
test_infra = { path = "test_infra" }
wait-timeout = "0.2.0"
[features]
default = ["common", "kvm"]
# Common features for all hypervisors
common = ["acpi", "cmos", "fwdebug"]
acpi = ["vmm/acpi"]
cmos = ["vmm/cmos"]
fwdebug = ["vmm/fwdebug"]
kvm = ["vmm/kvm"]
mshv = ["vmm/mshv"]
tdx = ["vmm/tdx"]
[workspace]
members = [
"api_client",
"arch",
"block",
"cloud-hypervisor",
"devices",
"event_monitor",
"hypervisor",
"net_util",
"offload_daemon",
"option_parser",
"pci",
"performance-metrics",
"rate_limiter",
"serial_buffer",
"test_infra",
"tracer",
"vhost_user_block",
"vhost_user_net",
"virtio-devices",
"vm-allocator",
"vm-device",
"vm-migration",
"vm-virtio",
"vmm",
"acpi_tables",
"api_client",
"arch",
"block_util",
"devices",
"event_monitor",
"hypervisor",
"net_gen",
"net_util",
"option_parser",
"pci",
"qcow",
"rate_limiter",
"vfio_user",
"vhdx",
"vhost_user_backend",
"vhost_user_block",
"vhost_user_net",
"virtio-devices",
"virtio-queue",
"vmm",
"vm-allocator",
"vm-device",
"vm-migration",
"vm-virtio"
]
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"
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 }
virtio-bindings = "0.2.6"
virtio-queue = "0.17.0"
vm-fdt = "0.3.0"
vm-memory = "0.17.1"
vmm-sys-util = "0.15.0"
# igvm crates
igvm = "0.4.0"
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 }
# other crates
anyhow = "1.0.102"
base64 = "0.22.1"
bitflags = "2.11.1"
byteorder = "1.5.0"
cfg-if = "1.0.4"
clap = "4.6.1"
dhat = "0.3.3"
dirs = "6.0.0"
env_logger = "0.11.10"
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"
thiserror = "2.0.18"
uuid = { version = "1.23.2" }
wait-timeout = "0.2.1"
zerocopy = { version = "0.8.50", default-features = false }
[workspace.lints.clippy]
# Any clippy lint (group) in alphabetical order:
# https://rust-lang.github.io/rust-clippy/master/index.html
# Groups
all = "deny" # shorthand for the other groups but here for compleness
complexity = "deny"
correctness = "deny"
perf = "deny"
style = "deny"
suspicious = "deny"
# Individual Lints
absolute_paths = "deny"
assertions_on_result_states = "deny"
if_not_else = "deny"
manual_string_new = "deny"
map_unwrap_or = "deny"
needless_pass_by_value = "deny"
redundant_else = "deny"
semicolon_if_nothing_returned = "deny"
undocumented_unsafe_blocks = "deny"
uninlined_format_args = "deny"
unnecessary_semicolon = "deny"
[workspace.lints.rust]
# `level = warn` is irrelevant here but mandatory for rustc/cargo
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(devcli_testenv)'] }
exclude = ["test_infra"]

299
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,299 @@
pipeline{
agent none
stages {
stage ('Early checks') {
agent { node { label 'built-in' } }
stages {
stage ('Check for RFC/WIP builds') {
when {
changeRequest comparator: 'REGEXP', title: '.*(rfc|RFC|wip|WIP).*'
beforeAgent true
}
steps {
error("Failing as this is marked as a WIP or RFC PR.")
}
}
stage ('Cancel older builds') {
when { not { branch 'main' } }
steps {
cancelPreviousBuilds()
}
}
}
}
stage ('Build') {
parallel {
stage ('Worker build') {
agent { node { label 'hirsute' } }
environment {
AZURE_CONNECTION_STRING = credentials('46b4e7d6-315f-4cc1-8333-b58780863b9b')
}
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Run OpenAPI tests') {
steps {
sh "scripts/run_openapi_tests.sh"
}
}
stage ('Run unit tests') {
steps {
sh "scripts/dev_cli.sh tests --unit"
}
}
stage ('Run integration tests') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "sudo modprobe openvswitch"
sh "scripts/dev_cli.sh tests --integration"
}
}
}
}
stage ('AArch64 worker build') {
agent { node { label 'bionic-arm64' } }
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Run unit tests') {
steps {
sh "scripts/dev_cli.sh tests --unit"
}
}
stage ('Run integration tests') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "sudo modprobe openvswitch"
sh "scripts/dev_cli.sh tests --integration"
}
}
}
post {
always {
sh "sudo chown -R jenkins.jenkins ${WORKSPACE}"
deleteDir()
}
}
}
stage ('Worker build (musl)') {
agent { node { label 'hirsute' } }
environment {
AZURE_CONNECTION_STRING = credentials('46b4e7d6-315f-4cc1-8333-b58780863b9b')
}
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Run unit tests for musl') {
steps {
sh "scripts/dev_cli.sh tests --unit --libc musl"
}
}
stage ('Run integration tests for musl') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "sudo modprobe openvswitch"
sh "scripts/dev_cli.sh tests --integration --libc musl"
}
}
}
}
stage ('Worker build SGX') {
agent { node { label 'bionic-sgx' } }
when {
beforeAgent true
branch 'main'
}
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Run SGX integration tests') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "scripts/dev_cli.sh tests --integration-sgx"
}
}
stage ('Run SGX integration tests for musl') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "scripts/dev_cli.sh tests --integration-sgx --libc musl"
}
}
}
post {
always {
sh "sudo chown -R jenkins.jenkins ${WORKSPACE}"
deleteDir()
}
}
}
stage ('Worker build VFIO') {
agent { node { label 'bionic-vfio' } }
when {
beforeAgent true
branch 'main'
}
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Run VFIO integration tests') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "scripts/dev_cli.sh tests --integration-vfio"
}
}
stage ('Run VFIO integration tests for musl') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "scripts/dev_cli.sh tests --integration-vfio --libc musl"
}
}
}
post {
always {
sh "sudo chown -R jenkins.jenkins ${WORKSPACE}"
deleteDir()
}
}
}
stage ('Worker build - Windows guest') {
agent { node { label 'hirsute' } }
environment {
AZURE_CONNECTION_STRING = credentials('46b4e7d6-315f-4cc1-8333-b58780863b9b')
}
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Install azure-cli') {
steps {
installAzureCli()
}
}
stage ('Download assets') {
steps {
sh "mkdir ${env.HOME}/workloads"
sh 'az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2019.raw" --name windows-server-2019.raw --connection-string "$AZURE_CONNECTION_STRING"'
}
}
stage ('Run Windows guest integration tests') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "scripts/dev_cli.sh tests --integration-windows"
}
}
stage ('Run Windows guest integration tests for musl') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "scripts/dev_cli.sh tests --integration-windows --libc musl"
}
}
}
}
stage ('Worker build - Live Migration') {
agent { node { label 'hirsute-small' } }
stages {
stage ('Checkout') {
steps {
checkout scm
}
}
stage ('Run live-migration integration tests') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "sudo modprobe openvswitch"
sh "scripts/dev_cli.sh tests --integration-live-migration"
}
}
stage ('Run live-migration integration tests for musl') {
options {
timeout(time: 1, unit: 'HOURS')
}
steps {
sh "sudo modprobe openvswitch"
sh "scripts/dev_cli.sh tests --integration-live-migration --libc musl"
}
}
}
}
}
}
}
post {
regression {
script {
if (env.BRANCH_NAME == 'main') {
slackSend (color: '#ff0000', message: '"main" branch build is now failing')
}
}
}
fixed {
script {
if (env.BRANCH_NAME == 'main') {
slackSend (color: '#00ff00', message: '"main" branch build is now fixed')
}
}
}
}
}
def cancelPreviousBuilds() {
// Check for other instances of this particular build, cancel any that are older than the current one
def jobName = env.JOB_NAME
def currentBuildNumber = env.BUILD_NUMBER.toInteger()
def currentJob = Jenkins.instance.getItemByFullName(jobName)
// Loop through all instances of this particular job/branch
for (def build : currentJob.builds) {
if (build.isBuilding() && (build.number.toInteger() < currentBuildNumber)) {
echo "Older build still queued. Sending kill signal to build number: ${build.number}"
build.doStop()
}
}
}
def installAzureCli() {
sh "sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg"
sh "curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null"
sh "echo \"deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ hirsute main\" | sudo tee /etc/apt/sources.list.d/azure-cli.list"
sh "sudo apt update"
sh "sudo apt install -y azure-cli"
}

View File

@@ -1,400 +0,0 @@
All documentations (e.g. files with extension `.md`) in this repository is
covered by the following license:
Attribution 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution 4.0 International Public License ("Public License"). To the
extent this Public License may be interpreted as a contract, You are
granted the Licensed Rights in consideration of Your acceptance of
these terms and conditions, and the Licensor grants You such rights in
consideration of benefits the Licensor receives from making the
Licensed Material available under these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -2,7 +2,6 @@
- Sebastien Boeuf - @sboeuf
- Robert Bradford - @rbradford
- Bo Chen - @likebreath
- Samuel Ortiz - @sameo
- Wei Liu - @liuw
- Michael Zhao - @michael2012z

467
README.md
View File

@@ -4,43 +4,35 @@
- [Architectures](#architectures)
- [Guest OS](#guest-os)
- [2. Getting Started](#2-getting-started)
- [Host OS](#host-os)
- [Use Pre-built Binaries](#use-pre-built-binaries)
- [Packages](#packages)
- [Building from Source](#building-from-source)
- [Booting Linux](#booting-linux)
- [Firmware Booting](#firmware-booting)
- [Custom Kernel and Disk Image](#custom-kernel-and-disk-image)
- [Building your Kernel](#building-your-kernel)
- [Preparation](#preparation)
- [Install prerequisites](#install-prerequisites)
- [Clone and build](#clone-and-build)
- [Containerized builds and tests](#containerized-builds-and-tests)
- [Run](#run)
- [Cloud image](#cloud-image)
- [Custom kernel and disk image](#custom-kernel-and-disk-image)
- [Building your kernel](#building-your-kernel)
- [Disk image](#disk-image)
- [Booting the guest VM](#booting-the-guest-vm)
- [3. Status](#3-status)
- [Hot Plug](#hot-plug)
- [Device Model](#device-model)
- [Roadmap](#roadmap)
- [4. Relationship with _Rust VMM_ Project](#4-relationship-with-rust-vmm-project)
- [Differences with Firecracker and crosvm](#differences-with-firecracker-and-crosvm)
- [TODO](#todo)
- [4. `rust-vmm` project dependency](#4-rust-vmm-project-dependency)
- [Firecracker and crosvm](#firecracker-and-crosvm)
- [5. Community](#5-community)
- [Contribute](#contribute)
- [Slack](#slack)
- [Mailing list](#mailing-list)
- [Join us](#join-us)
- [Security issues](#security-issues)
# 1. What is Cloud Hypervisor?
Cloud Hypervisor is an open source Virtual Machine Monitor (VMM) that runs on
top of the [KVM](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt)
hypervisor and the Microsoft Hypervisor (MSHV).
Cloud Hypervisor is an open source Virtual Machine Monitor (VMM) that runs on top of [KVM](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt) and the MSHV hypervisors .
The project focuses on running modern, _Cloud Workloads_, on specific, common,
hardware architectures. In this case _Cloud Workloads_ refers to those that are
run by customers inside a Cloud Service Provider. This means modern operating
systems with most I/O handled by
paravirtualised devices (e.g. _virtio_), no requirement for legacy devices, and
64-bit CPUs.
The project focuses on exclusively running modern, cloud workloads, on top of a limited set of hardware architectures and platforms.
Cloud workloads refers to those that are usually run by customers inside a cloud provider. For our purposes this means modern operating systems with most I/O handled by paravirtualised devices (i.e. virtio), no requirement for legacy devices, and 64-bit CPUs.
Cloud Hypervisor is implemented in [Rust](https://www.rust-lang.org/) and is
based on the [Rust VMM](https://github.com/rust-vmm) crates.
Cloud Hypervisor is implemented in [Rust](https://www.rust-lang.org/) and is based on the [rust-vmm](https://github.com/rust-vmm) crates.
## Objectives
@@ -59,13 +51,7 @@ 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).
The `riscv64` architecture support is experimental and offers limited
functionality. For more details and instructions, please refer to [riscv
documentation](docs/riscv.md).
Cloud Hypervisor supports the `x86-64` and `AArch64` architectures. There are some small differences in functionality between the two architectures (see [#1125](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/1125)).
### Guest OS
@@ -73,202 +59,181 @@ Cloud Hypervisor supports `64-bit Linux` and Windows 10/Windows Server 2019.
# 2. Getting Started
The following sections describe how to build and run Cloud Hypervisor.
Below sections describe how to build and run Cloud Hypervisor on the `x86_64`
platform. For getting started on the `AArch64` platform, please refer to the
[Arm64 documentation](docs/arm64.md).
## Prerequisites for AArch64
## Preparation
- AArch64 servers (recommended) or development boards equipped with the GICv3
interrupt controller.
## Host OS
For required KVM functionality and adequate performance the recommended host
kernel version is 5.13. The majority of the CI currently tests with kernel
version 5.15.
## Use Pre-built Binaries
The recommended approach to getting started with Cloud Hypervisor is by using a
pre-built binary. Binaries are available for the [latest
release](https://github.com/cloud-hypervisor/cloud-hypervisor/releases/latest).
Use `cloud-hypervisor-static` for `x86-64` or `cloud-hypervisor-static-aarch64`
for `AArch64` platform.
## Packages
For convenience, packages are also available targeting some popular Linux
distributions. This is thanks to the [Open Build
Service](https://build.opensuse.org). The [OBS
README](https://github.com/cloud-hypervisor/obs-packaging) explains how to
enable the repository in a supported Linux distribution and install Cloud Hypervisor
and accompanying packages. Please report any packaging issues in the
[obs-packaging](https://github.com/cloud-hypervisor/obs-packaging) repository.
## Building from Source
Please see the [instructions for building from source](docs/building.md) if you
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.
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
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.
### Firmware Booting
Cloud Hypervisor supports booting disk images containing all needed components
to run cloud workloads, a.k.a. cloud images.
The following sample commands will download an Ubuntu Cloud image, converting
it into a format that Cloud Hypervisor can use and a firmware to boot the image
with.
We create a folder to build and run `cloud-hypervisor` at `$HOME/cloud-hypervisor`
```shell
$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img
$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw
$ wget https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/download/0.4.2/hypervisor-fw
$ export CLOUDH=$HOME/cloud-hypervisor
$ mkdir $CLOUDH
```
The Ubuntu cloud images do not ship with a default password so it necessary to
use a `cloud-init` disk image to customise the image on the first boot. A basic
`cloud-init` image is generated by this [script](scripts/create-cloud-init.sh).
This seeds the image with a default username/password of `cloud/cloud123`. It
is only necessary to add this disk image on the first boot. Script also assigns
default IP address using `test_data/cloud-init/ubuntu/local/network-config` details
with `--net "mac=12:34:56:78:90:ab,tap="` option. Then the matching mac address
interface will be enabled as per `network-config` details.
## Install prerequisites
You need to install some prerequisite packages in order to build and test Cloud Hypervisor.
Here, all the steps are based on Ubuntu, for other Linux distributions please replace the
package manager and package name.
```shell
$ sudo setcap cap_net_admin+ep ./cloud-hypervisor
$ ./create-cloud-init.sh
$ ./cloud-hypervisor \
--firmware ./hypervisor-fw \
--disk path=focal-server-cloudimg-amd64.raw path=/tmp/ubuntu-cloudinit.img \
# Install git
$ sudo apt install git
# Install rust tool chain
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install build-essential
$ sudo apt install build-essential
# If you want to build statically linked binary please add musl target
$ rustup target add x86_64-unknown-linux-musl
```
## Clone and build
First you need to clone and build the cloud-hypervisor repo:
```shell
$ pushd $CLOUDH
$ git clone https://github.com/cloud-hypervisor/cloud-hypervisor.git
$ cd cloud-hypervisor
$ cargo build --release
# We need to give the cloud-hypervisor binary the NET_ADMIN capabilities for it to set TAP interfaces up on the host.
$ sudo setcap cap_net_admin+ep ./target/release/cloud-hypervisor
# If you want to build statically linked binary
$ cargo build --release --target=x86_64-unknown-linux-musl --all
$ popd
```
This will build a `cloud-hypervisor` binary under `$CLOUDH/cloud-hypervisor/target/release/cloud-hypervisor`.
### Containerized builds and tests
If you want to build and test Cloud Hypervisor without having to install all the
required dependencies (The rust toolchain, cargo tools, etc), you can also use
Cloud Hypervisor's development script: `dev_cli.sh`. Please note that upon its
first invocation, this script will pull a fairly large container image.
For example, to build the Cloud Hypervisor release binary:
```shell
$ pushd $CLOUDH
$ cd cloud-hypervisor
$ ./scripts/dev_cli.sh build --release
```
With `dev_cli.sh`, one can also run the Cloud Hypervisor CI locally. This can be
very convenient for debugging CI errors without having to fully rely on the
Cloud Hypervisor CI infrastructure.
For example, to run the Cloud Hypervisor unit tests:
```shell
$ ./scripts/dev_cli.sh tests --unit
```
Run the `./scripts/dev_cli.sh --help` command to view all the supported
development script commands and their related options.
## Run
You can run a guest VM by either using an existing cloud image or booting into your own kernel and disk image.
### Cloud image
Cloud Hypervisor supports booting disk images containing all needed
components to run cloud workloads, a.k.a. cloud images. To do that we rely on
the [Rust Hypervisor
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) project to provide
an ELF
formatted KVM firmware for `cloud-hypervisor` to directly boot into.
We need to get the latest `rust-hypervisor-firmware` release and also a working cloud image. Here we will use a Ubuntu image:
```shell
$ pushd $CLOUDH
$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img
$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw
$ wget https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/download/0.3.2/hypervisor-fw
$ popd
```
```shell
$ pushd $CLOUDH
$ sudo setcap cap_net_admin+ep ./cloud-hypervisor/target/release/cloud-hypervisor
$ ./cloud-hypervisor/target/release/cloud-hypervisor \
--kernel ./hypervisor-fw \
--disk path=focal-server-cloudimg-amd64.raw \
--cpus boot=4 \
--memory size=1024M \
--net "tap=,mac=,ip=,mask="
$ popd
```
If access to the firmware messages or interaction with the boot loader (e.g.
GRUB) is required then it necessary to switch to the serial console instead of
`virtio-console`.
Multiple arguments can be given to the `--disk` parameter.
```shell
$ ./cloud-hypervisor \
--kernel ./hypervisor-fw \
--disk path=focal-server-cloudimg-amd64.raw path=/tmp/ubuntu-cloudinit.img \
--cpus boot=4 \
--memory size=1024M \
--net "tap=,mac=,ip=,mask=" \
--serial tty \
--console off
```
### Custom kernel and disk image
## Booting: `--firmware` vs `--kernel`
#### Building your kernel
The following scenarios are supported by Cloud Hypervisor to bootstrap a VM, i.e.,
to load a payload/bootitem(s):
- Provide firmware
- Provide kernel \[+ cmdline\]\ [+ initrd\]
Please note that our Cloud Hypervisor firmware (`hypervisor-fw`) has a Xen PVH
boot entry, therefore it can also be booted via the `--kernel` parameter, as
seen in some examples.
### Custom Kernel and Disk Image
#### Building your Kernel
Cloud Hypervisor also supports direct kernel boot. For x86-64, a `vmlinux` ELF kernel (compiled with PVH support) or a regular bzImage are supported. In order to support development there is a custom branch; however provided the required options are enabled any recent kernel will suffice.
Cloud Hypervisor also supports direct kernel boot into a `vmlinux` ELF kernel. In order to support virtio-iommu we have our own development branch. You are of course able to use your own kernel but these instructions will continue with the version that we develop and test against.
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
$ pushd $CLOUDH
$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-5.15.12 linux-cloud-hypervisor
$ pushd linux-cloud-hypervisor
$ make ch_defconfig
# Do native build of the x86-64 kernel
# Use the cloud-hypervisor kernel config to build your kernel
$ cp $CLOUDH/cloud-hypervisor/resources/linux-config-x86_64 .config
$ KCFLAGS="-Wa,-mx86-used-note=no" make bzImage -j `nproc`
# Do native build of the AArch64 kernel
$ make -j `nproc`
$ popd
```
For x86-64, the `vmlinux` kernel image will then be located at
`linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin`.
For AArch64, the `Image` kernel image will then be located at
`linux-cloud-hypervisor/arch/arm64/boot/Image`.
The `vmlinux` kernel image will then be located at `linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin`.
#### Disk image
For the disk image the same Ubuntu image as before can be used. This contains
an `ext4` root filesystem.
For the disk image, we will use a Ubuntu cloud image that contains a root partition:
```shell
$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img # x86-64
$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img # AArch64
$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw # x86-64
$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-arm64.img focal-server-cloudimg-arm64.raw # AArch64
$ pushd $CLOUDH
$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img
$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw
$ popd
```
#### Booting the guest VM
These sample commands boot the disk image using the custom kernel whilst also
supplying the desired kernel command line.
- x86-64
Now we can directly boot into our custom kernel and make it use the Ubuntu root partition.
If we want to have 4 vCPUs and 1024 MBytes of memory:
```shell
$ sudo setcap cap_net_admin+ep ./cloud-hypervisor
$ ./create-cloud-init.sh
$ ./cloud-hypervisor \
$ pushd $CLOUDH
$ sudo setcap cap_net_admin+ep ./cloud-hypervisor/target/release/cloud-hypervisor
$ ./cloud-hypervisor/target/release/cloud-hypervisor \
--kernel ./linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin \
--disk path=focal-server-cloudimg-amd64.raw path=/tmp/ubuntu-cloudinit.img \
--disk path=focal-server-cloudimg-amd64.raw \
--cmdline "console=hvc0 root=/dev/vda1 rw" \
--cpus boot=4 \
--memory size=1024M \
--net "tap=,mac=,ip=,mask="
```
- AArch64
The above example use the `virtio-console` device as the guest console, and this
device may not be enabled soon enough by the guest kernel to get early kernel
debug messages.
When in need for earlier debug messages, using the legacy serial device based
console is preferred:
```shell
$ sudo setcap cap_net_admin+ep ./cloud-hypervisor
$ ./create-cloud-init.sh
$ ./cloud-hypervisor \
--kernel ./linux-cloud-hypervisor/arch/arm64/boot/Image \
--disk path=focal-server-cloudimg-arm64.raw path=/tmp/ubuntu-cloudinit.img \
--cmdline "console=hvc0 root=/dev/vda1 rw" \
--cpus boot=4 \
--memory size=1024M \
--net "tap=,mac=,ip=,mask="
```
If earlier kernel messages are required the serial console should be used instead of `virtio-console`.
- x86-64
```shell
$ ./cloud-hypervisor \
$ ./cloud-hypervisor/target/release/cloud-hypervisor \
--kernel ./linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin \
--console off \
--serial tty \
@@ -279,130 +244,102 @@ $ ./cloud-hypervisor \
--net "tap=,mac=,ip=,mask="
```
- AArch64
```shell
$ ./cloud-hypervisor \
--kernel ./linux-cloud-hypervisor/arch/arm64/boot/Image \
--console off \
--serial tty \
--disk path=focal-server-cloudimg-arm64.raw \
--cmdline "console=ttyAMA0 root=/dev/vda1 rw" \
--cpus boot=4 \
--memory size=1024M \
--net "tap=,mac=,ip=,mask="
```
# 3. Status
Cloud Hypervisor is under active development. The following stability
guarantees are currently made:
Cloud Hypervisor is under active development. The following stability guarantees are currently made:
* The API (including command line options) will not be removed or changed in a
breaking way without a minimum of 2 major releases notice. Where possible
warnings will be given about the use of deprecated functionality and the
deprecations will be documented in the release notes.
breaking way without a minimum of 2 releases notice. Where possible warnings
will be given about the use of deprecated functionality and the deprecations
will be documented in the release notes.
* Point releases will be made between individual releases where there are
substantial bug fixes or security issues that need to be fixed. These point
releases will only include bug fixes.
substantial bug fixes or security issues that need to be fixed.
Currently the following items are **not** guaranteed across updates:
* Snapshot/restore is not supported across different versions
* Live migration is not supported across different versions
* The following features are considered experimental and may change
substantially between releases: TDX, vfio-user, vDPA.
substantially between releases: TDX, SGX.
Further details can be found in the [release documentation](docs/releases.md).
As of 2023-01-03, the following cloud images are supported:
As of 2021-04-29, the following cloud images are supported:
- [Ubuntu Focal](https://cloud-images.ubuntu.com/focal/current/) (focal-server-cloudimg-{amd64,arm64}.img)
- [Ubuntu Jammy](https://cloud-images.ubuntu.com/jammy/current/) (jammy-server-cloudimg-{amd64,arm64}.img)
- [Ubuntu Noble](https://cloud-images.ubuntu.com/noble/current/) (noble-server-cloudimg-{amd64,arm64}.img)
- [Fedora 36](https://archives.fedoraproject.org/pub/archive/fedora/linux/releases/36/Cloud/) ([Fedora-Cloud-Base-36-1.5.x86_64.raw.xz](https://archives.fedoraproject.org/pub/archive/fedora/linux/releases/36/Cloud/x86_64/images/) / [Fedora-Cloud-Base-36-1.5.aarch64.raw.xz](https://archives.fedoraproject.org/pub/archive/fedora/linux/releases/36/Cloud/aarch64/images/))
- [Ubuntu Bionic](https://cloud-images.ubuntu.com/bionic/current/) (cloudimg)
- [Ubuntu Focal](https://cloud-images.ubuntu.com/focal/current/) (cloudimg)
- [Ubuntu Groovy](https://cloud-images.ubuntu.com/groovy/current/) (cloudimg)
- [Ubuntu Hirsute](https://cloud-images.ubuntu.com/hirsute/current/) (cloudimg)
Direct kernel boot to userspace should work with a rootfs from most
distributions although you may need to enable exotic filesystem types in the
reference kernel configuration (e.g. XFS or btrfs.)
Direct kernel boot to userspace should work with a rootfs from most distributions.
## Hot Plug
Cloud Hypervisor supports hotplug of CPUs, passthrough devices (VFIO),
`virtio-{net,block,pmem,fs,vsock}` and memory resizing. This
[document](docs/hotplug.md) details how to add devices to a running VM.
Cloud Hypervisor supports hotplug of CPUs, passthrough devices (VFIO), `virtio-{net,block,pmem,fs,vsock}` and memory resizing. This [document](docs/hotplug.md) details how to add devices to
a running VM.
## Device Model
Details of the device model can be found in this
[documentation](docs/device_model.md).
Details of the device model can be found in this [documentation](docs/device_model.md).
## Roadmap
## TODO
The project roadmap is tracked through a [GitHub
project](https://github.com/orgs/cloud-hypervisor/projects/6).
We are not tracking the Cloud Hypervisor TODO list from a specific git tracked file but through
[github issues](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new) instead.
# 4. Relationship with _Rust VMM_ Project
# 4. `rust-vmm` project dependency
In order to satisfy the design goal of having a high-performance,
security-focused hypervisor the decision was made to use the
[Rust](https://www.rust-lang.org/) programming language. The language's strong
focus on memory and thread safety makes it an ideal candidate for implementing
VMMs.
In order to satisfy the design goal of having a high-performance, security-focused hypervisor the decision
was made to use the [Rust](https://www.rust-lang.org/) programming language.
The language's strong focus on memory and thread safety makes it an ideal candidate for implementing VMMs.
Instead of implementing the VMM components from scratch, Cloud Hypervisor is
importing the [Rust VMM](https://github.com/rust-vmm) crates, and sharing code
and architecture together with other VMMs like e.g. Amazon's
[Firecracker](https://firecracker-microvm.github.io/) and Google's
[crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/).
Instead of implementing the VMM components from scratch, Cloud Hypervisor is importing the [rust-vmm](https://github.com/rust-vmm)
crates, and sharing code and architecture together with other VMMs like e.g. Amazon's [Firecracker](https://firecracker-microvm.github.io/)
and Google's [crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/).
Cloud Hypervisor embraces the _Rust VMM_ project's goals, which is to be able
to share and re-use as many virtualization crates as possible.
Cloud Hypervisor embraces the rust-vmm project goals, which is to be able to share and re-use
as many virtualization crates as possible. As such, the Cloud Hypervisor relationship with the rust-vmm
project is twofold:
## Differences with Firecracker and crosvm
1. It will use as much of the rust-vmm code as possible. Any new rust-vmm crate that's relevant to the project
goals will be integrated as soon as possible.
2. As it is likely that the rust-vmm project will lack some of the features that Cloud Hypervisor needs (e.g. ACPI,
VFIO, vhost-user, etc), we will be using the Cloud Hypervisor VMM to implement and test them, and contribute them
back to the rust-vmm project.
A large part of the Cloud Hypervisor code is based on either the Firecracker or
the crosvm project's implementations. Both of these are VMMs written in Rust
with a focus on safety and security, like Cloud Hypervisor.
## Firecracker and crosvm
The goal of the Cloud Hypervisor project differs from the aforementioned
projects in that it aims to be a general purpose VMM for _Cloud Workloads_ and
not limited to container/serverless or client workloads.
A large part of the Cloud Hypervisor code is based on either the Firecracker or the crosvm projects implementations.
Both of these are VMMs written in Rust with a focus on safety and security, like Cloud Hypervisor.
The Cloud Hypervisor community thanks the communities of both the Firecracker
and crosvm projects for their excellent work.
However we want to emphasize that the Cloud Hypervisor project is neither a fork nor a reimplementation of any of those
projects. The goals and use cases we're trying to meet are different.
We're aiming at supporting cloud workloads, i.e. those modern, full Linux distribution images currently being run by
Cloud Service Provider (CSP) tenants.
Our primary target is not to support client or serverless use cases, and as such our code base already diverges
from the crosvm and Firecracker ones. As we add more features to support our use cases, we believe that the divergence
will increase while at the same time sharing as much of the fundamental virtualization code through the rust-vmm project
crates as possible.
# 5. Community
The Cloud Hypervisor project follows the governance, and community guidelines
described in the [Community](https://github.com/cloud-hypervisor/community)
repository.
The Cloud Hypervisor project follows the governance, and community guidelines described in
the [Community](https://github.com/cloud-hypervisor/community) repository.
## Contribute
The project strongly believes in building a global, diverse and collaborative
community around the Cloud Hypervisor project. Anyone who is interested in
[contributing](CONTRIBUTING.md) to the project is welcome to participate.
We are working on building a global, diverse and collaborative community around the Cloud Hypervisor project.
Anyone who is interested in [contributing](CONTRIBUTING.md) to the project is welcome to participate.
Contributing to a open source project like Cloud Hypervisor covers a lot more
than just sending code. Testing, documentation, pull request
reviews, bug reports, feature requests, project improvement suggestions, etc,
are all equal and welcome means of contribution. See the
[CONTRIBUTING](CONTRIBUTING.md) document for more details.
We believe that contributing to a open source project like Cloud Hypervisor covers a lot more than just sending
code. Testing, documentation, pull request reviews, bug reports, feature requests, project improvement suggestions,
etc, are all equal and welcome means of contribution. See the [CONTRIBUTING](CONTRIBUTING.md) document for more details.
## Slack
## Join us
Get an [invite to our Slack channel](https://join.slack.com/t/cloud-hypervisor/shared_invite/enQtNjY3MTE3MDkwNDQ4LWQ1MTA1ZDVmODkwMWQ1MTRhYzk4ZGNlN2UwNTI3ZmFlODU0OTcwOWZjMTkwZDExYWE3YjFmNzgzY2FmNDAyMjI),
[join us on Slack](https://cloud-hypervisor.slack.com/), and [participate in our community activities](https://cloud-hypervisor.slack.com/archives/C04R5DUQVBN).
## Mailing list
Please report bugs using the [GitHub issue
tracker](https://github.com/cloud-hypervisor/cloud-hypervisor/issues) but for
broader community discussions you may use our [mailing
list](https://lists.cloudhypervisor.org/g/dev/).
Get an [invite to our Slack channel](https://join.slack.com/t/cloud-hypervisor/shared_invite/enQtNjY3MTE3MDkwNDQ4LWQ1MTA1ZDVmODkwMWQ1MTRhYzk4ZGNlN2UwNTI3ZmFlODU0OTcwOWZjMTkwZDExYWE3YjFmNzgzY2FmNDAyMjI)
and [join us on Slack](https://cloud-hypervisor.slack.com/).
## Security issues
Please contact the maintainers listed in the MAINTAINERS.md file with security issues.
Please use the GitHub security advisories feature for reporting issues: https://github.com/cloud-hypervisor/cloud-hypervisor/security/advisories/new

8
acpi_tables/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "acpi_tables"
version = "0.1.0"
authors = ["The Cloud Hypervisor Authors"]
edition = "2018"
[dependencies]
vm-memory = "0.7.0"

1894
acpi_tables/src/aml.rs Normal file

File diff suppressed because it is too large Load Diff

12
acpi_tables/src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
pub mod aml;
pub mod rsdp;
pub mod sdt;
fn generate_checksum(data: &[u8]) -> u8 {
(255 - data.iter().fold(0u8, |acc, x| acc.wrapping_add(*x))).wrapping_add(1)
}

68
acpi_tables/src/rsdp.rs Normal file
View File

@@ -0,0 +1,68 @@
// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
use vm_memory::ByteValued;
#[repr(packed)]
#[derive(Clone, Copy, Default)]
pub struct Rsdp {
pub signature: [u8; 8],
pub checksum: u8,
pub oem_id: [u8; 6],
pub revision: u8,
_rsdt_addr: u32,
pub length: u32,
pub xsdt_addr: u64,
pub extended_checksum: u8,
_reserved: [u8; 3],
}
// SAFETY: Rsdp only contains a series of integers
unsafe impl ByteValued for Rsdp {}
impl Rsdp {
pub fn new(oem_id: [u8; 6], xsdt_addr: u64) -> Self {
let mut rsdp = Rsdp {
signature: *b"RSD PTR ",
checksum: 0,
oem_id,
revision: 2,
_rsdt_addr: 0,
length: std::mem::size_of::<Rsdp>() as u32,
xsdt_addr,
extended_checksum: 0,
_reserved: [0; 3],
};
rsdp.checksum = super::generate_checksum(&rsdp.as_slice()[0..19]);
rsdp.extended_checksum = super::generate_checksum(rsdp.as_slice());
rsdp
}
pub fn len() -> usize {
std::mem::size_of::<Rsdp>()
}
}
#[cfg(test)]
mod tests {
use super::Rsdp;
use vm_memory::bytes::ByteValued;
#[test]
fn test_rsdp() {
let rsdp = Rsdp::new(*b"CHYPER", 0xdead_beef);
let sum = rsdp
.as_slice()
.iter()
.fold(0u8, |acc, x| acc.wrapping_add(*x));
assert_eq!(sum, 0);
let sum: u8 = rsdp
.as_slice()
.iter()
.fold(0u8, |acc, x| acc.wrapping_add(*x));
assert_eq!(sum, 0);
}
}

146
acpi_tables/src/sdt.rs Normal file
View File

@@ -0,0 +1,146 @@
// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
#[repr(packed)]
pub struct GenericAddress {
pub address_space_id: u8,
pub register_bit_width: u8,
pub register_bit_offset: u8,
pub access_size: u8,
pub address: u64,
}
impl GenericAddress {
pub fn io_port_address<T>(address: u16) -> Self {
GenericAddress {
address_space_id: 1,
register_bit_width: 8 * std::mem::size_of::<T>() as u8,
register_bit_offset: 0,
access_size: std::mem::size_of::<T>() as u8,
address: u64::from(address),
}
}
pub fn mmio_address<T>(address: u64) -> Self {
GenericAddress {
address_space_id: 0,
register_bit_width: 8 * std::mem::size_of::<T>() as u8,
register_bit_offset: 0,
access_size: std::mem::size_of::<T>() as u8,
address,
}
}
}
pub struct Sdt {
data: Vec<u8>,
}
#[allow(clippy::len_without_is_empty)]
impl Sdt {
pub fn new(
signature: [u8; 4],
length: u32,
revision: u8,
oem_id: [u8; 6],
oem_table: [u8; 8],
oem_revision: u32,
) -> Self {
assert!(length >= 36);
let mut data = Vec::with_capacity(length as usize);
data.extend_from_slice(&signature);
data.extend_from_slice(&length.to_le_bytes());
data.push(revision);
data.push(0); // checksum
data.extend_from_slice(&oem_id);
data.extend_from_slice(&oem_table);
data.extend_from_slice(&oem_revision.to_le_bytes());
data.extend_from_slice(b"CLDH");
data.extend_from_slice(&0u32.to_le_bytes());
assert_eq!(data.len(), 36);
data.resize(length as usize, 0);
let mut sdt = Sdt { data };
sdt.update_checksum();
sdt
}
pub fn update_checksum(&mut self) {
self.data[9] = 0;
let checksum = super::generate_checksum(self.data.as_slice());
self.data[9] = checksum
}
pub fn as_slice(&self) -> &[u8] {
self.data.as_slice()
}
pub fn append<T>(&mut self, value: T) {
let orig_length = self.data.len();
let new_length = orig_length + std::mem::size_of::<T>();
self.data.resize(new_length, 0);
self.write_u32(4, new_length as u32);
self.write(orig_length, value);
}
pub fn append_slice(&mut self, data: &[u8]) {
let orig_length = self.data.len();
let new_length = orig_length + data.len();
self.write_u32(4, new_length as u32);
self.data.extend_from_slice(data);
self.update_checksum();
}
/// Write a value at the given offset
pub fn write<T>(&mut self, offset: usize, value: T) {
assert!((offset + (std::mem::size_of::<T>() - 1)) < self.data.len());
unsafe {
*(((self.data.as_mut_ptr() as usize) + offset) as *mut T) = value;
}
self.update_checksum();
}
pub fn write_u8(&mut self, offset: usize, val: u8) {
self.write(offset, val);
}
pub fn write_u16(&mut self, offset: usize, val: u16) {
self.write(offset, val);
}
pub fn write_u32(&mut self, offset: usize, val: u32) {
self.write(offset, val);
}
pub fn write_u64(&mut self, offset: usize, val: u64) {
self.write(offset, val);
}
pub fn len(&self) -> usize {
self.data.len()
}
}
#[cfg(test)]
mod tests {
use super::Sdt;
#[test]
fn test_sdt() {
let mut sdt = Sdt::new(*b"TEST", 40, 1, *b"CLOUDH", *b"TESTTEST", 1);
let sum: u8 = sdt
.as_slice()
.iter()
.fold(0u8, |acc, x| acc.wrapping_add(*x));
assert_eq!(sum, 0);
sdt.write_u32(36, 0x12345678);
let sum: u8 = sdt
.as_slice()
.iter()
.fold(0u8, |acc, x| acc.wrapping_add(*x));
assert_eq!(sum, 0);
}
}

View File

@@ -1,14 +1,8 @@
[package]
authors = ["The Cloud Hypervisor Authors"]
edition.workspace = true
license = "Apache-2.0"
name = "api_client"
rust-version.workspace = true
version = "0.1.0"
authors = ["The Cloud Hypervisor Authors"]
edition = "2018"
[dependencies]
thiserror = { workspace = true }
vmm-sys-util = { workspace = true }
[lints]
workspace = true
vmm-sys-util = "0.9.0"

View File

@@ -3,34 +3,39 @@
// SPDX-License-Identifier: Apache-2.0
//
use std::io::{self, Read, Write};
use std::fmt;
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)]
#[derive(Debug)]
pub enum Error {
#[error("Error writing to or reading from HTTP socket")]
Socket(#[source] io::Error),
#[error("Error sending file descriptors")]
SocketSendFds(#[source] errno::Error),
#[error("Error parsing HTTP status code")]
StatusCodeParsing(#[source] num::ParseIntError),
#[error("HTTP output is missing protocol statement")]
Socket(std::io::Error),
SocketSendFds(vmm_sys_util::errno::Error),
StatusCodeParsing(std::num::ParseIntError),
MissingProtocol,
#[error("Error parsing HTTP Content-Length field")]
ContentLengthParsing(#[source] num::ParseIntError),
#[error("Server responded with error {0:?}: {1:?}")]
ServerResponse(
StatusCode,
// TODO: Move `api` module from `vmm` to dedicated crate and use a common type definition
Option<
String, /* Untyped: Currently Vec<String> of error messages from top to root cause */
>,
),
ContentLengthParsing(std::num::ParseIntError),
ServerResponse(StatusCode, Option<String>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Error::*;
match self {
Socket(e) => write!(f, "Error writing to or reading from HTTP socket: {}", e),
SocketSendFds(e) => write!(f, "Error writing to or reading from HTTP socket: {}", e),
StatusCodeParsing(e) => write!(f, "Error parsing HTTP status code: {}", e),
MissingProtocol => write!(f, "HTTP output is missing protocol statement"),
ContentLengthParsing(e) => write!(f, "Error parsing HTTP Content-Length field: {}", e),
ServerResponse(s, o) => {
if let Some(o) = o {
write!(f, "Server responded with an error: {:?}: {}", s, o)
} else {
write!(f, "Server responded with an error: {:?}", s)
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
@@ -40,7 +45,6 @@ pub enum StatusCode {
NoContent,
BadRequest,
NotFound,
TooManyRequests,
InternalServerError,
NotImplemented,
Unknown,
@@ -54,7 +58,6 @@ impl StatusCode {
204 => StatusCode::NoContent,
400 => StatusCode::BadRequest,
404 => StatusCode::NotFound,
429 => StatusCode::TooManyRequests,
500 => StatusCode::InternalServerError,
501 => StatusCode::NotImplemented,
_ => StatusCode::Unknown,
@@ -76,7 +79,7 @@ impl StatusCode {
}
fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> {
let header_str = format!("{header}: ");
let header_str = format!("{}: ", header);
res.find(&header_str)
.map(|o| &res[o + header_str.len()..o + res[o..].find('\r').unwrap()])
}
@@ -98,11 +101,7 @@ fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
loop {
let mut bytes = vec![0; 256];
let count = socket.read(&mut bytes).map_err(Error::Socket)?;
// If the return value is 0, the peer has performed an orderly shutdown.
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") {
@@ -120,14 +119,15 @@ fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
}
}
if let Some(body_offset) = body_offset
&& let Some(content_length) = content_length
&& res.len() >= content_length + body_offset
{
break;
if let Some(body_offset) = body_offset {
if let Some(content_length) = content_length {
if res.len() >= content_length + body_offset {
break;
}
}
}
}
let body_string = content_length.and(body_offset.map(|o| String::from(&res[o..])));
let body_string = content_length.and(Some(String::from(&res[body_offset.unwrap()..])));
let status_code = get_status_code(&res)?;
if status_code.is_server_error() {
@@ -137,22 +137,21 @@ fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
}
}
/// Make an API request using the fully qualified command name.
/// For example, full_command could be "vm.create" or "vmm.ping".
pub fn simple_api_full_command_with_fds_and_response<T: Read + Write + ScmSocket>(
pub fn simple_api_command_with_fds<T: Read + Write + ScmSocket>(
socket: &mut T,
method: &str,
full_command: &str,
c: &str,
request_body: Option<&str>,
request_fds: &[RawFd],
) -> Result<Option<String>, Error> {
request_fds: Vec<RawFd>,
) -> Result<(), Error> {
socket
.send_with_fds(
&[format!(
"{method} /api/v1/{full_command} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n"
"{} /api/v1/vm.{} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n",
method, c
)
.as_bytes()],
request_fds,
&request_fds,
)
.map_err(Error::SocketSendFds)?;
@@ -172,68 +171,17 @@ pub fn simple_api_full_command_with_fds_and_response<T: Read + Write + ScmSocket
socket.flush().map_err(Error::Socket)?;
parse_http_response(socket)
}
pub fn simple_api_full_command_with_fds<T: Read + Write + ScmSocket>(
socket: &mut T,
method: &str,
full_command: &str,
request_body: Option<&str>,
request_fds: &[RawFd],
) -> Result<(), Error> {
let response = simple_api_full_command_with_fds_and_response(
socket,
method,
full_command,
request_body,
request_fds,
)?;
if let Some(response) = response {
println!("{response}");
if let Some(body) = parse_http_response(socket)? {
println!("{}", body);
}
Ok(())
}
pub fn simple_api_full_command<T: Read + Write + ScmSocket>(
socket: &mut T,
method: &str,
full_command: &str,
request_body: Option<&str>,
) -> Result<(), Error> {
simple_api_full_command_with_fds(socket, method, full_command, request_body, &[])
}
pub fn simple_api_full_command_and_response<T: Read + Write + ScmSocket>(
socket: &mut T,
method: &str,
full_command: &str,
request_body: Option<&str>,
) -> Result<Option<String>, Error> {
simple_api_full_command_with_fds_and_response(socket, method, full_command, request_body, &[])
}
pub fn simple_api_command_with_fds<T: Read + Write + ScmSocket>(
socket: &mut T,
method: &str,
c: &str,
request_body: Option<&str>,
request_fds: &[RawFd],
) -> Result<(), Error> {
// Create the full VM command. For VMM commands, use
// simple_api_full_command().
let full_command = format!("vm.{c}");
simple_api_full_command_with_fds(socket, method, &full_command, request_body, request_fds)
}
pub fn simple_api_command<T: Read + Write + ScmSocket>(
socket: &mut T,
method: &str,
c: &str,
request_body: Option<&str>,
) -> Result<(), Error> {
simple_api_command_with_fds(socket, method, c, request_body, &[])
simple_api_command_with_fds(socket, method, c, request_body, Vec::new())
}

View File

@@ -1,37 +1,31 @@
[package]
authors = ["The Chromium OS Authors"]
edition.workspace = true
name = "arch"
rust-version.workspace = true
version = "0.1.0"
authors = ["The Chromium OS Authors"]
edition = "2018"
[features]
default = []
fw_cfg = []
kvm = ["hypervisor/kvm"]
sev_snp = []
acpi = ["acpi_tables"]
tdx = []
[dependencies]
anyhow = { workspace = true }
byteorder = { workspace = true }
acpi_tables = { path = "../acpi_tables", optional = true }
anyhow = "1.0.52"
byteorder = "1.4.3"
hypervisor = { path = "../hypervisor" }
libc = { workspace = true }
linux-loader = { workspace = true, features = ["bzimage", "elf", "pe"] }
log = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
thiserror = { workspace = true }
uuid = { workspace = true }
vm-memory = { workspace = true, features = ["backend-bitmap", "backend-mmap"] }
vmm-sys-util = { workspace = true, features = ["with-serde"] }
libc = "0.2.112"
linux-loader = { version = "0.4.0", features = ["elf", "bzimage", "pe"] }
log = "0.4.14"
serde = { version = "1.0.133", features = ["rc"] }
serde_derive = "1.0.133"
thiserror = "1.0.30"
versionize = "0.1.6"
versionize_derive = "0.1.4"
vm-memory = { version = "0.7.0", features = ["backend-mmap", "backend-bitmap"] }
vm-migration = { path = "../vm-migration" }
vmm-sys-util = { version = "0.9.0", 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 }
[lints]
workspace = true
[target.'cfg(target_arch = "aarch64")'.dependencies]
fdt_parser = { version = "0.1.3", package = 'fdt'}
vm-fdt = { git = "https://github.com/rust-vmm/vm-fdt", branch = "main" }

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

@@ -6,33 +6,26 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the THIRD-PARTY file.
use crate::{NumaNodes, PciSpaceInfo};
use byteorder::{BigEndian, ByteOrder};
use std::cmp;
use std::collections::HashMap;
use std::ffi::CStr;
use std::fmt::Debug;
use std::hash::BuildHasher;
use std::sync::{Arc, Mutex};
use std::{cmp, result, str};
use std::result;
use std::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 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::super::DeviceType;
use super::super::GuestMemoryMmap;
use super::super::InitramfsConfig;
use super::get_fdt_addr;
use super::gic::GicDevice;
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,
PCI_MMIO_CONFIG_SIZE_PER_SEGMENT,
IRQ_BASE, MEM_32BIT_DEVICES_SIZE, MEM_32BIT_DEVICES_START, MEM_PCI_IO_SIZE, MEM_PCI_IO_START,
PCI_HIGH_BASE, PCI_MMIO_CONFIG_SIZE_PER_SEGMENT,
};
use crate::{NumaNodes, PciSpaceInfo};
use vm_fdt::{FdtWriter, FdtWriterResult};
use vm_memory::{Address, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryRegion};
// This is a value for uniquely identifying the FDT node declaring the interrupt controller.
const GIC_PHANDLE: u32 = 1;
@@ -47,12 +40,8 @@ const VIRTIO_IOMMU_PHANDLE: u32 = 5;
// NOTE: Keep FIRST_VCPU_PHANDLE the last PHANDLE defined.
// This is a value for uniquely identifying the FDT node containing the first vCPU.
// The last number of vCPU phandle depends on the number of vCPUs.
const FIRST_VCPU_PHANDLE: u32 = 8;
const FIRST_VCPU_PHANDLE: u32 = 6;
// This is a value for uniquely identifying the FDT node containing the L2 cache info
const L2_CACHE_PHANDLE: u32 = 6;
// This is a value for uniquely identifying the FDT node containing the L3 cache info
const L3_CACHE_PHANDLE: u32 = 7;
// Read the documentation specified when appending the root node to the FDT.
const ADDRESS_CELLS: u32 = 0x2;
const SIZE_CELLS: u32 = 0x2;
@@ -82,35 +71,33 @@ pub trait DeviceInfoForFdt {
}
/// Errors thrown while configuring the Flattened Device Tree for aarch64.
#[derive(Debug, Error)]
#[derive(Debug)]
pub enum Error {
/// Failure in writing FDT in memory.
#[error("Failure in writing FDT in memory")]
WriteFdtToMemory(#[source] GuestMemoryError),
WriteFdtToMemory(GuestMemoryError),
}
type Result<T> = result::Result<T, Error>;
/// 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],
vcpu_topology: Option<(u16, u16, u16, u16)>,
vcpu_mpidr: Vec<u64>,
vcpu_topology: Option<(u8, u8, u8)>,
device_info: &HashMap<(DeviceType, String), T, S>,
gic_device: &Arc<Mutex<dyn Vgic>>,
gic_device: &dyn GicDevice,
initrd: &Option<InitramfsConfig>,
pci_space_info: &[PciSpaceInfo],
numa_nodes: &NumaNodes,
virtio_iommu_bdf: Option<u32>,
pmu_supported: bool,
) -> FdtWriterResult<Vec<u8>> {
// Allocate stuff necessary for the holding the blob.
let mut fdt = FdtWriter::new().unwrap();
// For an explanation why these nodes were introduced in the blob take a look at
// the "Device Node Requirements" chapter of the Devicetree Specification.
// https://www.devicetree.org/specifications/
// https://github.com/torvalds/linux/blob/master/Documentation/devicetree/booting-without-of.txt#L845
// Look for "Required nodes and properties".
// Header or the root node as per above mentioned documentation.
let root_node = fdt.begin_node("")?;
@@ -122,14 +109,11 @@ pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
// This is not mandatory but we use it to point the root node to the node
// containing description of the interrupt controller for this VM.
fdt.property_u32("interrupt-parent", GIC_PHANDLE)?;
create_cpu_nodes(&mut fdt, vcpu_mpidr, vcpu_topology, numa_nodes)?;
create_cpu_nodes(&mut fdt, &vcpu_mpidr, vcpu_topology, numa_nodes)?;
create_memory_node(&mut fdt, guest_mem, numa_nodes)?;
create_chosen_node(&mut fdt, cmdline, initrd)?;
create_gic_node(&mut fdt, gic_device)?;
create_timer_node(&mut fdt)?;
if pmu_supported {
create_pmu_node(&mut fdt)?;
}
create_clock_node(&mut fdt)?;
create_psci_node(&mut fdt)?;
create_devices_node(&mut fdt, device_info)?;
@@ -146,10 +130,11 @@ pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
Ok(fdt_final)
}
pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Result<()> {
pub fn write_fdt_to_memory(fdt_final: Vec<u8>, guest_mem: &GuestMemoryMmap) -> Result<()> {
// Write FDT to memory.
let fdt_address = GuestAddress(get_fdt_addr());
guest_mem
.write_slice(fdt_final, super::layout::FDT_START)
.write_slice(fdt_final.as_slice(), fdt_address)
.map_err(Error::WriteFdtToMemory)?;
Ok(())
}
@@ -158,7 +143,7 @@ pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Res
fn create_cpu_nodes(
fdt: &mut FdtWriter,
vcpu_mpidr: &[u64],
vcpu_topology: Option<(u16, u16, u16, u16)>,
vcpu_topology: Option<(u8, u8, u8)>,
numa_nodes: &NumaNodes,
) -> FdtWriterResult<()> {
// See https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/arm/cpus.yaml.
@@ -167,45 +152,12 @@ fn create_cpu_nodes(
fdt.property_u32("#size-cells", 0x0)?;
let num_cpus = vcpu_mpidr.len();
let (threads_per_core, cores_per_die, dies_per_package, packages) =
vcpu_topology.unwrap_or((1, 1, 1, 1));
let cores_per_package = cores_per_die * dies_per_package;
let max_cpus: u32 =
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();
// Arm boot protocol requires a minimal Device Tree
// https://docs.kernel.org/arch/arm64/booting.html
// As Generic initiators are supported only in ACPI
// When a guest kernel does not boot under "acpi=force" mode it can
// hang due to conflicting numa information present in FDT which
// does not support Generic Initiators
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
if has_generic_initiator {
info!("Skipping NUMA CPU node encoding in FDT with Generic Initiator devices");
}
let threads_per_core = vcpu_topology.unwrap_or_default().0 as u8;
let cores_per_package = vcpu_topology.unwrap_or_default().1 as u8;
let packages = vcpu_topology.unwrap_or_default().2 as u8;
for (cpu_id, mpidr) in vcpu_mpidr.iter().enumerate().take(num_cpus) {
let cpu_name = format!("cpu@{cpu_id:x}");
let cpu_name = format!("cpu@{:x}", cpu_id);
let cpu_node = fdt.begin_node(&cpu_name)?;
fdt.property_string("device_type", "cpu")?;
fdt.property_string("compatible", "arm,arm-v8")?;
@@ -218,132 +170,76 @@ fn create_cpu_nodes(
fdt.property_u32("reg", (mpidr & 0x7FFFFF) as u32)?;
fdt.property_u32("phandle", cpu_id as u32 + FIRST_VCPU_PHANDLE)?;
// Skipping NUMA encoding in FDT when Generic Initiator devices
// are present allowed such guest kernels to boot properly and
// rely solely on ACPI tables to setup NUMA
if numa_nodes.len() > 1 && !has_generic_initiator {
// Add `numa-node-id` property if there is any numa config.
if numa_nodes.len() > 1 {
for numa_node_idx in 0..numa_nodes.len() {
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
if numa_node.unwrap().cpus.contains(&(cpu_id as u32)) {
if numa_node.unwrap().cpus.contains(&(cpu_id as u8)) {
fdt.property_u32("numa-node-id", numa_node_idx as u32)?;
}
}
}
if cache_exist && l1_d_cache_size != 0 && l1_i_cache_size != 0 {
// Add cache info.
fdt.property_u32("d-cache-size", l1_d_cache_size)?;
fdt.property_u32("d-cache-line-size", l1_d_cache_line_size)?;
fdt.property_u32("d-cache-sets", l1_d_cache_sets)?;
fdt.property_u32("i-cache-size", l1_i_cache_size)?;
fdt.property_u32("i-cache-line-size", l1_i_cache_line_size)?;
fdt.property_u32("i-cache-sets", l1_i_cache_sets)?;
if l2_cache_size != 0 && !l2_cache_shared {
fdt.property_u32(
"next-level-cache",
cpu_id as u32 + max_cpus + FIRST_VCPU_PHANDLE + L2_CACHE_PHANDLE,
)?;
let l2_cache_name = "l2-cache0";
let l2_cache_node = fdt.begin_node(l2_cache_name)?;
// PHANDLE is used to mark device node, and PHANDLE is unique. To avoid phandle
// conflicts with other device nodes, consider the previous CPU PHANDLE, so the
// CPU L2 cache PHANDLE must start from the largest CPU PHANDLE plus 1.
fdt.property_u32(
"phandle",
cpu_id as u32 + max_cpus + FIRST_VCPU_PHANDLE + L2_CACHE_PHANDLE,
)?;
fdt.property_string("compatible", "cache")?;
fdt.property_u32("cache-size", l2_cache_size)?;
fdt.property_u32("cache-line-size", l2_cache_line_size)?;
fdt.property_u32("cache-sets", l2_cache_sets)?;
fdt.property_u32("cache-level", 2)?;
if l3_cache_size != 0 && l3_cache_shared {
let package_id: u32 = cpu_id as u32 / cores_per_package as u32;
fdt.property_u32(
"next-level-cache",
package_id
+ num_cpus as u32
+ max_cpus
+ FIRST_VCPU_PHANDLE
+ L2_CACHE_PHANDLE
+ L3_CACHE_PHANDLE,
)?;
}
fdt.end_node(l2_cache_node)?;
}
}
fdt.end_node(cpu_node)?;
}
if cache_exist && l3_cache_size != 0 && !l2_cache_shared && l3_cache_shared {
let mut i: u32 = 0;
while i < packages.into() {
let l3_cache_name = "l3-cache0";
let l3_cache_node = fdt.begin_node(l3_cache_name)?;
// ARM L3 cache is generally shared within the package (socket), so the
// L3 cache node pointed to by the CPU in the package has the same L3
// cache PHANDLE. The L3 cache phandle must start from the largest L2
// cache PHANDLE plus 1 to avoid duplication.
fdt.property_u32(
"phandle",
i + num_cpus as u32
+ max_cpus
+ FIRST_VCPU_PHANDLE
+ L2_CACHE_PHANDLE
+ L3_CACHE_PHANDLE,
)?;
fdt.property_string("compatible", "cache")?;
fdt.property_null("cache-unified")?;
fdt.property_u32("cache-size", l3_cache_size)?;
fdt.property_u32("cache-line-size", l3_cache_line_size)?;
fdt.property_u32("cache-sets", l3_cache_sets)?;
fdt.property_u32("cache-level", 3)?;
fdt.end_node(l3_cache_node)?;
i += 1;
}
}
if let Some(topology) = vcpu_topology {
let (threads_per_core, cores_per_die, dies_per_package, packages) = topology;
let cores_per_package = cores_per_die * dies_per_package;
// If there is a valid cpu topology config, create the cpu-map node.
if (threads_per_core > 0)
&& (cores_per_package > 0)
&& (packages > 0)
&& (num_cpus as u8 == threads_per_core * cores_per_package * packages)
{
let cpu_map_node = fdt.begin_node("cpu-map")?;
// Create mappings between CPU index and cluster,core, and thread.
let mut cluster_core_thread_to_cpuidx = HashMap::new();
for cpu_idx in 0..num_cpus as u8 {
if threads_per_core > 1 {
cluster_core_thread_to_cpuidx.insert(
(
cpu_idx / (cores_per_package * threads_per_core),
(cpu_idx / threads_per_core) % cores_per_package,
cpu_idx % threads_per_core,
),
cpu_idx,
);
} else {
cluster_core_thread_to_cpuidx.insert(
(cpu_idx / cores_per_package, cpu_idx % cores_per_package, 0),
cpu_idx,
);
}
}
// Create device tree nodes with regard of above mapping.
for package_idx in 0..packages {
let package_name = format!("socket{package_idx:x}");
let package_node = fdt.begin_node(&package_name)?;
// Cluster is the container of cores, and it is mandatory in the CPU topology.
// Add a default "cluster0" in each socket/package.
let cluster_node = fdt.begin_node("cluster0")?;
for cluster_idx in 0..packages {
let cluster_name = format!("cluster{:x}", cluster_idx);
let cluster_node = fdt.begin_node(&cluster_name)?;
for core_idx in 0..cores_per_package {
let core_name = format!("core{core_idx:x}");
let core_name = format!("core{:x}", core_idx);
let core_node = fdt.begin_node(&core_name)?;
for thread_idx in 0..threads_per_core {
let thread_name = format!("thread{thread_idx:x}");
let thread_node = fdt.begin_node(&thread_name)?;
let cpu_idx = threads_per_core * cores_per_package * package_idx
+ threads_per_core * core_idx
+ thread_idx;
fdt.property_u32("cpu", cpu_idx as u32 + FIRST_VCPU_PHANDLE)?;
fdt.end_node(thread_node)?;
}
if threads_per_core > 1 {
for thread_idx in 0..threads_per_core {
let thread_name = format!("thread{:x}", thread_idx);
let thread_node = fdt.begin_node(&thread_name)?;
let cpu_idx = cluster_core_thread_to_cpuidx
.get(&(cluster_idx, core_idx, thread_idx))
.unwrap();
fdt.property_u32("cpu", *cpu_idx as u32 + FIRST_VCPU_PHANDLE)?;
fdt.end_node(thread_node)?;
}
} else {
let cpu_idx = cluster_core_thread_to_cpuidx
.get(&(cluster_idx, core_idx, 0))
.unwrap();
fdt.property_u32("cpu", *cpu_idx as u32 + FIRST_VCPU_PHANDLE)?;
}
fdt.end_node(core_node)?;
}
fdt.end_node(cluster_node)?;
fdt.end_node(package_node)?;
}
fdt.end_node(cpu_map_node)?;
} else {
@@ -360,35 +256,23 @@ fn create_memory_node(
guest_mem: &GuestMemoryMmap,
numa_nodes: &NumaNodes,
) -> FdtWriterResult<()> {
// See https://github.com/torvalds/linux/blob/58ae0b51506802713aa0e9956d1853ba4c722c98/Documentation/devicetree/bindings/numa.txt
// for NUMA setting in memory node.
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
if has_generic_initiator {
info!("Skipping NUMA memory node encoding in FDT with Generic Initiator devices");
}
// Skipping NUMA encoding in FDT when Generic Initiator devices
// are present allowed guest kernels to boot and
// rely solely on ACPI tables to setup NUMA
if numa_nodes.len() > 1 && !has_generic_initiator {
if numa_nodes.len() > 1 {
for numa_node_idx in 0..numa_nodes.len() {
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
let mut mem_reg_prop: Vec<u64> = Vec::new();
let mut node_memory_addr: u64 = 0;
// Each memory zone of numa will have its own memory node, but
// different numa nodes should not share same memory zones.
for memory_region in numa_node.unwrap().memory_regions.iter() {
let memory_region_start_addr: u64 = memory_region.start_addr().raw_value();
let memory_region_size: u64 = memory_region.size() as u64;
mem_reg_prop.push(memory_region_start_addr);
mem_reg_prop.push(memory_region_size);
// Set the node address the first non-zero region address
if node_memory_addr == 0 {
node_memory_addr = memory_region_start_addr;
let mem_reg_prop = [memory_region_start_addr, memory_region_size];
// With feature `acpi` enabled, RAM at 0-4M is for edk2 only
// and should be hidden to the guest.
#[cfg(feature = "acpi")]
if memory_region_start_addr == 0 {
continue;
}
}
// Only create a memory node if this NUMA node has memory regions
if !mem_reg_prop.is_empty() {
let memory_node_name = format!("memory@{node_memory_addr:x}");
let memory_node_name = format!("memory@{:x}", memory_region_start_addr);
let memory_node = fdt.begin_node(&memory_node_name)?;
fdt.property_string("device_type", "memory")?;
fdt.property_array_u64("reg", &mem_reg_prop)?;
@@ -397,89 +281,15 @@ fn create_memory_node(
}
}
} else {
// Note: memory regions from "GuestMemory" are sorted and non-zero sized.
let ram_regions = {
let mut ram_regions = Vec::new();
let mut current_start = guest_mem
.iter()
.next()
.map(GuestMemoryRegion::start_addr)
.expect("GuestMemory must have one memory region at least")
.raw_value();
let mut current_end = current_start;
let mem_size = guest_mem.last_addr().raw_value() - super::layout::RAM_64BIT_START + 1;
// See https://github.com/torvalds/linux/blob/master/Documentation/devicetree/booting-without-of.txt#L960
// for an explanation of this.
let mem_reg_prop = [super::layout::RAM_64BIT_START as u64, mem_size as u64];
let memory_node = fdt.begin_node("memory")?;
for (start, size) in guest_mem
.iter()
.map(|m| (m.start_addr().raw_value(), m.len()))
{
if current_end == start {
// This zone is continuous with the previous one.
current_end += size;
} else {
ram_regions.push((current_start, current_end));
current_start = start;
current_end = start + size;
}
}
ram_regions.push((current_start, current_end));
ram_regions
};
if ram_regions.len() > 2 {
panic!(
"There should be up to two non-continuous regions, divided by the
gap at the end of 32bit address space."
);
}
// Create the memory node for memory region before the gap
{
let (first_region_start, first_region_end) = ram_regions
.first()
.expect("There should be at last one memory region");
let ram_start = super::layout::RAM_START.raw_value();
let mem_32bit_reserved_start = super::layout::MEM_32BIT_RESERVED_START.raw_value();
if !((first_region_start <= &ram_start)
&& (first_region_end > &ram_start)
&& (first_region_end <= &mem_32bit_reserved_start))
{
panic!(
"Unexpected first memory region layout: (start: 0x{first_region_start:08x}, end: 0x{first_region_end:08x}).
ram_start: 0x{ram_start:08x}, mem_32bit_reserved_start: 0x{mem_32bit_reserved_start:08x}"
);
}
let mem_size = first_region_end - ram_start;
let mem_reg_prop = [ram_start, mem_size];
let memory_node_name = format!("memory@{ram_start:x}");
let memory_node = fdt.begin_node(&memory_node_name)?;
fdt.property_string("device_type", "memory")?;
fdt.property_array_u64("reg", &mem_reg_prop)?;
fdt.end_node(memory_node)?;
}
// Create the memory map entry for memory region after the gap if any
if let Some((second_region_start, second_region_end)) = ram_regions.get(1) {
let ram_64bit_start = super::layout::RAM_64BIT_START.raw_value();
if second_region_start != &ram_64bit_start {
panic!(
"Unexpected second memory region layout: start: 0x{second_region_start:08x}, ram_64bit_start: 0x{ram_64bit_start:08x}"
);
}
let mem_size = second_region_end - ram_64bit_start;
let mem_reg_prop = [ram_64bit_start, mem_size];
let memory_node_name = format!("memory@{ram_64bit_start:x}");
let memory_node = fdt.begin_node(&memory_node_name)?;
fdt.property_string("device_type", "memory")?;
fdt.property_array_u64("reg", &mem_reg_prop)?;
fdt.end_node(memory_node)?;
}
fdt.property_string("device_type", "memory")?;
fdt.property_array_u64("reg", &mem_reg_prop)?;
fdt.end_node(memory_node)?;
}
Ok(())
@@ -494,7 +304,7 @@ fn create_chosen_node(
fdt.property_string("bootargs", cmdline)?;
if let Some(initrd_config) = initrd {
let initrd_start = initrd_config.address.raw_value();
let initrd_start = initrd_config.address.raw_value() as u64;
let initrd_end = initrd_config.address.raw_value() + initrd_config.size as u64;
fdt.property_u64("linux,initrd-start", initrd_start)?;
fdt.property_u64("linux,initrd-end", initrd_end)?;
@@ -505,18 +315,18 @@ fn create_chosen_node(
Ok(())
}
fn create_gic_node(fdt: &mut FdtWriter, gic_device: &Arc<Mutex<dyn Vgic>>) -> FdtWriterResult<()> {
let gic_reg_prop = gic_device.lock().unwrap().device_properties();
fn create_gic_node(fdt: &mut FdtWriter, gic_device: &dyn GicDevice) -> FdtWriterResult<()> {
let gic_reg_prop = gic_device.device_properties();
let intc_node = fdt.begin_node("intc")?;
fdt.property_string("compatible", gic_device.lock().unwrap().fdt_compatibility())?;
fdt.property_string("compatible", gic_device.fdt_compatibility())?;
fdt.property_null("interrupt-controller")?;
// "interrupt-cells" field specifies the number of cells needed to encode an
// interrupt source. The type shall be a <u32> and the value shall be 3 if no PPI affinity description
// is required.
fdt.property_u32("#interrupt-cells", 3)?;
fdt.property_array_u64("reg", &gic_reg_prop)?;
fdt.property_array_u64("reg", gic_reg_prop)?;
fdt.property_u32("phandle", GIC_PHANDLE)?;
fdt.property_u32("#address-cells", 2)?;
fdt.property_u32("#size-cells", 2)?;
@@ -524,26 +334,18 @@ fn create_gic_node(fdt: &mut FdtWriter, gic_device: &Arc<Mutex<dyn Vgic>>) -> Fd
let gic_intr_prop = [
GIC_FDT_IRQ_TYPE_PPI,
gic_device.lock().unwrap().fdt_maint_irq(),
gic_device.fdt_maint_irq(),
IRQ_TYPE_LEVEL_HI,
];
fdt.property_array_u32("interrupts", &gic_intr_prop)?;
if gic_device.lock().unwrap().msi_compatible() {
if gic_device.msi_compatible() {
let msic_node = fdt.begin_node("msic")?;
let msi_compatibility = gic_device.lock().unwrap().msi_compatibility().to_string();
fdt.property_string("compatible", msi_compatibility.as_str())?;
fdt.property_string("compatible", gic_device.msi_compatibility())?;
fdt.property_null("msi-controller")?;
fdt.property_u32("phandle", MSI_PHANDLE)?;
let msi_reg_prop = gic_device.lock().unwrap().msi_properties();
fdt.property_array_u64("reg", &msi_reg_prop)?;
if msi_compatibility == GIC_V2M_COMPATIBLE {
fdt.property_u32("arm,msi-base-spi", GICV2M_SPI_BASE)?;
fdt.property_u32("arm,msi-num-spis", GICV2M_SPI_NUM)?;
}
let msi_reg_prop = gic_device.msi_properties();
fdt.property_array_u64("reg", msi_reg_prop)?;
fdt.end_node(msic_node)?;
}
@@ -570,14 +372,9 @@ fn create_clock_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> {
fn create_timer_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> {
// See
// https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/timer/arm%2Carch_timer.yaml
// https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/interrupt-controller/arch_timer.txt
// These are fixed interrupt numbers for the timer device.
let irqs = [
AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ,
AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ,
AARCH64_ARCH_TIMER_VIRT_IRQ,
AARCH64_ARCH_TIMER_HYP_IRQ,
];
let irqs = [13, 14, 11, 10];
let compatible = "arm,armv8-timer";
let mut timer_reg_cells: Vec<u32> = Vec::new();
@@ -712,22 +509,7 @@ fn create_gpio_node<T: DeviceInfoForFdt + Clone + Debug>(
Ok(())
}
// https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/fw-cfg.txt
#[cfg(feature = "fw_cfg")]
fn create_fw_cfg_node<T: DeviceInfoForFdt + Clone + Debug>(
fdt: &mut FdtWriter,
dev_info: &T,
) -> FdtWriterResult<()> {
// FwCfg node
let fw_cfg_node = fdt.begin_node(&format!("fw-cfg@{:x}", dev_info.addr()))?;
fdt.property("compatible", b"qemu,fw-cfg-mmio\0")?;
fdt.property_array_u64("reg", &[dev_info.addr(), dev_info.length()])?;
fdt.end_node(fw_cfg_node)?;
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<()> {
@@ -742,8 +524,6 @@ fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
DeviceType::Virtio(_) => {
ordered_virtio_device.push(info);
}
#[cfg(feature = "fw_cfg")]
DeviceType::FwCfg => create_fw_cfg_node(fdt, info)?,
}
}
@@ -760,17 +540,6 @@ fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
Ok(())
}
fn create_pmu_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> {
let compatible = "arm,armv8-pmuv3";
let irq = [GIC_FDT_IRQ_TYPE_PPI, AARCH64_PMU_IRQ, IRQ_TYPE_LEVEL_HI];
let pmu_node = fdt.begin_node("pmu")?;
fdt.property_string("compatible", compatible)?;
fdt.property_array_u32("interrupts", &irq)?;
fdt.end_node(pmu_node)?;
Ok(())
}
fn create_pci_nodes(
fdt: &mut FdtWriter,
pci_device_info: &[PciSpaceInfo],
@@ -783,22 +552,23 @@ fn create_pci_nodes(
for pci_device_info_elem in pci_device_info.iter() {
// EDK2 requires the PCIe high space above 4G address.
// The actual space in CLH follows the RAM. If the RAM space is small, the PCIe high space
// could fall below 4G.
// could fall bellow 4G.
// Here we cut off PCI device space below 8G in FDT to workaround the EDK2 check.
// But the address written in ACPI is not impacted.
let (pci_device_base_64bit, pci_device_size_64bit) =
if pci_device_info_elem.pci_device_space_start < PCI_HIGH_BASE.raw_value() {
(
PCI_HIGH_BASE.raw_value(),
pci_device_info_elem.pci_device_space_size
- (PCI_HIGH_BASE.raw_value() - pci_device_info_elem.pci_device_space_start),
)
} else {
(
pci_device_info_elem.pci_device_space_start,
pci_device_info_elem.pci_device_space_size,
)
};
let (pci_device_base_64bit, pci_device_size_64bit) = if cfg!(feature = "acpi")
&& (pci_device_info_elem.pci_device_space_start < PCI_HIGH_BASE)
{
(
PCI_HIGH_BASE,
pci_device_info_elem.pci_device_space_size
- (PCI_HIGH_BASE - pci_device_info_elem.pci_device_space_start),
)
} else {
(
pci_device_info_elem.pci_device_space_start,
pci_device_info_elem.pci_device_space_size,
)
};
// There is no specific requirement of the 32bit MMIO range, and
// therefore at least we can make these ranges 4K aligned.
let pci_device_size_32bit: u64 =
@@ -873,39 +643,39 @@ fn create_pci_nodes(
fdt.property_array_u32("msi-map", &msi_map)?;
fdt.property_u32("msi-parent", MSI_PHANDLE)?;
if pci_device_info_elem.pci_segment_id == 0
&& let Some(virtio_iommu_bdf) = virtio_iommu_bdf
{
// See kernel document Documentation/devicetree/bindings/pci/pci-iommu.txt
// for 'iommu-map' attribute setting.
let iommu_map = [
0_u32,
VIRTIO_IOMMU_PHANDLE,
0_u32,
virtio_iommu_bdf,
virtio_iommu_bdf + 1,
VIRTIO_IOMMU_PHANDLE,
virtio_iommu_bdf + 1,
0xffff - virtio_iommu_bdf,
];
fdt.property_array_u32("iommu-map", &iommu_map)?;
if pci_device_info_elem.pci_segment_id == 0 {
if let Some(virtio_iommu_bdf) = virtio_iommu_bdf {
// See kernel document Documentation/devicetree/bindings/pci/pci-iommu.txt
// for 'iommu-map' attribute setting.
let iommu_map = [
0_u32,
VIRTIO_IOMMU_PHANDLE,
0_u32,
virtio_iommu_bdf,
virtio_iommu_bdf + 1,
VIRTIO_IOMMU_PHANDLE,
virtio_iommu_bdf + 1,
0xffff - virtio_iommu_bdf,
];
fdt.property_array_u32("iommu-map", &iommu_map)?;
// See kernel document Documentation/devicetree/bindings/virtio/iommu.txt
// for virtio-iommu node settings.
let virtio_iommu_node_name = format!("virtio_iommu@{virtio_iommu_bdf:x}");
let virtio_iommu_node = fdt.begin_node(&virtio_iommu_node_name)?;
fdt.property_u32("#iommu-cells", 1)?;
fdt.property_string("compatible", "virtio,pci-iommu")?;
// See kernel document Documentation/devicetree/bindings/virtio/iommu.txt
// for virtio-iommu node settings.
let virtio_iommu_node_name = format!("virtio_iommu@{:x}", virtio_iommu_bdf);
let virtio_iommu_node = fdt.begin_node(&virtio_iommu_node_name)?;
fdt.property_u32("#iommu-cells", 1)?;
fdt.property_string("compatible", "virtio,pci-iommu")?;
// 'reg' is a five-cell address encoded as
// (phys.hi phys.mid phys.lo size.hi size.lo). phys.hi should contain the
// device's BDF as 0b00000000 bbbbbbbb dddddfff 00000000. The other cells
// should be zero.
let reg = [virtio_iommu_bdf << 8, 0_u32, 0_u32, 0_u32, 0_u32];
fdt.property_array_u32("reg", &reg)?;
fdt.property_u32("phandle", VIRTIO_IOMMU_PHANDLE)?;
// 'reg' is a five-cell address encoded as
// (phys.hi phys.mid phys.lo size.hi size.lo). phys.hi should contain the
// device's BDF as 0b00000000 bbbbbbbb dddddfff 00000000. The other cells
// should be zero.
let reg = [virtio_iommu_bdf << 8, 0_u32, 0_u32, 0_u32, 0_u32];
fdt.property_array_u32("reg", &reg)?;
fdt.property_u32("phandle", VIRTIO_IOMMU_PHANDLE)?;
fdt.end_node(virtio_iommu_node)?;
fdt.end_node(virtio_iommu_node)?;
}
}
fdt.end_node(pci_node)?;
@@ -915,22 +685,6 @@ fn create_pci_nodes(
}
fn create_distance_map_node(fdt: &mut FdtWriter, numa_nodes: &NumaNodes) -> FdtWriterResult<()> {
// When Generic Initiator nodes are present, skip ALL FDT NUMA information.
// Let ACPI (which supports Generic Initiator via SRAT Type 5) handle the entire NUMA topology.
// FDT cannot represent Generic Initiator nodes, and mixing FDT + ACPI NUMA info causes conflicts.
let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some());
if has_generic_initiator {
info!("Skipping NUMA distance map encoding in FDT with Generic Initiator devices");
return Ok(());
}
// At this point, we know there are no Generic Initiator nodes
let mut numa_ids: Vec<u32> = numa_nodes.keys().cloned().collect();
// If we only have one node, no distance map is needed
if numa_ids.len() <= 1 {
return Ok(());
}
let distance_map_node = fdt.begin_node("distance-map")?;
fdt.property_string("compatible", "numa-distance-map-v1")?;
// Construct the distance matrix.
@@ -943,33 +697,26 @@ fn create_distance_map_node(fdt: &mut FdtWriter, numa_nodes: &NumaNodes) -> FdtW
// a value greater than 10.
// 4. distance-matrix should have entries in lexicographical ascending
// order of nodes.
numa_ids.sort_unstable(); // lexicographical order
let mut distance_matrix = Vec::new();
// Iterate over actual numa IDs instead of 0..len()
for numa_id in numa_ids.iter() {
let numa_node = &numa_nodes[numa_id];
for dest_numa_id in numa_ids.iter() {
if *numa_id == *dest_numa_id {
distance_matrix.push(*numa_id);
distance_matrix.push(*dest_numa_id);
for numa_node_idx in 0..numa_nodes.len() {
let numa_node = numa_nodes.get(&(numa_node_idx as u32));
for dest_numa_node in 0..numa_node.unwrap().distances.len() + 1 {
if numa_node_idx == dest_numa_node {
distance_matrix.push(numa_node_idx as u32);
distance_matrix.push(dest_numa_node as u32);
distance_matrix.push(10_u32);
continue;
}
distance_matrix.push(*numa_id);
distance_matrix.push(*dest_numa_id);
// Use user-specified distance, checking both directions for symmetry
let distance = if let Some(&dist) = numa_node.distances.get(dest_numa_id) {
// Forward direction: current node -> dest node
dist
} else if let Some(dest_node) = numa_nodes.get(dest_numa_id) {
// Reverse direction for symmetry: dest node -> current node
dest_node.distances.get(numa_id).copied().unwrap_or(20)
} else {
// Default distance when neither direction is specified
20
};
distance_matrix.push(distance as u32);
distance_matrix.push(numa_node_idx as u32);
distance_matrix.push(dest_numa_node as u32);
distance_matrix.push(
*numa_node
.unwrap()
.distances
.get(&(dest_numa_node as u32))
.unwrap() as u32,
);
}
}
fdt.property_array_u32("distance-matrix", distance_matrix.as_ref())?;
@@ -993,7 +740,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;
@@ -1023,7 +770,10 @@ fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) {
// - At first, try to convert it to CStr and print,
// - If failed, print it as u32 array.
let value_result = match CStr::from_bytes_with_nul(value) {
Ok(value_cstr) => value_cstr.to_str().ok(),
Ok(value_cstr) => match value_cstr.to_str() {
Ok(value_str) => Some(value_str),
Err(_e) => None,
},
Err(_e) => None,
};
@@ -1046,7 +796,7 @@ fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) {
array,
indent = (n_spaces + 2)
);
}
};
}
// Print children nodes if there is any
@@ -1054,118 +804,3 @@ fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) {
print_node(child, n_spaces + 2);
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::NumaNode;
// Helper function to create a simple NumaNode for testing
fn create_test_numa_node(cpus: Vec<u32>, device_id: Option<String>) -> NumaNode {
NumaNode {
memory_regions: Vec::new(),
hotplug_regions: Vec::new(),
cpus,
pci_segments: Vec::new(),
distances: BTreeMap::new(),
memory_zones: Vec::new(),
device_id,
}
}
#[test]
fn test_fdt_generic_initiator_detection_and_skip() {
// No Generic Initiator - should not skip FDT NUMA
let mut numa_nodes = BTreeMap::new();
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
numa_nodes.insert(1, create_test_numa_node(vec![2, 3], None));
let has_gi = numa_nodes.values().any(|node| node.device_id.is_some());
assert!(
!has_gi,
"Should not detect Generic Initiator when none present"
);
// One Generic Initiator - should skip FDT NUMA
let mut numa_nodes = BTreeMap::new();
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
numa_nodes.insert(1, create_test_numa_node(vec![], Some("vfio0".to_string())));
let has_gi = numa_nodes.values().any(|node| node.device_id.is_some());
assert!(has_gi, "Should detect Generic Initiator when present");
let mut fdt = FdtWriter::new().unwrap();
let result = create_distance_map_node(&mut fdt, &numa_nodes);
assert!(result.is_ok(), "Should skip distance map when GI present");
// Multiple Generic Initiators - should skip FDT NUMA
let mut numa_nodes = BTreeMap::new();
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
numa_nodes.insert(1, create_test_numa_node(vec![], Some("vfio0".to_string())));
numa_nodes.insert(2, create_test_numa_node(vec![], Some("vfio1".to_string())));
let has_gi = numa_nodes.values().any(|node| node.device_id.is_some());
assert!(has_gi, "Should detect multiple Generic Initiators");
}
#[test]
fn test_fdt_distance_map() {
// Single NUMA node - should skip distance map
let mut numa_nodes = BTreeMap::new();
numa_nodes.insert(0, create_test_numa_node(vec![0, 1], None));
let mut fdt = FdtWriter::new().unwrap();
let result = create_distance_map_node(&mut fdt, &numa_nodes);
assert!(result.is_ok(), "Should skip distance map for single node");
// Empty NUMA nodes - should handle gracefully
let numa_nodes = BTreeMap::new();
let mut fdt = FdtWriter::new().unwrap();
let result = create_distance_map_node(&mut fdt, &numa_nodes);
assert!(result.is_ok(), "Should handle empty NUMA nodes");
// Non-contiguous NUMA IDs (0, 2, 5) with distance symmetry
let mut numa_nodes = BTreeMap::new();
let mut node0 = create_test_numa_node(vec![0], None);
node0.distances.insert(2, 20);
// node0 has no explicit distance to node5
let mut node2 = create_test_numa_node(vec![1], None);
node2.distances.insert(0, 20);
node2.distances.insert(5, 25);
let mut node5 = create_test_numa_node(vec![2], None);
node5.distances.insert(0, 30);
node5.distances.insert(2, 25);
// node5->node0 (should be used for node0->node5)
numa_nodes.insert(0, node0);
numa_nodes.insert(2, node2);
numa_nodes.insert(5, node5);
// Verify IDs are sorted lexicographically
let mut numa_ids: Vec<u32> = numa_nodes.keys().cloned().collect();
numa_ids.sort_unstable();
assert_eq!(numa_ids, vec![0, 2, 5]);
let mut fdt = FdtWriter::new().unwrap();
let result = create_distance_map_node(&mut fdt, &numa_nodes);
assert!(
result.is_ok(),
"Should handle non-contiguous IDs and symmetry"
);
// Default distance (20) when no distance specified in either direction
let mut numa_nodes = BTreeMap::new();
numa_nodes.insert(0, create_test_numa_node(vec![0], None));
numa_nodes.insert(1, create_test_numa_node(vec![1], None));
// Neither node has distance to the other
let mut fdt = FdtWriter::new().unwrap();
let result = create_distance_map_node(&mut fdt, &numa_nodes);
assert!(result.is_ok(), "Should default to 20 for missing distances");
}
}

View File

@@ -1,17 +1,16 @@
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use kvm_ioctls::DeviceFd;
use crate::arch::aarch64::gic::{Error, Result};
use crate::device::HypervisorDeviceError;
use crate::kvm::kvm_bindings::{
KVM_DEV_ARM_VGIC_GRP_DIST_REGS, KVM_DEV_ARM_VGIC_GRP_NR_IRQS, kvm_device_attr,
use super::{Error, Result};
use crate::layout::IRQ_BASE;
use hypervisor::kvm::kvm_bindings::{
kvm_device_attr, KVM_DEV_ARM_VGIC_GRP_DIST_REGS, KVM_DEV_ARM_VGIC_GRP_NR_IRQS,
};
use std::sync::Arc;
/*
Distributor registers as detailed at page 456 from
https://developer.arm.com/documentation/ihi0069/c/?lang=en.
https://static.docs.arm.com/ihi0069/c/IHI0069C_gic_architecture_specification.pdf.
Address offsets are relative to the Distributor base address defined
by the system memory map. Unless otherwise stated in the register description,
all GIC registers are 32-bits wide.
@@ -78,71 +77,55 @@ static VGIC_DIST_REGS: &[DistReg] = &[
VGIC_DIST_REG!(GICD_IPRIORITYR, 8, 0),
];
fn dist_attr_set(gic: &DeviceFd, offset: u32, val: u32) -> Result<()> {
let gic_dist_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_DIST_REGS,
attr: offset as u64,
addr: &raw const val as u64,
flags: 0,
};
gic.set_device_attr(&gic_dist_attr).map_err(|e| {
Error::SetDeviceAttribute(HypervisorDeviceError::SetDeviceAttribute(e.into()))
})?;
Ok(())
}
fn dist_attr_get(gic: &DeviceFd, offset: u32) -> Result<u32> {
let mut val = 0;
fn dist_attr_access(
gic: &Arc<dyn hypervisor::Device>,
offset: u32,
val: &u32,
set: bool,
) -> Result<()> {
let mut gic_dist_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_DIST_REGS,
attr: offset as u64,
addr: &raw mut val as u64,
addr: val as *const u32 as u64,
flags: 0,
};
// SAFETY: gic_dist_attr.addr is safe to write to.
unsafe { gic.get_device_attr(&mut gic_dist_attr) }.map_err(|e| {
Error::GetDeviceAttribute(HypervisorDeviceError::GetDeviceAttribute(e.into()))
})?;
Ok(val)
if set {
gic.set_device_attr(&gic_dist_attr)
.map_err(Error::SetDeviceAttribute)?;
} else {
gic.get_device_attr(&mut gic_dist_attr)
.map_err(Error::GetDeviceAttribute)?;
}
Ok(())
}
/// Get the distributor control register.
pub fn read_ctlr(gic: &DeviceFd) -> Result<u32> {
dist_attr_get(gic, GICD_CTLR)
pub fn read_ctlr(gic: &Arc<dyn hypervisor::Device>) -> Result<u32> {
let val: u32 = 0;
dist_attr_access(gic, GICD_CTLR, &val, false)?;
Ok(val)
}
/// Set the distributor control register.
pub fn write_ctlr(gic: &DeviceFd, val: u32) -> Result<()> {
dist_attr_set(gic, GICD_CTLR, val)
pub fn write_ctlr(gic: &Arc<dyn hypervisor::Device>, val: u32) -> Result<()> {
dist_attr_access(gic, GICD_CTLR, &val, true)
}
fn get_interrupts_num(gic: &DeviceFd) -> Result<u32> {
let mut num_irq = 0;
fn get_interrupts_num(gic: &Arc<dyn hypervisor::Device>) -> Result<u32> {
let num_irq = 0;
let mut nr_irqs_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_NR_IRQS,
attr: 0,
addr: &raw mut num_irq as u64,
addr: &num_irq as *const u32 as u64,
flags: 0,
};
// SAFETY: nr_irqs_attr.addr is safe to write to.
unsafe { gic.get_device_attr(&mut nr_irqs_attr) }.map_err(|e| {
Error::GetDeviceAttribute(HypervisorDeviceError::GetDeviceAttribute(e.into()))
})?;
gic.get_device_attr(&mut nr_irqs_attr)
.map_err(Error::GetDeviceAttribute)?;
Ok(num_irq)
}
fn compute_reg_len(gic: &DeviceFd, reg: &DistReg, base: u32) -> Result<u32> {
// FIXME:
// Redefine some GIC constants to avoid the dependency on `layout` crate.
// This is temporary solution, will be fixed in future refactoring.
const LAYOUT_IRQ_BASE: u32 = 32;
fn compute_reg_len(gic: &Arc<dyn hypervisor::Device>, reg: &DistReg, base: u32) -> Result<u32> {
let mut end = base;
let num_irq = get_interrupts_num(gic)?;
if reg.length > 0 {
@@ -155,8 +138,8 @@ fn compute_reg_len(gic: &DeviceFd, reg: &DistReg, base: u32) -> Result<u32> {
// This is the type of register that takes into account the number of interrupts
// that the model has. It is also the type of register where
// a register relates to multiple interrupts.
end = base + (reg.bpi as u32 * (num_irq - LAYOUT_IRQ_BASE) / 8);
if !(reg.bpi as u32 * (num_irq - LAYOUT_IRQ_BASE)).is_multiple_of(8) {
end = base + (reg.bpi as u32 * (num_irq - IRQ_BASE) / 8);
if reg.bpi as u32 * (num_irq - IRQ_BASE) % 8 > 0 {
end += REG_SIZE as u32;
}
}
@@ -164,7 +147,7 @@ fn compute_reg_len(gic: &DeviceFd, reg: &DistReg, base: u32) -> Result<u32> {
}
/// Set distributor registers of the GIC.
pub fn set_dist_regs(gic: &DeviceFd, state: &[u32]) -> Result<()> {
pub fn set_dist_regs(gic: &Arc<dyn hypervisor::Device>, state: &[u32]) -> Result<()> {
let mut idx = 0;
for dreg in VGIC_DIST_REGS {
@@ -172,7 +155,8 @@ pub fn set_dist_regs(gic: &DeviceFd, state: &[u32]) -> Result<()> {
let end = compute_reg_len(gic, dreg, base)?;
while base < end {
dist_attr_set(gic, base, state[idx])?;
let val = state[idx];
dist_attr_access(gic, base, &val, true)?;
idx += 1;
base += REG_SIZE as u32;
}
@@ -180,7 +164,7 @@ pub fn set_dist_regs(gic: &DeviceFd, state: &[u32]) -> Result<()> {
Ok(())
}
/// Get distributor registers of the GIC.
pub fn get_dist_regs(gic: &DeviceFd) -> Result<Vec<u32>> {
pub fn get_dist_regs(gic: &Arc<dyn hypervisor::Device>) -> Result<Vec<u32>> {
let mut state = Vec::new();
for dreg in VGIC_DIST_REGS {
@@ -188,7 +172,9 @@ pub fn get_dist_regs(gic: &DeviceFd) -> Result<Vec<u32>> {
let end = compute_reg_len(gic, dreg, base)?;
while base < end {
state.push(dist_attr_get(gic, base)?);
let val: u32 = 0;
dist_attr_access(gic, base, &val, false)?;
state.push(val);
base += REG_SIZE as u32;
}
}

View File

@@ -0,0 +1,261 @@
// Copyright 2021 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
//
// This file implements the GicV3 device.
pub mod kvm {
use crate::aarch64::gic::dist_regs::{get_dist_regs, read_ctlr, set_dist_regs, write_ctlr};
use crate::aarch64::gic::icc_regs::{get_icc_regs, set_icc_regs};
use crate::aarch64::gic::kvm::{save_pending_tables, KvmGicDevice};
use crate::aarch64::gic::redist_regs::{
construct_gicr_typers, get_redist_regs, set_redist_regs,
};
use crate::aarch64::gic::GicDevice;
use crate::layout;
use anyhow::anyhow;
use hypervisor::kvm::kvm_bindings;
use hypervisor::CpuState;
use std::any::Any;
use std::convert::TryInto;
use std::sync::Arc;
use std::{boxed::Box, result};
use versionize::{VersionMap, Versionize, VersionizeResult};
use versionize_derive::Versionize;
use vm_migration::{
Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable,
VersionMapped,
};
/// Errors thrown while saving/restoring the GICv3.
#[derive(Debug)]
pub enum Error {
/// Error in saving RDIST pending tables into guest RAM.
SavePendingTables(crate::aarch64::gic::Error),
/// Error in saving GIC distributor registers.
SaveDistributorRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC distributor registers.
RestoreDistributorRegisters(crate::aarch64::gic::Error),
/// Error in saving GIC distributor control registers.
SaveDistributorCtrlRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC distributor control registers.
RestoreDistributorCtrlRegisters(crate::aarch64::gic::Error),
/// Error in saving GIC redistributor registers.
SaveRedistributorRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC redistributor registers.
RestoreRedistributorRegisters(crate::aarch64::gic::Error),
/// Error in saving GIC CPU interface registers.
SaveIccRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC CPU interface registers.
RestoreIccRegisters(crate::aarch64::gic::Error),
}
type Result<T> = result::Result<T, Error>;
pub struct KvmGicV3 {
/// The hypervisor agnostic device for the GicV3
device: Arc<dyn hypervisor::Device>,
/// Vector holding values of GICR_TYPER for each vCPU
gicr_typers: Vec<u64>,
/// GIC device properties, to be used for setting up the fdt entry
properties: [u64; 4],
/// Number of CPUs handled by the device
vcpu_count: u64,
}
#[derive(Versionize)]
pub struct Gicv3State {
dist: Vec<u32>,
rdist: Vec<u32>,
icc: Vec<u32>,
// special register that enables interrupts and affinity routing
gicd_ctlr: u32,
}
impl VersionMapped for Gicv3State {}
impl KvmGicV3 {
// Device trees specific constants
pub const ARCH_GIC_V3_MAINT_IRQ: u32 = 9;
/// Get the address of the GIC distributor.
pub fn get_dist_addr() -> u64 {
layout::GIC_V3_DIST_START
}
/// Get the size of the GIC distributor.
pub fn get_dist_size() -> u64 {
layout::GIC_V3_DIST_SIZE
}
/// Get the address of the GIC redistributors.
pub fn get_redists_addr(vcpu_count: u64) -> u64 {
KvmGicV3::get_dist_addr() - KvmGicV3::get_redists_size(vcpu_count)
}
/// Get the size of the GIC redistributors.
pub fn get_redists_size(vcpu_count: u64) -> u64 {
vcpu_count * layout::GIC_V3_REDIST_SIZE
}
/// Save the state of GIC.
fn state(&self, gicr_typers: &[u64]) -> Result<Gicv3State> {
let gicd_ctlr =
read_ctlr(self.device()).map_err(Error::SaveDistributorCtrlRegisters)?;
let dist_state =
get_dist_regs(self.device()).map_err(Error::SaveDistributorRegisters)?;
let rdist_state = get_redist_regs(self.device(), gicr_typers)
.map_err(Error::SaveRedistributorRegisters)?;
let icc_state =
get_icc_regs(self.device(), gicr_typers).map_err(Error::SaveIccRegisters)?;
Ok(Gicv3State {
dist: dist_state,
rdist: rdist_state,
icc: icc_state,
gicd_ctlr,
})
}
/// Restore the state of GIC.
fn set_state(&mut self, gicr_typers: &[u64], state: &Gicv3State) -> Result<()> {
write_ctlr(self.device(), state.gicd_ctlr)
.map_err(Error::RestoreDistributorCtrlRegisters)?;
set_dist_regs(self.device(), &state.dist)
.map_err(Error::RestoreDistributorRegisters)?;
set_redist_regs(self.device(), gicr_typers, &state.rdist)
.map_err(Error::RestoreRedistributorRegisters)?;
set_icc_regs(self.device(), gicr_typers, &state.icc)
.map_err(Error::RestoreIccRegisters)?;
Ok(())
}
}
impl GicDevice for KvmGicV3 {
fn device(&self) -> &Arc<dyn hypervisor::Device> {
&self.device
}
fn fdt_compatibility(&self) -> &str {
"arm,gic-v3"
}
fn fdt_maint_irq(&self) -> u32 {
KvmGicV3::ARCH_GIC_V3_MAINT_IRQ
}
fn device_properties(&self) -> &[u64] {
&self.properties
}
fn vcpu_count(&self) -> u64 {
self.vcpu_count
}
fn set_its_device(&mut self, _its_device: Option<Arc<dyn hypervisor::Device>>) {}
fn set_gicr_typers(&mut self, vcpu_states: &[CpuState]) {
let gicr_typers = construct_gicr_typers(vcpu_states);
self.gicr_typers = gicr_typers;
}
fn as_any_concrete_mut(&mut self) -> &mut dyn Any {
self
}
}
impl KvmGicDevice for KvmGicV3 {
fn version() -> u32 {
kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V3
}
fn create_device(
device: Arc<dyn hypervisor::Device>,
vcpu_count: u64,
) -> Box<dyn GicDevice> {
Box::new(KvmGicV3 {
device,
gicr_typers: vec![0; vcpu_count.try_into().unwrap()],
properties: [
KvmGicV3::get_dist_addr(),
KvmGicV3::get_dist_size(),
KvmGicV3::get_redists_addr(vcpu_count),
KvmGicV3::get_redists_size(vcpu_count),
],
vcpu_count,
})
}
fn init_device_attributes(
_vm: &Arc<dyn hypervisor::Vm>,
gic_device: &mut dyn GicDevice,
) -> crate::aarch64::gic::Result<()> {
/* Setting up the distributor attribute.
We are placing the GIC below 1GB so we need to substract the size of the distributor.
*/
Self::set_device_attribute(
gic_device.device(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR,
u64::from(kvm_bindings::KVM_VGIC_V3_ADDR_TYPE_DIST),
&KvmGicV3::get_dist_addr() as *const u64 as u64,
0,
)?;
/* Setting up the redistributors' attribute.
We are calculating here the start of the redistributors address. We have one per CPU.
*/
Self::set_device_attribute(
gic_device.device(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR,
u64::from(kvm_bindings::KVM_VGIC_V3_ADDR_TYPE_REDIST),
&KvmGicV3::get_redists_addr(gic_device.vcpu_count()) as *const u64 as u64,
0,
)?;
Ok(())
}
}
pub const GIC_V3_SNAPSHOT_ID: &str = "gic-v3";
impl Snapshottable for KvmGicV3 {
fn id(&self) -> String {
GIC_V3_SNAPSHOT_ID.to_string()
}
fn snapshot(&mut self) -> std::result::Result<Snapshot, MigratableError> {
let gicr_typers = self.gicr_typers.clone();
Snapshot::new_from_versioned_state(&self.id(), &self.state(&gicr_typers).unwrap())
}
fn restore(&mut self, snapshot: Snapshot) -> std::result::Result<(), MigratableError> {
let gicr_typers = self.gicr_typers.clone();
self.set_state(&gicr_typers, &snapshot.to_versioned_state(&self.id())?)
.map_err(|e| {
MigratableError::Restore(anyhow!("Could not restore GICv3 state {:?}", e))
})
}
}
impl Pausable for KvmGicV3 {
fn pause(&mut self) -> std::result::Result<(), MigratableError> {
// Flush redistributors pending tables to guest RAM.
save_pending_tables(self.device()).map_err(|e| {
MigratableError::Pause(anyhow!("Could not save GICv3 GIC pending tables {:?}", e))
})?;
Ok(())
}
}
impl Transportable for KvmGicV3 {}
impl Migratable for KvmGicV3 {}
}

View File

@@ -0,0 +1,514 @@
// Copyright 2021 Arm Limited (or its affiliates). All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// This file implements the GicV3 device with ITS (Virtual Interrupt Translation Service).
pub mod kvm {
use crate::aarch64::gic::dist_regs::{get_dist_regs, read_ctlr, set_dist_regs, write_ctlr};
use crate::aarch64::gic::icc_regs::{get_icc_regs, set_icc_regs};
use crate::aarch64::gic::redist_regs::{
construct_gicr_typers, get_redist_regs, set_redist_regs,
};
use crate::aarch64::gic::gicv3::kvm::KvmGicV3;
use crate::aarch64::gic::kvm::{save_pending_tables, KvmGicDevice};
use crate::aarch64::gic::GicDevice;
use crate::layout;
use anyhow::anyhow;
use hypervisor::kvm::kvm_bindings;
use hypervisor::CpuState;
use std::any::Any;
use std::convert::TryInto;
use std::sync::Arc;
use std::{boxed::Box, result};
use versionize::{VersionMap, Versionize, VersionizeResult};
use versionize_derive::Versionize;
use vm_migration::{
Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable,
VersionMapped,
};
const GITS_CTLR: u32 = 0x0000;
const GITS_IIDR: u32 = 0x0004;
const GITS_CBASER: u32 = 0x0080;
const GITS_CWRITER: u32 = 0x0088;
const GITS_CREADR: u32 = 0x0090;
const GITS_BASER: u32 = 0x0100;
/// Errors thrown while saving/restoring the GICv3ITS.
#[derive(Debug)]
pub enum Error {
/// Error in saving RDIST pending tables into guest RAM.
SavePendingTables(crate::aarch64::gic::Error),
/// Error in saving GIC distributor registers.
SaveDistributorRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC distributor registers.
RestoreDistributorRegisters(crate::aarch64::gic::Error),
/// Error in saving GIC distributor control registers.
SaveDistributorCtrlRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC distributor control registers.
RestoreDistributorCtrlRegisters(crate::aarch64::gic::Error),
/// Error in saving GIC redistributor registers.
SaveRedistributorRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC redistributor registers.
RestoreRedistributorRegisters(crate::aarch64::gic::Error),
/// Error in saving GIC CPU interface registers.
SaveIccRegisters(crate::aarch64::gic::Error),
/// Error in restoring GIC CPU interface registers.
RestoreIccRegisters(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS IIDR register.
SaveITSIIDR(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS IIDR register.
RestoreITSIIDR(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS CBASER register.
SaveITSCBASER(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS CBASER register.
RestoreITSCBASER(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS CREADR register.
SaveITSCREADR(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS CREADR register.
RestoreITSCREADR(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS CWRITER register.
SaveITSCWRITER(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS CWRITER register.
RestoreITSCWRITER(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS BASER register.
SaveITSBASER(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS BASER register.
RestoreITSBASER(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS CTLR register.
SaveITSCTLR(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS CTLR register.
RestoreITSCTLR(crate::aarch64::gic::Error),
/// Error in saving GICv3ITS restore tables.
SaveITSTables(crate::aarch64::gic::Error),
/// Error in restoring GICv3ITS restore tables.
RestoreITSTables(crate::aarch64::gic::Error),
}
type Result<T> = result::Result<T, Error>;
/// Access an ITS device attribute.
///
/// This is a helper function to get/set the ITS device attribute depending
/// the bool parameter `set` provided.
pub fn gicv3_its_attr_access(
its_device: &Arc<dyn hypervisor::Device>,
group: u32,
attr: u32,
val: &u64,
set: bool,
) -> crate::aarch64::gic::Result<()> {
let mut gicv3_its_attr = kvm_bindings::kvm_device_attr {
group,
attr: attr as u64,
addr: val as *const u64 as u64,
flags: 0,
};
if set {
its_device
.set_device_attr(&gicv3_its_attr)
.map_err(crate::aarch64::gic::Error::SetDeviceAttribute)?;
} else {
its_device
.get_device_attr(&mut gicv3_its_attr)
.map_err(crate::aarch64::gic::Error::GetDeviceAttribute)?;
}
Ok(())
}
/// Function that saves/restores ITS tables into guest RAM.
///
/// The tables get flushed to guest RAM whenever the VM gets stopped.
pub fn gicv3_its_tables_access(
its_device: &Arc<dyn hypervisor::Device>,
save: bool,
) -> crate::aarch64::gic::Result<()> {
let attr = if save {
u64::from(kvm_bindings::KVM_DEV_ARM_ITS_SAVE_TABLES)
} else {
u64::from(kvm_bindings::KVM_DEV_ARM_ITS_RESTORE_TABLES)
};
let init_gic_attr = kvm_bindings::kvm_device_attr {
group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_CTRL,
attr,
addr: 0,
flags: 0,
};
its_device
.set_device_attr(&init_gic_attr)
.map_err(crate::aarch64::gic::Error::SetDeviceAttribute)
}
pub struct KvmGicV3Its {
/// The hypervisor agnostic device for the GicV3
device: Arc<dyn hypervisor::Device>,
/// The hypervisor agnostic device for the Its device
its_device: Option<Arc<dyn hypervisor::Device>>,
/// Vector holding values of GICR_TYPER for each vCPU
gicr_typers: Vec<u64>,
/// GIC device properties, to be used for setting up the fdt entry
gic_properties: [u64; 4],
/// MSI device properties, to be used for setting up the fdt entry
msi_properties: [u64; 2],
/// Number of CPUs handled by the device
vcpu_count: u64,
}
#[derive(Versionize)]
pub struct Gicv3ItsState {
dist: Vec<u32>,
rdist: Vec<u32>,
icc: Vec<u32>,
// special register that enables interrupts and affinity routing
gicd_ctlr: u32,
its_ctlr: u64,
its_iidr: u64,
its_cbaser: u64,
its_cwriter: u64,
its_creadr: u64,
its_baser: [u64; 8],
}
impl VersionMapped for Gicv3ItsState {}
impl KvmGicV3Its {
fn get_msi_size() -> u64 {
layout::GIC_V3_ITS_SIZE
}
fn get_msi_addr(vcpu_count: u64) -> u64 {
KvmGicV3::get_redists_addr(vcpu_count) - KvmGicV3Its::get_msi_size()
}
/// Save the state of GICv3ITS.
fn state(&self, gicr_typers: &[u64]) -> Result<Gicv3ItsState> {
let gicd_ctlr =
read_ctlr(self.device()).map_err(Error::SaveDistributorCtrlRegisters)?;
let dist_state =
get_dist_regs(self.device()).map_err(Error::SaveDistributorRegisters)?;
let rdist_state = get_redist_regs(self.device(), gicr_typers)
.map_err(Error::SaveRedistributorRegisters)?;
let icc_state =
get_icc_regs(self.device(), gicr_typers).map_err(Error::SaveIccRegisters)?;
let its_baser_state: [u64; 8] = [0; 8];
for i in 0..8 {
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_BASER + i * 8,
&its_baser_state[i as usize],
false,
)
.map_err(Error::SaveITSBASER)?;
}
let its_ctlr_state: u64 = 0;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CTLR,
&its_ctlr_state,
false,
)
.map_err(Error::SaveITSCTLR)?;
let its_cbaser_state: u64 = 0;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CBASER,
&its_cbaser_state,
false,
)
.map_err(Error::SaveITSCBASER)?;
let its_creadr_state: u64 = 0;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CREADR,
&its_creadr_state,
false,
)
.map_err(Error::SaveITSCREADR)?;
let its_cwriter_state: u64 = 0;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CWRITER,
&its_cwriter_state,
false,
)
.map_err(Error::SaveITSCWRITER)?;
let its_iidr_state: u64 = 0;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_IIDR,
&its_iidr_state,
false,
)
.map_err(Error::SaveITSIIDR)?;
Ok(Gicv3ItsState {
dist: dist_state,
rdist: rdist_state,
icc: icc_state,
gicd_ctlr,
its_ctlr: its_ctlr_state,
its_iidr: its_iidr_state,
its_cbaser: its_cbaser_state,
its_cwriter: its_cwriter_state,
its_creadr: its_creadr_state,
its_baser: its_baser_state,
})
}
/// Restore the state of GICv3ITS.
fn set_state(&mut self, gicr_typers: &[u64], state: &Gicv3ItsState) -> Result<()> {
write_ctlr(self.device(), state.gicd_ctlr)
.map_err(Error::RestoreDistributorCtrlRegisters)?;
set_dist_regs(self.device(), &state.dist)
.map_err(Error::RestoreDistributorRegisters)?;
set_redist_regs(self.device(), gicr_typers, &state.rdist)
.map_err(Error::RestoreRedistributorRegisters)?;
set_icc_regs(self.device(), gicr_typers, &state.icc)
.map_err(Error::RestoreIccRegisters)?;
//Restore GICv3ITS registers
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_IIDR,
&state.its_iidr,
true,
)
.map_err(Error::RestoreITSIIDR)?;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CBASER,
&state.its_cbaser,
true,
)
.map_err(Error::RestoreITSCBASER)?;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CREADR,
&state.its_creadr,
true,
)
.map_err(Error::RestoreITSCREADR)?;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CWRITER,
&state.its_cwriter,
true,
)
.map_err(Error::RestoreITSCWRITER)?;
for i in 0..8 {
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_BASER + i * 8,
&state.its_baser[i as usize],
true,
)
.map_err(Error::RestoreITSBASER)?;
}
// Restore ITS tables
gicv3_its_tables_access(self.its_device().unwrap(), false)
.map_err(Error::RestoreITSTables)?;
gicv3_its_attr_access(
self.its_device().unwrap(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ITS_REGS,
GITS_CTLR,
&state.its_ctlr,
true,
)
.map_err(Error::RestoreITSCTLR)?;
Ok(())
}
}
impl GicDevice for KvmGicV3Its {
fn device(&self) -> &Arc<dyn hypervisor::Device> {
&self.device
}
fn its_device(&self) -> Option<&Arc<dyn hypervisor::Device>> {
self.its_device.as_ref()
}
fn fdt_compatibility(&self) -> &str {
"arm,gic-v3"
}
fn msi_compatible(&self) -> bool {
true
}
fn msi_compatibility(&self) -> &str {
"arm,gic-v3-its"
}
fn fdt_maint_irq(&self) -> u32 {
KvmGicV3::ARCH_GIC_V3_MAINT_IRQ
}
fn msi_properties(&self) -> &[u64] {
&self.msi_properties
}
fn device_properties(&self) -> &[u64] {
&self.gic_properties
}
fn vcpu_count(&self) -> u64 {
self.vcpu_count
}
fn set_its_device(&mut self, its_device: Option<Arc<dyn hypervisor::Device>>) {
self.its_device = its_device;
}
fn set_gicr_typers(&mut self, vcpu_states: &[CpuState]) {
let gicr_typers = construct_gicr_typers(vcpu_states);
self.gicr_typers = gicr_typers;
}
fn as_any_concrete_mut(&mut self) -> &mut dyn Any {
self
}
}
impl KvmGicDevice for KvmGicV3Its {
fn version() -> u32 {
KvmGicV3::version()
}
fn create_device(
device: Arc<dyn hypervisor::Device>,
vcpu_count: u64,
) -> Box<dyn GicDevice> {
Box::new(KvmGicV3Its {
device,
its_device: None,
gicr_typers: vec![0; vcpu_count.try_into().unwrap()],
gic_properties: [
KvmGicV3::get_dist_addr(),
KvmGicV3::get_dist_size(),
KvmGicV3::get_redists_addr(vcpu_count),
KvmGicV3::get_redists_size(vcpu_count),
],
msi_properties: [
KvmGicV3Its::get_msi_addr(vcpu_count),
KvmGicV3Its::get_msi_size(),
],
vcpu_count,
})
}
fn init_device_attributes(
vm: &Arc<dyn hypervisor::Vm>,
gic_device: &mut dyn GicDevice,
) -> crate::aarch64::gic::Result<()> {
KvmGicV3::init_device_attributes(vm, gic_device)?;
let mut its_device = kvm_bindings::kvm_create_device {
type_: kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_ITS,
fd: 0,
flags: 0,
};
let its_fd = vm
.create_device(&mut its_device)
.map_err(crate::aarch64::gic::Error::CreateGic)?;
Self::set_device_attribute(
&its_fd,
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_ADDR,
u64::from(kvm_bindings::KVM_VGIC_ITS_ADDR_TYPE),
&KvmGicV3Its::get_msi_addr(gic_device.vcpu_count()) as *const u64 as u64,
0,
)?;
Self::set_device_attribute(
&its_fd,
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_CTRL,
u64::from(kvm_bindings::KVM_DEV_ARM_VGIC_CTRL_INIT),
0,
0,
)?;
gic_device.set_its_device(Some(its_fd));
Ok(())
}
}
pub const GIC_V3_ITS_SNAPSHOT_ID: &str = "gic-v3-its";
impl Snapshottable for KvmGicV3Its {
fn id(&self) -> String {
GIC_V3_ITS_SNAPSHOT_ID.to_string()
}
fn snapshot(&mut self) -> std::result::Result<Snapshot, MigratableError> {
let gicr_typers = self.gicr_typers.clone();
Snapshot::new_from_versioned_state(&self.id(), &self.state(&gicr_typers).unwrap())
}
fn restore(&mut self, snapshot: Snapshot) -> std::result::Result<(), MigratableError> {
let gicr_typers = self.gicr_typers.clone();
self.set_state(&gicr_typers, &snapshot.to_versioned_state(&self.id())?)
.map_err(|e| {
MigratableError::Restore(anyhow!("Could not restore GICv3ITS state {:?}", e))
})
}
}
impl Pausable for KvmGicV3Its {
fn pause(&mut self) -> std::result::Result<(), MigratableError> {
// Flush redistributors pending tables to guest RAM.
save_pending_tables(self.device()).map_err(|e| {
MigratableError::Pause(anyhow!(
"Could not save GICv3ITS GIC pending tables {:?}",
e
))
})?;
// Flush ITS tables to guest RAM.
gicv3_its_tables_access(self.its_device().unwrap(), true).map_err(|e| {
MigratableError::Pause(anyhow!("Could not save GICv3ITS ITS tables {:?}", e))
})?;
Ok(())
}
}
impl Transportable for KvmGicV3Its {}
impl Migratable for KvmGicV3Its {}
}

View File

@@ -1,18 +1,14 @@
// Copyright 2022 Arm Limited (or its affiliates). All rights reserved.
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use kvm_ioctls::DeviceFd;
use crate::arch::aarch64::gic::{Error, Result};
use crate::device::HypervisorDeviceError;
use crate::kvm::kvm_bindings::{
KVM_DEV_ARM_VGIC_GRP_CPU_SYSREGS, KVM_REG_ARM64_SYSREG_CRM_MASK,
use super::{Error, Result};
use hypervisor::kvm::kvm_bindings::{
kvm_device_attr, KVM_DEV_ARM_VGIC_GRP_CPU_SYSREGS, KVM_REG_ARM64_SYSREG_CRM_MASK,
KVM_REG_ARM64_SYSREG_CRM_SHIFT, KVM_REG_ARM64_SYSREG_CRN_MASK, KVM_REG_ARM64_SYSREG_CRN_SHIFT,
KVM_REG_ARM64_SYSREG_OP0_MASK, KVM_REG_ARM64_SYSREG_OP0_SHIFT, KVM_REG_ARM64_SYSREG_OP1_MASK,
KVM_REG_ARM64_SYSREG_OP1_SHIFT, KVM_REG_ARM64_SYSREG_OP2_MASK, KVM_REG_ARM64_SYSREG_OP2_SHIFT,
kvm_device_attr,
};
use std::sync::Arc;
const KVM_DEV_ARM_VGIC_V3_MPIDR_SHIFT: u32 = 32;
const KVM_DEV_ARM_VGIC_V3_MPIDR_MASK: u64 = 0xffffffff << KVM_DEV_ARM_VGIC_V3_MPIDR_SHIFT as u64;
@@ -81,41 +77,31 @@ static VGIC_ICC_REGS: &[u64] = &[
SYS_ICC_AP1R3_EL1,
];
fn icc_attr_set(gic: &DeviceFd, offset: u64, typer: u64, val: u32) -> Result<()> {
let gic_icc_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_CPU_SYSREGS,
attr: ((typer & KVM_DEV_ARM_VGIC_V3_MPIDR_MASK) | offset), // this needs the mpidr
addr: &raw const val as u64,
flags: 0,
};
gic.set_device_attr(&gic_icc_attr).map_err(|e| {
Error::SetDeviceAttribute(HypervisorDeviceError::SetDeviceAttribute(e.into()))
})?;
Ok(())
}
fn icc_attr_get(gic: &DeviceFd, offset: u64, typer: u64) -> Result<u32> {
let mut val = 0;
fn icc_attr_access(
gic: &Arc<dyn hypervisor::Device>,
offset: u64,
typer: u64,
val: &u32,
set: bool,
) -> Result<()> {
let mut gic_icc_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_CPU_SYSREGS,
attr: ((typer & KVM_DEV_ARM_VGIC_V3_MPIDR_MASK) | offset), // this needs the mpidr
addr: &raw mut val as u64,
addr: val as *const u32 as u64,
flags: 0,
};
// SAFETY: gic_icc_attr.addr is safe to write to.
unsafe { gic.get_device_attr(&mut gic_icc_attr) }.map_err(|e| {
Error::GetDeviceAttribute(HypervisorDeviceError::GetDeviceAttribute(e.into()))
})?;
Ok(val)
if set {
gic.set_device_attr(&gic_icc_attr)
.map_err(Error::SetDeviceAttribute)?;
} else {
gic.get_device_attr(&mut gic_icc_attr)
.map_err(Error::GetDeviceAttribute)?;
}
Ok(())
}
/// Get ICC registers.
pub fn get_icc_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
pub fn get_icc_regs(gic: &Arc<dyn hypervisor::Device>, gicr_typer: &[u64]) -> Result<Vec<u32>> {
let mut state: Vec<u32> = Vec::new();
// We need this for the ICC_AP<m>R<n>_EL1 registers.
let mut num_priority_bits = 0;
@@ -123,9 +109,10 @@ pub fn get_icc_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
for ix in gicr_typer {
let i = *ix;
for icc_offset in VGIC_ICC_REGS {
let val = 0;
if *icc_offset == SYS_ICC_CTLR_EL1 {
// calculate priority bits by reading the ctrl_el1 register.
let val = icc_attr_get(gic, *icc_offset, i)?;
icc_attr_access(gic, *icc_offset, i, &val, false)?;
// The priority bits are found in the ICC_CTLR_EL1 register (bits from 10:8).
// See page 194 from https://static.docs.arm.com/ihi0069/c/IHI0069C_gic_
// architecture_specification.pdf.
@@ -145,7 +132,8 @@ pub fn get_icc_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
// 7 bits of priority.
else if *icc_offset == SYS_ICC_AP0R1_EL1 || *icc_offset == SYS_ICC_AP1R1_EL1 {
if num_priority_bits >= 6 {
state.push(icc_attr_get(gic, *icc_offset, i)?);
icc_attr_access(gic, *icc_offset, i, &val, false)?;
state.push(val);
}
} else if *icc_offset == SYS_ICC_AP0R2_EL1
|| *icc_offset == SYS_ICC_AP0R3_EL1
@@ -153,10 +141,12 @@ pub fn get_icc_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
|| *icc_offset == SYS_ICC_AP1R3_EL1
{
if num_priority_bits == 7 {
state.push(icc_attr_get(gic, *icc_offset, i)?);
icc_attr_access(gic, *icc_offset, i, &val, false)?;
state.push(val);
}
} else {
state.push(icc_attr_get(gic, *icc_offset, i)?);
icc_attr_access(gic, *icc_offset, i, &val, false)?;
state.push(val);
}
}
}
@@ -164,7 +154,11 @@ pub fn get_icc_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
}
/// Set ICC registers.
pub fn set_icc_regs(gic: &DeviceFd, gicr_typer: &[u64], state: &[u32]) -> Result<()> {
pub fn set_icc_regs(
gic: &Arc<dyn hypervisor::Device>,
gicr_typer: &[u64],
state: &[u32],
) -> Result<()> {
let mut num_priority_bits = 0;
let mut idx = 0;
for ix in gicr_typer {
@@ -177,7 +171,7 @@ pub fn set_icc_regs(gic: &DeviceFd, gicr_typer: &[u64], state: &[u32]) -> Result
}
if *icc_offset == SYS_ICC_AP0R1_EL1 || *icc_offset == SYS_ICC_AP1R1_EL1 {
if num_priority_bits >= 6 {
icc_attr_set(gic, *icc_offset, i, state[idx])?;
icc_attr_access(gic, *icc_offset, i, &state[idx], true)?;
idx += 1;
}
continue;
@@ -188,12 +182,12 @@ pub fn set_icc_regs(gic: &DeviceFd, gicr_typer: &[u64], state: &[u32]) -> Result
|| *icc_offset == SYS_ICC_AP1R3_EL1
{
if num_priority_bits == 7 {
icc_attr_set(gic, *icc_offset, i, state[idx])?;
icc_attr_access(gic, *icc_offset, i, &state[idx], true)?;
idx += 1;
}
continue;
}
icc_attr_set(gic, *icc_offset, i, state[idx])?;
icc_attr_access(gic, *icc_offset, i, &state[idx], true)?;
idx += 1;
}
}

220
arch/src/aarch64/gic/mod.rs Normal file
View File

@@ -0,0 +1,220 @@
// Copyright 2021 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
pub mod dist_regs;
pub mod gicv3;
pub mod gicv3_its;
pub mod icc_regs;
pub mod redist_regs;
pub use self::dist_regs::{get_dist_regs, read_ctlr, set_dist_regs, write_ctlr};
pub use self::icc_regs::{get_icc_regs, set_icc_regs};
pub use self::redist_regs::{get_redist_regs, set_redist_regs};
use hypervisor::CpuState;
use std::any::Any;
use std::result;
use std::sync::Arc;
/// Errors thrown while setting up the GIC.
#[derive(Debug)]
pub enum Error {
/// Error while calling KVM ioctl for setting up the global interrupt controller.
CreateGic(hypervisor::HypervisorVmError),
/// Error while setting device attributes for the GIC.
SetDeviceAttribute(hypervisor::HypervisorDeviceError),
/// Error while getting device attributes for the GIC.
GetDeviceAttribute(hypervisor::HypervisorDeviceError),
}
type Result<T> = result::Result<T, Error>;
pub trait GicDevice: Send {
/// Returns the hypervisor agnostic Device of the GIC device
fn device(&self) -> &Arc<dyn hypervisor::Device>;
/// Returns the hypervisor agnostic Device of the ITS device
fn its_device(&self) -> Option<&Arc<dyn hypervisor::Device>> {
None
}
/// Returns the fdt compatibility property of the device
fn fdt_compatibility(&self) -> &str;
/// Returns the maint_irq fdt property of the device
fn fdt_maint_irq(&self) -> u32;
/// Returns an array with GIC device properties
fn device_properties(&self) -> &[u64];
/// Returns the number of vCPUs this GIC handles
fn vcpu_count(&self) -> u64;
/// Returns whether the GIC device is MSI compatible or not
fn msi_compatible(&self) -> bool {
false
}
/// Returns the MSI compatibility property of the device
fn msi_compatibility(&self) -> &str {
""
}
/// Returns the MSI reg property of the device
fn msi_properties(&self) -> &[u64] {
&[]
}
fn set_its_device(&mut self, its_device: Option<Arc<dyn hypervisor::Device>>);
/// Get the values of GICR_TYPER for each vCPU.
fn set_gicr_typers(&mut self, vcpu_states: &[CpuState]);
/// Downcast the trait object to its concrete type.
fn as_any_concrete_mut(&mut self) -> &mut dyn Any;
}
pub mod kvm {
use super::GicDevice;
use super::Result;
use crate::aarch64::gic::gicv3_its::kvm::KvmGicV3Its;
use crate::layout;
use hypervisor::kvm::kvm_bindings;
use std::boxed::Box;
use std::sync::Arc;
/// Trait for GIC devices.
pub trait KvmGicDevice: Send + Sync + GicDevice {
/// Returns the GIC version of the device
fn version() -> u32;
/// Create the GIC device object
fn create_device(
device: Arc<dyn hypervisor::Device>,
vcpu_count: u64,
) -> Box<dyn GicDevice>;
/// Setup the device-specific attributes
fn init_device_attributes(
vm: &Arc<dyn hypervisor::Vm>,
gic_device: &mut dyn GicDevice,
) -> Result<()>;
/// Initialize a GIC device
fn init_device(vm: &Arc<dyn hypervisor::Vm>) -> Result<Arc<dyn hypervisor::Device>> {
let mut gic_device = kvm_bindings::kvm_create_device {
type_: Self::version(),
fd: 0,
flags: 0,
};
vm.create_device(&mut gic_device)
.map_err(super::Error::CreateGic)
}
/// Set a GIC device attribute
fn set_device_attribute(
device: &Arc<dyn hypervisor::Device>,
group: u32,
attr: u64,
addr: u64,
flags: u32,
) -> Result<()> {
let attr = kvm_bindings::kvm_device_attr {
flags,
group,
attr,
addr,
};
device
.set_device_attr(&attr)
.map_err(super::Error::SetDeviceAttribute)?;
Ok(())
}
/// Get a GIC device attribute
fn get_device_attribute(
device: &Arc<dyn hypervisor::Device>,
group: u32,
attr: u64,
addr: u64,
flags: u32,
) -> Result<()> {
let mut attr = kvm_bindings::kvm_device_attr {
flags,
group,
attr,
addr,
};
device
.get_device_attr(&mut attr)
.map_err(super::Error::GetDeviceAttribute)?;
Ok(())
}
/// Finalize the setup of a GIC device
fn finalize_device(gic_device: &dyn GicDevice) -> Result<()> {
/* We need to tell the kernel how many irqs to support with this vgic.
* See the `layout` module for details.
*/
let nr_irqs: u32 = layout::IRQ_NUM;
let nr_irqs_ptr = &nr_irqs as *const u32;
Self::set_device_attribute(
gic_device.device(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_NR_IRQS,
0,
nr_irqs_ptr as u64,
0,
)?;
/* Finalize the GIC.
* See https://code.woboq.org/linux/linux/virt/kvm/arm/vgic/vgic-kvm-device.c.html#211.
*/
Self::set_device_attribute(
gic_device.device(),
kvm_bindings::KVM_DEV_ARM_VGIC_GRP_CTRL,
u64::from(kvm_bindings::KVM_DEV_ARM_VGIC_CTRL_INIT),
0,
0,
)?;
Ok(())
}
/// Method to initialize the GIC device
#[allow(clippy::new_ret_no_self)]
fn new(vm: &Arc<dyn hypervisor::Vm>, vcpu_count: u64) -> Result<Box<dyn GicDevice>> {
let vgic_fd = Self::init_device(vm)?;
let mut device = Self::create_device(vgic_fd, vcpu_count);
Self::init_device_attributes(vm, &mut *device)?;
Self::finalize_device(&*device)?;
Ok(device)
}
}
/// Create a GICv3-ITS device.
///
pub fn create_gic(vm: &Arc<dyn hypervisor::Vm>, vcpu_count: u64) -> Result<Box<dyn GicDevice>> {
debug!("creating a GICv3-ITS");
KvmGicV3Its::new(vm, vcpu_count)
}
/// Function that saves RDIST pending tables into guest RAM.
///
/// The tables get flushed to guest RAM whenever the VM gets stopped.
pub fn save_pending_tables(gic: &Arc<dyn hypervisor::Device>) -> Result<()> {
let init_gic_attr = kvm_bindings::kvm_device_attr {
group: kvm_bindings::KVM_DEV_ARM_VGIC_GRP_CTRL,
attr: u64::from(kvm_bindings::KVM_DEV_ARM_VGIC_SAVE_PENDING_TABLES),
addr: 0,
flags: 0,
};
gic.set_device_attr(&init_gic_attr)
.map_err(super::Error::SetDeviceAttribute)
}
}

View File

@@ -1,18 +1,10 @@
// Copyright 2022 Arm Limited (or its affiliates). All rights reserved.
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use kvm_ioctls::DeviceFd;
use crate::CpuState;
use crate::arch::aarch64::gic::{Error, Result};
use crate::device::HypervisorDeviceError;
use crate::kvm::VcpuKvmState;
use crate::kvm::kvm_bindings::{
KVM_DEV_ARM_VGIC_GRP_REDIST_REGS, KVM_REG_ARM64, KVM_REG_ARM64_SYSREG,
KVM_REG_ARM64_SYSREG_OP0_MASK, KVM_REG_ARM64_SYSREG_OP0_SHIFT, KVM_REG_ARM64_SYSREG_OP2_MASK,
KVM_REG_ARM64_SYSREG_OP2_SHIFT, KVM_REG_SIZE_U64, kvm_device_attr, kvm_one_reg,
};
use super::{Error, Result};
use hypervisor::kvm::kvm_bindings::{kvm_device_attr, KVM_DEV_ARM_VGIC_GRP_REDIST_REGS};
use hypervisor::CpuState;
use std::sync::Arc;
// Relevant redistributor registers that we want to save/restore.
const GICR_CTLR: u32 = 0x0000;
@@ -39,14 +31,8 @@ const GICR_ICFGR0: u32 = GICR_SGI_OFFSET + 0x0C00;
const KVM_DEV_ARM_VGIC_V3_MPIDR_SHIFT: u32 = 32;
const KVM_DEV_ARM_VGIC_V3_MPIDR_MASK: u64 = 0xffffffff << KVM_DEV_ARM_VGIC_V3_MPIDR_SHIFT as u64;
const KVM_ARM64_SYSREG_MPIDR_EL1: u64 = KVM_REG_ARM64
| KVM_REG_SIZE_U64
| KVM_REG_ARM64_SYSREG as u64
| (((3_u64) << KVM_REG_ARM64_SYSREG_OP0_SHIFT) & KVM_REG_ARM64_SYSREG_OP0_MASK as u64)
| (((5_u64) << KVM_REG_ARM64_SYSREG_OP2_SHIFT) & KVM_REG_ARM64_SYSREG_OP2_MASK as u64);
/// This is how we represent the registers of a distributor.
/// It is relevant their offset from the base address of the
/// It is relrvant their offset from the base address of the
/// distributor.
/// Each register has a different number
/// of bits_per_irq and is therefore variable length.
@@ -96,41 +82,31 @@ static VGIC_SGI_REGS: &[RdistReg] = &[
VGIC_RDIST_REG!(GICR_IPRIORITYR0, 32),
];
fn redist_attr_set(gic: &DeviceFd, offset: u32, typer: u64, val: u32) -> Result<()> {
let gic_redist_attr = kvm_device_attr {
fn redist_attr_access(
gic: &Arc<dyn hypervisor::Device>,
offset: u32,
typer: u64,
val: &u32,
set: bool,
) -> Result<()> {
let mut gic_dist_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_REDIST_REGS,
attr: (typer & KVM_DEV_ARM_VGIC_V3_MPIDR_MASK) | (offset as u64), // this needs the mpidr
addr: &raw const val as u64,
addr: val as *const u32 as u64,
flags: 0,
};
gic.set_device_attr(&gic_redist_attr).map_err(|e| {
Error::SetDeviceAttribute(HypervisorDeviceError::SetDeviceAttribute(e.into()))
})?;
if set {
gic.set_device_attr(&gic_dist_attr)
.map_err(Error::SetDeviceAttribute)?;
} else {
gic.get_device_attr(&mut gic_dist_attr)
.map_err(Error::GetDeviceAttribute)?;
}
Ok(())
}
fn redist_attr_get(gic: &DeviceFd, offset: u32, typer: u64) -> Result<u32> {
let mut val = 0;
let mut gic_redist_attr = kvm_device_attr {
group: KVM_DEV_ARM_VGIC_GRP_REDIST_REGS,
attr: (typer & KVM_DEV_ARM_VGIC_V3_MPIDR_MASK) | (offset as u64), // this needs the mpidr
addr: &raw mut val as u64,
flags: 0,
};
// SAFETY: gic_redist_attr.addr is safe to write to.
unsafe { gic.get_device_attr(&mut gic_redist_attr) }.map_err(|e| {
Error::GetDeviceAttribute(HypervisorDeviceError::GetDeviceAttribute(e.into()))
})?;
Ok(val)
}
fn access_redists_aux(
gic: &DeviceFd,
gic: &Arc<dyn hypervisor::Device>,
gicr_typer: &[u64],
state: &mut Vec<u32>,
reg_list: &[RdistReg],
@@ -143,11 +119,14 @@ fn access_redists_aux(
let end = base + rdreg.length as u32;
while base < end {
let mut val = 0;
if set {
redist_attr_set(gic, base, *i, state[*idx])?;
val = state[*idx];
redist_attr_access(gic, base, *i, &val, true)?;
*idx += 1;
} else {
state.push(redist_attr_get(gic, base, *i)?);
redist_attr_access(gic, base, *i, &val, false)?;
state.push(val);
}
base += REG_SIZE as u32;
}
@@ -157,7 +136,7 @@ fn access_redists_aux(
}
/// Get redistributor registers.
pub fn get_redist_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
pub fn get_redist_regs(gic: &Arc<dyn hypervisor::Device>, gicr_typer: &[u64]) -> Result<Vec<u32>> {
let mut state = Vec::new();
let mut idx: usize = 0;
access_redists_aux(
@@ -174,7 +153,11 @@ pub fn get_redist_regs(gic: &DeviceFd, gicr_typer: &[u64]) -> Result<Vec<u32>> {
}
/// Set redistributor registers.
pub fn set_redist_regs(gic: &DeviceFd, gicr_typer: &[u64], state: &[u32]) -> Result<()> {
pub fn set_redist_regs(
gic: &Arc<dyn hypervisor::Device>,
gicr_typer: &[u64],
state: &[u32],
) -> Result<()> {
let mut idx: usize = 0;
let mut mut_state = state.to_owned();
access_redists_aux(
@@ -210,18 +193,17 @@ pub fn construct_gicr_typers(vcpu_states: &[CpuState]) -> Vec<u64> {
*/
let mut gicr_typers: Vec<u64> = Vec::new();
for (index, state) in vcpu_states.iter().enumerate() {
let state: VcpuKvmState = state.clone().into();
let last = (index == vcpu_states.len() - 1) as u64;
// state.sys_regs is a big collection of system registers, including MIPDR_EL1
let mpidr: Vec<kvm_one_reg> = state
.sys_regs
.into_iter()
.filter(|reg| reg.id == KVM_ARM64_SYSREG_MPIDR_EL1)
.collect();
let last = {
if index == vcpu_states.len() - 1 {
1
} else {
0
}
};
//calculate affinity
let mut cpu_affid = mpidr[0].addr & 1095233437695;
let mut cpu_affid = state.mpidr & 1095233437695;
cpu_affid = ((cpu_affid & 0xFF00000000) >> 8) | (cpu_affid & 0xFFFFFF);
gicr_typers.push((cpu_affid << 32) | (1 << 24) | ((index as u64) << 8) | (last << 4));
gicr_typers.push((cpu_affid << 32) | (1 << 24) | (index as u64) << 8 | (last << 4));
}
gicr_typers

View File

@@ -3,7 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
//
// Memory layout of AArch64 guest:
// Memory layout of Aarch64 guest:
//
// Physical +---------------------------------------------------------------+
// address | |
@@ -20,13 +20,6 @@
// | DRAM |
// | |
// | |
// 4GB +---------------------------------------------------------------+
// | 32-bit devices hole |
// 4GB-64M +---------------------------------------------------------------+
// | |
// | |
// | DRAM |
// | |
// | |
// 1GB +---------------------------------------------------------------+
// | |
@@ -42,11 +35,11 @@
// | Legacy devices space |
// | |
// 144 M +---------------------------------------------------------------|
// | |
// | Reserved (now GIC is here) |
// 64 M +---------------------------------------------------------------+
// | |
// | UEFI space |
// | |
// 4 M +---------------------------------------------------------------+
// | UEFI flash |
// 0GB +---------------------------------------------------------------+
//
//
@@ -55,16 +48,17 @@ use vm_memory::GuestAddress;
/// 0x0 ~ 0x40_0000 (4 MiB) is reserved to UEFI
/// UEFI binary size is required less than 3 MiB, reserving 4 MiB is enough.
pub const UEFI_START: GuestAddress = GuestAddress(0);
pub const UEFI_START: u64 = 0x0;
pub const MEM_UEFI_START: GuestAddress = GuestAddress(0);
pub const UEFI_SIZE: u64 = 0x040_0000;
/// Below this address will reside the GIC, above this address will reside the MMIO devices.
const MAPPED_IO_START: GuestAddress = GuestAddress(0x0900_0000);
pub const MAPPED_IO_START: u64 = 0x0900_0000;
/// See kernel file arch/arm64/include/uapi/asm/kvm.h for the GIC related definitions.
/// 0x08ff_0000 ~ 0x0900_0000 is reserved for GICv3 Distributor
pub const GIC_V3_DIST_SIZE: u64 = 0x01_0000;
pub const GIC_V3_DIST_START: GuestAddress = GuestAddress(MAPPED_IO_START.0 - GIC_V3_DIST_SIZE);
pub const GIC_V3_DIST_START: u64 = MAPPED_IO_START - GIC_V3_DIST_SIZE;
/// Below 0x08ff_0000 is reserved for GICv3 Redistributor.
/// The size defined here is for each vcpu.
/// The total size is 'number_of_vcpu * GIC_V3_REDIST_SIZE'
@@ -73,9 +67,9 @@ pub const GIC_V3_REDIST_SIZE: u64 = 0x02_0000;
pub const GIC_V3_ITS_SIZE: u64 = 0x02_0000;
/// Space 0x0900_0000 ~ 0x0905_0000 is reserved for legacy devices.
pub const LEGACY_SERIAL_MAPPED_IO_START: GuestAddress = MAPPED_IO_START;
pub const LEGACY_RTC_MAPPED_IO_START: GuestAddress = GuestAddress(0x0901_0000);
pub const LEGACY_GPIO_MAPPED_IO_START: GuestAddress = GuestAddress(0x0902_0000);
pub const LEGACY_SERIAL_MAPPED_IO_START: u64 = 0x0900_0000;
pub const LEGACY_RTC_MAPPED_IO_START: u64 = 0x0901_0000;
pub const LEGACY_GPIO_MAPPED_IO_START: u64 = 0x0902_0000;
/// Space 0x0905_0000 ~ 0x0906_0000 is reserved for pcie io address
pub const MEM_PCI_IO_START: GuestAddress = GuestAddress(0x0905_0000);
@@ -91,41 +85,28 @@ pub const PCI_MMCONFIG_SIZE: u64 = 256 << 20;
// One bus with potentially 256 devices (32 slots x 8 functions).
pub const PCI_MMIO_CONFIG_SIZE_PER_SEGMENT: u64 = 4096 * 256;
/// Start of RAM.
pub const RAM_START: GuestAddress = GuestAddress(0x4000_0000);
/// 32-bit reserved area: 64MiB before 4GiB
pub const MEM_32BIT_RESERVED_START: GuestAddress = GuestAddress(0xfc00_0000);
pub const MEM_32BIT_RESERVED_SIZE: u64 = 0x0400_0000;
/// TPM Address Range
/// This Address range is specific to CRB Interface
pub const TPM_START: GuestAddress = GuestAddress(0xfed4_0000);
pub const TPM_SIZE: u64 = 0x1000;
/// Start of 64-bit RAM.
pub const RAM_64BIT_START: GuestAddress = GuestAddress(0x1_0000_0000);
/// Start of RAM on 64 bit ARM.
pub const RAM_64BIT_START: u64 = 0x4000_0000;
/// Kernel command line maximum size.
/// As per `arch/arm64/include/uapi/asm/setup.h`.
pub const CMDLINE_MAX_SIZE: usize = 2048;
/// FDT is at the beginning of RAM.
pub const FDT_START: GuestAddress = RAM_START;
/// Maximum size of the device tree blob as specified in [the kernel
/// documentation](https://www.kernel.org/doc/Documentation/arm64/booting.txt).
pub const FDT_MAX_SIZE: u64 = 0x20_0000;
/// Maximum size of the device tree blob as specified in https://www.kernel.org/doc/Documentation/arm64/booting.txt.
pub const FDT_START: u64 = RAM_64BIT_START;
pub const FDT_MAX_SIZE: usize = 0x20_0000;
/// Put ACPI table above dtb
pub const ACPI_START: GuestAddress = GuestAddress(RAM_START.0 + FDT_MAX_SIZE);
pub const ACPI_MAX_SIZE: u64 = 0x20_0000;
pub const RSDP_POINTER: GuestAddress = ACPI_START;
pub const ACPI_START: u64 = RAM_64BIT_START + FDT_MAX_SIZE as u64;
pub const ACPI_MAX_SIZE: usize = 0x20_0000;
pub const RSDP_POINTER: GuestAddress = GuestAddress(ACPI_START);
/// Kernel start after FDT and ACPI
pub const KERNEL_START: GuestAddress = GuestAddress(ACPI_START.0 + ACPI_MAX_SIZE);
pub const KERNEL_START: u64 = ACPI_START + ACPI_MAX_SIZE as u64;
/// Pci high memory base
pub const PCI_HIGH_BASE: GuestAddress = GuestAddress(0x2_0000_0000);
pub const PCI_HIGH_BASE: u64 = 0x2_0000_0000_u64;
// As per virt/kvm/arm/vgic/vgic-kvm-device.c we need
// the number of interrupts our GIC will support to be:
@@ -138,12 +119,3 @@ pub const IRQ_BASE: u32 = 32;
/// Number of supported interrupts
pub const IRQ_NUM: u32 = 256;
/// Base SPI interrupt number for the GICv2M MSI frame
pub const GICV2M_SPI_BASE: u32 = 128;
/// Total number of SPIs for the GICv2M MSI frame
pub const GICV2M_SPI_NUM: u32 = 64;
/// GICv2M compatible string
pub const GIC_V2M_COMPATIBLE: &str = "arm,gic-v2m-frame";

View File

@@ -2,61 +2,53 @@
// 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;
/// Module for the global interrupt controller configuration.
pub mod gic;
/// Layout for this aarch64 system.
pub mod layout;
/// Logic for configuring aarch64 registers.
pub mod regs;
/// Module for loading UEFI binary.
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;
use hypervisor::arch::aarch64::regs::MPIDR_EL1;
use log::{Level, log_enabled};
use thiserror::Error;
use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic};
pub use self::fdt::DeviceInfoForFdt;
use crate::{DeviceType, GuestMemoryMmap, NumaNodes, PciSpaceInfo, RegionType};
pub const _NSIG: i32 = 65;
use gic::GicDevice;
use log::{log_enabled, Level};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt::Debug;
use std::sync::Arc;
use vm_memory::{Address, GuestAddress, GuestMemory, GuestUsize};
/// Errors thrown while configuring aarch64 system.
#[derive(Debug, Error)]
#[derive(Debug)]
pub enum Error {
/// Failed to create a FDT.
#[error("Failed to create a FDT")]
SetupFdt,
/// Failed to write FDT to memory.
#[error("Failed to write FDT to memory")]
WriteFdtToMemory(#[source] fdt::Error),
WriteFdtToMemory(fdt::Error),
/// Failed to create a GIC.
#[error("Failed to create a GIC")]
SetupGic,
SetupGic(gic::Error),
/// Failed to compute the initramfs address.
#[error("Failed to compute the initramfs address")]
InitramfsAddress,
/// Error configuring the general purpose registers
#[error("Error configuring the general purpose registers")]
RegsConfiguration(#[source] hypervisor::HypervisorCpuError),
RegsConfiguration(regs::Error),
/// Error configuring the MPIDR register
#[error("Error configuring the MPIDR register")]
VcpuRegMpidr(#[source] hypervisor::HypervisorCpuError),
VcpuRegMpidr(hypervisor::HypervisorCpuError),
}
/// Error initializing PMU for vcpu
#[error("Error initializing PMU for vcpu")]
VcpuInitPmu,
impl From<Error> for super::Error {
fn from(e: Error) -> super::Error {
super::Error::AArch64Setup(e)
}
}
#[derive(Debug, Copy, Clone)]
@@ -69,29 +61,45 @@ pub struct EntryPoint {
/// Configure the specified VCPU, and return its MPIDR.
pub fn configure_vcpu(
vcpu: &dyn hypervisor::Vcpu,
id: u32,
boot_setup: Option<(EntryPoint, &GuestMemoryAtomic<GuestMemoryMmap>)>,
fd: &Arc<dyn hypervisor::Vcpu>,
id: u8,
kernel_entry_point: Option<EntryPoint>,
) -> super::Result<u64> {
if let Some((kernel_entry_point, _guest_memory)) = boot_setup {
vcpu.setup_regs(
id,
kernel_entry_point.entry_addr.raw_value(),
super::layout::FDT_START.raw_value(),
)
.map_err(Error::RegsConfiguration)?;
if let Some(kernel_entry_point) = kernel_entry_point {
regs::setup_regs(fd, id, kernel_entry_point.entry_addr.raw_value())
.map_err(Error::RegsConfiguration)?;
}
let mpidr = vcpu.get_sys_reg(MPIDR_EL1).map_err(Error::VcpuRegMpidr)?;
let mpidr = fd.read_mpidr().map_err(Error::VcpuRegMpidr)?;
Ok(mpidr)
}
pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
pub fn arch_memory_regions(size: GuestUsize) -> Vec<(GuestAddress, usize, RegionType)> {
// Normally UEFI should be loaded to a flash area at the beginning of memory.
// But now flash memory type is not supported.
// As a workaround, we take 4 MiB memory from the main RAM for UEFI.
// As a result, the RAM that the guest can see is less than what has been
// assigned in command line, when ACPI and UEFI is enabled.
let ram_deduction = if cfg!(feature = "acpi") {
layout::UEFI_SIZE
} else {
0
};
vec![
// 0 MiB ~ 256 MiB: UEFI, GIC and legacy devices
// 0 ~ 4 MiB: Reserved for UEFI space
#[cfg(feature = "acpi")]
(GuestAddress(0), layout::UEFI_SIZE as usize, RegionType::Ram),
#[cfg(not(feature = "acpi"))]
(
GuestAddress(0),
layout::MEM_32BIT_DEVICES_START.0 as usize,
layout::UEFI_SIZE as usize,
RegionType::Reserved,
),
// 4 MiB ~ 256 MiB: Gic and legacy devices
(
GuestAddress(layout::UEFI_SIZE),
(layout::MEM_32BIT_DEVICES_START.0 - layout::UEFI_SIZE) as usize,
RegionType::Reserved,
),
// 256 MiB ~ 768 MiB: MMIO space
@@ -106,37 +114,28 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
layout::PCI_MMCONFIG_SIZE as usize,
RegionType::Reserved,
),
// 1GiB ~ 4032 MiB: RAM before the gap
// 1 GiB ~ : Ram
(
layout::RAM_START,
layout::MEM_32BIT_RESERVED_START.unchecked_offset_from(layout::RAM_START) as usize,
GuestAddress(layout::RAM_64BIT_START),
(size - ram_deduction) as usize,
RegionType::Ram,
),
// 4GiB ~ inf: RAM after the gap
(layout::RAM_64BIT_START, usize::MAX, RegionType::Ram),
// Add the 32-bit reserved memory hole as a reserved region
(
layout::MEM_32BIT_RESERVED_START,
layout::MEM_32BIT_RESERVED_SIZE as usize,
RegionType::Reserved,
),
]
}
/// 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],
vcpu_topology: Option<(u16, u16, u16, u16)>,
vcpu_mpidr: Vec<u64>,
vcpu_topology: Option<(u8, u8, u8)>,
device_info: &HashMap<(DeviceType, String), T, S>,
initrd: &Option<super::InitramfsConfig>,
pci_space_info: &[PciSpaceInfo],
virtio_iommu_bdf: Option<u32>,
gic_device: &Arc<Mutex<dyn Vgic>>,
gic_device: &dyn GicDevice,
numa_nodes: &NumaNodes,
pmu_supported: bool,
) -> super::Result<()> {
let fdt_final = fdt::create_fdt(
guest_mem,
@@ -149,7 +148,6 @@ pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
pci_space_info,
numa_nodes,
virtio_iommu_bdf,
pmu_supported,
)
.map_err(|_| Error::SetupFdt)?;
@@ -157,7 +155,7 @@ pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
fdt::print_fdt(&fdt_final);
}
fdt::write_fdt_to_memory(&fdt_final, guest_mem).map_err(Error::WriteFdtToMemory)?;
fdt::write_fdt_to_memory(fdt_final, guest_mem).map_err(Error::WriteFdtToMemory)?;
Ok(())
}
@@ -176,15 +174,33 @@ pub fn initramfs_load_addr(
if guest_mem.address_in_range(offset) {
Ok(offset.raw_value())
} else {
Err(super::Error::PlatformSpecific(Error::InitramfsAddress))
Err(super::Error::AArch64Setup(Error::InitramfsAddress))
}
}
None => Err(super::Error::PlatformSpecific(Error::InitramfsAddress)),
None => Err(super::Error::AArch64Setup(Error::InitramfsAddress)),
}
}
pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
let host_cpu_phys_bits = hypervisor.get_host_ipa_limit().try_into().unwrap();
/// Returns the memory address where the kernel could be loaded.
pub fn get_kernel_start() -> u64 {
layout::KERNEL_START
}
///Return guest memory address where the uefi should be loaded.
pub fn get_uefi_start() -> u64 {
layout::UEFI_START
}
// Auxiliary function to get the address where the device tree blob is loaded.
fn get_fdt_addr() -> u64 {
layout::FDT_START
}
pub fn get_host_cpu_phys_bits() -> u8 {
// A dummy hypervisor created only for querying the host IPA size and will
// be freed after the query.
let hv = hypervisor::new().unwrap();
let host_cpu_phys_bits = hv.get_host_ipa_limit().try_into().unwrap();
if host_cpu_phys_bits == 0 {
// Host kernel does not support `get_host_ipa_limit`,
// we return the default value 40 here.
@@ -195,16 +211,15 @@ pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
}
#[cfg(test)]
mod unit_tests {
mod tests {
use super::*;
#[test]
fn test_arch_memory_regions_dram() {
let regions = arch_memory_regions();
assert_eq!(6, regions.len());
assert_eq!(layout::RAM_START, regions[3].0);
assert_eq!(RegionType::Ram, regions[3].2);
assert_eq!(RegionType::Reserved, regions[5].2);
let regions = arch_memory_regions((1usize << 32) as u64); //4GB
assert_eq!(5, regions.len());
assert_eq!(GuestAddress(layout::RAM_64BIT_START), regions[4].0);
assert_eq!(1usize << 32, regions[4].1);
assert_eq!(RegionType::Ram, regions[4].2);
}
}

75
arch/src/aarch64/regs.rs Normal file
View File

@@ -0,0 +1,75 @@
// 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 super::get_fdt_addr;
use hypervisor::kvm::kvm_bindings::{
kvm_regs, user_pt_regs, KVM_REG_ARM64, KVM_REG_ARM_CORE, KVM_REG_SIZE_U64,
};
use hypervisor::{arm64_core_reg_id, offset__of};
use std::sync::Arc;
use std::{mem, result};
/// Errors thrown while setting aarch64 registers.
#[derive(Debug)]
pub enum Error {
/// Failed to set core register (PC, PSTATE or general purpose ones).
SetCoreRegister(hypervisor::HypervisorCpuError),
/// Failed to get a system register.
GetSysRegister(hypervisor::HypervisorCpuError),
}
type Result<T> = result::Result<T, Error>;
#[allow(non_upper_case_globals)]
// PSR (Processor State Register) bits.
// Taken from arch/arm64/include/uapi/asm/ptrace.h.
const PSR_MODE_EL1h: u64 = 0x0000_0005;
const PSR_F_BIT: u64 = 0x0000_0040;
const PSR_I_BIT: u64 = 0x0000_0080;
const PSR_A_BIT: u64 = 0x0000_0100;
const PSR_D_BIT: u64 = 0x0000_0200;
// Taken from arch/arm64/kvm/inject_fault.c.
const PSTATE_FAULT_BITS_64: u64 = PSR_MODE_EL1h | PSR_A_BIT | PSR_F_BIT | PSR_I_BIT | PSR_D_BIT;
/// Configure core registers for a given CPU.
///
/// # Arguments
///
/// * `vcpu` - Structure for the VCPU that holds the VCPU's fd.
/// * `cpu_id` - Index of current vcpu.
/// * `boot_ip` - Starting instruction pointer.
/// * `mem` - Reserved DRAM for current VM.
pub fn setup_regs(vcpu: &Arc<dyn hypervisor::Vcpu>, cpu_id: u8, boot_ip: u64) -> Result<()> {
let kreg_off = offset__of!(kvm_regs, regs);
// Get the register index of the PSTATE (Processor State) register.
let pstate = offset__of!(user_pt_regs, pstate) + kreg_off;
vcpu.set_reg(
arm64_core_reg_id!(KVM_REG_SIZE_U64, pstate),
PSTATE_FAULT_BITS_64,
)
.map_err(Error::SetCoreRegister)?;
// Other vCPUs are powered off initially awaiting PSCI wakeup.
if cpu_id == 0 {
// Setting the PC (Processor Counter) to the current program address (kernel address).
let pc = offset__of!(user_pt_regs, pc) + kreg_off;
vcpu.set_reg(arm64_core_reg_id!(KVM_REG_SIZE_U64, pc), boot_ip as u64)
.map_err(Error::SetCoreRegister)?;
// Last mandatory thing to set -> the address pointing to the FDT (also called DTB).
// "The device tree blob (dtb) must be placed on an 8-byte boundary and must
// not exceed 2 megabytes in size." -> https://www.kernel.org/doc/Documentation/arm64/booting.txt.
// We are choosing to place it the end of DRAM. See `get_fdt_addr`.
let regs0 = offset__of!(user_pt_regs, regs) + kreg_off;
vcpu.set_reg(
arm64_core_reg_id!(KVM_REG_SIZE_U64, regs0),
get_fdt_addr() as u64,
)
.map_err(Error::SetCoreRegister)?;
}
Ok(())
}

View File

@@ -1,28 +1,19 @@
// Copyright 2020 Arm Limited (or its affiliates). All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
use std::io::{Read, Seek, SeekFrom};
use std::os::fd::AsFd;
use std::result;
use thiserror::Error;
use vm_memory::{Bytes, GuestAddress, GuestMemory};
/// Errors thrown while loading UEFI binary
#[derive(Debug, Error)]
#[derive(Debug)]
pub enum Error {
/// Unable to seek to UEFI image start.
#[error("Unable to seek to UEFI image start")]
SeekUefiStart,
/// Unable to seek to UEFI image end.
#[error("Unable to seek to UEFI image end")]
SeekUefiEnd,
/// UEFI image too big.
#[error("UEFI image too big")]
UefiTooBig,
/// Unable to read UEFI image
#[error("Unable to read UEFI image")]
ReadUefiImage,
}
type Result<T> = result::Result<T, Error>;
@@ -33,7 +24,7 @@ pub fn load_uefi<F, M: GuestMemory>(
uefi_image: &mut F,
) -> Result<()>
where
F: Read + Seek + AsFd,
F: Read + Seek,
{
let uefi_size = uefi_image
.seek(SeekFrom::End(0))
@@ -43,8 +34,10 @@ where
if uefi_size > 0x300000 {
return Err(Error::UefiTooBig);
}
uefi_image.rewind().map_err(|_| Error::SeekUefiStart)?;
uefi_image
.seek(SeekFrom::Start(0))
.map_err(|_| Error::SeekUefiStart)?;
guest_mem
.read_exact_volatile_from(guest_addr, &mut uefi_image.as_fd(), uefi_size)
.read_exact_from(guest_addr, uefi_image, uefi_size)
.map_err(|_| Error::ReadUefiImage)
}

View File

@@ -1,4 +1,3 @@
// Copyright © 2024 Institute of Software, CAS. All rights reserved.
// Copyright 2020 Arm Limited (or its affiliates). All rights reserved.
// Copyright © 2020, Oracle and/or its affiliates.
//
@@ -6,80 +5,60 @@
// SPDX-License-Identifier: Apache-2.0
//! Implements platform specific functionality.
//! Supported platforms: x86_64, aarch64, riscv64.
//! Supported platforms: x86_64, aarch64.
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[cfg(target_arch = "x86_64")]
use crate::x86_64::SgxEpcSection;
use std::collections::BTreeMap;
use std::str::FromStr;
use std::fmt;
use std::result;
use std::sync::Arc;
use std::{fmt, result};
use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult};
use versionize_derive::Versionize;
use vm_migration::VersionMapped;
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)]
#[derive(Debug)]
pub enum Error {
#[cfg(target_arch = "x86_64")]
#[error("Platform specific error (x86_64)")]
PlatformSpecific(#[from] x86_64::Error),
/// X86_64 specific error triggered during system configuration.
X86_64Setup(x86_64::Error),
#[cfg(target_arch = "aarch64")]
#[error("Platform specific error (aarch64)")]
PlatformSpecific(#[from] aarch64::Error),
#[cfg(target_arch = "riscv64")]
#[error("Platform specific error (riscv64)")]
PlatformSpecific(#[from] riscv64::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,
#[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),
#[error("Failed to compute initramfs address")]
InitramfsAddress,
#[error("Error writing module entry to guest memory")]
ModlistSetup(#[source] vm_memory::GuestMemoryError),
#[error("RSDP extends past the end of guest memory")]
RsdpPastRamEnd,
#[error("Failed to setup Zero Page for bzImage")]
ZeroPageSetup(#[source] vm_memory::GuestMemoryError),
#[error("Zero Page for bzImage past RAM end")]
/// AArch64 specific error triggered during system configuration.
AArch64Setup(aarch64::Error),
/// The zero page extends past the end of guest_mem.
ZeroPagePastRamEnd,
/// Error writing the zero page of guest memory.
ZeroPageSetup(vm_memory::GuestMemoryError),
/// The memory map table extends past the end of guest memory.
MemmapTablePastRamEnd,
/// Error writing memory map table to guest memory.
MemmapTableSetup,
/// The hvm_start_info structure extends past the end of guest memory.
StartInfoPastRamEnd,
/// Error writing hvm_start_info to guest memory.
StartInfoSetup,
/// Failed to compute initramfs address.
InitramfsAddress,
/// Error writing module entry to guest memory.
ModlistSetup(vm_memory::GuestMemoryError),
/// RSDP Beyond Guest Memory
RsdpPastRamEnd,
}
/// 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)]
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Versionize)]
pub enum RegionType {
/// RAM type
Ram,
@@ -97,26 +76,17 @@ pub enum RegionType {
Reserved,
}
impl VersionMapped for RegionType {}
/// Module for aarch64 related functionality.
#[cfg(target_arch = "aarch64")]
pub mod aarch64;
#[cfg(target_arch = "aarch64")]
pub use aarch64::{
_NSIG, EntryPoint, arch_memory_regions, configure_system, configure_vcpu,
fdt::DeviceInfoForFdt, get_host_cpu_phys_bits, initramfs_load_addr, layout,
layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, uefi,
};
/// Module for riscv64 related functionality.
#[cfg(target_arch = "riscv64")]
pub mod riscv64;
#[cfg(target_arch = "riscv64")]
pub use riscv64::{
_NSIG, EntryPoint, arch_memory_regions, configure_system, configure_vcpu,
fdt::DeviceInfoForFdt, get_host_cpu_phys_bits, initramfs_load_addr, layout,
layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, uefi,
arch_memory_regions, configure_system, configure_vcpu, fdt::DeviceInfoForFdt,
get_host_cpu_phys_bits, get_kernel_start, get_uefi_start, initramfs_load_addr, layout,
layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, uefi, EntryPoint,
};
#[cfg(target_arch = "x86_64")]
@@ -124,17 +94,16 @@ 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,
arch_memory_regions, configure_system, configure_vcpu, generate_common_cpuid,
get_host_cpu_phys_bits, initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE,
layout::CMDLINE_START, regs,
layout::CMDLINE_START, regs, CpuidFeatureEntry, EntryPoint,
};
/// Safe wrapper for `sysconf(_SC_PAGESIZE)`.
#[cfg(target_arch = "x86_64")]
#[inline(always)]
fn pagesize() -> usize {
// SAFETY: Trivially safe
// Trivially safe
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
}
@@ -142,11 +111,11 @@ fn pagesize() -> usize {
pub struct NumaNode {
pub memory_regions: Vec<Arc<GuestRegionMmap>>,
pub hotplug_regions: Vec<Arc<GuestRegionMmap>>,
pub cpus: Vec<u32>,
pub pci_segments: Vec<u16>,
pub cpus: Vec<u8>,
pub distances: BTreeMap<u32, u8>,
pub memory_zones: Vec<String>,
pub device_id: Option<String>,
#[cfg(target_arch = "x86_64")]
pub sgx_epc_sections: Vec<SgxEpcSection>,
}
pub type NumaNodes = BTreeMap<u32, NumaNode>;
@@ -165,7 +134,7 @@ pub enum DeviceType {
/// Device Type: Virtio.
Virtio(u32),
/// Device Type: Serial.
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
#[cfg(target_arch = "aarch64")]
Serial,
/// Device Type: RTC.
#[cfg(target_arch = "aarch64")]
@@ -173,9 +142,6 @@ pub enum DeviceType {
/// Device Type: GPIO.
#[cfg(target_arch = "aarch64")]
Gpio,
/// Device Type: fw_cfg.
#[cfg(feature = "fw_cfg")]
FwCfg,
}
/// Default (smallest) memory page size for the supported architectures.
@@ -183,22 +149,21 @@ pub const PAGE_SIZE: usize = 4096;
impl fmt::Display for DeviceType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
write!(f, "{:?}", self)
}
}
/// Structure to describe MMIO device information
#[derive(Clone, Debug)]
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
#[cfg(target_arch = "aarch64")]
pub struct MmioDeviceInfo {
pub addr: u64,
pub len: u64,
pub irq: u32,
}
/// Structure to describe PCI space information
#[derive(Clone, Debug)]
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
#[cfg(target_arch = "aarch64")]
pub struct PciSpaceInfo {
pub pci_segment_id: u16,
pub mmio_config_address: u64,
@@ -206,7 +171,7 @@ pub struct PciSpaceInfo {
pub pci_device_space_size: u64,
}
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
#[cfg(target_arch = "aarch64")]
impl DeviceInfoForFdt for MmioDeviceInfo {
fn addr(&self) -> u64 {
self.addr
@@ -215,6 +180,6 @@ impl DeviceInfoForFdt for MmioDeviceInfo {
self.irq
}
fn length(&self) -> u64 {
self.len
4096
}
}

View File

@@ -1,487 +0,0 @@
// Copyright © 2024 Institute of Software, CAS. All rights reserved.
// 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::collections::HashMap;
use std::ffi::CStr;
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use std::{cmp, result, str};
use byteorder::{BigEndian, ByteOrder};
use hypervisor::arch::riscv64::aia::Vaia;
use log::debug;
use thiserror::Error;
use vm_fdt::{FdtWriter, FdtWriterResult};
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion};
use super::super::{DeviceType, GuestMemoryMmap, InitramfsConfig};
use super::layout::{
IRQ_BASE, MEM_32BIT_DEVICES_SIZE, MEM_32BIT_DEVICES_START, MEM_PCI_IO_SIZE, MEM_PCI_IO_START,
PCI_HIGH_BASE, PCI_MMIO_CONFIG_SIZE_PER_SEGMENT,
};
use crate::PciSpaceInfo;
const AIA_APLIC_PHANDLE: u32 = 1;
const AIA_IMSIC_PHANDLE: u32 = 2;
const CPU_INTC_BASE_PHANDLE: u32 = 3;
const CPU_BASE_PHANDLE: u32 = 256 + CPU_INTC_BASE_PHANDLE;
// Read the documentation specified when appending the root node to the FDT.
const ADDRESS_CELLS: u32 = 0x2;
const SIZE_CELLS: u32 = 0x2;
// From https://elixir.bootlin.com/linux/v6.10/source/include/dt-bindings/interrupt-controller/irq.h#L14
const _IRQ_TYPE_EDGE_RISING: u32 = 1;
const IRQ_TYPE_LEVEL_HI: u32 = 4;
const S_MODE_EXT_IRQ: u32 = 9;
/// Trait for devices to be added to the Flattened Device Tree.
pub trait DeviceInfoForFdt {
/// Returns the address where this device will be loaded.
fn addr(&self) -> u64;
/// Returns the associated interrupt for this device.
fn irq(&self) -> u32;
/// Returns the amount of memory that needs to be reserved for this device.
fn length(&self) -> u64;
}
/// Errors thrown while configuring the Flattened Device Tree for riscv64.
#[derive(Debug, Error)]
pub enum Error {
/// Failure in writing FDT in memory.
#[error("Failure in writing FDT in memory")]
WriteFdtToMemory(#[source] GuestMemoryError),
}
type Result<T> = result::Result<T, Error>;
/// Creates the flattened device tree for this riscv64 VM.
#[expect(clippy::too_many_arguments)]
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
num_vcpu: u32,
isa_string: &str,
device_info: &HashMap<(DeviceType, String), T, S>,
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()?;
// For an explanation why these nodes were introduced in the blob take a look at
// https://github.com/devicetree-org/devicetree-specification/releases/tag/v0.4
// In chapter 3.
// Header or the root node as per above mentioned documentation.
let root_node = fdt.begin_node("")?;
fdt.property_string("compatible", "linux,dummy-virt")?;
// For info on #address-cells and size-cells resort to Table 3.1 Root Node
// 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_memory_node(&mut fdt, guest_mem)?;
create_chosen_node(&mut fdt, cmdline, initrd)?;
create_aia_node(&mut fdt, aia_device)?;
create_devices_node(&mut fdt, device_info)?;
create_pci_nodes(&mut fdt, pci_space_info)?;
// End Header node.
fdt.end_node(root_node)?;
let fdt_final = fdt.finish()?;
Ok(fdt_final)
}
pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Result<()> {
// Write FDT to memory.
guest_mem
.write_slice(fdt_final, super::layout::FDT_START)
.map_err(Error::WriteFdtToMemory)?;
Ok(())
}
// 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<()> {
// 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)?;
fdt.property_u32("timebase-frequency", timebase_frequency)?;
for cpu_index in 0..num_cpus {
let cpu = fdt.begin_node(&format!("cpu@{cpu_index:x}"))?;
fdt.property_string("device_type", "cpu")?;
fdt.property_string("compatible", "riscv")?;
fdt.property_string("mmu-type", "sv48")?;
fdt.property_string("riscv,isa", isa_string)?;
fdt.property_string("status", "okay")?;
fdt.property_u32("reg", cpu_index)?;
fdt.property_u32("phandle", CPU_BASE_PHANDLE + cpu_index)?;
// interrupt controller node
let intc_node = fdt.begin_node("interrupt-controller")?;
fdt.property_string("compatible", "riscv,cpu-intc")?;
fdt.property_u32("#interrupt-cells", 1u32)?;
fdt.property_null("interrupt-controller")?;
fdt.property_u32("phandle", CPU_INTC_BASE_PHANDLE + cpu_index)?;
fdt.end_node(intc_node)?;
fdt.end_node(cpu)?;
}
fdt.end_node(cpus)?;
Ok(())
}
fn create_memory_node(fdt: &mut FdtWriter, guest_mem: &GuestMemoryMmap) -> FdtWriterResult<()> {
// Note: memory regions from "GuestMemory" are sorted and non-zero sized.
let ram_regions = {
let mut ram_regions = Vec::new();
let mut current_start = guest_mem
.iter()
.next()
.map(GuestMemoryRegion::start_addr)
.expect("GuestMemory must have one memory region at least")
.raw_value();
let mut current_end = current_start;
for (start, size) in guest_mem
.iter()
.map(|m| (m.start_addr().raw_value(), m.len()))
{
if current_end == start {
// This zone is continuous with the previous one.
current_end += size;
} else {
ram_regions.push((current_start, current_end));
current_start = start;
current_end = start + size;
}
}
ram_regions.push((current_start, current_end));
ram_regions
};
let mut mem_reg_property = Vec::new();
for region in ram_regions {
let mem_size = region.1 - region.0;
mem_reg_property.push(region.0);
mem_reg_property.push(mem_size);
}
let ram_start = super::layout::RAM_START.raw_value();
let memory_node_name = format!("memory@{ram_start:x}");
let memory_node = fdt.begin_node(&memory_node_name)?;
fdt.property_string("device_type", "memory")?;
fdt.property_array_u64("reg", &mem_reg_property)?;
fdt.end_node(memory_node)?;
Ok(())
}
fn create_chosen_node(
fdt: &mut FdtWriter,
cmdline: &str,
initrd: &Option<InitramfsConfig>,
) -> FdtWriterResult<()> {
let chosen_node = fdt.begin_node("chosen")?;
fdt.property_string("bootargs", cmdline)?;
if let Some(initrd_config) = initrd {
let initrd_start = initrd_config.address.raw_value();
let initrd_end = initrd_config.address.raw_value() + initrd_config.size as u64;
fdt.property_u64("linux,initrd-start", initrd_start)?;
fdt.property_u64("linux,initrd-end", initrd_end)?;
}
fdt.end_node(chosen_node)?;
Ok(())
}
fn create_aia_node(fdt: &mut FdtWriter, aia_device: &Arc<Mutex<dyn Vaia>>) -> FdtWriterResult<()> {
// IMSIC
if aia_device.lock().unwrap().msi_compatible() {
use super::layout::IMSIC_START;
let imsic_name = format!("imsics@{:x}", IMSIC_START.0);
let imsic_node = fdt.begin_node(&imsic_name)?;
fdt.property_string(
"compatible",
aia_device.lock().unwrap().imsic_compatibility(),
)?;
let imsic_reg_prop = aia_device.lock().unwrap().imsic_properties();
fdt.property_array_u32("reg", &imsic_reg_prop)?;
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)?;
fdt.property_u32("phandle", AIA_IMSIC_PHANDLE)?;
let mut irq_cells = Vec::new();
let num_cpus = aia_device.lock().unwrap().vcpu_count();
for i in 0..num_cpus {
irq_cells.push(CPU_INTC_BASE_PHANDLE + i);
irq_cells.push(S_MODE_EXT_IRQ);
}
fdt.property_array_u32("interrupts-extended", &irq_cells)?;
fdt.end_node(imsic_node)?;
}
// APLIC
use super::layout::APLIC_START;
let aplic_name = format!("aplic@{:x}", APLIC_START.0);
let aplic_node = fdt.begin_node(&aplic_name)?;
fdt.property_string(
"compatible",
aia_device.lock().unwrap().aplic_compatibility(),
)?;
let reg_cells = aia_device.lock().unwrap().aplic_properties();
fdt.property_array_u32("reg", &reg_cells)?;
fdt.property_u32("#interrupt-cells", 2u32)?;
fdt.property_null("interrupt-controller")?;
// TODO complete num-srcs
fdt.property_u32("riscv,num-sources", 96u32)?;
fdt.property_u32("phandle", AIA_APLIC_PHANDLE)?;
fdt.property_u32("msi-parent", AIA_IMSIC_PHANDLE)?;
fdt.end_node(aplic_node)?;
Ok(())
}
fn create_serial_node<T: DeviceInfoForFdt + Clone + Debug>(
fdt: &mut FdtWriter,
dev_info: &T,
) -> FdtWriterResult<()> {
let serial_reg_prop = [dev_info.addr(), dev_info.length()];
let irq = [dev_info.irq() - IRQ_BASE, IRQ_TYPE_LEVEL_HI];
let serial_node = fdt.begin_node(&format!("serial@{:x}", dev_info.addr()))?;
fdt.property_string("compatible", "ns16550a")?;
fdt.property_array_u64("reg", &serial_reg_prop)?;
fdt.property_u32("clock-frequency", 3686400)?;
fdt.property_u32("interrupt-parent", AIA_APLIC_PHANDLE)?;
fdt.property_array_u32("interrupts", &irq)?;
fdt.end_node(serial_node)?;
Ok(())
}
fn create_devices_node<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
fdt: &mut FdtWriter,
dev_info: &HashMap<(DeviceType, String), T, S>,
) -> FdtWriterResult<()> {
for ((device_type, _device_id), info) in dev_info {
match device_type {
DeviceType::Serial => create_serial_node(fdt, info)?,
DeviceType::Virtio(_) => unreachable!(),
}
}
Ok(())
}
fn create_pci_nodes(fdt: &mut FdtWriter, pci_device_info: &[PciSpaceInfo]) -> FdtWriterResult<()> {
// Add node for PCIe controller.
// See Documentation/devicetree/bindings/pci/host-generic-pci.txt in the kernel
// and https://elinux.org/Device_Tree_Usage.
// In multiple PCI segments setup, each PCI segment needs a PCI node.
for pci_device_info_elem in pci_device_info.iter() {
// EDK2 requires the PCIe high space above 4G address.
// The actual space in CLH follows the RAM. If the RAM space is small, the PCIe high space
// could fall below 4G.
// Here we cut off PCI device space below 8G in FDT to workaround the EDK2 check.
// But the address written in ACPI is not impacted.
let (pci_device_base_64bit, pci_device_size_64bit) =
if pci_device_info_elem.pci_device_space_start < PCI_HIGH_BASE.raw_value() {
(
PCI_HIGH_BASE.raw_value(),
pci_device_info_elem.pci_device_space_size
- (PCI_HIGH_BASE.raw_value() - pci_device_info_elem.pci_device_space_start),
)
} else {
(
pci_device_info_elem.pci_device_space_start,
pci_device_info_elem.pci_device_space_size,
)
};
// There is no specific requirement of the 32bit MMIO range, and
// therefore at least we can make these ranges 4K aligned.
let pci_device_size_32bit: u64 =
MEM_32BIT_DEVICES_SIZE / ((1 << 12) * pci_device_info.len() as u64) * (1 << 12);
let pci_device_base_32bit: u64 = MEM_32BIT_DEVICES_START.0
+ pci_device_size_32bit * pci_device_info_elem.pci_segment_id as u64;
let ranges = [
// io addresses. Since AArch64 will not use IO address,
// we can set the same IO address range for every segment.
0x1000000,
0_u32,
0_u32,
(MEM_PCI_IO_START.0 >> 32) as u32,
MEM_PCI_IO_START.0 as u32,
(MEM_PCI_IO_SIZE >> 32) as u32,
MEM_PCI_IO_SIZE as u32,
// mmio addresses
0x2000000, // (ss = 10: 32-bit memory space)
(pci_device_base_32bit >> 32) as u32, // PCI address
pci_device_base_32bit as u32,
(pci_device_base_32bit >> 32) as u32, // CPU address
pci_device_base_32bit as u32,
(pci_device_size_32bit >> 32) as u32, // size
pci_device_size_32bit as u32,
// device addresses
0x3000000, // (ss = 11: 64-bit memory space)
(pci_device_base_64bit >> 32) as u32, // PCI address
pci_device_base_64bit as u32,
(pci_device_base_64bit >> 32) as u32, // CPU address
pci_device_base_64bit as u32,
(pci_device_size_64bit >> 32) as u32, // size
pci_device_size_64bit as u32,
];
let bus_range = [0, 0]; // Only bus 0
let reg = [
pci_device_info_elem.mmio_config_address,
PCI_MMIO_CONFIG_SIZE_PER_SEGMENT,
];
// See kernel document Documentation/devicetree/bindings/pci/pci-msi.txt
let msi_map = [
// rid-base: A single cell describing the first RID matched by the entry.
0x0,
// msi-controller: A single phandle to an MSI controller.
AIA_IMSIC_PHANDLE,
// msi-base: An msi-specifier describing the msi-specifier produced for the
// first RID matched by the entry.
(pci_device_info_elem.pci_segment_id as u32) << 8,
// length: A single cell describing how many consecutive RIDs are matched
// following the rid-base.
0x100,
];
let pci_node_name = format!("pci@{:x}", pci_device_info_elem.mmio_config_address);
let pci_node = fdt.begin_node(&pci_node_name)?;
fdt.property_string("compatible", "pci-host-ecam-generic")?;
fdt.property_string("device_type", "pci")?;
fdt.property_array_u32("ranges", &ranges)?;
fdt.property_array_u32("bus-range", &bus_range)?;
fdt.property_u32(
"linux,pci-domain",
pci_device_info_elem.pci_segment_id as u32,
)?;
fdt.property_u32("#address-cells", 3)?;
fdt.property_u32("#size-cells", 2)?;
fdt.property_array_u64("reg", &reg)?;
fdt.property_u32("#interrupt-cells", 1)?;
fdt.property_null("interrupt-map")?;
fdt.property_null("interrupt-map-mask")?;
fdt.property_null("dma-coherent")?;
fdt.property_array_u32("msi-map", &msi_map)?;
fdt.property_u32("msi-parent", AIA_IMSIC_PHANDLE)?;
fdt.end_node(pci_node)?;
}
Ok(())
}
// Parse the DTB binary and print for debugging
pub fn print_fdt(dtb: &[u8]) {
match fdt_parser::Fdt::new(dtb) {
Ok(fdt) => {
if let Some(root) = fdt.find_node("/") {
debug!("Printing the FDT:");
print_node(root, 0);
} else {
debug!("Failed to find root node in FDT for debugging.");
}
}
Err(_) => debug!("Failed to parse FDT for debugging."),
}
}
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;
// If the property is 'compatible', its value requires special handling.
// The u8 array could contain multiple null-terminated strings.
// We copy the original array and simply replace all 'null' characters with spaces.
let value = if name == "compatible" {
let mut compatible = vec![0u8; 256];
let handled_value = property
.value
.iter()
.map(|&c| if c == 0 { b' ' } else { c })
.collect::<Vec<_>>();
let len = cmp::min(255, handled_value.len());
compatible[..len].copy_from_slice(&handled_value[..len]);
compatible[..(len + 1)].to_vec()
} else {
property.value.to_vec()
};
let value = &value;
// Now the value can be either:
// - A null-terminated C string, or
// - Binary data
// We follow a very simple logic to present the value:
// - At first, try to convert it to CStr and print,
// - If failed, print it as u32 array.
let value_result = match CStr::from_bytes_with_nul(value) {
Ok(value_cstr) => value_cstr.to_str().ok(),
Err(_e) => None,
};
if let Some(value_str) = value_result {
debug!(
"{:indent$}{} : {:#?}",
"",
name,
value_str,
indent = (n_spaces + 2)
);
} else {
let mut array = Vec::with_capacity(256);
array.resize(value.len() / 4, 0u32);
BigEndian::read_u32_into(value, &mut array);
debug!(
"{:indent$}{} : {:X?}",
"",
name,
array,
indent = (n_spaces + 2)
);
}
}
// Print children nodes if there is any
for child in node.children() {
print_node(child, n_spaces + 2);
}
}

View File

@@ -1,117 +0,0 @@
// Copyright © 2024 Institute of Software, CAS. All rights reserved.
// 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
//
// Memory layout of RISC-V 64-bit guest:
//
// Physical +---------------------------------------------------------------+
// address | |
// end | |
// ~ ~ ~ ~
// | |
// | Highmem PCI MMIO space |
// | |
// RAM end +---------------------------------------------------------------+
// (dynamic, | |
// including | |
// hotplug ~ ~ ~ ~
// memory) | |
// | DRAM |
// | |
// | |
// | |
// | |
// 1 GB +---------------------------------------------------------------+
// | |
// | PCI MMCONFIG space |
// | |
// 768 MB +---------------------------------------------------------------+
// | |
// | |
// | PCI MMIO space |
// | |
// 256 MB +---------------------------------------------------------------|
// | |
// | Legacy devices space |
// | |
// 128 MB +---------------------------------------------------------------|
// | |
// | IMSICs |
// | |
// 64 MB +---------------------------------------------------------------+
// | |
// | APLICs |
// | |
// 4 MB +---------------------------------------------------------------+
// | UEFI flash |
// 0 GB +---------------------------------------------------------------+
//
//
use vm_memory::GuestAddress;
/// 0x0 ~ 0x40_0000 (4 MiB) is reserved to UEFI
/// UEFI binary size is required less than 3 MiB, reserving 4 MiB is enough.
pub const UEFI_START: GuestAddress = GuestAddress(0);
pub const UEFI_SIZE: u64 = 0x040_0000;
/// AIA related devices
/// See https://elixir.bootlin.com/linux/v6.10/source/arch/riscv/include/uapi/asm/kvm.h
/// 0x40_0000 ~ 0x0400_0000 (64 MiB) resides APLICs
pub const APLIC_START: GuestAddress = GuestAddress(0x40_0000);
pub const APLIC_SIZE: u64 = 0x4000;
/// 0x0400_0000 ~ 0x0800_0000 (64 MiB) resides IMSICs
pub const IMSIC_START: GuestAddress = GuestAddress(0x0400_0000);
pub const IMSIC_SIZE: u64 = 0x1000;
/// Below this address will reside the AIA, above this address will reside the MMIO devices.
const MAPPED_IO_START: GuestAddress = GuestAddress(0x0800_0000);
/// Space 0x0800_0000 ~ 0x1000_0000 is reserved for legacy devices.
pub const LEGACY_SERIAL_MAPPED_IO_START: GuestAddress = MAPPED_IO_START;
/// Space 0x0905_0000 ~ 0x0906_0000 is reserved for pcie io address
pub const MEM_PCI_IO_START: GuestAddress = GuestAddress(0x0905_0000);
pub const MEM_PCI_IO_SIZE: u64 = 0x1_0000;
/// Starting from 0x1000_0000 (256MiB) to 0x3000_0000 (768MiB) is used for PCIE MMIO
pub const MEM_32BIT_DEVICES_START: GuestAddress = GuestAddress(0x1000_0000);
pub const MEM_32BIT_DEVICES_SIZE: u64 = 0x2000_0000;
/// PCI MMCONFIG space (start: after the device space at 768MiB, length: 256MiB)
pub const PCI_MMCONFIG_START: GuestAddress = GuestAddress(0x3000_0000);
pub const PCI_MMCONFIG_SIZE: u64 = 256 << 20;
// One bus with potentially 256 devices (32 slots x 8 functions).
pub const PCI_MMIO_CONFIG_SIZE_PER_SEGMENT: u64 = 4096 * 256;
/// Start of RAM.
pub const RAM_START: GuestAddress = GuestAddress(0x4000_0000);
/// Kernel command line maximum size on RISC-V.
/// See https://elixir.bootlin.com/linux/v6.10/source/arch/riscv/include/uapi/asm/setup.h
pub const CMDLINE_MAX_SIZE: usize = 1024;
/// FDT is at the beginning of RAM.
pub const FDT_START: GuestAddress = RAM_START;
pub const FDT_MAX_SIZE: u64 = 0x1_0000;
/// Put ACPI table above dtb
pub const ACPI_START: GuestAddress = GuestAddress(RAM_START.0 + FDT_MAX_SIZE);
pub const ACPI_MAX_SIZE: u64 = 0x20_0000;
pub const RSDP_POINTER: GuestAddress = ACPI_START;
/// Kernel start after FDT and ACPI
pub const KERNEL_START: GuestAddress = GuestAddress(RAM_START.0 + FDT_MAX_SIZE);
/// Pci high memory base
pub const PCI_HIGH_BASE: GuestAddress = GuestAddress(0x2_0000_0000);
/// First usable interrupt on riscv64
pub const IRQ_BASE: u32 = 0;
// As per https://elixir.bootlin.com/linux/v6.10/source/arch/riscv/include/asm/kvm_host.h#L31
/// Number of supported interrupts
pub const IRQ_NUM: u32 = 1023;

View File

@@ -1,232 +0,0 @@
// Copyright © 2024 Institute of Software, CAS. All rights reserved.
// 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
/// Module for the flattened device tree.
pub mod fdt;
/// Layout for this riscv64 system.
pub mod layout;
/// Module for loading UEFI binary.
pub mod uefi;
use std::collections::HashMap;
use std::fmt::Debug;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::sync::{Arc, Mutex};
use hypervisor::arch::riscv64::aia::Vaia;
use log::{Level, log_enabled};
use thiserror::Error;
use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic};
pub use self::fdt::DeviceInfoForFdt;
use crate::{DeviceType, GuestMemoryMmap, PciSpaceInfo, RegionType};
pub const CLOUDHV_IRQCHIP_NUM_MSIS: u16 = 255;
pub const CLOUDHV_IRQCHIP_NUM_SOURCES: u8 = 96;
pub const CLOUDHV_IRQCHIP_NUM_PRIO_BITS: u8 = 3;
pub const CLOUDHV_IRQCHIP_MAX_GUESTS_BITS: u8 = 3;
pub const CLOUDHV_IRQCHIP_MAX_GUESTS: u8 = (1 << CLOUDHV_IRQCHIP_MAX_GUESTS_BITS) - 1;
pub const _NSIG: i32 = 65;
/// Errors thrown while configuring riscv64 system.
#[derive(Debug, Error)]
pub enum Error {
/// Failed to create a FDT.
#[error("Failed to create a FDT")]
SetupFdt,
/// Failed to write FDT to memory.
#[error("Failed to write FDT to memory")]
WriteFdtToMemory(#[source] fdt::Error),
/// Failed to create a AIA.
#[error("Failed to create a AIA")]
SetupAia,
/// Failed to compute the initramfs address.
#[error("Failed to compute the initramfs address")]
InitramfsAddress,
/// Error configuring the general purpose registers
#[error("Error configuring the general purpose registers")]
RegsConfiguration(#[source] hypervisor::HypervisorCpuError),
/// Error opening /proc/cpuinfo
#[error("Error opening /proc/cpuinfo")]
OpenCpuInfo(#[source] std::io::Error),
/// Error reading /proc/cpuinfo
#[error("Error reading /proc/cpuinfo")]
ReadCpuInfo(#[source] std::io::Error),
/// Invalid ISA string
#[error("Invalid ISA string: {0}")]
InvalidIsaString(String),
/// Error parsing /proc/cpuinfo
#[error("Error parsing /proc/cpuinfo")]
CpuInfoParsing,
}
#[derive(Debug, Copy, Clone)]
/// Specifies the entry point address where the guest must start
/// executing code.
pub struct EntryPoint {
/// Address in guest memory where the guest must start execution
pub entry_addr: GuestAddress,
}
/// Configure the specified VCPU, and return its MPIDR.
pub fn configure_vcpu(
vcpu: &dyn hypervisor::Vcpu,
id: u32,
boot_setup: Option<(EntryPoint, &GuestMemoryAtomic<GuestMemoryMmap>)>,
) -> super::Result<()> {
if let Some((kernel_entry_point, _guest_memory)) = boot_setup {
vcpu.setup_regs(
id,
kernel_entry_point.entry_addr.raw_value(),
layout::FDT_START.raw_value(),
)
.map_err(Error::RegsConfiguration)?;
}
Ok(())
}
pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
vec![
// 0 MiB ~ 256 MiB: AIA and legacy devices
(
GuestAddress(0),
layout::MEM_32BIT_DEVICES_START.0 as usize,
RegionType::Reserved,
),
// 256 MiB ~ 768 MiB: MMIO space
(
layout::MEM_32BIT_DEVICES_START,
layout::MEM_32BIT_DEVICES_SIZE as usize,
RegionType::SubRegion,
),
// 768 MiB ~ 1 GiB: reserved. The leading 256M for PCIe MMCONFIG space
(
layout::PCI_MMCONFIG_START,
layout::PCI_MMCONFIG_SIZE as usize,
RegionType::Reserved,
),
// 1GiB ~ inf: RAM
(layout::RAM_START, usize::MAX, RegionType::Ram),
]
}
// Read the first "isa" string from /proc/cpuinfo and filter out the H extension,
// while correctly preserving multi-letter extensions.
fn isa_string_from_host() -> Result<String, Error> {
let file = File::open("/proc/cpuinfo").map_err(Error::OpenCpuInfo)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line.map_err(Error::ReadCpuInfo)?;
let trimmed_line = line.trim();
if trimmed_line.starts_with("isa") {
let parts: Vec<&str> = trimmed_line.split(':').collect();
if parts.len() == 2 {
let isa_string = parts[1].trim();
// Split the string by underscores to separate single letter vs long-form
// extensions
let mut components: Vec<String> =
isa_string.split('_').map(|s| s.to_string()).collect();
if components.is_empty() {
return Err(Error::InvalidIsaString(isa_string.to_string()));
}
// Remove H extension if present in single letter extensions
let first_component = components[0].chars().filter(|&c| c != 'h').collect();
components[0] = first_component;
return Ok(components.join("_"));
}
}
}
Err(Error::CpuInfoParsing)
}
/// Configures the system and should be called once per vm before starting vcpu threads.
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
guest_mem: &GuestMemoryMmap,
cmdline: &str,
num_vcpu: u32,
device_info: &HashMap<(DeviceType, String), T, S>,
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(
guest_mem,
cmdline,
num_vcpu,
&isa_string,
device_info,
aia_device,
initrd,
pci_space_info,
timebase_frequency,
)
.map_err(|_| Error::SetupFdt)?;
if log_enabled!(Level::Debug) {
fdt::print_fdt(&fdt_final);
}
fdt::write_fdt_to_memory(&fdt_final, guest_mem).map_err(Error::WriteFdtToMemory)?;
Ok(())
}
/// Returns the memory address where the initramfs could be loaded.
pub fn initramfs_load_addr(
guest_mem: &GuestMemoryMmap,
initramfs_size: usize,
) -> super::Result<u64> {
let round_to_pagesize = |size| (size + (super::PAGE_SIZE - 1)) & !(super::PAGE_SIZE - 1);
match guest_mem
.last_addr()
.checked_sub(round_to_pagesize(initramfs_size) as u64 - 1)
{
Some(offset) => {
if guest_mem.address_in_range(offset) {
Ok(offset.raw_value())
} else {
Err(super::Error::PlatformSpecific(Error::InitramfsAddress))
}
}
None => Err(super::Error::PlatformSpecific(Error::InitramfsAddress)),
}
}
pub fn get_host_cpu_phys_bits(_hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
40
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_arch_memory_regions_dram() {
let regions = arch_memory_regions();
assert_eq!(4, regions.len());
assert_eq!(layout::RAM_START, regions[3].0);
assert_eq!(RegionType::Ram, regions[3].2);
}
}

View File

@@ -1,50 +0,0 @@
// Copyright 2020 Arm Limited (or its affiliates). All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
use std::io::{Read, Seek, SeekFrom};
use std::os::fd::AsFd;
use std::result;
use thiserror::Error;
use vm_memory::{Bytes, GuestAddress, GuestMemory};
/// Errors thrown while loading UEFI binary
#[derive(Debug, Error)]
pub enum Error {
/// Unable to seek to UEFI image start.
#[error("Unable to seek to UEFI image start")]
SeekUefiStart,
/// Unable to seek to UEFI image end.
#[error("Unable to seek to UEFI image end")]
SeekUefiEnd,
/// UEFI image too big.
#[error("UEFI image too big")]
UefiTooBig,
/// Unable to read UEFI image
#[error("Unable to read UEFI image")]
ReadUefiImage,
}
type Result<T> = result::Result<T, Error>;
pub fn load_uefi<F, M: GuestMemory>(
guest_mem: &M,
guest_addr: GuestAddress,
uefi_image: &mut F,
) -> Result<()>
where
F: Read + Seek + AsFd,
{
let uefi_size = uefi_image
.seek(SeekFrom::End(0))
.map_err(|_| Error::SeekUefiEnd)? as usize;
// edk2 image on virtual platform is smaller than 3M
if uefi_size > 0x300000 {
return Err(Error::UefiTooBig);
}
uefi_image.rewind().map_err(|_| Error::SeekUefiStart)?;
guest_mem
.read_exact_volatile_from(guest_addr, &mut uefi_image.as_fd(), uefi_size)
.map_err(|_| Error::ReadUefiImage)
}

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

@@ -5,7 +5,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use hypervisor::x86_64::LapicState;
use std::io::Cursor;
use std::mem;
use std::result;
use std::sync::Arc;
pub type Result<T> = result::Result<T, hypervisor::HypervisorCpuError>;
@@ -15,6 +20,32 @@ pub const APIC_LVT1: usize = 0x360;
pub const APIC_MODE_NMI: u32 = 0x4;
pub const APIC_MODE_EXTINT: u32 = 0x7;
pub fn get_klapic_reg(klapic: &LapicState, reg_offset: usize) -> u32 {
let sliceu8 = unsafe {
// This array is only accessed as parts of a u32 word, so interpret it as a u8 array.
// Cursors are only readable on arrays of u8, not i8(c_char).
mem::transmute::<&[i8], &[u8]>(&klapic.regs[reg_offset..])
};
let mut reader = Cursor::new(sliceu8);
// Following call can't fail if the offsets defined above are correct.
reader
.read_u32::<LittleEndian>()
.expect("Failed to read klapic register")
}
pub fn set_klapic_reg(klapic: &mut LapicState, reg_offset: usize, value: u32) {
let sliceu8 = unsafe {
// This array is only accessed as parts of a u32 word, so interpret it as a u8 array.
// Cursors are only readable on arrays of u8, not i8(c_char).
mem::transmute::<&mut [i8], &mut [u8]>(&mut klapic.regs[reg_offset..])
};
let mut writer = Cursor::new(sliceu8);
// Following call can't fail if the offsets defined above are correct.
writer
.write_u32::<LittleEndian>(value)
.expect("Failed to write klapic register")
}
pub fn set_apic_delivery_mode(reg: u32, mode: u32) -> u32 {
((reg) & !0x700) | ((mode) << 8)
}
@@ -23,16 +54,45 @@ pub fn set_apic_delivery_mode(reg: u32, mode: u32) -> u32 {
///
/// # Arguments
/// * `vcpu` - The VCPU object to configure.
pub fn set_lint(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
pub fn set_lint(vcpu: &Arc<dyn hypervisor::Vcpu>) -> Result<()> {
let mut klapic = vcpu.get_lapic()?;
let lvt_lint0 = klapic.get_klapic_reg(APIC_LVT0);
klapic.set_klapic_reg(
let lvt_lint0 = get_klapic_reg(&klapic, APIC_LVT0);
set_klapic_reg(
&mut klapic,
APIC_LVT0,
set_apic_delivery_mode(lvt_lint0, APIC_MODE_EXTINT),
);
let lvt_lint1 = klapic.get_klapic_reg(APIC_LVT1);
klapic.set_klapic_reg(APIC_LVT1, set_apic_delivery_mode(lvt_lint1, APIC_MODE_NMI));
let lvt_lint1 = get_klapic_reg(&klapic, APIC_LVT1);
set_klapic_reg(
&mut klapic,
APIC_LVT1,
set_apic_delivery_mode(lvt_lint1, APIC_MODE_NMI),
);
vcpu.set_lapic(&klapic)
}
#[cfg(test)]
mod tests {
use super::*;
const KVM_APIC_REG_SIZE: usize = 0x400;
#[test]
fn test_set_and_get_klapic_reg() {
let reg_offset = 0x340;
let mut klapic = LapicState::default();
set_klapic_reg(&mut klapic, reg_offset, 3);
let value = get_klapic_reg(&klapic, reg_offset);
assert_eq!(value, 3);
}
#[test]
#[should_panic]
fn test_set_and_get_klapic_out_of_bounds() {
let reg_offset = KVM_APIC_REG_SIZE + 10;
let mut klapic = LapicState::default();
set_klapic_reg(&mut klapic, reg_offset, 3);
}
}

View File

@@ -24,9 +24,6 @@ pub const LOW_RAM_START: GuestAddress = GuestAddress(0x0);
// == Fixed addresses within the "Low RAM" range: ==
// Location of EBDA address
pub const EBDA_POINTER: GuestAddress = GuestAddress(0x40e);
// Initial GDT/IDT needed to boot kernel
pub const BOOT_GDT_START: GuestAddress = GuestAddress(0x500);
pub const BOOT_IDT_START: GuestAddress = GuestAddress(0x520);
@@ -107,11 +104,6 @@ pub const KVM_TSS_SIZE: u64 = (3 * 4) << 10;
pub const KVM_IDENTITY_MAP_START: GuestAddress = GuestAddress(KVM_TSS_START.0 + KVM_TSS_SIZE);
pub const KVM_IDENTITY_MAP_SIZE: u64 = 4 << 10;
/// TPM Address Range
/// This Address range is specific to CRB Interface
pub const TPM_START: GuestAddress = GuestAddress(0xfed4_0000);
pub const TPM_SIZE: u64 = 0x1000;
// IOAPIC
pub const IOAPIC_START: GuestAddress = GuestAddress(0xfec0_0000);
pub const IOAPIC_SIZE: u64 = 0x20;

File diff suppressed because it is too large Load Diff

View File

@@ -1,179 +1,112 @@
// Copyright 2017 The Chromium OS Authors. All rights reserved.
//
// 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;
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_char; 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);
// 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 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_char; 4usize],
pub length: ::std::os::raw::c_ushort,
pub spec: ::std::os::raw::c_char,
pub checksum: ::std::os::raw::c_char,
pub oem: [::std::os::raw::c_char; 8usize],
pub productid: [::std::os::raw::c_char; 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);
};
// 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_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);
// 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_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);
// 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_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);
// 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_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);
// 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);
// 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_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_char; 4usize],
pub length: ::std::os::raw::c_ushort,
pub rev: ::std::os::raw::c_char,
pub checksum: ::std::os::raw::c_char,
pub mpc: [::std::os::raw::c_char; 8usize],
}
const _: () = assert!(size_of::<mpc_oemtable>() == 16);
// SAFETY: all members of this struct are plain integers
// and the sum of their sizes is the size of the struct, so
// padding and reserved values are not possible as there
// would be nowhere for them to exist.
unsafe impl ByteValued for mpc_oemtable {}

View File

@@ -5,17 +5,15 @@
// 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 libc::c_uchar;
use log::{info, warn};
use thiserror::Error;
use vm_memory::{Address, ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError};
use super::MAX_SUPPORTED_CPUS_LEGACY;
use crate::GuestMemoryMmap;
use crate::layout::{APIC_START, HIGH_RAM_START, IOAPIC_START};
use crate::x86_64::{get_x2apic_id, mpspec};
use crate::x86_64::mpspec;
use crate::GuestMemoryMmap;
use libc::c_char;
use std::io;
use std::mem;
use std::result;
use std::slice;
use vm_memory::{Address, ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError};
// This is a workaround to the Rust enforcement specifying that any implementation of a foreign
// trait (in this case `ByteValued`) where:
@@ -39,70 +37,68 @@ struct MpfIntelWrapper(mpspec::mpf_intel);
// SAFETY: These `mpspec` wrapper types are only data, reading them from data is a safe initialization.
unsafe impl ByteValued for MpcBusWrapper {}
// SAFETY: see above
unsafe impl ByteValued for MpcCpuWrapper {}
// SAFETY: see above
unsafe impl ByteValued for MpcIntsrcWrapper {}
// SAFETY: see above
unsafe impl ByteValued for MpcIoapicWrapper {}
// SAFETY: see above
unsafe impl ByteValued for MpcTableWrapper {}
// SAFETY: see above
unsafe impl ByteValued for MpcLintsrcWrapper {}
// SAFETY: see above
unsafe impl ByteValued for MpfIntelWrapper {}
#[derive(Debug, Error)]
#[derive(Debug)]
pub enum Error {
/// There was too little guest memory to store the entire MP table.
#[error("There was too little guest memory to store the entire MP table")]
NotEnoughMemory,
/// The MP table has too little address space to be stored.
#[error("The MP table has too little address space to be stored")]
AddressOverflow,
/// Failure while zeroing out the memory for the MP table.
#[error("Failure while zeroing out the memory for the MP table")]
Clear(#[source] GuestMemoryError),
Clear(GuestMemoryError),
/// Number of CPUs exceeds the maximum supported CPUs
TooManyCpus,
/// Failure to write the MP floating pointer.
#[error("Failure to write the MP floating pointer")]
WriteMpfIntel(#[source] GuestMemoryError),
WriteMpfIntel(GuestMemoryError),
/// Failure to write MP CPU entry.
#[error("Failure to write MP CPU entry")]
WriteMpcCpu(#[source] GuestMemoryError),
WriteMpcCpu(GuestMemoryError),
/// Failure to write MP ioapic entry.
#[error("Failure to write MP ioapic entry")]
WriteMpcIoapic(#[source] GuestMemoryError),
WriteMpcIoapic(GuestMemoryError),
/// Failure to write MP bus entry.
#[error("Failure to write MP bus entry")]
WriteMpcBus(#[source] GuestMemoryError),
WriteMpcBus(GuestMemoryError),
/// Failure to write MP interrupt source entry.
#[error("Failure to write MP interrupt source entry")]
WriteMpcIntsrc(#[source] GuestMemoryError),
WriteMpcIntsrc(GuestMemoryError),
/// Failure to write MP local interrupt source entry.
#[error("Failure to write MP local interrupt source entry")]
WriteMpcLintsrc(#[source] GuestMemoryError),
WriteMpcLintsrc(GuestMemoryError),
/// Failure to write MP table header.
#[error("Failure to write MP table header")]
WriteMpcTable(#[source] GuestMemoryError),
WriteMpcTable(GuestMemoryError),
}
pub type Result<T> = result::Result<T, Error>;
// With APIC/xAPIC, there are only 255 APIC IDs available. And IOAPIC occupies
// one APIC ID, so only 254 CPUs at maximum may be supported. Actually it's
// a large number for FC usecases.
pub const MAX_SUPPORTED_CPUS: u32 = 254;
// Convenience macro for making arrays of diverse character types.
macro_rules! char_array {
($t:ty; $( $c:expr ),*) => ( [ $( $c as $t ),* ] )
}
// Most of these variables are sourced from the Intel MP Spec 1.4.
const SMP_MAGIC_IDENT: &[c_uchar; 4] = b"_MP_";
const MPC_SIGNATURE: &[c_uchar; 4] = b"PCMP";
const MPC_SPEC: u8 = 4;
const MPC_OEM: &[c_uchar; 8] = b"FC ";
const MPC_PRODUCT_ID: &[c_uchar; 12] = &[b'0'; 12];
const BUS_TYPE_ISA: &[c_uchar; 6] = b"ISA ";
const SMP_MAGIC_IDENT: [c_char; 4] = char_array!(c_char; '_', 'M', 'P', '_');
const MPC_SIGNATURE: [c_char; 4] = char_array!(c_char; 'P', 'C', 'M', 'P');
const MPC_SPEC: i8 = 4;
const MPC_OEM: [c_char; 8] = char_array!(c_char; 'F', 'C', ' ', ' ', ' ', ' ', ' ', ' ');
const MPC_PRODUCT_ID: [c_char; 12] = ['0' as c_char; 12];
const BUS_TYPE_ISA: [u8; 6] = char_array!(u8; 'I', 'S', 'A', ' ', ' ', ' ');
const APIC_VERSION: u8 = 0x14;
const CPU_STEPPING: u32 = 0x600;
const CPU_FEATURE_APIC: u32 = 0x200;
const CPU_FEATURE_FPU: u32 = 0x001;
fn compute_checksum<T: Copy + ByteValued>(v: &T) -> u8 {
fn compute_checksum<T: Copy>(v: &T) -> u8 {
// Safe because 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
@@ -113,30 +109,20 @@ fn mpf_intel_compute_checksum(v: &mpspec::mpf_intel) -> u8 {
(!checksum).wrapping_add(1)
}
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
fn compute_mp_size(num_cpus: u8) -> usize {
mem::size_of::<MpfIntelWrapper>()
+ mem::size_of::<MpcTableWrapper>()
+ mem::size_of::<MpcCpuWrapper>() * (num_cpus as usize)
+ mem::size_of::<MpcIoapicWrapper>()
+ mem::size_of::<MpcBusWrapper>()
+ mem::size_of::<MpcIntsrcWrapper>() * 16
+ mem::size_of::<MpcLintsrcWrapper>() * 2
}
/// Performs setup of the MP table for the given `num_cpus`.
pub fn setup_mptable(
offset: GuestAddress,
mem: &GuestMemoryMmap,
num_cpus: u32,
topology: Option<(u16, u16, u16, u16)>,
) -> Result<()> {
if num_cpus > 0 {
let cpu_id_max = num_cpus - 1;
let x2apic_id_max = get_x2apic_id(cpu_id_max, topology);
if x2apic_id_max >= MAX_SUPPORTED_CPUS_LEGACY {
info!("Skipping mptable creation due to too many CPUs");
return Ok(());
}
pub fn setup_mptable(offset: GuestAddress, mem: &GuestMemoryMmap, num_cpus: u8) -> Result<()> {
if num_cpus as u32 > MAX_SUPPORTED_CPUS {
return Err(Error::TooManyCpus);
}
// Used to keep track of the next base pointer into the MP table.
@@ -150,7 +136,7 @@ pub fn setup_mptable(
}
let mut checksum: u8 = 0;
let ioapicid: u8 = MAX_SUPPORTED_CPUS_LEGACY as u8 + 1;
let ioapicid: u8 = num_cpus + 1;
// The checked_add here ensures the all of the following base_mp.unchecked_add's will be without
// overflow.
@@ -162,13 +148,13 @@ pub fn setup_mptable(
return Err(Error::AddressOverflow);
}
mem.read_exact_volatile_from(base_mp, &mut vec![0; mp_size].as_slice(), mp_size)
mem.read_exact_from(base_mp, &mut io::repeat(0), mp_size)
.map_err(Error::Clear)?;
{
let mut mpf_intel = MpfIntelWrapper(mpspec::mpf_intel::default());
let size = size_of::<MpfIntelWrapper>() as u64;
mpf_intel.0.signature = *SMP_MAGIC_IDENT;
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;
mpf_intel.0.physptr = (base_mp.raw_value() + size) as u32;
@@ -181,14 +167,14 @@ 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;
mpc_cpu.0.apicid = get_x2apic_id(cpu_id, topology) as u8;
mpc_cpu.0.apicid = cpu_id;
mpc_cpu.0.apicver = APIC_VERSION;
mpc_cpu.0.cpuflag = mpspec::CPU_ENABLED as u8
| if cpu_id == 0 {
@@ -205,18 +191,18 @@ 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;
mpc_bus.0.bustype = *BUS_TYPE_ISA;
mpc_bus.0.bustype = BUS_TYPE_ISA;
mem.write_obj(mpc_bus, base_mp)
.map_err(Error::WriteMpcBus)?;
base_mp = base_mp.unchecked_add(size as u64);
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 +216,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 +231,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 +246,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;
@@ -280,14 +266,14 @@ pub fn setup_mptable(
{
let mut mpc_table = MpcTableWrapper(mpspec::mpc_table::default());
mpc_table.0.signature = *MPC_SIGNATURE;
mpc_table.0.signature = MPC_SIGNATURE;
mpc_table.0.length = table_end.unchecked_offset_from(table_base) as u16;
mpc_table.0.spec = MPC_SPEC;
mpc_table.0.oem = *MPC_OEM;
mpc_table.0.productid = *MPC_PRODUCT_ID;
mpc_table.0.oem = MPC_OEM;
mpc_table.0.productid = MPC_PRODUCT_ID;
mpc_table.0.lapic = APIC_START.0 as u32;
checksum = checksum.wrapping_add(compute_checksum(&mpc_table.0));
mpc_table.0.checksum = (!checksum).wrapping_add(1);
mpc_table.0.checksum = (!checksum).wrapping_add(1) as i8;
mem.write_obj(mpc_table, table_base)
.map_err(Error::WriteMpcTable)?;
}
@@ -296,21 +282,19 @@ pub fn setup_mptable(
}
#[cfg(test)]
mod unit_tests {
use vm_memory::bitmap::BitmapSlice;
use vm_memory::{GuestUsize, VolatileMemoryError, VolatileSlice, WriteVolatile};
mod tests {
use super::*;
use crate::layout::MPTABLE_START;
use vm_memory::{GuestAddress, GuestUsize};
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>(),
_ => panic!("unrecognized mpc table entry type: {type_}"),
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_),
}
}
@@ -320,7 +304,7 @@ mod unit_tests {
let mem =
GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus))]).unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus).unwrap();
}
#[test]
@@ -329,7 +313,7 @@ mod unit_tests {
let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus) - 1)])
.unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap_err();
assert!(setup_mptable(MPTABLE_START, &mem, num_cpus).is_err());
}
#[test]
@@ -338,7 +322,7 @@ mod unit_tests {
let mem =
GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus))]).unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus).unwrap();
let mpf_intel: MpfIntelWrapper = mem.read_obj(MPTABLE_START).unwrap();
@@ -354,31 +338,27 @@ mod unit_tests {
let mem =
GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus))]).unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap();
setup_mptable(MPTABLE_START, &mem, num_cpus).unwrap();
let mpf_intel: MpfIntelWrapper = mem.read_obj(MPTABLE_START).unwrap();
let mpc_offset = GuestAddress(mpf_intel.0.physptr as GuestUsize);
let mpc_table: MpcTableWrapper = mem.read_obj(mpc_offset).unwrap();
struct Sum(u8);
impl WriteVolatile for Sum {
fn write_volatile<B: BitmapSlice>(
&mut self,
buf: &VolatileSlice<B>,
) -> result::Result<usize, VolatileMemoryError> {
let mut tmp = vec![0u8; buf.len()];
tmp.write_all_volatile(buf)?;
for v in tmp.iter() {
impl io::Write for Sum {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for v in buf.iter() {
self.0 = self.0.wrapping_add(*v);
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let mut sum = Sum(0);
mem.write_volatile_to(mpc_offset, &mut sum, mpc_table.0.length as usize)
mem.write_to(mpc_offset, &mut sum, mpc_table.0.length as usize)
.unwrap();
assert_eq!(sum.0, 0);
}
@@ -387,12 +367,12 @@ mod unit_tests {
fn cpu_entry_count() {
let mem = GuestMemoryMmap::from_ranges(&[(
MPTABLE_START,
compute_mp_size(MAX_SUPPORTED_CPUS_LEGACY),
compute_mp_size(MAX_SUPPORTED_CPUS as u8),
)])
.unwrap();
for i in 0..MAX_SUPPORTED_CPUS_LEGACY {
setup_mptable(MPTABLE_START, &mem, i, None).unwrap();
for i in 0..MAX_SUPPORTED_CPUS as u8 {
setup_mptable(MPTABLE_START, &mem, i).unwrap();
let mpf_intel: MpfIntelWrapper = mem.read_obj(MPTABLE_START).unwrap();
let mpc_offset = GuestAddress(mpf_intel.0.physptr as GuestUsize);
@@ -402,7 +382,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 {
@@ -421,9 +401,11 @@ mod unit_tests {
#[test]
fn cpu_entry_count_max() {
let cpus = MAX_SUPPORTED_CPUS_LEGACY + 1;
let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(cpus))]).unwrap();
let cpus = MAX_SUPPORTED_CPUS + 1;
let mem =
GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(cpus as u8))]).unwrap();
setup_mptable(MPTABLE_START, &mem, cpus, None).unwrap();
let result = setup_mptable(MPTABLE_START, &mem, cpus as u8);
assert!(result.is_err());
}
}

View File

@@ -6,57 +6,41 @@
// 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 crate::layout::{BOOT_GDT_START, BOOT_IDT_START, PVH_INFO_START};
use crate::GuestMemoryMmap;
use hypervisor::arch::x86::gdt::{gdt_entry, segment_from_gdt};
use hypervisor::arch::x86::regs::CR0_PE;
use hypervisor::arch::x86::{FpuState, SpecialRegisters};
use thiserror::Error;
use hypervisor::arch::x86::regs::*;
use hypervisor::x86_64::{FpuState, SpecialRegisters, StandardRegisters};
use std::sync::Arc;
use std::{mem, result};
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError};
use crate::layout::{
BOOT_GDT_START, BOOT_IDT_START, BOOT_STACK_POINTER, PVH_INFO_START, ZERO_PAGE_START,
};
use crate::{EntryPoint, GuestMemoryMmap};
#[derive(Debug, Error)]
#[derive(Debug)]
pub enum Error {
/// Failed to get SREGs for this CPU.
#[error("Failed to get SREGs for this CPU")]
GetStatusRegisters(#[source] hypervisor::HypervisorCpuError),
GetStatusRegisters(hypervisor::HypervisorCpuError),
/// Failed to set base registers for this CPU.
#[error("Failed to set base registers for this CPU")]
SetBaseRegisters(#[source] hypervisor::HypervisorCpuError),
SetBaseRegisters(hypervisor::HypervisorCpuError),
/// Failed to configure the FPU.
#[error("Failed to configure the FPU")]
SetFpuRegisters(#[source] hypervisor::HypervisorCpuError),
SetFpuRegisters(hypervisor::HypervisorCpuError),
/// Setting up MSRs failed.
#[error("Setting up MSRs failed")]
SetModelSpecificRegisters(#[source] hypervisor::HypervisorCpuError),
SetModelSpecificRegisters(hypervisor::HypervisorCpuError),
/// Failed to set SREGs for this CPU.
#[error("Failed to set SREGs for this CPU")]
SetStatusRegisters(#[source] hypervisor::HypervisorCpuError),
SetStatusRegisters(hypervisor::HypervisorCpuError),
/// Checking the GDT address failed.
#[error("Checking the GDT address failed")]
CheckGdtAddr,
/// Writing the GDT to RAM failed.
#[error("Writing the GDT to RAM failed")]
WriteGdt(#[source] GuestMemoryError),
WriteGdt(GuestMemoryError),
/// Writing the IDT to RAM failed.
#[error("Writing the IDT to RAM failed")]
WriteIdt(#[source] GuestMemoryError),
WriteIdt(GuestMemoryError),
/// Writing PDPTE to RAM failed.
#[error("Writing PDPTE to RAM failed")]
WritePdpteAddress(#[source] GuestMemoryError),
WritePdpteAddress(GuestMemoryError),
/// Writing PDE to RAM failed.
#[error("Writing PDE to RAM failed")]
WritePdeAddress(#[source] GuestMemoryError),
WritePdeAddress(GuestMemoryError),
/// Writing PML4 to RAM failed.
#[error("Writing PML4 to RAM failed")]
WritePml4Address(#[source] GuestMemoryError),
WritePml4Address(GuestMemoryError),
/// Writing PML5 to RAM failed.
#[error("Writing PML5 to RAM failed")]
WritePml5Address(#[source] GuestMemoryError),
WritePml5Address(GuestMemoryError),
}
pub type Result<T> = result::Result<T, Error>;
@@ -66,7 +50,7 @@ pub type Result<T> = result::Result<T, Error>;
/// # Arguments
///
/// * `vcpu` - Structure for the VCPU that holds the VCPU's fd.
pub fn setup_fpu(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
pub fn setup_fpu(vcpu: &Arc<dyn hypervisor::Vcpu>) -> Result<()> {
let fpu: FpuState = FpuState {
fcw: 0x37f,
mxcsr: 0x1f80,
@@ -81,8 +65,8 @@ pub fn setup_fpu(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
/// # Arguments
///
/// * `vcpu` - Structure for the VCPU that holds the VCPU's fd.
pub fn setup_msrs(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
vcpu.set_msrs(vcpu.boot_msr_entries())
pub fn setup_msrs(vcpu: &Arc<dyn hypervisor::Vcpu>) -> Result<()> {
vcpu.set_msrs(&hypervisor::x86_64::boot_msr_entries())
.map_err(Error::SetModelSpecificRegisters)?;
Ok(())
@@ -93,22 +77,14 @@ pub fn setup_msrs(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
/// # Arguments
///
/// * `vcpu` - Structure for the VCPU that holds the VCPU's fd.
/// * `entry_point` - Description of the boot entry to set up.
pub fn setup_regs(vcpu: &dyn hypervisor::Vcpu, entry_point: EntryPoint) -> Result<()> {
let mut regs = vcpu.create_standard_regs();
match entry_point.setup_header {
None => {
regs.set_rflags(0x0000000000000002u64);
regs.set_rip(entry_point.entry_addr.raw_value());
regs.set_rbx(PVH_INFO_START.raw_value());
}
Some(_) => {
regs.set_rflags(0x0000000000000002u64);
regs.set_rip(entry_point.entry_addr.raw_value());
regs.set_rsp(BOOT_STACK_POINTER.raw_value());
regs.set_rsi(ZERO_PAGE_START.raw_value());
}
}
/// * `boot_ip` - Starting instruction pointer.
pub fn setup_regs(vcpu: &Arc<dyn hypervisor::Vcpu>, boot_ip: u64) -> Result<()> {
let regs = StandardRegisters {
rflags: 0x0000000000000002u64,
rbx: PVH_INFO_START.raw_value(),
rip: boot_ip,
..Default::default()
};
vcpu.set_regs(&regs).map_err(Error::SetBaseRegisters)
}
@@ -118,13 +94,9 @@ pub fn setup_regs(vcpu: &dyn hypervisor::Vcpu, entry_point: EntryPoint) -> Resul
///
/// * `mem` - The memory that will be passed to the guest.
/// * `vcpu` - Structure for the VCPU that holds the VCPU's fd.
pub fn setup_sregs(
mem: &GuestMemoryMmap,
vcpu: &dyn hypervisor::Vcpu,
enable_x2_apic_mode: bool,
) -> Result<()> {
pub fn setup_sregs(mem: &GuestMemoryMmap, vcpu: &Arc<dyn hypervisor::Vcpu>) -> Result<()> {
let mut sregs: SpecialRegisters = vcpu.get_sregs().map_err(Error::GetStatusRegisters)?;
configure_segments_and_sregs(mem, &mut sregs, enable_x2_apic_mode)?;
configure_segments_and_sregs(mem, &mut sregs)?;
vcpu.set_sregs(&sregs).map_err(Error::SetStatusRegisters)
}
@@ -134,7 +106,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)?;
}
@@ -151,9 +123,8 @@ fn write_idt_value(val: u64, guest_mem: &GuestMemoryMmap) -> Result<()> {
pub fn configure_segments_and_sregs(
mem: &GuestMemoryMmap,
sregs: &mut SpecialRegisters,
enable_x2_apic_mode: bool,
) -> Result<()> {
let gdt_table: [u64; BOOT_GDT_MAX] = {
let gdt_table: [u64; BOOT_GDT_MAX as usize] = {
// Configure GDT entries as specified by PVH boot protocol
[
gdt_entry(0, 0, 0), // NULL
@@ -170,11 +141,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;
@@ -187,19 +158,14 @@ pub fn configure_segments_and_sregs(
sregs.cr0 = CR0_PE;
sregs.cr4 = 0;
if enable_x2_apic_mode {
const X2APIC_ENABLE_BIT: u64 = 1 << 10;
sregs.apic_base |= X2APIC_ENABLE_BIT;
}
Ok(())
}
#[cfg(test)]
mod unit_tests {
use vm_memory::GuestAddress;
mod tests {
use super::*;
use crate::GuestMemoryMmap;
use vm_memory::GuestAddress;
fn create_guest_mem() -> GuestMemoryMmap {
GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap()
@@ -213,7 +179,7 @@ mod unit_tests {
fn segments_and_sregs() {
let mut sregs: SpecialRegisters = Default::default();
let gm = create_guest_mem();
configure_segments_and_sregs(&gm, &mut sregs, false).unwrap();
configure_segments_and_sregs(&gm, &mut sregs).unwrap();
assert_eq!(0x0, read_u64(&gm, BOOT_GDT_START));
assert_eq!(
0xcf9b000000ffff,

View File

@@ -6,207 +6,146 @@
//
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
use std::result;
use thiserror::Error;
use uuid::Uuid;
use vm_memory::{Address, ByteValued, Bytes, GuestAddress};
use crate::GuestMemoryMmap;
use crate::layout::SMBIOS_START;
use crate::GuestMemoryMmap;
use std::fmt::{self, Display};
use std::mem;
use std::result;
use std::slice;
use vm_memory::ByteValued;
use vm_memory::{Address, Bytes, GuestAddress};
#[derive(Debug, Error)]
#[allow(unused_variables)]
#[derive(Debug)]
pub enum Error {
/// There was too little guest memory to store the entire SMBIOS table.
#[error("There was too little guest memory to store the SMBIOS table")]
NotEnoughMemory,
/// The SMBIOS table has too little address space to be stored.
#[error("The SMBIOS table has too little address space to be stored")]
AddressOverflow,
/// Failure while zeroing out the memory for the SMBIOS table.
#[error("Failure while zeroing out the memory for the SMBIOS table")]
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),
/// 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,
WriteData,
}
impl std::error::Error for Error {}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
let description = match self {
NotEnoughMemory => "There was too little guest memory to store the SMBIOS table",
AddressOverflow => "The SMBIOS table has too little address space to be stored",
Clear => "Failure while zeroing out the memory for the SMBIOS table",
WriteSmbiosEp => "Failure to write SMBIOS entrypoint structure",
WriteData => "Failure to write additional data to memory",
};
write!(f, "SMBIOS error: {}", description)
}
}
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 {
// Safe because 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)]
#[derive(Default, Copy, Clone)]
struct Smbios30Entrypoint {
signature: [u8; 5usize],
checksum: u8,
length: u8,
majorver: u8,
minorver: u8,
docrev: u8,
revision: u8,
reserved: u8,
max_size: u32,
physptr: u64,
#[repr(packed)]
#[derive(Default, Copy)]
pub struct Smbios30Entrypoint {
pub signature: [u8; 5usize],
pub checksum: u8,
pub length: u8,
pub majorver: u8,
pub minorver: u8,
pub docrev: u8,
pub revision: u8,
pub reserved: u8,
pub max_size: u32,
pub physptr: u64,
}
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosBiosInfo {
r#type: u8,
length: u8,
handle: u16,
vendor: u8,
version: u8,
start_addr: u16,
release_date: u8,
rom_size: u8,
characteristics: u64,
characteristics_ext1: u8,
characteristics_ext2: u8,
impl Clone for Smbios30Entrypoint {
fn clone(&self) -> Self {
*self
}
}
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosSysInfo {
r#type: u8,
length: u8,
handle: u16,
manufacturer: u8,
product_name: u8,
version: u8,
serial_number: u8,
uuid: [u8; 16usize],
wake_up_type: u8,
sku: u8,
family: u8,
#[repr(packed)]
#[derive(Default, Copy)]
pub struct SmbiosBiosInfo {
pub typ: u8,
pub length: u8,
pub handle: u16,
pub vendor: u8,
pub version: u8,
pub start_addr: u16,
pub release_date: u8,
pub rom_size: u8,
pub characteristics: u64,
pub characteristics_ext1: u8,
pub characteristics_ext2: u8,
}
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosOemStrings {
r#type: u8,
length: u8,
handle: u16,
count: u8,
impl Clone for SmbiosBiosInfo {
fn clone(&self) -> Self {
*self
}
}
/// 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(packed)]
#[derive(Default, Copy)]
pub struct SmbiosSysInfo {
pub typ: u8,
pub length: u8,
pub handle: u16,
pub manufacturer: u8,
pub product_name: u8,
pub version: u8,
pub serial_number: u8,
pub uuid: [u8; 16usize],
pub wake_up_type: u8,
pub sku: u8,
pub family: u8,
}
#[repr(C, packed)]
#[derive(Default, Copy, Clone)]
struct SmbiosEndOfTable {
r#type: u8,
length: u8,
handle: u16,
impl Clone for SmbiosSysInfo {
fn clone(&self) -> Self {
*self
}
}
// SAFETY: data structure only contain a series of integers
// SAFETY: These data structures only contain a series of integers
unsafe impl ByteValued for Smbios30Entrypoint {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for SmbiosBiosInfo {}
// SAFETY: data structure only contain a series of integers
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>(
mem: &GuestMemoryMmap,
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,9 @@ fn write_string(
Ok(curptr)
}
fn write_opt_string(
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);
pub fn setup_smbios(mem: &GuestMemoryMmap) -> 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;
@@ -379,8 +172,8 @@ 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,
typ: BIOS_INFORMATION,
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,47 +187,38 @@ 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)?;
if let Some(chassis) = chassis {
write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?;
}
if !oem_strings.is_empty() {
{
handle += 1;
let smbios_oemstrings = SmbiosOemStrings {
r#type: OEM_STRINGS,
length: size_of::<SmbiosOemStrings>() as u8,
let smbios_sysinfo = SmbiosSysInfo {
typ: SYSTEM_INFORMATION,
length: mem::size_of::<SmbiosSysInfo>() as u8,
handle,
count: oem_strings.len() as u8,
manufacturer: 1, // First string written in this section
product_name: 2, // Second string written in this section
..Default::default()
};
curptr = write_and_incr(mem, smbios_oemstrings, curptr)?;
for s in oem_strings {
curptr = write_string(mem, s, curptr)?;
}
curptr = write_string_terminator(mem, curptr, true)?;
curptr = write_and_incr(mem, smbios_sysinfo, curptr)?;
curptr = write_string(mem, "Cloud Hypervisor", curptr)?;
curptr = write_string(mem, "cloud-hypervisor", curptr)?;
curptr = write_and_incr(mem, 0u8, curptr)?;
}
{
handle += 1;
let smbios_end = SmbiosEndOfTable {
r#type: END_OF_TABLE,
length: size_of::<SmbiosEndOfTable>() as u8,
let smbios_sysinfo = SmbiosSysInfo {
typ: END_OF_TABLE,
length: mem::size_of::<SmbiosSysInfo>() as u8,
handle,
..Default::default()
};
curptr = write_and_incr(mem, smbios_end, curptr)?;
curptr = write_and_incr(mem, 0u8, curptr)?;
curptr = write_and_incr(mem, smbios_sysinfo, curptr)?;
curptr = write_and_incr(mem, 0u8, curptr)?;
}
{
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 +230,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))
}
#[cfg(test)]
mod unit_tests {
mod 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).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

@@ -1,43 +1,30 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::slice;
use std::str::FromStr;
use log::{debug, info};
use thiserror::Error;
use uuid::Uuid;
use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemoryError};
use crate::GuestMemoryMmap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use thiserror::Error;
use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemoryError};
#[derive(Error, Debug)]
pub enum TdvfError {
#[error("Failed read TDVF descriptor")]
ReadDescriptor(#[source] io::Error),
#[error("Failed read TDVF descriptor offset")]
ReadDescriptorOffset(#[source] io::Error),
#[error("Failed read GUID table")]
ReadGuidTable(#[source] io::Error),
#[error("Failed read TDVF descriptor: {0}")]
ReadDescriptor(#[source] std::io::Error),
#[error("Failed read TDVF descriptor offset: {0}")]
ReadDescriptorOffset(#[source] std::io::Error),
#[error("Invalid descriptor signature")]
InvalidDescriptorSignature,
#[error("Invalid descriptor size")]
InvalidDescriptorSize,
#[error("Invalid descriptor version")]
InvalidDescriptorVersion,
#[error("Failed to write HOB details to guest memory")]
#[error("Failed to write HOB details to guest memory: {0}")]
GuestMemoryWriteHob(#[source] GuestMemoryError),
#[error("Failed to create Uuid")]
UuidCreation(#[source] uuid::Error),
}
const TABLE_FOOTER_GUID: &str = "96b582de-1fb2-45f7-baea-a366c55a082d";
const TDVF_METADATA_OFFSET_GUID: &str = "e47a6535-984a-4798-865e-4685a7bf8ec2";
// TDVF_DESCRIPTOR
#[repr(C, packed)]
#[repr(packed)]
#[derive(Default)]
pub struct TdvfDescriptor {
signature: [u8; 4],
@@ -47,7 +34,7 @@ pub struct TdvfDescriptor {
}
// TDVF_SECTION
#[repr(C, packed)]
#[repr(packed)]
#[derive(Clone, Copy, Default, Debug)]
pub struct TdvfSection {
pub data_offset: u32,
@@ -59,85 +46,73 @@ pub struct TdvfSection {
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug)]
pub enum TdvfSectionType {
Bfv,
Cfv,
TdHob,
TempMem,
PermMem,
Payload,
PayloadParam,
#[default]
Reserved = 0xffffffff,
}
fn tdvf_descriptor_offset(file: &mut File) -> Result<(SeekFrom, bool), TdvfError> {
// Let's first try to identify the presence of the table footer GUID
file.seek(SeekFrom::End(-0x30))
.map_err(TdvfError::ReadGuidTable)?;
let mut table_footer_guid: [u8; 16] = [0; 16];
file.read_exact(&mut table_footer_guid)
.map_err(TdvfError::ReadGuidTable)?;
let uuid =
Uuid::from_slice_le(table_footer_guid.as_slice()).map_err(TdvfError::UuidCreation)?;
let expected_uuid = Uuid::from_str(TABLE_FOOTER_GUID).map_err(TdvfError::UuidCreation)?;
if uuid == expected_uuid {
// Retrieve the table size
file.seek(SeekFrom::End(-0x32))
.map_err(TdvfError::ReadGuidTable)?;
let mut table_size: [u8; 2] = [0; 2];
file.read_exact(&mut table_size)
.map_err(TdvfError::ReadGuidTable)?;
let table_size = u16::from_le_bytes(table_size) as usize;
let mut table: Vec<u8> = vec![0; table_size];
// Read the entire table
file.seek(SeekFrom::End(-(table_size as i64 + 0x20)))
.map_err(TdvfError::ReadGuidTable)?;
file.read_exact(table.as_mut_slice())
.map_err(TdvfError::ReadGuidTable)?;
// Let's start from the top and go backward down the table.
// We start after the footer GUID and the table length.
let mut offset = table_size - 18;
debug!("Parsing GUID structure");
while offset >= 18 {
let entry_uuid = Uuid::from_slice_le(&table[offset - 16..offset])
.map_err(TdvfError::UuidCreation)?;
let entry_size =
u16::from_le_bytes(table[offset - 18..offset - 16].try_into().unwrap()) as usize;
debug!(
"Entry GUID = {}, size = {}",
entry_uuid.hyphenated(),
entry_size
);
// Avoid going through an infinite loop if the entry size is 0
if entry_size == 0 {
break;
}
offset -= entry_size;
let expected_uuid =
Uuid::from_str(TDVF_METADATA_OFFSET_GUID).map_err(TdvfError::UuidCreation)?;
if entry_uuid == expected_uuid && entry_size == 22 {
return Ok((
SeekFrom::End(
-(u32::from_le_bytes(table[offset..offset + 4].try_into().unwrap()) as i64),
),
true,
));
}
}
impl Default for TdvfSectionType {
fn default() -> Self {
TdvfSectionType::Reserved
}
}
// If we end up here, this means the firmware doesn't support the new way
// of exposing the TDVF descriptor offset through the table of GUIDs.
// That's why we fallback onto the deprecated method.
#[repr(C)]
#[derive(Clone, Copy, Default, Debug)]
pub struct TdVmmDataRegion {
pub start_address: u64,
pub length: u64,
pub region_type: TdVmmDataRegionType,
}
#[repr(u16)]
#[derive(Clone, Copy, Debug)]
pub enum TdVmmDataRegionType {
Signature = 0x0000,
InterfaceVersion = 0x0001,
SystemUuid = 0x0002,
RamSize = 0x0003,
GraphicsEnabled = 0x0004,
SmpCpuCount = 0x0005,
MachineId = 0x0006,
KernelAddress = 0x0007,
KernelSize = 0x0008,
KernelCommandLine = 0x0009,
InitrdAddress = 0x000a,
InitrdSize = 0x000b,
BootDevice = 0x000c,
NumaData = 0x000d,
BootMenu = 0x000e,
MaximumCpuCount = 0x000f,
KernelEntry = 0x0010,
KernelData = 0x0011,
InitrdData = 0x0012,
CommandLineAddress = 0x0013,
CommandLineSize = 0x0014,
CommandLineData = 0x0015,
KernelSetupAddress = 0x0016,
KernelSetupSize = 0x0017,
KernelSetupData = 0x0018,
FileDir = 0x0019,
AcpiTables = 0x8000,
SmbiosTables = 0x8001,
Irq0Override = 0x8002,
E820Table = 0x8003,
HpetData = 0x8004,
Reserved = 0xffff,
}
impl Default for TdVmmDataRegionType {
fn default() -> Self {
TdVmmDataRegionType::Reserved
}
}
pub fn parse_tdvf_sections(file: &mut File) -> Result<Vec<TdvfSection>, TdvfError> {
// The 32-bit offset to the TDVF metadata is located 32 bytes from
// the end of the file.
// See "TDVF Metadata Pointer" in "TDX Virtual Firmware Design Guide
@@ -147,23 +122,18 @@ fn tdvf_descriptor_offset(file: &mut File) -> Result<(SeekFrom, bool), TdvfError
let mut descriptor_offset: [u8; 4] = [0; 4];
file.read_exact(&mut descriptor_offset)
.map_err(TdvfError::ReadDescriptorOffset)?;
let descriptor_offset = u32::from_le_bytes(descriptor_offset) as u64;
Ok((
SeekFrom::Start(u32::from_le_bytes(descriptor_offset) as u64),
false,
))
}
pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool), TdvfError> {
let (descriptor_offset, guid_found) = tdvf_descriptor_offset(file)?;
file.seek(descriptor_offset)
file.seek(SeekFrom::Start(descriptor_offset))
.map_err(TdvfError::ReadDescriptor)?;
let mut descriptor: TdvfDescriptor = Default::default();
// SAFETY: we read exactly the size of the descriptor header
// Safe as 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 +142,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);
}
@@ -184,30 +155,35 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
let mut sections = Vec::new();
sections.resize_with(descriptor.num_sections as usize, TdvfSection::default);
// SAFETY: we read exactly the advertised sections
// Safe as 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)?;
Ok((sections, guid_found))
Ok(sections)
}
#[repr(u16)]
#[derive(Copy, Clone, Debug, Default)]
#[derive(Copy, Clone, Debug)]
enum HobType {
Handoff = 0x1,
ResourceDescriptor = 0x3,
GuidExtension = 0x4,
#[default]
Unused = 0xfffe,
EndOfHobList = 0xffff,
}
#[repr(C, packed)]
impl Default for HobType {
fn default() -> Self {
HobType::Unused
}
}
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
struct HobHeader {
r#type: HobType,
@@ -215,7 +191,7 @@ struct HobHeader {
reserved: u32,
}
#[repr(C, packed)]
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
struct HobHandoffInfoTable {
header: HobHeader,
@@ -228,7 +204,7 @@ struct HobHandoffInfoTable {
efi_end_of_hob_list: u64,
}
#[repr(C, packed)]
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
struct EfiGuid {
data1: u32,
@@ -237,7 +213,7 @@ struct EfiGuid {
data4: [u8; 8],
}
#[repr(C, packed)]
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
struct HobResourceDescriptor {
header: HobHeader,
@@ -248,48 +224,27 @@ struct HobResourceDescriptor {
resource_length: u64,
}
#[repr(C, packed)]
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
struct HobGuidType {
header: HobHeader,
name: EfiGuid,
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, Default)]
pub enum PayloadImageType {
#[default]
ExecutablePayload,
BzImage,
RawVmLinux,
}
#[repr(C, packed)]
#[repr(C)]
#[derive(Copy, Clone, Default, Debug)]
pub struct PayloadInfo {
pub image_type: PayloadImageType,
pub entry_point: u64,
}
#[repr(C, packed)]
#[derive(Copy, Clone, Default, Debug)]
struct TdPayload {
struct TdVmmData {
guid_type: HobGuidType,
payload_info: PayloadInfo,
region: TdVmmDataRegion,
}
// SAFETY: data structure only contain a series of integers
// SAFETY: These data structures only contain a series of integers
unsafe impl ByteValued for TdVmmDataRegion {}
unsafe impl ByteValued for HobHeader {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for HobHandoffInfoTable {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for HobResourceDescriptor {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for HobGuidType {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for PayloadInfo {}
// SAFETY: data structure only contain a series of integers
unsafe impl ByteValued for TdPayload {}
unsafe impl ByteValued for TdVmmData {}
pub struct TdHob {
start_offset: u64,
@@ -297,12 +252,12 @@ pub struct TdHob {
}
fn align_hob(v: u64) -> u64 {
v.div_ceil(8) * 8
(v + 7) / 8 * 8
}
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 +274,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 +287,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 +314,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(),
@@ -384,29 +339,22 @@ impl TdHob {
physical_start: u64,
resource_length: u64,
ram: bool,
guid_found: bool,
) -> Result<(), TdvfError> {
self.add_resource(
mem,
physical_start,
resource_length,
if ram {
if guid_found {
0x7 /* EFI_RESOURCE_MEMORY_UNACCEPTED */
} else {
0 /* EFI_RESOURCE_SYSTEM_MEMORY */
}
} else if guid_found {
0 /* EFI_RESOURCE_SYSTEM_MEMORY */
} else {
0x5 /*EFI_RESOURCE_MEMORY_RESERVED */
},
/* TODO:
* QEMU currently fills it in like this:
* EFI_RESOURCE_ATTRIBUTE_PRESENT | EFI_RESOURCE_ATTRIBUTE_INITIALIZED | EFI_RESOURCE_ATTRIBUTE_TESTED
* EFI_RESOURCE_ATTRIBUTE_PRESENT | EFI_RESOURCE_ATTRIBUTE_INITIALIZED | EFI_RESOURCE_ATTRIBUTE_ENCRYPTED | EFI_RESOURCE_ATTRIBUTE_TESTED
* which differs from the spec (due to TDVF implementation issue?)
*/
0x7,
0x04000007,
)
}
@@ -428,104 +376,50 @@ impl TdHob {
)
}
pub fn add_acpi_table(
pub fn add_td_vmm_data(
&mut self,
mem: &GuestMemoryMmap,
table_content: &[u8],
region: TdVmmDataRegion,
) -> Result<(), TdvfError> {
// 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 hob_guid_type = HobGuidType {
header: HobHeader {
r#type: HobType::GuidExtension,
length,
reserved: 0,
},
// ACPI_TABLE_HOB_GUID
// 0x6a0c5870, 0xd4ed, 0x44f4, {0xa1, 0x35, 0xdd, 0x23, 0x8b, 0x6f, 0xc, 0x8d }
name: EfiGuid {
data1: 0x6a0c_5870,
data2: 0xd4ed,
data3: 0x44f4,
data4: [0xa1, 0x35, 0xdd, 0x23, 0x8b, 0x6f, 0xc, 0x8d],
},
};
info!(
"Writing HOB ACPI table {:x} {:x?} {:x?}",
self.current_offset, hob_guid_type, table_content
);
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;
// In case the table is quite large, let's make sure we can handle
// retrying until everything has been correctly copied.
let mut offset: usize = 0;
loop {
let bytes_written = mem
.write(
&table_content[offset..],
GuestAddress(current_offset + offset as u64),
)
.map_err(TdvfError::GuestMemoryWriteHob)?;
offset += bytes_written;
if offset >= table_content.len() {
break;
}
}
self.current_offset += length as u64;
Ok(())
}
pub fn add_payload(
&mut self,
mem: &GuestMemoryMmap,
payload_info: PayloadInfo,
) -> Result<(), TdvfError> {
let payload = TdPayload {
let td_vmm_data = TdVmmData {
guid_type: HobGuidType {
header: HobHeader {
r#type: HobType::GuidExtension,
length: size_of::<TdPayload>() as u16,
length: std::mem::size_of::<TdVmmData>() as u16,
reserved: 0,
},
// HOB_PAYLOAD_INFO_GUID
// 0xb96fa412, 0x461f, 0x4be3, {0x8c, 0xd, 0xad, 0x80, 0x5a, 0x49, 0x7a, 0xc0
// TD_VMM_DATA_GUID CF2643E4-C0D3-46FF-0000-72EE623DDE38
name: EfiGuid {
data1: 0xb96f_a412,
data2: 0x461f,
data3: 0x4be3,
data4: [0x8c, 0xd, 0xad, 0x80, 0x5a, 0x49, 0x7a, 0xc0],
data1: 0xcf26_43e4,
data2: 0xc0d3,
data3: 0x46ff,
data4: [0x00, 0x00, 0x72, 0xee, 0x62, 0x3d, 0xde, 0x38],
},
},
payload_info,
region,
};
info!(
"Writing HOB TD_PAYLOAD {:x} {:x?}",
self.current_offset, payload
"Writing HOB TD_VMM_DATA {:x} {:x?}",
self.current_offset, td_vmm_data
);
mem.write_obj(payload, GuestAddress(self.current_offset))
mem.write_obj(td_vmm_data, GuestAddress(self.current_offset))
.map_err(TdvfError::GuestMemoryWriteHob)?;
self.update_offset::<TdPayload>();
self.update_offset::<TdVmmData>();
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
mod tests {
use super::*;
#[test]
#[ignore]
fn test_parse_tdvf_sections() {
let mut f = File::open("tdvf.fd").unwrap();
let (sections, _) = parse_tdvf_sections(&mut f).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?}");
eprintln!("{:x?}", section)
}
}
}

View File

@@ -1,42 +0,0 @@
[package]
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"
flate2 = "1.1"
io-uring = { version = "0.7.12", optional = true }
libc = { workspace = true }
log = { workspace = true }
remain = "0.2.15"
serde = { workspace = true, features = ["derive"] }
smallvec = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
virtio-bindings = { workspace = true }
virtio-queue = { workspace = true }
vm-memory = { workspace = true, features = [
"backend-atomic",
"backend-bitmap",
"backend-mmap",
] }
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

@@ -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

@@ -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));
}
}

View File

@@ -1,89 +0,0 @@
// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0
use std::{io, result};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Zlib decompress error")]
ZlibDecompress(#[source] flate2::DecompressError),
#[error("Zlib unexpected status: {0:?}")]
ZlibUnexpectedStatus(flate2::Status),
#[error("Zstd decompress error")]
ZstdDecompress(#[source] io::Error),
#[error("Zstd: failed to fill buffer")]
ZstdFillBuffer(#[source] io::Error),
}
pub(super) type Result<T> = result::Result<T, Error>;
/// Generic trait for decoding zlib/zstd formats
pub trait Decoder: Send + Sync {
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize>;
}
#[derive(Default)]
pub(super) struct ZlibDecoder {}
impl Decoder for ZlibDecoder {
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
use flate2::{Decompress, FlushDecompress, Status};
let mut decompressor = Decompress::new(false);
let status = decompressor
.decompress(input, output, FlushDecompress::Finish)
.map_err(Error::ZlibDecompress)?;
if status == Status::StreamEnd {
Ok(decompressor.total_out() as usize)
} else {
Err(Error::ZlibUnexpectedStatus(status))
}
}
}
#[derive(Default)]
pub(super) struct ZstdDecoder {}
impl Decoder for ZstdDecoder {
fn decode(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
use std::io::Read;
let mut decoder = zstd::stream::read::Decoder::new(input).map_err(Error::ZstdDecompress)?;
let decoded_size = decoder.read(output).map_err(Error::ZstdFillBuffer)?;
Ok(decoded_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zlib_decode() {
let d = ZlibDecoder::default();
let valid_input = vec![99, 96, 100, 98, 6, 0];
let mut output1 = vec![0; 4];
d.decode(&valid_input, &mut output1).unwrap();
assert_eq!(&output1, b"\x00\x01\x02\x03");
let invalid_input = vec![1, 2, 3, 4];
let mut output2 = vec![0; 1024];
d.decode(&invalid_input, &mut output2).unwrap_err();
}
#[test]
fn test_zstd_decode() {
let d = ZstdDecoder::default();
let valid_input = vec![40, 181, 47, 253, 32, 2, 17, 0, 0, 1, 254];
let mut output1 = vec![0; 2];
d.decode(&valid_input, &mut output1).unwrap();
assert_eq!(&output1, b"\x01\xfe");
let invalid_input = vec![1, 2, 3, 4];
let mut output2 = vec![0; 1024];
d.decode(&invalid_input, &mut output2).unwrap_err();
}
}

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,589 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::collections::btree_map::BTreeMap;
use std::os::unix::fs::FileExt;
use std::{io, result};
use byteorder::{ByteOrder, LittleEndian};
use remain::sorted;
use thiserror::Error;
use uuid::Uuid;
use zerocopy::{FromBytes, Immutable, IntoBytes};
use crate::aligned_file::AlignedFile;
const VHDX_SIGN: u64 = 0x656C_6966_7864_6876; // "vhdxfile"
const HEADER_SIGN: u32 = 0x6461_6568; // "head"
const REGION_SIGN: u32 = 0x6967_6572; // "regi"
const FILE_START: u64 = 0; // The first element
const HEADER_1_START: u64 = 64 * 1024; // Header 1 start in Bytes
const HEADER_2_START: u64 = 128 * 1024; // Header 2 start in Bytes
pub(super) const REGION_TABLE_1_START: u64 = 192 * 1024; // Region 1 start in Bytes
const REGION_TABLE_2_START: u64 = 256 * 1024; // Region 2 start in Bytes
const HEADER_SIZE: u64 = 4 * 1024; // Each header is 64 KiB, but only first 4 kiB contains info
const REGION_SIZE: u64 = 64 * 1024; // Each region size is 64 KiB
const REGION_ENTRY_REQUIRED: u32 = 1;
// VHDX stores GUIDs using little-endian GUID byte order.
const BAT_GUID: [u8; 16] = [
0x66, 0x77, 0xc2, 0x2d, 0x23, 0xf6, 0x00, 0x42, 0x9d, 0x64, 0x11, 0x5e, 0x9b, 0xfd, 0x4a, 0x08,
];
const MDR_GUID: [u8; 16] = [
0x06, 0xa2, 0x7c, 0x8b, 0x90, 0x47, 0x9a, 0x4b, 0xb8, 0xfe, 0x57, 0x5f, 0x05, 0x0f, 0x88, 0x6e,
];
#[sorted]
#[derive(Error, Debug)]
pub enum VhdxHeaderError {
#[error("Failed to calculate checksum")]
CalculateChecksum,
#[error("BAT entry is not unique")]
DuplicateBATEntry,
#[error("Metadata region entry is not unique")]
DuplicateMDREntry,
#[error("Checksum doesn't match for {0}")]
InvalidChecksum(String),
#[error("Invalid entry count")]
InvalidEntryCount,
#[error("Not a valid VHDx header")]
InvalidHeaderSign,
#[error("Not a valid VHDx region")]
InvalidRegionSign,
#[error("Not a VHDx file")]
InvalidVHDXSign,
#[error("No valid header found")]
NoValidHeader,
#[error("Cannot read checksum")]
ReadChecksum,
#[error("Failed to read File Type Identifier {0}")]
ReadFileTypeIdentifier(#[source] io::Error),
#[error("Failed to read headers {0}")]
ReadHeader(#[source] io::Error),
#[error("Failed to read metadata {0}")]
ReadMetadata(#[source] io::Error),
#[error("Failed to read region table entries {0}")]
ReadRegionTableEntries(#[source] io::Error),
#[error("Failed to read region table header {0}")]
ReadRegionTableHeader(#[source] io::Error),
#[error("Failed to read region entries")]
RegionEntryCollectionFailed,
#[error("Region entry file offset ({0}) and length ({1}) overflow u64")]
RegionEntryOverflow(u64 /* start */, usize /* length */),
#[error("Overlapping regions found")]
RegionOverlap,
#[error("Reserved region has non-zero value")]
ReservedIsNonZero,
#[error("We do not recognize this entry")]
UnrecognizedRegionEntry,
#[error("Failed to write header {0}")]
WriteHeader(#[source] io::Error),
}
pub(super) type Result<T> = result::Result<T, VhdxHeaderError>;
#[derive(Clone, Debug)]
pub(super) struct FileTypeIdentifier {
pub _signature: u64,
}
impl FileTypeIdentifier {
/// Reads the File Type Identifier structure from a reference VHDx file
pub(super) fn new(f: &AlignedFile) -> Result<FileTypeIdentifier> {
let mut buf = [0u8; size_of::<u64>()];
f.read_exact_at(&mut buf, FILE_START)
.map_err(VhdxHeaderError::ReadFileTypeIdentifier)?;
let _signature = LittleEndian::read_u64(&buf);
if _signature != VHDX_SIGN {
return Err(VhdxHeaderError::InvalidVHDXSign);
}
Ok(FileTypeIdentifier { _signature })
}
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)]
pub(super) struct Header {
pub signature: u32,
pub checksum: u32,
pub sequence_number: u64,
pub file_write_guid: u128,
pub data_write_guid: u128,
pub log_guid: u128,
pub log_version: u16,
pub version: u16,
pub log_length: u32,
pub log_offset: u64,
}
impl Header {
/// Reads the Header structure from a reference VHDx file
pub(super) fn new(f: &AlignedFile, start: u64) -> Result<Header> {
// Read the whole header into a buffer. We will need it for
// calculating checksum.
let mut buffer = [0; HEADER_SIZE as usize];
f.read_exact_at(&mut buffer, start)
.map_err(VhdxHeaderError::ReadHeader)?;
let header = Header::read_from_prefix(&buffer).unwrap().0;
if header.signature != HEADER_SIGN {
return Err(VhdxHeaderError::InvalidHeaderSign);
}
let new_checksum = calculate_checksum(&mut buffer, size_of::<u32>());
if header.checksum != new_checksum {
return Err(VhdxHeaderError::InvalidChecksum(String::from("Header")));
}
Ok(header)
}
/// Creates and returns new updated header from the provided current header
fn update_header(
f: &AlignedFile,
current_header: &Header,
change_data_guid: bool,
file_write_guid: u128,
start: u64,
) -> Result<Header> {
let mut buffer = [0u8; HEADER_SIZE as usize];
let data_write_guid = if change_data_guid {
Uuid::new_v4().as_u128()
} else {
current_header.data_write_guid
};
let file_write_guid = if file_write_guid == 0 {
current_header.file_write_guid
} else {
file_write_guid
};
let mut new_header = Header {
signature: current_header.signature,
checksum: 0,
sequence_number: current_header.sequence_number + 1,
file_write_guid,
data_write_guid,
log_guid: current_header.log_guid,
log_version: current_header.log_version,
version: current_header.version,
log_length: current_header.log_length,
log_offset: current_header.log_offset,
};
new_header.write_to_prefix(&mut buffer).unwrap();
new_header.checksum = calculate_checksum(&mut buffer, size_of::<u32>());
new_header.write_to_prefix(&mut buffer).unwrap();
f.write_all_at(&buffer, start)
.map_err(VhdxHeaderError::WriteHeader)?;
Ok(new_header)
}
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, FromBytes)]
struct RegionTableHeader {
pub signature: u32,
pub checksum: u32,
pub entry_count: u32,
pub reserved: u32,
}
impl RegionTableHeader {
/// Reads the Region Table Header structure from a reference VHDx file
pub(crate) fn new(f: &AlignedFile, start: u64) -> Result<RegionTableHeader> {
// Read the whole header into a buffer. We will need it for calculating
// checksum.
let mut buffer = [0u8; REGION_SIZE as usize];
f.read_exact_at(&mut buffer, start)
.map_err(VhdxHeaderError::ReadRegionTableHeader)?;
let region_table_header = RegionTableHeader::read_from_prefix(&buffer).unwrap().0;
if region_table_header.signature != REGION_SIGN {
return Err(VhdxHeaderError::InvalidRegionSign);
}
let new_checksum = calculate_checksum(&mut buffer, size_of::<u32>());
if region_table_header.checksum != new_checksum {
return Err(VhdxHeaderError::InvalidChecksum(String::from("Region")));
}
if region_table_header.entry_count > 2047 {
return Err(VhdxHeaderError::InvalidEntryCount);
}
if region_table_header.reserved != 0 {
return Err(VhdxHeaderError::ReservedIsNonZero);
}
Ok(region_table_header)
}
}
/// Returns `true` if the half-open byte ranges `[a_start, a_end)` and
/// `[b_start, b_end)` overlap.
fn ranges_overlap(a_start: u64, a_end: u64, b_start: u64, b_end: u64) -> bool {
a_start < b_end && b_start < a_end
}
pub(super) struct RegionInfo {
pub bat_entry: RegionTableEntry,
pub mdr_entry: RegionTableEntry,
pub region_entries: BTreeMap<u64, u64>,
}
impl RegionInfo {
/// Collect all entries in a BTreeMap from the Region Table and identifies
/// BAT and metadata regions
pub(super) fn new(f: &AlignedFile, region_start: u64, entry_count: u32) -> Result<RegionInfo> {
let mut bat_entry: Option<RegionTableEntry> = None;
let mut mdr_entry: Option<RegionTableEntry> = None;
let mut offset = 0;
let mut region_entries = BTreeMap::new();
let mut buffer = [0; REGION_SIZE as usize];
// Read after the Region Table Header
f.read_exact_at(
&mut buffer,
region_start + size_of::<RegionTableHeader>() as u64,
)
.map_err(VhdxHeaderError::ReadRegionTableEntries)?;
for _ in 0..entry_count {
let entry = RegionTableEntry::read_from_bytes(
&buffer[offset..offset + size_of::<RegionTableEntry>()],
)
.unwrap();
offset += size_of::<RegionTableEntry>();
let start = entry.file_offset;
let end = start.checked_add(entry.length as u64).ok_or(
VhdxHeaderError::RegionEntryOverflow(start, entry.length as usize),
)?;
for (region_ent_start, region_ent_end) in region_entries.iter() {
if ranges_overlap(start, end, *region_ent_start, *region_ent_end) {
return Err(VhdxHeaderError::RegionOverlap);
}
}
region_entries.insert(start, end);
if entry.guid == BAT_GUID {
if bat_entry.is_none() {
bat_entry = Some(entry);
continue;
}
return Err(VhdxHeaderError::DuplicateBATEntry);
}
if entry.guid == MDR_GUID {
if mdr_entry.is_none() {
mdr_entry = Some(entry);
continue;
}
return Err(VhdxHeaderError::DuplicateMDREntry);
}
if (entry.required & REGION_ENTRY_REQUIRED) == 1 {
// This implementation doesn't recognize this field.
// Therefore, according to the spec, we are throwing an error.
return Err(VhdxHeaderError::UnrecognizedRegionEntry);
}
}
if bat_entry.is_none() || mdr_entry.is_none() {
region_entries.clear();
return Err(VhdxHeaderError::RegionEntryCollectionFailed);
}
// It's safe to unwrap as we checked both entries have been filled.
// Otherwise, an error is already returned.
let bat_entry = bat_entry.unwrap();
let mdr_entry = mdr_entry.unwrap();
Ok(RegionInfo {
bat_entry,
mdr_entry,
region_entries,
})
}
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, FromBytes)]
pub(super) struct RegionTableEntry {
guid: [u8; 16],
pub file_offset: u64,
pub length: u32,
pub required: u32,
}
enum HeaderNo {
First,
Second,
}
/// Contains the information from the header of a VHDx file
#[derive(Clone, Debug)]
pub(super) struct VhdxHeader {
_file_type_identifier: FileTypeIdentifier,
header_1: Header,
header_2: Header,
region_table_1: RegionTableHeader,
_region_table_2: RegionTableHeader,
}
impl VhdxHeader {
/// Creates a VhdxHeader from a reference to a file
pub(super) fn new(f: &AlignedFile) -> Result<VhdxHeader> {
Ok(VhdxHeader {
_file_type_identifier: FileTypeIdentifier::new(f)?,
header_1: Header::new(f, HEADER_1_START)?,
header_2: Header::new(f, HEADER_2_START)?,
region_table_1: RegionTableHeader::new(f, REGION_TABLE_1_START)?,
_region_table_2: RegionTableHeader::new(f, REGION_TABLE_2_START)?,
})
}
/// Identify the current header and return both headers along with an
/// integer indicating the current header.
fn current_header(
header_1: Result<Header>,
header_2: Result<Header>,
) -> Result<(HeaderNo, Header)> {
let header_1 = header_1.ok();
let header_2 = header_2.ok();
match (header_1, header_2) {
(None, None) => Err(VhdxHeaderError::NoValidHeader),
(Some(header_1), None) => Ok((HeaderNo::First, header_1)),
(None, Some(header_2)) => Ok((HeaderNo::Second, header_2)),
(Some(header_1), Some(header_2)) => {
if header_1.sequence_number >= header_2.sequence_number {
Ok((HeaderNo::First, header_1))
} else {
Ok((HeaderNo::Second, header_2))
}
}
}
}
/// This takes two headers and update the noncurrent header with the
/// current one. Returns both headers as a tuple sequenced the way it was
/// received from the parameter list.
fn update_header(
f: &AlignedFile,
header_1: Result<Header>,
header_2: Result<Header>,
guid: u128,
) -> Result<(Header, Header)> {
let (header_no, current_header) = VhdxHeader::current_header(header_1, header_2)?;
match header_no {
HeaderNo::First => {
let other_header =
Header::update_header(f, &current_header, true, guid, HEADER_2_START)?;
Ok((current_header, other_header))
}
HeaderNo::Second => {
let other_header =
Header::update_header(f, &current_header, true, guid, HEADER_1_START)?;
Ok((other_header, current_header))
}
}
}
// Update the provided headers according to the spec
fn update_headers(
f: &AlignedFile,
header_1: Result<Header>,
header_2: Result<Header>,
guid: u128,
) -> Result<(Header, Header)> {
// According to the spec, update twice
let (header_1, header_2) = VhdxHeader::update_header(f, header_1, header_2, guid)?;
VhdxHeader::update_header(f, Ok(header_1), Ok(header_2), guid)
}
pub(super) fn update(&mut self, f: &AlignedFile) -> Result<()> {
let headers = VhdxHeader::update_headers(f, Ok(self.header_1), Ok(self.header_2), 0)?;
self.header_1 = headers.0;
self.header_2 = headers.1;
Ok(())
}
pub(super) fn region_entry_count(&self) -> u32 {
self.region_table_1.entry_count
}
}
/// Calculates the checksum of a buffer that itself contains its checksum
/// Therefore, before calculating, the existing checksum is retrieved and the
/// corresponding field is made zero. After the calculation, the existing checksum
/// is put back to the buffer.
fn calculate_checksum(buffer: &mut [u8], csum_offset: usize) -> u32 {
// Read the original checksum from the buffer
let orig_csum = LittleEndian::read_u32(&buffer[csum_offset..csum_offset + 4]);
// Zero the checksum in the buffer
LittleEndian::write_u32(&mut buffer[csum_offset..csum_offset + 4], 0);
// Calculate the checksum on the resulting buffer
let mut crc = crc_any::CRC::crc32c();
crc.digest(&buffer);
let new_csum = crc.get_crc() as u32;
// Put back the original checksum in the buffer
LittleEndian::write_u32(&mut buffer[csum_offset..csum_offset + 4], orig_csum);
new_csum
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::FileExt;
use vmm_sys_util::tempfile::TempFile;
use zerocopy::{FromBytes, IntoBytes};
use super::{
BAT_GUID, HEADER_SIGN, Header, MDR_GUID, REGION_TABLE_1_START, RegionInfo,
RegionTableHeader, VhdxHeaderError, ranges_overlap,
};
use crate::aligned_file::AlignedFile;
#[test]
fn test_header_bytes_round_trip() {
let header = Header {
signature: HEADER_SIGN,
checksum: 0x1122_3344,
sequence_number: 0x0102_0304_0506_0708,
file_write_guid: 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100,
data_write_guid: 0x1f1e_1d1c_1b1a_1918_1716_1514_1312_1110,
log_guid: 0x2f2e_2d2c_2b2a_2928_2726_2524_2322_2120,
log_version: 0xabcd,
version: 0x0001,
log_length: 0x0010_0000,
log_offset: 0x0000_0100_0000_0000,
};
let bytes = header.as_bytes();
assert_eq!(&bytes[0..4], &header.signature.to_le_bytes()[..]);
assert_eq!(&bytes[8..16], &header.sequence_number.to_le_bytes()[..]);
assert_eq!(&bytes[16..32], &header.file_write_guid.to_le_bytes()[..]);
assert_eq!(&bytes[64..66], &header.log_version.to_le_bytes()[..]);
assert_eq!(&bytes[72..80], &header.log_offset.to_le_bytes()[..]);
let parsed = Header::read_from_bytes(bytes).unwrap();
assert_eq!({ parsed.signature }, { header.signature });
assert_eq!({ parsed.checksum }, { header.checksum });
assert_eq!({ parsed.sequence_number }, { header.sequence_number });
assert_eq!({ parsed.file_write_guid }, { header.file_write_guid });
assert_eq!({ parsed.data_write_guid }, { header.data_write_guid });
assert_eq!({ parsed.log_guid }, { header.log_guid });
assert_eq!({ parsed.log_version }, { header.log_version });
assert_eq!({ parsed.version }, { header.version });
assert_eq!({ parsed.log_length }, { header.log_length });
assert_eq!({ parsed.log_offset }, { header.log_offset });
}
#[test]
fn test_ranges_overlap() {
// (new [start,end), existing [s,e), expected overlap)
let cases: &[(u64, u64, u64, u64, bool)] = &[
// Genuine overlaps — all of these must be detected.
(0, 10, 0, 10, true), // identical
(2, 8, 0, 10, true), // new fully inside existing
(0, 20, 5, 10, true), // new fully contains existing
(5, 15, 0, 10, true), // partial, new starts inside existing
(0, 8, 5, 15, true), // partial, new starts before existing
// Non-overlapping — must not be flagged.
(0, 5, 10, 20, false), // disjoint, new before existing
(30, 40, 10, 20, false), // disjoint, new after existing
(0, 10, 10, 20, false), // touching at the boundary (half-open)
];
for &(a_start, a_end, b_start, b_end, expected) in cases {
assert_eq!(
ranges_overlap(a_start, a_end, b_start, b_end),
expected,
"[{a_start},{a_end}) vs [{b_start},{b_end})"
);
// Overlap is symmetric.
assert_eq!(
ranges_overlap(b_start, b_end, a_start, a_end),
expected,
"symmetry: [{b_start},{b_end}) vs [{a_start},{a_end})"
);
}
}
/// Builds the 32-byte on-disk region table entry for `guid` describing
/// the region `[file_offset, file_offset + length)`.
fn region_entry(guid: [u8; 16], file_offset: u64, length: u32) -> [u8; 32] {
let mut e = [0u8; 32];
e[0..16].copy_from_slice(&guid);
e[16..24].copy_from_slice(&file_offset.to_le_bytes());
e[24..28].copy_from_slice(&length.to_le_bytes());
// `required` (e[28..32]) left zero.
e
}
#[test]
fn test_region_info_rejects_overlapping_regions() {
// BAT region [1 MiB, 3 MiB) and metadata region [2 MiB, 4 MiB) overlap
// on [2 MiB, 3 MiB); per [MS-VHDX] all region objects must be
// non-overlapping, so this image must be rejected.
const MIB: u64 = 1024 * 1024;
let region_start = REGION_TABLE_1_START;
let entries_at = region_start + size_of::<RegionTableHeader>() as u64;
let temp = TempFile::new().unwrap();
let f = temp.into_file();
f.set_len(entries_at + 64 * 1024).unwrap();
f.write_all_at(&region_entry(BAT_GUID, MIB, (2 * MIB) as u32), entries_at)
.unwrap();
f.write_all_at(
&region_entry(MDR_GUID, 2 * MIB, (2 * MIB) as u32),
entries_at + 32,
)
.unwrap();
let af = AlignedFile::new(f, false);
let res = RegionInfo::new(&af, region_start, 2);
assert!(
matches!(res, Err(VhdxHeaderError::RegionOverlap)),
"expected RegionOverlap for an overlapping region table"
);
}
#[test]
fn test_region_info_rejects_overflowing_region() {
// A region whose file offset plus length wraps past u64::MAX must be
// rejected rather than silently producing a small end offset that
// could mask a genuine overlap.
let region_start = REGION_TABLE_1_START;
let entries_at = region_start + size_of::<RegionTableHeader>() as u64;
let temp = TempFile::new().unwrap();
let f = temp.into_file();
f.set_len(entries_at + 64 * 1024).unwrap();
f.write_all_at(&region_entry(BAT_GUID, u64::MAX, 0x1000), entries_at)
.unwrap();
let af = AlignedFile::new(f, false);
let res = RegionInfo::new(&af, region_start, 1);
assert!(
matches!(res, Err(VhdxHeaderError::RegionEntryOverflow(..))),
"expected RegionEntryOverflow for a wrapping region entry"
);
}
}

View File

@@ -1,218 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::os::unix::fs::FileExt;
use std::{io, result};
use remain::sorted;
use thiserror::Error;
use super::bat::{self, BatEntry, VhdxBatError};
use super::metadata::{self, DiskSpec};
use crate::aligned_file::AlignedFile;
const SECTOR_SIZE: u64 = 512;
#[sorted]
#[derive(Error, Debug)]
pub enum VhdxIoError {
#[error("Invalid BAT entry state")]
InvalidBatEntryState,
#[error("Invalid BAT entry count")]
InvalidBatIndex,
#[error("Invalid disk size")]
InvalidDiskSize,
#[error("Failed reading sector blocks from file {0}")]
ReadSectorBlock(#[source] io::Error),
#[error("Failed changing file length {0}")]
ResizeFile(#[source] io::Error),
#[error("Differencing mode is not supported yet")]
UnsupportedMode,
#[error("Failed writing BAT to file {0}")]
WriteBat(#[source] VhdxBatError),
}
pub(super) type Result<T> = result::Result<T, VhdxIoError>;
macro_rules! align {
($n:expr, $align:expr) => {{ $n.div_ceil($align) * $align }};
}
#[derive(Default)]
struct Sector {
bat_index: u64,
free_sectors: u64,
free_bytes: u64,
file_offset: u64,
block_offset: u64,
}
impl Sector {
/// Translate sector index and count of data in file to actual offsets and
/// BAT index.
pub(crate) fn new(
disk_spec: &DiskSpec,
bat: &[BatEntry],
sector_index: u64,
sector_count: u64,
) -> Result<Sector> {
let mut sector = Sector::default();
sector.bat_index = sector_index / disk_spec.sectors_per_block as u64;
sector.block_offset = sector_index % disk_spec.sectors_per_block as u64;
sector.free_sectors = disk_spec.sectors_per_block as u64 - sector.block_offset;
if sector.free_sectors > sector_count {
sector.free_sectors = sector_count;
}
sector.free_bytes = sector.free_sectors * disk_spec.logical_sector_size as u64;
sector.block_offset *= disk_spec.logical_sector_size as u64;
let bat_entry = match bat.get(sector.bat_index as usize) {
Some(entry) => entry.0,
None => {
return Err(VhdxIoError::InvalidBatIndex);
}
};
sector.file_offset = bat_entry & bat::BAT_FILE_OFF_MASK;
if sector.file_offset != 0 {
sector.file_offset += sector.block_offset;
}
Ok(sector)
}
}
/// VHDx IO read routine: requires relative sector index and count for the
/// requested data.
pub(super) fn read(
f: &AlignedFile,
buf: &mut [u8],
disk_spec: &DiskSpec,
bat: &[BatEntry],
mut sector_index: u64,
mut sector_count: u64,
) -> Result<usize> {
if disk_spec.has_parent {
return Err(VhdxIoError::UnsupportedMode);
}
let mut read_count: usize = 0;
while sector_count > 0 {
let sector = Sector::new(disk_spec, bat, sector_index, sector_count)?;
let bat_entry = match bat.get(sector.bat_index as usize) {
Some(entry) => entry.0,
None => {
return Err(VhdxIoError::InvalidBatIndex);
}
};
match bat_entry & bat::BAT_STATE_BIT_MASK {
bat::PAYLOAD_BLOCK_NOT_PRESENT
| bat::PAYLOAD_BLOCK_UNDEFINED
| bat::PAYLOAD_BLOCK_UNMAPPED
| bat::PAYLOAD_BLOCK_ZERO => {}
bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
f.read_exact_at(
&mut buf
[read_count..(read_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
sector.file_offset,
)
.map_err(VhdxIoError::ReadSectorBlock)?;
}
bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => {
return Err(VhdxIoError::UnsupportedMode);
}
_ => {
return Err(VhdxIoError::InvalidBatEntryState);
}
}
sector_count -= sector.free_sectors;
sector_index += sector.free_sectors;
read_count += sector.free_bytes as usize;
}
Ok(read_count)
}
/// VHDx IO write routine: requires relative sector index and count for the
/// requested data.
pub(super) fn write(
f: &AlignedFile,
buf: &[u8],
disk_spec: &mut DiskSpec,
bat_offset: u64,
bat: &mut [BatEntry],
mut sector_index: u64,
mut sector_count: u64,
) -> Result<usize> {
if disk_spec.has_parent {
return Err(VhdxIoError::UnsupportedMode);
}
let mut write_count: usize = 0;
while sector_count > 0 {
let sector = Sector::new(disk_spec, bat, sector_index, sector_count)?;
let bat_entry = match bat.get(sector.bat_index as usize) {
Some(entry) => entry.0,
None => {
return Err(VhdxIoError::InvalidBatIndex);
}
};
match bat_entry & bat::BAT_STATE_BIT_MASK {
bat::PAYLOAD_BLOCK_NOT_PRESENT
| bat::PAYLOAD_BLOCK_UNDEFINED
| bat::PAYLOAD_BLOCK_UNMAPPED
| bat::PAYLOAD_BLOCK_ZERO => {
let file_offset = align!(disk_spec.image_size, metadata::BLOCK_SIZE_MIN as u64);
let new_size = file_offset
.checked_add(disk_spec.block_size as u64)
.ok_or(VhdxIoError::InvalidDiskSize)?;
f.file()
.set_len(new_size)
.map_err(VhdxIoError::ResizeFile)?;
disk_spec.image_size = new_size;
let new_bat_entry =
file_offset | (bat::PAYLOAD_BLOCK_FULLY_PRESENT & bat::BAT_STATE_BIT_MASK);
bat[sector.bat_index as usize] = BatEntry(new_bat_entry);
BatEntry::write_bat_entries(f, bat_offset, bat).map_err(VhdxIoError::WriteBat)?;
if file_offset < metadata::BLOCK_SIZE_MIN as u64 {
break;
}
f.write_all_at(
&buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
file_offset,
)
.map_err(VhdxIoError::ReadSectorBlock)?;
}
bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
if sector.file_offset < metadata::BLOCK_SIZE_MIN as u64 {
break;
}
f.write_all_at(
&buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
sector.file_offset,
)
.map_err(VhdxIoError::ReadSectorBlock)?;
}
bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => {
return Err(VhdxIoError::UnsupportedMode);
}
_ => {
return Err(VhdxIoError::InvalidBatEntryState);
}
}
sector_count -= sector.free_sectors;
sector_index += sector.free_sectors;
write_count += sector.free_bytes as usize;
}
Ok(write_count)
}

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,308 +0,0 @@
// Copyright © 2021 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::collections::btree_map::BTreeMap;
use std::fs::File;
use std::io::{
Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, Write,
};
use std::os::fd::{AsRawFd, RawFd};
use std::result;
use remain::sorted;
use thiserror::Error;
use super::bat::{BatEntry, VhdxBatError};
use super::header::{self, RegionInfo, RegionTableEntry, VhdxHeader, VhdxHeaderError};
use super::io::{self, VhdxIoError};
use super::metadata::{DiskSpec, VhdxMetadataError};
use crate::aligned_file::AlignedFile;
#[sorted]
#[derive(Error, Debug)]
pub enum VhdxError {
#[error("Not a VHDx file")]
NotVhdx(#[source] VhdxHeaderError),
#[error("Failed to parse VHDx header")]
ParseVhdxHeader(#[source] VhdxHeaderError),
#[error("Failed to parse VHDx metadata")]
ParseVhdxMetadata(#[source] VhdxMetadataError),
#[error("Failed to parse VHDx region entries")]
ParseVhdxRegionEntry(#[source] VhdxHeaderError),
#[error("Failed reading metadata")]
ReadBatEntry(#[source] VhdxBatError),
#[error("Failed reading sector from disk")]
ReadFailed(#[source] VhdxIoError),
#[error("Failed writing to sector on disk")]
WriteFailed(#[source] VhdxIoError),
}
pub(super) type Result<T> = result::Result<T, VhdxError>;
#[derive(Debug)]
pub struct Vhdx {
aligned: AlignedFile,
vhdx_header: VhdxHeader,
region_entries: BTreeMap<u64, u64>,
bat_entry: RegionTableEntry,
mdr_entry: RegionTableEntry,
disk_spec: DiskSpec,
bat_entries: Vec<BatEntry>,
current_offset: u64,
first_write: bool,
}
impl Vhdx {
/// Parse the Vhdx header, BAT, and metadata from a file and store info
// in Vhdx structure.
pub fn new(file: File, direct_io: bool) -> Result<Vhdx> {
let aligned = AlignedFile::new(file, direct_io);
let vhdx_header = VhdxHeader::new(&aligned).map_err(VhdxError::ParseVhdxHeader)?;
let collected_entries = RegionInfo::new(
&aligned,
header::REGION_TABLE_1_START,
vhdx_header.region_entry_count(),
)
.map_err(VhdxError::ParseVhdxRegionEntry)?;
let bat_entry = collected_entries.bat_entry;
let mdr_entry = collected_entries.mdr_entry;
let disk_spec =
DiskSpec::new(&aligned, &mdr_entry).map_err(VhdxError::ParseVhdxMetadata)?;
let bat_entries = BatEntry::collect_bat_entries(&aligned, &disk_spec, &bat_entry)
.map_err(VhdxError::ReadBatEntry)?;
Ok(Vhdx {
aligned,
vhdx_header,
region_entries: collected_entries.region_entries,
bat_entry,
mdr_entry,
disk_spec,
bat_entries,
current_offset: 0,
first_write: true,
})
}
pub fn virtual_disk_size(&self) -> u64 {
self.disk_spec.virtual_disk_size
}
}
impl Read for Vhdx {
/// Wrapper function to satisfy Read trait implementation for VHDx disk.
/// Convert the offset to sector index and buffer length to sector count.
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64);
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
let result = io::read(
&self.aligned,
buf,
&self.disk_spec,
&self.bat_entries,
sector_index,
sector_count,
)
.map_err(|e| {
IoError::other(format!(
"Failed reading {sector_count} sectors from VHDx at index {sector_index}: {e}"
))
})?;
self.current_offset = self.current_offset.checked_add(result as u64).unwrap();
Ok(result)
}
}
impl Write for Vhdx {
fn flush(&mut self) -> IoResult<()> {
self.aligned.file_mut().flush()
}
/// Wrapper function to satisfy Write trait implementation for VHDx disk.
/// Convert the offset to sector index and buffer length to sector count.
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64);
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
if self.first_write {
self.first_write = false;
self.vhdx_header
.update(&self.aligned)
.map_err(|e| IoError::other(format!("Failed to update VHDx header: {e}")))?;
}
let result = io::write(
&self.aligned,
buf,
&mut self.disk_spec,
self.bat_entry.file_offset,
&mut self.bat_entries,
sector_index,
sector_count,
)
.map_err(|e| {
IoError::other(format!(
"Failed writing {sector_count} sectors on VHDx at index {sector_index}: {e}"
))
})?;
self.current_offset = self.current_offset.checked_add(result as u64).unwrap();
Ok(result)
}
}
impl Seek for Vhdx {
/// Wrapper function to satisfy Seek trait implementation for VHDx disk.
/// Updates the offset field in the Vhdx struct.
fn seek(&mut self, pos: SeekFrom) -> IoResult<u64> {
let new_offset: Option<u64> = match pos {
SeekFrom::Start(off) => Some(off),
SeekFrom::End(off) => {
if off < 0 {
0i64.checked_sub(off).and_then(|increment| {
self.virtual_disk_size().checked_sub(increment as u64)
})
} else {
self.virtual_disk_size().checked_add(off as u64)
}
}
SeekFrom::Current(off) => {
if off < 0 {
0i64.checked_sub(off)
.and_then(|increment| self.current_offset.checked_sub(increment as u64))
} else {
self.current_offset.checked_add(off as u64)
}
}
};
if let Some(o) = new_offset
&& o <= self.virtual_disk_size()
{
self.current_offset = o;
return Ok(o);
}
Err(IoError::new(
IoErrorKind::InvalidData,
"Failed seek operation",
))
}
}
impl Vhdx {
pub(crate) fn physical_size(&self) -> result::Result<u64, crate::Error> {
self.aligned
.file()
.metadata()
.map(|m| m.len())
.map_err(crate::Error::GetFileMetadata)
}
}
impl Clone for Vhdx {
fn clone(&self) -> Self {
Vhdx {
aligned: self.aligned.try_clone().unwrap(),
vhdx_header: self.vhdx_header.clone(),
region_entries: self.region_entries.clone(),
bat_entry: self.bat_entry,
mdr_entry: self.mdr_entry,
disk_spec: self.disk_spec.clone(),
bat_entries: self.bat_entries.clone(),
current_offset: self.current_offset,
first_write: self.first_write,
}
}
}
impl AsRawFd for Vhdx {
fn as_raw_fd(&self) -> RawFd {
self.aligned.file().as_raw_fd()
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
use crate::formats::vhdx::test_util::create_dynamic_vhdx;
/// An unaligned sector write under a forced O_DIRECT alignment must go
/// through `AlignedFile`'s read-modify-write bounce (the data block and the
/// BAT update both land at unaligned host offsets) and read back intact.
#[test]
fn unaligned_write_is_rmw() {
let Some(tf) = create_dynamic_vhdx(16) else {
eprintln!("skipping unaligned_write_is_rmw: qemu-img unavailable");
return;
};
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
// Force a non-zero alignment so all of vhdx's positioned I/O exercises
// the bounce/RMW path even though the tempfile is not really O_DIRECT.
vhdx.aligned = AlignedFile::with_alignment(vhdx.aligned.file().try_clone().unwrap(), 512);
let sector = vhdx.disk_spec.logical_sector_size as usize;
let data: Vec<u8> = (0..sector).map(|i| ((i + 1) % 251) as u8).collect();
// Write at virtual offset 0 (allocates a new data block + rewrites BAT).
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.write(&data).unwrap(), data.len());
vhdx.flush().unwrap();
// Read it back through a fresh, forced-alignment handle.
let mut readback = vec![0u8; sector];
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len());
assert_eq!(readback, data);
}
#[test]
fn header_update_survives_reopen() {
let Some(tf) = create_dynamic_vhdx(16) else {
eprintln!("skipping header_update_survives_reopen: qemu-img unavailable");
return;
};
let data = [0xa5u8; 512];
{
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.write(&data).unwrap(), data.len());
vhdx.flush().unwrap();
}
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
let mut readback = [0u8; 512];
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len());
assert_eq!(readback, data);
}
}

View File

@@ -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,
}
}

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