mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Compare commits
15 Commits
main
...
stable/v35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c15de0118 | ||
|
|
ca9e20c27a | ||
|
|
028c3054ea | ||
|
|
82f1dcea3c | ||
|
|
bd6c898b1d | ||
|
|
98c1e21d92 | ||
|
|
7992997299 | ||
|
|
a1627d295a | ||
|
|
9278d565e2 | ||
|
|
f3c6f5c512 | ||
|
|
7c3c2ee0d6 | ||
|
|
19e45650a6 | ||
|
|
c3a4289cbb | ||
|
|
0886e79630 | ||
|
|
a40a0268bb |
@@ -1,50 +0,0 @@
|
||||
[profile.default]
|
||||
# Don't let one individual test run for more than 10 minutes
|
||||
slow-timeout = { period = "60s", terminate-after = 10 }
|
||||
|
||||
[test-groups]
|
||||
windows = { max-threads = 4 }
|
||||
|
||||
[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"
|
||||
|
||||
[profile.windows]
|
||||
inherits = "integration"
|
||||
default-filter = 'test(windows::)'
|
||||
junit.path = "/root/workloads/junit/windows.xml"
|
||||
|
||||
[[profile.windows.overrides]]
|
||||
filter = 'test(windows::)'
|
||||
test-group = 'windows'
|
||||
@@ -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
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,7 +4,7 @@ about: File a bug report
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: Bug
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/feature_request.md
vendored
36
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a feature or enhancement
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: Feature
|
||||
---
|
||||
|
||||
**Elevator pitch**
|
||||
A clear and concise description of what the feature (or enhancement) is.
|
||||
|
||||
**Motivation**
|
||||
Why is this feature important to you? What problem does it solve? Why should we
|
||||
carry this feature?
|
||||
|
||||
**Prior art**
|
||||
Examples of similar features in this project or similar (e.g QEMU, Firecracker,
|
||||
Crosvm, etc)
|
||||
|
||||
**API/CLI**
|
||||
Does this feature require any API or CLI changes?
|
||||
|
||||
**Testing**
|
||||
Can it be tested? Any special CI requirements?
|
||||
|
||||
**Interactions**
|
||||
How does this feature interact with existing features (e.g. hotplug, live
|
||||
migration, etc).
|
||||
|
||||
**Implementation**
|
||||
Do you have an Implementation already? If so link to the branch.
|
||||
|
||||
**Full feature description**
|
||||
Please ensure the structured section above is completed and then fill out this
|
||||
section with any additional details you want.
|
||||
83
.github/dependabot.yml
vendored
83
.github/dependabot.yml
vendored
@@ -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: 1
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
- package-ecosystem: cargo
|
||||
directory: "/fuzz"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 1
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
|
||||
15
.github/workflows/audit.yaml
vendored
Normal file
15
.github/workflows/audit.yaml
vendored
Normal 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 }}
|
||||
64
.github/workflows/build.yaml
vendored
Normal file
64
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
- "1.64"
|
||||
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 musl-gcc
|
||||
run: sudo apt install -y musl-tools
|
||||
|
||||
- name: Install Rust toolchain (${{ matrix.rust }})
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
|
||||
- name: Build (default features)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (kvm)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (default features + tdx)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --features "tdx" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (default features + dbus_api)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --features "dbus_api" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (default features + guest_debug)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --features "guest_debug" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (mshv)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "mshv" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (sev_snp)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "sev_snp" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Build (mshv + kvm)
|
||||
run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "mshv,kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- 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)"
|
||||
968
.github/workflows/ci.yaml
vendored
968
.github/workflows/ci.yaml
vendored
@@ -1,968 +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@v7
|
||||
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@v7
|
||||
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@2a66e89f53d0771bb131a7fa31f3136336094aa6 # v3.4.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: 50
|
||||
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: 45
|
||||
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
|
||||
virtio-villain:
|
||||
name: virtio-villain
|
||||
needs: [preflight, dco, quality, build]
|
||||
if: needs.preflight.outputs.full == 'true'
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VILLAIN_REPO: https://github.com/weltling/virtio-villain.git
|
||||
VILLAIN_REF: v0.6.4
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Verify KVM is available
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
test -e /dev/kvm || { echo "::error::/dev/kvm missing on runner"; exit 1; }
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools cpio gzip python3
|
||||
sudo apt-get install -y virtiofsd || true
|
||||
- name: Build cloud-hypervisor (kvm)
|
||||
run: cargo build --locked --release --bin cloud-hypervisor --no-default-features --features kvm
|
||||
- name: Clone virtio-villain
|
||||
id: villain-src
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
git clone "$VILLAIN_REPO" virtio-villain
|
||||
git -C virtio-villain checkout "$VILLAIN_REF"
|
||||
echo "sha=$(git -C virtio-villain rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache virtio-villain build
|
||||
id: villain-cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: virtio-villain/target
|
||||
key: virtio-villain-${{ runner.os }}-${{ runner.arch }}-${{ steps.villain-src.outputs.sha }}
|
||||
- name: Build virtio-villain initramfs
|
||||
if: steps.villain-cache.outputs.cache-hit != 'true'
|
||||
run: make -C virtio-villain -j"$(nproc)" initramfs
|
||||
- name: Run virtio-villain suite
|
||||
working-directory: virtio-villain
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
mkdir -p villain-logs
|
||||
sudo ./run \
|
||||
--vmm "${GITHUB_WORKSPACE}/target/release/cloud-hypervisor" \
|
||||
--blk-queues 2 --net-queues 2 --cpus 2 --memory 256M \
|
||||
--order=fast \
|
||||
--jobs 4 --batch 10 --timeout 45 --retries 2 --log-dir villain-logs \
|
||||
--format junit --output villain-logs/results.xml \
|
||||
| tee villain-logs/run.out
|
||||
- name: Publish results to run summary
|
||||
if: always()
|
||||
working-directory: virtio-villain
|
||||
run: |
|
||||
set -eufo pipefail
|
||||
{
|
||||
echo '## virtio-villain'
|
||||
echo '```'
|
||||
if [ -f villain-logs/run.out ]; then
|
||||
sed -n '/tests passed/,$p' villain-logs/run.out
|
||||
else
|
||||
echo 'no results (suite did not produce output)'
|
||||
fi
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Upload virtio-villain logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: virtio-villain-logs
|
||||
path: virtio-villain/villain-logs
|
||||
if-no-files-found: ignore
|
||||
# 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
18
.github/workflows/dco.yaml
vendored
Normal 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"
|
||||
104
.github/workflows/docker-image.yaml
vendored
104
.github/workflows/docker-image.yaml
vendored
@@ -1,119 +1,63 @@
|
||||
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
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: docker/login-action@v4.6.0
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
file: ./resources/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }}
|
||||
|
||||
- name: Export digest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=raw,value=20260522-0
|
||||
type=raw,value={{date 'YYYYMMDD'}}-0
|
||||
type=sha
|
||||
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@v4.6.0
|
||||
- name: Build and push
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./resources/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
- name: Build only
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
file: ./resources/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
29
.github/workflows/fuzz-build.yaml
vendored
Normal file
29
.github/workflows/fuzz-build.yaml
vendored
Normal 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
|
||||
26
.github/workflows/hadolint.yaml
vendored
Normal file
26
.github/workflows/hadolint.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Lint Dockerfile
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- resources/Dockerfile
|
||||
pull_request:
|
||||
paths:
|
||||
- resources/Dockerfile
|
||||
|
||||
jobs:
|
||||
hadolint:
|
||||
name: Run Hadolint Dockerfile Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Lint Dockerfile
|
||||
uses: hadolint/hadolint-action@master
|
||||
with:
|
||||
dockerfile: ./resources/Dockerfile
|
||||
format: tty
|
||||
no-fail: false
|
||||
verbose: true
|
||||
failure-threshold: info
|
||||
32
.github/workflows/integration-metrics.yaml
vendored
32
.github/workflows/integration-metrics.yaml
vendored
@@ -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'
|
||||
130
.github/workflows/quality.yaml
vendored
Normal file
130
.github/workflows/quality.yaml
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
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
|
||||
- aarch64-unknown-linux-musl
|
||||
- x86_64-unknown-linux-gnu
|
||||
- x86_64-unknown-linux-musl
|
||||
|
||||
experimental: [false]
|
||||
include:
|
||||
- rust: beta
|
||||
target: aarch64-unknown-linux-gnu
|
||||
experimental: true
|
||||
- rust: beta
|
||||
target: aarch64-unknown-linux-musl
|
||||
experimental: true
|
||||
- rust: beta
|
||||
target: x86_64-unknown-linux-gnu
|
||||
experimental: true
|
||||
- rust: beta
|
||||
target: x86_64-unknown-linux-musl
|
||||
experimental: true
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust toolchain (${{ matrix.rust }})
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Debug Check (default features)
|
||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||
run: |
|
||||
set -e
|
||||
commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }})
|
||||
for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Formatting (rustfmt)
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Clippy (kvm)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (default features)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (default features + guest_debug)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "guest_debug" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (default features + tracing)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "tracing" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (mshv)
|
||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "mshv" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (mshv + kvm)
|
||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "mshv,kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (sev_snp)
|
||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "sev_snp" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Clippy (kvm + tdx)
|
||||
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
|
||||
command: clippy
|
||||
args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "tdx,kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks
|
||||
|
||||
- name: Check build did not modify any files
|
||||
run: test -z "$(git status --porcelain)"
|
||||
|
||||
typos:
|
||||
if: github.event_name == 'pull_request'
|
||||
name: Typos / Spellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# Executes "typos ."
|
||||
- uses: crate-ci/typos@v1.16.11
|
||||
196
.github/workflows/release.yaml
vendored
196
.github/workflows/release.yaml
vendored
@@ -1,69 +1,134 @@
|
||||
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: [pull_request, 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') || github.event_name == 'pull_request'
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v2
|
||||
- name: Install musl-gcc
|
||||
if: contains(matrix.platform.target, 'musl')
|
||||
run: sudo apt install -y musl-tools
|
||||
- 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:
|
||||
toolchain: "1.67.1"
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: Install Rust toolchain (x86_64-unknown-linux-musl)
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.67.1"
|
||||
target: x86_64-unknown-linux-musl
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: "1.67.1"
|
||||
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
|
||||
args: --all --release --features mshv --target=x86_64-unknown-linux-gnu
|
||||
- name: Static Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
name: Artifacts for ${{ matrix.platform.target }}
|
||||
path: |
|
||||
./${{ matrix.platform.name_ch }}
|
||||
./${{ matrix.platform.name_ch_remote }}
|
||||
toolchain: "1.67.1"
|
||||
command: build
|
||||
args: --all --release --features mshv --target=x86_64-unknown-linux-musl
|
||||
- name: Install Rust toolchain (aarch64-unknown-linux-musl)
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.67.1"
|
||||
target: aarch64-unknown-linux-musl
|
||||
override: true
|
||||
- name: Create Release
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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
|
||||
prerelease: true
|
||||
- name: Upload cloud-hypervisor
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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: Clean build tree ahead of cross build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clean
|
||||
- name: Static Build (AArch64)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: true
|
||||
command: build
|
||||
args: --all --release --target=aarch64-unknown-linux-musl
|
||||
- name: Upload static AArch64 cloud-hypervisor
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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
|
||||
if: github.event_name == 'create' && github.event.ref_type == 'tag'
|
||||
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
|
||||
- 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
|
||||
@@ -71,25 +136,16 @@ jobs:
|
||||
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 }}
|
||||
working-directory: ../
|
||||
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
|
||||
id: upload-release-cloud-hypervisor-vendored-sources
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
./${{ matrix.platform.name_ch }}
|
||||
./${{ matrix.platform.name_ch_remote }}
|
||||
./cloud-hypervisor-${{ github.event.ref }}.tar.xz
|
||||
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
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,13 +1,8 @@
|
||||
/build
|
||||
/.cargo
|
||||
/target
|
||||
**/*.rs.bk
|
||||
**/Cargo.lock
|
||||
**/rusty-tags.vi
|
||||
/.agents
|
||||
/.cargo
|
||||
/.claude
|
||||
/.codex
|
||||
/.vscode
|
||||
/build
|
||||
/rpm/SOURCES
|
||||
/target
|
||||
/vendor
|
||||
__pycache__
|
||||
/.vscode
|
||||
|
||||
13
.gitlint
13
.gitlint
@@ -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
|
||||
|
||||
36
.lychee.toml
36
.lychee.toml
@@ -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
|
||||
12
.reuse/dep5
12
.reuse/dep5
@@ -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
|
||||
@@ -1,4 +1 @@
|
||||
edition = "2024"
|
||||
group_imports="StdExternalCrate"
|
||||
imports_granularity="Module"
|
||||
|
||||
edition = "2021"
|
||||
@@ -1,6 +0,0 @@
|
||||
include = ["**/Cargo.toml"]
|
||||
|
||||
[formatting]
|
||||
indent_string = " " # 2 spaces: keep in sync with .editorconfig
|
||||
reorder_arrays = true
|
||||
reorder_keys = true
|
||||
15
.typos.toml
15
.typos.toml
@@ -2,27 +2,20 @@
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"hypervisor/src/kvm/x86_64/mod.rs",
|
||||
"resources/linux-config-*",
|
||||
"hypervisor/src/kvm/x86_64/mod.rs",
|
||||
"resources/linux-config-*",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
ba = "ba"
|
||||
CLASSE = "CLASSE"
|
||||
conectix = "conectix"
|
||||
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"
|
||||
|
||||
79
AGENTS.md
79
AGENTS.md
@@ -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.
|
||||
224
CONTRIBUTING.md
224
CONTRIBUTING.md
@@ -11,71 +11,14 @@ license of those projects.
|
||||
New code should be under the [Apache v2
|
||||
License](https://opensource.org/licenses/Apache-2.0).
|
||||
|
||||
Cloud Hypervisor's main supported architectures are `x86_64` and `aarch64`,
|
||||
and the main hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the
|
||||
most regular exercise, but changes should not make the other supported
|
||||
architecture and backend combinations worse.
|
||||
## Coding Style
|
||||
|
||||
## Coding Style & Code Comments
|
||||
|
||||
We use the [Rust Style] guide and enforce formatting and linting in CI,
|
||||
including `rustfmt`, `clippy`, and other common Rust quality checks, for every
|
||||
pull request. We adapt to best practices, new lints and new tooling as the
|
||||
ecosystem evolves.
|
||||
|
||||
Code should **speak for itself** (for example, by using descriptive identifiers)
|
||||
and be **easy to read and maintain**. Beyond the conventions and tooling
|
||||
described above, contributors have _some_ room to apply their own style and
|
||||
preferred structure. Maintainers may still suggest refactorings where they
|
||||
believe readability, consistency, or maintainability can be improved.
|
||||
|
||||
For new code, add documentation and comments where they **provide additional value**:
|
||||
|
||||
* **Rustdoc** explains the API to its users.
|
||||
* **Inline comments** explain the code the reader, especially *why* it is
|
||||
written that way.
|
||||
* **Commit messages** explain the broader context of a change (for more
|
||||
information on commit messages, see below).
|
||||
|
||||
Comments should be concise and add additional context or information to the code.
|
||||
|
||||
Logging should be minimal and high signal. Use `info!` for important normal
|
||||
state changes that matter in production; use `warn!` or `error!` only for
|
||||
abnormal conditions. Keep `debug!` for focused diagnostics. Please find more
|
||||
information in [`docs/logging.md`](docs/logging.md).
|
||||
|
||||
Error messages should be sentence-style: start with a capital letter and stay
|
||||
concise. For `thiserror`-style errors, put all non-`#[source]` attributes
|
||||
(if they provide clear value) in the outer `Display` text to improve helpfulness,
|
||||
but do not repeat a `#[source]` value there because Cloud Hypervisor prints the
|
||||
full chain elsewhere.
|
||||
|
||||
[Rust Style]: https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src
|
||||
We follow the [Rust Style](https://github.com/rust-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).
|
||||
|
||||
## 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.
|
||||
@@ -83,9 +26,9 @@ 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
|
||||
cargo fmt -- --check || exit 1
|
||||
cargo check --locked --all --all-targets --tests || exit 1
|
||||
cargo clippy --locked --all --all-targets --tests -- -D warnings || exit 1
|
||||
```
|
||||
|
||||
You will need to `chmod +x .git/hooks/pre-commit` to have it run on every
|
||||
@@ -93,93 +36,55 @@ commit you make.
|
||||
|
||||
## Certificate of Origin
|
||||
|
||||
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://www.kernel.org/doc/Documentation/process/submitting-patches.rst)
|
||||
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://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
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Before opening a pull request for a new feature or enhancement request please
|
||||
> create an issue with the "feature request" template and ensure there is
|
||||
> agreement from the maintainers to move ahead with that feature.
|
||||
|
||||
> [!TIP]
|
||||
> When fixing a bug, especially a complex one, please consider opening an issue
|
||||
> with the "bug report" template to make it easier for other users to discover
|
||||
> your fix and aid reviewers. _This is not required for opening a pull request
|
||||
> nor is any agreement required before implementation._
|
||||
|
||||
Cloud Hypervisor uses the “fork-and-pull” development model. Follow these steps if
|
||||
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. 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 it can be integrated.
|
||||
|
||||
## Issue tracking
|
||||
|
||||
@@ -196,83 +101,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.
|
||||
|
||||
2309
Cargo.lock
generated
2309
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
219
Cargo.toml
219
Cargo.toml
@@ -1,141 +1,104 @@
|
||||
# Cloud Hypervisor Workspace
|
||||
#
|
||||
# The main crate producing the binaries is in `./cloud-hypervisor`.
|
||||
[package]
|
||||
name = "cloud-hypervisor"
|
||||
version = "35.1.0"
|
||||
authors = ["The Cloud Hypervisor Authors"]
|
||||
edition = "2021"
|
||||
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"
|
||||
# Minimum buildable version:
|
||||
# Keep in sync with version in .github/workflows/build.yaml
|
||||
# Policy on MSRV (see #4318):
|
||||
# Can only be bumped by:
|
||||
# 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.
|
||||
rust-version = "1.64"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
|
||||
[profile.profiling]
|
||||
debug = true
|
||||
inherits = "release"
|
||||
strip = false
|
||||
debug = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
api_client = { path = "api_client" }
|
||||
argh = "0.1.9"
|
||||
dhat = { version = "0.3.2", optional = true }
|
||||
epoll = "4.3.3"
|
||||
event_monitor = { path = "event_monitor" }
|
||||
hypervisor = { path = "hypervisor" }
|
||||
libc = "0.2.147"
|
||||
log = { version = "0.4.17", features = ["std"] }
|
||||
option_parser = { path = "option_parser" }
|
||||
seccompiler = "0.3.0"
|
||||
serde_json = "1.0.107"
|
||||
signal-hook = "0.3.17"
|
||||
thiserror = "1.0.40"
|
||||
tpm = { path = "tpm"}
|
||||
tracer = { path = "tracer" }
|
||||
vmm = { path = "vmm" }
|
||||
vmm-sys-util = "0.11.0"
|
||||
vm-memory = "0.12.2"
|
||||
zbus = { version = "3.11.1", optional = true }
|
||||
|
||||
# List of patched crates
|
||||
[patch.crates-io]
|
||||
kvm-bindings = { git = "https://github.com/cloud-hypervisor/kvm-bindings", branch = "ch-v0.6.0-tdx" }
|
||||
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 = "5.0.0"
|
||||
net_util = { path = "net_util" }
|
||||
once_cell = "1.18.0"
|
||||
serde_json = "1.0.107"
|
||||
test_infra = { path = "test_infra" }
|
||||
wait-timeout = "0.2.0"
|
||||
|
||||
[features]
|
||||
default = ["kvm", "io_uring"]
|
||||
dbus_api = ["zbus", "vmm/dbus_api"]
|
||||
dhat-heap = ["dhat"] # For heap profiling
|
||||
guest_debug = ["vmm/guest_debug"]
|
||||
io_uring = ["vmm/io_uring"]
|
||||
kvm = ["vmm/kvm"]
|
||||
mshv = ["vmm/mshv"]
|
||||
sev_snp = ["vmm/sev_snp", "mshv"]
|
||||
tdx = ["vmm/tdx"]
|
||||
tracing = ["vmm/tracing", "tracer/tracing"]
|
||||
|
||||
[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",
|
||||
"api_client",
|
||||
"arch",
|
||||
"block",
|
||||
"devices",
|
||||
"event_monitor",
|
||||
"hypervisor",
|
||||
"net_gen",
|
||||
"net_util",
|
||||
"option_parser",
|
||||
"pci",
|
||||
"performance-metrics",
|
||||
"rate_limiter",
|
||||
"serial_buffer",
|
||||
"test_infra",
|
||||
"tracer",
|
||||
"vhost_user_block",
|
||||
"vhost_user_net",
|
||||
"virtio-devices",
|
||||
"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.14.0"
|
||||
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.8.0", default-features = false }
|
||||
vfio_user = { version = "0.1.4", default-features = false }
|
||||
vhost = { version = "0.17.0", default-features = false }
|
||||
vhost-user-backend = { version = "0.23.0", default-features = false }
|
||||
virtio-bindings = "0.2.6"
|
||||
virtio-queue = "0.18.0"
|
||||
vm-fdt = "0.3.0"
|
||||
vm-memory = "0.18.0"
|
||||
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.23.0"
|
||||
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)'] }
|
||||
|
||||
509
Jenkinsfile
vendored
Normal file
509
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,509 @@
|
||||
def runWorkers = true
|
||||
pipeline {
|
||||
agent none
|
||||
options {
|
||||
timeout(time: 4, unit: 'HOURS')
|
||||
}
|
||||
stages {
|
||||
stage('Early checks') {
|
||||
agent { node { label 'built-in' } }
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Check if worker build can be skipped') {
|
||||
when {
|
||||
expression {
|
||||
return skipWorkerBuild()
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
runWorkers = false
|
||||
echo 'No changes requiring a build'
|
||||
}
|
||||
}
|
||||
}
|
||||
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 'jammy' } }
|
||||
when {
|
||||
beforeAgent true
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Prepare environment') {
|
||||
steps {
|
||||
sh 'scripts/prepare_vdpa.sh'
|
||||
}
|
||||
}
|
||||
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('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 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('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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Worker build - AMD') {
|
||||
agent { node { label 'jammy-amd' } }
|
||||
when {
|
||||
beforeAgent true
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Prepare environment') {
|
||||
steps {
|
||||
sh 'scripts/prepare_vdpa.sh'
|
||||
}
|
||||
}
|
||||
stage('Run integration tests') {
|
||||
options {
|
||||
timeout(time: 1, unit: 'HOURS')
|
||||
}
|
||||
steps {
|
||||
sh 'sudo modprobe openvswitch'
|
||||
sh 'scripts/dev_cli.sh tests --integration -- -- --skip common_parallel::test_vfio'
|
||||
}
|
||||
}
|
||||
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 integration tests for musl') {
|
||||
options {
|
||||
timeout(time: 1, unit: 'HOURS')
|
||||
}
|
||||
steps {
|
||||
sh 'sudo modprobe openvswitch'
|
||||
sh 'scripts/dev_cli.sh tests --integration --libc musl -- -- --skip common_parallel::test_vfio'
|
||||
}
|
||||
}
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('AArch64 worker build') {
|
||||
agent { node { label 'bionic-arm64' } }
|
||||
when {
|
||||
beforeAgent true
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
environment {
|
||||
AZURE_CONNECTION_STRING = credentials('46b4e7d6-315f-4cc1-8333-b58780863b9b')
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Run unit tests') {
|
||||
steps {
|
||||
sh 'scripts/dev_cli.sh tests --unit --libc musl'
|
||||
}
|
||||
}
|
||||
stage('Run integration tests') {
|
||||
options {
|
||||
timeout(time: 1, unit: 'HOURS')
|
||||
}
|
||||
steps {
|
||||
sh 'sudo modprobe openvswitch'
|
||||
sh 'scripts/dev_cli.sh tests --integration --libc musl'
|
||||
}
|
||||
}
|
||||
stage('Install azure-cli') {
|
||||
steps {
|
||||
installAzureCli('focal', 'arm64')
|
||||
}
|
||||
}
|
||||
stage('Download Windows image') {
|
||||
steps {
|
||||
sh '''#!/bin/bash -x
|
||||
IMG_BASENAME=windows-11-iot-enterprise-aarch64.raw
|
||||
IMG_PATH=$HOME/workloads/$IMG_BASENAME
|
||||
IMG_GZ_PATH=$HOME/workloads/$IMG_BASENAME.gz
|
||||
IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-9-min.raw.gz
|
||||
cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/"
|
||||
pushd "$HOME/workloads"
|
||||
if sha1sum "$IMG_BASENAME.sha1" --check; then
|
||||
exit
|
||||
fi
|
||||
popd
|
||||
mkdir -p "$HOME/workloads"
|
||||
az storage blob download \
|
||||
--container-name private-images \
|
||||
--file "$IMG_GZ_PATH" \
|
||||
--name "$IMG_GZ_BLOB_NAME" \
|
||||
--connection-string "$AZURE_CONNECTION_STRING"
|
||||
gzip -d $IMG_GZ_PATH
|
||||
'''
|
||||
}
|
||||
}
|
||||
stage('Run Windows guest integration tests') {
|
||||
options {
|
||||
timeout(time: 1, unit: 'HOURS')
|
||||
}
|
||||
steps {
|
||||
sh 'scripts/dev_cli.sh tests --integration-windows --libc musl'
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
sh "sudo chown -R jenkins.jenkins ${WORKSPACE}"
|
||||
deleteDir()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Worker build - Windows guest') {
|
||||
agent { node { label 'jammy' } }
|
||||
when {
|
||||
beforeAgent true
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
environment {
|
||||
AZURE_CONNECTION_STRING = credentials('46b4e7d6-315f-4cc1-8333-b58780863b9b')
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Install azure-cli') {
|
||||
steps {
|
||||
installAzureCli('jammy', 'amd64')
|
||||
}
|
||||
}
|
||||
stage('Download assets') {
|
||||
steps {
|
||||
sh "mkdir ${env.HOME}/workloads"
|
||||
sh 'az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2022-amd64-2.raw" --name windows-server-2022-amd64-2.raw --connection-string "$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 - Metrics') {
|
||||
agent { node { label 'jammy-metrics' } }
|
||||
when {
|
||||
branch 'main'
|
||||
beforeAgent true
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
environment {
|
||||
METRICS_PUBLISH_KEY = credentials('52e0945f-ce7a-43d1-87af-67d1d87cc40f')
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Run metrics tests') {
|
||||
options {
|
||||
timeout(time: 1, unit: 'HOURS')
|
||||
}
|
||||
steps {
|
||||
sh 'scripts/dev_cli.sh tests --metrics -- -- --report-file /root/workloads/metrics.json'
|
||||
}
|
||||
}
|
||||
stage('Upload metrics report') {
|
||||
steps {
|
||||
sh 'curl -X PUT https://cloud-hypervisor-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Worker build - Rate Limiter') {
|
||||
agent { node { label 'focal-metrics' } }
|
||||
when {
|
||||
branch 'main'
|
||||
beforeAgent true
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Run rate-limiter integration tests') {
|
||||
options {
|
||||
timeout(time: 10, unit: 'MINUTES')
|
||||
}
|
||||
steps {
|
||||
sh 'scripts/dev_cli.sh tests --integration-rate-limiter'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Worker build - SGX') {
|
||||
agent { node { label 'jammy-sgx' } }
|
||||
when {
|
||||
beforeAgent true
|
||||
allOf {
|
||||
branch 'main'
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
}
|
||||
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 'jammy-vfio' } }
|
||||
when {
|
||||
beforeAgent true
|
||||
allOf {
|
||||
branch 'main'
|
||||
expression {
|
||||
return runWorkers
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
regression {
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
slackSend(color: '#ff0000', message: '"main" branch build is now failing', channel: '#jenkins-ci')
|
||||
}
|
||||
}
|
||||
}
|
||||
fixed {
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
slackSend(color: '#00ff00', message: '"main" branch build is now fixed', channel: '#jenkins-ci')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(distro, arch) {
|
||||
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=${arch}] https://packages.microsoft.com/repos/azure-cli/ ${distro} main\" | sudo tee /etc/apt/sources.list.d/azure-cli.list"
|
||||
sh 'sudo apt update'
|
||||
sh 'sudo apt install -y azure-cli'
|
||||
}
|
||||
|
||||
def boolean skipWorkerBuild() {
|
||||
if (env.CHANGE_TARGET == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (sh(
|
||||
returnStatus: true,
|
||||
script: "git diff --name-only origin/${env.CHANGE_TARGET}... | grep -v '\\.md'"
|
||||
) != 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (sh(
|
||||
returnStatus: true,
|
||||
script: "git diff --name-only origin/${env.CHANGE_TARGET}... | grep -v -E 'fuzz/'"
|
||||
) != 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (sh(
|
||||
returnStatus: true,
|
||||
script: "git diff --name-only origin/${env.CHANGE_TARGET}... | grep -v -E '.github/'"
|
||||
) != 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
74
README.md
74
README.md
@@ -59,13 +59,9 @@ 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
|
||||
minor differences in functionality between the two architectures
|
||||
(see [#1125](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/1125)).
|
||||
|
||||
### Guest OS
|
||||
|
||||
@@ -111,25 +107,19 @@ do not wish to use the pre-built binaries.
|
||||
|
||||
## Booting Linux
|
||||
|
||||
Cloud Hypervisor boots guests in one of two ways. The first is direct
|
||||
kernel boot, where a kernel image is passed to `--kernel`. The x86-64
|
||||
kernel must be built with PVH support or be a bzImage. The second is
|
||||
firmware boot, where a firmware image is passed to `--firmware` and
|
||||
brings up the guest's normal boot loader.
|
||||
Cloud Hypervisor supports direct kernel boot (the x86-64 kernel requires the kernel
|
||||
built with PVH support) or booting via a firmware (either [Rust Hypervisor
|
||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) or an
|
||||
edk2 UEFI firmware called `CLOUDHV` / `CLOUDHV_EFI`.)
|
||||
|
||||
Two firmware options are supported, and which one works best depends
|
||||
on the guest OS. [Rust Hypervisor
|
||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware)
|
||||
is a lightweight Rust-based PVH firmware. The edk2 UEFI firmware is
|
||||
called `CLOUDHV.fd` for x86-64 and `CLOUDHV_EFI.fd` for AArch64.
|
||||
Prebuilt binaries for both are available at their respective releases
|
||||
pages, [Rust Hypervisor
|
||||
Binary builds of the firmware files are available for the latest release of
|
||||
[Rust Hypervisor
|
||||
Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/latest)
|
||||
and [our edk2
|
||||
fork](https://github.com/cloud-hypervisor/edk2/releases/latest).
|
||||
The edk2 fork carries customizations required to boot AArch64 guests
|
||||
on cloud-hypervisor. See [docs/uefi.md](docs/uefi.md) for differences
|
||||
with upstream tianocore/edk2.
|
||||
repository](https://github.com/cloud-hypervisor/edk2/releases/latest)
|
||||
|
||||
The choice of firmware depends on your guest OS choice; some experimentation
|
||||
may be required.
|
||||
|
||||
### Firmware Booting
|
||||
|
||||
@@ -159,8 +149,8 @@ interface will be enabled as per `network-config` details.
|
||||
$ 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 \
|
||||
--kernel ./hypervisor-fw \
|
||||
--disk path=focal-server-cloudimg-amd64.raw --disk path=/tmp/ubuntu-cloudinit.img \
|
||||
--cpus boot=4 \
|
||||
--memory size=1024M \
|
||||
--net "tap=,mac=,ip=,mask="
|
||||
@@ -173,7 +163,7 @@ GRUB) is required then it necessary to switch to the serial console instead of
|
||||
```shell
|
||||
$ ./cloud-hypervisor \
|
||||
--kernel ./hypervisor-fw \
|
||||
--disk path=focal-server-cloudimg-amd64.raw path=/tmp/ubuntu-cloudinit.img \
|
||||
--disk path=focal-server-cloudimg-amd64.raw --disk path=/tmp/ubuntu-cloudinit.img \
|
||||
--cpus boot=4 \
|
||||
--memory size=1024M \
|
||||
--net "tap=,mac=,ip=,mask=" \
|
||||
@@ -181,31 +171,24 @@ $ ./cloud-hypervisor \
|
||||
--console off
|
||||
```
|
||||
|
||||
## Booting: `--firmware` vs `--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. For x86-64, a `vmlinux` ELF kernel (compiled with PVH support) is needed. In order to support development there is a custom branch; however provided the required options are enabled any recent kernel will suffice.
|
||||
|
||||
To build the kernel:
|
||||
|
||||
```shell
|
||||
# Clone the Cloud Hypervisor Linux branch
|
||||
$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.16.9 linux-cloud-hypervisor
|
||||
$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.2 linux-cloud-hypervisor
|
||||
$ pushd linux-cloud-hypervisor
|
||||
$ make ch_defconfig
|
||||
# Use the x86-64 cloud-hypervisor kernel config to build your kernel for x86-64
|
||||
$ wget https://raw.githubusercontent.com/cloud-hypervisor/cloud-hypervisor/main/resources/linux-config-x86_64
|
||||
# Use the AArch64 cloud-hypervisor kernel config to build your kernel for AArch64
|
||||
$ wget https://raw.githubusercontent.com/cloud-hypervisor/cloud-hypervisor/main/resources/linux-config-aarch64
|
||||
$ cp linux-config-x86_64 .config # x86-64
|
||||
$ cp linux-config-aarch64 .config # AArch64
|
||||
# Do native build of the x86-64 kernel
|
||||
$ KCFLAGS="-Wa,-mx86-used-note=no" make bzImage -j `nproc`
|
||||
# Do native build of the AArch64 kernel
|
||||
@@ -242,7 +225,7 @@ $ sudo setcap cap_net_admin+ep ./cloud-hypervisor
|
||||
$ ./create-cloud-init.sh
|
||||
$ ./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 --disk path=/tmp/ubuntu-cloudinit.img \
|
||||
--cmdline "console=hvc0 root=/dev/vda1 rw" \
|
||||
--cpus boot=4 \
|
||||
--memory size=1024M \
|
||||
@@ -256,7 +239,7 @@ $ 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 \
|
||||
--disk path=focal-server-cloudimg-arm64.raw --disk path=/tmp/ubuntu-cloudinit.img \
|
||||
--cmdline "console=hvc0 root=/dev/vda1 rw" \
|
||||
--cpus boot=4 \
|
||||
--memory size=1024M \
|
||||
@@ -319,9 +302,8 @@ Further details can be found in the [release documentation](docs/releases.md).
|
||||
As of 2023-01-03, 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 Jammy](https://cloud-images.ubuntu.com/jammy/current/) (jammy-server-cloudimg-{amd64,arm64}.img )
|
||||
- [Fedora 36](https://fedora.mirrorservice.org/fedora/linux/releases/36/Cloud/) ([Fedora-Cloud-Base-36-1.5.x86_64.raw.xz](https://fedora.mirrorservice.org/fedora/linux/releases/36/Cloud/x86_64/images/) / [Fedora-Cloud-Base-36-1.5.aarch64.raw.xz](https://fedora.mirrorservice.org/fedora/linux/releases/36/Cloud/aarch64/images/))
|
||||
|
||||
Direct kernel boot to userspace should work with a rootfs from most
|
||||
distributions although you may need to enable exotic filesystem types in the
|
||||
|
||||
68
SECURITY.md
68
SECURITY.md
@@ -1,68 +0,0 @@
|
||||
# Cloud Hypervisor Security Policy
|
||||
|
||||
## What Is A Vulnerability?
|
||||
|
||||
Cloud Hypervisor's threat model is in [docs/threat-model.md](docs/threat-model.md).
|
||||
A vulnerability is defined as an entity defined in the threat model as
|
||||
untrusted being able to cause Cloud Hypervisor to do something that the
|
||||
threat model states it should not be able to cause.
|
||||
|
||||
Any known or potential memory corruption is assumed exploitable until
|
||||
and unless proven otherwise. Attackers have shown repeatedly that memory
|
||||
corruption can usually be turned into arbitrary code execution. While
|
||||
doing so may be very difficult, LLMs have made this much easier.
|
||||
|
||||
Mishandling of a memory allocation failure (either user-mode or
|
||||
kernel-mode) is still in scope. While this will typically result in
|
||||
Cloud Hypervisor crashing, Cloud Hypervisor must not corrupt its own
|
||||
memory or otherwise behave insecurely.
|
||||
|
||||
## How To Report A Vulnerability?
|
||||
|
||||
Vulnerabilities should be reported using the GitHub Security Advisory
|
||||
process. Do not file an issue, as that immediately gives malicious
|
||||
actors knowledge of the vulnerability. A proof of concept is strongly
|
||||
preferred but not strictly required. A patch is also greatly
|
||||
appreciated but is also not a requirement.
|
||||
|
||||
Cloud Hypervisor does not currently have any bug bounty program.
|
||||
|
||||
The use of automated tooling to find vulnerabilities is encouraged.
|
||||
This includes large language models and other forms of AI. The tool used
|
||||
should be noted in the report. The human making the report is
|
||||
responsible for its contents and for filtering out false positives.
|
||||
|
||||
It is not expected that every single report will be valid, but reporters
|
||||
must make a good-faith effort to avoid false positives. Striving to achieve a
|
||||
zero false-positive rate will reduce the number of correct reports and is not
|
||||
worthwhile.
|
||||
|
||||
## When A Vulnerability Is Reported
|
||||
|
||||
The Cloud Hypervisor maintainers will triage any reported
|
||||
vulnerabilities. Once patches are ready, an embargo period of up to 14
|
||||
days starts. There will be a public announcement that a vulnerability
|
||||
is under embargo, along with its GHSA number.
|
||||
|
||||
The following organizations will receive full access to embargoed
|
||||
information. They are only permitted to use this information for
|
||||
preparing and deploying patches. Information must be limited to those
|
||||
who need to know. This includes access to both patched source code and
|
||||
patched binaries.
|
||||
|
||||
- Microsoft
|
||||
- Crusoe
|
||||
- Cyberus Technology
|
||||
- Meta
|
||||
- Google
|
||||
- UbiCloud
|
||||
|
||||
This list may be extended by filing a PR. It will only include:
|
||||
|
||||
- Organizations that distribute Cloud Hypervisor to a significant number
|
||||
of users.
|
||||
|
||||
- Organizations that use Cloud Hypervisor to provide a managed service
|
||||
to a significant number of users.
|
||||
|
||||
The list is documented here for the purposes of transparency.
|
||||
@@ -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 = "2021"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
vmm-sys-util = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
vmm-sys-util = "0.11.0"
|
||||
|
||||
@@ -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,
|
||||
@@ -102,7 +105,7 @@ fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
res.push_str(str::from_utf8(&bytes[0..count]).unwrap());
|
||||
res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap());
|
||||
|
||||
// End of headers
|
||||
if let Some(o) = res.find("\r\n\r\n") {
|
||||
@@ -120,11 +123,12 @@ 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..])));
|
||||
@@ -144,7 +148,7 @@ pub fn simple_api_full_command_with_fds_and_response<T: Read + Write + ScmSocket
|
||||
method: &str,
|
||||
full_command: &str,
|
||||
request_body: Option<&str>,
|
||||
request_fds: &[RawFd],
|
||||
request_fds: Vec<RawFd>,
|
||||
) -> Result<Option<String>, Error> {
|
||||
socket
|
||||
.send_with_fds(
|
||||
@@ -152,7 +156,7 @@ pub fn simple_api_full_command_with_fds_and_response<T: Read + Write + ScmSocket
|
||||
"{method} /api/v1/{full_command} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n"
|
||||
)
|
||||
.as_bytes()],
|
||||
request_fds,
|
||||
&request_fds,
|
||||
)
|
||||
.map_err(Error::SocketSendFds)?;
|
||||
|
||||
@@ -180,7 +184,7 @@ pub fn simple_api_full_command_with_fds<T: Read + Write + ScmSocket>(
|
||||
method: &str,
|
||||
full_command: &str,
|
||||
request_body: Option<&str>,
|
||||
request_fds: &[RawFd],
|
||||
request_fds: Vec<RawFd>,
|
||||
) -> Result<(), Error> {
|
||||
let response = simple_api_full_command_with_fds_and_response(
|
||||
socket,
|
||||
@@ -190,8 +194,8 @@ pub fn simple_api_full_command_with_fds<T: Read + Write + ScmSocket>(
|
||||
request_fds,
|
||||
)?;
|
||||
|
||||
if let Some(response) = response {
|
||||
println!("{response}");
|
||||
if response.is_some() {
|
||||
println!("{}", response.unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -203,7 +207,7 @@ pub fn simple_api_full_command<T: Read + Write + ScmSocket>(
|
||||
full_command: &str,
|
||||
request_body: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
simple_api_full_command_with_fds(socket, method, full_command, request_body, &[])
|
||||
simple_api_full_command_with_fds(socket, method, full_command, request_body, Vec::new())
|
||||
}
|
||||
|
||||
pub fn simple_api_full_command_and_response<T: Read + Write + ScmSocket>(
|
||||
@@ -212,7 +216,13 @@ pub fn simple_api_full_command_and_response<T: Read + Write + ScmSocket>(
|
||||
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, &[])
|
||||
simple_api_full_command_with_fds_and_response(
|
||||
socket,
|
||||
method,
|
||||
full_command,
|
||||
request_body,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn simple_api_command_with_fds<T: Read + Write + ScmSocket>(
|
||||
@@ -220,7 +230,7 @@ pub fn simple_api_command_with_fds<T: Read + Write + ScmSocket>(
|
||||
method: &str,
|
||||
c: &str,
|
||||
request_body: Option<&str>,
|
||||
request_fds: &[RawFd],
|
||||
request_fds: Vec<RawFd>,
|
||||
) -> Result<(), Error> {
|
||||
// Create the full VM command. For VMM commands, use
|
||||
// simple_api_full_command().
|
||||
@@ -235,5 +245,5 @@ pub fn simple_api_command<T: Read + Write + ScmSocket>(
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
[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 = "2021"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
fw_cfg = []
|
||||
kvm = ["hypervisor/kvm"]
|
||||
sev_snp = []
|
||||
tdx = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
anyhow = "1.0.75"
|
||||
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.147"
|
||||
linux-loader = { version = "0.9.1", features = ["elf", "bzimage", "pe"] }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1.0.168", features = ["rc", "derive"] }
|
||||
thiserror = "1.0.40"
|
||||
uuid = "1.3.4"
|
||||
versionize = "0.1.10"
|
||||
versionize_derive = "0.1.4"
|
||||
vm-memory = { version = "0.12.2", features = ["backend-mmap", "backend-bitmap"] }
|
||||
vm-migration = { path = "../vm-migration" }
|
||||
vmm-sys-util = { version = "0.11.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.4", package = "fdt" }
|
||||
vm-fdt = { git = "https://github.com/rust-vmm/vm-fdt", branch = "main" }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -6,33 +6,28 @@
|
||||
// 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 hypervisor::arch::aarch64::gic::Vgic;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CStr;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::BuildHasher;
|
||||
use std::result;
|
||||
use std::str;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{cmp, result, str};
|
||||
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use fdt_parser::node::FdtNode;
|
||||
use hypervisor::arch::aarch64::gic::Vgic;
|
||||
use hypervisor::arch::aarch64::regs::{
|
||||
AARCH64_ARCH_TIMER_HYP_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ,
|
||||
AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_PMU_IRQ,
|
||||
};
|
||||
use log::{debug, info};
|
||||
use thiserror::Error;
|
||||
use vm_fdt::{FdtWriter, FdtWriterResult};
|
||||
use vm_memory::{Address, Bytes, GuestMemoryBackend, 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::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 std::fs;
|
||||
use std::path::Path;
|
||||
use vm_fdt::{FdtWriter, FdtWriterResult};
|
||||
use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion};
|
||||
|
||||
// This is a value for uniquely identifying the FDT node declaring the interrupt controller.
|
||||
const GIC_PHANDLE: u32 = 1;
|
||||
@@ -67,6 +62,9 @@ const GIC_FDT_IRQ_TYPE_PPI: u32 = 1;
|
||||
const IRQ_TYPE_EDGE_RISING: u32 = 1;
|
||||
const IRQ_TYPE_LEVEL_HI: u32 = 4;
|
||||
|
||||
// PMU PPI interrupt number
|
||||
pub const AARCH64_PMU_IRQ: u32 = 7;
|
||||
|
||||
// Keys and Buttons
|
||||
// System Power Down
|
||||
const KEY_POWER: u32 = 116;
|
||||
@@ -82,21 +80,146 @@ 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>;
|
||||
|
||||
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() {
|
||||
warn!("File: {} does not exist.", file_directory);
|
||||
0
|
||||
} else {
|
||||
info!("File: {} exist.", file_directory);
|
||||
|
||||
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
|
||||
// The content of the file is as simple as a size, like: "32K"
|
||||
let src = src.trim();
|
||||
let src_digits: u32 = src[0..src.len() - 1].parse().unwrap();
|
||||
let src_unit = &src[src.len() - 1..];
|
||||
|
||||
src_digits
|
||||
* match src_unit {
|
||||
"K" => 1024,
|
||||
"M" => 1024u32.pow(2),
|
||||
"G" => 1024u32.pow(3),
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
warn!("File: {} does not exist.", file_directory);
|
||||
0
|
||||
} else {
|
||||
info!("File: {} exist.", file_directory);
|
||||
|
||||
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
|
||||
src.trim().parse::<u32>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
warn!("File: {} does not exist.", file_directory);
|
||||
0
|
||||
} else {
|
||||
info!("File: {} exist.", file_directory);
|
||||
|
||||
let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted.");
|
||||
src.trim().parse::<u32>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
warn!("File: {} does not exist.", file_directory);
|
||||
result = false;
|
||||
} else {
|
||||
info!("File: {} exist.", file_directory);
|
||||
|
||||
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(',');
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Creates the flattened device tree for this aarch64 VM.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
|
||||
guest_mem: &GuestMemoryMmap,
|
||||
cmdline: &str,
|
||||
vcpu_mpidr: &[u64],
|
||||
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>>,
|
||||
initrd: &Option<InitramfsConfig>,
|
||||
@@ -109,8 +232,8 @@ pub fn create_fdt<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
|
||||
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,7 +245,7 @@ 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)?;
|
||||
@@ -146,10 +269,10 @@ 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.
|
||||
guest_mem
|
||||
.write_slice(fdt_final, super::layout::FDT_START)
|
||||
.write_slice(fdt_final.as_slice(), super::layout::FDT_START)
|
||||
.map_err(Error::WriteFdtToMemory)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -158,7 +281,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,41 +290,67 @@ 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;
|
||||
let (threads_per_core, cores_per_package, packages) = vcpu_topology.unwrap_or((1, 1, 1));
|
||||
let max_cpus: u32 = (threads_per_core * cores_per_package * packages).into();
|
||||
|
||||
// Add cache info.
|
||||
let cache_info = read_cache_topology();
|
||||
let cache_exist = cache_info.is_some();
|
||||
let CacheTopologyInfo {
|
||||
l1_d_cache_size,
|
||||
l1_d_cache_line_size,
|
||||
l1_d_cache_sets,
|
||||
l1_i_cache_size,
|
||||
l1_i_cache_line_size,
|
||||
l1_i_cache_sets,
|
||||
l2_cache_size,
|
||||
l2_cache_line_size,
|
||||
l2_cache_sets,
|
||||
l3_cache_size,
|
||||
l3_cache_line_size,
|
||||
l3_cache_sets,
|
||||
l2_cache_shared,
|
||||
l3_cache_shared,
|
||||
} = cache_info.unwrap_or_default();
|
||||
// L1 Data Cache Info.
|
||||
let mut l1_d_cache_size: u32 = 0;
|
||||
let mut l1_d_cache_line_size: u32 = 0;
|
||||
let mut l1_d_cache_sets: u32 = 0;
|
||||
|
||||
// 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");
|
||||
// L1 Instruction Cache Info.
|
||||
let mut l1_i_cache_size: u32 = 0;
|
||||
let mut l1_i_cache_line_size: u32 = 0;
|
||||
let mut l1_i_cache_sets: u32 = 0;
|
||||
|
||||
// L2 Cache Info.
|
||||
let mut l2_cache_size: u32 = 0;
|
||||
let mut l2_cache_line_size: u32 = 0;
|
||||
let mut l2_cache_sets: u32 = 0;
|
||||
|
||||
// L3 Cache Info.
|
||||
let mut l3_cache_size: u32 = 0;
|
||||
let mut l3_cache_line_size: u32 = 0;
|
||||
let mut l3_cache_sets: u32 = 0;
|
||||
|
||||
// Cache Shared Info.
|
||||
let mut l2_cache_shared: bool = false;
|
||||
let mut l3_cache_shared: bool = false;
|
||||
|
||||
let cache_path = Path::new("/sys/devices/system/cpu/cpu0/cache");
|
||||
let cache_exist: bool = cache_path.exists();
|
||||
if !cache_exist {
|
||||
warn!("cache sysfs system does not exist.");
|
||||
} else {
|
||||
info!("cache sysfs system exists.");
|
||||
// L1 Data Cache Info.
|
||||
l1_d_cache_size = get_cache_size(CacheLevel::L1D);
|
||||
l1_d_cache_line_size = get_cache_coherency_line_size(CacheLevel::L1D);
|
||||
l1_d_cache_sets = get_cache_number_of_sets(CacheLevel::L1D);
|
||||
|
||||
// L1 Instruction Cache Info.
|
||||
l1_i_cache_size = get_cache_size(CacheLevel::L1I);
|
||||
l1_i_cache_line_size = get_cache_coherency_line_size(CacheLevel::L1I);
|
||||
l1_i_cache_sets = get_cache_number_of_sets(CacheLevel::L1I);
|
||||
|
||||
// L2 Cache Info.
|
||||
l2_cache_size = get_cache_size(CacheLevel::L2);
|
||||
l2_cache_line_size = get_cache_coherency_line_size(CacheLevel::L2);
|
||||
l2_cache_sets = get_cache_number_of_sets(CacheLevel::L2);
|
||||
|
||||
// L3 Cache Info.
|
||||
l3_cache_size = get_cache_size(CacheLevel::L3);
|
||||
l3_cache_line_size = get_cache_coherency_line_size(CacheLevel::L3);
|
||||
l3_cache_sets = get_cache_number_of_sets(CacheLevel::L3);
|
||||
|
||||
// Cache Shared Info.
|
||||
if l2_cache_size != 0 {
|
||||
l2_cache_shared = get_cache_shared(CacheLevel::L2);
|
||||
}
|
||||
if l3_cache_size != 0 {
|
||||
l3_cache_shared = get_cache_shared(CacheLevel::L3);
|
||||
}
|
||||
}
|
||||
|
||||
for (cpu_id, mpidr) in vcpu_mpidr.iter().enumerate().take(num_cpus) {
|
||||
@@ -218,13 +367,11 @@ 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)?;
|
||||
}
|
||||
}
|
||||
@@ -277,6 +424,9 @@ fn create_cpu_nodes(
|
||||
|
||||
fdt.end_node(l2_cache_node)?;
|
||||
}
|
||||
if l2_cache_size != 0 && l2_cache_shared {
|
||||
warn!("L2 cache shared with other cpus");
|
||||
}
|
||||
}
|
||||
|
||||
fdt.end_node(cpu_node)?;
|
||||
@@ -285,8 +435,8 @@ fn create_cpu_nodes(
|
||||
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 = format!("l3-cache{i}");
|
||||
let l3_cache_node = fdt.begin_node(&l3_cache_name)?;
|
||||
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
|
||||
@@ -313,8 +463,7 @@ fn create_cpu_nodes(
|
||||
}
|
||||
|
||||
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;
|
||||
let (threads_per_core, cores_per_package, packages) = topology;
|
||||
let cpu_map_node = fdt.begin_node("cpu-map")?;
|
||||
|
||||
// Create device tree nodes with regard of above mapping.
|
||||
@@ -362,14 +511,7 @@ fn create_memory_node(
|
||||
) -> 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();
|
||||
@@ -381,30 +523,27 @@ fn create_memory_node(
|
||||
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
|
||||
// Set the node address the first non-zero regison address
|
||||
if node_memory_addr == 0 {
|
||||
node_memory_addr = memory_region_start_addr;
|
||||
}
|
||||
}
|
||||
// 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 = fdt.begin_node(&memory_node_name)?;
|
||||
fdt.property_string("device_type", "memory")?;
|
||||
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
||||
fdt.property_u32("numa-node-id", numa_node_idx as u32)?;
|
||||
fdt.end_node(memory_node)?;
|
||||
}
|
||||
let memory_node_name = format!("memory@{node_memory_addr: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.property_u32("numa-node-id", numa_node_idx as u32)?;
|
||||
fdt.end_node(memory_node)?;
|
||||
}
|
||||
} else {
|
||||
// Note: memory regions from "GuestMemoryBackend" are sorted and non-zero sized.
|
||||
// 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("GuestMemoryBackend must have one memory region at least")
|
||||
.expect("GuestMemory must have one memory region at least")
|
||||
.raw_value();
|
||||
let mut current_end = current_start;
|
||||
|
||||
@@ -430,7 +569,7 @@ fn create_memory_node(
|
||||
|
||||
if ram_regions.len() > 2 {
|
||||
panic!(
|
||||
"There should be up to two non-continuous regions, divided by the
|
||||
"There should be up to two non-continuous regions, devidided by the
|
||||
gap at the end of 32bit address space."
|
||||
);
|
||||
}
|
||||
@@ -448,14 +587,15 @@ fn create_memory_node(
|
||||
&& (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}"
|
||||
"Unexpected first memory region layout: (start: 0x{:08x}, end: 0x{:08x}).
|
||||
ram_start: 0x{:08x}, mem_32bit_reserved_start: 0x{:08x}",
|
||||
first_region_start, first_region_end, ram_start, mem_32bit_reserved_start
|
||||
);
|
||||
}
|
||||
|
||||
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_name = format!("memory@{:x}", ram_start);
|
||||
let memory_node = fdt.begin_node(&memory_node_name)?;
|
||||
fdt.property_string("device_type", "memory")?;
|
||||
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
||||
@@ -468,13 +608,14 @@ fn create_memory_node(
|
||||
|
||||
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}"
|
||||
"Unexpected second memory region layout: start: 0x{:08x}, ram_64bit_start: 0x{:08x}",
|
||||
second_region_start, ram_64bit_start
|
||||
);
|
||||
}
|
||||
|
||||
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_name = format!("memory@{:x}", ram_64bit_start);
|
||||
let memory_node = fdt.begin_node(&memory_node_name)?;
|
||||
fdt.property_string("device_type", "memory")?;
|
||||
fdt.property_array_u64("reg", &mem_reg_prop)?;
|
||||
@@ -531,19 +672,11 @@ fn create_gic_node(fdt: &mut FdtWriter, gic_device: &Arc<Mutex<dyn Vgic>>) -> Fd
|
||||
|
||||
if gic_device.lock().unwrap().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.lock().unwrap().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)?;
|
||||
}
|
||||
|
||||
fdt.end_node(msic_node)?;
|
||||
}
|
||||
|
||||
@@ -570,14 +703,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 +840,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 +855,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)?,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,7 +894,7 @@ 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) =
|
||||
@@ -873,39 +984,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@{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")?;
|
||||
|
||||
// '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", ®)?;
|
||||
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", ®)?;
|
||||
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 +1026,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 +1038,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 +1081,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 +1111,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 +1137,7 @@ fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) {
|
||||
array,
|
||||
indent = (n_spaces + 2)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Print children nodes if there is any
|
||||
@@ -1054,118 +1145,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +111,8 @@ pub const RAM_64BIT_START: GuestAddress = GuestAddress(0x1_0000_0000);
|
||||
pub const CMDLINE_MAX_SIZE: usize = 2048;
|
||||
|
||||
/// FDT is at the beginning of RAM.
|
||||
/// Maximum size of the device tree blob as specified in https://www.kernel.org/doc/Documentation/arm64/booting.txt.
|
||||
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;
|
||||
|
||||
/// Put ACPI table above dtb
|
||||
@@ -138,12 +137,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";
|
||||
|
||||
@@ -2,63 +2,58 @@
|
||||
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// Module for cache info.
|
||||
pub mod cache;
|
||||
/// Module for the flattened device tree.
|
||||
pub mod fdt;
|
||||
/// Layout for this aarch64 system.
|
||||
pub mod layout;
|
||||
/// Module for system registers definition
|
||||
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, GuestMemoryAtomic, GuestMemoryBackend};
|
||||
|
||||
pub use self::fdt::DeviceInfoForFdt;
|
||||
use crate::{DeviceType, GuestMemoryMmap, NumaNodes, PciSpaceInfo, RegionType};
|
||||
use hypervisor::arch::aarch64::gic::Vgic;
|
||||
use log::{log_enabled, Level};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic};
|
||||
|
||||
pub const _NSIG: i32 = 65;
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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(hypervisor::HypervisorCpuError),
|
||||
|
||||
/// 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::PlatformSpecific(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
/// Specifies the entry point address where the guest must start
|
||||
/// executing code.
|
||||
@@ -69,8 +64,8 @@ pub struct EntryPoint {
|
||||
|
||||
/// Configure the specified VCPU, and return its MPIDR.
|
||||
pub fn configure_vcpu(
|
||||
vcpu: &dyn hypervisor::Vcpu,
|
||||
id: u32,
|
||||
vcpu: &Arc<dyn hypervisor::Vcpu>,
|
||||
id: u8,
|
||||
boot_setup: Option<(EntryPoint, &GuestMemoryAtomic<GuestMemoryMmap>)>,
|
||||
) -> super::Result<u64> {
|
||||
if let Some((kernel_entry_point, _guest_memory)) = boot_setup {
|
||||
@@ -82,7 +77,9 @@ pub fn configure_vcpu(
|
||||
.map_err(Error::RegsConfiguration)?;
|
||||
}
|
||||
|
||||
let mpidr = vcpu.get_sys_reg(MPIDR_EL1).map_err(Error::VcpuRegMpidr)?;
|
||||
let mpidr = vcpu
|
||||
.get_sys_reg(regs::MPIDR_EL1)
|
||||
.map_err(Error::VcpuRegMpidr)?;
|
||||
Ok(mpidr)
|
||||
}
|
||||
|
||||
@@ -124,12 +121,12 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> {
|
||||
}
|
||||
|
||||
/// Configures the system and should be called once per vm before starting vcpu threads.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: BuildHasher>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn configure_system<T: DeviceInfoForFdt + Clone + Debug, S: ::std::hash::BuildHasher>(
|
||||
guest_mem: &GuestMemoryMmap,
|
||||
cmdline: &str,
|
||||
vcpu_mpidr: &[u64],
|
||||
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],
|
||||
@@ -157,7 +154,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(())
|
||||
}
|
||||
@@ -183,7 +180,7 @@ pub fn initramfs_load_addr(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
|
||||
pub fn get_host_cpu_phys_bits(hypervisor: &Arc<dyn hypervisor::Hypervisor>) -> u8 {
|
||||
let host_cpu_phys_bits = hypervisor.get_host_ipa_limit().try_into().unwrap();
|
||||
if host_cpu_phys_bits == 0 {
|
||||
// Host kernel does not support `get_host_ipa_limit`,
|
||||
@@ -195,7 +192,7 @@ pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
43
arch/src/aarch64/regs.rs
Normal file
43
arch/src/aarch64/regs.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2022 Arm Limited (or its affiliates). All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// AArch64 system register encoding:
|
||||
// See https://developer.arm.com/documentation/ddi0487 (chapter D12)
|
||||
//
|
||||
// 31 22 21 20 19 18 16 15 12 11 8 7 5 4 0
|
||||
// +----------+---+-----+-----+-----+-----+-----+----+
|
||||
// |1101010100| L | op0 | op1 | CRn | CRm | op2 | Rt |
|
||||
// +----------+---+-----+-----+-----+-----+-----+----+
|
||||
//
|
||||
// Notes:
|
||||
// - L and Rt are reserved as implementation defined fields, ignored.
|
||||
|
||||
const SYSREG_HEAD: u32 = 0b1101010100u32 << 22;
|
||||
const SYSREG_OP0_SHIFT: u32 = 19;
|
||||
const SYSREG_OP0_MASK: u32 = 0b11u32 << 19;
|
||||
const SYSREG_OP1_SHIFT: u32 = 16;
|
||||
const SYSREG_OP1_MASK: u32 = 0b111u32 << 16;
|
||||
const SYSREG_CRN_SHIFT: u32 = 12;
|
||||
const SYSREG_CRN_MASK: u32 = 0b1111u32 << 12;
|
||||
const SYSREG_CRM_SHIFT: u32 = 8;
|
||||
const SYSREG_CRM_MASK: u32 = 0b1111u32 << 8;
|
||||
const SYSREG_OP2_SHIFT: u32 = 5;
|
||||
const SYSREG_OP2_MASK: u32 = 0b111u32 << 5;
|
||||
|
||||
/// Define the ID of system registers
|
||||
#[macro_export]
|
||||
macro_rules! arm64_sys_reg {
|
||||
($name: tt, $op0: tt, $op1: tt, $crn: tt, $crm: tt, $op2: tt) => {
|
||||
pub const $name: u32 = SYSREG_HEAD
|
||||
| ((($op0 as u32) << SYSREG_OP0_SHIFT) & SYSREG_OP0_MASK as u32)
|
||||
| ((($op1 as u32) << SYSREG_OP1_SHIFT) & SYSREG_OP1_MASK as u32)
|
||||
| ((($crn as u32) << SYSREG_CRN_SHIFT) & SYSREG_CRN_MASK as u32)
|
||||
| ((($crm as u32) << SYSREG_CRM_SHIFT) & SYSREG_CRM_MASK as u32)
|
||||
| ((($op2 as u32) << SYSREG_OP2_SHIFT) & SYSREG_OP2_MASK as u32);
|
||||
};
|
||||
}
|
||||
|
||||
arm64_sys_reg!(MPIDR_EL1, 3, 0, 0, 0, 5);
|
||||
arm64_sys_reg!(ID_AA64MMFR0_EL1, 3, 0, 0, 7, 0);
|
||||
arm64_sys_reg!(TTBR1_EL1, 3, 0, 2, 0, 1);
|
||||
arm64_sys_reg!(TCR_EL1, 3, 0, 2, 0, 2);
|
||||
@@ -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))
|
||||
@@ -45,6 +36,6 @@ where
|
||||
}
|
||||
uefi_image.rewind().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)
|
||||
}
|
||||
|
||||
110
arch/src/lib.rs
110
arch/src/lib.rs
@@ -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,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Implements platform specific functionality.
|
||||
//! Supported platforms: x86_64, aarch64, riscv64.
|
||||
//! Supported platforms: x86_64, aarch64.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt, result};
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use serde::de::{IntoDeserializer, value};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use crate::x86_64::SgxEpcSection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::result;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use vm_memory::bitmap::AtomicBitmap;
|
||||
use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult};
|
||||
use versionize_derive::Versionize;
|
||||
use vm_migration::VersionMapped;
|
||||
|
||||
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)]
|
||||
pub enum Error {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[error("Platform specific error (x86_64)")]
|
||||
PlatformSpecific(#[from] x86_64::Error),
|
||||
#[error("Platform specific error (x86_64): {0:?}")]
|
||||
PlatformSpecific(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("Platform specific error (aarch64): {0:?}")]
|
||||
PlatformSpecific(aarch64::Error),
|
||||
#[error("The memory map table extends past the end of guest memory")]
|
||||
MemmapTablePastRamEnd,
|
||||
#[error("Error writing memory map table to guest memory")]
|
||||
MemmapTableSetup(#[source] vm_memory::GuestMemoryError),
|
||||
#[error("Error generating memory map table")]
|
||||
MemmapTableGeneration,
|
||||
MemmapTableSetup,
|
||||
#[error("The hvm_start_info structure extends past the end of guest memory")]
|
||||
StartInfoPastRamEnd,
|
||||
#[error("Error writing hvm_start_info to guest memory")]
|
||||
StartInfoSetup(#[source] vm_memory::GuestMemoryError),
|
||||
StartInfoSetup,
|
||||
#[error("Failed to compute initramfs address")]
|
||||
InitramfsAddress,
|
||||
#[error("Error writing module entry to guest memory")]
|
||||
#[error("Error writing module entry to guest memory: {0}")]
|
||||
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")]
|
||||
ZeroPagePastRamEnd,
|
||||
}
|
||||
|
||||
/// 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, Eq, Debug, Serialize, Deserialize, Versionize)]
|
||||
pub enum RegionType {
|
||||
/// RAM type
|
||||
Ram,
|
||||
@@ -97,26 +72,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, initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE,
|
||||
layout::IRQ_BASE, uefi, EntryPoint, _NSIG,
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
@@ -124,10 +90,9 @@ pub mod x86_64;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub use x86_64::{
|
||||
_NSIG, CpuidConfig, CpuidFeatureEntry, EntryPoint, arch_memory_regions, configure_system,
|
||||
configure_vcpu, 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, _NSIG,
|
||||
};
|
||||
|
||||
/// Safe wrapper for `sysconf(_SC_PAGESIZE)`.
|
||||
@@ -142,11 +107,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 +130,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 +138,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.
|
||||
@@ -189,7 +151,7 @@ impl fmt::Display for DeviceType {
|
||||
|
||||
/// 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,
|
||||
@@ -198,7 +160,7 @@ pub struct MmioDeviceInfo {
|
||||
|
||||
/// 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 +168,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
|
||||
|
||||
@@ -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, GuestMemoryBackend, 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 "GuestMemoryBackend" 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("GuestMemoryBackend 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", ®_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", ®)?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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, GuestMemoryAtomic, GuestMemoryBackend};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
// Copyright © 2026 Cyberus Technology GmbH
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use hypervisor::arch::x86::{MsrEntry, VcpuMsrConfigUpdate};
|
||||
use log::{debug, error};
|
||||
|
||||
use crate::x86_64::Error;
|
||||
|
||||
/// The register address of the IA32_ARCH_CAPABILITIES MSR
|
||||
const IA32_ARCH_CAPABILITIES: u32 = 0x10a;
|
||||
|
||||
/// Check that the MSR updates required by the CPU profile are compatible with the
|
||||
/// host's feature MSRs.
|
||||
pub(crate) fn valid_required_arch_capabilities_update(
|
||||
required_updates: &VcpuMsrConfigUpdate,
|
||||
host_feature_msrs: &[MsrEntry],
|
||||
) -> Result<(), Error> {
|
||||
let find_arch_capabilities = |msrs: &[MsrEntry]| {
|
||||
msrs.iter()
|
||||
.find(|msr| msr.index == IA32_ARCH_CAPABILITIES)
|
||||
.map(|entry| entry.data)
|
||||
};
|
||||
|
||||
let Some(required_arch_capabilities_msr) =
|
||||
find_arch_capabilities(&required_updates.feature_msrs)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(host_arch_capabilities) = find_arch_capabilities(host_feature_msrs) else {
|
||||
error!("Unable to find MSR IA32_ARCH_CAPABILITIES, but it is required by the CPU profile");
|
||||
return Err(Error::CpuProfileMissingMsr);
|
||||
};
|
||||
|
||||
if arch_capabilities_compatible(
|
||||
required_arch_capabilities_msr,
|
||||
host_arch_capabilities,
|
||||
"CPU Profile",
|
||||
"Host",
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::CpuProfileMsrIncompatibility)
|
||||
}
|
||||
}
|
||||
|
||||
/// If `src_val` and `dest_val` are two different possible values of IA32_ARCH_CAPABILITIES, then
|
||||
/// this returns `true` when `src_val` is considered compatible with `dest_val`.
|
||||
///
|
||||
/// If this check fails then programs that work when the value is `src_val`, may possibly
|
||||
/// no longer work if the value is `dest_val`.
|
||||
///
|
||||
/// The `src_id` and `dest_id` parameters are used to identify where `src_val` and `dest_val`
|
||||
/// originate from (e.g. CPU profile, Host) when logging the detected incompatibility.
|
||||
fn arch_capabilities_compatible(src_val: u64, dest_val: u64, src_id: &str, dest_id: &str) -> bool {
|
||||
const RSBA_MASK: u64 = 1 << 2;
|
||||
const RRSBA_MASK: u64 = 1 << 19;
|
||||
// We consider it unsafe to migrate from a machine without RSBA or RRSBA to one that advertises this behavior.
|
||||
// We consider the converse safe: Return stack buffer underflow mitigations can still be applied even if they
|
||||
// may no longer be necessary after migrating. This of course assumes that the destination is capable of applying
|
||||
// said mitigations, but that should be ensured by other CPUID and/or MSR value checks.
|
||||
const SUPERSET_MASK: u64 = RSBA_MASK | RRSBA_MASK;
|
||||
|
||||
// Bits 31 and 33..=61 are (currently) reserved
|
||||
const RESERVED_MASK: u64 = {
|
||||
let bits_0_to_61 = (1_u64 << 62) - 1;
|
||||
let bits_0_to_32 = (1_u64 << 33) - 1;
|
||||
(bits_0_to_61 ^ bits_0_to_32) | (1 << 31)
|
||||
};
|
||||
|
||||
const SUBSET_MASK: u64 = !(SUPERSET_MASK | RESERVED_MASK);
|
||||
|
||||
const MDS_NO_MASK: u64 = 1 << 5;
|
||||
const TAA_NO_MASK: u64 = 1 << 8;
|
||||
const SBDR_SSDP_NO_MASK: u64 = 1 << 13;
|
||||
const FBSDP_NO_MASK: u64 = 1 << 14;
|
||||
const PSDP_NO_MASK: u64 = 1 << 15;
|
||||
const FB_CLEAR_MASK: u64 = 1 << 17;
|
||||
const TOLERATE_MISSING_FB_CLEAR_MASK: u64 =
|
||||
MDS_NO_MASK | TAA_NO_MASK | SBDR_SSDP_NO_MASK | FBSDP_NO_MASK | PSDP_NO_MASK;
|
||||
|
||||
// For safety reasons we will require equality on the reserved bits for now: If/when they become unreserved then we can adjust the checks
|
||||
// accordingly.
|
||||
let reserved_eq_check = {
|
||||
let src_reserved = src_val & RESERVED_MASK;
|
||||
let dest_reserved = dest_val & RESERVED_MASK;
|
||||
if src_reserved == dest_reserved {
|
||||
true
|
||||
} else {
|
||||
let only_in_src = src_reserved & (src_reserved ^ dest_reserved);
|
||||
let only_in_dest = dest_reserved & (dest_reserved ^ src_reserved);
|
||||
debug_log_features_only_in(only_in_src, src_id);
|
||||
debug_log_features_only_in(only_in_dest, dest_id);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mut subset_check = true;
|
||||
if let Err(only_in_src) = check_subset(src_val & SUBSET_MASK, dest_val & SUBSET_MASK) {
|
||||
// If the only bit that is only in source is 17 (FB_CLEAR) and dest_val has
|
||||
// certain mitigation bits set, then src_val is actually compatible with
|
||||
// dest_val. QEMU does in fact always artificially set bit 17 in that case: See
|
||||
// https://github.com/qemu/qemu/blob/v11.0.1/target/i386/kvm/kvm.c#L679-L685
|
||||
//
|
||||
// TODO: Perhaps we should also rather make Hypervisor::get_msr_based_features() adjust bit
|
||||
// 17? With CPU profiles this doesn't seem necessary though.
|
||||
if !(((dest_val & TOLERATE_MISSING_FB_CLEAR_MASK) == TOLERATE_MISSING_FB_CLEAR_MASK)
|
||||
&& (only_in_src == FB_CLEAR_MASK))
|
||||
{
|
||||
subset_check = false;
|
||||
debug_log_features_only_in(only_in_src, src_id);
|
||||
}
|
||||
}
|
||||
|
||||
let superset_check = {
|
||||
if let Err(only_in_dest) = check_subset(dest_val & SUPERSET_MASK, src_val & SUPERSET_MASK) {
|
||||
debug_log_features_only_in(only_in_dest, dest_id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
let is_err = !(reserved_eq_check && subset_check && superset_check);
|
||||
|
||||
if is_err {
|
||||
error!(
|
||||
"IA32_ARCH_CAPABILITIES compatibility check failed: {src_id} value={src_val:#x}, {dest_id} value={dest_val:#x}"
|
||||
);
|
||||
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that no bits are only in `a`.
|
||||
///
|
||||
/// Upon error a bitset is returned with the bits that are only available in
|
||||
/// `a`.
|
||||
fn check_subset(a: u64, b: u64) -> Result<(), u64> {
|
||||
let only_in_a = a & (a ^ b);
|
||||
if only_in_a != 0 {
|
||||
Err(only_in_a)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_log_features_only_in(mut only_in: u64, id: &str) {
|
||||
while only_in != 0 {
|
||||
// Obtain the lowest set bit
|
||||
let bit_pos = only_in.trailing_zeros();
|
||||
debug!(
|
||||
"IA32_ARCH_CAPABILITIES compatibility check failed: bit={bit_pos} is only set for {id}"
|
||||
);
|
||||
// Unset the lowest set bit
|
||||
only_in &= only_in - 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use super::arch_capabilities_compatible;
|
||||
|
||||
#[test]
|
||||
fn check_arch_compatibilities_cascade_lake_sapphire_rapids() {
|
||||
// Value of IA32_ARCH_CAPABILITIES on Intel Cascade Lake obtained from KVM (kernel version 6.12.60)
|
||||
let cascade_lake_msr_value: u64 = 0xc0aa0eb;
|
||||
|
||||
// Value of IA32_ARCH_CAPABILITIES on Sapphire Rapids obtained from KVM (kernel version 6.18.33)
|
||||
let sapphire_rapids_msr_value: u64 = 0x400000000c08e1eb;
|
||||
|
||||
// Live migration from Intel Cascade Lake to Sapphire Rapids should work as far as IA32_ARCH_CAPABILITIES
|
||||
// is concerned.
|
||||
// NOTE: The Cascade Lake has the FB_CLEAR bit set (bit 17), but this is not the case for Sapphire Rapids.
|
||||
// This means that the code path for the fallback compatibility check must necessarily get exercised.
|
||||
assert!(arch_capabilities_compatible(
|
||||
cascade_lake_msr_value,
|
||||
sapphire_rapids_msr_value,
|
||||
"Cascade Lake",
|
||||
"Sapphire Rapids",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_arch_capabilities_sapphire_rapids_granite_rapids() {
|
||||
// Value of IA32_ARCH_CAPABILITIES on Sapphire Rapids (obtained from KVM with kernel version 6.18.33)
|
||||
let sapphire_rapids_msr_value: u64 = 0x400000000c08e1eb;
|
||||
// Value of IA32_ARCH_CAPABILITIES on Granite Rapids (obtained from KVM with kernel version 6.12.91)
|
||||
// TODO: Consider extracting the values from KVM with the same Linux Kernel versions, but we do not
|
||||
// expect this to change the values of this MSR though.
|
||||
let granite_rapids_msr_value: u64 = 0x400000000d08e1eb;
|
||||
|
||||
// Migration from sapphire rapids to granite rapids without a CPU profile should work
|
||||
assert!(arch_capabilities_compatible(
|
||||
sapphire_rapids_msr_value,
|
||||
granite_rapids_msr_value,
|
||||
"Sapphire Rapids",
|
||||
"Granite Rapids",
|
||||
));
|
||||
|
||||
// On the other hand it should NOT be possible to migrate from the
|
||||
// Granite Rapids machine (without applying a CPU profile) to the
|
||||
// Sapphire Rapids, because PRBS_NO (IA32_ARCH_CAPABILITIES[24]) is set
|
||||
// on the former, but not the latter.
|
||||
assert!(!arch_capabilities_compatible(
|
||||
granite_rapids_msr_value,
|
||||
sapphire_rapids_msr_value,
|
||||
"Granite Rapids",
|
||||
"Sapphire Rapids",
|
||||
));
|
||||
|
||||
// The value extracted from the Sapphire Rapids machine, but with the
|
||||
// TSX CTRL bit unset. All CPU profiles apart from host will adapt
|
||||
// CPUID to indicate that TSX is not available because that feature is
|
||||
// riddled with CVEs and we expect operators to disable it globally (at
|
||||
// the kernel level).
|
||||
let restricted_sapphire_rapids_msr_value: u64 = 0x400000000c08e16b;
|
||||
|
||||
// It must be possible to apply the Sapphire Rapids CPU profile on
|
||||
// the host that the profile is based on
|
||||
assert!(arch_capabilities_compatible(
|
||||
restricted_sapphire_rapids_msr_value,
|
||||
sapphire_rapids_msr_value,
|
||||
"Sapphire Rapids profile",
|
||||
"Sapphire Rapids host",
|
||||
));
|
||||
|
||||
// It should also be possible to apply the Sapphire Rapids profile on
|
||||
// the Granite Rapids machine
|
||||
assert!(arch_capabilities_compatible(
|
||||
restricted_sapphire_rapids_msr_value,
|
||||
granite_rapids_msr_value,
|
||||
"Sapphire Rapids profile",
|
||||
"Granite Rapids host",
|
||||
));
|
||||
}
|
||||
|
||||
// Check that if reserved bits are different then we get an error.
|
||||
//
|
||||
// This test is somewhat contrived and simplistic. Reserved bits in
|
||||
// IA32_ARCH_CAPABILITIES will be 0 in practice. We do however want to be
|
||||
// safe if/when bits are no longer reserved on future hardware generations,
|
||||
// hence we add a simple test as a reality check that differing reserved
|
||||
// bits is not allowed.
|
||||
#[test]
|
||||
fn check_arch_capabilities_compatibility_reserved_bits() {
|
||||
const RESERVED_ONE: u64 = 1 << 31;
|
||||
const RESERVED_TWO: u64 = 1 << 42;
|
||||
const RESERVED_THREE: u64 = 1 << 61;
|
||||
let sapphire_rapids_msr_value: u64 = 0x400000000c08e1eb;
|
||||
let with_reserved_bits = sapphire_rapids_msr_value | RESERVED_ONE | RESERVED_THREE;
|
||||
let with_other_reserved_bits = sapphire_rapids_msr_value | RESERVED_TWO;
|
||||
|
||||
assert!(!arch_capabilities_compatible(
|
||||
with_reserved_bits,
|
||||
with_other_reserved_bits,
|
||||
"Reserved 1",
|
||||
"Reserved 2",
|
||||
));
|
||||
|
||||
assert!(!arch_capabilities_compatible(
|
||||
with_other_reserved_bits,
|
||||
with_reserved_bits,
|
||||
"Reserved 2",
|
||||
"Reserved 1",
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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)>,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,107 +0,0 @@
|
||||
// Copyright © 2026 Cyberus Technology GmbH
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use hypervisor::arch::x86::MsrEntry;
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::x86_64::Error;
|
||||
use crate::x86_64::helpers::{
|
||||
deserialize_u32_hex, deserialize_u64_hex, serialize_u32_hex, serialize_u64_hex,
|
||||
};
|
||||
|
||||
/// The register address of an MSR
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RegisterAddress(
|
||||
#[serde(
|
||||
serialize_with = "serialize_u32_hex",
|
||||
deserialize_with = "deserialize_u32_hex"
|
||||
)]
|
||||
pub u32,
|
||||
);
|
||||
|
||||
/// Used to adjust the value of a Feature MSR.
|
||||
///
|
||||
/// Instances of this struct typically adjust MSR values according to the
|
||||
/// following formula: `msr_value = (self.mask & msr_value) | self.replacements`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FeatureMsrAdjustment {
|
||||
/// Packs values to be placed into the given feature MSR value.
|
||||
#[serde(
|
||||
serialize_with = "serialize_u64_hex",
|
||||
deserialize_with = "deserialize_u64_hex"
|
||||
)]
|
||||
pub replacements: u64,
|
||||
|
||||
/// 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 feature MSR value that are **not** supposed to be replaced/overwritten should be set in
|
||||
/// this mask.
|
||||
#[serde(
|
||||
serialize_with = "serialize_u64_hex",
|
||||
deserialize_with = "deserialize_u64_hex"
|
||||
)]
|
||||
pub mask: u64,
|
||||
}
|
||||
|
||||
impl FeatureMsrAdjustment {
|
||||
/// Adjusts the given `feature_msrs` according to `adjustments`.
|
||||
///
|
||||
/// An error is returned if there exists an MSR register address in
|
||||
/// `adjustments` without a matching entry in `feature_msrs`.
|
||||
pub(super) fn adjust_feature_msrs(
|
||||
feature_msrs: &[MsrEntry],
|
||||
adjustments: &[(RegisterAddress, FeatureMsrAdjustment)],
|
||||
) -> Result<Vec<MsrEntry>, Error> {
|
||||
let mut missing_msr = false;
|
||||
let mut output_feature_msrs = Vec::with_capacity(adjustments.len());
|
||||
for (reg_address, adjustment) in adjustments {
|
||||
let Some(entry) = feature_msrs
|
||||
.iter()
|
||||
.find(|entry| entry.index == reg_address.0)
|
||||
else {
|
||||
missing_msr = true;
|
||||
error!(
|
||||
"Did not find feature based MSR entry for MSR {:#x}",
|
||||
reg_address.0
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut entry = *entry;
|
||||
let data = entry.data;
|
||||
entry.data = (adjustment.mask & data) | adjustment.replacements;
|
||||
|
||||
debug!(
|
||||
"Prepared adjusted MSR feature: register address={:#x} value={:#x}, previous value={data:#x}",
|
||||
entry.index, entry.data
|
||||
);
|
||||
output_feature_msrs.push(entry);
|
||||
}
|
||||
|
||||
if missing_msr {
|
||||
Err(Error::CpuProfileMissingMsr)
|
||||
} else {
|
||||
Ok(output_feature_msrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data describing MSR adjustments related to a CPU profile.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct MsrProfileData {
|
||||
/// Describes feature MSR adjustments necessary to become compatible with
|
||||
/// the desired target.
|
||||
pub adjustments: Vec<(RegisterAddress, FeatureMsrAdjustment)>,
|
||||
/// List of the MSRs that the CPU profile requires.
|
||||
///
|
||||
/// When applying a CPU profile then the union of the sets of MSRs obtained
|
||||
/// from `Hypervisor::get_feature_msrs` and `Hypervisor::get_msr_index_list`
|
||||
/// must necessarily contain all MSRs listed here. Otherwise the host is
|
||||
/// considered incompatible with the CPU profile. Exceptions are made for
|
||||
/// missing Hyper-V MSRs when `kvm_hyperv=off`.
|
||||
pub required_msrs: Vec<RegisterAddress>,
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright © 2026 Cyberus Technology GmbH
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
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<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<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"))
|
||||
})
|
||||
}
|
||||
|
||||
/// 64-bit version of `serialize_u32_hex`
|
||||
pub(crate) fn serialize_u64_hex<S: Serializer>(
|
||||
input: &u64,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&format!("{input:#x}"))
|
||||
}
|
||||
|
||||
/// 64-bit version of `deserialize_u32_hex`
|
||||
pub(crate) fn deserialize_u64_hex<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<u64, D::Error> {
|
||||
let hex: &str = <&str>::deserialize(deserializer)?;
|
||||
u64::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 64 bit integer"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use std::fmt::Debug;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||
struct TestStruct64 {
|
||||
#[serde(
|
||||
serialize_with = "serialize_u64_hex",
|
||||
deserialize_with = "deserialize_u64_hex"
|
||||
)]
|
||||
foo: u64,
|
||||
#[serde(
|
||||
serialize_with = "serialize_u64_hex",
|
||||
deserialize_with = "deserialize_u64_hex"
|
||||
)]
|
||||
bar: u64,
|
||||
}
|
||||
|
||||
// Check that our hex serializers satisfy the two following invariants
|
||||
// 1. Serialization followed by deserialization is the identity.
|
||||
// 2. Values of type u32/u64 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).
|
||||
fn test_hex_serialization<T>(t: T) -> Result<(), TestCaseError>
|
||||
where
|
||||
T: Serialize + Debug + Eq + Copy,
|
||||
for<'de> T: Deserialize<'de>,
|
||||
{
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn hex_serialization_works_32(foo in any::<u32>(), bar in any::<u32>()) {
|
||||
let t = TestStruct { foo , bar };
|
||||
test_hex_serialization(t)?;
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn hex_serialization_works_64(foo in any::<u64>(), bar in any::<u64>()) {
|
||||
let t = TestStruct64 { foo , bar };
|
||||
test_hex_serialization(t)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright © 2026 Cyberus Technology GmbH
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
const HV_X64_MSR_GUEST_OS_ID: u32 = 0x40000000;
|
||||
const HV_X64_MSR_HYPERCALL: u32 = 0x40000001;
|
||||
const HV_X64_MSR_VP_INDEX: u32 = 0x40000002;
|
||||
const HV_X64_MSR_RESET: u32 = 0x40000003;
|
||||
const HV_X64_MSR_VP_RUNTIME: u32 = 0x40000010;
|
||||
const HV_X64_MSR_TIME_REF_COUNT: u32 = 0x40000020;
|
||||
const HV_X64_MSR_REFERENCE_TSC: u32 = 0x40000021;
|
||||
const HV_X64_MSR_TSC_FREQUENCY: u32 = 0x40000022;
|
||||
const HV_X64_MSR_APIC_FREQUENCY: u32 = 0x40000023;
|
||||
const HV_X64_MSR_EOI: u32 = 0x40000070;
|
||||
const HV_X64_MSR_ICR: u32 = 0x40000071;
|
||||
const HV_X64_MSR_TPR: u32 = 0x40000072;
|
||||
const HV_X64_MSR_VP_ASSIST_PAGE: u32 = 0x40000073;
|
||||
const HV_X64_MSR_SCONTROL: u32 = 0x40000080;
|
||||
const HV_X64_MSR_SVERSION: u32 = 0x40000081;
|
||||
const HV_X64_MSR_SIEFP: u32 = 0x40000082;
|
||||
const HV_X64_MSR_SIMP: u32 = 0x40000083;
|
||||
const HV_X64_MSR_EOM: u32 = 0x40000084;
|
||||
const HV_X64_MSR_SINT0: u32 = 0x40000090;
|
||||
const HV_X64_MSR_SINT1: u32 = 0x40000091;
|
||||
const HV_X64_MSR_SINT2: u32 = 0x40000092;
|
||||
const HV_X64_MSR_SINT3: u32 = 0x40000093;
|
||||
const HV_X64_MSR_SINT4: u32 = 0x40000094;
|
||||
const HV_X64_MSR_SINT5: u32 = 0x40000095;
|
||||
const HV_X64_MSR_SINT6: u32 = 0x40000096;
|
||||
const HV_X64_MSR_SINT7: u32 = 0x40000097;
|
||||
const HV_X64_MSR_SINT8: u32 = 0x40000098;
|
||||
const HV_X64_MSR_SINT9: u32 = 0x40000099;
|
||||
const HV_X64_MSR_SINT10: u32 = 0x4000009A;
|
||||
const HV_X64_MSR_SINT11: u32 = 0x4000009B;
|
||||
const HV_X64_MSR_SINT12: u32 = 0x4000009C;
|
||||
const HV_X64_MSR_SINT13: u32 = 0x4000009D;
|
||||
const HV_X64_MSR_SINT14: u32 = 0x4000009E;
|
||||
const HV_X64_MSR_SINT15: u32 = 0x4000009F;
|
||||
const HV_X64_MSR_STIMER0_CONFIG: u32 = 0x400000B0;
|
||||
const HV_X64_MSR_STIMER0_COUNT: u32 = 0x400000B1;
|
||||
const HV_X64_MSR_STIMER1_CONFIG: u32 = 0x400000B2;
|
||||
const HV_X64_MSR_STIMER1_COUNT: u32 = 0x400000B3;
|
||||
const HV_X64_MSR_STIMER2_CONFIG: u32 = 0x400000B4;
|
||||
const HV_X64_MSR_STIMER2_COUNT: u32 = 0x400000B5;
|
||||
const HV_X64_MSR_STIMER3_CONFIG: u32 = 0x400000B6;
|
||||
const HV_X64_MSR_STIMER3_COUNT: u32 = 0x400000B7;
|
||||
const HV_X64_MSR_GUEST_IDLE: u32 = 0x400000F0;
|
||||
const HV_X64_MSR_CRASH_P0: u32 = 0x40000100;
|
||||
const HV_X64_MSR_CRASH_P1: u32 = 0x40000101;
|
||||
const HV_X64_MSR_CRASH_P2: u32 = 0x40000102;
|
||||
const HV_X64_MSR_CRASH_P3: u32 = 0x40000103;
|
||||
const HV_X64_MSR_CRASH_P4: u32 = 0x40000104;
|
||||
const HV_X64_MSR_CRASH_CTL: u32 = 0x40000105;
|
||||
const HV_X64_MSR_REENLIGHTENMENT_CONTROL: u32 = 0x40000106;
|
||||
const HV_X64_MSR_TSC_EMULATION_CONTROL: u32 = 0x40000107;
|
||||
const HV_X64_MSR_TSC_EMULATION_STATUS: u32 = 0x40000108;
|
||||
const HV_X64_MSR_TSC_INVARIANT_CONTROL: u32 = 0x40000118;
|
||||
const HV_X64_MSR_SYNDBG_CONTROL: u32 = 0x400000F1;
|
||||
const HV_X64_MSR_SYNDBG_STATUS: u32 = 0x400000F2;
|
||||
const HV_X64_MSR_SYNDBG_SEND_BUFFER: u32 = 0x400000F3;
|
||||
const HV_X64_MSR_SYNDBG_RECV_BUFFER: u32 = 0x400000F4;
|
||||
const HV_X64_MSR_SYNDBG_PENDING_BUFFER: u32 = 0x400000F5;
|
||||
const HV_X64_MSR_SYNDBG_OPTIONS: u32 = 0x400000FF;
|
||||
|
||||
// All Hyper-V MSRs extracted from https://elixir.bootlin.com/linux/v7.1.1/source/tools/testing/selftests/kvm/include/x86/hyperv.h#L23
|
||||
pub const HYPERV_MSRS: [u32; 59] = [
|
||||
HV_X64_MSR_GUEST_OS_ID,
|
||||
HV_X64_MSR_HYPERCALL,
|
||||
HV_X64_MSR_VP_INDEX,
|
||||
HV_X64_MSR_RESET,
|
||||
HV_X64_MSR_VP_RUNTIME,
|
||||
HV_X64_MSR_TIME_REF_COUNT,
|
||||
HV_X64_MSR_REFERENCE_TSC,
|
||||
HV_X64_MSR_TSC_FREQUENCY,
|
||||
HV_X64_MSR_APIC_FREQUENCY,
|
||||
HV_X64_MSR_EOI,
|
||||
HV_X64_MSR_ICR,
|
||||
HV_X64_MSR_TPR,
|
||||
HV_X64_MSR_VP_ASSIST_PAGE,
|
||||
HV_X64_MSR_SCONTROL,
|
||||
HV_X64_MSR_SVERSION,
|
||||
HV_X64_MSR_SIEFP,
|
||||
HV_X64_MSR_SIMP,
|
||||
HV_X64_MSR_EOM,
|
||||
HV_X64_MSR_SINT0,
|
||||
HV_X64_MSR_SINT1,
|
||||
HV_X64_MSR_SINT2,
|
||||
HV_X64_MSR_SINT3,
|
||||
HV_X64_MSR_SINT4,
|
||||
HV_X64_MSR_SINT5,
|
||||
HV_X64_MSR_SINT6,
|
||||
HV_X64_MSR_SINT7,
|
||||
HV_X64_MSR_SINT8,
|
||||
HV_X64_MSR_SINT9,
|
||||
HV_X64_MSR_SINT10,
|
||||
HV_X64_MSR_SINT11,
|
||||
HV_X64_MSR_SINT12,
|
||||
HV_X64_MSR_SINT13,
|
||||
HV_X64_MSR_SINT14,
|
||||
HV_X64_MSR_SINT15,
|
||||
HV_X64_MSR_STIMER0_CONFIG,
|
||||
HV_X64_MSR_STIMER0_COUNT,
|
||||
HV_X64_MSR_STIMER1_CONFIG,
|
||||
HV_X64_MSR_STIMER1_COUNT,
|
||||
HV_X64_MSR_STIMER2_CONFIG,
|
||||
HV_X64_MSR_STIMER2_COUNT,
|
||||
HV_X64_MSR_STIMER3_CONFIG,
|
||||
HV_X64_MSR_STIMER3_COUNT,
|
||||
HV_X64_MSR_GUEST_IDLE,
|
||||
HV_X64_MSR_CRASH_P0,
|
||||
HV_X64_MSR_CRASH_P1,
|
||||
HV_X64_MSR_CRASH_P2,
|
||||
HV_X64_MSR_CRASH_P3,
|
||||
HV_X64_MSR_CRASH_P4,
|
||||
HV_X64_MSR_CRASH_CTL,
|
||||
HV_X64_MSR_REENLIGHTENMENT_CONTROL,
|
||||
HV_X64_MSR_TSC_EMULATION_CONTROL,
|
||||
HV_X64_MSR_TSC_EMULATION_STATUS,
|
||||
HV_X64_MSR_TSC_INVARIANT_CONTROL,
|
||||
HV_X64_MSR_SYNDBG_CONTROL,
|
||||
HV_X64_MSR_SYNDBG_STATUS,
|
||||
HV_X64_MSR_SYNDBG_SEND_BUFFER,
|
||||
HV_X64_MSR_SYNDBG_RECV_BUFFER,
|
||||
HV_X64_MSR_SYNDBG_PENDING_BUFFER,
|
||||
HV_X64_MSR_SYNDBG_OPTIONS,
|
||||
];
|
||||
@@ -6,6 +6,7 @@
|
||||
// found in the LICENSE-BSD-3-Clause file.
|
||||
|
||||
use std::result;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type Result<T> = result::Result<T, hypervisor::HypervisorCpuError>;
|
||||
|
||||
@@ -23,7 +24,7 @@ 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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {}
|
||||
|
||||
@@ -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, GuestMemoryBackend, 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:
|
||||
@@ -52,57 +50,61 @@ 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 {
|
||||
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
|
||||
let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::<T>()) };
|
||||
let mut checksum: u8 = 0;
|
||||
for i in v.as_slice().iter() {
|
||||
for i in v_slice.iter() {
|
||||
checksum = checksum.wrapping_add(*i);
|
||||
}
|
||||
checksum
|
||||
@@ -113,30 +115,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 +142,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 +154,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 +173,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 +197,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 +222,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 +237,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 +252,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 +272,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,20 +288,18 @@ 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>(),
|
||||
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 +310,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 +319,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 +328,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 +344,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 +373,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 +388,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 +407,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,62 +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};
|
||||
#[cfg(all(feature = "kvm", not(feature = "sev_snp")))]
|
||||
use log::error;
|
||||
use thiserror::Error;
|
||||
use vm_memory::{Address, Bytes, GuestMemoryBackend, GuestMemoryError};
|
||||
use hypervisor::arch::x86::{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),
|
||||
/// Setting up MSRs failed because not all setup entries were set.
|
||||
#[error("Some MSRs could not be set")]
|
||||
SetModelSpecificRegistersAll,
|
||||
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>;
|
||||
@@ -71,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,
|
||||
@@ -86,31 +65,10 @@ pub fn setup_fpu(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `vcpu` - Structure for the VCPU that holds the VCPU's fd.
|
||||
#[cfg_attr(
|
||||
any(not(feature = "kvm"), feature = "sev_snp"),
|
||||
allow(unused_variables)
|
||||
)]
|
||||
pub fn setup_msrs(vcpu: &dyn hypervisor::Vcpu) -> Result<()> {
|
||||
let setup_entries = vcpu.boot_msr_entries();
|
||||
let num_msrs_set = vcpu
|
||||
.set_msrs(&setup_entries)
|
||||
pub fn setup_msrs(vcpu: &Arc<dyn hypervisor::Vcpu>) -> Result<()> {
|
||||
vcpu.set_msrs(&vcpu.boot_msr_entries())
|
||||
.map_err(Error::SetModelSpecificRegisters)?;
|
||||
|
||||
// Check that all setup entries were set. We can only do this for KVM
|
||||
// (when SEV-SNP is not enabled) as MSHV always returns Ok(0) on success.
|
||||
#[cfg(all(feature = "kvm", not(feature = "sev_snp")))]
|
||||
if matches!(vcpu.hypervisor_type(), hypervisor::HypervisorType::Kvm)
|
||||
&& num_msrs_set != setup_entries.len()
|
||||
{
|
||||
for msr in &setup_entries[num_msrs_set..] {
|
||||
error!(
|
||||
"Could not set MSR with register address={:#x} and value={:#x}",
|
||||
msr.index, msr.data
|
||||
);
|
||||
}
|
||||
|
||||
return Err(Error::SetModelSpecificRegistersAll);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -119,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(®s).map_err(Error::SetBaseRegisters)
|
||||
}
|
||||
|
||||
@@ -144,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)
|
||||
}
|
||||
|
||||
@@ -160,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)?;
|
||||
}
|
||||
@@ -177,7 +123,6 @@ 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] = {
|
||||
// Configure GDT entries as specified by PVH boot protocol
|
||||
@@ -196,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;
|
||||
@@ -213,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()
|
||||
@@ -239,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,
|
||||
|
||||
@@ -6,96 +6,78 @@
|
||||
//
|
||||
// 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 uuid::Uuid;
|
||||
use vm_memory::ByteValued;
|
||||
use vm_memory::{Address, Bytes, GuestAddress};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[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),
|
||||
WriteData,
|
||||
/// Failure to parse uuid, uuid format may be error
|
||||
#[error("Failure to parse uuid: {1}")]
|
||||
ParseUuid(#[source] uuid::Error, String),
|
||||
/// SMBIOS string index overflow (u8 limit reached).
|
||||
#[error("SMBIOS string index overflow (u8 limit reached: {})", u8::MAX)]
|
||||
TooManyStrings,
|
||||
ParseUuid(uuid::Error),
|
||||
}
|
||||
|
||||
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".to_string()
|
||||
}
|
||||
AddressOverflow => {
|
||||
"The SMBIOS table has too little address space to be stored".to_string()
|
||||
}
|
||||
Clear => "Failure while zeroing out the memory for the SMBIOS table".to_string(),
|
||||
WriteSmbiosEp => "Failure to write SMBIOS entrypoint structure".to_string(),
|
||||
WriteData => "Failure to write additional data to memory".to_string(),
|
||||
ParseUuid(e) => format!("Failure to parse uuid: {e}"),
|
||||
};
|
||||
|
||||
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 {
|
||||
// SAFETY: we are only reading the bytes within the size of the `T` reference `v`.
|
||||
let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::<T>()) };
|
||||
let mut checksum: u8 = 0;
|
||||
for i in v.as_slice().iter() {
|
||||
for i in v_slice.iter() {
|
||||
checksum = checksum.wrapping_add(*i);
|
||||
}
|
||||
(!checksum).wrapping_add(1)
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[repr(packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct Smbios30Entrypoint {
|
||||
signature: [u8; 5usize],
|
||||
@@ -110,7 +92,8 @@ struct Smbios30Entrypoint {
|
||||
physptr: u64,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[repr(packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct SmbiosBiosInfo {
|
||||
r#type: u8,
|
||||
@@ -126,7 +109,8 @@ struct SmbiosBiosInfo {
|
||||
characteristics_ext2: u8,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[repr(packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct SmbiosSysInfo {
|
||||
r#type: u8,
|
||||
@@ -142,7 +126,8 @@ struct SmbiosSysInfo {
|
||||
family: u8,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[repr(packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct SmbiosOemStrings {
|
||||
r#type: u8,
|
||||
@@ -151,34 +136,8 @@ struct SmbiosOemStrings {
|
||||
count: u8,
|
||||
}
|
||||
|
||||
/// SMBIOS Chassis Table (Type 3) as defined in DMTF SMBIOS 3.9.0:
|
||||
/// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf
|
||||
/// Note: trailing fields are omitted, so this structure is not complete.
|
||||
#[repr(C, packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct SmbiosChassis {
|
||||
r#type: u8,
|
||||
length: u8,
|
||||
handle: u16,
|
||||
manufacturer: u8,
|
||||
chassis_type: u8,
|
||||
version: u8,
|
||||
serial_number: u8,
|
||||
asset_tag: u8,
|
||||
bootup_state: u8,
|
||||
power_supply_state: u8,
|
||||
thermal_state: u8,
|
||||
security_status: u8,
|
||||
oem_defined: u32,
|
||||
height: u8,
|
||||
number_of_power_cords: u8,
|
||||
contained_element_count: u8,
|
||||
contained_element_record_length: u8,
|
||||
// followed by contained element records (optional, variable-length)
|
||||
// followed by sku_number: u8, rack_type: u8, rack_height: u8
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[repr(packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct SmbiosEndOfTable {
|
||||
r#type: u8,
|
||||
@@ -195,8 +154,6 @@ unsafe impl ByteValued for SmbiosSysInfo {}
|
||||
// SAFETY: data structure only contain a series of integers
|
||||
unsafe impl ByteValued for SmbiosOemStrings {}
|
||||
// SAFETY: data structure only contain a series of integers
|
||||
unsafe impl ByteValued for SmbiosChassis {}
|
||||
// SAFETY: data structure only contain a series of integers
|
||||
unsafe impl ByteValued for SmbiosEndOfTable {}
|
||||
|
||||
fn write_and_incr<T: ByteValued>(
|
||||
@@ -204,9 +161,9 @@ fn write_and_incr<T: ByteValued>(
|
||||
val: T,
|
||||
mut curptr: GuestAddress,
|
||||
) -> Result<GuestAddress> {
|
||||
mem.write_obj(val, curptr).map_err(Error::WriteData)?;
|
||||
mem.write_obj(val, curptr).map_err(|_| Error::WriteData)?;
|
||||
curptr = curptr
|
||||
.checked_add(size_of::<T>() as u64)
|
||||
.checked_add(mem::size_of::<T>() as u64)
|
||||
.ok_or(Error::NotEnoughMemory)?;
|
||||
Ok(curptr)
|
||||
}
|
||||
@@ -223,155 +180,14 @@ fn write_string(
|
||||
Ok(curptr)
|
||||
}
|
||||
|
||||
fn write_opt_string(
|
||||
pub fn setup_smbios(
|
||||
mem: &GuestMemoryMmap,
|
||||
s: Option<&str>,
|
||||
cur: GuestAddress,
|
||||
) -> Result<GuestAddress> {
|
||||
if let Some(v) = s {
|
||||
write_string(mem, v, cur)
|
||||
} else {
|
||||
Ok(cur)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string_terminator(
|
||||
mem: &GuestMemoryMmap,
|
||||
cur: GuestAddress,
|
||||
has_strings: bool,
|
||||
) -> Result<GuestAddress> {
|
||||
// SMBIOS DSP0134 §6.1.3: if all string-reference fields are 0, follow the
|
||||
// formatted section with two null bytes (empty string-set).
|
||||
if has_strings {
|
||||
write_and_incr(mem, 0u8, cur)
|
||||
} else {
|
||||
let cur = write_and_incr(mem, 0u8, cur)?;
|
||||
write_and_incr(mem, 0u8, cur)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate the next string index for an SMBIOS string-set.
|
||||
///
|
||||
/// Per SMBIOS DSP0134, index `0` means "no string", so valid indices run from
|
||||
/// `1` to `255`. Returns `0` when `present` is `false`. Otherwise returns the
|
||||
/// current value of `*next` and advances it by one. Fails with
|
||||
/// [`Error::TooManyStrings`] once all 255 indices have been used: `next`
|
||||
/// starts at `1`, so it can only be `0` here after wrapping past `255`.
|
||||
fn alloc_index(next: &mut u8, present: bool) -> Result<u8> {
|
||||
if !present {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let idx = *next;
|
||||
if idx == 0 {
|
||||
return Err(Error::TooManyStrings);
|
||||
}
|
||||
|
||||
*next = next.wrapping_add(1);
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
fn write_type1_system(
|
||||
mem: &GuestMemoryMmap,
|
||||
curptr: &mut GuestAddress,
|
||||
handle: &mut u16,
|
||||
system: Option<&SmbiosSystem>,
|
||||
) -> Result<()> {
|
||||
*handle += 1;
|
||||
|
||||
let manufacturer = system
|
||||
.and_then(|s| s.manufacturer.as_deref())
|
||||
.unwrap_or(DEFAULT_SYSTEM_MANUFACTURER);
|
||||
let product = system
|
||||
.and_then(|s| s.product_name.as_deref())
|
||||
.unwrap_or(DEFAULT_SYSTEM_PRODUCT_NAME);
|
||||
let version = system.and_then(|s| s.version.as_deref());
|
||||
let serial = system.and_then(|s| s.serial_number.as_deref());
|
||||
let uuid = system.and_then(|s| s.uuid.as_deref());
|
||||
let sku = system.and_then(|s| s.sku_number.as_deref());
|
||||
let family = system.and_then(|s| s.family.as_deref());
|
||||
|
||||
let uuid_number = uuid
|
||||
.map(Uuid::parse_str)
|
||||
.transpose()
|
||||
.map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))?
|
||||
.unwrap_or(Uuid::nil());
|
||||
|
||||
let mut next = 1u8;
|
||||
let manufacturer_idx = alloc_index(&mut next, true)?;
|
||||
let product_idx = alloc_index(&mut next, true)?;
|
||||
let version_idx = alloc_index(&mut next, version.is_some())?;
|
||||
let serial_idx = alloc_index(&mut next, serial.is_some())?;
|
||||
let sku_idx = alloc_index(&mut next, sku.is_some())?;
|
||||
let family_idx = alloc_index(&mut next, family.is_some())?;
|
||||
|
||||
let sys = SmbiosSysInfo {
|
||||
r#type: SYSTEM_INFORMATION,
|
||||
length: size_of::<SmbiosSysInfo>() as u8,
|
||||
handle: *handle,
|
||||
manufacturer: manufacturer_idx,
|
||||
product_name: product_idx,
|
||||
version: version_idx,
|
||||
serial_number: serial_idx,
|
||||
uuid: uuid_number.to_bytes_le(),
|
||||
wake_up_type: SYSTEM_WAKE_UP_TYPE_UNKNOWN,
|
||||
sku: sku_idx,
|
||||
family: family_idx,
|
||||
};
|
||||
|
||||
*curptr = write_and_incr(mem, sys, *curptr)?;
|
||||
*curptr = write_string(mem, manufacturer, *curptr)?;
|
||||
*curptr = write_string(mem, product, *curptr)?;
|
||||
*curptr = write_opt_string(mem, version, *curptr)?;
|
||||
*curptr = write_opt_string(mem, serial, *curptr)?;
|
||||
*curptr = write_opt_string(mem, sku, *curptr)?;
|
||||
*curptr = write_opt_string(mem, family, *curptr)?;
|
||||
*curptr = write_and_incr(mem, 0u8, *curptr)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_type3_chassis(
|
||||
mem: &GuestMemoryMmap,
|
||||
curptr: &mut GuestAddress,
|
||||
handle: &mut u16,
|
||||
chassis: &SmbiosChassisConfig,
|
||||
) -> Result<()> {
|
||||
*handle += 1;
|
||||
|
||||
let asset_tag = chassis.asset_tag.as_deref();
|
||||
let mut next = 1u8;
|
||||
let asset_idx = alloc_index(&mut next, asset_tag.is_some())?;
|
||||
|
||||
let ch = SmbiosChassis {
|
||||
r#type: SYSTEM_ENCLOSURE,
|
||||
length: size_of::<SmbiosChassis>() as u8,
|
||||
handle: *handle,
|
||||
manufacturer: 0,
|
||||
chassis_type: CHASSIS_TYPE_UNKNOWN,
|
||||
version: 0,
|
||||
serial_number: 0,
|
||||
asset_tag: asset_idx,
|
||||
bootup_state: CHASSIS_STATE_UNKNOWN,
|
||||
power_supply_state: CHASSIS_STATE_UNKNOWN,
|
||||
thermal_state: CHASSIS_STATE_UNKNOWN,
|
||||
security_status: CHASSIS_SECURITY_STATUS_NONE,
|
||||
contained_element_count: 0,
|
||||
contained_element_record_length: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
*curptr = write_and_incr(mem, ch, *curptr)?;
|
||||
*curptr = write_opt_string(mem, asset_tag, *curptr)?;
|
||||
*curptr = write_string_terminator(mem, *curptr, asset_tag.is_some())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Result<u64> {
|
||||
let system = smbios.and_then(|cfg| cfg.system.as_ref());
|
||||
let chassis = smbios.and_then(|cfg| cfg.chassis.as_ref());
|
||||
let oem_strings: &[String] = smbios.map_or(&[], |cfg| &cfg.oem_strings);
|
||||
serial_number: Option<&str>,
|
||||
uuid: Option<&str>,
|
||||
oem_strings: Option<&[&str]>,
|
||||
) -> Result<u64> {
|
||||
let physptr = GuestAddress(SMBIOS_START)
|
||||
.checked_add(size_of::<Smbios30Entrypoint>() as u64)
|
||||
.checked_add(mem::size_of::<Smbios30Entrypoint>() as u64)
|
||||
.ok_or(Error::NotEnoughMemory)?;
|
||||
let mut curptr = physptr;
|
||||
let mut handle = 0;
|
||||
@@ -380,7 +196,7 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
|
||||
handle += 1;
|
||||
let smbios_biosinfo = SmbiosBiosInfo {
|
||||
r#type: BIOS_INFORMATION,
|
||||
length: size_of::<SmbiosBiosInfo>() as u8,
|
||||
length: mem::size_of::<SmbiosBiosInfo>() as u8,
|
||||
handle,
|
||||
vendor: 1, // First string written in this section
|
||||
version: 2, // Second string written in this section
|
||||
@@ -394,18 +210,39 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
|
||||
curptr = write_and_incr(mem, 0u8, curptr)?;
|
||||
}
|
||||
|
||||
write_type1_system(mem, &mut curptr, &mut handle, system)?;
|
||||
{
|
||||
handle += 1;
|
||||
|
||||
if let Some(chassis) = chassis {
|
||||
write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?;
|
||||
let uuid_number = uuid
|
||||
.map(Uuid::parse_str)
|
||||
.transpose()
|
||||
.map_err(Error::ParseUuid)?
|
||||
.unwrap_or(Uuid::nil());
|
||||
let smbios_sysinfo = SmbiosSysInfo {
|
||||
r#type: SYSTEM_INFORMATION,
|
||||
length: mem::size_of::<SmbiosSysInfo>() as u8,
|
||||
handle,
|
||||
manufacturer: 1, // First string written in this section
|
||||
product_name: 2, // Second string written in this section
|
||||
serial_number: serial_number.map(|_| 3).unwrap_or_default(), // 3rd string
|
||||
uuid: uuid_number.to_bytes_le(), // set uuid
|
||||
..Default::default()
|
||||
};
|
||||
curptr = write_and_incr(mem, smbios_sysinfo, curptr)?;
|
||||
curptr = write_string(mem, "Cloud Hypervisor", curptr)?;
|
||||
curptr = write_string(mem, "cloud-hypervisor", curptr)?;
|
||||
if let Some(serial_number) = serial_number {
|
||||
curptr = write_string(mem, serial_number, curptr)?;
|
||||
}
|
||||
curptr = write_and_incr(mem, 0u8, curptr)?;
|
||||
}
|
||||
|
||||
if !oem_strings.is_empty() {
|
||||
if let Some(oem_strings) = oem_strings {
|
||||
handle += 1;
|
||||
|
||||
let smbios_oemstrings = SmbiosOemStrings {
|
||||
r#type: OEM_STRINGS,
|
||||
length: size_of::<SmbiosOemStrings>() as u8,
|
||||
length: mem::size_of::<SmbiosOemStrings>() as u8,
|
||||
handle,
|
||||
count: oem_strings.len() as u8,
|
||||
};
|
||||
@@ -416,14 +253,14 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
|
||||
curptr = write_string(mem, s, curptr)?;
|
||||
}
|
||||
|
||||
curptr = write_string_terminator(mem, curptr, true)?;
|
||||
curptr = write_and_incr(mem, 0u8, curptr)?;
|
||||
}
|
||||
|
||||
{
|
||||
handle += 1;
|
||||
let smbios_end = SmbiosEndOfTable {
|
||||
r#type: END_OF_TABLE,
|
||||
length: size_of::<SmbiosEndOfTable>() as u8,
|
||||
length: mem::size_of::<SmbiosEndOfTable>() as u8,
|
||||
handle,
|
||||
};
|
||||
curptr = write_and_incr(mem, smbios_end, curptr)?;
|
||||
@@ -434,7 +271,7 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
|
||||
{
|
||||
let mut smbios_ep = Smbios30Entrypoint {
|
||||
signature: *SM3_MAGIC_IDENT,
|
||||
length: size_of::<Smbios30Entrypoint>() as u8,
|
||||
length: mem::size_of::<Smbios30Entrypoint>() as u8,
|
||||
// SMBIOS rev 3.2.0
|
||||
majorver: 0x03,
|
||||
minorver: 0x02,
|
||||
@@ -446,261 +283,43 @@ pub fn setup_smbios(mem: &GuestMemoryMmap, smbios: Option<&SmbiosConfig>) -> Res
|
||||
};
|
||||
smbios_ep.checksum = compute_checksum(&smbios_ep);
|
||||
mem.write_obj(smbios_ep, GuestAddress(SMBIOS_START))
|
||||
.map_err(Error::WriteSmbiosEp)?;
|
||||
.map_err(|_| Error::WriteSmbiosEp)?;
|
||||
}
|
||||
|
||||
Ok(curptr.unchecked_offset_from(physptr) + size_of::<Smbios30Entrypoint>() as u64)
|
||||
Ok(curptr.unchecked_offset_from(physptr) + std::mem::size_of::<Smbios30Entrypoint>() as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
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, None, None, None).unwrap();
|
||||
|
||||
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
|
||||
let mut cur = GuestAddress(smbios_ep.physptr);
|
||||
|
||||
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
|
||||
cur = cur.checked_add(bios.length as u64).unwrap();
|
||||
let (_, next) = read_string_set(&mem, cur);
|
||||
cur = next;
|
||||
|
||||
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
|
||||
cur = cur.checked_add(sys.length as u64).unwrap();
|
||||
let (_, next) = read_string_set(&mem, cur);
|
||||
cur = next;
|
||||
|
||||
let chassis: SmbiosChassis = mem.read_obj(cur).unwrap();
|
||||
cur = cur.checked_add(chassis.length as u64).unwrap();
|
||||
// SMBIOS DSP0134 §6.1.3: empty string-set ends with double NUL.
|
||||
let b0: u8 = mem.read_obj(cur).unwrap();
|
||||
let b1: u8 = mem.read_obj(cur.checked_add(1).unwrap()).unwrap();
|
||||
assert_eq!(b0, 0);
|
||||
assert_eq!(b1, 0);
|
||||
cur = cur.checked_add(2).unwrap();
|
||||
|
||||
let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(end.r#type, END_OF_TABLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smbios_chassis_oem_strings_layout() {
|
||||
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
|
||||
|
||||
let smbios = SmbiosConfig {
|
||||
chassis: Some(SmbiosChassisConfig {
|
||||
asset_tag: Some("rack1".to_string()),
|
||||
}),
|
||||
oem_strings: ["o1".to_string(), "o2".to_string()].into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
setup_smbios(&mem, Some(&smbios)).unwrap();
|
||||
|
||||
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
|
||||
let mut cur = GuestAddress(smbios_ep.physptr);
|
||||
|
||||
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
|
||||
cur = cur.checked_add(bios.length as u64).unwrap();
|
||||
let (_, next) = read_string_set(&mem, cur);
|
||||
cur = next;
|
||||
|
||||
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
|
||||
cur = cur.checked_add(sys.length as u64).unwrap();
|
||||
let (_, next) = read_string_set(&mem, cur);
|
||||
cur = next;
|
||||
|
||||
let chassis: SmbiosChassis = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(chassis.r#type, SYSTEM_ENCLOSURE);
|
||||
assert_eq!(chassis.asset_tag, 1);
|
||||
cur = cur.checked_add(chassis.length as u64).unwrap();
|
||||
let (chassis_strings, next) = read_string_set(&mem, cur);
|
||||
assert_eq!(chassis_strings, vec!["rack1"]);
|
||||
cur = next;
|
||||
|
||||
let oem: SmbiosOemStrings = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(oem.r#type, OEM_STRINGS);
|
||||
assert_eq!(oem.count, 2);
|
||||
cur = cur.checked_add(oem.length as u64).unwrap();
|
||||
let (oem_strings, next) = read_string_set(&mem, cur);
|
||||
assert_eq!(oem_strings, vec!["o1", "o2"]);
|
||||
cur = next;
|
||||
|
||||
let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(end.r#type, END_OF_TABLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smbios_strings_terminators_default() {
|
||||
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
|
||||
|
||||
setup_smbios(&mem, None).unwrap();
|
||||
|
||||
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
|
||||
let mut cur = GuestAddress(smbios_ep.physptr);
|
||||
|
||||
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(bios.r#type, BIOS_INFORMATION);
|
||||
cur = cur.checked_add(bios.length as u64).unwrap();
|
||||
let (bios_strings, next) = read_string_set(&mem, cur);
|
||||
assert_eq!(bios_strings, vec!["cloud-hypervisor", "0"]);
|
||||
cur = next;
|
||||
|
||||
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(sys.r#type, SYSTEM_INFORMATION);
|
||||
assert_eq!(sys.manufacturer, 1);
|
||||
assert_eq!(sys.product_name, 2);
|
||||
assert_eq!(sys.version, 0);
|
||||
assert_eq!(sys.serial_number, 0);
|
||||
assert_eq!(sys.sku, 0);
|
||||
assert_eq!(sys.family, 0);
|
||||
cur = cur.checked_add(sys.length as u64).unwrap();
|
||||
let (sys_strings, next) = read_string_set(&mem, cur);
|
||||
assert_eq!(
|
||||
sys_strings,
|
||||
vec![DEFAULT_SYSTEM_MANUFACTURER, DEFAULT_SYSTEM_PRODUCT_NAME]
|
||||
);
|
||||
cur = next;
|
||||
|
||||
let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(end.r#type, END_OF_TABLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smbios_strings_too_many() {
|
||||
let mut next = 1u8;
|
||||
for _ in 0..255 {
|
||||
alloc_index(&mut next, true).unwrap();
|
||||
}
|
||||
let err = alloc_index(&mut next, true).unwrap_err();
|
||||
assert!(matches!(err, Error::TooManyStrings));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smbios_uuid_invalid_rejected() {
|
||||
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
|
||||
let smbios = SmbiosConfig {
|
||||
system: Some(SmbiosSystem {
|
||||
uuid: Some("not-a-uuid".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let err = setup_smbios(&mem, Some(&smbios)).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseUuid(_, _)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smbios_uuid_written_le() {
|
||||
let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap();
|
||||
let uuid_str = "00112233-4455-6677-8899-aabbccddeeff";
|
||||
let smbios = SmbiosConfig {
|
||||
system: Some(SmbiosSystem {
|
||||
uuid: Some(uuid_str.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
setup_smbios(&mem, Some(&smbios)).unwrap();
|
||||
|
||||
let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap();
|
||||
let mut cur = GuestAddress(smbios_ep.physptr);
|
||||
|
||||
let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap();
|
||||
cur = cur.checked_add(bios.length as u64).unwrap();
|
||||
let (_, next) = read_string_set(&mem, cur);
|
||||
cur = next;
|
||||
|
||||
let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap();
|
||||
assert_eq!(sys.uuid, Uuid::parse_str(uuid_str).unwrap().to_bytes_le());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smbios_write_fails_with_too_small_memory() {
|
||||
let mem = GuestMemoryMmap::from_ranges(&[(
|
||||
GuestAddress(SMBIOS_START),
|
||||
size_of::<Smbios30Entrypoint>(),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
let err = setup_smbios(&mem, None).unwrap_err();
|
||||
assert!(matches!(err, Error::WriteData(_)));
|
||||
assert_eq!(compute_checksum(&smbios_ep), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use crate::GuestMemoryMmap;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
use std::slice;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::str::FromStr;
|
||||
|
||||
use log::{debug, info};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemoryError};
|
||||
|
||||
use crate::GuestMemoryMmap;
|
||||
|
||||
#[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("Failed read GUID table: {0}")]
|
||||
ReadGuidTable(#[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")]
|
||||
#[error("Failed to create Uuid: {0}")]
|
||||
UuidCreation(#[source] uuid::Error),
|
||||
}
|
||||
|
||||
@@ -37,7 +33,7 @@ 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 +43,7 @@ pub struct TdvfDescriptor {
|
||||
}
|
||||
|
||||
// TDVF_SECTION
|
||||
#[repr(C, packed)]
|
||||
#[repr(packed)]
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct TdvfSection {
|
||||
pub data_offset: u32,
|
||||
@@ -110,7 +106,7 @@ fn tdvf_descriptor_offset(file: &mut File) -> Result<(SeekFrom, bool), TdvfError
|
||||
u16::from_le_bytes(table[offset - 18..offset - 16].try_into().unwrap()) as usize;
|
||||
debug!(
|
||||
"Entry GUID = {}, size = {}",
|
||||
entry_uuid.hyphenated(),
|
||||
entry_uuid.hyphenated().to_string(),
|
||||
entry_size
|
||||
);
|
||||
|
||||
@@ -163,7 +159,10 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
|
||||
let mut descriptor: TdvfDescriptor = Default::default();
|
||||
// SAFETY: we read exactly the size of the descriptor header
|
||||
file.read_exact(unsafe {
|
||||
slice::from_raw_parts_mut((&raw mut descriptor).cast(), size_of::<TdvfDescriptor>())
|
||||
std::slice::from_raw_parts_mut(
|
||||
&mut descriptor as *mut _ as *mut u8,
|
||||
std::mem::size_of::<TdvfDescriptor>(),
|
||||
)
|
||||
})
|
||||
.map_err(TdvfError::ReadDescriptor)?;
|
||||
|
||||
@@ -172,7 +171,8 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
|
||||
}
|
||||
|
||||
if descriptor.length as usize
|
||||
!= size_of::<TdvfDescriptor>() + size_of::<TdvfSection>() * descriptor.num_sections as usize
|
||||
!= std::mem::size_of::<TdvfDescriptor>()
|
||||
+ std::mem::size_of::<TdvfSection>() * descriptor.num_sections as usize
|
||||
{
|
||||
return Err(TdvfError::InvalidDescriptorSize);
|
||||
}
|
||||
@@ -186,9 +186,9 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec<TdvfSection>, bool),
|
||||
|
||||
// SAFETY: we read exactly the advertised sections
|
||||
file.read_exact(unsafe {
|
||||
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)?;
|
||||
@@ -207,7 +207,7 @@ enum HobType {
|
||||
EndOfHobList = 0xffff,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct HobHeader {
|
||||
r#type: HobType,
|
||||
@@ -215,7 +215,7 @@ struct HobHeader {
|
||||
reserved: u32,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct HobHandoffInfoTable {
|
||||
header: HobHeader,
|
||||
@@ -228,7 +228,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 +237,7 @@ struct EfiGuid {
|
||||
data4: [u8; 8],
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct HobResourceDescriptor {
|
||||
header: HobHeader,
|
||||
@@ -248,7 +248,7 @@ struct HobResourceDescriptor {
|
||||
resource_length: u64,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct HobGuidType {
|
||||
header: HobHeader,
|
||||
@@ -264,14 +264,14 @@ pub enum PayloadImageType {
|
||||
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)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct TdPayload {
|
||||
guid_type: HobGuidType,
|
||||
@@ -297,12 +297,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 +319,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 +332,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 +359,7 @@ impl TdHob {
|
||||
let resource_descriptor = HobResourceDescriptor {
|
||||
header: HobHeader {
|
||||
r#type: HobType::ResourceDescriptor,
|
||||
length: size_of::<HobResourceDescriptor>() as u16,
|
||||
length: std::mem::size_of::<HobResourceDescriptor>() as u16,
|
||||
reserved: 0,
|
||||
},
|
||||
owner: EfiGuid::default(),
|
||||
@@ -436,7 +436,8 @@ impl TdHob {
|
||||
// We already know the HobGuidType size is 8 bytes multiple, but we
|
||||
// need the total size to be 8 bytes multiple. That is why the ACPI
|
||||
// table size must be 8 bytes multiple as well.
|
||||
let length = size_of::<HobGuidType>() as u16 + align_hob(table_content.len() as u64) as u16;
|
||||
let length = std::mem::size_of::<HobGuidType>() as u16
|
||||
+ align_hob(table_content.len() as u64) as u16;
|
||||
let hob_guid_type = HobGuidType {
|
||||
header: HobHeader {
|
||||
r#type: HobType::GuidExtension,
|
||||
@@ -458,7 +459,7 @@ impl TdHob {
|
||||
);
|
||||
mem.write_obj(hob_guid_type, GuestAddress(self.current_offset))
|
||||
.map_err(TdvfError::GuestMemoryWriteHob)?;
|
||||
let current_offset = self.current_offset + size_of::<HobGuidType>() as u64;
|
||||
let current_offset = self.current_offset + std::mem::size_of::<HobGuidType>() as u64;
|
||||
|
||||
// In case the table is quite large, let's make sure we can handle
|
||||
// retrying until everything has been correctly copied.
|
||||
@@ -489,7 +490,7 @@ impl TdHob {
|
||||
guid_type: HobGuidType {
|
||||
header: HobHeader {
|
||||
r#type: HobType::GuidExtension,
|
||||
length: size_of::<TdPayload>() as u16,
|
||||
length: std::mem::size_of::<TdPayload>() as u16,
|
||||
reserved: 0,
|
||||
},
|
||||
// HOB_PAYLOAD_INFO_GUID
|
||||
@@ -516,16 +517,16 @@ impl TdHob {
|
||||
}
|
||||
|
||||
#[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 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!("{section:x?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,27 @@
|
||||
[package]
|
||||
authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"]
|
||||
edition.workspace = true
|
||||
name = "block"
|
||||
rust-version.workspace = true
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["The Cloud Hypervisor Authors", "The Chromium OS Authors"]
|
||||
|
||||
[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",
|
||||
] }
|
||||
byteorder = "1.4.3"
|
||||
crc32c = "0.6.4"
|
||||
io-uring = { version = "0.6.1", optional = true }
|
||||
libc = "0.2.147"
|
||||
log = "0.4.17"
|
||||
remain = "0.2.11"
|
||||
smallvec = "1.11.0"
|
||||
thiserror = "1.0.40"
|
||||
uuid = { version = "1.3.4", features = ["v4"] }
|
||||
versionize = "0.1.10"
|
||||
versionize_derive = "0.1.4"
|
||||
virtio-bindings = { version = "0.2.0", features = ["virtio-v5_0_0"] }
|
||||
virtio-queue = "0.9.0"
|
||||
vm-memory = { version = "0.12.2", features = ["backend-mmap", "backend-atomic", "backend-bitmap"] }
|
||||
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
|
||||
vmm-sys-util = "0.11.0"
|
||||
|
||||
@@ -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[..]);
|
||||
}
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fs::{File, Metadata};
|
||||
use std::os::fd::{AsFd, BorrowedFd};
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::{io, slice};
|
||||
|
||||
use vmm_sys_util::file_traits::FileSync;
|
||||
use vmm_sys_util::seek_hole::SeekHole;
|
||||
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
|
||||
|
||||
use crate::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))
|
||||
}
|
||||
|
||||
/// True when `offset` and every iovec base/length satisfy `alignment`
|
||||
/// (`alignment == 0` means no O_DIRECT, so everything is "aligned").
|
||||
fn iovecs_are_aligned(alignment: usize, iovecs: &[libc::iovec], offset: u64) -> bool {
|
||||
alignment == 0
|
||||
|| (offset.is_multiple_of(alignment as u64)
|
||||
&& iovecs.iter().all(|iov| {
|
||||
(iov.iov_base as usize).is_multiple_of(alignment)
|
||||
&& iov.iov_len.is_multiple_of(alignment)
|
||||
}))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Read into the buffers described by `iovecs`, starting at `offset`.
|
||||
///
|
||||
/// # Safety
|
||||
/// Every iovec must describe valid, writable memory of `iov_len` bytes.
|
||||
pub(crate) unsafe fn read_vectored_at(
|
||||
&self,
|
||||
iovecs: &[libc::iovec],
|
||||
offset: u64,
|
||||
) -> io::Result<usize> {
|
||||
if iovecs.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
if iovecs_are_aligned(self.alignment, iovecs, offset) {
|
||||
// SAFETY: upheld by this fn contract.
|
||||
let ret = unsafe {
|
||||
libc::preadv(
|
||||
self.file.as_raw_fd(),
|
||||
iovecs.as_ptr(),
|
||||
iovecs.len() as libc::c_int,
|
||||
offset as libc::off_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
return Ok(ret as usize);
|
||||
}
|
||||
let total_len = iovecs.iter().map(|iov| iov.iov_len).sum();
|
||||
self.read_unaligned(offset, total_len, |mut data| {
|
||||
for iov in iovecs {
|
||||
if data.is_empty() {
|
||||
break;
|
||||
}
|
||||
let n = data.len().min(iov.iov_len);
|
||||
// SAFETY: upheld by this fn contract.
|
||||
let dst = unsafe { slice::from_raw_parts_mut(iov.iov_base as *mut u8, n) };
|
||||
dst.copy_from_slice(&data[..n]);
|
||||
data = &data[n..];
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the buffers described by `iovecs`, starting at `offset`.
|
||||
///
|
||||
/// # Safety
|
||||
/// Every iovec must describe valid, readable memory of `iov_len` bytes.
|
||||
pub(crate) unsafe fn write_vectored_at(
|
||||
&self,
|
||||
iovecs: &[libc::iovec],
|
||||
offset: u64,
|
||||
) -> io::Result<usize> {
|
||||
if iovecs.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
if iovecs_are_aligned(self.alignment, iovecs, offset) {
|
||||
// SAFETY: upheld by this fn contract.
|
||||
let ret = unsafe {
|
||||
libc::pwritev(
|
||||
self.file.as_raw_fd(),
|
||||
iovecs.as_ptr(),
|
||||
iovecs.len() as libc::c_int,
|
||||
offset as libc::off_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
return Ok(ret as usize);
|
||||
}
|
||||
let total_len = iovecs.iter().map(|iov| iov.iov_len).sum();
|
||||
self.write_unaligned(offset, total_len, |mut dst| {
|
||||
for iov in iovecs {
|
||||
if dst.is_empty() {
|
||||
break;
|
||||
}
|
||||
let n = dst.len().min(iov.iov_len);
|
||||
// SAFETY: upheld by this fn contract.
|
||||
let src = unsafe { slice::from_raw_parts(iov.iov_base as *const u8, n) };
|
||||
dst[..n].copy_from_slice(src);
|
||||
dst = &mut dst[n..];
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// Build an iovec over `buf`, which must outlive the iovec.
|
||||
fn iovec_of(buf: &mut [u8]) -> libc::iovec {
|
||||
libc::iovec {
|
||||
iov_base: buf.as_mut_ptr() as *mut libc::c_void,
|
||||
iov_len: buf.len(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vectored_empty_is_noop() {
|
||||
let tf = pattern_file(100);
|
||||
let af = forced(tf.as_file().try_clone().unwrap(), 512);
|
||||
// SAFETY: empty iovec slices point to no memory.
|
||||
assert_eq!(unsafe { af.read_vectored_at(&[], 0) }.unwrap(), 0);
|
||||
// SAFETY: empty iovec slices point to no memory.
|
||||
assert_eq!(unsafe { af.write_vectored_at(&[], 0) }.unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vectored_fast_path_roundtrip() {
|
||||
let tf = pattern_file(4096);
|
||||
// alignment 0 sends any iovecs through the single preadv/pwritev path.
|
||||
let af = forced(tf.as_file().try_clone().unwrap(), 0);
|
||||
|
||||
let mut w0: Vec<u8> = (0..30).map(|i| ((i + 7) % 239) as u8).collect();
|
||||
let mut w1: Vec<u8> = (0..70).map(|i| ((i + 37) % 239) as u8).collect();
|
||||
let wiovecs = [iovec_of(&mut w0), iovec_of(&mut w1)];
|
||||
// SAFETY: the iovecs describe the live w0/w1 buffers.
|
||||
assert_eq!(unsafe { af.write_vectored_at(&wiovecs, 10) }.unwrap(), 100);
|
||||
let data = [w0.as_slice(), &w1].concat();
|
||||
|
||||
let mut plain = vec![0u8; 100];
|
||||
tf.as_file().read_exact_at(&mut plain, 10).unwrap();
|
||||
assert_eq!(plain, data);
|
||||
|
||||
let mut r0 = vec![0u8; 30];
|
||||
let mut r1 = vec![0u8; 70];
|
||||
let riovecs = [iovec_of(&mut r0), iovec_of(&mut r1)];
|
||||
// SAFETY: the iovecs describe the live r0/r1 buffers.
|
||||
assert_eq!(unsafe { af.read_vectored_at(&riovecs, 10) }.unwrap(), 100);
|
||||
assert_eq!([r0.as_slice(), &r1].concat(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vectored_unaligned_scatter_gather_roundtrip() {
|
||||
let tf = pattern_file(8192);
|
||||
let af = forced(tf.as_file().try_clone().unwrap(), 512);
|
||||
|
||||
// Gather three iovecs into an unaligned read-modify-write.
|
||||
let mut w0: Vec<u8> = (0..50).map(|i| ((i + 1) % 239) as u8).collect();
|
||||
let mut w1: Vec<u8> = (0..100).map(|i| ((i + 51) % 239) as u8).collect();
|
||||
let mut w2: Vec<u8> = (0..50).map(|i| ((i + 151) % 239) as u8).collect();
|
||||
let wiovecs = [iovec_of(&mut w0), iovec_of(&mut w1), iovec_of(&mut w2)];
|
||||
// SAFETY: the iovecs describe the live w0/w1/w2 buffers.
|
||||
assert_eq!(unsafe { af.write_vectored_at(&wiovecs, 100) }.unwrap(), 200);
|
||||
let data = [w0.as_slice(), &w1, &w2].concat();
|
||||
|
||||
// Independently confirm the region and the untouched neighbors.
|
||||
let mut expected: Vec<u8> = (0..8192).map(|i| (i % 251) as u8).collect();
|
||||
expected[100..300].copy_from_slice(&data);
|
||||
let mut whole = vec![0u8; 8192];
|
||||
tf.as_file().read_exact_at(&mut whole, 0).unwrap();
|
||||
assert_eq!(whole, expected);
|
||||
|
||||
// Scatter the same region back across three iovecs.
|
||||
let mut r0 = vec![0u8; 50];
|
||||
let mut r1 = vec![0u8; 100];
|
||||
let mut r2 = vec![0u8; 50];
|
||||
let riovecs = [iovec_of(&mut r0), iovec_of(&mut r1), iovec_of(&mut r2)];
|
||||
// SAFETY: the iovecs describe the live r0/r1/r2 buffers.
|
||||
assert_eq!(unsafe { af.read_vectored_at(&riovecs, 100) }.unwrap(), 200);
|
||||
assert_eq!([r0.as_slice(), &r1, &r2].concat(), data);
|
||||
}
|
||||
}
|
||||
60
block/src/async_io.rs
Normal file
60
block/src/async_io.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||
|
||||
use crate::DiskTopology;
|
||||
use thiserror::Error;
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DiskFileError {
|
||||
/// Failed getting disk file size.
|
||||
#[error("Failed getting disk file size: {0}")]
|
||||
Size(#[source] std::io::Error),
|
||||
/// Failed creating a new AsyncIo.
|
||||
#[error("Failed creating a new AsyncIo: {0}")]
|
||||
NewAsyncIo(#[source] std::io::Error),
|
||||
}
|
||||
|
||||
pub type DiskFileResult<T> = std::result::Result<T, DiskFileError>;
|
||||
|
||||
pub trait DiskFile: Send {
|
||||
fn size(&mut self) -> DiskFileResult<u64>;
|
||||
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>>;
|
||||
fn topology(&mut self) -> DiskTopology {
|
||||
DiskTopology::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AsyncIoError {
|
||||
/// Failed vectored reading from file.
|
||||
#[error("Failed vectored reading from file: {0}")]
|
||||
ReadVectored(#[source] std::io::Error),
|
||||
/// Failed vectored writing to file.
|
||||
#[error("Failed vectored writing to file: {0}")]
|
||||
WriteVectored(#[source] std::io::Error),
|
||||
/// Failed synchronizing file.
|
||||
#[error("Failed synchronizing file: {0}")]
|
||||
Fsync(#[source] std::io::Error),
|
||||
}
|
||||
|
||||
pub type AsyncIoResult<T> = std::result::Result<T, AsyncIoError>;
|
||||
|
||||
pub trait AsyncIo: Send {
|
||||
fn notifier(&self) -> &EventFd;
|
||||
fn read_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()>;
|
||||
fn write_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()>;
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()>;
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)>;
|
||||
}
|
||||
@@ -1,181 +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
|
||||
//! - [`MetadataSync`] - flush of format metadata cached in memory
|
||||
//!
|
||||
//! [`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 + MetadataSync
|
||||
//! \ /
|
||||
//! 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<()>;
|
||||
}
|
||||
|
||||
/// Flush of format metadata cached in memory.
|
||||
///
|
||||
/// Default is a no-op for formats that keep no metadata cache
|
||||
/// (e.g. raw, fixed vhd).
|
||||
pub trait MetadataSync: Send + Debug {
|
||||
/// Flushes format metadata cached in memory (e.g. qcow2 L2/refcount
|
||||
/// tables) to the underlying file.
|
||||
///
|
||||
/// Called on device pause so that an externally copied or reopened
|
||||
/// image is self-consistent without requiring a guest-initiated
|
||||
/// flush.
|
||||
fn sync_metadata(&self) -> BlockResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, resize,
|
||||
/// and metadata sync. Used by consumers that need feature negotiation
|
||||
/// without async I/O (e.g. vhost user block).
|
||||
pub trait FullDiskFile:
|
||||
DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable + MetadataSync
|
||||
{
|
||||
}
|
||||
|
||||
/// Blanket implementation: any type implementing all constituent traits
|
||||
/// automatically satisfies [`FullDiskFile`].
|
||||
impl<T: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable + MetadataSync> 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 {}
|
||||
@@ -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>;
|
||||
@@ -1,312 +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::formats::vmdk::VmdkDisk;
|
||||
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::FlatVmdk => open_flat_vmdk(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))?,
|
||||
))
|
||||
}
|
||||
|
||||
fn open_flat_vmdk(
|
||||
file: fs::File,
|
||||
options: &DiskOpenOptions<'_>,
|
||||
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
|
||||
info!("Opening VMDK disk file with synchronous backend");
|
||||
Ok(Box::new(
|
||||
VmdkDisk::new(file, options.path, options.direct).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);
|
||||
}
|
||||
}
|
||||
90
block/src/fixed_vhd.rs
Normal file
90
block/src/fixed_vhd.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::vhd::VhdFooter;
|
||||
use crate::BlockBackend;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FixedVhd {
|
||||
file: File,
|
||||
size: u64,
|
||||
position: u64,
|
||||
}
|
||||
|
||||
impl FixedVhd {
|
||||
pub fn new(mut file: File) -> std::io::Result<Self> {
|
||||
let footer = VhdFooter::new(&mut file)?;
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
size: footer.current_size(),
|
||||
position: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for FixedVhd {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.file.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for FixedVhd {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self.file.read(buf) {
|
||||
Ok(r) => {
|
||||
self.position = self.position.checked_add(r.try_into().unwrap()).unwrap();
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for FixedVhd {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
match self.file.write(buf) {
|
||||
Ok(r) => {
|
||||
self.position = self.position.checked_add(r.try_into().unwrap()).unwrap();
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.file.sync_all()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for FixedVhd {
|
||||
fn seek(&mut self, newpos: SeekFrom) -> std::io::Result<u64> {
|
||||
match self.file.seek(newpos) {
|
||||
Ok(pos) => {
|
||||
self.position = pos;
|
||||
Ok(pos)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockBackend for FixedVhd {
|
||||
fn size(&self) -> std::result::Result<u64, crate::Error> {
|
||||
Ok(self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FixedVhd {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
file: self.file.try_clone().expect("FixedVhd cloning failed"),
|
||||
size: self.size,
|
||||
position: self.position,
|
||||
}
|
||||
}
|
||||
}
|
||||
103
block/src/fixed_vhd_async.rs
Normal file
103
block/src/fixed_vhd_async.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoError, AsyncIoResult, DiskFile, DiskFileError, DiskFileResult,
|
||||
};
|
||||
use crate::fixed_vhd::FixedVhd;
|
||||
use crate::raw_async::RawFileAsync;
|
||||
use crate::BlockBackend;
|
||||
use std::fs::File;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
pub struct FixedVhdDiskAsync(FixedVhd);
|
||||
|
||||
impl FixedVhdDiskAsync {
|
||||
pub fn new(file: File) -> std::io::Result<Self> {
|
||||
Ok(Self(FixedVhd::new(file)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl DiskFile for FixedVhdDiskAsync {
|
||||
fn size(&mut self) -> DiskFileResult<u64> {
|
||||
Ok(self.0.size().unwrap())
|
||||
}
|
||||
|
||||
fn new_async_io(&self, ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||
Ok(Box::new(
|
||||
FixedVhdAsync::new(self.0.as_raw_fd(), ring_depth, self.0.size().unwrap())
|
||||
.map_err(DiskFileError::NewAsyncIo)?,
|
||||
) as Box<dyn AsyncIo>)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FixedVhdAsync {
|
||||
raw_file_async: RawFileAsync,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl FixedVhdAsync {
|
||||
pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> std::io::Result<Self> {
|
||||
let raw_file_async = RawFileAsync::new(fd, ring_depth)?;
|
||||
|
||||
Ok(FixedVhdAsync {
|
||||
raw_file_async,
|
||||
size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncIo for FixedVhdAsync {
|
||||
fn notifier(&self) -> &EventFd {
|
||||
self.raw_file_async.notifier()
|
||||
}
|
||||
|
||||
fn read_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
if offset as u64 >= self.size {
|
||||
return Err(AsyncIoError::ReadVectored(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid offset {}, can't be larger than file size {}",
|
||||
offset, self.size
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
self.raw_file_async.read_vectored(offset, iovecs, user_data)
|
||||
}
|
||||
|
||||
fn write_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
if offset as u64 >= self.size {
|
||||
return Err(AsyncIoError::WriteVectored(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid offset {}, can't be larger than file size {}",
|
||||
offset, self.size
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
self.raw_file_async
|
||||
.write_vectored(offset, iovecs, user_data)
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||
self.raw_file_async.fsync(user_data)
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
self.raw_file_async.next_completed_request()
|
||||
}
|
||||
}
|
||||
100
block/src/fixed_vhd_sync.rs
Normal file
100
block/src/fixed_vhd_sync.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoError, AsyncIoResult, DiskFile, DiskFileError, DiskFileResult,
|
||||
};
|
||||
use crate::fixed_vhd::FixedVhd;
|
||||
use crate::raw_sync::RawFileSync;
|
||||
use crate::BlockBackend;
|
||||
use std::fs::File;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
pub struct FixedVhdDiskSync(FixedVhd);
|
||||
|
||||
impl FixedVhdDiskSync {
|
||||
pub fn new(file: File) -> std::io::Result<Self> {
|
||||
Ok(Self(FixedVhd::new(file)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl DiskFile for FixedVhdDiskSync {
|
||||
fn size(&mut self) -> DiskFileResult<u64> {
|
||||
Ok(self.0.size().unwrap())
|
||||
}
|
||||
|
||||
fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult<Box<dyn AsyncIo>> {
|
||||
Ok(Box::new(
|
||||
FixedVhdSync::new(self.0.as_raw_fd(), self.0.size().unwrap())
|
||||
.map_err(DiskFileError::NewAsyncIo)?,
|
||||
) as Box<dyn AsyncIo>)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FixedVhdSync {
|
||||
raw_file_sync: RawFileSync,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl FixedVhdSync {
|
||||
pub fn new(fd: RawFd, size: u64) -> std::io::Result<Self> {
|
||||
Ok(FixedVhdSync {
|
||||
raw_file_sync: RawFileSync::new(fd),
|
||||
size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncIo for FixedVhdSync {
|
||||
fn notifier(&self) -> &EventFd {
|
||||
self.raw_file_sync.notifier()
|
||||
}
|
||||
|
||||
fn read_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
if offset as u64 >= self.size {
|
||||
return Err(AsyncIoError::ReadVectored(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid offset {}, can't be larger than file size {}",
|
||||
offset, self.size
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
self.raw_file_sync.read_vectored(offset, iovecs, user_data)
|
||||
}
|
||||
|
||||
fn write_vectored(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
iovecs: &[libc::iovec],
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
if offset as u64 >= self.size {
|
||||
return Err(AsyncIoError::WriteVectored(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid offset {}, can't be larger than file size {}",
|
||||
offset, self.size
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
self.raw_file_sync.write_vectored(offset, iovecs, user_data)
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||
self.raw_file_sync.fsync(user_data)
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
||||
self.raw_file_sync.next_completed_request()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +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;
|
||||
pub mod vmdk;
|
||||
@@ -1,173 +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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,361 +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::cmp::min;
|
||||
use std::io;
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
|
||||
|
||||
use super::decoder::Decoder;
|
||||
use super::metadata::{
|
||||
BackingRead, ClusterReadMapping, ClusterWriteMapping, DeallocAction, QcowMetadata,
|
||||
};
|
||||
use super::qcow_raw_file::QcowRawFile;
|
||||
use crate::async_io::{AsyncIoError, AsyncIoOperation, AsyncIoResult};
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Applies one deallocation action to the data file and refcount table.
|
||||
pub(super) fn apply_dealloc_action(
|
||||
metadata: &QcowMetadata,
|
||||
data_file: &mut QcowRawFile,
|
||||
action: &DeallocAction,
|
||||
) -> io::Result<()> {
|
||||
match action {
|
||||
DeallocAction::PunchHole {
|
||||
host_offset,
|
||||
length,
|
||||
} => {
|
||||
data_file.file_mut().punch_hole(*host_offset, *length)?;
|
||||
metadata.complete_punch_hole(*host_offset);
|
||||
Ok(())
|
||||
}
|
||||
DeallocAction::WriteZeroes {
|
||||
host_offset,
|
||||
length,
|
||||
} => data_file
|
||||
.file_mut()
|
||||
.write_zeroes_at(*host_offset, *length)
|
||||
.map(|_| ()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deallocates a byte range and returns the completion result, 0 on
|
||||
/// success or a negative errno on the first failing action.
|
||||
pub(super) fn deallocate_range_result(
|
||||
metadata: &QcowMetadata,
|
||||
data_file: &mut QcowRawFile,
|
||||
offset: u64,
|
||||
length: usize,
|
||||
sparse: bool,
|
||||
write_zeroes: bool,
|
||||
backing_file: Option<&dyn BackingRead>,
|
||||
) -> i32 {
|
||||
let wrap_error: fn(io::Error) -> AsyncIoError = if write_zeroes {
|
||||
AsyncIoError::WriteZeroes
|
||||
} else {
|
||||
AsyncIoError::PunchHole
|
||||
};
|
||||
let result = metadata
|
||||
.deallocate_bytes(offset, length, sparse, write_zeroes, backing_file)
|
||||
.and_then(|actions| {
|
||||
let mut first_error = None;
|
||||
for action in &actions {
|
||||
if let Err(e) = apply_dealloc_action(metadata, data_file, action) {
|
||||
first_error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
first_error.map_or(Ok(()), Err)
|
||||
})
|
||||
.map_err(wrap_error);
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(AsyncIoError::PunchHole(e) | AsyncIoError::WriteZeroes(e)) => {
|
||||
-e.raw_os_error().unwrap_or(libc::EIO)
|
||||
}
|
||||
Err(_) => -libc::EIO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an operation to the data file cluster by cluster, allocating
|
||||
/// and copying up backing data as needed. Writes are synchronous because
|
||||
/// the host offset is only known after the metadata allocation.
|
||||
pub(super) fn cow_write_sync(
|
||||
address: u64,
|
||||
op: &AsyncIoOperation,
|
||||
metadata: &QcowMetadata,
|
||||
data_file: &QcowRawFile,
|
||||
backing_file: &Option<Arc<dyn BackingRead>>,
|
||||
cluster_size: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
let total_len = op.total_len();
|
||||
let mut buf_offset = 0usize;
|
||||
|
||||
while buf_offset < total_len {
|
||||
let curr_addr = address + buf_offset as u64;
|
||||
let intra_offset = curr_addr & (cluster_size - 1);
|
||||
let remaining_in_cluster = (cluster_size - intra_offset) as usize;
|
||||
let count = min(total_len - buf_offset, remaining_in_cluster);
|
||||
|
||||
let backing_data = if let Some(backing) = backing_file
|
||||
.as_ref()
|
||||
.filter(|_| intra_offset != 0 || count < cluster_size as usize)
|
||||
{
|
||||
let cluster_begin = curr_addr - intra_offset;
|
||||
let mut data = vec![0u8; cluster_size as usize];
|
||||
backing
|
||||
.read_at(cluster_begin, &mut data)
|
||||
.map_err(AsyncIoError::WriteVectored)?;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mapping = metadata
|
||||
.map_cluster_for_write(curr_addr, backing_data)
|
||||
.map_err(AsyncIoError::WriteVectored)?;
|
||||
|
||||
match mapping {
|
||||
ClusterWriteMapping::Allocated {
|
||||
offset: host_offset,
|
||||
} => {
|
||||
let mut buf = vec![0u8; count];
|
||||
op.read_bytes_at(buf_offset, &mut buf)
|
||||
.map_err(AsyncIoError::WriteVectored)?;
|
||||
data_file
|
||||
.file()
|
||||
.write_all_at(&buf, host_offset)
|
||||
.map_err(AsyncIoError::WriteVectored)?;
|
||||
}
|
||||
}
|
||||
buf_offset += count;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads cluster mappings synchronously into an owned operation, filling
|
||||
/// holes, decompressing, and reading from the backing file as each
|
||||
/// mapping requires.
|
||||
pub(super) fn scatter_read_sync(
|
||||
mappings: Vec<ClusterReadMapping>,
|
||||
op: &mut AsyncIoOperation,
|
||||
data_file: &QcowRawFile,
|
||||
backing_file: &Option<Arc<dyn BackingRead>>,
|
||||
cluster_size: u64,
|
||||
decoder: &dyn Decoder,
|
||||
) -> AsyncIoResult<()> {
|
||||
let mut buf_offset = 0usize;
|
||||
for mapping in mappings {
|
||||
match mapping {
|
||||
ClusterReadMapping::Zero { length } => {
|
||||
op.fill_zeroes_at(buf_offset, length as usize)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
buf_offset += length as usize;
|
||||
}
|
||||
ClusterReadMapping::Allocated {
|
||||
offset: host_offset,
|
||||
length,
|
||||
} => {
|
||||
let len = length as usize;
|
||||
let mut buf = vec![0u8; len];
|
||||
data_file
|
||||
.file()
|
||||
.read_exact_at(&mut buf, host_offset)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
op.write_bytes_at(buf_offset, &buf)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
buf_offset += len;
|
||||
}
|
||||
ClusterReadMapping::Compressed {
|
||||
host_offset,
|
||||
compressed_size,
|
||||
cluster_offset,
|
||||
length,
|
||||
} => {
|
||||
let mut compressed = vec![0u8; compressed_size];
|
||||
data_file
|
||||
.file()
|
||||
.read_exact_at(&mut compressed, host_offset)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
let decompressed = decompress_cluster(&compressed, cluster_size as usize, decoder)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
op.write_bytes_at(
|
||||
buf_offset,
|
||||
&decompressed[cluster_offset..cluster_offset + length],
|
||||
)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
buf_offset += length;
|
||||
}
|
||||
ClusterReadMapping::Backing {
|
||||
offset: backing_offset,
|
||||
length,
|
||||
} => {
|
||||
let mut buf = vec![0u8; length as usize];
|
||||
backing_file
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read_at(backing_offset, &mut buf)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
op.write_bytes_at(buf_offset, &buf)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
buf_offset += length as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,910 +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
|
||||
|
||||
//! QCOW2 async disk backend.
|
||||
|
||||
use std::io;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use super::common::{cow_write_sync, deallocate_range_result, scatter_read_sync};
|
||||
use super::decoder::Decoder;
|
||||
use super::metadata::{BackingRead, ClusterReadMapping, QcowMetadata};
|
||||
use super::qcow_raw_file::QcowRawFile;
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, UringDataIo,
|
||||
};
|
||||
|
||||
/// Per queue QCOW2 I/O worker using io_uring.
|
||||
///
|
||||
/// Reads against fully allocated single mapping clusters are submitted
|
||||
/// to io_uring for true asynchronous completion. All other cluster
|
||||
/// types (zero, compressed, backing) and multi mapping reads fall back
|
||||
/// to synchronous I/O with synthetic completions.
|
||||
///
|
||||
/// Writes are synchronous because metadata allocation must complete
|
||||
/// before the host offset is known.
|
||||
pub(super) struct QcowAsync {
|
||||
metadata: Arc<QcowMetadata>,
|
||||
// Drop before data_file so pending SQEs can be submitted while fd is valid.
|
||||
data_io: UringDataIo,
|
||||
data_file: QcowRawFile,
|
||||
backing_file: Option<Arc<dyn BackingRead>>,
|
||||
sparse: bool,
|
||||
cluster_size: u64,
|
||||
decoder: Arc<dyn Decoder>,
|
||||
}
|
||||
|
||||
impl QcowAsync {
|
||||
pub(crate) fn new(
|
||||
metadata: Arc<QcowMetadata>,
|
||||
data_file: QcowRawFile,
|
||||
backing_file: Option<Arc<dyn BackingRead>>,
|
||||
sparse: bool,
|
||||
ring_depth: u32,
|
||||
) -> io::Result<Self> {
|
||||
Ok(QcowAsync {
|
||||
cluster_size: metadata.cluster_size(),
|
||||
decoder: metadata.decoder(),
|
||||
metadata,
|
||||
data_io: UringDataIo::new(ring_depth)?,
|
||||
data_file,
|
||||
backing_file,
|
||||
sparse,
|
||||
})
|
||||
}
|
||||
|
||||
fn async_error_result(error: &AsyncIoError) -> i32 {
|
||||
let io_error = match error {
|
||||
AsyncIoError::ReadVectored(e)
|
||||
| AsyncIoError::WriteVectored(e)
|
||||
| AsyncIoError::SubmitBatchRequests(e)
|
||||
| AsyncIoError::Fsync(e)
|
||||
| AsyncIoError::PunchHole(e)
|
||||
| AsyncIoError::WriteZeroes(e) => e,
|
||||
};
|
||||
-io_error.raw_os_error().unwrap_or(libc::EIO)
|
||||
}
|
||||
|
||||
fn inject_operation_completion(&mut self, op: AsyncIoOperation, result: i32) {
|
||||
self.data_io
|
||||
.inject_completion(AsyncIoCompletion::from_operation(op, result));
|
||||
}
|
||||
|
||||
fn prepare_read_operation(
|
||||
&mut self,
|
||||
mut op: AsyncIoOperation,
|
||||
) -> Result<Option<AsyncIoOperation>, Box<(AsyncIoOperation, AsyncIoError)>> {
|
||||
let total_len = op.total_len();
|
||||
let host_offset = match Self::resolve_read(
|
||||
&self.metadata,
|
||||
&self.data_file,
|
||||
&self.backing_file,
|
||||
op.offset() as u64,
|
||||
&mut op,
|
||||
total_len,
|
||||
self.cluster_size,
|
||||
&*self.decoder,
|
||||
) {
|
||||
Ok(host_offset) => host_offset,
|
||||
Err(e) => return Err(Box::new((op, e))),
|
||||
};
|
||||
|
||||
if let Some(host_offset) = host_offset {
|
||||
op.set_offset(host_offset as libc::off_t);
|
||||
Ok(Some(op))
|
||||
} else {
|
||||
self.inject_operation_completion(op, total_len as i32);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_write_operation_sync(
|
||||
&mut self,
|
||||
op: AsyncIoOperation,
|
||||
) -> Result<(), Box<(AsyncIoOperation, AsyncIoError)>> {
|
||||
// TODO Make writes async.
|
||||
// Writes are synchronous. Async writes require a multi step
|
||||
// state machine for COW (backing read, cluster allocation, data
|
||||
// write, L2 commit) with per request buffer lifetime tracking
|
||||
// and write ordering.
|
||||
let total_len = op.total_len();
|
||||
if let Err(e) = cow_write_sync(
|
||||
op.offset() as u64,
|
||||
&op,
|
||||
&self.metadata,
|
||||
&self.data_file,
|
||||
&self.backing_file,
|
||||
self.cluster_size,
|
||||
) {
|
||||
return Err(Box::new((op, e)));
|
||||
}
|
||||
|
||||
self.inject_operation_completion(op, total_len as i32);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncIo for QcowAsync {
|
||||
fn notifier(&self) -> &EventFd {
|
||||
self.data_io.notifier()
|
||||
}
|
||||
|
||||
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> {
|
||||
if op.is_read() {
|
||||
match self.prepare_read_operation(op) {
|
||||
Ok(Some(op)) => {
|
||||
self.data_io
|
||||
.submit_operation(self.data_file.as_raw_fd(), op)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => return Err(e.1),
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
self.complete_write_operation_sync(op).map_err(|e| e.1)
|
||||
}
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||
self.metadata.flush().map_err(AsyncIoError::Fsync)?;
|
||||
if let Some(user_data) = user_data {
|
||||
self.data_io
|
||||
.inject_completion(AsyncIoCompletion::new(user_data, 0, None));
|
||||
}
|
||||
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<()> {
|
||||
let result = deallocate_range_result(
|
||||
&self.metadata,
|
||||
&mut self.data_file,
|
||||
offset,
|
||||
length as usize,
|
||||
self.sparse,
|
||||
false,
|
||||
self.backing_file.as_deref(),
|
||||
);
|
||||
self.data_io
|
||||
.inject_completion(AsyncIoCompletion::new(user_data, result, None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
||||
let result = deallocate_range_result(
|
||||
&self.metadata,
|
||||
&mut self.data_file,
|
||||
offset,
|
||||
length as usize,
|
||||
self.sparse,
|
||||
true,
|
||||
self.backing_file.as_deref(),
|
||||
);
|
||||
self.data_io
|
||||
.inject_completion(AsyncIoCompletion::new(user_data, result, None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn batch_requests_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn submit_batch_requests(&mut self, batch_request: Vec<AsyncIoOperation>) -> AsyncIoResult<()> {
|
||||
let mut async_reads = Vec::new();
|
||||
|
||||
for op in batch_request {
|
||||
if op.is_read() {
|
||||
match self.prepare_read_operation(op) {
|
||||
Ok(Some(op)) => async_reads.push(op),
|
||||
Ok(None) => {}
|
||||
Err(boxed) => {
|
||||
let (op, e) = *boxed;
|
||||
// The operation was not submitted to the kernel. Accept
|
||||
// it at the qcow layer and surface the failure through
|
||||
// the common completion path so batch acceptance remains
|
||||
// all-or-none for the virtqueue.
|
||||
let result = Self::async_error_result(&e);
|
||||
self.inject_operation_completion(op, result);
|
||||
}
|
||||
}
|
||||
} else if let Err(boxed) = self.complete_write_operation_sync(op) {
|
||||
let (op, e) = *boxed;
|
||||
let result = Self::async_error_result(&e);
|
||||
self.inject_operation_completion(op, result);
|
||||
}
|
||||
}
|
||||
|
||||
if !async_reads.is_empty() {
|
||||
self.data_io
|
||||
.submit_batch(self.data_file.as_raw_fd(), async_reads)
|
||||
.map_err(AsyncIoError::SubmitBatchRequests)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl QcowAsync {
|
||||
/// Resolves read mappings for a guest read request.
|
||||
///
|
||||
/// Returns `Some(host_offset)` if the entire read falls within a single
|
||||
/// allocated cluster (fast path). Otherwise handles the read
|
||||
/// synchronously via `scatter_read_sync` and returns `None`.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn resolve_read(
|
||||
metadata: &QcowMetadata,
|
||||
data_file: &QcowRawFile,
|
||||
backing_file: &Option<Arc<dyn BackingRead>>,
|
||||
address: u64,
|
||||
op: &mut AsyncIoOperation,
|
||||
total_len: usize,
|
||||
cluster_size: u64,
|
||||
decoder: &dyn Decoder,
|
||||
) -> AsyncIoResult<Option<u64>> {
|
||||
let has_backing = backing_file.is_some();
|
||||
let mappings = metadata
|
||||
.map_clusters_for_read(address, total_len, has_backing)
|
||||
.map_err(AsyncIoError::ReadVectored)?;
|
||||
|
||||
// The fast path returns a host offset so the caller can submit a
|
||||
// single io_uring readv with the original iovecs. This only works
|
||||
// without O_DIRECT because it requires I/O
|
||||
// size and file offset to be multiples of the device sector size.
|
||||
// Guest requests can be smaller (e.g. 512 byte UEFI reads on a
|
||||
// 4096 byte sector device), so O_DIRECT reads fall through to the
|
||||
// alignment aware synchronous path instead.
|
||||
if !data_file.file().is_direct()
|
||||
&& mappings.len() == 1
|
||||
&& let ClusterReadMapping::Allocated {
|
||||
offset: host_offset,
|
||||
length,
|
||||
} = &mappings[0]
|
||||
&& *length as usize == total_len
|
||||
{
|
||||
return Ok(Some(*host_offset));
|
||||
}
|
||||
|
||||
scatter_read_sync(mappings, op, data_file, backing_file, cluster_size, decoder)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::{mem, thread};
|
||||
|
||||
use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap};
|
||||
use vmm_sys_util::tempfile::TempFile;
|
||||
|
||||
use super::*;
|
||||
use crate::SECTOR_SIZE;
|
||||
use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, GuestMemoryTarget, OwnedIoBuffer};
|
||||
use crate::disk_file::AsyncDiskFile;
|
||||
use crate::formats::qcow::common::unit_tests::compress_allocated_clusters;
|
||||
use crate::formats::qcow::{BackingFileConfig, ImageType, QcowDisk, QcowTempDisk};
|
||||
|
||||
fn create_disk_with_data(
|
||||
file_size: u64,
|
||||
data: &[u8],
|
||||
offset: u64,
|
||||
sparse: bool,
|
||||
) -> (TempFile, QcowDisk) {
|
||||
let temp_file = if data.is_empty() {
|
||||
QcowTempDisk::new(file_size, None, false, sparse, true)
|
||||
.unwrap()
|
||||
.into_tempfile()
|
||||
} else {
|
||||
let tmp_disk = QcowTempDisk::new(file_size, None, false, sparse, true).unwrap();
|
||||
tmp_disk.disk().write_all_at(offset, data);
|
||||
tmp_disk.into_tempfile()
|
||||
};
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
sparse,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
(temp_file, disk)
|
||||
}
|
||||
|
||||
fn create_overlay_disk_with_raw_backing_pattern(
|
||||
file_size: u64,
|
||||
value: u8,
|
||||
) -> (TempFile, TempFile, QcowDisk) {
|
||||
let backing_temp = TempFile::new().unwrap();
|
||||
let backing_data = vec![value; file_size as usize];
|
||||
backing_temp.as_file().write_all(&backing_data).unwrap();
|
||||
backing_temp.as_file().sync_all().unwrap();
|
||||
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
||||
|
||||
let backing_config = BackingFileConfig {
|
||||
path: backing_path,
|
||||
format: Some(ImageType::Raw),
|
||||
};
|
||||
let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
|
||||
let disk = QcowDisk::new(
|
||||
overlay_temp.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(backing_temp, overlay_temp, disk)
|
||||
}
|
||||
|
||||
fn wait_for_completion(async_io: &mut dyn AsyncIo) -> AsyncIoCompletion {
|
||||
loop {
|
||||
if let Some(c) = async_io.next_completed_request() {
|
||||
return c;
|
||||
}
|
||||
// Block until the eventfd is signaled (io_uring or synthetic).
|
||||
let fd = async_io.notifier().as_raw_fd();
|
||||
let mut val = 0u64;
|
||||
// SAFETY: reading 8 bytes from a valid eventfd.
|
||||
unsafe {
|
||||
libc::read(fd, (&raw mut val).cast(), 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_tuple(completion: &AsyncIoCompletion) -> (u64, i32) {
|
||||
(completion.user_data, completion.result)
|
||||
}
|
||||
|
||||
fn async_write(disk: &QcowDisk, offset: u64, data: &[u8]) {
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io
|
||||
.write_from_vec(
|
||||
offset as libc::off_t,
|
||||
OwnedIoBuffer::from_vec(data.to_vec()),
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
let completion = wait_for_completion(async_io.as_mut());
|
||||
let (user_data, result) = completion_tuple(&completion);
|
||||
assert_eq!(user_data, 2);
|
||||
assert_eq!(
|
||||
result as usize,
|
||||
data.len(),
|
||||
"write should return requested length"
|
||||
);
|
||||
}
|
||||
|
||||
fn async_read(disk: &QcowDisk, offset: u64, len: usize) -> Vec<u8> {
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io
|
||||
.read_to_vec(
|
||||
offset as libc::off_t,
|
||||
OwnedIoBuffer::from_vec(vec![0xFF; len]),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
let mut completion = wait_for_completion(async_io.as_mut());
|
||||
let (user_data, result) = completion_tuple(&completion);
|
||||
assert_eq!(user_data, 1);
|
||||
assert_eq!(result as usize, len, "read should return requested length");
|
||||
match completion.buffer.take() {
|
||||
Some(buffer) => buffer.as_slice().to_vec(),
|
||||
other => panic!("unexpected read completion: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_punch_hole_completion() {
|
||||
let data = vec![0xDD; 128 * 1024];
|
||||
let offset = 0u64;
|
||||
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
||||
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io.punch_hole(offset, data.len() as u64, 100).unwrap();
|
||||
let completion = async_io.next_completed_request().unwrap();
|
||||
let (user_data, result) = completion_tuple(&completion);
|
||||
assert_eq!(user_data, 100);
|
||||
assert_eq!(result, 0, "punch_hole should succeed");
|
||||
drop(async_io);
|
||||
|
||||
let read_buf = async_read(&disk, offset, data.len());
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0),
|
||||
"Punched hole should read as zeros"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_write_zeroes_completion() {
|
||||
let data = vec![0xAA; 128 * 1024];
|
||||
let offset = 0u64;
|
||||
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
||||
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io
|
||||
.write_zeroes(offset, data.len() as u64, 200)
|
||||
.unwrap();
|
||||
let completion = async_io.next_completed_request().unwrap();
|
||||
let (user_data, result) = completion_tuple(&completion);
|
||||
assert_eq!(user_data, 200);
|
||||
assert_eq!(result, 0, "write_zeroes should succeed");
|
||||
drop(async_io);
|
||||
|
||||
let read_buf = async_read(&disk, offset, data.len());
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0),
|
||||
"Write zeroes region should read as zeros"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_write_zeroes_unallocated_overlay_with_backing_must_read_zero() {
|
||||
let cluster_size = 1u64 << 16;
|
||||
let file_size = cluster_size * 4;
|
||||
let offset = cluster_size;
|
||||
let (_backing_temp, _overlay_temp, disk) =
|
||||
create_overlay_disk_with_raw_backing_pattern(file_size, 0xAB);
|
||||
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io.write_zeroes(offset, cluster_size, 201).unwrap();
|
||||
let completion = wait_for_completion(async_io.as_mut());
|
||||
let (user_data, result) = completion_tuple(&completion);
|
||||
assert_eq!(user_data, 201);
|
||||
assert_eq!(result, 0, "write_zeroes should succeed");
|
||||
drop(async_io);
|
||||
|
||||
let read_buf = async_read(&disk, offset, cluster_size as usize);
|
||||
assert!(
|
||||
read_buf.iter().all(|&b| b == 0),
|
||||
"zeroed unallocated overlay cluster exposed backing data"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_write_read_roundtrip() {
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let temp_file = QcowTempDisk::new(file_size, None, false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let pattern: Vec<u8> = (0..128 * 1024).map(|i| (i % 251) as u8).collect();
|
||||
let offset = 64 * 1024;
|
||||
|
||||
async_write(&disk, offset, &pattern);
|
||||
let read_buf = async_read(&disk, offset, pattern.len());
|
||||
assert_eq!(read_buf, pattern, "read should match written data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_read_spanning_cluster_boundary() {
|
||||
let cluster_size: u64 = 65536;
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
|
||||
// Write distinct patterns into two adjacent clusters.
|
||||
let pattern_a = vec![0xAA; cluster_size as usize];
|
||||
let pattern_b = vec![0xBB; cluster_size as usize];
|
||||
let (_temp, disk) = create_disk_with_data(file_size, &pattern_a, 0, true);
|
||||
async_write(&disk, cluster_size, &pattern_b);
|
||||
|
||||
// Read across the boundary: last 4K of cluster 0 + first 4K of cluster 1.
|
||||
let read_offset = cluster_size - 4096;
|
||||
let read_len = 8192;
|
||||
let buf = async_read(&disk, read_offset, read_len);
|
||||
|
||||
assert!(
|
||||
buf[..4096].iter().all(|&b| b == 0xAA),
|
||||
"first half should come from cluster 0"
|
||||
);
|
||||
assert!(
|
||||
buf[4096..].iter().all(|&b| b == 0xBB),
|
||||
"second half should come from cluster 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_sync_read_to_guest_memory() {
|
||||
let cluster_size = 65536usize;
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let data: Vec<u8> = (0..cluster_size * 2).map(|i| (i % 251) as u8).collect();
|
||||
let (_temp, disk) = create_disk_with_data(file_size, &data, 0, true);
|
||||
let mem = Arc::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0x1000), 0x4000)]).unwrap(),
|
||||
);
|
||||
let ranges = [(GuestAddress(0x1000), 2048), (GuestAddress(0x2000), 2048)];
|
||||
let target = GuestMemoryTarget::new(Arc::clone(&mem), &ranges).unwrap();
|
||||
let read_offset = cluster_size as u64 - 2048;
|
||||
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io
|
||||
.read_to_memory(read_offset as libc::off_t, target, 55)
|
||||
.unwrap();
|
||||
|
||||
let completion = wait_for_completion(async_io.as_mut());
|
||||
assert_eq!(completion_tuple(&completion), (55, 4096));
|
||||
assert!(completion.buffer.is_none());
|
||||
|
||||
let mut first = vec![0u8; 2048];
|
||||
let mut second = vec![0u8; 2048];
|
||||
mem.read_slice(&mut first, GuestAddress(0x1000)).unwrap();
|
||||
mem.read_slice(&mut second, GuestAddress(0x2000)).unwrap();
|
||||
|
||||
let expected = &data[read_offset as usize..read_offset as usize + 4096];
|
||||
assert_eq!(&first[..], &expected[..2048]);
|
||||
assert_eq!(&second[..], &expected[2048..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_sync_write_from_guest_memory() {
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let temp_file = QcowTempDisk::new(file_size, None, false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let mem = Arc::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0x1000), 0x4000)]).unwrap(),
|
||||
);
|
||||
let first = vec![0x5a; 2048];
|
||||
let second = vec![0xc3; 2048];
|
||||
mem.write_slice(&first, GuestAddress(0x1000)).unwrap();
|
||||
mem.write_slice(&second, GuestAddress(0x2000)).unwrap();
|
||||
let ranges = [(GuestAddress(0x1000), 2048), (GuestAddress(0x2000), 2048)];
|
||||
let target = GuestMemoryTarget::new(Arc::clone(&mem), &ranges).unwrap();
|
||||
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io.write_from_memory(4096, target, 56).unwrap();
|
||||
let completion = wait_for_completion(async_io.as_mut());
|
||||
assert_eq!(completion_tuple(&completion), (56, 4096));
|
||||
drop(async_io);
|
||||
|
||||
let mut expected = first;
|
||||
expected.extend_from_slice(&second);
|
||||
let read_buf = async_read(&disk, 4096, expected.len());
|
||||
assert_eq!(read_buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_batch_mixed_requests() {
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let temp_file = QcowTempDisk::new(file_size, None, false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut async_io = disk.create_async_io(8).unwrap();
|
||||
|
||||
// Prepare write data for two regions.
|
||||
let write_a = vec![0xAA; 4096];
|
||||
let write_b = vec![0xBB; 4096];
|
||||
let offset_a: u64 = 0;
|
||||
let offset_b: u64 = 65536;
|
||||
|
||||
let batch = vec![
|
||||
AsyncIoOperation::write_from_vec(
|
||||
offset_a as libc::off_t,
|
||||
OwnedIoBuffer::from_vec(write_a.clone()),
|
||||
10,
|
||||
),
|
||||
AsyncIoOperation::write_from_vec(
|
||||
offset_b as libc::off_t,
|
||||
OwnedIoBuffer::from_vec(write_b.clone()),
|
||||
20,
|
||||
),
|
||||
];
|
||||
|
||||
async_io.submit_batch_requests(batch).unwrap();
|
||||
|
||||
let mut completions = [
|
||||
completion_tuple(&wait_for_completion(async_io.as_mut())),
|
||||
completion_tuple(&wait_for_completion(async_io.as_mut())),
|
||||
];
|
||||
completions.sort_by_key(|c| c.0);
|
||||
assert_eq!(completions[0], (10, 4096));
|
||||
assert_eq!(completions[1], (20, 4096));
|
||||
drop(async_io);
|
||||
|
||||
// Batch read both regions back.
|
||||
let mut async_io = disk.create_async_io(8).unwrap();
|
||||
let read_batch = vec![
|
||||
AsyncIoOperation::read_to_vec(
|
||||
offset_a as libc::off_t,
|
||||
OwnedIoBuffer::from_vec(vec![0; 4096]),
|
||||
30,
|
||||
),
|
||||
AsyncIoOperation::read_to_vec(
|
||||
offset_b as libc::off_t,
|
||||
OwnedIoBuffer::from_vec(vec![0; 4096]),
|
||||
40,
|
||||
),
|
||||
];
|
||||
|
||||
async_io.submit_batch_requests(read_batch).unwrap();
|
||||
|
||||
let mut completion_a = wait_for_completion(async_io.as_mut());
|
||||
let mut completion_b = wait_for_completion(async_io.as_mut());
|
||||
if completion_a.user_data > completion_b.user_data {
|
||||
mem::swap(&mut completion_a, &mut completion_b);
|
||||
}
|
||||
assert_eq!(completion_tuple(&completion_a), (30, 4096));
|
||||
assert_eq!(completion_tuple(&completion_b), (40, 4096));
|
||||
|
||||
let read_a = match completion_a.buffer.take() {
|
||||
Some(buffer) => buffer.as_slice().to_vec(),
|
||||
other => panic!("unexpected read completion A: {other:?}"),
|
||||
};
|
||||
let read_b = match completion_b.buffer.take() {
|
||||
Some(buffer) => buffer.as_slice().to_vec(),
|
||||
other => panic!("unexpected read completion B: {other:?}"),
|
||||
};
|
||||
assert_eq!(read_a, write_a, "batch read A should match written data");
|
||||
assert_eq!(read_b, write_b, "batch read B should match written data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_read_unallocated() {
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let temp_file = QcowTempDisk::new(file_size, None, false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let buf = async_read(&disk, 0, 128 * 1024);
|
||||
assert!(
|
||||
buf.iter().all(|&b| b == 0),
|
||||
"unallocated region should read as zeroes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_sub_cluster_write() {
|
||||
let cluster_size = 65536usize;
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let temp_file = QcowTempDisk::new(file_size, None, false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write 4K into the middle of a cluster.
|
||||
let write_offset = 4096u64;
|
||||
let write_len = 4096;
|
||||
let pattern = vec![0xCC; write_len];
|
||||
async_write(&disk, write_offset, &pattern);
|
||||
|
||||
// Read the entire cluster back.
|
||||
let buf = async_read(&disk, 0, cluster_size);
|
||||
|
||||
assert!(
|
||||
buf[..write_offset as usize].iter().all(|&b| b == 0),
|
||||
"bytes before the write should be zero"
|
||||
);
|
||||
assert_eq!(
|
||||
&buf[write_offset as usize..write_offset as usize + write_len],
|
||||
&pattern[..],
|
||||
"written region should match"
|
||||
);
|
||||
assert!(
|
||||
buf[write_offset as usize + write_len..]
|
||||
.iter()
|
||||
.all(|&b| b == 0),
|
||||
"bytes after the write should be zero"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_write_after_punch_hole() {
|
||||
let data = vec![0xAA; 64 * 1024];
|
||||
let offset = 0u64;
|
||||
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
||||
|
||||
let buf = async_read(&disk, offset, data.len());
|
||||
assert!(buf.iter().all(|&b| b == 0xAA));
|
||||
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io.punch_hole(offset, data.len() as u64, 10).unwrap();
|
||||
let result = wait_for_completion(async_io.as_mut()).result;
|
||||
assert_eq!(result, 0);
|
||||
drop(async_io);
|
||||
|
||||
let buf = async_read(&disk, offset, data.len());
|
||||
assert!(
|
||||
buf.iter().all(|&b| b == 0),
|
||||
"should be zero after punch hole"
|
||||
);
|
||||
|
||||
let new_data = vec![0xBB; 64 * 1024];
|
||||
async_write(&disk, offset, &new_data);
|
||||
|
||||
let buf = async_read(&disk, offset, new_data.len());
|
||||
assert_eq!(buf, new_data, "should read new data after rewrite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_large_sequential_io() {
|
||||
let cluster_size = 64 * 1024;
|
||||
let num_clusters = 8;
|
||||
let total_len = cluster_size * num_clusters;
|
||||
let offset = 0u64;
|
||||
|
||||
let mut data = vec![0u8; total_len];
|
||||
for (i, chunk) in data.chunks_mut(cluster_size).enumerate() {
|
||||
chunk.fill((i + 1) as u8);
|
||||
}
|
||||
|
||||
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
||||
|
||||
let buf = async_read(&disk, offset, total_len);
|
||||
assert_eq!(buf.len(), total_len);
|
||||
for (i, chunk) in buf.chunks(cluster_size).enumerate() {
|
||||
assert!(
|
||||
chunk.iter().all(|&b| b == (i + 1) as u8),
|
||||
"cluster {i} mismatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_alignment_without_direct_io() {
|
||||
let file_size = 100 * 1024 * 1024;
|
||||
let temp_file = QcowTempDisk::new(file_size, None, false, true, false)
|
||||
.unwrap()
|
||||
.into_tempfile();
|
||||
let disk = QcowDisk::new(
|
||||
temp_file.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let async_io = disk.create_async_io(1).unwrap();
|
||||
assert_eq!(async_io.alignment(), SECTOR_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_alignment_with_direct_io() {
|
||||
let tmp_disk = match QcowTempDisk::new(100 * 1024 * 1024, None, true, true, true) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
eprintln!("skipping: O_DIRECT not supported on this filesystem");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let async_io = tmp_disk.disk().create_async_io(1).unwrap();
|
||||
assert!(async_io.alignment() >= SECTOR_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_sub_sector_read_with_direct_io() {
|
||||
let tmp_disk = match QcowTempDisk::new(100 * 1024 * 1024, None, true, true, true) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
eprintln!("skipping: O_DIRECT not supported on this filesystem");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let pattern = vec![0xAB; 65536];
|
||||
async_write(tmp_disk.disk(), 0, &pattern);
|
||||
|
||||
let buf = async_read(tmp_disk.disk(), 0, 512);
|
||||
assert!(
|
||||
buf.iter().all(|&b| b == 0xAB),
|
||||
"sub-sector O_DIRECT read should return written data"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qcow_async_direct_io_write_read_roundtrip() {
|
||||
let tmp_disk = match QcowTempDisk::new(100 * 1024 * 1024, None, true, true, true) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
eprintln!("skipping: O_DIRECT not supported on this filesystem");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let pattern: Vec<u8> = (0..128 * 1024).map(|i| (i % 251) as u8).collect();
|
||||
async_write(tmp_disk.disk(), 0, &pattern);
|
||||
|
||||
let buf = async_read(tmp_disk.disk(), 0, pattern.len());
|
||||
assert_eq!(buf, pattern, "O_DIRECT roundtrip should match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compressed_read_multi_queue() {
|
||||
let cluster_size = 65536usize;
|
||||
let data: Vec<u8> = (0..=255).cycle().take(cluster_size).collect();
|
||||
let (temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, 0, false);
|
||||
drop(disk);
|
||||
|
||||
compress_allocated_clusters(&mut temp.as_file().try_clone().unwrap());
|
||||
|
||||
let disk = Arc::new(
|
||||
QcowDisk::new(
|
||||
temp.as_file().try_clone().unwrap(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let handles: Vec<_> = (0..4)
|
||||
.map(|_| {
|
||||
let disk = Arc::clone(&disk);
|
||||
let expected = data.clone();
|
||||
thread::spawn(move || {
|
||||
let mut async_io = disk.create_async_io(1).unwrap();
|
||||
async_io
|
||||
.read_to_vec(0, OwnedIoBuffer::from_vec(vec![0xFF; cluster_size]), 1)
|
||||
.unwrap();
|
||||
let mut completion = wait_for_completion(async_io.as_mut());
|
||||
let result = completion.result;
|
||||
assert_eq!(result as usize, cluster_size);
|
||||
let buf = match completion.buffer.take() {
|
||||
Some(buffer) => buffer.as_slice().to_vec(),
|
||||
other => panic!("unexpected read completion: {other:?}"),
|
||||
};
|
||||
assert_eq!(buf, expected);
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,452 +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 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::MetadataSync for QcowDisk {
|
||||
fn sync_metadata(&self) -> BlockResult<()> {
|
||||
self.metadata
|
||||
.flush()
|
||||
.map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::SyncMetadata(e)))
|
||||
}
|
||||
}
|
||||
|
||||
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 std::os::unix::fs::FileExt;
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fn dirty_bit_is_set(file: &File) -> bool {
|
||||
let mut buf = [0u8; 8];
|
||||
file.read_exact_at(&mut buf, header::V2_BARE_HEADER_SIZE as u64)
|
||||
.unwrap();
|
||||
u64::from_be_bytes(buf) & IncompatFeatures::DIRTY.bits() != 0
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_clone_does_not_clear_dirty_bit() {
|
||||
let file = make_qcow_file();
|
||||
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
|
||||
let cloned = disk.try_clone().unwrap();
|
||||
|
||||
drop(cloned);
|
||||
|
||||
assert_ne!(
|
||||
disk.metadata().header().incompatible_features & IncompatFeatures::DIRTY.bits(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_io_clears_dirty_bit_when_last_metadata_owner_drops() {
|
||||
let file = make_qcow_file();
|
||||
let inspect = file.try_clone().unwrap();
|
||||
let disk = QcowDisk::new(file, false, false, true, false).unwrap();
|
||||
let async_io = disk.create_async_io(1).unwrap();
|
||||
|
||||
drop(disk);
|
||||
assert!(dirty_bit_is_set(&inspect));
|
||||
|
||||
drop(async_io);
|
||||
assert!(!dirty_bit_is_set(&inspect));
|
||||
}
|
||||
|
||||
#[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
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,149 +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, 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, &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(&mut self.raw_file, 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(&mut self.raw_file, 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);
|
||||
}
|
||||
}
|
||||
@@ -1,135 +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::io;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, CompletionCommon,
|
||||
};
|
||||
use crate::sparse::{punch_hole, write_zeroes};
|
||||
use crate::{AlignedFile, is_block_device};
|
||||
|
||||
pub(crate) struct RawSync {
|
||||
raw_file: AlignedFile,
|
||||
completions: CompletionCommon,
|
||||
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,
|
||||
completions: CompletionCommon::new(),
|
||||
alignment,
|
||||
is_block_device,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncIo for RawSync {
|
||||
fn notifier(&self) -> &EventFd {
|
||||
self.completions.notifier()
|
||||
}
|
||||
|
||||
fn alignment(&self) -> u64 {
|
||||
self.alignment
|
||||
}
|
||||
|
||||
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> {
|
||||
let is_read = op.is_read();
|
||||
let iovecs = op.iovecs();
|
||||
let offset = op.offset() as u64;
|
||||
|
||||
let result = if is_read {
|
||||
// SAFETY: op.iovecs() describes valid memory for iov_len bytes by
|
||||
// construction of AsyncIoOperation.
|
||||
unsafe { self.raw_file.read_vectored_at(iovecs, offset) }
|
||||
.map_err(AsyncIoError::ReadVectored)?
|
||||
} else {
|
||||
// SAFETY: op.iovecs() describes valid memory for iov_len bytes by
|
||||
// construction of AsyncIoOperation.
|
||||
unsafe { self.raw_file.write_vectored_at(iovecs, offset) }
|
||||
.map_err(AsyncIoError::WriteVectored)?
|
||||
} as i32;
|
||||
self.completions
|
||||
.complete(AsyncIoCompletion::from_operation(op, result));
|
||||
|
||||
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.completions
|
||||
.complete(AsyncIoCompletion::new(user_data, result, None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
|
||||
self.completions.next_completed()
|
||||
}
|
||||
|
||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
||||
punch_hole(&mut self.raw_file, self.is_block_device, offset, length)
|
||||
.map_err(AsyncIoError::PunchHole)?;
|
||||
self.completions
|
||||
.complete(AsyncIoCompletion::new(user_data, 0, None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
||||
write_zeroes(&mut self.raw_file, self.is_block_device, offset, length)
|
||||
.map_err(AsyncIoError::WriteZeroes)?;
|
||||
self.completions
|
||||
.complete(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 = 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);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +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 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::{punch_hole, write_zeroes};
|
||||
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, 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, &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 op in batch_request {
|
||||
if operation_is_aligned(&op, self.alignment) {
|
||||
aligned_batch.push(op);
|
||||
} else {
|
||||
let result = run_unaligned_operation(&self.raw_file, &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<()> {
|
||||
// Run synchronously rather than submitting a fallocate request through
|
||||
// the ring. This avoids reaping ENOTSUPP in the completion routine and
|
||||
// reissuing the request, and lets the sparse helper handle the ioctl
|
||||
// path for block devices and the write fallback for unsupported
|
||||
// filesystems.
|
||||
punch_hole(&mut self.raw_file, self.is_block_device, 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.
|
||||
self.data_io
|
||||
.submit_nop(user_data)
|
||||
.map_err(AsyncIoError::PunchHole)
|
||||
}
|
||||
|
||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
||||
// Same rationale as punch_hole().
|
||||
write_zeroes(&mut self.raw_file, self.is_block_device, offset, length)
|
||||
.map_err(AsyncIoError::WriteZeroes)?;
|
||||
self.data_io
|
||||
.submit_nop(user_data)
|
||||
.map_err(AsyncIoError::WriteZeroes)
|
||||
}
|
||||
}
|
||||
@@ -1,323 +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::MetadataSync for RawDisk {}
|
||||
|
||||
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: &AsyncIoOperation,
|
||||
) -> AsyncIoResult<i32> {
|
||||
let offset = op.offset() as u64;
|
||||
let iovecs = op.iovecs();
|
||||
|
||||
let n = if op.is_read() {
|
||||
// SAFETY: op.iovecs() describes valid memory for iov_len bytes by
|
||||
// construction of AsyncIoOperation.
|
||||
unsafe { aligned_file.read_vectored_at(iovecs, offset) }
|
||||
.map_err(AsyncIoError::ReadVectored)?
|
||||
} else {
|
||||
// SAFETY: op.iovecs() describes valid memory for iov_len bytes by
|
||||
// construction of AsyncIoOperation.
|
||||
unsafe { aligned_file.write_vectored_at(iovecs, offset) }
|
||||
.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());
|
||||
}
|
||||
}
|
||||
@@ -1,158 +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;
|
||||
|
||||
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.
|
||||
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;
|
||||
async_io.write_zeroes(offset, length, 2).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));
|
||||
}
|
||||
@@ -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",
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,334 +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::MetadataSync for VhdDisk {}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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, CompletionCommon,
|
||||
};
|
||||
use crate::formats::vhdx::Vhdx;
|
||||
|
||||
pub(super) struct VhdxSync {
|
||||
vhdx_file: Arc<Mutex<Vhdx>>,
|
||||
completions: CompletionCommon,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl VhdxSync {
|
||||
pub(super) fn new(vhdx_file: Arc<Mutex<Vhdx>>, size: u64) -> Self {
|
||||
VhdxSync {
|
||||
vhdx_file,
|
||||
completions: CompletionCommon::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.completions.notifier()
|
||||
}
|
||||
|
||||
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.completions
|
||||
.complete(AsyncIoCompletion::from_operation(op, result as i32));
|
||||
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.completions
|
||||
.complete(AsyncIoCompletion::new(user_data, 0, None));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
|
||||
self.completions.next_completed()
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,311 +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;
|
||||
|
||||
#[sorted]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VhdxIoError {
|
||||
#[error("Invalid BAT entry state")]
|
||||
InvalidBatEntryState,
|
||||
#[error("Invalid BAT entry count")]
|
||||
InvalidBatIndex,
|
||||
#[error("Buffer length does not match the requested sector count")]
|
||||
InvalidBufferLength,
|
||||
#[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 expected_len = sector_count
|
||||
.checked_mul(disk_spec.logical_sector_size as u64)
|
||||
.ok_or(VhdxIoError::InvalidBufferLength)?;
|
||||
if buf.len() as u64 != expected_len {
|
||||
return Err(VhdxIoError::InvalidBufferLength);
|
||||
}
|
||||
|
||||
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_bytes 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 expected_len = sector_count
|
||||
.checked_mul(disk_spec.logical_sector_size as u64)
|
||||
.ok_or(VhdxIoError::InvalidBufferLength)?;
|
||||
if buf.len() as u64 != expected_len {
|
||||
return Err(VhdxIoError::InvalidBufferLength);
|
||||
}
|
||||
|
||||
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_bytes 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_bytes 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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vmm_sys_util::tempfile::TempFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
// 512 is the only sector size read/write allowed by metadata::parse_metadata.
|
||||
// [MS-VHDX] allows 4096, but it's not currently implemented
|
||||
const SECTOR_SIZE: u64 = 512;
|
||||
|
||||
// The first BLOCK_SIZE_MIN bytes of a VHDx file are always headers, so
|
||||
// write() treats a file offset below BLOCK_SIZE_MIN as malformed and skips
|
||||
// the writing operation.
|
||||
// Use a DATA_OFFSET greater than BLOCK_SIZE_MIN to bypass that early exit.
|
||||
const DATA_OFFSET: u64 = 2 * metadata::BLOCK_SIZE_MIN as u64;
|
||||
|
||||
fn fixture() -> (AlignedFile, DiskSpec, Vec<BatEntry>) {
|
||||
let disk_spec = DiskSpec {
|
||||
// One block == one sector, so there's exactly one BAT entry.
|
||||
sectors_per_block: 1,
|
||||
logical_sector_size: SECTOR_SIZE as u32,
|
||||
virtual_disk_size: SECTOR_SIZE,
|
||||
image_size: DATA_OFFSET + SECTOR_SIZE,
|
||||
block_size: SECTOR_SIZE as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let file = TempFile::new().unwrap().into_file();
|
||||
file.set_len(DATA_OFFSET + SECTOR_SIZE).unwrap();
|
||||
file.write_all_at(&vec![0xABu8; SECTOR_SIZE as usize], DATA_OFFSET)
|
||||
.unwrap();
|
||||
// A BAT entry saying "this block's data is already written to the
|
||||
// file at `file_offset`".
|
||||
let bat = vec![BatEntry(DATA_OFFSET | bat::PAYLOAD_BLOCK_FULLY_PRESENT)];
|
||||
(AlignedFile::new(file, false), disk_spec, bat)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_sector() {
|
||||
let (f, disk_spec, bat) = fixture();
|
||||
|
||||
let mut buf = vec![0u8; SECTOR_SIZE as usize];
|
||||
let n = read(&f, &mut buf, &disk_spec, &bat, 0, 1).unwrap();
|
||||
|
||||
assert_eq!(n, SECTOR_SIZE as usize);
|
||||
assert!(buf.iter().all(|&b| b == 0xAB));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_sector() {
|
||||
let (f, mut disk_spec, mut bat) = fixture();
|
||||
|
||||
let data = vec![0xCDu8; SECTOR_SIZE as usize];
|
||||
let n = write(&f, &data, &mut disk_spec, 0, &mut bat, 0, 1).unwrap();
|
||||
assert_eq!(n, SECTOR_SIZE as usize);
|
||||
|
||||
let mut readback = vec![0u8; SECTOR_SIZE as usize];
|
||||
f.file().read_exact_at(&mut readback, DATA_OFFSET).unwrap();
|
||||
assert_eq!(readback, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_short_buffer_is_rejected() {
|
||||
let (f, disk_spec, bat) = fixture();
|
||||
|
||||
let mut buf = vec![0u8; SECTOR_SIZE as usize - 1];
|
||||
let err = read(&f, &mut buf, &disk_spec, &bat, 0, 1).unwrap_err();
|
||||
|
||||
assert!(matches!(err, VhdxIoError::InvalidBufferLength));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_short_buffer_is_rejected() {
|
||||
let (f, mut disk_spec, mut bat) = fixture();
|
||||
|
||||
let data = vec![0xCDu8; SECTOR_SIZE as usize - 1];
|
||||
let err = write(&f, &data, &mut disk_spec, 0, &mut bat, 0, 1).unwrap_err();
|
||||
|
||||
assert!(matches!(err, VhdxIoError::InvalidBufferLength));
|
||||
}
|
||||
}
|
||||
@@ -1,120 +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::MetadataSync for VhdxDisk {}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
@@ -1,366 +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_size = self.disk_spec.logical_sector_size as u64;
|
||||
if !(buf.len() as u64).is_multiple_of(sector_size) {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Read buffer length {} is not a multiple of the {sector_size}-byte logical sector size",
|
||||
buf.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let sector_count = buf.len() as u64 / sector_size;
|
||||
let sector_index = self.current_offset / sector_size;
|
||||
|
||||
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_size = self.disk_spec.logical_sector_size as u64;
|
||||
if !(buf.len() as u64).is_multiple_of(sector_size) {
|
||||
return Err(IoError::new(
|
||||
IoErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Write buffer length {} is not a multiple of the {sector_size}-byte logical sector size",
|
||||
buf.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let sector_count = buf.len() as u64 / sector_size;
|
||||
let sector_index = self.current_offset / sector_size;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_misaligned_buffer_is_rejected() {
|
||||
let Some(tf) = create_dynamic_vhdx(16) else {
|
||||
eprintln!("skipping read_misaligned_buffer_is_rejected: 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();
|
||||
|
||||
let mut buf = vec![0u8; vhdx.disk_spec.logical_sector_size as usize - 1];
|
||||
let err = vhdx.read(&mut buf).unwrap_err();
|
||||
assert_eq!(err.kind(), IoErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_misaligned_buffer_is_rejected() {
|
||||
let Some(tf) = create_dynamic_vhdx(16) else {
|
||||
eprintln!("skipping write_misaligned_buffer_is_rejected: 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();
|
||||
|
||||
let buf = vec![0u8; vhdx.disk_spec.logical_sector_size as usize - 1];
|
||||
let err = vhdx.write(&buf).unwrap_err();
|
||||
assert_eq!(err.kind(), IoErrorKind::InvalidInput);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,531 +0,0 @@
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Parser for the flat VMDK text descriptor.
|
||||
//!
|
||||
//! A flat VMDK stores its layout in a small text descriptor: a header, a list
|
||||
//! of `FLAT` extent lines, and a disk database (DDB). Only the `monolithicFlat`
|
||||
//! and `twoGbMaxExtentFlat` create types are recognized.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::path::Path;
|
||||
use std::str::Lines;
|
||||
|
||||
use crate::AlignedFile;
|
||||
|
||||
const VMDK_DESCRIPTOR_HEADER: &str = "# Disk DescriptorFile";
|
||||
const VMDK_DESCRIPTOR_EXTENTS: &str = "# Extent description";
|
||||
const VMDK_DESCRIPTOR_DDB: &str = "# The Disk Data Base";
|
||||
const VMDK_DESCRIPTOR_DDB_2: &str = "#DDB";
|
||||
|
||||
/// Flat VMDK create types.
|
||||
#[derive(Debug, Default)]
|
||||
pub enum VMDKDiskType {
|
||||
#[default]
|
||||
CreateTypeUnsupported,
|
||||
MonolithicFlat,
|
||||
TwoGbMaxExtentFlat,
|
||||
}
|
||||
|
||||
/// Flat VMDK extent line fields.
|
||||
/// Format of each extent line:
|
||||
/// `<access> <sectors> <type> "<file>" [offset]`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkExtentHeader {
|
||||
pub access: String,
|
||||
pub size_in_sectors: u64,
|
||||
pub extent_type: String,
|
||||
pub filename: String,
|
||||
pub offset_in_sectors: u64,
|
||||
}
|
||||
|
||||
/// Descriptor header fields.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkDescriptorHeader {
|
||||
pub create_type: VMDKDiskType,
|
||||
}
|
||||
|
||||
/// Ordered list of extents.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkDescriptorExtents {
|
||||
pub extents: Vec<VmdkExtentHeader>,
|
||||
}
|
||||
|
||||
/// Parsed flat VMDK descriptor
|
||||
///
|
||||
/// extents_list: ordered extent list
|
||||
/// base_path: descriptor file's parent directory
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkDescriptor {
|
||||
pub base_path: String,
|
||||
pub extents_list: VmdkDescriptorExtents,
|
||||
}
|
||||
|
||||
impl VmdkDescriptor {
|
||||
pub fn new(file: &File, path: &Path) -> io::Result<Self> {
|
||||
// The descriptor's directory anchors the relative extent filenames.
|
||||
let base_path = path
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Cannot retrieve parent directory of the file",
|
||||
)
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Valid descriptor file must be much larger than 4 bytes.
|
||||
if file.metadata()?.len() < 4 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid VMDK descriptor file: file is empty or too small",
|
||||
));
|
||||
}
|
||||
|
||||
let content = read_descriptor(file)?;
|
||||
let mut lines = content.lines();
|
||||
let (_header, last_line) = parse_header(&mut lines)?;
|
||||
let extents_list = parse_extents(&mut lines, last_line)?;
|
||||
|
||||
Ok(Self {
|
||||
base_path,
|
||||
extents_list,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Read the whole descriptor into memory through an `AlignedFile` and return it
|
||||
// as a `String`.
|
||||
fn read_descriptor(file: &File) -> io::Result<String> {
|
||||
let aligned = AlignedFile::new(file.try_clone()?, true);
|
||||
let len = file.metadata()?.len() as usize;
|
||||
let mut buf = vec![0u8; len];
|
||||
let mut filled = 0;
|
||||
while filled < buf.len() {
|
||||
match aligned.read_at(&mut buf[filled..], filled as u64) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => filled += n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
buf.truncate(filled);
|
||||
// A descriptor is ASCII text, so invalid UTF-8 means "not a descriptor".
|
||||
String::from_utf8(buf).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK descriptor is not valid UTF-8",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_header<'a>(
|
||||
lines: &mut Lines<'a>,
|
||||
) -> io::Result<(VmdkDescriptorHeader, &'a str)> {
|
||||
let header_line = lines.next().unwrap_or_default();
|
||||
|
||||
// Reject actual disk data (or an embedded descriptor, which is
|
||||
// unsupported): a flat descriptor must start with the header line.
|
||||
if header_line.trim_end() != VMDK_DESCRIPTOR_HEADER {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Not a VMDK descriptor file: missing header: {header_line}"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut header = VmdkDescriptorHeader::default();
|
||||
let mut last_comment_line = "";
|
||||
|
||||
for line in lines.by_ref() {
|
||||
if line.starts_with('#') {
|
||||
// End of the header section.
|
||||
last_comment_line = line;
|
||||
break;
|
||||
}
|
||||
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
||||
if parts.len() == 2 && parts[0] == "createType" {
|
||||
// Tools such as qemu-img quote the value, strip quotes for comparison.
|
||||
header.create_type = match parts[1].trim_matches('"') {
|
||||
"monolithicFlat" => VMDKDiskType::MonolithicFlat,
|
||||
"twoGbMaxExtentFlat" => VMDKDiskType::TwoGbMaxExtentFlat,
|
||||
_ => VMDKDiskType::CreateTypeUnsupported,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok((header, last_comment_line))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_extents(
|
||||
lines: &mut Lines<'_>,
|
||||
last_comment_line: &str,
|
||||
) -> io::Result<VmdkDescriptorExtents> {
|
||||
let mut extents = VmdkDescriptorExtents::default();
|
||||
|
||||
if last_comment_line != VMDK_DESCRIPTOR_EXTENTS {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Expected the extents section comment line",
|
||||
));
|
||||
}
|
||||
|
||||
for line in lines.by_ref() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if line.starts_with('#') {
|
||||
if line == VMDK_DESCRIPTOR_DDB || line == VMDK_DESCRIPTOR_DDB_2 {
|
||||
break;
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Expected the DDB section comment line",
|
||||
));
|
||||
}
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() == 4 || parts.len() == 5 {
|
||||
let size_in_sectors = parts[1].parse::<u64>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK extent size '{}' is not a valid sector count",
|
||||
parts[1]
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let offset_in_sectors = match parts.get(4) {
|
||||
Some(offset) => offset.parse::<u64>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("VMDK extent offset '{offset}' is not a valid sector count"),
|
||||
)
|
||||
})?,
|
||||
None => 0,
|
||||
};
|
||||
extents.extents.push(VmdkExtentHeader {
|
||||
access: parts[0].to_string(),
|
||||
size_in_sectors,
|
||||
extent_type: parts[2].to_string(),
|
||||
filename: parts[3].trim_matches('"').to_string(),
|
||||
offset_in_sectors,
|
||||
});
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Malformed VMDK extent line",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(extents)
|
||||
}
|
||||
|
||||
/// Returns true when `prefix` begins with the `# Disk DescriptorFile` header.
|
||||
pub fn has_descriptor_header(prefix: &[u8]) -> bool {
|
||||
prefix.starts_with(VMDK_DESCRIPTOR_HEADER.as_bytes())
|
||||
}
|
||||
|
||||
/// Returns true for a supported flat VMDK: create type `monolithicFlat` or
|
||||
/// `twoGbMaxExtentFlat` with only `FLAT` extents.
|
||||
pub fn is_flat_vmdk(f: &mut File) -> io::Result<bool> {
|
||||
let content = match read_descriptor(f) {
|
||||
Ok(content) => content,
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let mut lines = content.lines();
|
||||
|
||||
let (header, last_line) = match parse_header(&mut lines) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
match header.create_type {
|
||||
VMDKDiskType::MonolithicFlat | VMDKDiskType::TwoGbMaxExtentFlat => {}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
let extents = match parse_extents(&mut lines, last_line) {
|
||||
Ok(extents) => extents,
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
for extent in &extents.extents {
|
||||
if extent.extent_type != "FLAT" {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_hdr(input: &str) -> io::Result<(VmdkDescriptorHeader, String)> {
|
||||
let mut lines = input.lines();
|
||||
let (header, last) = parse_header(&mut lines)?;
|
||||
Ok((header, last.to_string()))
|
||||
}
|
||||
|
||||
fn parse_body(last_comment: &str, body: &str) -> io::Result<VmdkDescriptorExtents> {
|
||||
let mut lines = body.lines();
|
||||
parse_extents(&mut lines, last_comment)
|
||||
}
|
||||
|
||||
// Two-stage parse, as `VmdkDescriptor::new` chains it.
|
||||
fn parse_full(input: &str) -> io::Result<(VmdkDescriptorHeader, VmdkDescriptorExtents)> {
|
||||
let mut lines = input.lines();
|
||||
let (header, last) = parse_header(&mut lines)?;
|
||||
let extents = parse_extents(&mut lines, last)?;
|
||||
Ok((header, extents))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_is_unaffected_by_shared_file_offset() {
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use vmm_sys_util::tempfile::TempFile;
|
||||
|
||||
// A minimal but complete monolithicFlat descriptor.
|
||||
let descriptor_text = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
createType=monolithicFlat\n\
|
||||
# Extent description\n\
|
||||
RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"ide\"\n";
|
||||
|
||||
let tmp = TempFile::new().unwrap();
|
||||
let mut file: &File = tmp.as_file();
|
||||
file.write_all(descriptor_text.as_bytes()).unwrap();
|
||||
|
||||
// Advance the shared OS file offset off zero, mimicking an earlier
|
||||
// image-type probe. `&File` is `Copy`, so this moves the same fd's
|
||||
// offset that `VmdkDescriptor::new` will see.
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut scratch = [0u8; 8];
|
||||
file.read_exact(&mut scratch).unwrap();
|
||||
assert_ne!(file.stream_position().unwrap(), 0);
|
||||
|
||||
// `read_descriptor` reads positionally (anchored at offset 0), so the
|
||||
// advanced offset must not affect the parse.
|
||||
let descriptor = VmdkDescriptor::new(file, tmp.as_path()).unwrap();
|
||||
assert_eq!(descriptor.extents_list.extents.len(), 1);
|
||||
assert_eq!(
|
||||
descriptor.extents_list.extents[0].filename,
|
||||
"disk-flat.vmdk"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_flat_extent_with_ddb() {
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"ide\"\n\
|
||||
ddb.geometry.sectors = \"63\"\n";
|
||||
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
let e = &extents.extents[0];
|
||||
assert_eq!(e.access, "RW");
|
||||
assert_eq!(e.size_in_sectors, 2_097_152);
|
||||
assert_eq!(e.extent_type, "FLAT");
|
||||
assert_eq!(e.filename, "disk-flat.vmdk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_extents_two_gb_max() {
|
||||
let body: &str = "RW 4192256 FLAT \"disk-s001.vmdk\"\n\
|
||||
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
||||
RW 2097152 FLAT \"disk-s003.vmdk\"\n\
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"lsilogic\"\n";
|
||||
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 3);
|
||||
assert_eq!(extents.extents[0].filename, "disk-s001.vmdk");
|
||||
assert_eq!(extents.extents[2].filename, "disk-s003.vmdk");
|
||||
assert!(extents.extents.iter().all(|e| e.extent_type == "FLAT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_line_with_optional_offset_field() {
|
||||
// 5-field form: <access> <sectors> <type> <file> <offset>
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\" 0\n";
|
||||
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_access_modes_are_preserved() {
|
||||
let body: &str = "RDONLY 2097152 FLAT \"ro.vmdk\"\n\
|
||||
NOACCESS 1048576 FLAT \"noaccess.vmdk\"\n";
|
||||
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 2);
|
||||
assert_eq!(extents.extents[0].access, "RDONLY");
|
||||
assert_eq!(extents.extents[1].access, "NOACCESS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_leading_comment() {
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n";
|
||||
// Must be told we are at "# Extent description", anything else errors.
|
||||
parse_body("# The Disk Data Base", body).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_extent_line() {
|
||||
// Only three fields -> malformed.
|
||||
let body: &str = "RW 2097152 FLAT\n";
|
||||
parse_body("# Extent description", body).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_numeric_extent_size() {
|
||||
// A non-numeric sector count must be rejected, not coerced to 0.
|
||||
let body: &str = "RW notanumber FLAT \"disk-flat.vmdk\"\n";
|
||||
parse_body("# Extent description", body).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_numeric_extent_offset() {
|
||||
// A non-numeric trailing offset must be rejected too.
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\" xyz\n";
|
||||
parse_body("# Extent description", body).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unexpected_comment_in_body() {
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
# Some other comment\n";
|
||||
parse_body("# Extent description", body).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_blank_line_inside_extent_section() {
|
||||
// qemu-img emits a blank line between the last extent and the
|
||||
// "# The Disk Data Base" marker, it must be skipped, not rejected.
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
\n\
|
||||
# The Disk Data Base\n";
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_descriptor_header() {
|
||||
let input: &str = "NOT_A_DESCRIPTOR\nversion=1\n";
|
||||
parse_hdr(input).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_header_fields() {
|
||||
let input: &str = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
CID=fffffffe\n\
|
||||
parentCID=ffffffff\n\
|
||||
createType=monolithicFlat\n\
|
||||
# Extent description\n";
|
||||
|
||||
let (header, last) = parse_hdr(input).unwrap();
|
||||
|
||||
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||
assert_eq!(last, "# Extent description");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_quoted_create_type() {
|
||||
let input: &str = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
createType=\"twoGbMaxExtentFlat\"\n\
|
||||
# Extent description\n";
|
||||
|
||||
let (header, _last) = parse_hdr(input).unwrap();
|
||||
assert!(matches!(
|
||||
header.create_type,
|
||||
VMDKDiskType::TwoGbMaxExtentFlat
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_monolithic_flat_descriptor() {
|
||||
let input: &str = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
createType=monolithicFlat\n\
|
||||
# Extent description\n\
|
||||
RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"ide\"\n";
|
||||
|
||||
let (header, extents) = parse_full(input).unwrap();
|
||||
|
||||
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].access, "RW");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_two_gb_max_extent_flat_descriptor() {
|
||||
let input: &str = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
createType=twoGbMaxExtentFlat\n\
|
||||
# Extent description\n\
|
||||
RW 4192256 FLAT \"disk-s001.vmdk\"\n\
|
||||
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
||||
# The Disk Data Base\n";
|
||||
|
||||
let (header, extents) = parse_full(input).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
header.create_type,
|
||||
VMDKDiskType::TwoGbMaxExtentFlat
|
||||
));
|
||||
assert_eq!(extents.extents.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_qemu_style_descriptor() {
|
||||
// Mirrors a real qemu-img monolithicFlat descriptor: quoted createType,
|
||||
// blank lines between sections, 5-field extent line with a trailing
|
||||
// offset, and the "#DDB" marker form.
|
||||
let input: &str = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
CID=eb2295a4\n\
|
||||
parentCID=ffffffff\n\
|
||||
createType=\"monolithicFlat\"\n\
|
||||
\n\
|
||||
# Extent description\n\
|
||||
RW 6291456 FLAT \"t-flat.vmdk\" 0\n\
|
||||
\n\
|
||||
# The Disk Data Base\n\
|
||||
#DDB\n\
|
||||
\n\
|
||||
ddb.virtualHWVersion = \"4\"\n\
|
||||
ddb.adapterType = \"ide\"\n";
|
||||
|
||||
let (header, extents) = parse_full(input).unwrap();
|
||||
|
||||
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].access, "RW");
|
||||
assert_eq!(extents.extents[0].size_in_sectors, 6_291_456);
|
||||
assert_eq!(extents.extents[0].extent_type, "FLAT");
|
||||
assert_eq!(extents.extents[0].filename, "t-flat.vmdk");
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::sync::Arc;
|
||||
use std::{cmp, io};
|
||||
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::AlignedFile;
|
||||
use crate::async_io::{
|
||||
AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, CompletionCommon,
|
||||
};
|
||||
use crate::formats::vmdk::flat::{ExtentAccess, VmdkExtent};
|
||||
|
||||
/// Synchronous, extent-aware I/O worker for flat VMDK images.
|
||||
///
|
||||
/// Maps each guest I/O request to one or more backing extents.
|
||||
///
|
||||
/// Async backends (io_uring/AIO) are not supported.
|
||||
pub(crate) struct FlatVmdkSync {
|
||||
extents: Arc<Vec<VmdkExtent>>,
|
||||
size: u64,
|
||||
completions: CompletionCommon,
|
||||
}
|
||||
|
||||
impl FlatVmdkSync {
|
||||
pub fn new(extents: Arc<Vec<VmdkExtent>>, size: u64) -> Self {
|
||||
FlatVmdkSync {
|
||||
extents,
|
||||
size,
|
||||
completions: CompletionCommon::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the extent containing virtual `offset`, or `None` if out of range.
|
||||
fn extent_at(&self, offset: u64) -> Option<&VmdkExtent> {
|
||||
self.extents
|
||||
.iter()
|
||||
.find(|e| offset >= e.virtual_start && offset < e.virtual_start + e.length)
|
||||
}
|
||||
|
||||
fn check_access(&self, start: u64, total: u64, is_read: bool) -> io::Result<()> {
|
||||
let end = start + total;
|
||||
let mut cur = start;
|
||||
while cur < end {
|
||||
let extent = self.extent_at(cur).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent")
|
||||
})?;
|
||||
match extent.access {
|
||||
ExtentAccess::NoAccess => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("VMDK extent at offset {cur} is NOACCESS; request rejected"),
|
||||
));
|
||||
}
|
||||
ExtentAccess::ReadOnly if !is_read => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("write to read-only VMDK extent at offset {cur} rejected"),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
cur = extent.virtual_start + extent.length;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Reads or writes a single contiguous segment of one extent through the
|
||||
// extent's `AlignedFile`.
|
||||
fn segment_io(
|
||||
file: &AlignedFile,
|
||||
file_offset: u64,
|
||||
op: &mut AsyncIoOperation,
|
||||
buf_start: usize,
|
||||
seg_len: usize,
|
||||
is_read: bool,
|
||||
) -> io::Result<usize> {
|
||||
// O_DIRECT unaligned
|
||||
if file.alignment() != 0 {
|
||||
return if is_read {
|
||||
file.read_unaligned(file_offset, seg_len, |data| {
|
||||
op.write_bytes_at(buf_start, data)
|
||||
})
|
||||
} else {
|
||||
file.write_unaligned(file_offset, seg_len, |data| {
|
||||
op.read_bytes_at(buf_start, data)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// Aligned & Buffered
|
||||
let mut buf = vec![0u8; seg_len];
|
||||
let mut done = 0usize;
|
||||
if is_read {
|
||||
while done < seg_len {
|
||||
match file.read_at(&mut buf[done..], file_offset + done as u64) {
|
||||
Ok(0) => break, // EOF: nothing more to read
|
||||
Ok(n) => done += n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
op.write_bytes_at(buf_start, &buf[..done])?;
|
||||
Ok(done)
|
||||
} else {
|
||||
op.read_bytes_at(buf_start, &mut buf)?;
|
||||
while done < seg_len {
|
||||
match file.write_at(&buf[done..], file_offset + done as u64) {
|
||||
Ok(0) => break, // no progress: avoid spinning forever
|
||||
Ok(n) => done += n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(done)
|
||||
}
|
||||
}
|
||||
|
||||
// Single-extent path: the whole request lives in `extent`.
|
||||
//
|
||||
// The guest iovecs are handed to `AlignedFile::{read,write}_vectored_at`
|
||||
fn single_extent_io(
|
||||
&self,
|
||||
extent: &VmdkExtent,
|
||||
op: &mut AsyncIoOperation,
|
||||
) -> io::Result<usize> {
|
||||
let file = extent.file.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"VMDK extent is not accessible",
|
||||
)
|
||||
})?;
|
||||
let file_offset = extent.file_base_offset + (op.offset() as u64 - extent.virtual_start);
|
||||
let iovecs = op.iovecs();
|
||||
|
||||
// SAFETY: the iovec buffers are owned by `op` and remain valid for the
|
||||
// duration of this call.
|
||||
unsafe {
|
||||
if op.is_read() {
|
||||
file.read_vectored_at(iovecs, file_offset)
|
||||
} else {
|
||||
file.write_vectored_at(iovecs, file_offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: the request straddles >= 2 extents.
|
||||
//
|
||||
// A single guest request here maps onto several different backing files,
|
||||
// Every segment goes through `segment_io` regardless of
|
||||
// alignment.
|
||||
fn spanning_io(&self, op: &mut AsyncIoOperation) -> io::Result<usize> {
|
||||
let start = op.offset() as u64;
|
||||
let total = op.total_len() as u64;
|
||||
let is_read = op.is_read();
|
||||
|
||||
let mut done: u64 = 0;
|
||||
while done < total {
|
||||
let cur = start + done;
|
||||
let extent = self.extent_at(cur).ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent")
|
||||
})?;
|
||||
let extent_end = extent.virtual_start + extent.length;
|
||||
// Bytes handled in this extent before reaching its boundary.
|
||||
let seg_len = cmp::min(total - done, extent_end - cur) as usize;
|
||||
let file = extent.file.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"VMDK extent is not accessible",
|
||||
)
|
||||
})?;
|
||||
let file_offset = extent.file_base_offset + (cur - extent.virtual_start);
|
||||
|
||||
let n = Self::segment_io(file, file_offset, op, done as usize, seg_len, is_read)?;
|
||||
done += n as u64;
|
||||
if n < seg_len {
|
||||
break; // short read/write
|
||||
}
|
||||
}
|
||||
|
||||
Ok(done as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncIo for FlatVmdkSync {
|
||||
fn notifier(&self) -> &EventFd {
|
||||
self.completions.notifier()
|
||||
}
|
||||
|
||||
fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> {
|
||||
let start = op.offset() as u64;
|
||||
let total = op.total_len() as u64;
|
||||
let is_read = op.is_read();
|
||||
|
||||
// Bounds check against the virtual disk size (overflow-safe: `start`
|
||||
// is checked before subtracting it from `size`).
|
||||
if start > self.size || total > self.size - start {
|
||||
let error = io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK request [{start}, {}) exceeds virtual size {}",
|
||||
start + total,
|
||||
self.size
|
||||
),
|
||||
);
|
||||
return Err(if is_read {
|
||||
AsyncIoError::ReadVectored(error)
|
||||
} else {
|
||||
AsyncIoError::WriteVectored(error)
|
||||
});
|
||||
}
|
||||
|
||||
// Reject the request up front if any extent it touches forbids it:
|
||||
// NOACCESS extents reject all I/O.
|
||||
if total != 0
|
||||
&& let Err(error) = self.check_access(start, total, is_read)
|
||||
{
|
||||
return Err(if is_read {
|
||||
AsyncIoError::ReadVectored(error)
|
||||
} else {
|
||||
AsyncIoError::WriteVectored(error)
|
||||
});
|
||||
}
|
||||
|
||||
let result = if total == 0 {
|
||||
Ok(0)
|
||||
} else if let Some(extent) = self.extent_at(start) {
|
||||
if start + total <= extent.virtual_start + extent.length {
|
||||
// Entire request fits in one extent
|
||||
self.single_extent_io(extent, &mut op)
|
||||
} else {
|
||||
// Request crosses an extent boundary
|
||||
self.spanning_io(&mut op)
|
||||
}
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"offset outside any VMDK extent",
|
||||
))
|
||||
};
|
||||
|
||||
let bytes = result.map_err(|e| {
|
||||
if is_read {
|
||||
AsyncIoError::ReadVectored(e)
|
||||
} else {
|
||||
AsyncIoError::WriteVectored(e)
|
||||
}
|
||||
})?;
|
||||
|
||||
self.completions
|
||||
.complete(AsyncIoCompletion::from_operation(op, bytes as i32));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
||||
// Flush every extent: a single guest flush must durably persist data
|
||||
// that may have been written across multiple extent files.
|
||||
for extent in self.extents.iter() {
|
||||
// Skip NoAccess extents, which have no open file.
|
||||
if let Some(file) = extent.file.as_ref() {
|
||||
file.sync_all().map_err(AsyncIoError::Fsync)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(user_data) = user_data {
|
||||
self.completions
|
||||
.complete(AsyncIoCompletion::new(user_data, 0, None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
|
||||
self.completions.next_completed()
|
||||
}
|
||||
|
||||
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
||||
// Flat VMDK is not sparse-capable (see `SparseCapable` impl), so this
|
||||
// should never be negotiated by the guest.
|
||||
Err(AsyncIoError::PunchHole(io::Error::other(
|
||||
"punch_hole not supported for flat VMDK",
|
||||
)))
|
||||
}
|
||||
|
||||
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
|
||||
Err(AsyncIoError::WriteZeroes(io::Error::other(
|
||||
"write_zeroes not supported for flat VMDK",
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -1,633 +0,0 @@
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Flat VMDK extent layout: opens the data extents referenced by the
|
||||
//! descriptor and maps the virtual disk onto them.
|
||||
|
||||
use std::ffi::{CString, OsStr};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||
use std::path::{Component, Path};
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::formats::vmdk::descriptor::VmdkDescriptor;
|
||||
use crate::{AlignedFile, DiskTopology, query_device_size};
|
||||
|
||||
const VMDK_SECTOR_SIZE: u64 = 512;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ExtentAccess {
|
||||
/// "RW": readable and writable.
|
||||
ReadWrite,
|
||||
/// "RDONLY": readable only, writes must be rejected.
|
||||
ReadOnly,
|
||||
/// "NOACCESS": cannot be accessed, reads and writes must be rejected.
|
||||
NoAccess,
|
||||
}
|
||||
|
||||
/// A single Flat VMDK extent
|
||||
///
|
||||
/// `twoGbMaxExtentFlat` images concatenate several of these to form the full
|
||||
/// virtual disk, `monolithicFlat` images have exactly one.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct VmdkExtent {
|
||||
/// Open, alignment-aware handle to this extent's data file. `None` for
|
||||
/// `NoAccess` extents, which are never opened because they cannot be
|
||||
/// accessed.
|
||||
///
|
||||
/// The handle is wrapped in an [`AlignedFile`]
|
||||
pub file: Option<AlignedFile>,
|
||||
/// Access mode declared for this extent in the descriptor.
|
||||
pub access: ExtentAccess,
|
||||
/// First virtual-disk offset (in bytes) backed by this extent.
|
||||
pub virtual_start: u64,
|
||||
/// Length (in bytes) of the virtual-disk range backed by this extent.
|
||||
pub length: u64,
|
||||
/// Starting offset (in bytes) within the backing file for this extent.
|
||||
/// Non-zero when several extents reference the same file at growing
|
||||
/// offsets (e.g. a >2GB file split under `twoGbMaxExtentFlat`).
|
||||
pub file_base_offset: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FlatVmdk {
|
||||
descriptor: Arc<VmdkDescriptor>,
|
||||
// Open handle to the VMDK descriptor file.
|
||||
descriptor_file: Arc<File>,
|
||||
// All opened data extents, in virtual-disk order.
|
||||
extents: Arc<Vec<VmdkExtent>>,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct OpenHow {
|
||||
flags: u64,
|
||||
mode: u64,
|
||||
resolve: u64,
|
||||
}
|
||||
|
||||
// Splits an untrusted extent `filename` into its `Normal` path components for
|
||||
// the fallback walk, rejecting any `..`/`.` traversal.
|
||||
fn extent_components(filename: &str) -> io::Result<Vec<&OsStr>> {
|
||||
let mut components = Vec::new();
|
||||
for component in Path::new(filename).components() {
|
||||
match component {
|
||||
Component::Normal(name) => components.push(name),
|
||||
Component::RootDir => {}
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK extent filename '{filename}' must not contain '..' or '.' path \
|
||||
components"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if components.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("VMDK extent filename '{filename}' is empty"),
|
||||
));
|
||||
}
|
||||
Ok(components)
|
||||
}
|
||||
|
||||
// Opens a single VMDK data extent for the descriptor whose directory is
|
||||
// base_path.
|
||||
//
|
||||
// The extent name may be relative to the descriptor or an absolute path. The
|
||||
// only difference between the two is:
|
||||
// - relative -> colocated with descriptor file
|
||||
// - absolute -> the filesystem root
|
||||
// The symlink policy rejects the final component if it is a symlink (O_NOFOLLOW).
|
||||
//
|
||||
// Resolution prefers openat2(2). On kernels without it (< 5.6, ENOSYS) or
|
||||
// where it is blocked (EPERM, e.g. a seccomp filter), it falls back to a
|
||||
// per-component openat walk.
|
||||
fn open_extent(
|
||||
base_path: &str,
|
||||
filename: &str,
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> io::Result<AlignedFile> {
|
||||
let anchor = if Path::new(filename).is_absolute() {
|
||||
"/"
|
||||
} else {
|
||||
base_path
|
||||
};
|
||||
|
||||
let dir = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
|
||||
.open(anchor)?;
|
||||
|
||||
match open_extent_openat2(dir.as_raw_fd(), filename, writable, direct) {
|
||||
Ok(file) => Ok(AlignedFile::new(file, direct)),
|
||||
Err(e) if matches!(e.raw_os_error(), Some(libc::ENOSYS) | Some(libc::EPERM)) => {
|
||||
let components = extent_components(filename)?;
|
||||
open_extent_walk(dir, &components, writable, direct)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_extent_openat2(
|
||||
dir_fd: RawFd,
|
||||
filename: &str,
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> io::Result<File> {
|
||||
let cname = CString::new(filename).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK extent filename contains an interior NUL byte",
|
||||
)
|
||||
})?;
|
||||
|
||||
let access = if writable {
|
||||
libc::O_RDWR
|
||||
} else {
|
||||
libc::O_RDONLY
|
||||
};
|
||||
|
||||
let mut flags = access | libc::O_CLOEXEC | libc::O_NOFOLLOW;
|
||||
if direct {
|
||||
flags |= libc::O_DIRECT;
|
||||
}
|
||||
let how = OpenHow {
|
||||
flags: flags as u64,
|
||||
mode: 0,
|
||||
resolve: 0,
|
||||
};
|
||||
|
||||
// SAFETY: FFI syscall. `cname` is NUL-terminated and outlives the call,
|
||||
// `how` is a correctly sized `open_how` passed by pointer, and `dir_fd` is a
|
||||
// valid directory fd.
|
||||
let ret = unsafe {
|
||||
libc::syscall(
|
||||
libc::SYS_openat2,
|
||||
dir_fd,
|
||||
cname.as_ptr(),
|
||||
&how as *const OpenHow,
|
||||
size_of::<OpenHow>(),
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: `openat2` returned a fresh descriptor we now own exclusively.
|
||||
Ok(unsafe { File::from_raw_fd(ret as RawFd) })
|
||||
}
|
||||
|
||||
fn open_extent_walk(
|
||||
mut dir: File,
|
||||
components: &[&OsStr],
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> io::Result<AlignedFile> {
|
||||
let last = components.len() - 1;
|
||||
for (i, name) in components.iter().enumerate() {
|
||||
let cname = CString::new(name.as_bytes()).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK extent filename contains an interior NUL byte",
|
||||
)
|
||||
})?;
|
||||
|
||||
let flags = if i < last {
|
||||
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC
|
||||
} else {
|
||||
// Final component: the extent file, opened with the declared access
|
||||
// and cache mode, and O_NOFOLLOW so it may not be a symlink either.
|
||||
let access = if writable {
|
||||
libc::O_RDWR
|
||||
} else {
|
||||
libc::O_RDONLY
|
||||
};
|
||||
let mut flags = access | libc::O_NOFOLLOW | libc::O_CLOEXEC;
|
||||
if direct {
|
||||
flags |= libc::O_DIRECT;
|
||||
}
|
||||
flags
|
||||
};
|
||||
|
||||
// SAFETY: `dir` is a valid open directory fd and `cname` is a
|
||||
// NUL-terminated C string that outlives the call.
|
||||
let fd = unsafe { libc::openat(dir.as_raw_fd(), cname.as_ptr(), flags) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: `fd` is a freshly opened descriptor we now own exclusively.
|
||||
let opened = unsafe { File::from_raw_fd(fd) };
|
||||
|
||||
if i < last {
|
||||
// Reassignment drops the previous directory `File`, closing that fd.
|
||||
dir = opened;
|
||||
} else {
|
||||
return Ok(AlignedFile::new(opened, direct));
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!("extent_components guarantees at least one component")
|
||||
}
|
||||
|
||||
// Builds the error returned when a sector count/offset from the (untrusted)
|
||||
// descriptor, scaled to bytes, does not fit in a u64.
|
||||
fn overflow_error(what: &str) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("VMDK {what} overflows a 64-bit byte count"),
|
||||
)
|
||||
}
|
||||
|
||||
impl FlatVmdk {
|
||||
/// Opens a flat VMDK image from its already-open descriptor file.
|
||||
pub fn new(file: File, path: &Path, direct: bool) -> io::Result<Self> {
|
||||
let descriptor = VmdkDescriptor::new(&file, path)?;
|
||||
|
||||
if descriptor.extents_list.extents.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK descriptor lists no extents",
|
||||
));
|
||||
}
|
||||
|
||||
// Open every data extent and record the virtual-disk byte range it
|
||||
// backs.
|
||||
let mut extents: Vec<VmdkExtent> =
|
||||
Vec::with_capacity(descriptor.extents_list.extents.len());
|
||||
let mut virtual_start: u64 = 0;
|
||||
for extent in &descriptor.extents_list.extents {
|
||||
// A flat extent is a fixed, pre-allocated region, a zero-sector
|
||||
// extent would back an empty virtual range that can never be read
|
||||
// or written
|
||||
if extent.size_in_sectors == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK flat extent has zero size",
|
||||
));
|
||||
}
|
||||
|
||||
let length = extent
|
||||
.size_in_sectors
|
||||
.checked_mul(VMDK_SECTOR_SIZE)
|
||||
.ok_or_else(|| overflow_error("extent size"))?;
|
||||
let file_base_offset = extent
|
||||
.offset_in_sectors
|
||||
.checked_mul(VMDK_SECTOR_SIZE)
|
||||
.ok_or_else(|| overflow_error("extent file offset"))?;
|
||||
file_base_offset
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| overflow_error("extent file range"))?;
|
||||
|
||||
// Open the backing file using exactly the access declared for this
|
||||
// extent. The VMDK spec defines three values:
|
||||
// "RW" -> read + write
|
||||
// "RDONLY" -> read only
|
||||
// "NOACCESS" -> not accessible, do not open the file at all
|
||||
let (access, extent_file) = match extent.access.as_str() {
|
||||
"RW" => {
|
||||
let f = open_extent(&descriptor.base_path, &extent.filename, true, direct)?;
|
||||
(ExtentAccess::ReadWrite, Some(f))
|
||||
}
|
||||
"RDONLY" => {
|
||||
let f = open_extent(&descriptor.base_path, &extent.filename, false, direct)?;
|
||||
(ExtentAccess::ReadOnly, Some(f))
|
||||
}
|
||||
"NOACCESS" => (ExtentAccess::NoAccess, None),
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unsupported VMDK extent access mode '{other}'"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
extents.push(VmdkExtent {
|
||||
file: extent_file,
|
||||
access,
|
||||
virtual_start,
|
||||
length,
|
||||
file_base_offset,
|
||||
});
|
||||
|
||||
virtual_start = virtual_start
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| overflow_error("total virtual size"))?;
|
||||
}
|
||||
|
||||
// The virtual disk size is the end offset of the last extent.
|
||||
let total_disk_size = virtual_start;
|
||||
|
||||
Ok(Self {
|
||||
descriptor: Arc::new(descriptor),
|
||||
descriptor_file: Arc::new(file),
|
||||
extents: Arc::new(extents),
|
||||
size: total_disk_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn virtual_block_size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Shared handle to the opened data extents, used to build the I/O worker.
|
||||
pub fn extents(&self) -> Arc<Vec<VmdkExtent>> {
|
||||
Arc::clone(&self.extents)
|
||||
}
|
||||
|
||||
/// Host allocation size: the sum of every opened extent's actually
|
||||
/// allocated storage (`st_blocks * 512` for regular files, device size for
|
||||
/// block devices), so sparse extents are reported correctly. `NoAccess`
|
||||
/// extents (no open file) contribute 0, as does any extent whose size
|
||||
/// cannot be queried.
|
||||
pub fn physical_block_size(&self) -> u64 {
|
||||
self.extents
|
||||
.iter()
|
||||
.filter_map(|extent| extent.file.as_ref())
|
||||
.map(|f| query_device_size(f.file()).map_or(0, |(_, physical)| physical))
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Sector/cluster geometry reported to the guest.
|
||||
pub fn topology(&self) -> DiskTopology {
|
||||
self.extents
|
||||
.iter()
|
||||
.find_map(|extent| extent.file.as_ref())
|
||||
.map(|f| {
|
||||
DiskTopology::probe(f.file()).unwrap_or_else(|_| {
|
||||
warn!("Unable to get VMDK extent topology. Using default topology");
|
||||
DiskTopology::default()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// Expose the descriptor file's fd as the disk's representative fd.
|
||||
impl AsRawFd for FlatVmdk {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.descriptor_file.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FlatVmdk {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
descriptor: Arc::clone(&self.descriptor),
|
||||
descriptor_file: Arc::clone(&self.descriptor_file),
|
||||
extents: Arc::clone(&self.extents),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extent_components_allows_bare_name() {
|
||||
// The common flat-VMDK case: a single co-located extent file.
|
||||
let comps = extent_components("disk-flat.vmdk").unwrap();
|
||||
assert_eq!(comps, [OsStr::new("disk-flat.vmdk")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_components_rejects_traversal_and_empty() {
|
||||
// `..`/`.` traversal and empty names are refused. (An absolute path is
|
||||
// decomposed into its Normal components, the leading `/` is skipped and
|
||||
// the caller anchors the walk at the filesystem root.)
|
||||
extent_components("../../etc/passwd").unwrap_err();
|
||||
extent_components("sub/../../escape").unwrap_err();
|
||||
extent_components("extent-1.vmdk/../../").unwrap_err();
|
||||
extent_components("./s001.vmdk").unwrap_err();
|
||||
extent_components("").unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_components_decomposes_absolute_path() {
|
||||
// A leading `/` is skipped, the remaining Normal components are walked
|
||||
// from the filesystem root by the caller.
|
||||
let comps = extent_components("/var/lib/layer.erofs").unwrap();
|
||||
assert_eq!(
|
||||
comps,
|
||||
[
|
||||
OsStr::new("var"),
|
||||
OsStr::new("lib"),
|
||||
OsStr::new("layer.erofs")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Opens `path` as a directory anchor fd, mirroring how `open_extent` opens
|
||||
// its anchor.
|
||||
fn open_dir(path: &Path) -> File {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
|
||||
.open(path)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Returns false when `openat2(2)` is unavailable.
|
||||
fn openat2_available(res: &io::Result<File>) -> bool {
|
||||
!matches!(
|
||||
res.as_ref().err().and_then(|e| e.raw_os_error()),
|
||||
Some(libc::ENOSYS) | Some(libc::EPERM)
|
||||
)
|
||||
}
|
||||
|
||||
// Opens the same anchor + `filename` with BOTH extent-open implementations
|
||||
// so a single scenario asserts they behave identically:
|
||||
fn open_both(
|
||||
base_path: &Path,
|
||||
filename: &str,
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> (io::Result<File>, io::Result<AlignedFile>) {
|
||||
let anchor: &Path = if Path::new(filename).is_absolute() {
|
||||
Path::new("/")
|
||||
} else {
|
||||
base_path
|
||||
};
|
||||
|
||||
let openat2_dir = open_dir(anchor);
|
||||
let openat2_res = open_extent_openat2(openat2_dir.as_raw_fd(), filename, writable, direct);
|
||||
|
||||
let walk_dir = open_dir(anchor);
|
||||
let walk_res = match extent_components(filename) {
|
||||
Ok(components) => open_extent_walk(walk_dir, &components, writable, direct),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
(openat2_res, walk_res)
|
||||
}
|
||||
|
||||
// Asserts openat2.
|
||||
fn check_openat2(res: &io::Result<File>, expect_ok: bool) {
|
||||
if !openat2_available(res) {
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
res.is_ok(),
|
||||
expect_ok,
|
||||
"openat2 result did not match expectation (expected ok = {expect_ok})"
|
||||
);
|
||||
}
|
||||
|
||||
// Asserts the per-component walk result.
|
||||
fn check_walk(res: &io::Result<AlignedFile>, expect_ok: bool) {
|
||||
assert_eq!(
|
||||
res.is_ok(),
|
||||
expect_ok,
|
||||
"walk result did not match expectation (expected ok = {expect_ok})"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_regular_file() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-regular-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
fs::write(base.join("disk-flat.vmdk"), b"data").unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "disk-flat.vmdk", false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_file_in_subdirectory() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// A relative sub-path resolves beneath the descriptor directory.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-subdir-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
fs::create_dir(base.join("extents")).unwrap();
|
||||
fs::write(base.join("extents").join("s001.vmdk"), b"data").unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "extents/s001.vmdk", false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_absolute_path_within_descriptor_dir() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-abs-in-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
fs::write(base.join("gpt_meta_head.img"), b"data").unwrap();
|
||||
let abs = base.join("gpt_meta_head.img");
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, abs.to_str().unwrap(), false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_absolute_path_outside_descriptor_dir() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
let desc_dir = TempDir::new_with_prefix("/tmp/vmdk-desc-test").unwrap();
|
||||
let layer_dir = TempDir::new_with_prefix("/tmp/vmdk-layer-test").unwrap();
|
||||
fs::write(layer_dir.as_path().join("layer.erofs"), b"data").unwrap();
|
||||
let abs = layer_dir.as_path().join("layer.erofs");
|
||||
|
||||
let (openat2_res, walk_res) =
|
||||
open_both(desc_dir.as_path(), abs.to_str().unwrap(), false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_rejects_symlinked_final_component_relative() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// A bare-named extent that is actually a symlink to a file the guest
|
||||
// must never reach. Both implementations refuse it via O_NOFOLLOW.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-symlink-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
let target = base.join("target-secret");
|
||||
fs::write(&target, b"secret").unwrap();
|
||||
symlink(&target, base.join("disk-flat.vmdk")).unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "disk-flat.vmdk", true, false);
|
||||
check_openat2(&openat2_res, false);
|
||||
check_walk(&walk_res, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_rejects_symlinked_final_component_absolute() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// Even for absolute paths, the extent file itself may not be a symlink.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-abs-finalsym-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
let target = base.join("target-secret");
|
||||
fs::write(&target, b"secret").unwrap();
|
||||
let link = base.join("extent-link.vmdk");
|
||||
symlink(&target, &link).unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, link.to_str().unwrap(), true, false);
|
||||
check_openat2(&openat2_res, false);
|
||||
check_walk(&walk_res, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_follows_symlinked_intermediate_directory_relative() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// A relative path may traverse a symlinked intermediate directory
|
||||
// (only the final component is guarded).
|
||||
let real = TempDir::new_with_prefix("/tmp/vmdk-rel-real-test").unwrap();
|
||||
fs::write(real.as_path().join("s001.vmdk"), b"data").unwrap();
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-rel-symdir-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
symlink(real.as_path(), base.join("sub")).unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "sub/s001.vmdk", false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_follows_symlinked_intermediate_directory_absolute() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// An absolute path may likewise traverse a symlinked intermediate
|
||||
// directory (common in container deployments).
|
||||
let real = TempDir::new_with_prefix("/tmp/vmdk-abs-realdir-test").unwrap();
|
||||
fs::write(real.as_path().join("layer.erofs"), b"data").unwrap();
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-abs-linkdir-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
symlink(real.as_path(), base.join("link")).unwrap();
|
||||
|
||||
let via_symlink = base.join("link").join("layer.erofs");
|
||||
let (openat2_res, walk_res) = open_both(base, via_symlink.to_str().unwrap(), false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Flat VMDK block backend.
|
||||
//!
|
||||
//! Supports the `monolithicFlat` and `twoGbMaxExtentFlat` create types with
|
||||
//! synchronous, extent-aware I/O.
|
||||
|
||||
mod descriptor;
|
||||
mod engine_sync;
|
||||
mod flat;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
pub use descriptor::{has_descriptor_header, is_flat_vmdk};
|
||||
|
||||
use self::engine_sync::FlatVmdkSync;
|
||||
use self::flat::FlatVmdk;
|
||||
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
||||
use crate::{DiskTopology, disk_file};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VmdkDisk {
|
||||
inner: FlatVmdk,
|
||||
}
|
||||
|
||||
impl VmdkDisk {
|
||||
/// Builds a Flat VMDK disk backend.
|
||||
pub fn new(file: File, path: &Path, direct: bool) -> Result<Self, BlockError> {
|
||||
let inner = FlatVmdk::new(file, path, direct)?;
|
||||
Ok(VmdkDisk { inner })
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::DiskSize for VmdkDisk {
|
||||
fn logical_size(&self) -> BlockResult<u64> {
|
||||
Ok(self.inner.virtual_block_size())
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::PhysicalSize for VmdkDisk {
|
||||
fn physical_size(&self) -> BlockResult<u64> {
|
||||
Ok(self.inner.physical_block_size())
|
||||
}
|
||||
}
|
||||
|
||||
// Expose the descriptor file's fd for advisory image locking.
|
||||
impl disk_file::DiskFd for VmdkDisk {
|
||||
fn fd(&self) -> BorrowedDiskFd<'_> {
|
||||
BorrowedDiskFd::new(self.inner.as_raw_fd())
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::Geometry for VmdkDisk {
|
||||
fn topology(&self) -> DiskTopology {
|
||||
self.inner.topology()
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::SparseCapable for VmdkDisk {}
|
||||
|
||||
// Flat VMDK keeps no in-memory format metadata, so no-op.
|
||||
impl disk_file::MetadataSync for VmdkDisk {}
|
||||
|
||||
impl disk_file::Resizable for VmdkDisk {
|
||||
fn resize(&mut self, _size: u64) -> BlockResult<()> {
|
||||
Err(BlockError::new(
|
||||
BlockErrorKind::UnsupportedFeature,
|
||||
DiskFileError::ResizeError(io::Error::other("resize not supported for flat VMDK")),
|
||||
)
|
||||
.with_op(ErrorOp::Resize))
|
||||
}
|
||||
}
|
||||
|
||||
impl disk_file::DiskFile for VmdkDisk {}
|
||||
|
||||
impl disk_file::AsyncDiskFile for VmdkDisk {
|
||||
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
||||
Ok(Box::new(VmdkDisk {
|
||||
inner: self.inner.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
||||
// VMDK provides a synchronous, extent-aware worker, so the io_uring ring
|
||||
// depth is unused here.
|
||||
let _ = ring_depth;
|
||||
|
||||
Ok(Box::new(FlatVmdkSync::new(
|
||||
self.inner.extents(),
|
||||
self.inner.virtual_block_size(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::disk_file::{AsyncDiskFile, DiskFd, DiskSize, PhysicalSize, Resizable};
|
||||
|
||||
const SECTOR: u64 = 512;
|
||||
|
||||
// Builds a flat VMDK in `dir`: a descriptor plus one backing data file per
|
||||
// extent. When `allocate` is true the extent files are filled with real
|
||||
// blocks (fixed / pre-allocated layout used in practice), otherwise they
|
||||
// are created sparse via `set_len` (declared length but no allocated
|
||||
// blocks). `extents` entries are (filename, access, sectors). Returns the
|
||||
// descriptor path.
|
||||
fn build_flat_vmdk(
|
||||
dir: &Path,
|
||||
create_type: &str,
|
||||
extents: &[(&str, &str, u64)],
|
||||
allocate: bool,
|
||||
) -> PathBuf {
|
||||
let mut desc = String::from("# Disk DescriptorFile\n");
|
||||
desc.push_str("version=1\n");
|
||||
desc.push_str("CID=fffffffe\n");
|
||||
desc.push_str("parentCID=ffffffff\n");
|
||||
desc.push_str(&format!("createType={create_type}\n"));
|
||||
desc.push_str("# Extent description\n");
|
||||
|
||||
for (filename, access, sectors) in extents {
|
||||
let mut data = File::create(dir.join(filename)).unwrap();
|
||||
if allocate {
|
||||
data.write_all(&vec![0u8; (sectors * SECTOR) as usize])
|
||||
.unwrap();
|
||||
} else {
|
||||
data.set_len(sectors * SECTOR).unwrap();
|
||||
}
|
||||
data.sync_all().unwrap();
|
||||
desc.push_str(&format!("{access} {sectors} FLAT \"{filename}\"\n"));
|
||||
}
|
||||
|
||||
desc.push_str("# The Disk Data Base\n");
|
||||
desc.push_str("ddb.adapterType = \"ide\"\n");
|
||||
|
||||
let desc_path = dir.join("disk.vmdk");
|
||||
let mut df = File::create(&desc_path).unwrap();
|
||||
df.write_all(desc.as_bytes()).unwrap();
|
||||
df.sync_all().unwrap();
|
||||
desc_path
|
||||
}
|
||||
|
||||
// Sparse extents.
|
||||
fn write_flat_vmdk(dir: &Path, create_type: &str, extents: &[(&str, &str, u64)]) -> PathBuf {
|
||||
build_flat_vmdk(dir, create_type, extents, false)
|
||||
}
|
||||
|
||||
// Fully pre-allocated extents.
|
||||
fn write_flat_vmdk_allocated(
|
||||
dir: &Path,
|
||||
create_type: &str,
|
||||
extents: &[(&str, &str, u64)],
|
||||
) -> PathBuf {
|
||||
build_flat_vmdk(dir, create_type, extents, true)
|
||||
}
|
||||
|
||||
fn open_descriptor(path: &Path) -> File {
|
||||
File::open(path).unwrap()
|
||||
}
|
||||
|
||||
// Writes a descriptor referencing `extent_lines` verbatim (no backing data
|
||||
// files are created). Used to exercise `FlatVmdk::new`'s per-extent
|
||||
// validation, whose zero-size/overflow checks all run before an extent file
|
||||
// would be opened.
|
||||
fn write_descriptor(dir: &Path, create_type: &str, extent_lines: &[&str]) -> PathBuf {
|
||||
let mut desc = String::from("# Disk DescriptorFile\n");
|
||||
desc.push_str("version=1\n");
|
||||
desc.push_str("CID=fffffffe\n");
|
||||
desc.push_str("parentCID=ffffffff\n");
|
||||
desc.push_str(&format!("createType={create_type}\n"));
|
||||
desc.push_str("# Extent description\n");
|
||||
for line in extent_lines {
|
||||
desc.push_str(line);
|
||||
desc.push('\n');
|
||||
}
|
||||
desc.push_str("# The Disk Data Base\n");
|
||||
desc.push_str("ddb.adapterType = \"ide\"\n");
|
||||
|
||||
let desc_path = dir.join("disk.vmdk");
|
||||
let mut df = File::create(&desc_path).unwrap();
|
||||
df.write_all(desc.as_bytes()).unwrap();
|
||||
df.sync_all().unwrap();
|
||||
desc_path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_and_physical_size_single_extent() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
assert_eq!(disk.logical_size().unwrap(), 2048 * SECTOR);
|
||||
// The extent is created sparse (`set_len`), so no blocks are allocated
|
||||
// and the `st_blocks`-based physical size is 0.
|
||||
assert_eq!(disk.physical_size().unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_size_sums_multiple_extents() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR);
|
||||
// Sparse extents: no blocks are allocated, so physical size is 0.
|
||||
assert_eq!(disk.physical_size().unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physical_size_matches_fully_allocated_extents() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk_allocated(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
// Fully pre-allocated extents: host allocation (st_blocks) equals the
|
||||
// declared logical size.
|
||||
assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR);
|
||||
assert_eq!(disk.physical_size().unwrap(), (2048 + 1024) * SECTOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fd_exposes_descriptor_file() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 64)],
|
||||
);
|
||||
let file = open_descriptor(&path);
|
||||
let expected = file.as_raw_fd();
|
||||
|
||||
let disk = VmdkDisk::new(file, &path, false).unwrap();
|
||||
|
||||
assert_eq!(disk.fd().as_raw_fd(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_is_unsupported() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 64)],
|
||||
);
|
||||
let mut disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
let err = disk.resize(4096).unwrap_err();
|
||||
assert_eq!(err.kind(), BlockErrorKind::UnsupportedFeature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_clone_preserves_size() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
let cloned = disk.try_clone().unwrap();
|
||||
assert_eq!(cloned.logical_size().unwrap(), disk.logical_size().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_async_io_builds_worker() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&[("disk-flat.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
// Ring depth is ignored by the synchronous VMDK worker.
|
||||
disk.create_async_io(0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_async_io_supports_multi_extent() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
|
||||
let path = write_flat_vmdk(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 2048)],
|
||||
);
|
||||
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
|
||||
|
||||
disk.create_async_io(32).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_sector_extent() {
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-zero-test").unwrap();
|
||||
let path = write_descriptor(
|
||||
dir.as_path(),
|
||||
"monolithicFlat",
|
||||
&["RW 0 FLAT \"disk-flat.vmdk\""],
|
||||
);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_extent_size_overflow() {
|
||||
// size_in_sectors * 512 must fit in a u64.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-size-test").unwrap();
|
||||
let line = format!("RW {} FLAT \"disk-flat.vmdk\"", u64::MAX);
|
||||
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_extent_file_offset_overflow() {
|
||||
// The offset * 512 must fit in a u64.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-offset-test").unwrap();
|
||||
let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {}", u64::MAX);
|
||||
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_extent_file_range_overflow() {
|
||||
// Each of offset*512 and size*512 fits in a u64, but their sum (the last
|
||||
// byte the extent addresses in its backing file) overflows.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-range-test").unwrap();
|
||||
// offset_in_sectors = floor(u64::MAX / 512) => offset bytes = u64::MAX -
|
||||
// 511, size 1 sector (512 bytes) pushes the end one byte past u64::MAX.
|
||||
let offset = u64::MAX / 512;
|
||||
let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {offset}");
|
||||
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_total_virtual_size_overflow() {
|
||||
// Two extents whose individual lengths fit in a u64 but whose running
|
||||
// sum (the total virtual disk size) overflows. size = 2^55 - 1 =>
|
||||
// length = 2^64 - 512, two of them overflow the total.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-total-test").unwrap();
|
||||
let size = (1u64 << 55) - 1;
|
||||
let l1 = format!("NOACCESS {size} FLAT \"s001.vmdk\"");
|
||||
let l2 = format!("NOACCESS {size} FLAT \"s002.vmdk\"");
|
||||
let path = write_descriptor(
|
||||
dir.as_path(),
|
||||
"twoGbMaxExtentFlat",
|
||||
&[l1.as_str(), l2.as_str()],
|
||||
);
|
||||
|
||||
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
// Copyright © 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
mod aio_data_io;
|
||||
mod common;
|
||||
mod completion;
|
||||
mod guest_memory_target;
|
||||
mod operation;
|
||||
mod owned_io_buffer;
|
||||
#[cfg(feature = "io_uring")]
|
||||
mod uring_data_io;
|
||||
|
||||
use std::{io, result};
|
||||
|
||||
pub use aio_data_io::AioDataIo;
|
||||
pub use completion::AsyncIoCompletion;
|
||||
pub(crate) use completion::CompletionCommon;
|
||||
pub use guest_memory_target::GuestMemoryTarget;
|
||||
pub use operation::AsyncIoOperation;
|
||||
pub use owned_io_buffer::OwnedIoBuffer;
|
||||
use thiserror::Error;
|
||||
#[cfg(feature = "io_uring")]
|
||||
pub use uring_data_io::UringDataIo;
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use crate::SECTOR_SIZE;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DiskFileError {
|
||||
/// Failed getting disk file size.
|
||||
#[error("Failed getting disk file size")]
|
||||
Size(#[source] io::Error),
|
||||
/// Failed creating a new AsyncIo.
|
||||
#[error("Failed creating a new AsyncIo")]
|
||||
NewAsyncIo(#[source] io::Error),
|
||||
/// Unsupported operation.
|
||||
#[error("Unsupported operation")]
|
||||
Unsupported,
|
||||
/// Resize failed
|
||||
#[error("Resize failed")]
|
||||
ResizeError(#[source] io::Error),
|
||||
/// Flushing cached metadata failed
|
||||
#[error("Flushing cached metadata failed")]
|
||||
SyncMetadata(#[source] io::Error),
|
||||
#[error("Failed cloning disk file")]
|
||||
Clone(#[source] io::Error),
|
||||
}
|
||||
|
||||
pub type DiskFileResult<T> = result::Result<T, DiskFileError>;
|
||||
|
||||
/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding disk file.
|
||||
///
|
||||
/// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations
|
||||
/// by some disk implementations, which wrap the effective [`File`]
|
||||
/// in an `Arc<Mutex<T>>`, making the use of [`BorrowedFd`] impossible.
|
||||
///
|
||||
/// [`BorrowedFd`]: std::os::fd::BorrowedFd
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct BorrowedDiskFd<'fd> {
|
||||
raw_fd: RawFd,
|
||||
_lifetime: PhantomData<&'fd OwnedFd>,
|
||||
}
|
||||
|
||||
impl BorrowedDiskFd<'_> {
|
||||
pub(crate) fn new(raw_fd: RawFd) -> Self {
|
||||
Self {
|
||||
raw_fd,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for BorrowedDiskFd<'_> {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.raw_fd
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AsyncIoError {
|
||||
/// Failed vectored reading from file.
|
||||
#[error("Failed vectored reading from file")]
|
||||
ReadVectored(#[source] io::Error),
|
||||
/// Failed vectored writing to file.
|
||||
#[error("Failed vectored writing to file")]
|
||||
WriteVectored(#[source] io::Error),
|
||||
/// Failed synchronizing file.
|
||||
#[error("Failed synchronizing file")]
|
||||
Fsync(#[source] io::Error),
|
||||
/// Failed punching hole.
|
||||
#[error("Failed punching hole")]
|
||||
PunchHole(#[source] io::Error),
|
||||
/// Failed writing zeroes.
|
||||
#[error("Failed writing zeroes")]
|
||||
WriteZeroes(#[source] io::Error),
|
||||
/// Failed submitting batch requests.
|
||||
#[error("Failed submitting batch requests")]
|
||||
SubmitBatchRequests(#[source] io::Error),
|
||||
}
|
||||
|
||||
pub type AsyncIoResult<T> = result::Result<T, AsyncIoError>;
|
||||
|
||||
pub trait AsyncIo: Send {
|
||||
fn notifier(&self) -> &EventFd;
|
||||
|
||||
/// Submits one owned data operation.
|
||||
///
|
||||
/// Takes ownership of `op`.
|
||||
/// Implementations that complete asynchronously must retain it until its
|
||||
/// completion is returned.
|
||||
fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()>;
|
||||
|
||||
/// Submits a read from `offset` into guest memory.
|
||||
fn read_to_memory(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
target: GuestMemoryTarget,
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
self.submit_data_operation(AsyncIoOperation::read_to_memory(offset, target, user_data))
|
||||
}
|
||||
|
||||
/// Submits a write to `offset` from guest memory.
|
||||
fn write_from_memory(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
target: GuestMemoryTarget,
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
self.submit_data_operation(AsyncIoOperation::write_from_memory(
|
||||
offset, target, user_data,
|
||||
))
|
||||
}
|
||||
|
||||
/// Submits a read from `offset` into an owned host-memory buffer.
|
||||
fn read_to_vec(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
buffer: OwnedIoBuffer,
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
self.submit_data_operation(AsyncIoOperation::read_to_vec(offset, buffer, user_data))
|
||||
}
|
||||
|
||||
/// Submits a write to `offset` from an owned host-memory buffer.
|
||||
fn write_from_vec(
|
||||
&mut self,
|
||||
offset: libc::off_t,
|
||||
buffer: OwnedIoBuffer,
|
||||
user_data: u64,
|
||||
) -> AsyncIoResult<()> {
|
||||
self.submit_data_operation(AsyncIoOperation::write_from_vec(offset, buffer, user_data))
|
||||
}
|
||||
|
||||
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()>;
|
||||
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
|
||||
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>;
|
||||
|
||||
/// Returns the next owned completion, if one is available.
|
||||
///
|
||||
/// Read completions from owned host-memory buffers return that buffer here.
|
||||
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion>;
|
||||
|
||||
fn batch_requests_enabled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Submits a batch of owned data operations.
|
||||
///
|
||||
/// Backends either accept the whole batch for eventual completion or return
|
||||
/// an error before taking ownership of any operation.
|
||||
fn submit_batch_requests(&mut self, batch_request: Vec<AsyncIoOperation>) -> AsyncIoResult<()> {
|
||||
if batch_request.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AsyncIoError::SubmitBatchRequests(io::Error::other(
|
||||
"batch requests are not supported by this backend",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn alignment(&self) -> u64 {
|
||||
SECTOR_SIZE
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
// Copyright © 2023 Intel Corporation
|
||||
//
|
||||
// Copyright © 2023 Crusoe Energy Systems LLC
|
||||
//
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::{io, slice};
|
||||
|
||||
use log::warn;
|
||||
use vmm_sys_util::aio;
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
use super::common::{duplicate_user_data_error, errno_result, validate_batch};
|
||||
use super::{AsyncIoCompletion, AsyncIoOperation, CompletionCommon};
|
||||
|
||||
/// Retained Linux AIO queue for owned async data I/O operations.
|
||||
pub struct AioDataIo {
|
||||
// Keep this before `in_flight`: Rust drops fields in declaration order, so
|
||||
// dropping the context destroys kernel AIO state before retained
|
||||
// operations release the buffers referenced by their iovecs.
|
||||
ctx: aio::IoContext,
|
||||
// `in_flight` tracks every user_data value accepted by the kernel. Owned
|
||||
// data operations store `Some(op)` so their iovecs and backing buffers
|
||||
// remain valid until completion; metadata operations store `None`.
|
||||
in_flight: HashMap<u64, Option<AsyncIoOperation>>,
|
||||
completions: CompletionCommon,
|
||||
}
|
||||
|
||||
impl AioDataIo {
|
||||
/// Creates a Linux AIO context and its completion eventfd.
|
||||
pub fn new(queue_depth: u32) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
ctx: aio::IoContext::new(queue_depth)?,
|
||||
in_flight: HashMap::new(),
|
||||
completions: CompletionCommon::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the eventfd signaled when completions are available.
|
||||
pub fn notifier(&self) -> &EventFd {
|
||||
self.completions.notifier()
|
||||
}
|
||||
|
||||
/// Submits one owned read or write operation to the queue.
|
||||
///
|
||||
/// Submission failures are converted into injected completions so callers
|
||||
/// can observe every accepted request through the normal completion path.
|
||||
pub fn submit_operation(&mut self, fd: RawFd, op: AsyncIoOperation) -> io::Result<()> {
|
||||
validate_batch(
|
||||
|user_data| self.in_flight.contains_key(&user_data),
|
||||
slice::from_ref(&op),
|
||||
)?;
|
||||
|
||||
let user_data = op.user_data();
|
||||
let iovecs = op.iovecs();
|
||||
let opcode = if op.is_read() {
|
||||
aio::IOCB_CMD_PREADV
|
||||
} else {
|
||||
aio::IOCB_CMD_PWRITEV
|
||||
};
|
||||
let mut iocb = aio::IoControlBlock {
|
||||
aio_fildes: fd.as_raw_fd() as u32,
|
||||
aio_lio_opcode: opcode as u16,
|
||||
aio_buf: iovecs.as_ptr() as u64,
|
||||
aio_nbytes: iovecs.len() as u64,
|
||||
aio_offset: op.offset(),
|
||||
aio_data: user_data,
|
||||
aio_flags: aio::IOCB_FLAG_RESFD,
|
||||
aio_resfd: self.completions.notifier().as_raw_fd() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
self.in_flight.insert(user_data, Some(op));
|
||||
|
||||
let result = match self.ctx.submit(&[&mut iocb]) {
|
||||
Ok(1) => return Ok(()),
|
||||
Ok(_) => -libc::EAGAIN,
|
||||
Err(e) => errno_result(&e),
|
||||
};
|
||||
|
||||
let buffer = self
|
||||
.in_flight
|
||||
.remove(&user_data)
|
||||
.flatten()
|
||||
.and_then(AsyncIoOperation::into_completion_buffer);
|
||||
self.inject_completion(AsyncIoCompletion::new(user_data, result, buffer));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Submits an fsync operation carrying `user_data`.
|
||||
pub fn submit_fsync(&mut self, fd: RawFd, user_data: u64) -> io::Result<()> {
|
||||
if self.in_flight.contains_key(&user_data) {
|
||||
return Err(duplicate_user_data_error(user_data));
|
||||
}
|
||||
|
||||
let mut iocb = aio::IoControlBlock {
|
||||
aio_fildes: fd.as_raw_fd() as u32,
|
||||
aio_lio_opcode: aio::IOCB_CMD_FSYNC as u16,
|
||||
aio_data: user_data,
|
||||
aio_flags: aio::IOCB_FLAG_RESFD,
|
||||
aio_resfd: self.completions.notifier().as_raw_fd() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
self.in_flight.insert(user_data, None);
|
||||
let result = match self.ctx.submit(&[&mut iocb]) {
|
||||
Ok(1) => return Ok(()),
|
||||
Ok(_) => -libc::EAGAIN,
|
||||
Err(e) => errno_result(&e),
|
||||
};
|
||||
|
||||
self.in_flight.remove(&user_data);
|
||||
self.inject_completion(AsyncIoCompletion::new(user_data, result, None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Injects a completion that did not come from a kernel AIO event.
|
||||
///
|
||||
/// The notifier is signaled so callers can drain it with
|
||||
/// [`Self::next_completion`].
|
||||
pub fn inject_completion(&mut self, completion: AsyncIoCompletion) {
|
||||
self.completions.complete(completion);
|
||||
}
|
||||
|
||||
/// Returns the next kernel or injected completion if one is available.
|
||||
///
|
||||
/// Consuming a kernel completion returns ownership of any buffer retained
|
||||
/// by the corresponding operation.
|
||||
pub fn next_completion(&mut self) -> Option<AsyncIoCompletion> {
|
||||
if let Some(completion) = self.completions.next_completed() {
|
||||
return Some(completion);
|
||||
}
|
||||
|
||||
let mut events = [aio::IoEvent::default(); 32];
|
||||
let rc = match self.ctx.get_events(0, &mut events, None) {
|
||||
Ok(rc) => rc,
|
||||
Err(e) => {
|
||||
warn!("Linux AIO get_events failed: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
for event in &events[..rc] {
|
||||
self.completions.complete(AsyncIoCompletion::new(
|
||||
event.data,
|
||||
event.res as i32,
|
||||
self.in_flight
|
||||
.remove(&event.data)
|
||||
.flatten()
|
||||
.and_then(AsyncIoOperation::into_completion_buffer),
|
||||
));
|
||||
}
|
||||
|
||||
self.completions.next_completed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{self, Write};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use vmm_sys_util::tempfile::TempFile;
|
||||
|
||||
use super::AioDataIo;
|
||||
use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, OwnedIoBuffer};
|
||||
|
||||
fn wait_for_completion(data_io: &mut AioDataIo) -> AsyncIoCompletion {
|
||||
for _ in 0..1000 {
|
||||
if let Some(completion) = data_io.next_completion() {
|
||||
return completion;
|
||||
}
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
|
||||
panic!("timed out waiting for Linux AIO completion");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aio_rejects_duplicate_user_data_for_metadata_ops() {
|
||||
let mut file = TempFile::new().unwrap().into_file();
|
||||
file.write_all(&[0xa5; 512]).unwrap();
|
||||
let fd = file.as_raw_fd();
|
||||
let mut data_io = AioDataIo::new(8).unwrap();
|
||||
|
||||
data_io
|
||||
.submit_operation(
|
||||
fd,
|
||||
AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), 7),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
data_io.submit_fsync(fd, 7).unwrap_err().kind(),
|
||||
io::ErrorKind::AlreadyExists
|
||||
);
|
||||
|
||||
let completion = wait_for_completion(&mut data_io);
|
||||
assert_eq!(completion.user_data, 7);
|
||||
assert_eq!(completion.result, 512);
|
||||
assert_eq!(
|
||||
completion.buffer.unwrap().as_slice(),
|
||||
[0xa5; 512].as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aio_injected_completion_uses_completion_path() {
|
||||
let mut data_io = AioDataIo::new(8).unwrap();
|
||||
|
||||
data_io.inject_completion(AsyncIoCompletion::new(9, -libc::EIO, None));
|
||||
|
||||
let completion = data_io.next_completion().unwrap();
|
||||
assert_eq!(completion.user_data, 9);
|
||||
assert_eq!(completion.result, -libc::EIO);
|
||||
assert!(completion.buffer.is_none());
|
||||
assert!(data_io.next_completion().is_none());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user