diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000000..a88e599093 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,3 @@ +[profile.default] +# Don't let one individual test run for more than 10 minutes +slow-timeout = { period = "60s", terminate-after = 10 } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..12f0b48b2f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# 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 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..3550a30f2d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4121e76a99..2f3865fb1a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,21 +1,73 @@ version: 2 updates: - package-ecosystem: cargo - directory: "/" + directories: + - "/" + - "/fuzz" schedule: - interval: daily - open-pull-requests-limit: 1 + interval: weekly allow: - - dependency-type: direct - - dependency-type: indirect + - dependency-name: "acpi_tables" + - 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 - directory: "/fuzz" + directories: + - "/" + - "/fuzz" schedule: - interval: daily - open-pull-requests-limit: 1 + interval: weekly allow: - - dependency-type: direct - - dependency-type: indirect + - 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: "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: diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml deleted file mode 100644 index 2e44b9af40..0000000000 --- a/.github/workflows/audit.yaml +++ /dev/null @@ -1,16 +0,0 @@ -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@v4 - - uses: actions-rust-lang/audit@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 070650ba6e..0000000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,71 +0,0 @@ -name: Cloud Hypervisor Build -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Build - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - rust: - - stable - - beta - - nightly - - "1.83.0" - target: - - x86_64-unknown-linux-gnu - - x86_64-unknown-linux-musl - steps: - - name: Code checkout - uses: actions/checkout@v4 - 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 rustc --locked --bin cloud-hypervisor -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (kvm) - run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (default features + tdx) - run: cargo rustc --locked --bin cloud-hypervisor --features "tdx" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (default features + dbus_api) - run: cargo rustc --locked --bin cloud-hypervisor --features "dbus_api" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (default features + guest_debug) - run: cargo rustc --locked --bin cloud-hypervisor --features "guest_debug" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (default features + pvmemcontrol) - run: cargo rustc --locked --bin cloud-hypervisor --features "pvmemcontrol" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (mshv) - run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "mshv" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (sev_snp) - run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "sev_snp" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (igvm) - run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "igvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Build (mshv + kvm) - run: cargo rustc --locked --bin cloud-hypervisor --no-default-features --features "mshv,kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - 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)" diff --git a/.github/workflows/build_nix.yaml b/.github/workflows/build_nix.yaml new file mode 100644 index 0000000000..586c985897 --- /dev/null +++ b/.github/workflows/build_nix.yaml @@ -0,0 +1,38 @@ +name: Cloud Hypervisor Build (Nix) +on: [push, pull_request, merge_group] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Code checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: cachix/install-nix-action@v31 + # We restore Nix evaluation and Nix tarball cache, speeding up the CI. + # This does not cover any Nix artifacts from the Nix store. + - name: Restore Nix cache + uses: actions/cache@v5 + with: + path: ~/.cache/nix + key: nix-cache-${{ github.job }} + # Nix binary cache + - uses: DeterminateSystems/magic-nix-cache-action@main + # Dedicated step to separate all the + # "copying path '/nix/store/...' from 'https://cache.nixos.org'." + # messages from the actual build output. + - name: Prepare Nix Store + run: nix develop --command bash -c "nix --version" + - name: Check Nix format + run: nix fmt -- --ci + - name: Check Nix Flake + run: nix flake check -L + - name: Build Cloud Hypervisor + run: | + nix build -L .#default + nix build -L .#cloud-hypervisor diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000..d7ff2b91ac --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,514 @@ +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@v6 + with: + fetch-depth: 0 + - id: changes + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + 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@v6 + - name: Set up Python 3.x + uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Check DCO + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + 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@v6 + with: + # PR head, not the merge ref, so gitlint sees the PR's commits. + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Set up Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade gitlint + - name: Lint git commit messages + run: | + gitlint --commits "origin/$GITHUB_BASE_REF.." + taplo: + name: taplo + needs: [preflight] + if: needs.preflight.outputs.cargo == 'true' + runs-on: ubuntu-latest + steps: + - name: Code checkout + uses: actions/checkout@v6 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Install build dependencies + run: sudo apt-get update && sudo apt-get -yqq install build-essential libssl-dev + - name: Install taplo + run: cargo install taplo-cli --locked + - name: Check formatting + run: taplo fmt --check + audit: + name: audit + needs: [preflight] + if: needs.preflight.outputs.cargo == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - 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@v6 + - 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@v6 + - name: Lint Dockerfile + uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 + with: + dockerfile: ./resources/Dockerfile + format: tty + no-fail: false + verbose: true + failure-threshold: info + reuse: + name: reuse + needs: [preflight] + if: needs.preflight.outputs.full == 'true' || needs.preflight.outputs.cargo == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - 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@v6 + - name: Install Rust toolchain (${{ matrix.rust }}) + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + components: rustfmt + - name: Formatting (rustfmt) + run: cargo fmt --all -- --check + - name: Formatting (fuzz) (rustfmt) + run: cargo fmt --all --manifest-path fuzz/Cargo.toml -- --check + package-consistency: + name: package-consistency + needs: [preflight] + if: needs.preflight.outputs.full == 'true' + runs-on: ubuntu-latest + steps: + - name: Code checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install dependencies + run: sudo apt install -y python3 + - name: Install Rust toolchain stable + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Check Rust VMM Package Consistency of root Workspace + run: python3 scripts/package-consistency-check.py github.com/rust-vmm + - name: Check Rust VMM Package Consistency of fuzz Workspace + run: | + 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@v6 + - name: Install Rust toolchain (${{ matrix.rust }}) + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + - name: Install Cargo fuzz + run: cargo install cargo-fuzz + - name: Fuzz Build + run: cargo fuzz build + - name: Fuzz Check + run: cargo fuzz check + openapi: + name: openapi + needs: [preflight] + if: needs.preflight.outputs.openapi == 'true' + runs-on: ubuntu-latest + container: openapitools/openapi-generator-cli + steps: + - uses: actions/checkout@v6 + - 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@v6 + - uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 + 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: + - stable + target: + - x86_64-unknown-linux-gnu + include: + - rust: stable + experimental: false + steps: + - name: Code checkout + uses: actions/checkout@v6 + 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 (sev_snp) + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + uses: houseabsolute/actions-rust-cross@v1 + with: + command: clippy + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + args: --locked --all --all-targets --no-default-features --tests --examples --features "sev_snp" -- -D warnings + - name: Clippy (igvm) + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + uses: houseabsolute/actions-rust-cross@v1 + with: + command: clippy + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + args: --locked --all --all-targets --no-default-features --tests --examples --features "igvm" -- -D warnings + - name: Clippy (kvm + tdx) + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + uses: houseabsolute/actions-rust-cross@v1 + with: + command: clippy + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + args: --locked --all --all-targets --no-default-features --tests --examples --features "tdx,kvm" -- -D warnings + - 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 + target: + - x86_64-unknown-linux-gnu + steps: + - name: Code checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install musl-gcc + run: sudo apt install -y musl-tools + - name: Install Rust toolchain (${{ matrix.rust }}) + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + - name: Build (default features) + run: cargo build --locked --bin cloud-hypervisor + - name: Build (kvm) + run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm" + - name: Build (default features + tdx) + run: cargo build --locked --bin cloud-hypervisor --features "tdx" + - name: Build (default features + dbus_api) + run: cargo build --locked --bin cloud-hypervisor --features "dbus_api" + - name: Build (default features + guest_debug) + run: cargo build --locked --bin cloud-hypervisor --features "guest_debug" + - name: Build (default features + pvmemcontrol) + run: cargo build --locked --bin cloud-hypervisor --features "pvmemcontrol" + - name: Build (default features + fw_cfg) + run: cargo build --locked --bin cloud-hypervisor --features "fw_cfg" + - name: Build (default features + ivshmem) + run: cargo build --locked --bin cloud-hypervisor --features "ivshmem" + - name: Build (mshv) + run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv" + - name: Build (sev_snp) + run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "sev_snp" + - name: Build (kvm + igvm + sev_snp + fw_cfg) + run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,igvm,sev_snp,fw_cfg" + - name: Build (igvm) + run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "igvm" + - name: Build (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)" + 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: 30 + runs-on: ubuntu-latest + steps: + - name: Code checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Run unit tests + run: scripts/dev_cli.sh tests --unit --libc gnu + + # The single required-status check. Branch protection requires this one job. + all-green: + name: all-green + needs: + - audit + - build + - dco + - formatting + - fuzz-build + - gitlint + - hadolint + - 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 diff --git a/.github/workflows/dco.yaml b/.github/workflows/dco.yaml deleted file mode 100644 index 888b685820..0000000000 --- a/.github/workflows/dco.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: DCO -on: [pull_request, merge_group] - -jobs: - check: - name: DCO Check ("Signed-Off-By") - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.x - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Check DCO - if: ${{ github.event_name == 'pull_request' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - pip3 install -U dco-check - dco-check -e "49699333+dependabot[bot]@users.noreply.github.com" diff --git a/.github/workflows/docker-image.yaml b/.github/workflows/docker-image.yaml deleted file mode 100644 index 6891d60997..0000000000 --- a/.github/workflows/docker-image.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Cloud Hypervisor's Docker image update -on: - push: - branches: main - paths: resources/Dockerfile - pull_request: - paths: resources/Dockerfile -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - main: - runs-on: ubuntu-latest - steps: - - name: Code checkout - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to ghcr - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # generate Docker tags based on the following events/attributes - tags: | - type=raw,value=20250412-0 - type=sha - - - name: Build and push - if: ${{ github.event_name == 'push' }} - uses: docker/build-push-action@v6 - with: - file: ./resources/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - - - name: Build only - if: ${{ github.event_name == 'pull_request' }} - uses: docker/build-push-action@v6 - with: - file: ./resources/Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/flake-bump-auto-approve.yaml b/.github/workflows/flake-bump-auto-approve.yaml new file mode 100644 index 0000000000..1df5f61c50 --- /dev/null +++ b/.github/workflows/flake-bump-auto-approve.yaml @@ -0,0 +1,78 @@ +name: Flake bump auto approve +on: + pull_request_target: + paths: + - 'flake.lock' + branches: + - gardenlinux + +jobs: + gitlint: + name: Flake bump auto approve + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.base.sha }} + fetch-depth: 0 + - name: Fetch pull request head + run: | + git fetch --no-tags origin \ + +refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/pr-head + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade gitlint + # this rule checks the prerequisits and write the exit code in its output + - name: Lint git commit messages + id: gitlint + run: | + set +e + gitlint --commits ${{ github.event.pull_request.base.sha }}..refs/remotes/origin/pr-head -C .gitlint_auto_approve + code=$? + if [ $code -eq 0 ]; then + echo "this merge request is eligible for a flake bump auto approve and merge" + else + echo "this merge request will not be automatically approved." + fi + echo "exit_code=$code" >> "$GITHUB_OUTPUT" + exit 0 + # the following steps only run if gitlint run successful + - name: Create variables + if: steps.gitlint.outputs.exit_code == '0' + id: create_variable + run: | + REPO=$(echo ${GITHUB_REPOSITORY} | cut -f 2 -d '/') + OWNER=$(echo ${GITHUB_REPOSITORY} | cut -f 1 -d '/') + echo "repo=$REPO" >> "$GITHUB_OUTPUT" + echo "owner=$OWNER" >> "$GITHUB_OUTPUT" + - name: Generate token + if: steps.gitlint.outputs.exit_code == '0' && steps.create_variable.outputs.repo != '' + id: generate_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_AUTO_APPROVE_APP_ID }} + private-key: ${{ secrets.GH_AUTO_APPROVE_APP_PRIVATE_KEY }} + owner: ${{ steps.create_variable.outputs.owner }} + repositories: ${{ steps.create_variable.outputs.repo }} + - name: Merge Pull request + if: steps.gitlint.outputs.exit_code == '0' && steps.generate_token.outputs.token != '' + shell: bash + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + # GitHub CLI api + # https://cli.github.com/manual/gh_api + gh api \ + --method PUT \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + /repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/merge \ + -f 'merge_method=rebase' diff --git a/.github/workflows/formatting.yaml b/.github/workflows/formatting.yaml deleted file mode 100644 index b6dd6cafc1..0000000000 --- a/.github/workflows/formatting.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Cloud Hypervisor Code Formatting -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Code Formatting - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - nightly - target: - - x86_64-unknown-linux-gnu - - aarch64-unknown-linux-musl - env: - RUSTFLAGS: -D warnings - steps: - - name: Code checkout - uses: actions/checkout@v4 - - 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 diff --git a/.github/workflows/fuzz-build.yaml b/.github/workflows/fuzz-build.yaml deleted file mode 100644 index db868de2be..0000000000 --- a/.github/workflows/fuzz-build.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Cloud Hypervisor Cargo Fuzz Build -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Cargo Fuzz Build - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - nightly - target: - - x86_64-unknown-linux-gnu - env: - RUSTFLAGS: -D warnings - steps: - - name: Code checkout - uses: actions/checkout@v4 - - 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 diff --git a/.github/workflows/gitlint.yaml b/.github/workflows/gitlint.yaml deleted file mode 100644 index 11ebf707a4..0000000000 --- a/.github/workflows/gitlint.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Commit messages check -on: - pull_request: - -jobs: - gitlint: - name: Check commit messages - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - 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.. diff --git a/.github/workflows/hadolint.yaml b/.github/workflows/hadolint.yaml deleted file mode 100644 index 31b8910984..0000000000 --- a/.github/workflows/hadolint.yaml +++ /dev/null @@ -1,25 +0,0 @@ -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@v4 - - - name: Lint Dockerfile - uses: hadolint/hadolint-action@master - with: - dockerfile: ./resources/Dockerfile - format: tty - no-fail: false - verbose: true - failure-threshold: info diff --git a/.github/workflows/integration-arm64.yaml b/.github/workflows/integration-arm64.yaml deleted file mode 100644 index d580a991cc..0000000000 --- a/.github/workflows/integration-arm64.yaml +++ /dev/null @@ -1,54 +0,0 @@ -name: Cloud Hypervisor Tests (ARM64) -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - timeout-minutes: 120 - name: Tests (ARM64) - runs-on: bookworm-arm64 - steps: - - name: Fix workspace permissions - run: sudo chown -R runner:runner ${GITHUB_WORKSPACE} - - name: Code checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Run unit tests (musl) - run: scripts/dev_cli.sh tests --unit --libc musl - - name: Load openvswitch module - run: sudo modprobe openvswitch - - name: Run integration tests (musl) - timeout-minutes: 60 - run: scripts/dev_cli.sh tests --integration --libc musl - - name: Install Azure CLI - if: ${{ github.event_name != 'pull_request' }} - run: | - sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg - curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null - echo "deb [arch=arm64] https://packages.microsoft.com/repos/azure-cli/ bookworm main" | sudo tee /etc/apt/sources.list.d/azure-cli.list - sudo apt update - sudo apt install -y azure-cli - - name: Download Windows image - if: ${{ github.event_name != 'pull_request' }} - shell: bash - run: | - IMG_BASENAME=windows-11-iot-enterprise-aarch64.raw - IMG_PATH=$HOME/workloads/$IMG_BASENAME - IMG_GZ_PATH=$HOME/workloads/$IMG_BASENAME.gz - IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-9-min.raw.gz - cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/" - pushd "$HOME/workloads" - if sha1sum "$IMG_BASENAME.sha1" --check; then - exit - fi - popd - mkdir -p "$HOME/workloads" - az storage blob download --container-name private-images --file "$IMG_GZ_PATH" --name "$IMG_GZ_BLOB_NAME" --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}" - gzip -d $IMG_GZ_PATH - - name: Run Windows guest integration tests - if: ${{ github.event_name != 'pull_request' }} - timeout-minutes: 30 - run: scripts/dev_cli.sh tests --integration-windows --libc musl diff --git a/.github/workflows/integration-metrics.yaml b/.github/workflows/integration-metrics.yaml deleted file mode 100644 index 440e9ad850..0000000000 --- a/.github/workflows/integration-metrics.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Cloud Hypervisor Tests (Metrics) -on: - push: - branches: - - main - -jobs: - build: - name: Tests (Metrics) - runs-on: bare-metal-9950x - env: - METRICS_PUBLISH_KEY: ${{ secrets.METRICS_PUBLISH_KEY }} - steps: - - name: Code checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Run metrics tests - timeout-minutes: 60 - run: scripts/dev_cli.sh tests --metrics -- -- --report-file /root/workloads/metrics.json - - name: Upload metrics report - run: 'curl -X PUT https://ch-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json' diff --git a/.github/workflows/integration-rate-limiter.yaml b/.github/workflows/integration-rate-limiter.yaml deleted file mode 100644 index 5700bfe46f..0000000000 --- a/.github/workflows/integration-rate-limiter.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Cloud Hypervisor Tests (Rate-Limiter) -on: [merge_group, pull_request] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Tests (Rate-Limiter) - runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'bare-metal-9950x' }} - env: - AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }} - steps: - - name: Code checkout - if: ${{ github.event_name != 'pull_request' }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Run rate-limiter integration tests - if: ${{ github.event_name != 'pull_request' }} - timeout-minutes: 20 - run: scripts/dev_cli.sh tests --integration-rate-limiter - - name: Skipping build for PR - if: ${{ github.event_name == 'pull_request' }} - run: echo "Skipping build for PR" diff --git a/.github/workflows/integration-vfio.yaml b/.github/workflows/integration-vfio.yaml deleted file mode 100644 index 3549ace272..0000000000 --- a/.github/workflows/integration-vfio.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Cloud Hypervisor Tests (VFIO) -on: [merge_group, pull_request] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Tests (VFIO) - runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'vfio-nvidia' }} - env: - AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }} - steps: - - name: Fix workspace permissions - if: ${{ github.event_name != 'pull_request' }} - run: sudo chown -R runner:runner ${GITHUB_WORKSPACE} - - name: Code checkout - if: ${{ github.event_name != 'pull_request' }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Run VFIO integration tests - if: ${{ github.event_name != 'pull_request' }} - timeout-minutes: 15 - run: scripts/dev_cli.sh tests --integration-vfio - # Most tests are failing with musl see #6790 - # - name: Run VFIO integration tests for musl - # if: ${{ github.event_name != 'pull_request' }} - # timeout-minutes: 15 - # run: scripts/dev_cli.sh tests --integration-vfio --libc musl - - name: Skipping build for PR - if: ${{ github.event_name == 'pull_request' }} - run: echo "Skipping build for PR" diff --git a/.github/workflows/integration-windows.yaml b/.github/workflows/integration-windows.yaml deleted file mode 100644 index 29aa04a78f..0000000000 --- a/.github/workflows/integration-windows.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: Cloud Hypervisor Tests (Windows Guest) -on: [merge_group, pull_request] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Tests (Windows Guest) - runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'garm-jammy-16' }} - steps: - - name: Code checkout - if: ${{ github.event_name != 'pull_request' }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install Docker - if: ${{ github.event_name != 'pull_request' }} - run: | - sudo apt-get update - sudo apt-get -y install ca-certificates curl gnupg - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg - sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt install -y docker-ce docker-ce-cli - - name: Install Azure CLI - if: ${{ github.event_name != 'pull_request' }} - run: | - sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg - curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null - echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list - sudo apt update - sudo apt install -y azure-cli - - name: Download Windows image - if: ${{ github.event_name != 'pull_request' }} - run: | - mkdir $HOME/workloads - az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2022-amd64-2.raw" --name windows-server-2022-amd64-2.raw --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}" - - name: Run Windows guest integration tests - if: ${{ github.event_name != 'pull_request' }} - timeout-minutes: 15 - run: scripts/dev_cli.sh tests --integration-windows - - name: Run Windows guest integration tests for musl - if: ${{ github.event_name != 'pull_request' }} - timeout-minutes: 15 - run: scripts/dev_cli.sh tests --integration-windows --libc musl - - name: Skipping build for PR - if: ${{ github.event_name == 'pull_request' }} - run: echo "Skipping build for PR" \ No newline at end of file diff --git a/.github/workflows/integration-x86-64.yaml b/.github/workflows/integration-x86-64.yaml deleted file mode 100644 index 80690512f5..0000000000 --- a/.github/workflows/integration-x86-64.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: Cloud Hypervisor Tests (x86-64) -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - runner: ['garm-jammy', "garm-jammy-amd"] - libc: ["musl", 'gnu'] - name: Tests (x86-64) - runs-on: ${{ github.event_name == 'pull_request' && !(matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') && 'ubuntu-latest' || format('{0}-16', matrix.runner) }} - steps: - - name: Code checkout - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install Docker - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - run: | - sudo apt-get update - sudo apt-get -y install ca-certificates curl gnupg - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg - sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt install -y docker-ce docker-ce-cli - - name: Prepare for VDPA - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - run: scripts/prepare_vdpa.sh - - name: Run unit tests - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - run: scripts/dev_cli.sh tests --unit --libc ${{ matrix.libc }} - - name: Load openvswitch module - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - run: sudo modprobe openvswitch - - name: Run integration tests - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - timeout-minutes: 40 - run: scripts/dev_cli.sh tests --integration --libc ${{ matrix.libc }} - - name: Run live-migration integration tests - if: ${{ github.event_name != 'pull_request' || (matrix.runner == 'garm-jammy' && matrix.libc == 'gnu') }} - timeout-minutes: 20 - run: scripts/dev_cli.sh tests --integration-live-migration --libc ${{ matrix.libc }} - - name: Skipping build for PR - if: ${{ github.event_name == 'pull_request' && matrix.runner != 'garm-jammy' && matrix.libc != 'gnu' }} - run: echo "Skipping build for PR" diff --git a/.github/workflows/lychee.yaml b/.github/workflows/lychee.yaml deleted file mode 100644 index dd3a372dc8..0000000000 --- a/.github/workflows/lychee.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Link Check (lychee) -on: - pull_request - -jobs: - link_check: - name: Link Check - runs-on: ubuntu-latest - steps: - - name: Code checkout - uses: actions/checkout@v4 - - - name: Link Availability Check - uses: lycheeverse/lychee-action@master - with: - args: --verbose --config .lychee.toml . diff --git a/.github/workflows/openapi.yaml b/.github/workflows/openapi.yaml deleted file mode 100644 index 0cd5b848cc..0000000000 --- a/.github/workflows/openapi.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: Cloud Hypervisor OpenAPI Validation -on: [pull_request, merge_group] - -jobs: - Validate: - runs-on: ubuntu-latest - container: openapitools/openapi-generator-cli - steps: - - uses: actions/checkout@v4 - - name: Validate OpenAPI - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - /usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml diff --git a/.github/workflows/package-consistency.yaml b/.github/workflows/package-consistency.yaml deleted file mode 100644 index 0c57baa6c0..0000000000 --- a/.github/workflows/package-consistency.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Cloud Hypervisor Consistency -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Rust VMM Consistency Check - runs-on: ubuntu-latest - steps: - - name: Code checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install dependencies - run: sudo apt install -y python3 - - - name: Install Rust toolchain stable - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - - name: Check Rust VMM Package Consistency of root Workspace - run: python3 scripts/package-consistency-check.py github.com/rust-vmm - - - name: Check Rust VMM Package Consistency of fuzz Workspace - run: | - pushd fuzz - python3 ../scripts/package-consistency-check.py github.com/rust-vmm - popd diff --git a/.github/workflows/preview-riscv64.yaml b/.github/workflows/preview-riscv64.yaml deleted file mode 100644 index 84435402a8..0000000000 --- a/.github/workflows/preview-riscv64.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: Cloud Hypervisor RISC-V 64-bit Preview -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Cargo - runs-on: riscv64-qemu-host - strategy: - fail-fast: false - matrix: - module: - - hypervisor - - arch - - vm-allocator - - devices - - steps: - - name: Code checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install Rust toolchain - run: /opt/scripts/exec-in-qemu.sh rustup default 1.83.0 - - - name: Build ${{ matrix.module }} Module (kvm) - run: /opt/scripts/exec-in-qemu.sh cargo rustc --locked -p ${{ matrix.module }} --no-default-features --features "kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Clippy ${{ matrix.module }} Module (kvm) - run: /opt/scripts/exec-in-qemu.sh cargo clippy --locked -p ${{ matrix.module }} --no-default-features --features "kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Test ${{ matrix.module }} Module (kvm) - run: /opt/scripts/exec-in-qemu.sh cargo test --locked -p ${{ matrix.module }} --no-default-features --features "kvm" - - - name: Check no files were modified - run: test -z "$(git status --porcelain)" diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml deleted file mode 100644 index 19a4981a4d..0000000000 --- a/.github/workflows/quality.yaml +++ /dev/null @@ -1,146 +0,0 @@ -name: Cloud Hypervisor Quality Checks -on: [pull_request, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Quality (clippy) - runs-on: ubuntu-latest - continue-on-error: ${{ matrix.experimental }} - strategy: - fail-fast: false - matrix: - rust: - - beta - - stable - target: - - aarch64-unknown-linux-gnu - - aarch64-unknown-linux-musl - - x86_64-unknown-linux-gnu - - x86_64-unknown-linux-musl - - include: - - rust: beta - experimental: true - - rust: stable - experimental: false - - steps: - - name: Code checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install Rust toolchain (${{ matrix.rust }}) - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - target: ${{ matrix.target }} - override: true - components: clippy - - - name: Bisectability Check (default features) - if: ${{ github.event_name == 'pull_request' && matrix.target == 'x86_64-unknown-linux-gnu' }} - run: | - set -e - commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }}) - for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done - git checkout ${{ github.sha }} - - - name: Clippy (kvm) - uses: 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 -W clippy::assertions_on_result_states - - - name: Clippy (mshv) - 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 -W clippy::assertions_on_result_states - - - name: Clippy (mshv + 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 "mshv,kvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - 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 -W clippy::assertions_on_result_states - - - 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 -W clippy::assertions_on_result_states - - - name: Clippy (default features + pvmemcontrol) - 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 "pvmemcontrol" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - 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 -W clippy::assertions_on_result_states - - - name: Clippy (mshv) - if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "mshv" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Clippy (mshv + kvm) - if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - uses: actions-rs/cargo@v1 - with: - 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 -W clippy::assertions_on_result_states - - - name: Clippy (sev_snp) - if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - uses: actions-rs/cargo@v1 - with: - 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 -W clippy::assertions_on_result_states - - - name: Clippy (igvm) - if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --target=${{ matrix.target }} --locked --all --all-targets --no-default-features --tests --examples --features "igvm" -- -D warnings -D clippy::undocumented_unsafe_blocks -W clippy::assertions_on_result_states - - - name: Clippy (kvm + tdx) - if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - uses: actions-rs/cargo@v1 - with: - 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 -W clippy::assertions_on_result_states - - - 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@v4 - # Executes "typos ." - - uses: crate-ci/typos@v1.34.0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 01d4d6d810..0000000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,95 +0,0 @@ -name: Cloud Hypervisor Release -on: [create, merge_group] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} - cancel-in-progress: true -env: - GITHUB_TOKEN: ${{ github.token }} - -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 - runs-on: ubuntu-latest - steps: - - name: Code checkout - uses: actions/checkout@v4 - - 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 - with: - command: build - target: ${{ matrix.platform.target }} - args: ${{ matrix.platform.args }} - strip: true - toolchain: "1.83.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@v4 - with: - name: Artifacts for ${{ matrix.platform.target }} - path: | - ./${{ matrix.platform.name_ch }} - ./${{ matrix.platform.name_ch_remote }} - - name: Vendor - if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && - matrix.platform.target == 'x86_64-unknown-linux-gnu' - working-directory: ../cloud-hypervisor-${{ github.event.ref }} - run: | - mkdir ../vendor-cargo-home - export CARGO_HOME=$(realpath ../vendor-cargo-home) - mkdir .cargo - cargo vendor > .cargo/config.toml - - name: Create vendored source archive - if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && - matrix.platform.target == 'x86_64-unknown-linux-gnu' - run: tar cJf cloud-hypervisor-${{ github.event.ref }}.tar.xz ../cloud-hypervisor-${{ github.event.ref }} - - name: Upload cloud-hypervisor vendored source archive - if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && - matrix.platform.target == 'x86_64-unknown-linux-gnu' - id: upload-release-cloud-hypervisor-vendored-sources - uses: actions/upload-artifact@v4 - 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@v2 - with: - draft: true - files: | - ./${{ matrix.platform.name_ch }} - ./${{ matrix.platform.name_ch_remote }} - ./cloud-hypervisor-${{ github.event.ref }}.tar.xz diff --git a/.github/workflows/reuse.yaml b/.github/workflows/reuse.yaml deleted file mode 100644 index a2161c2818..0000000000 --- a/.github/workflows/reuse.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: REUSE Compliance Check - -on: [push, pull_request] - -jobs: - reuse: - name: REUSE Compliance Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: REUSE Compliance Check - uses: fsfe/reuse-action@v5 diff --git a/.github/workflows/shlint.yaml b/.github/workflows/shlint.yaml deleted file mode 100644 index 9089964f06..0000000000 --- a/.github/workflows/shlint.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: Shell scripts check -on: - pull_request: - merge_group: - push: - branches: - - main - -jobs: - sh-checker: - name: Check shell scripts - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Run the shell script checkers - uses: luizm/action-sh-checker@master - env: - SHFMT_OPTS: -i 4 -d - SHELLCHECK_OPTS: -x --source-path scripts diff --git a/.github/workflows/taplo.yaml b/.github/workflows/taplo.yaml deleted file mode 100644 index 2b1e618984..0000000000 --- a/.github/workflows/taplo.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: Cargo.toml Formatting (taplo) -on: - pull_request: - paths: - - '**/Cargo.toml' - -jobs: - cargo_toml_format: - name: Cargo.toml Formatting - runs-on: ubuntu-latest - steps: - - name: Code checkout - uses: actions/checkout@v4 - - 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 diff --git a/.gitignore b/.gitignore index ed2e4ec24e..45be7c88ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ -/build -/.cargo -/target **/*.rs.bk **/Cargo.lock **/rusty-tags.vi -/rpm/SOURCES +/.agents +/.cargo +/.claude +/.codex /.vscode +/build +/rpm/SOURCES +/target /vendor __pycache__ diff --git a/.gitlint b/.gitlint index 455dd0281a..d65c4ab73e 100644 --- a/.gitlint +++ b/.gitlint @@ -1,7 +1,7 @@ [general] extra-path=scripts/gitlint/rules regex-style-search=true -ignore=body-max-line-length +ignore=body-max-line-length,body-hard-tab [ignore-by-author-name] regex=dependabot diff --git a/.gitlint_auto_approve b/.gitlint_auto_approve new file mode 100644 index 0000000000..d79e9b21f5 --- /dev/null +++ b/.gitlint_auto_approve @@ -0,0 +1,15 @@ +[general] +extra-path=ci/gitlint/rules_auto_approve +regex-style-search=true +ignore=body-is-missing,body-max-line-length + +# default 72 +[title-max-length] +line-length=72 + +# Empty bodies are fine +[body-min-length] +min-length=0 + +[UC-flake] +filepath=flake.lock diff --git a/.lychee.toml b/.lychee.toml index 9eb8f9fdf7..54cd92f57a 100644 --- a/.lychee.toml +++ b/.lychee.toml @@ -1,21 +1,33 @@ verbose = "info" -exclude = [ - # Availability of links below should be manually verified. - # Page for intel SGX support, returns 403 while querying. - '^https://www.intel.com/content/www/us/en/developer/tools/software-guard-extensions/linux-overview.html', - # 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', +exclude_path = [".lychee.toml"] - # OSDev has added bot protection and accesses my result in 403 Forbidden. - '^https://wiki.osdev.org', +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/', ] +# Exclude loopback addresses +exclude_loopback = true + max_retries = 3 retry_wait_time = 5 diff --git a/.reuse/dep5 b/.reuse/dep5 index d7f2867324..e624ecf662 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -7,6 +7,6 @@ Files: docs/*.md *.md Copyright: 2024 License: CC-BY-4.0 -Files: scripts/* test_data/* *.toml .git* fuzz/Cargo.lock fuzz/.gitignore resources/linux-config-* vmm/src/api/openapi/cloud-hypervisor.yaml CODEOWNERS Cargo.lock +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 flake.nix flake.lock chv.nix .envrc Copyright: 2024 License: Apache-2.0 diff --git a/.rustfmt.toml b/.rustfmt.toml index 754d7badfd..394a1065be 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,4 @@ -edition = "2021" +edition = "2024" group_imports="StdExternalCrate" imports_granularity="Module" diff --git a/.taplo.toml b/.taplo.toml index 9e7ab9e996..3b2f6f015d 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -1,5 +1,6 @@ include = ["**/Cargo.toml"] [formatting] +indent_string = " " # 2 spaces: keep in sync with .editorconfig reorder_arrays = true reorder_keys = true diff --git a/.typos.toml b/.typos.toml index de411b118b..ef9c7b96d0 100644 --- a/.typos.toml +++ b/.typos.toml @@ -2,15 +2,18 @@ [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-ignore-re = ["_TME_"] [default.extend-words] CLASSE = "CLASSE" Dake = "Dake" EXTINT = "EXTINT" INOUT = "INOUT" +MSIS = "MSIS" # MSIs (Message Signaled Interrupt) SME = "SME" # Secure Memory Encryption THR = "THR" # Transmitter Holding Register TRANSLATER = "TRANSLATER" @@ -20,5 +23,12 @@ liness = "liness" outout = "outout" [default.extend-identifiers] +consts = "consts" fo = "fo" fpr = "fpr" +# Public Linux API +msg_controllen = "msg_controllen" +tme = "tme" +l3c_qm_conver_factor = "l3c_qm_conver_factor" +IA32_PMC_GPn_CFG_C = "IA32_PMC_GPn_CFG_C" +IA32_PMC_FXm_CFG_C = "IA32_PMC_FXm_CFG_C" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..2c6a5d4bf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,111 @@ +## 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. + +This checkout is a Cyberus Technology fork of Cloud Hypervisor. It is maintained +independently from upstream, while still following upstream contribution and +code-quality guidance unless fork-specific requirements say otherwise. + +## 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! +- The main supported architectures are `x86_64` and `aarch64`; the main + hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the most regular + exercise, but changes must not make the other first-class targets worse. +- Treat live migration and the vCPU lifecycle as first-class production areas. + Preserve deterministic state transfer, robust failure handling, correct device + and memory state, and explicit race-free vCPU state transitions. + +### 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. +- Prefer simple solutions over unnecessary traits, excessive indirection, or + premature abstraction. +- Prefer `Result` over panics for recoverable production-path errors. Handle + syscall and KVM ioctl return values explicitly and include useful context in + error messages. +- Do not invent APIs, behavior, or requirements. If something is uncertain, + state the uncertainty and proceed only with minimal, explicit assumptions. + +### 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. +- Keep KVM code aligned with the kernel API. Do not rely on undocumented + behavior or ignore backend-specific failure modes. +- Keep docs and comments short and useful. Document non-trivial invariants at + struct definitions and critical state transitions. +- Logging should be minimal and high signal. Use `info!` for important normal + state changes that matter in production; use `warn!` or `error!` only for + abnormal conditions. Keep `debug!` for focused diagnostics. + +### Build and Test Notes + +- 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. Do not + assume the tests can be run directly in a restricted agent environment; ask + the developer to run them when real integration coverage is needed. +- For broader VM behavior, this fork also uses an external `libvirt-tests` suite + outside this repository. If a change likely needs that coverage, say so and + ask whether it should be run, skipped, or handled manually by the developer. + Only run it yourself if the developer provides the necessary instructions and + access details. + +### 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. +- 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. +- Commits need a `On-behalf-of: SAP $firstname.$lastname@sap.com` trailer: e.g.: + ``` + $component: $summary + + $body + + On-behalf-of: SAP philipp.schuster@sap.com + Signed-off-by: Philipp Schuster + ``` + as our work is sponsored by SAP, which gets its money from the EU (Apeiro + project). The enforcing CI rule is in + `./scripts/gitlint/rules/on-behalf-of-marker.py` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdd75e31a1..3d9f6b06f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,14 +11,55 @@ license of those projects. New code should be under the [Apache v2 License](https://opensource.org/licenses/Apache-2.0). -## Coding Style +## Coding Style & Code Comments -We follow the [Rust Style](https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src) -convention and enforce it through the Continuous Integration (CI) process calling into `rustfmt` -for each submitted Pull Request (PR). +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. + +[Rust Style]: https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src ## Basic Checks +```sh +# We currently rely on nightly-only formatting features +cargo +nightly fmt --all +cargo check --all-targets --tests +cargo clippy --all-targets --tests +# Please note that this will not execute integration tests. +cargo test --all-targets --tests + +# To lint your last three commits +gitlint --commits "HEAD~3..HEAD" +``` + +### \[Optional\] Run Integration Tests + +_Caution: These tests are taking a long time to complete (40+ mins) and need special setup._ + +```sh + bash ./scripts/dev_cli.sh tests --integration -- --test-filter '' +``` + +### 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. @@ -26,9 +67,9 @@ have specific features that you regularly develop against. ```sh #!/bin/sh -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 +cargo +nightly fmt --all -- --check || exit 1 +cargo check --locked --all-targets --tests || exit 1 +cargo clippy --locked --all-targets --tests -- -D warnings || exit 1 ``` You will need to `chmod +x .git/hooks/pre-commit` to have it run on every @@ -36,42 +77,65 @@ commit you make. ## Certificate of Origin -In order to get a clear contribution chain of trust we use the [signed-off-by language](https://web.archive.org/web/20230406041855/https://01.org/community/signed-process) +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) used by the Linux kernel project. -## Patch format +## Patch format & Git Commit Hygiene -Beside the signed-off-by footer, we expect each patch to comply with the following format: +_We use **Patch** as synonym for **Commit**._ -``` -: Change summary +We require patches to: -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. +- Have a `Signed-off-by: Name ` footer +- Follow the pattern: \ + ``` + : 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. + + Signed-off-by: + ``` -Signed-off-by: -``` -For example: +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: ``` 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 ``` +### Git Commit History + +We value a clean, **reviewable** commit history. Each commit should represent +a self-contained, logical step that guides reviewers clearly from A to B. + +Avoid patterns like `init A -> init B -> fix A` or \ +`init design A -> revert A -> use design B`. Commits must be independently +reviewable - don't leave "fix previous commit" or earlier design attempts in +the history. + +Intermediate work-in-progress changes are acceptable only if a subsequent +commit in the same series cleans them up (e.g. a temporary `#[allow(unused)]` +removed in the next commit). + ## Pull requests Cloud Hypervisor uses the “fork-and-pull” development model. Follow these steps if @@ -82,10 +146,14 @@ you want to merge your changes to `cloud-hypervisor`: 1. Within your fork, create a branch for your contribution. 1. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) against the main branch of the Cloud Hypervisor repository. -1. To update your pull request amend existing commits whenever applicable and - then push the new changes to your pull request branch. +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 `: addressing review for A`-style commits. + ## Issue tracking If you have a problem, please let us know. We recommend using @@ -101,16 +169,83 @@ 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 ``` 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. diff --git a/Cargo.lock b/Cargo.lock index afa7538716..86cfecee80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,17 +4,18 @@ version = 4 [[package]] name = "acpi_tables" -version = "0.1.0" -source = "git+https://github.com/rust-vmm/acpi_tables?branch=main#e08a3f0b0a59b98859dbf59f5aa7fd4d2eb4018a" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad581b2b0fa02638f3df6ff3f852ebc30dc7cfe531e9745d1ca4c0f283a6dbe" dependencies = [ - "zerocopy 0.8.26", + "zerocopy", ] [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -27,18 +28,18 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.15" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -51,57 +52,61 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "api_client" version = "0.1.0" dependencies = [ - "thiserror 2.0.12", + "thiserror", "vmm-sys-util", ] [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "arch" @@ -109,25 +114,31 @@ version = "0.1.0" dependencies = [ "anyhow", "byteorder", + "clap", "fdt", + "flate2", "hypervisor", "libc", "linux-loader", "log", + "prettyplease", + "proptest", + "quote", "serde", - "thiserror 2.0.12", + "serde_json", + "syn", + "thiserror", "uuid", "vm-fdt", "vm-memory", - "vm-migration", "vmm-sys-util", ] [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", @@ -137,9 +148,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -149,41 +160,41 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", + "pin-project-lite", "slab", ] [[package]] name = "async-io" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix 1.0.7", + "rustix", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -192,9 +203,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", @@ -205,8 +216,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 0.38.44", - "tracing", + "rustix", ] [[package]] @@ -222,9 +232,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -232,10 +242,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.44", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -246,9 +256,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -263,15 +273,37 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -279,9 +311,24 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitfield-struct" version = "0.10.1" @@ -293,6 +340,17 @@ dependencies = [ "syn", ] +[[package]] +name = "bitfield-struct" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca6739863c590881f038d033a146c51ddae239186a4327014839fd864f44ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -301,36 +359,49 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block" version = "0.1.0" dependencies = [ + "bitflags 2.11.1", "byteorder", + "cfg-if", "crc-any", + "flate2", "io-uring", "libc", "log", "remain", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror", "uuid", "virtio-bindings", "virtio-queue", "vm-memory", "vm-virtio", "vmm-sys-util", + "zstd", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", ] [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -341,9 +412,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -353,39 +424,47 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cfg_aliases" -version = "0.2.1" +name = "chacha20" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] [[package]] name = "clap" -version = "4.5.13" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.13" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -396,22 +475,25 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cloud-hypervisor" -version = "46.0.0" +version = "52.0.0" dependencies = [ "anyhow", "api_client", + "block", "clap", "dhat", "dirs", + "env_logger", "epoll", "event_monitor", "hypervisor", + "jiff", "libc", "log", "net_util", @@ -420,21 +502,31 @@ dependencies = [ "serde_json", "signal-hook", "test_infra", - "thiserror 2.0.12", + "thiserror", "tpm", "tracer", "vm-memory", + "vm-migration", "vmm", "vmm-sys-util", "wait-timeout", "zbus", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concat-idents" @@ -455,6 +547,21 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc-any" version = "2.5.0" @@ -466,9 +573,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -479,11 +586,20 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" -version = "0.20.10" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -491,11 +607,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -505,9 +620,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -527,22 +642,25 @@ dependencies = [ "acpi_tables", "anyhow", "arch", - "bitflags 2.9.0", + "bitfield-struct 0.13.0", + "bitflags 2.11.1", "byteorder", "event_monitor", "hypervisor", "libc", + "linux-loader", "log", "num_enum", "pci", "serde", - "thiserror 2.0.12", + "thiserror", "tpm", "vm-allocator", "vm-device", "vm-memory", "vm-migration", "vmm-sys-util", + "zerocopy", ] [[package]] @@ -554,13 +672,24 @@ dependencies = [ "backtrace", "lazy_static", "mintex", - "parking_lot 0.12.1", + "parking_lot", "rustc-hash", "serde", "serde_json", "thousands", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -579,20 +708,32 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -600,9 +741,9 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", @@ -611,9 +752,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -621,24 +762,24 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "epoll" -version = "4.3.3" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" +checksum = "e74d68fe2927dbf47aa976d14d93db9b23dced457c7bb2bdc6925a16d31b736e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.1", "libc", ] @@ -650,19 +791,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -671,9 +812,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -685,15 +826,19 @@ version = "0.1.0" dependencies = [ "flume", "libc", + "log", "serde", "serde_json", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +dependencies = [ + "getrandom 0.3.4", +] [[package]] name = "fdt" @@ -701,15 +846,31 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784a4df722dc6267a04af36895398f59d21d07dce47232adf31ec0ff2fa45e67" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ + "fastrand", "futures-core", "futures-sink", - "nanorand", "spin", ] @@ -719,11 +880,23 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -736,9 +909,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -746,15 +919,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -763,15 +936,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -782,9 +955,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -793,21 +966,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -817,29 +990,28 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "gdbstub" -version = "0.7.1" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6341b3480afbb34eaefc7f92713bc92f2d83e338aaa1c44192f9c2956f4a4903" +checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.1", "cfg-if", "log", "managed", "num-traits", - "paste", + "pastey", ] [[package]] name = "gdbstub_arch" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3b1357bd3203fc09a6601327ae0ab38865d14231d0b65d3143f5762cc7977d" +checksum = "6c02bfe7bd65f42bcda751456869dfa1eb2bd1c36e309b9ec27f4888d41cf258" dependencies = [ "gdbstub", "num-traits", @@ -847,52 +1019,81 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -901,10 +1102,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "humantime" -version = "2.1.0" +name = "hybrid-array" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] [[package]] name = "hypervisor" @@ -912,7 +1116,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arc-swap", - "bitfield-struct", + "bitfield-struct 0.13.0", "byteorder", "cfg-if", "concat-idents", @@ -930,11 +1134,11 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.12", + "thiserror", "vfio-ioctls", "vm-memory", "vmm-sys-util", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] @@ -946,6 +1150,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -954,59 +1164,72 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "igvm" -version = "0.3.4" -source = "git+https://github.com/microsoft/igvm?branch=main#01daa631a596459cb4de58505881007dd13d4410" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67578b05ebcdfa1aa0fe13f77a13bdd7d87036128898a327f1bf8e7356cf09cd" dependencies = [ - "bitfield-struct", + "bitfield-struct 0.10.1", "crc32fast", "hex", "igvm_defs", "open-enum", "range_map_vec", "static_assertions", - "thiserror 2.0.12", + "thiserror", "tracing", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] name = "igvm_defs" -version = "0.3.4" -source = "git+https://github.com/microsoft/igvm?branch=main#01daa631a596459cb4de58505881007dd13d4410" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eedd8c64460676101062f9f2ecdeb52d8f43e622da6a6c5bf5158f4ef08b0906" dependencies = [ - "bitfield-struct", + "bitfield-struct 0.10.1", "open-enum", "static_assertions", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] name = "indexmap" -version = "2.8.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] -name = "instant" -version = "0.1.13" +name = "io-uring" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ + "bitflags 2.11.1", "cfg-if", + "libc", ] [[package]] -name = "io-uring" -version = "0.6.4" +name = "iommufd-bindings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7de3a04f6fd55f171a6682852f7aa360bb848a85e0c610513349e006b3c139" + +[[package]] +name = "iommufd-ioctls" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5" +checksum = "4eabd3414d9c4e716c9a198fbfac484625f088c075605372daf037edfe336e18" dependencies = [ - "bitflags 1.3.2", - "libc", + "iommufd-bindings", + "thiserror", + "vmm-sys-util", ] [[package]] @@ -1020,44 +1243,89 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "kvm-bindings" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4933174d0cc4b77b958578cd45784071cc5ae212c2d78fbd755aaaa6dfa71a" +checksum = "4b3c06ff73c7ce03e780887ec2389d62d2a2a9ddf471ab05c2ff69207cd3f3b4" dependencies = [ "serde", "vmm-sys-util", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "kvm-ioctls" -version = "0.19.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e013ae7fcd2c6a8f384104d16afe7ea02969301ea2bb2a56e44b011ebc907cab" +checksum = "333f77a20344a448f3f70664918135fddeb804e938f28a99d685bd92926e0b19" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.1", "kvm-bindings", "libc", "vmm-sys-util", @@ -1065,13 +1333,13 @@ dependencies = [ [[package]] name = "landlock" -version = "0.4.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafb8a4afee64f167eb2b52d32f0eea002e41a7a6450e68c799c8ec3a81a634c" +checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088" dependencies = [ "enumflags2", "libc", - "thiserror 1.0.62", + "thiserror", ] [[package]] @@ -1080,27 +1348,32 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.172" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.9.0", "libc", ] [[package]] name = "libssh2-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" dependencies = [ "cc", "libc", @@ -1112,9 +1385,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.18" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -1124,40 +1397,33 @@ dependencies = [ [[package]] name = "linux-loader" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870c3814345f050991f99869417779f6062542bcf4ed81db7a1b926ad1306638" +checksum = "de72cb02c55ecffcf75fe78295926f872eb6eb0a58d629c58a8c324dc26380f6" dependencies = [ "vm-memory", ] [[package]] name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "managed" @@ -1167,9 +1433,9 @@ checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" [[package]] name = "memchr" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -1183,7 +1449,7 @@ dependencies = [ [[package]] name = "micro_http" version = "0.1.0" -source = "git+https://github.com/firecracker-microvm/micro-http?branch=main#4f621532e81ee2ad096a9c9592fdacc40d19de48" +source = "git+https://github.com/firecracker-microvm/micro-http?branch=main#5c2254d6cf4f32a668d0d8e57ba20bebad9d4fba" dependencies = [ "libc", "vmm-sys-util", @@ -1191,76 +1457,60 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mintex" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bec4598fddb13cc7b528819e697852653252b760f1228b7642679bf2ff2cd07" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "mshv-bindings" -version = "0.5.1" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909de5fd4a5a3347a6c62872f6816e6279efd8615a753f10a3bc4daaef8a72ef" +checksum = "83303108160c2b7a7bdd25000ee679384e19471386d23e501ed832574c9229ef" dependencies = [ "libc", "num_enum", "serde", "serde_derive", "vmm-sys-util", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] name = "mshv-ioctls" -version = "0.5.1" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7d94972588d562bd349b916de6a43f2ee268e6e9c91cfb5b30549ed4ea2751" +checksum = "1db4449ac7012237b133da366f5b32ce4af1f8caf770486e5a9d54f7f6b73c4c" dependencies = [ "libc", "mshv-bindings", - "thiserror 2.0.12", + "thiserror", "vmm-sys-util", ] [[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "net_gen" -version = "0.1.0" -dependencies = [ - "vmm-sys-util", -] - -[[package]] -name = "net_util" +name = "net_util" version = "0.1.0" dependencies = [ "epoll", - "getrandom 0.3.3", + "getrandom 0.4.2", "libc", "log", - "net_gen", "pnet", "pnet_datalink", "rate_limiter", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror", "virtio-bindings", "virtio-queue", "vm-memory", @@ -1268,19 +1518,6 @@ dependencies = [ "vmm-sys-util", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "no-std-net" version = "0.6.0" @@ -1298,18 +1535,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1319,18 +1557,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open-enum" @@ -1354,18 +1598,18 @@ dependencies = [ [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -1384,7 +1628,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" name = "option_parser" version = "0.1.0" dependencies = [ - "thiserror 2.0.12", + "thiserror", ] [[package]] @@ -1405,57 +1649,32 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "instant", "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.9", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.6" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", - "instant", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", - "winapi", + "windows-link", ] [[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.4.1", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" [[package]] name = "pci" @@ -1467,7 +1686,7 @@ dependencies = [ "libc", "log", "serde", - "thiserror 2.0.12", + "thiserror", "vfio-bindings", "vfio-ioctls", "vfio_user", @@ -1482,32 +1701,28 @@ dependencies = [ name = "performance-metrics" version = "0.1.0" dependencies = [ + "block", "clap", "dirs", + "libc", "serde", "serde_json", "test_infra", - "thiserror 2.0.12", - "wait-timeout", + "thiserror", + "vmm-sys-util", ] [[package]] name = "pin-project-lite" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -1516,9 +1731,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "pnet" @@ -1613,69 +1828,135 @@ dependencies = [ [[package]] name = "polling" -version = "3.6.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.52.0", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", ] [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", ] [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.11.1", + "num-traits", + "rand 0.9.4", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -1685,16 +1966,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", ] [[package]] @@ -1710,44 +2006,35 @@ dependencies = [ "epoll", "libc", "log", - "thiserror 2.0.12", + "thiserror", "vmm-sys-util", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.12", + "thiserror", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1757,9 +2044,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1768,9 +2055,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "remain" @@ -1783,11 +2070,25 @@ dependencies = [ "syn", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -1797,41 +2098,69 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "rustix" -version = "1.0.7" +name = "rustls" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "ryu" -version = "1.0.20" +name = "rusty-fork" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] [[package]] name = "scopeguard" @@ -1848,20 +2177,36 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" -version = "1.0.208" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1870,20 +2215,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", @@ -1892,20 +2239,19 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ - "serde", - "serde_derive", + "serde_core", "serde_with_macros", ] [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", @@ -1917,6 +2263,17 @@ dependencies = [ name = "serial_buffer" version = "0.1.0" +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1925,9 +2282,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.18" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -1935,27 +2292,31 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "spin" @@ -1968,14 +2329,14 @@ dependencies = [ [[package]] name = "ssh2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" +checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", "libssh2-sys", - "parking_lot 0.11.2", + "parking_lot", ] [[package]] @@ -1990,11 +2351,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" -version = "2.0.104" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2003,25 +2370,25 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.4.2", "once_cell", - "rustix 0.38.44", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "rustix 0.38.44", - "windows-sys 0.48.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2031,48 +2398,28 @@ dependencies = [ "dirs", "epoll", "libc", - "serde", + "rand 0.10.1", "serde_json", "ssh2", - "thiserror 2.0.12", + "thiserror", "vmm-sys-util", "wait-timeout", ] [[package]] name = "thiserror" -version = "1.0.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" -dependencies = [ - "thiserror-impl 1.0.62", -] - -[[package]] -name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2087,18 +2434,31 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] name = "toml_datetime" -version = "0.6.8" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ "winnow", ] @@ -2107,11 +2467,9 @@ name = "tpm" version = "0.1.0" dependencies = [ "anyhow", - "byteorder", "libc", "log", - "net_gen", - "thiserror 2.0.12", + "thiserror", "vmm-sys-util", ] @@ -2127,9 +2485,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2138,9 +2496,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -2149,29 +2507,53 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "utf8parse" @@ -2181,28 +2563,17 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", - "rand", - "uuid-macro-internal", + "rand 0.10.1", + "serde_core", "wasm-bindgen", ] -[[package]] -name = "uuid-macro-internal" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -2211,25 +2582,29 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vfio-bindings" -version = "0.4.0" -source = "git+https://github.com/rust-vmm/vfio?branch=main#3d158a14460cac7ca3c99c2effa0a46880935cb0" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "188dac3057a0cbc94470085204c84b82ff7ec5dac629a514323cd133d1f9abe0" dependencies = [ "vmm-sys-util", ] [[package]] name = "vfio-ioctls" -version = "0.4.0" -source = "git+https://github.com/rust-vmm/vfio?branch=main#3d158a14460cac7ca3c99c2effa0a46880935cb0" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b1d98dff7f0d219278e406323e7eda4d426447bd203c7828189baf0d8c07b7" dependencies = [ "byteorder", + "iommufd-bindings", + "iommufd-ioctls", "kvm-bindings", "kvm-ioctls", "libc", "log", "mshv-bindings", "mshv-ioctls", - "thiserror 1.0.62", + "thiserror", "vfio-bindings", "vm-memory", "vmm-sys-util", @@ -2237,16 +2612,17 @@ dependencies = [ [[package]] name = "vfio_user" -version = "0.1.0" -source = "git+https://github.com/rust-vmm/vfio-user?branch=main#3febcdd3fa2531623865663ca1721e1962ed9979" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731c2582dd43f4f174ab47b4c933a1a9bb872d9d1b7f54c5867e12dbc1491b75" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", "log", "serde", "serde_derive", "serde_json", - "thiserror 1.0.62", + "thiserror", "vfio-bindings", "vm-memory", "vmm-sys-util", @@ -2254,10 +2630,11 @@ dependencies = [ [[package]] name = "vhost" -version = "0.12.1" -source = "git+https://github.com/rust-vmm/vhost?rev=d983ae0#d983ae07f78663b7d24059667376992460b571a2" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee90657203a8644e9a0860a0db6a7887d8ef0c7bc09fc22dfa4ae75df65bac86" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.11.1", "libc", "uuid", "vm-memory", @@ -2266,8 +2643,9 @@ dependencies = [ [[package]] name = "vhost-user-backend" -version = "0.16.1" -source = "git+https://github.com/rust-vmm/vhost?rev=d983ae0#d983ae07f78663b7d24059667376992460b571a2" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5925983d8fb537752ad3e26604c0a17abfa5de77cb6773a096c8a959c9eca0f" dependencies = [ "libc", "log", @@ -2285,11 +2663,10 @@ dependencies = [ "block", "clap", "env_logger", - "epoll", "libc", "log", "option_parser", - "thiserror 2.0.12", + "thiserror", "vhost", "vhost-user-backend", "virtio-bindings", @@ -2309,7 +2686,7 @@ dependencies = [ "log", "net_util", "option_parser", - "thiserror 2.0.12", + "thiserror", "vhost", "vhost-user-backend", "virtio-bindings", @@ -2319,24 +2696,23 @@ dependencies = [ [[package]] name = "virtio-bindings" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1711e61c00f8cb450bd15368152a1e37a12ef195008ddc7d0f4812f9e2b30a68" +checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868" [[package]] name = "virtio-devices" version = "0.1.0" dependencies = [ "anyhow", - "arc-swap", "block", "byteorder", "epoll", "event_monitor", + "hypervisor", "libc", "log", "mshv-ioctls", - "net_gen", "net_util", "pci", "rate_limiter", @@ -2345,7 +2721,7 @@ dependencies = [ "serde_json", "serde_with", "serial_buffer", - "thiserror 2.0.12", + "thiserror", "vhost", "virtio-bindings", "virtio-queue", @@ -2359,10 +2735,11 @@ dependencies = [ [[package]] name = "virtio-queue" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872e2f3fbd70a7e6f01689720cce3d5c2c5efe52b484dd07b674246ada0e9a8d" +checksum = "e358084f32ed165fddb41d98ff1b7ff3c08b9611d8d6114a1b422e2e85688baf" dependencies = [ + "libc", "log", "virtio-bindings", "vm-memory", @@ -2375,6 +2752,7 @@ version = "0.1.0" dependencies = [ "arch", "libc", + "thiserror", "vm-memory", ] @@ -2382,10 +2760,9 @@ dependencies = [ name = "vm-device" version = "0.1.0" dependencies = [ - "anyhow", "hypervisor", "serde", - "thiserror 2.0.12", + "thiserror", "vfio-ioctls", "vm-memory", "vmm-sys-util", @@ -2394,17 +2771,18 @@ dependencies = [ [[package]] name = "vm-fdt" version = "0.3.0" -source = "git+https://github.com/rust-vmm/vm-fdt?branch=main#ef5bd734f5f66fb07722d766981adbc915f0d941" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23" [[package]] name = "vm-memory" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1720e7240cdc739f935456eb77f370d7e9b2a3909204da1e2b47bef1137a013" +checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380" dependencies = [ "arc-swap", "libc", - "thiserror 1.0.62", + "thiserror", "winapi", ] @@ -2413,17 +2791,19 @@ name = "vm-migration" version = "0.1.0" dependencies = [ "anyhow", + "itertools", + "rustls", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror", "vm-memory", + "zerocopy", ] [[package]] name = "vm-virtio" version = "0.1.0" dependencies = [ - "log", "virtio-queue", "vm-memory", ] @@ -2434,9 +2814,8 @@ version = "0.1.0" dependencies = [ "acpi_tables", "anyhow", - "arc-swap", "arch", - "bitflags 2.9.0", + "bitflags 2.11.1", "block", "blocking", "cfg-if", @@ -2453,6 +2832,8 @@ dependencies = [ "hypervisor", "igvm", "igvm_defs", + "iommufd-ioctls", + "kvm-bindings", "landlock", "libc", "linux-loader", @@ -2468,15 +2849,17 @@ dependencies = [ "serde", "serde_json", "serial_buffer", + "sha2", "signal-hook", - "thiserror 2.0.12", + "tempfile", + "thiserror", "tracer", "uuid", "vfio-ioctls", "vfio_user", + "vhost", "virtio-bindings", "virtio-devices", - "virtio-queue", "vm-allocator", "vm-device", "vm-memory", @@ -2484,14 +2867,14 @@ dependencies = [ "vm-virtio", "vmm-sys-util", "zbus", - "zerocopy 0.8.26", + "zerocopy", ] [[package]] name = "vmm-sys-util" -version = "0.12.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede" +checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" dependencies = [ "bitflags 1.3.2", "libc", @@ -2501,59 +2884,55 @@ dependencies = [ [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen 0.57.1", ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen 0.51.0", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2561,26 +2940,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2604,13 +3017,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -2618,31 +3028,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -2651,46 +3046,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2705,75 +3082,136 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" +name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnu" +name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "winnow" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "winnow" -version = "0.7.2" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ - "memchr", + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ - "bitflags 2.9.0", + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "zbus" -version = "5.7.1" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", "async-executor", @@ -2789,13 +3227,15 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix", "serde", "serde_repr", "tracing", "uds_windows", - "windows-sys 0.59.0", + "uuid", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", @@ -2804,9 +3244,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.7.1" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2819,62 +3259,80 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "static_assertions", "winnow", "zvariant", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", + "zerocopy-derive", ] [[package]] -name = "zerocopy" -version = "0.8.26" +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zerocopy-derive 0.8.26", + "zstd-safe", ] [[package]] -name = "zerocopy-derive" -version = "0.7.35" +name = "zstd-safe" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "zstd-sys", ] [[package]] -name = "zerocopy-derive" -version = "0.8.26" +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cc", + "pkg-config", ] [[package]] name = "zvariant" -version = "5.5.3" +version = "5.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" +checksum = "c4db0ecb8987cf5e92653c57c098f7f0e39a03112edb796f4fe089fb7eaa14ff" dependencies = [ "endi", "enumflags2", @@ -2886,9 +3344,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.5.3" +version = "5.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" +checksum = "5b949b639ab1b4bed763aa7481ba0e368af68d8b55532f8ed4bec86a59f2ca98" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2899,14 +3357,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index fefbd227e4..28d970f440 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,87 +1,40 @@ -[package] -authors = ["The Cloud Hypervisor Authors"] -build = "build.rs" -default-run = "cloud-hypervisor" -description = "Open source Virtual Machine Monitor (VMM) that runs on top of KVM & MSHV" -edition = "2021" -homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor" -license = "Apache-2.0 AND BSD-3-Clause" -name = "cloud-hypervisor" -version = "46.0.0" -# 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. -rust-version = "1.83.0" +# Cloud Hypervisor Workspace +# +# The main crate producing the binaries is in `./cloud-hypervisor`. [profile.release] codegen-units = 1 lto = true opt-level = "s" -strip = true -[profile.profiling] -debug = true +# Tradeof between performance and fast compilation times for local testing and +# development with frequent rebuilds. +[profile.optimized-dev] +codegen-units = 16 inherits = "release" +lto = false +opt-level = 2 strip = false -[dependencies] -anyhow = "1.0.94" -api_client = { path = "api_client" } -clap = { version = "4.5.13", features = ["string"] } -dhat = { version = "0.3.3", optional = true } -epoll = "4.3.3" -event_monitor = { path = "event_monitor" } -hypervisor = { path = "hypervisor" } -libc = "0.2.167" -log = { version = "0.4.22", features = ["std"] } -option_parser = { path = "option_parser" } -seccompiler = { workspace = true } -serde_json = { workspace = true } -signal-hook = "0.3.18" -thiserror = { workspace = true } -tpm = { path = "tpm" } -tracer = { path = "tracer" } -vm-memory = { workspace = true } -vmm = { path = "vmm" } -vmm-sys-util = { workspace = true } -zbus = { version = "5.7.1", optional = true } - -[dev-dependencies] -dirs = "6.0.0" -net_util = { path = "net_util" } -serde_json = { workspace = true } -test_infra = { path = "test_infra" } -wait-timeout = "0.2.0" +# Optimize more for dependencies: They don't require frequent rebuilds. +[profile.optimized-dev.package."*"] +codegen-units = 1 +opt-level = 3 -# Please adjust `vmm::feature_list()` accordingly when changing the -# feature list below -[features] -dbus_api = ["vmm/dbus_api", "zbus"] -default = ["io_uring", "kvm"] -dhat-heap = ["dhat", "vmm/dhat-heap"] # For heap profiling -guest_debug = ["vmm/guest_debug"] -igvm = ["mshv", "vmm/igvm"] -io_uring = ["vmm/io_uring"] -kvm = ["vmm/kvm"] -mshv = ["vmm/mshv"] -pvmemcontrol = ["vmm/pvmemcontrol"] -sev_snp = ["igvm", "mshv", "vmm/sev_snp"] -tdx = ["vmm/tdx"] -tracing = ["tracer/tracing", "vmm/tracing"] +[profile.profiling] +debug = true +inherits = "release" +strip = false [workspace] members = [ "api_client", "arch", "block", + "cloud-hypervisor", "devices", "event_monitor", "hypervisor", - "net_gen", "net_util", "option_parser", "pci", @@ -99,36 +52,100 @@ members = [ "vm-virtio", "vmm", ] +package.edition = "2024" +# Minimum buildable version: +# Keep in sync with version in .github/workflows/build.yaml +# Policy on MSRV (see #4318): +# Can only be bumped if satisfying any of the following: +# a.) A dependency requires it, +# b.) If we want to use a new feature and that MSRV is at least 6 months old, +# c.) There is a security issue that is addressed by the toolchain update. +package.rust-version = "1.89.0" +resolver = "3" [workspace.dependencies] # rust-vmm crates -acpi_tables = { git = "https://github.com/rust-vmm/acpi_tables", branch = "main" } -kvm-bindings = "0.10.0" -kvm-ioctls = "0.19.1" -linux-loader = "0.13.0" -mshv-bindings = "0.5.1" -mshv-ioctls = "0.5.1" +acpi_tables = "0.2.0" +iommufd-ioctls = "0.1.0" +kvm-bindings = "0.14.0" +kvm-ioctls = "0.24.0" +linux-loader = "0.13.2" +mshv-bindings = "0.6.9" +mshv-ioctls = "0.6.9" seccompiler = "0.5.0" -vfio-bindings = { git = "https://github.com/rust-vmm/vfio", branch = "main" } -vfio-ioctls = { git = "https://github.com/rust-vmm/vfio", branch = "main", default-features = false } -vfio_user = { git = "https://github.com/rust-vmm/vfio-user", branch = "main" } -vhost = { git = "https://github.com/rust-vmm/vhost", rev = "d983ae0" } -vhost-user-backend = { git = "https://github.com/rust-vmm/vhost", rev = "d983ae0" } -virtio-bindings = "0.2.4" -virtio-queue = "0.14.0" -vm-fdt = { git = "https://github.com/rust-vmm/vm-fdt", branch = "main" } -vm-memory = "0.16.1" -vmm-sys-util = "0.12.1" +vfio-bindings = { version = "0.6.2", default-features = false } +vfio-ioctls = { version = "0.6.0", default-features = false } +vfio_user = { version = "0.1.3", default-features = false } +vhost = { version = "0.16.0", default-features = false } +vhost-user-backend = { version = "0.22.0", default-features = false } +virtio-bindings = "0.2.6" +virtio-queue = "0.17.0" +vm-fdt = "0.3.0" +vm-memory = "0.17.1" +vmm-sys-util = "0.15.0" # igvm crates -# TODO: bump to 0.3.5 release -igvm = { git = "https://github.com/microsoft/igvm", branch = "main" } -igvm_defs = { git = "https://github.com/microsoft/igvm", branch = "main" } +igvm = "0.4.0" +igvm_defs = "0.4.0" # serde crates -serde_json = "1.0.120" +serde = "1.0.228" +serde_json = "1.0.149" +serde_with = { version = "3.18.0", default-features = false } # other crates -thiserror = "2.0.12" -uuid = { version = "1.17.0" } -zerocopy = { version = "0.8.26", default-features = false } +anyhow = "1.0.102" +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" +# Used for (de-) compressing CPU profiles +flate2 = "1.1.9" +flume = "0.12.0" +itertools = "0.14.0" +jiff = { version = "0.2", default-features = false, features = ["std"] } +libc = "0.2.186" +log = "0.4.29" +rustls = { version = "0.23.38", default-features = false, features = [ + "aws-lc-rs", + "std", + "tls12", +] } +sha2 = "0.11.0" +signal-hook = "0.4.4" +thiserror = "2.0.18" +uuid = { version = "1.23.1" } +wait-timeout = "0.2.1" +zerocopy = { version = "0.8.48", 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 +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)'] } diff --git a/README.md b/README.md index 4609903d0f..0ee4b3af4b 100644 --- a/README.md +++ b/README.md @@ -1,390 +1,27 @@ -- [1. What is Cloud Hypervisor?](#1-what-is-cloud-hypervisor) - - [Objectives](#objectives) - - [High Level](#high-level) - - [Architectures](#architectures) - - [Guest OS](#guest-os) -- [2. Getting Started](#2-getting-started) - - [Host OS](#host-os) - - [Use Pre-built Binaries](#use-pre-built-binaries) - - [Packages](#packages) - - [Building from Source](#building-from-source) - - [Booting Linux](#booting-linux) - - [Firmware Booting](#firmware-booting) - - [Custom Kernel and Disk Image](#custom-kernel-and-disk-image) - - [Building your Kernel](#building-your-kernel) - - [Disk image](#disk-image) - - [Booting the guest VM](#booting-the-guest-vm) -- [3. Status](#3-status) - - [Hot Plug](#hot-plug) - - [Device Model](#device-model) - - [Roadmap](#roadmap) -- [4. Relationship with _Rust VMM_ Project](#4-relationship-with-rust-vmm-project) - - [Differences with Firecracker and crosvm](#differences-with-firecracker-and-crosvm) -- [5. Community](#5-community) - - [Contribute](#contribute) - - [Slack](#slack) - - [Mailing list](#mailing-list) - - [Security issues](#security-issues) - -# 1. What is Cloud Hypervisor? - -Cloud Hypervisor is an open source Virtual Machine Monitor (VMM) that runs on -top of the [KVM](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt) -hypervisor and the Microsoft Hypervisor (MSHV). - -The project focuses on running modern, _Cloud Workloads_, on specific, common, -hardware architectures. In this case _Cloud Workloads_ refers to those that are -run by customers inside a Cloud Service Provider. This means modern operating -systems with most I/O handled by -paravirtualised devices (e.g. _virtio_), no requirement for legacy devices, and -64-bit CPUs. - -Cloud Hypervisor is implemented in [Rust](https://www.rust-lang.org/) and is -based on the [Rust VMM](https://github.com/rust-vmm) crates. - -## Objectives - -### High Level - -- Runs on KVM or MSHV -- Minimal emulation -- Low latency -- Low memory footprint -- Low complexity -- High performance -- Small attack surface -- 64-bit support only -- CPU, memory, PCI hotplug -- Machine to machine migration - -### Architectures - -Cloud Hypervisor supports the `x86-64`, `AArch64` and `riscv64` -architectures, with functionality varying across these platforms. The -functionality differences between `x86-64` and `AArch64` are documented -in [#1125](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/1125). -The `riscv64` architecture support is experimental and offers limited -functionality. For more details and instructions, please refer to [riscv -documentation](docs/riscv.md). - -### Guest OS - -Cloud Hypervisor supports `64-bit Linux` and Windows 10/Windows Server 2019. - -# 2. Getting Started - -The following sections describe how to build and run Cloud Hypervisor. - -## Prerequisites for AArch64 - -- AArch64 servers (recommended) or development boards equipped with the GICv3 - interrupt controller. - -## Host OS - -For required KVM functionality and adequate performance the recommended host -kernel version is 5.13. The majority of the CI currently tests with kernel -version 5.15. - -## Use Pre-built Binaries - -The recommended approach to getting started with Cloud Hypervisor is by using a -pre-built binary. Binaries are available for the [latest -release](https://github.com/cloud-hypervisor/cloud-hypervisor/releases/latest). -Use `cloud-hypervisor-static` for `x86-64` or `cloud-hypervisor-static-aarch64` -for `AArch64` platform. - -## Packages - -For convenience, packages are also available targeting some popular Linux -distributions. This is thanks to the [Open Build -Service](https://build.opensuse.org). The [OBS -README](https://github.com/cloud-hypervisor/obs-packaging) explains how to -enable the repository in a supported Linux distribution and install Cloud Hypervisor -and accompanying packages. Please report any packaging issues in the -[obs-packaging](https://github.com/cloud-hypervisor/obs-packaging) repository. - -## Building from Source - -Please see the [instructions for building from source](docs/building.md) if you -do not wish to use the pre-built binaries. - -## Booting Linux - -Cloud Hypervisor supports direct kernel boot (the x86-64 kernel requires the kernel -built with PVH support or a bzImage) or booting via a firmware (either [Rust Hypervisor -Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) or an -edk2 UEFI firmware called `CLOUDHV` / `CLOUDHV_EFI`.) - -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 -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 - -Cloud Hypervisor supports booting disk images containing all needed components -to run cloud workloads, a.k.a. cloud images. - -The following sample commands will download an Ubuntu Cloud image, converting -it into a format that Cloud Hypervisor can use and a firmware to boot the image -with. - -```shell -$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img -$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw -$ wget https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/download/0.4.2/hypervisor-fw -``` - -The Ubuntu cloud images do not ship with a default password so it necessary to -use a `cloud-init` disk image to customise the image on the first boot. A basic -`cloud-init` image is generated by this [script](scripts/create-cloud-init.sh). -This seeds the image with a default username/password of `cloud/cloud123`. It -is only necessary to add this disk image on the first boot. Script also assigns -default IP address using `test_data/cloud-init/ubuntu/local/network-config` details -with `--net "mac=12:34:56:78:90:ab,tap="` option. Then the matching mac address -interface will be enabled as per `network-config` details. - -```shell -$ sudo setcap cap_net_admin+ep ./cloud-hypervisor -$ ./create-cloud-init.sh -$ ./cloud-hypervisor \ - --kernel ./hypervisor-fw \ - --disk path=focal-server-cloudimg-amd64.raw path=/tmp/ubuntu-cloudinit.img \ - --cpus boot=4 \ - --memory size=1024M \ - --net "tap=,mac=,ip=,mask=" -``` - -If access to the firmware messages or interaction with the boot loader (e.g. -GRUB) is required then it necessary to switch to the serial console instead of -`virtio-console`. - -```shell -$ ./cloud-hypervisor \ - --kernel ./hypervisor-fw \ - --disk path=focal-server-cloudimg-amd64.raw path=/tmp/ubuntu-cloudinit.img \ - --cpus boot=4 \ - --memory size=1024M \ - --net "tap=,mac=,ip=,mask=" \ - --serial tty \ - --console off -``` - -### Custom Kernel and Disk Image - -#### 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. - -To build the kernel: - -```shell -# Clone the Cloud Hypervisor Linux branch -$ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.12.8 linux-cloud-hypervisor -$ pushd linux-cloud-hypervisor -$ make ch_defconfig -# 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 -$ make -j `nproc` -$ popd -``` - -For x86-64, the `vmlinux` kernel image will then be located at -`linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin`. -For AArch64, the `Image` kernel image will then be located at -`linux-cloud-hypervisor/arch/arm64/boot/Image`. - -#### Disk image - -For the disk image the same Ubuntu image as before can be used. This contains -an `ext4` root filesystem. - -```shell -$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img # x86-64 -$ wget https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img # AArch64 -$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-amd64.img focal-server-cloudimg-amd64.raw # x86-64 -$ qemu-img convert -p -f qcow2 -O raw focal-server-cloudimg-arm64.img focal-server-cloudimg-arm64.raw # AArch64 -``` - -#### Booting the guest VM - -These sample commands boot the disk image using the custom kernel whilst also -supplying the desired kernel command line. - -- x86-64 - -```shell -$ 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 \ - --cmdline "console=hvc0 root=/dev/vda1 rw" \ - --cpus boot=4 \ - --memory size=1024M \ - --net "tap=,mac=,ip=,mask=" -``` - -- AArch64 - -```shell -$ sudo setcap cap_net_admin+ep ./cloud-hypervisor -$ ./create-cloud-init.sh -$ ./cloud-hypervisor \ - --kernel ./linux-cloud-hypervisor/arch/arm64/boot/Image \ - --disk path=focal-server-cloudimg-arm64.raw path=/tmp/ubuntu-cloudinit.img \ - --cmdline "console=hvc0 root=/dev/vda1 rw" \ - --cpus boot=4 \ - --memory size=1024M \ - --net "tap=,mac=,ip=,mask=" -``` - -If earlier kernel messages are required the serial console should be used instead of `virtio-console`. - -- x86-64 - -```shell -$ ./cloud-hypervisor \ - --kernel ./linux-cloud-hypervisor/arch/x86/boot/compressed/vmlinux.bin \ - --console off \ - --serial tty \ - --disk path=focal-server-cloudimg-amd64.raw \ - --cmdline "console=ttyS0 root=/dev/vda1 rw" \ - --cpus boot=4 \ - --memory size=1024M \ - --net "tap=,mac=,ip=,mask=" -``` - -- AArch64 - -```shell -$ ./cloud-hypervisor \ - --kernel ./linux-cloud-hypervisor/arch/arm64/boot/Image \ - --console off \ - --serial tty \ - --disk path=focal-server-cloudimg-arm64.raw \ - --cmdline "console=ttyAMA0 root=/dev/vda1 rw" \ - --cpus boot=4 \ - --memory size=1024M \ - --net "tap=,mac=,ip=,mask=" -``` - -# 3. Status - -Cloud Hypervisor is under active development. The following stability -guarantees are currently made: - -* The API (including command line options) will not be removed or changed in a - breaking way without a minimum of 2 major releases notice. Where possible - warnings will be given about the use of deprecated functionality and the - deprecations will be documented in the release notes. - -* Point releases will be made between individual releases where there are - substantial bug fixes or security issues that need to be fixed. These point - releases will only include bug fixes. - -Currently the following items are **not** guaranteed across updates: - -* Snapshot/restore is not supported across different versions -* Live migration is not supported across different versions -* The following features are considered experimental and may change - substantially between releases: TDX, vfio-user, vDPA. - -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/)) - -Direct kernel boot to userspace should work with a rootfs from most -distributions although you may need to enable exotic filesystem types in the -reference kernel configuration (e.g. XFS or btrfs.) - -## Hot Plug - -Cloud Hypervisor supports hotplug of CPUs, passthrough devices (VFIO), -`virtio-{net,block,pmem,fs,vsock}` and memory resizing. This -[document](docs/hotplug.md) details how to add devices to a running VM. - -## Device Model - -Details of the device model can be found in this -[documentation](docs/device_model.md). - -## Roadmap - -The project roadmap is tracked through a [GitHub -project](https://github.com/orgs/cloud-hypervisor/projects/6). - -# 4. Relationship with _Rust VMM_ Project - -In order to satisfy the design goal of having a high-performance, -security-focused hypervisor the decision was made to use the -[Rust](https://www.rust-lang.org/) programming language. The language's strong -focus on memory and thread safety makes it an ideal candidate for implementing -VMMs. - -Instead of implementing the VMM components from scratch, Cloud Hypervisor is -importing the [Rust VMM](https://github.com/rust-vmm) crates, and sharing code -and architecture together with other VMMs like e.g. Amazon's -[Firecracker](https://firecracker-microvm.github.io/) and Google's -[crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/). - -Cloud Hypervisor embraces the _Rust VMM_ project's goals, which is to be able -to share and re-use as many virtualization crates as possible. - -## Differences with Firecracker and crosvm - -A large part of the Cloud Hypervisor code is based on either the Firecracker or -the crosvm project's implementations. Both of these are VMMs written in Rust -with a focus on safety and security, like Cloud Hypervisor. - -The goal of the Cloud Hypervisor project differs from the aforementioned -projects in that it aims to be a general purpose VMM for _Cloud Workloads_ and -not limited to container/serverless or client workloads. - -The Cloud Hypervisor community thanks the communities of both the Firecracker -and crosvm projects for their excellent work. - -# 5. Community - -The Cloud Hypervisor project follows the governance, and community guidelines -described in the [Community](https://github.com/cloud-hypervisor/community) -repository. - -## Contribute - -The project strongly believes in building a global, diverse and collaborative -community around the Cloud Hypervisor project. Anyone who is interested in -[contributing](CONTRIBUTING.md) to the project is welcome to participate. - -Contributing to a open source project like Cloud Hypervisor covers a lot more -than just sending code. Testing, documentation, pull request -reviews, bug reports, feature requests, project improvement suggestions, etc, -are all equal and welcome means of contribution. See the -[CONTRIBUTING](CONTRIBUTING.md) document for more details. - -## Slack - -Get an [invite to our Slack channel](https://join.slack.com/t/cloud-hypervisor/shared_invite/enQtNjY3MTE3MDkwNDQ4LWQ1MTA1ZDVmODkwMWQ1MTRhYzk4ZGNlN2UwNTI3ZmFlODU0OTcwOWZjMTkwZDExYWE3YjFmNzgzY2FmNDAyMjI), - [join us on Slack](https://cloud-hypervisor.slack.com/), and [participate in our community activities](https://cloud-hypervisor.slack.com/archives/C04R5DUQVBN). - -## Mailing list - -Please report bugs using the [GitHub issue -tracker](https://github.com/cloud-hypervisor/cloud-hypervisor/issues) but for -broader community discussions you may use our [mailing -list](https://lists.cloudhypervisor.org/g/dev/). - -## Security issues - -Please contact the maintainers listed in the MAINTAINERS.md file with security issues. +# Cloud Hypervisor Fork for SAP gardenlinux + +The `gardenlinux` branch is the branch from that our SAP colleagues [build] +[sap-gl-ci] their Cloud Hypervisor packages. + +## Development Model + +- The `gardenlinux` branch is always what SAP builds. From SAPs side, we can + force push or rewrite history on that branch. +- We use branch protection for `gradenlinux`, PRs, CI, and code reviews +- With every new CHV release, we rename `gardenlinux` to `gardenlinux-vXX` and + create a new `gardenlinux` branch manually: + - use release as base and push it into the repo + - cherry-pick all commits from `gardenlinux-vXX` that are still relevant onto a + new branch and create a pull request against this fork + - adapt git commit history +- PoC Development: + - happens here (in [cyberus-technology/cloud-hypervisor](https://github.com/cyberus-technology/cloud-hypervisor)) + - open PR against `gardenlinux` + - Branch name patterns **must not** follow `gardenlinux-*` pattern + - We recommend `cyberus-fork-*` as branch pattern to better keep the overview. +- Productization: + - happens upstream (in [cloud-hypervisor/cloud-hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor)) + - We recommend `productize-*` as branch pattern to better keep the overview. + + +[sap-gl-ci]: https://github.com/gardenlinux/package-cloud-hypervisor-gl/blob/main/prepare_source#L1 diff --git a/api_client/Cargo.toml b/api_client/Cargo.toml index 630f1b4c44..1ab0e5862e 100644 --- a/api_client/Cargo.toml +++ b/api_client/Cargo.toml @@ -1,9 +1,14 @@ [package] authors = ["The Cloud Hypervisor Authors"] -edition = "2021" +edition.workspace = true +license = "Apache-2.0" name = "api_client" +rust-version.workspace = true version = "0.1.0" [dependencies] thiserror = { workspace = true } vmm-sys-util = { workspace = true } + +[lints] +workspace = true diff --git a/api_client/src/lib.rs b/api_client/src/lib.rs index 52e85a3367..f5bdd1302a 100644 --- a/api_client/src/lib.rs +++ b/api_client/src/lib.rs @@ -118,12 +118,11 @@ fn parse_http_response(socket: &mut dyn Read) -> Result, Error> { } } - if let Some(body_offset) = body_offset { - if let Some(content_length) = content_length { - if res.len() >= content_length + body_offset { - break; - } - } + if let Some(body_offset) = body_offset + && let Some(content_length) = content_length + && res.len() >= content_length + body_offset + { + break; } } let body_string = content_length.and(body_offset.map(|o| String::from(&res[o..]))); @@ -143,7 +142,7 @@ pub fn simple_api_full_command_with_fds_and_response, - request_fds: Vec, + request_fds: &[RawFd], ) -> Result, Error> { socket .send_with_fds( @@ -151,7 +150,7 @@ pub fn simple_api_full_command_with_fds_and_response( method: &str, full_command: &str, request_body: Option<&str>, - request_fds: Vec, + request_fds: &[RawFd], ) -> Result<(), Error> { let response = simple_api_full_command_with_fds_and_response( socket, @@ -189,8 +188,8 @@ pub fn simple_api_full_command_with_fds( request_fds, )?; - if response.is_some() { - println!("{}", response.unwrap()); + if let Some(response) = response { + println!("{response}"); } Ok(()) @@ -202,7 +201,7 @@ pub fn simple_api_full_command( full_command: &str, request_body: Option<&str>, ) -> Result<(), Error> { - simple_api_full_command_with_fds(socket, method, full_command, request_body, Vec::new()) + simple_api_full_command_with_fds(socket, method, full_command, request_body, &[]) } pub fn simple_api_full_command_and_response( @@ -211,13 +210,7 @@ pub fn simple_api_full_command_and_response( full_command: &str, request_body: Option<&str>, ) -> Result, Error> { - simple_api_full_command_with_fds_and_response( - socket, - method, - full_command, - request_body, - Vec::new(), - ) + simple_api_full_command_with_fds_and_response(socket, method, full_command, request_body, &[]) } pub fn simple_api_command_with_fds( @@ -225,7 +218,7 @@ pub fn simple_api_command_with_fds( method: &str, c: &str, request_body: Option<&str>, - request_fds: Vec, + request_fds: &[RawFd], ) -> Result<(), Error> { // Create the full VM command. For VMM commands, use // simple_api_full_command(). @@ -240,5 +233,5 @@ pub fn simple_api_command( c: &str, request_body: Option<&str>, ) -> Result<(), Error> { - simple_api_command_with_fds(socket, method, c, request_body, Vec::new()) + simple_api_command_with_fds(socket, method, c, request_body, &[]) } diff --git a/arch/Cargo.toml b/arch/Cargo.toml index 4c068d131f..a0b8fdb5df 100644 --- a/arch/Cargo.toml +++ b/arch/Cargo.toml @@ -1,29 +1,59 @@ [package] authors = ["The Chromium OS Authors"] -edition = "2021" +edition.workspace = true name = "arch" +rust-version.workspace = true version = "0.1.0" +# TODO: Consider making this a binary of the main package instead +[[bin]] +name = "generate-cpu-profile" +path = "src/bin/generate-cpu-profile.rs" +required-features = ["cpu_profile_generation"] + [features] default = [] +fw_cfg = [] kvm = ["hypervisor/kvm"] sev_snp = [] tdx = [] +# Currently cpu profiles can only be generated with KVM +cpu_profile_generation = ["dep:clap", "kvm"] [dependencies] -anyhow = "1.0.94" -byteorder = "1.5.0" +anyhow = { workspace = true } +byteorder = { workspace = true } +clap = { workspace = true, optional = true } hypervisor = { path = "../hypervisor" } -libc = "0.2.167" +libc = { workspace = true } linux-loader = { workspace = true, features = ["bzimage", "elf", "pe"] } -log = "0.4.22" -serde = { version = "1.0.208", features = ["derive", "rc"] } +log = { workspace = true } +serde = { workspace = true, features = ["derive", "rc"] } +# We currently use this for (de-)serializing CPU profile data +serde_json = { workspace = true } thiserror = { workspace = true } uuid = { workspace = true } vm-memory = { workspace = true, features = ["backend-bitmap", "backend-mmap"] } -vm-migration = { path = "../vm-migration" } vmm-sys-util = { workspace = true, features = ["with-serde"] } +[target.'cfg(target_arch = "x86_64")'.dependencies] +flate2 = { workspace = true } + [target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies] fdt_parser = { version = "0.1.5", package = "fdt" } vm-fdt = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +flate2 = { workspace = true } +prettyplease = "0.2.37" +quote = "1.0.45" +syn = "2.0.117" + +# Use this to test our custom serialization logic +[dev-dependencies] +proptest = "1.0.0" +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/arch/build.rs b/arch/build.rs new file mode 100644 index 0000000000..01170f7289 --- /dev/null +++ b/arch/build.rs @@ -0,0 +1,254 @@ +// Copyright © 2026 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::collections::BTreeSet; +use std::ffi::OsStr; +use std::io::{Read, Write}; +use std::path::Path; +use std::{env, fs}; + +use anyhow::Context; +use flate2::Compression; +use flate2::write::ZlibEncoder; +use quote::{format_ident, quote}; + +/// This is where the CPU profile generation tool writes the JSON files associated with +/// a CPU profile. +const X86_64_CPU_PROFILES_PATH: &str = "./src/x86_64/cpu_profiles"; + +fn main() -> anyhow::Result<()> { + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH") + .context("Could not get env var CARGO_CFG_TARGET_ARCH")?; + + if target_arch == "x86_64" { + generate_code_for_x86_64_cpu_profiles().context("CPU profile code generation failed")?; + // We only want the build script to be rerun if new CPU profiles are generated, or the + // build script itself changes (see the final println! before this function returns). + println!("cargo::rerun-if-changed={X86_64_CPU_PROFILES_PATH}"); + } + + // Disable automatic rerun after package changes. + // See: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed + println!("cargo::rerun-if-changed=build.rs"); + Ok(()) +} + +/// This function generates the `generated_cpu_profiles.rs` file which consists of the following: +/// +/// - a `CpuProfile` enum with a `Host` variant and one additional variant per pre-generated CPU profile. +/// - A function `compressed_cpuid_data` that takes a `&CpuProfile` and returns the compressed CPUID adjustment data required for the given CPU profile. +/// - A function `compressed_msr_data` that takes a `&CpuProfile` and returns the compressed MSR adjustment data required for the given CPU profile. +/// +/// This function works by traversing the JSON files in `X86_64_CPU_PROFILES_PATH` generated by +/// the CPU profile generation tool. +fn generate_code_for_x86_64_cpu_profiles() -> anyhow::Result<()> { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let profile_names = x86_64_cpu_profile_names() + .context("Failed to extract CPU profile names from pre-generated JSON files")?; + // Compress each CPUID and MSR JSON file + compress_json_files(&profile_names, &out_dir) + .context("Failed to create compressed CPU profile data files")?; + + let mut out = generate_cpu_profile_enum(&profile_names); + out.push('\n'); + out.push_str(&generate_compressed_data_fn( + &profile_names, + DataType::Cpuid, + )); + out.push('\n'); + out.push_str(&generate_compressed_data_fn(&profile_names, DataType::Msr)); + + let generated_file_path = Path::new(&out_dir).join("generated_cpu_profiles.rs"); + let mut f = fs::File::create(&generated_file_path) + .with_context(|| format!("Could not create file with path:={generated_file_path:#?}"))?; + f.write_all(out.as_bytes()) + .with_context(|| format!("Could not write to file with path:={generated_file_path:#?}"))?; + Ok(()) +} + +/// The name of a pre-generated CPU profile. +/// +/// Each CPU profile has two associated JSON files: +/// +/// 1. .cpuid.json +/// 2. .msr.json +/// +/// and each instance of `ProfileName` is extracted from +/// ``. +struct ProfileName { + /// The `kebab_case` name converted to camel case. + camel_case: String, + kebab_case: String, +} + +/// Each CPU profile has two associated JSON files: +/// +/// one for CPUID adjustment data and one for MSR adjustment data. +#[derive(Copy, Clone)] +enum DataType { + Cpuid, + Msr, +} + +impl DataType { + fn as_str(&self) -> &str { + match self { + Self::Cpuid => "cpuid", + Self::Msr => "msr", + } + } +} + +/// Traverse the `X86_64_CPU_PROFILES_PATH` and extract a `[ProfileName]` per encountered +/// pre-generated CPU profile. +fn x86_64_cpu_profile_names() -> anyhow::Result> { + let dir = fs::read_dir(X86_64_CPU_PROFILES_PATH) + .with_context(|| format!("Could not read directory:={X86_64_CPU_PROFILES_PATH}"))?; + + let mut profile_names_kebab_case = BTreeSet::new(); + for entry in dir { + let file = entry.with_context(|| { + format!("Encountered error while traversing directory:={X86_64_CPU_PROFILES_PATH}") + })?; + let file_name = file.file_name().into_string().unwrap(); + let profile_name_kebab_case = { + let dot_pos = file_name + .find('.') + .expect("all files in the cpu_profiles directory should contain a '.' character"); + file_name[..dot_pos].to_string() + }; + profile_names_kebab_case.insert(profile_name_kebab_case); + } + + let profile_name_iter = profile_names_kebab_case.into_iter().map(|kebab_case| { + let mut camel_case = String::new(); + for part in kebab_case.split('-') { + if let Some(first_char) = part.chars().next() { + camel_case.extend(first_char.to_uppercase()); + let rest = &part[first_char.len_utf8()..]; + camel_case.push_str(rest); + } + } + ProfileName { + camel_case, + kebab_case, + } + }); + Ok(profile_name_iter.collect()) +} + +/// Compresses the CPUID and MSR related JSON files per CPU profile +/// that are found in `X86_64_CPU_PROFILES_PATH`. +fn compress_json_files(names: &[ProfileName], out_dir: &OsStr) -> anyhow::Result<()> { + for ProfileName { + kebab_case, + camel_case: _, + } in names + { + let file_bytes = |data_type: &str| -> anyhow::Result> { + let path = + Path::new(X86_64_CPU_PROFILES_PATH).join(format!("{kebab_case}.{data_type}.json")); + let mut file = fs::File::open(&path) + .with_context(|| format!("Could not open file with path:={path:#?}"))?; + let mut v = Vec::new(); + file.read_to_end(&mut v) + .with_context(|| format!("Could not read contents of file with path:={path:#?}"))?; + Ok(v) + }; + let cpuid_bytes = file_bytes("cpuid")?; + let msr_bytes = file_bytes("msr")?; + let compress_to_file = |data_type: &str, data: &[u8]| -> anyhow::Result<()> { + let path = Path::new(&out_dir).join(format!("{kebab_case}.{data_type}.zz")); + let file = fs::File::create(&path) + .with_context(|| format!("Could not create file with path:={path:#?}"))?; + let mut encoder = ZlibEncoder::new(file, Compression::best()); + encoder.write_all(data).with_context(|| { + format!("Could not write compressed bytes to file with path:={path:#?}") + })?; + encoder + .flush() + .with_context(|| format!("Could not flush to file with path:={path:#?}"))?; + Ok(()) + }; + compress_to_file(DataType::Cpuid.as_str(), &cpuid_bytes)?; + compress_to_file(DataType::Msr.as_str(), &msr_bytes)?; + } + + Ok(()) +} + +/// Generates Rust code as a String defining a `CpuProfile` enum with a `Host` variant +/// together with a variant per entry in `profile_names`. +fn generate_cpu_profile_enum(profile_names: &[ProfileName]) -> String { + // Obtain a vector of the non-host CPU profile enum variants from the previously parsed camel case names + let non_host_enum_variants = non_host_cpu_profile_variants(profile_names); + + // Use the quote crate to build the CpuProfile enum as a TokenStream. + let tokens = quote! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)] + #[serde(rename_all = "kebab-case")] + pub enum CpuProfile { + #[default] + Host, + #(#non_host_enum_variants),* + } + }; + + // Parse this to a syntax tree and return it and convert it to a pretty printed string of Rust code + let syntax_tree = syn::parse2(tokens).unwrap(); + prettyplease::unparse(&syntax_tree) +} + +/// Generates the function that extracts the compressed bytes for `data_type` corresponding to the user's +/// selected CPU profile. +fn generate_compressed_data_fn(profile_names: &[ProfileName], data_type: DataType) -> String { + let data_type_str = data_type.as_str(); + let doc_str = format!( + "Extract compressed {data_type_str} CPU profile data corresponding to the given profile" + ); + let non_host_enum_variants = non_host_cpu_profile_variants(profile_names); + let compressed_file_names: Vec = profile_names + .iter() + .map( + |ProfileName { + kebab_case, + camel_case: _, + }| format!("/{kebab_case}.{data_type_str}.zz"), + ) + .collect(); + + // Workaround to interpolate `data_type_str` in the function name within a `quote!` invocation. + let fn_name_ident = format_ident!("compressed_{data_type_str}_data"); + + // We now use quote! to produce our function that matches against each enum variant and returns the compressed file as a byte slice. + // + // Note that the compressed bytes are no longer stand alone files after compiling since we will use `include_bytes!` to compile them + // into the final binary. + let tokens = quote! { + #[doc=#doc_str] + fn #fn_name_ident (profile: &CpuProfile) -> Option<&'static [u8]> { + use CpuProfile::*; + match profile { + Host => None, + #(#non_host_enum_variants => Some(&include_bytes!(concat!(env!("OUT_DIR"), #compressed_file_names))[..])),* + } + } + }; + + // Parse this to a syntax tree and return it and convert it to a pretty printed string of Rust code + let syntax_tree = syn::parse2(tokens).unwrap(); + prettyplease::unparse(&syntax_tree) +} + +/// Converts the parsed CPU profile names to a enum variants that may be placed into a token stream. +fn non_host_cpu_profile_variants(names: &[ProfileName]) -> Vec { + names + .iter() + .map(|name| { + syn::parse_str(name.camel_case.as_str()) + .expect("Should be able to parse camelcase name to syn::Variant") + }) + .collect() +} diff --git a/arch/src/aarch64/fdt.rs b/arch/src/aarch64/fdt.rs index 23df4d805a..0310cf5b08 100644 --- a/arch/src/aarch64/fdt.rs +++ b/arch/src/aarch64/fdt.rs @@ -19,14 +19,16 @@ use hypervisor::arch::aarch64::regs::{ AARCH64_ARCH_TIMER_HYP_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ, AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_PMU_IRQ, }; +use log::{debug, info, warn}; use thiserror::Error; use vm_fdt::{FdtWriter, FdtWriterResult}; use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion}; use super::super::{DeviceType, GuestMemoryMmap, InitramfsConfig}; use super::layout::{ - GIC_V2M_COMPATIBLE, 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, SPI_BASE, SPI_NUM, + 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, }; use crate::{NumaNodes, PciSpaceInfo}; @@ -86,6 +88,7 @@ pub enum Error { } type Result = result::Result; +#[derive(Copy, Clone)] pub enum CacheLevel { /// L1 data cache L1D = 0, @@ -109,12 +112,7 @@ pub fn get_cache_size(cache_level: CacheLevel) -> u32 { } 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); - + 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(); @@ -128,6 +126,8 @@ pub fn get_cache_size(cache_level: CacheLevel) -> u32 { "G" => 1024u32.pow(3), _ => 1, } + } else { + 0 } } @@ -143,14 +143,11 @@ pub fn get_cache_coherency_line_size(cache_level: CacheLevel) -> u32 { } 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); - + if file_path.exists() { let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted."); src.trim().parse::().unwrap() + } else { + 0 } } @@ -166,14 +163,11 @@ pub fn get_cache_number_of_sets(cache_level: CacheLevel) -> u32 { } 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); - + if file_path.exists() { let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted."); src.trim().parse::().unwrap() + } else { + 0 } } @@ -194,12 +188,7 @@ pub fn get_cache_shared(cache_level: CacheLevel) -> bool { } 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); - + 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() { @@ -207,6 +196,8 @@ pub fn get_cache_shared(cache_level: CacheLevel) -> bool { } else { result = src.contains('-') || src.contains(','); } + } else { + result = false; } result @@ -217,8 +208,8 @@ pub fn get_cache_shared(cache_level: CacheLevel) -> bool { pub fn create_fdt( guest_mem: &GuestMemoryMmap, cmdline: &str, - vcpu_mpidr: Vec, - vcpu_topology: Option<(u8, u8, u8)>, + vcpu_mpidr: &[u64], + vcpu_topology: Option<(u16, u16, u16, u16)>, device_info: &HashMap<(DeviceType, String), T, S>, gic_device: &Arc>, initrd: &Option, @@ -231,8 +222,8 @@ pub fn create_fdt, guest_mem: &GuestMemoryMmap) -> Result<()> { +pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Result<()> { // Write FDT to memory. guest_mem - .write_slice(fdt_final.as_slice(), super::layout::FDT_START) + .write_slice(fdt_final, super::layout::FDT_START) .map_err(Error::WriteFdtToMemory)?; Ok(()) } @@ -280,7 +271,7 @@ pub fn write_fdt_to_memory(fdt_final: Vec, guest_mem: &GuestMemoryMmap) -> R fn create_cpu_nodes( fdt: &mut FdtWriter, vcpu_mpidr: &[u64], - vcpu_topology: Option<(u8, u8, u8)>, + vcpu_topology: Option<(u16, u16, u16, u16)>, numa_nodes: &NumaNodes, ) -> FdtWriterResult<()> { // See https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/arm/cpus.yaml. @@ -289,8 +280,11 @@ fn create_cpu_nodes( fdt.property_u32("#size-cells", 0x0)?; let num_cpus = vcpu_mpidr.len(); - 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(); + let (threads_per_core, cores_per_die, dies_per_package, packages) = + vcpu_topology.unwrap_or((1, 1, 1, 1)); + let cores_per_package = cores_per_die * dies_per_package; + let max_cpus: u32 = + threads_per_core as u32 * cores_per_die as u32 * dies_per_package as u32 * packages as u32; // Add cache info. // L1 Data Cache Info. @@ -319,10 +313,7 @@ fn create_cpu_nodes( 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."); + if cache_exist { // L1 Data Cache Info. l1_d_cache_size = get_cache_size(CacheLevel::L1D); l1_d_cache_line_size = get_cache_coherency_line_size(CacheLevel::L1D); @@ -350,6 +341,19 @@ fn create_cpu_nodes( if l3_cache_size != 0 { l3_cache_shared = get_cache_shared(CacheLevel::L3); } + } else { + warn!("cache sysfs system does not exist."); + } + + // Arm boot protocol requires a minimal Device Tree + // https://docs.kernel.org/arch/arm64/booting.html + // 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"); } for (cpu_id, mpidr) in vcpu_mpidr.iter().enumerate().take(num_cpus) { @@ -366,11 +370,13 @@ fn create_cpu_nodes( fdt.property_u32("reg", (mpidr & 0x7FFFFF) as u32)?; fdt.property_u32("phandle", cpu_id as u32 + FIRST_VCPU_PHANDLE)?; - // Add `numa-node-id` property if there is any numa config. - if numa_nodes.len() > 1 { + // 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 { 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 u8)) { + if numa_node.unwrap().cpus.contains(&(cpu_id as u32)) { fdt.property_u32("numa-node-id", numa_node_idx as u32)?; } } @@ -423,9 +429,6 @@ 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)?; @@ -462,7 +465,8 @@ fn create_cpu_nodes( } if let Some(topology) = vcpu_topology { - let (threads_per_core, cores_per_package, packages) = topology; + let (threads_per_core, cores_per_die, dies_per_package, packages) = topology; + let cores_per_package = cores_per_die * dies_per_package; let cpu_map_node = fdt.begin_node("cpu-map")?; // Create device tree nodes with regard of above mapping. @@ -510,7 +514,14 @@ fn create_memory_node( ) -> FdtWriterResult<()> { // See https://github.com/torvalds/linux/blob/58ae0b51506802713aa0e9956d1853ba4c722c98/Documentation/devicetree/bindings/numa.txt // for NUMA setting in memory node. - if numa_nodes.len() > 1 { + 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 { 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 = Vec::new(); @@ -527,12 +538,15 @@ fn create_memory_node( node_memory_addr = memory_region_start_addr; } } - 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)?; + // 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)?; + } } } else { // Note: memory regions from "GuestMemory" are sorted and non-zero sized. @@ -678,8 +692,8 @@ fn create_gic_node(fdt: &mut FdtWriter, gic_device: &Arc>) -> Fd fdt.property_array_u64("reg", &msi_reg_prop)?; if msi_compatibility == GIC_V2M_COMPATIBLE { - fdt.property_u32("arm,msi-base-spi", SPI_BASE)?; - fdt.property_u32("arm,msi-num-spis", SPI_NUM)?; + 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)?; @@ -850,6 +864,21 @@ fn create_gpio_node( Ok(()) } +// https://www.kernel.org/doc/Documentation/devicetree/bindings/arm/fw-cfg.txt +#[cfg(feature = "fw_cfg")] +fn create_fw_cfg_node( + 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( fdt: &mut FdtWriter, dev_info: &HashMap<(DeviceType, String), T, S>, @@ -865,6 +894,8 @@ fn create_devices_node { ordered_virtio_device.push(info); } + #[cfg(feature = "fw_cfg")] + DeviceType::FwCfg => create_fw_cfg_node(fdt, info)?, } } @@ -994,39 +1025,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 { - 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")?; - - // '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)?; - } + 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)?; + + // 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)?; + + fdt.end_node(virtio_iommu_node)?; } fdt.end_node(pci_node)?; @@ -1036,6 +1067,22 @@ 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 = 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. @@ -1048,26 +1095,33 @@ 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(); - 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); + // 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); distance_matrix.push(10_u32); continue; } - 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, - ); + 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); } } fdt.property_array_u32("distance-matrix", distance_matrix.as_ref())?; @@ -1144,7 +1198,7 @@ fn print_node(node: fdt_parser::node::FdtNode<'_, '_>, n_spaces: usize) { array, indent = (n_spaces + 2) ); - }; + } } // Print children nodes if there is any @@ -1152,3 +1206,118 @@ fn print_node(node: fdt_parser::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, device_id: Option) -> 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 = 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"); + } +} diff --git a/arch/src/aarch64/layout.rs b/arch/src/aarch64/layout.rs index dc2c74e398..66a12958a8 100644 --- a/arch/src/aarch64/layout.rs +++ b/arch/src/aarch64/layout.rs @@ -139,11 +139,11 @@ pub const IRQ_BASE: u32 = 32; /// Number of supported interrupts pub const IRQ_NUM: u32 = 256; -/// Base SPI interrupt number -pub const SPI_BASE: u32 = 32; +/// Base SPI interrupt number for the GICv2M MSI frame +pub const GICV2M_SPI_BASE: u32 = 128; -/// Total number of SPIs -pub const SPI_NUM: u32 = 64; +/// 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"; diff --git a/arch/src/aarch64/mod.rs b/arch/src/aarch64/mod.rs index 51f51ccaf6..79c2670920 100644 --- a/arch/src/aarch64/mod.rs +++ b/arch/src/aarch64/mod.rs @@ -15,7 +15,7 @@ use std::sync::{Arc, Mutex}; use hypervisor::arch::aarch64::gic::Vgic; use hypervisor::arch::aarch64::regs::MPIDR_EL1; -use log::{log_enabled, Level}; +use log::{Level, log_enabled}; use thiserror::Error; use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic}; @@ -66,8 +66,8 @@ pub struct EntryPoint { /// Configure the specified VCPU, and return its MPIDR. pub fn configure_vcpu( - vcpu: &Arc, - id: u8, + vcpu: &dyn hypervisor::Vcpu, + id: u32, boot_setup: Option<(EntryPoint, &GuestMemoryAtomic)>, ) -> super::Result { if let Some((kernel_entry_point, _guest_memory)) = boot_setup { @@ -125,8 +125,8 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> { pub fn configure_system( guest_mem: &GuestMemoryMmap, cmdline: &str, - vcpu_mpidr: Vec, - vcpu_topology: Option<(u8, u8, u8)>, + vcpu_mpidr: &[u64], + vcpu_topology: Option<(u16, u16, u16, u16)>, device_info: &HashMap<(DeviceType, String), T, S>, initrd: &Option, pci_space_info: &[PciSpaceInfo], @@ -154,7 +154,7 @@ pub fn configure_system) -> u8 { +pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 { let host_cpu_phys_bits = hypervisor.get_host_ipa_limit().try_into().unwrap(); if host_cpu_phys_bits == 0 { // Host kernel does not support `get_host_ipa_limit`, @@ -192,7 +192,7 @@ pub fn get_host_cpu_phys_bits(hypervisor: &Arc) -> u } #[cfg(test)] -mod tests { +mod unit_tests { use super::*; #[test] diff --git a/arch/src/aarch64/uefi.rs b/arch/src/aarch64/uefi.rs index bd40e36ff0..2ff3a8638f 100644 --- a/arch/src/aarch64/uefi.rs +++ b/arch/src/aarch64/uefi.rs @@ -7,7 +7,7 @@ use std::os::fd::AsFd; use std::result; use thiserror::Error; -use vm_memory::{GuestAddress, GuestMemory}; +use vm_memory::{Bytes, GuestAddress, GuestMemory}; /// Errors thrown while loading UEFI binary #[derive(Debug, Error)] diff --git a/arch/src/bin/generate-cpu-profile.rs b/arch/src/bin/generate-cpu-profile.rs new file mode 100644 index 0000000000..4710fd277e --- /dev/null +++ b/arch/src/bin/generate-cpu-profile.rs @@ -0,0 +1,30 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// +#![cfg(all( + target_arch = "x86_64", + feature = "cpu_profile_generation", + feature = "kvm" +))] + +use anyhow::Context; +use clap::{Arg, Command}; + +fn main() -> anyhow::Result<()> { + let cmd_arg = Command::new("generate-cpu-profile") + .version(env!("CARGO_PKG_VERSION")) + .arg_required_else_help(true) + .arg( + Arg::new("name") + .help("The name to give the CPU profile") + .num_args(1) + .required(true), + ) + .get_matches(); + + let profile_name = cmd_arg.get_one::("name").unwrap(); + + let hypervisor = hypervisor::new().context("Could not obtain hypervisor")?; + arch::x86_64::cpu_profile_generation::generate_profile_data(hypervisor.as_ref(), profile_name) +} diff --git a/arch/src/lib.rs b/arch/src/lib.rs index 333a65d9c4..28c095fff6 100644 --- a/arch/src/lib.rs +++ b/arch/src/lib.rs @@ -8,18 +8,18 @@ //! Implements platform specific functionality. //! Supported platforms: x86_64, aarch64, riscv64. -#[macro_use] -extern crate log; - use std::collections::BTreeMap; +use std::io::Write; +use std::str::FromStr; use std::sync::Arc; use std::{fmt, result}; -use serde::{Deserialize, Serialize}; +use serde::de::IntoDeserializer; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; #[cfg(target_arch = "x86_64")] -use crate::x86_64::SgxEpcSection; +pub use crate::x86_64::cpu_profile::CpuProfile; type GuestMemoryMmap = vm_memory::GuestMemoryMmap; type GuestRegionMmap = vm_memory::GuestRegionMmap; @@ -59,6 +59,68 @@ pub enum Error { /// Type for returning public functions outcome. pub type Result = result::Result; +// If the target_arch is x86_64 we import CpuProfile from the x86_64 module, otherwise we +// declare it here. +#[cfg(not(target_arch = "x86_64"))] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +/// A [`CpuProfile`] is a mechanism for ensuring live migration compatibility +/// between host's with potentially different CPU models. +pub enum CpuProfile { + #[default] + Host, +} + +impl FromStr for CpuProfile { + type Err = serde::de::value::Error; + fn from_str(s: &str) -> result::Result { + // Should accept both plain strings, and strings surrounded by `"`. + let normalized = s + .strip_prefix('"') + .unwrap_or(s) + .strip_suffix('"') + .unwrap_or(s); + Self::deserialize(normalized.into_deserializer()) + } +} + +// We introduce some utilities for serializing u32 values as hex. +// These are only necessary for (de-)serializing CPU profile data. + +/// Serializes the given `input` as a hex string (starting with "0x") +fn serialize_u32_hex( + input: &u32, + serializer: S, +) -> std::result::Result { + eval_u32_hex(*input, |hex| serializer.serialize_str(hex)) +} + +/// Converts `input` into a hex string representation (starting with "0x", but the length may vary) and +/// applies the given `callback` to it. +fn eval_u32_hex(input: u32, callback: F) -> T +where + F: FnOnce(&str) -> T, +{ + // two bytes for "0x" prefix and at most eight for the hex encoded number + let mut buffer = [0_u8; 10]; + let mut write_slice = &mut buffer[..]; + write!(write_slice, "{input:#x}").expect("This write should be infallible"); + let len = 10 - write_slice.len(); + let str = core::str::from_utf8(&buffer[..len]) + .expect("the buffer should be filled with valid UTF-8 bytes"); + callback(str) +} + +/// Deserializes a u32 from a hex string representation +fn deserialize_u32_hex<'de, D: Deserializer<'de>>( + deserializer: D, +) -> std::result::Result { + let hex = <&'de str as Deserialize>::deserialize(deserializer)?; + u32::from_str_radix(hex.strip_prefix("0x").unwrap_or(""), 16).map_err(|_| { + ::custom(format!("{hex} is not a hex encoded 32 bit integer")) + }) +} + /// Type for memory region types. #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum RegionType { @@ -84,9 +146,9 @@ pub mod aarch64; #[cfg(target_arch = "aarch64")] pub use aarch64::{ - 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, + _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. @@ -95,9 +157,9 @@ pub mod riscv64; #[cfg(target_arch = "riscv64")] pub use riscv64::{ - 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, EntryPoint, _NSIG, + _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, }; #[cfg(target_arch = "x86_64")] @@ -105,10 +167,9 @@ pub mod x86_64; #[cfg(target_arch = "x86_64")] pub use x86_64::{ - arch_memory_regions, configure_system, configure_vcpu, generate_common_cpuid, - generate_ram_ranges, get_host_cpu_phys_bits, initramfs_load_addr, layout, - layout::CMDLINE_MAX_SIZE, layout::CMDLINE_START, regs, CpuidConfig, CpuidFeatureEntry, - EntryPoint, _NSIG, + _NSIG, CpuidConfig, CpuidFeatureEntry, EntryPoint, arch_memory_regions, configure_system, + configure_vcpu, generate_common_cpuid, generate_ram_ranges, get_host_cpu_phys_bits, + initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE, layout::CMDLINE_START, regs, }; /// Safe wrapper for `sysconf(_SC_PAGESIZE)`. @@ -123,12 +184,11 @@ fn pagesize() -> usize { pub struct NumaNode { pub memory_regions: Vec>, pub hotplug_regions: Vec>, - pub cpus: Vec, + pub cpus: Vec, pub pci_segments: Vec, pub distances: BTreeMap, pub memory_zones: Vec, - #[cfg(target_arch = "x86_64")] - pub sgx_epc_sections: Vec, + pub device_id: Option, } pub type NumaNodes = BTreeMap; @@ -155,6 +215,9 @@ 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. diff --git a/arch/src/riscv64/fdt.rs b/arch/src/riscv64/fdt.rs index 1a7e2e5f46..f97e6d6b5e 100644 --- a/arch/src/riscv64/fdt.rs +++ b/arch/src/riscv64/fdt.rs @@ -15,6 +15,7 @@ use std::{cmp, result, str}; use byteorder::{BigEndian, ByteOrder}; use hypervisor::arch::riscv64::aia::Vaia; +use log::debug; use thiserror::Error; use vm_fdt::{FdtWriter, FdtWriterResult}; use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion}; @@ -65,6 +66,7 @@ pub fn create_fdt, aia_device: &Arc>, initrd: &Option, @@ -84,7 +86,7 @@ pub fn create_fdt, guest_mem: &GuestMemoryMmap) -> Result<()> { +pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Result<()> { // Write FDT to memory. guest_mem - .write_slice(fdt_final.as_slice(), super::layout::FDT_START) + .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) -> FdtWriterResult<()> { +fn create_cpu_nodes(fdt: &mut FdtWriter, num_cpus: u32, isa_string: &str) -> FdtWriterResult<()> { // See https://elixir.bootlin.com/linux/v6.10/source/Documentation/devicetree/bindings/riscv/cpus.yaml let cpus = fdt.begin_node("cpus")?; // As per documentation, on RISC-V 64-bit systems value should be set to 1. @@ -119,11 +121,11 @@ fn create_cpu_nodes(fdt: &mut FdtWriter, num_cpus: u32) -> FdtWriterResult<()> { fdt.property_u32("timebase-frequency", timebase_frequency)?; for cpu_index in 0..num_cpus { - let cpu = fdt.begin_node(&format!("cpu@{:x}", cpu_index))?; + 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", "rv64imafdc_smaia_ssaia")?; + 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)?; @@ -184,7 +186,7 @@ fn create_memory_node(fdt: &mut FdtWriter, guest_mem: &GuestMemoryMmap) -> FdtWr } let ram_start = super::layout::RAM_START.raw_value(); - let memory_node_name = format!("memory@{:x}", ram_start); + 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)?; @@ -448,10 +450,7 @@ fn print_node(node: fdt_parser::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) => match value_cstr.to_str() { - Ok(value_str) => Some(value_str), - Err(_e) => None, - }, + Ok(value_cstr) => value_cstr.to_str().ok(), Err(_e) => None, }; @@ -474,7 +473,7 @@ fn print_node(node: fdt_parser::node::FdtNode<'_, '_>, n_spaces: usize) { array, indent = (n_spaces + 2) ); - }; + } } // Print children nodes if there is any diff --git a/arch/src/riscv64/layout.rs b/arch/src/riscv64/layout.rs index 40583301c1..0b9de1bc54 100644 --- a/arch/src/riscv64/layout.rs +++ b/arch/src/riscv64/layout.rs @@ -44,16 +44,23 @@ // | | // | 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 -/// 0x0 ~ 0x0400_0000 (64 MiB) resides APLICs -pub const APLIC_START: GuestAddress = GuestAddress(0); +/// 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 @@ -91,7 +98,12 @@ pub const CMDLINE_MAX_SIZE: usize = 1024; pub const FDT_START: GuestAddress = RAM_START; pub const FDT_MAX_SIZE: u64 = 0x1_0000; -/// Kernel start after FDT +/// 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 diff --git a/arch/src/riscv64/mod.rs b/arch/src/riscv64/mod.rs index a04cf9471f..8b89b94a5d 100644 --- a/arch/src/riscv64/mod.rs +++ b/arch/src/riscv64/mod.rs @@ -7,19 +7,28 @@ 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::{log_enabled, Level}; +use log::{Level, log_enabled}; use thiserror::Error; use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic}; pub use self::fdt::DeviceInfoForFdt; use crate::{DeviceType, GuestMemoryMmap, PciSpaceInfo, RegionType}; +pub const CLOUDHV_IRQCHIP_NUM_MSIS: u16 = 255; +pub const CLOUDHV_IRQCHIP_NUM_SOURCES: u8 = 96; +pub const CLOUDHV_IRQCHIP_NUM_PRIO_BITS: u8 = 3; +pub const CLOUDHV_IRQCHIP_MAX_GUESTS_BITS: u8 = 3; +pub const CLOUDHV_IRQCHIP_MAX_GUESTS: u8 = (1 << CLOUDHV_IRQCHIP_MAX_GUESTS_BITS) - 1; pub const _NSIG: i32 = 65; /// Errors thrown while configuring riscv64 system. @@ -44,6 +53,22 @@ pub enum Error { /// 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)] @@ -56,8 +81,8 @@ pub struct EntryPoint { /// Configure the specified VCPU, and return its MPIDR. pub fn configure_vcpu( - vcpu: &Arc, - id: u8, + vcpu: &dyn hypervisor::Vcpu, + id: u32, boot_setup: Option<(EntryPoint, &GuestMemoryAtomic)>, ) -> super::Result<()> { if let Some((kernel_entry_point, _guest_memory)) = boot_setup { @@ -97,6 +122,43 @@ pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> { ] } +// 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 { + 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 = + 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. #[allow(clippy::too_many_arguments)] pub fn configure_system( @@ -108,10 +170,12 @@ pub fn configure_system>, ) -> 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, @@ -123,7 +187,7 @@ pub fn configure_system) -> u8 { +pub fn get_host_cpu_phys_bits(_hypervisor: &dyn hypervisor::Hypervisor) -> u8 { 40 } #[cfg(test)] -mod tests { +mod unit_tests { use super::*; #[test] diff --git a/arch/src/riscv64/uefi.rs b/arch/src/riscv64/uefi.rs new file mode 100644 index 0000000000..bd40e36ff0 --- /dev/null +++ b/arch/src/riscv64/uefi.rs @@ -0,0 +1,50 @@ +// 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::{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 = result::Result; + +pub fn load_uefi( + 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) +} diff --git a/arch/src/x86_64/cpu_profile.rs b/arch/src/x86_64/cpu_profile.rs new file mode 100644 index 0000000000..820c296b32 --- /dev/null +++ b/arch/src/x86_64/cpu_profile.rs @@ -0,0 +1,493 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::io::{Read, Write}; + +use flate2::read::ZlibDecoder; +use hypervisor::arch::x86::{CpuIdEntry, MsrEntry}; +use hypervisor::{CpuVendor, HypervisorType}; +use log::error; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::deserialize_u32_hex; +use crate::x86_64::CpuidReg; +use crate::x86_64::cpuid_definitions::Parameters; +use crate::x86_64::msr_definitions::RegisterAddress; + +// build.rs generates a CpuProfiles enum with a variant for each +// CPU profile in arch/x86_64/cpu_profiles and also has the default +// host variant as well. +// +// Furthermore the build script also generates the functions +// `compressed_cpuid_data`, `compressed_msr_data` for obtaining the +// compressed JSON data associated with the given cpu profile. +include!(concat!(env!("OUT_DIR"), "/generated_cpu_profiles.rs")); + +impl CpuProfile { + /// Loads pre-generated CPUID data associated with a CPU profile. + /// + /// If the `amx` flag is false then the AMX tile state components will be + /// zeroed out from the associated profile data. This is necessary because + /// they will then not be present in the vector of [`CpuidEntry`] values + /// obtained from the hypervisor. + // + // We can only generate CPU profiles for the KVM hypervisor for the time being. + pub(in crate::x86_64) fn cpuid_data(&self, amx: bool) -> Option { + const ESTIMATED_CPUID_CPU_PROFILE_DATA_COMPRESSION_RATIO: usize = 32; + + // The compressed_cpuid_data function is generated by build.rs + let compressed: &[u8] = compressed_cpuid_data(self)?; + let mut data: CpuIdProfileData = { + serde_json::from_slice(&Self::decompress_cpu_profile_data( + compressed, + ESTIMATED_CPUID_CPU_PROFILE_DATA_COMPRESSION_RATIO, + )) + .expect("Should be able to deserialize CPU profile CPUID data") + }; + if !amx { + // In this case we will need to wipe out the AMX tile state components (if they are included in the profile) + for adj in data.adjustments.iter_mut() { + if adj.0.sub_leaf.start() != adj.0.sub_leaf.end() { + continue; + } + let sub_leaf = *adj.0.sub_leaf.start(); + let leaf = adj.0.leaf; + if (leaf == 0xd) && (sub_leaf == 0) && (adj.0.register == CpuidReg::EAX) { + adj.1.replacements &= !((1 << 17) | (1 << 18)); + } + + if (leaf == 0xd) && (sub_leaf == 1) && (adj.0.register == CpuidReg::ECX) { + adj.1.replacements &= !((1 << 17) | (1 << 18)); + } + + if (leaf == 0xd) && ((sub_leaf == 17) | (sub_leaf == 18)) { + adj.1.replacements = 0; + } + } + } + + Some(data) + } + + /// Loads pre-generated MSR data associated with a CPU profile. + pub(in crate::x86_64) fn msr_data(&self) -> Option { + const ESTIMATED_MSR_CPU_PROFILE_DATA_COMPRESSION_RATIO: usize = 4; + + // compressed_msr_data is created by build.rs + let compressed: &[u8] = compressed_msr_data(self)?; + serde_json::from_slice(&Self::decompress_cpu_profile_data( + compressed, + ESTIMATED_MSR_CPU_PROFILE_DATA_COMPRESSION_RATIO, + )) + .expect("Should be able to deserialize CPU profile MSR data") + } + + /// Decompress the `compressed` byte slice. + /// + /// The `estimated_compression_ratio` is just used for optimizing the number of necessary allocations + /// and does not have to be accurate. + fn decompress_cpu_profile_data( + compressed: &[u8], + estimated_compression_ratip: usize, + ) -> Vec { + let mut decoder = ZlibDecoder::new(compressed); + // Don't expect more than a 32x compression ratio + let mut v = Vec::with_capacity(compressed.len() * estimated_compression_ratip); + decoder + .read_to_end(&mut v) + .expect("Should be able to decompress CPU profile data"); + v + } +} + +/// Every [`CpuProfile`] different from `Host` has associated [`CpuIdProfileData`]. +/// +/// New constructors of this struct may only be generated through the CHV CLI (when built from source with +/// the `cpu-profile-generation` feature) which other hosts may then attempt to load in order to +/// increase the likelihood of successful live migrations among all hosts that opted in to the given +/// CPU profile. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[allow(dead_code)] +pub struct CpuIdProfileData { + /// The hypervisor used when generating this CPU profile. + pub(in crate::x86_64) hypervisor: HypervisorType, + /// The vendor of the CPU belonging to the host that generated this CPU profile. + pub(in crate::x86_64) cpu_vendor: CpuVendor, + /// Adjustments necessary to become compatible with the desired target. + pub(in crate::x86_64) adjustments: Vec<(Parameters, CpuidOutputRegisterAdjustments)>, +} + +/// Used for adjusting an entire cpuid output register (EAX, EBX, ECX or EDX) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub(super) struct CpuidOutputRegisterAdjustments { + #[serde(deserialize_with = "deserialize_u32_hex")] + pub(in crate::x86_64) replacements: u32, + /// Used to zero out the area `replacements` occupy. This mask is not necessarily !replacements, as replacements may pack values of different types (i.e. it is wrong to think of it as a bitset conceptually speaking). + #[serde(deserialize_with = "deserialize_u32_hex")] + pub(in crate::x86_64) mask: u32, +} + +/* +We want to serialize the values as 10 bytes, starting with 0x, +regardless of the value. This makes it easier for humans to compare different serialized values. +*/ +impl Serialize for CpuidOutputRegisterAdjustments { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("CpuidOutputRegisterAdjustments", 2)?; + let mut serialize_field = |key, value| { + // two bytes for "0x" prefix and eight for the hex encoded number + let mut buffer = [0_u8; 10]; + write!(&mut buffer[..], "{value:#010x}").expect("This write should be infallible"); + let str = core::str::from_utf8(&buffer[..]) + .expect("the buffer should be filled with valid UTF-8 bytes"); + s.serialize_field(key, str) + }; + serialize_field("replacements", self.replacements)?; + serialize_field("mask", self.mask)?; + s.end() + } +} + +impl CpuidOutputRegisterAdjustments { + pub(in crate::x86_64) fn adjust(self, cpuid_output_register: &mut u32) { + let temp_register_copy = *cpuid_output_register; + let replacements_area_masked_in_temp_copy = temp_register_copy & self.mask; + *cpuid_output_register = replacements_area_masked_in_temp_copy | self.replacements; + } + + pub(in crate::x86_64) fn adjust_cpuid_entries( + mut cpuid: Vec, + adjustments: &[(Parameters, Self)], + ) -> Result, 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), + ] { + // Get the adjustment corresponding to the entry's function/leaf and index/sub-leaf for each of the register. If no such + // adjustment is found we use the trivial adjustment (leading to the register being zeroed out entirely). + let adjustment = adjustments + .iter() + .find_map(|(param, adjustment)| { + ((param.leaf == entry.function) + & param.sub_leaf.contains(&entry.index) + & (param.register == reg)) + .then_some(*adjustment) + }) + .unwrap_or(CpuidOutputRegisterAdjustments { + mask: 0, + replacements: 0, + }); + adjustment.adjust(reg_value); + } + } + + Self::expected_entries_found(&cpuid, adjustments).map(|_| 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: &[(Parameters, Self)], + ) -> Result<(), MissingCpuidEntriesError> { + let mut missing_entry = false; + + // Invalid state components can be ignored. The next few lines obtain the relevant entries to + // check for this. + let eax_0xd_0 = cpuid + .iter() + .find(|entry| (entry.function == 0xd) && (entry.index == 0)) + .map_or(0, |entry| entry.eax); + let ecx_0xd_1 = cpuid + .iter() + .find(|entry| (entry.function == 0xd) && (entry.index == 1)) + .map_or(0, |entry| entry.ecx); + + let edx_0xd_0 = cpuid + .iter() + .find(|entry| (entry.function == 0xd) && (entry.index == 0)) + .map_or(0, |entry| entry.edx); + let edx_0xd_1 = cpuid + .iter() + .find(|entry| (entry.function == 0xd) && (entry.index == 1)) + .map_or(0, |entry| entry.edx); + + for (param, adjustment) in adjustments { + if adjustment.replacements == 0 { + continue; + } + let sub_start = *param.sub_leaf.start(); + let sub_end = *param.sub_leaf.end(); + + let can_skip_lo = if (param.leaf == 0xd) && (2..32).contains(&sub_start) { + let start = sub_start; + let end = std::cmp::min(sub_end, 31); + let mask = (start..=end).fold(0, |acc, next| acc | (1 << next)); + ((mask & eax_0xd_0) == 0) & ((mask & ecx_0xd_1) == 0) + } else { + false + }; + + let can_skip_hi = if (param.leaf == 0xd) && (32..64).contains(&sub_end) { + let start = std::cmp::max(32, sub_start); + let end = sub_end; + let mask = (start..=end) + .map(|val| val - 32) + .fold(0, |acc, next| acc | (1 << next)); + ((mask & edx_0xd_0) == 0) & ((mask & edx_0xd_1) == 0) + } else { + false + }; + + if can_skip_lo && can_skip_hi { + // This means that all state components referred to by the specified sub-leaf range are not valid + // and may be skipped. + 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(()) + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(in crate::x86_64) struct FeatureMsrAdjustment { + pub(in crate::x86_64) mask: u64, + pub(in crate::x86_64) replacements: u64, +} + +impl Serialize for FeatureMsrAdjustment { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("FeatureMsrAdjustment", 2)?; + let mut serialize_field = |key, value| { + // two bytes for "0x" prefix and 16 for the hex encoded number + let mut buffer = [0_u8; 18]; + let _ = write!(&mut buffer[..], "{value:#018x}"); + let str = core::str::from_utf8(&buffer[..]) + .expect("the buffer should be filled with valid UTF-8 bytes"); + s.serialize_field(key, str) + }; + serialize_field("mask", self.mask)?; + serialize_field("replacements", self.replacements)?; + s.end() + } +} + +impl<'de> Deserialize<'de> for FeatureMsrAdjustment { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct ProvisionalFeatureMsrAdjustment<'a> { + #[serde(borrow)] + mask: &'a str, + #[serde(borrow)] + replacements: &'a str, + } + + let ProvisionalFeatureMsrAdjustment { mask, replacements } = + ProvisionalFeatureMsrAdjustment::deserialize(deserializer)?; + let parse_u64 = |hex: &str, field_name: &str| { + u64::from_str_radix(hex.strip_prefix("0x").unwrap_or(""), 16).map_err(|_| { + ::custom(format!("Unable to deserialize FeatureMsrAdjustment: could not deserialize {field_name} the value {hex} is not a hex encoded 64 bit integer")) + }) + }; + let mask = parse_u64(mask, "mask")?; + let replacements = parse_u64(replacements, "replacements")?; + Ok(FeatureMsrAdjustment { mask, replacements }) + } +} + +impl FeatureMsrAdjustment { + /// Returns a struct describing the Feature MSRs that should be set + /// and the ones that should be denied based on `adjustments` and the given + /// `feature_msrs`. + /// + /// # Errors + /// + /// The only way for this to error is if there exists one or more entries in + /// `adjustments` that do not have a corresponding entry in `feature_msrs`. + /// In this case the missing MSR will be logged and the unit type is returned + /// as the error variant. + pub(in crate::x86_64) fn adjust_to( + adjustments: &[(RegisterAddress, FeatureMsrAdjustment)], + feature_msrs: &[MsrEntry], + ) -> Result, ()> { + let mut output_feature_msrs = Vec::with_capacity(feature_msrs.len()); + for (reg_address, adjustment) in adjustments { + let Some(entry) = feature_msrs + .iter() + .find(|entry| entry.index == reg_address.0) + else { + error!( + "Did not find feature based MSR entry for MSR:={:#x}", + reg_address.0 + ); + return Err(()); + }; + // Adjust the entry and push it to outputs + { + let mut entry = *entry; + let data = entry.data; + entry.data = (adjustment.mask & data) | adjustment.replacements; + // TODO: Perhaps trace! would be a better log level? + log::debug!( + "adjusted MSR-based feature: register address:={:#x} value:={:#x}, previous value:={data:#x}", + entry.index, + entry.data + ); + output_feature_msrs.push(entry); + } + } + Ok(output_feature_msrs) + } +} + +pub struct RequiredMsrUpdates { + pub msr_based_features: Vec, + pub denied_msrs: Vec, +} + +/// Every [`CpuProfile`] different from `Host` has associated [`MsrProfileData`]. +/// +/// New constructors of this struct may only be generated through the CHV CLI (when built from source with +/// the `cpu-profile-generation` feature) which other hosts may then attempt to load in order to +/// increase the likelihood of successful live migrations among all hosts that opted in to the given +/// CPU profile. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub(in crate::x86_64) struct MsrProfileData { + pub(in crate::x86_64) cpu_vendor: CpuVendor, + pub(in crate::x86_64) hypervisor_type: HypervisorType, + pub(in crate::x86_64) adjustments: Vec<(RegisterAddress, FeatureMsrAdjustment)>, + pub(in crate::x86_64) permitted_msrs: Vec, +} + +#[derive(Debug, Error)] +#[error("Required CPUID entries not found")] +pub struct MissingCpuidEntriesError; + +#[derive(Debug, Error)] +#[error("Required MSR entries not found")] +pub struct MissingMsrEntriesError; + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::CpuidOutputRegisterAdjustments; + use crate::CpuProfile; + #[cfg(feature = "kvm")] + use crate::x86_64::cpu_profile::{CpuIdProfileData, MsrProfileData}; + + // Check that serializing and then deserializing `CpuidOutputResiterAdjustments` results in the same value we started with. + // + // Also check that the serialized numeric values satisfy our expectations: They are 10-byte hex encoded strings + proptest! { + #[test] + fn cpuid_output_register_adjustments_serialization_works(replacements in any::(), mask in any::()) { + // Randomly generate these values. Several of the generated values will not represent anything that may be + // produced in practice, but (de-)serialization does not take such domain knowledge into account (if that changes + // then this test will need to be updated). + let adjustments = CpuidOutputRegisterAdjustments { + replacements, + mask + }; + let serialized = serde_json::to_string(&adjustments).unwrap(); + let deserialized: CpuidOutputRegisterAdjustments = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(&deserialized, &adjustments); + let json = serde_json::to_value(adjustments).unwrap(); + let replacements_str = json.get("replacements").unwrap().as_str().unwrap(); + let mask_str = json.get("mask").unwrap().as_str().unwrap(); + let check_str_invariants = |value: &str| { + prop_assert!(value.starts_with("0x")); + prop_assert_eq!(value.len(),10); + prop_assert!(value.as_bytes().iter().all(|byte| byte.is_ascii())); + let is_hex_digit = |byte: &u8| -> bool { + byte.is_ascii_digit() | (*byte == b'a') | (*byte == b'b') | (*byte == b'c') | (*byte == b'd') | (*byte == b'e') | (*byte == b'f') + }; + prop_assert!( + value.as_bytes()[2..].iter().all(is_hex_digit) + ); + Ok(()) + }; + check_str_invariants(replacements_str)?; + check_str_invariants(mask_str)?; + } + } + + #[test] + fn cpu_profile_host_loads_no_data() { + assert_eq!(CpuProfile::Host.cpuid_data(true), None); + assert_eq!(CpuProfile::Host.cpuid_data(false), None); + assert_eq!(CpuProfile::Host.msr_data(), None); + } + + /// Check that the `CpuProfile::cpuid_data` and `CpuProfile::msr_data` methods + /// coincide with direct deserialization for the `sapphire-rapids` profile. + #[cfg(feature = "kvm")] + #[test] + fn cpu_profile_loading_sapphire_rapids() { + // Now check that the methods coincide with direct deserialization. For the + // Sapphire Rapids profile this should be the case when `amx` is enabled. + let profile = CpuProfile::SapphireRapids; + let cpuid_data = profile.cpuid_data(true).unwrap(); + let deserialized_cpuid_data: CpuIdProfileData = + serde_json::from_slice(include_bytes!("./cpu_profiles/sapphire-rapids.cpuid.json")) + .unwrap(); + + assert_eq!(cpuid_data, deserialized_cpuid_data); + + let msr_data = profile.msr_data().unwrap(); + let deserialized_msr_data: MsrProfileData = + serde_json::from_slice(include_bytes!("./cpu_profiles/sapphire-rapids.msr.json")) + .unwrap(); + assert_eq!(msr_data, deserialized_msr_data); + } + + /// Check that the `CpuProfile::cpuid_data` and `CpuProfile::msr_data` methods + /// coincide with direct deserialization for the `skylake` profile. + #[cfg(feature = "kvm")] + #[test] + fn cpu_profile_loading_skylake() { + // Now check that the methods coincide with direct deserialization. For the + // Sapphire Rapids profile this should be the case when `amx` is enabled. + let profile = CpuProfile::Skylake; + let cpuid_data = profile.cpuid_data(true).unwrap(); + let deserialized_cpuid_data: CpuIdProfileData = + serde_json::from_slice(include_bytes!("./cpu_profiles/skylake.cpuid.json")).unwrap(); + + assert_eq!(cpuid_data, deserialized_cpuid_data); + + let msr_data = profile.msr_data().unwrap(); + let deserialized_msr_data: MsrProfileData = + serde_json::from_slice(include_bytes!("./cpu_profiles/skylake.msr.json")).unwrap(); + assert_eq!(msr_data, deserialized_msr_data); + } +} diff --git a/arch/src/x86_64/cpu_profile_generation.rs b/arch/src/x86_64/cpu_profile_generation.rs new file mode 100644 index 0000000000..a66a336bfa --- /dev/null +++ b/arch/src/x86_64/cpu_profile_generation.rs @@ -0,0 +1,591 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::collections::HashSet; +use std::fs::File; +use std::io::Write; +use std::ops::{BitOr, RangeInclusive, Shl}; +use std::path::PathBuf; + +use anyhow::{Context, anyhow}; +use hypervisor::arch::x86::{CpuIdEntry, MsrEntry}; +use hypervisor::{CpuVendor, Hypervisor, HypervisorError, HypervisorType}; +use log::warn; + +use crate::x86_64::cpu_profile::{CpuIdProfileData, FeatureMsrAdjustment, MsrProfileData}; +#[cfg(feature = "kvm")] +use crate::x86_64::cpuid_definitions::CpuidDefinitions; +use crate::x86_64::cpuid_definitions::intel::INTEL_CPUID_DEFINITIONS; +use crate::x86_64::cpuid_definitions::kvm::KVM_CPUID_DEFINITIONS; +use crate::x86_64::cpuid_definitions::{Parameters, ProfilePolicy}; +use crate::x86_64::msr_definitions::{self, MsrDefinitions, RegisterAddress}; +use crate::x86_64::{CpuidOutputRegisterAdjustments, CpuidReg}; + +/// Generate CPU profile data and convert it to a string, embeddable as Rust code, which is +/// written to the given `writer` (e.g. a File). +// +// NOTE: The MVP only works with KVM as the hypervisor and Intel CPUs. +#[cfg(feature = "kvm")] +pub fn generate_profile_data( + hypervisor: &dyn Hypervisor, + profile_name: &str, +) -> anyhow::Result<()> { + let cpu_vendor = hypervisor.get_cpu_vendor(); + if cpu_vendor != CpuVendor::Intel { + unimplemented!("CPU profiles can only be generated for Intel CPUs at this point in time"); + } + + let hypervisor_type = hypervisor.hypervisor_type(); + // This is just a reality check. + if hypervisor_type != HypervisorType::Kvm { + unimplemented!( + "CPU profiles can only be generated when using KVM as the hypervisor at this point in time" + ); + } + + let brand_string_bytes = cpu_brand_string_bytes(cpu_vendor, profile_name)?; + let cpuid = supported_cpuid(hypervisor)?; + let cpuid = overwrite_brand_string(cpuid, brand_string_bytes); + let supported_cpuid_sorted = sort_entries(cpuid); + + let Files { + cpuid_data_file, + cpuid_data_license_file, + msr_data_file, + msr_data_license_file, + } = create_files(profile_name)?; + + generate_cpuid_profile_data_with( + hypervisor_type, + cpu_vendor, + &supported_cpuid_sorted, + &INTEL_CPUID_DEFINITIONS, + &KVM_CPUID_DEFINITIONS, + cpuid_data_file, + cpuid_data_license_file, + )?; + + let supported_feature_msrs = hypervisor.get_msr_based_features().context("CPU profile generation failed: Could not get the supported MSR-based features from the hypervisor")?; + let supported_msrs = hypervisor + .get_msr_index_list() + .context("CPU profile generation failed: Could not get MSR index list")? + .into_iter() + .collect(); + + generate_msr_profile_data_with( + MsrProfileDataParams { + hypervisor_type, + cpu_vendor, + processor_feature_msr_definitions: + &msr_definitions::intel::INTEL_MSR_FEATURE_DEFINITIONS, + supported_feature_msrs: &supported_feature_msrs, + supported_msrs, + permitted_architectural_msrs: &msr_definitions::intel::PERMITTED_IA32_MSRS[..], + permitted_hypervisor_msrs: &msr_definitions::kvm::PROFILE_PERMITTED_KVM_MSRS[..], + permitted_hyperv_msrs: &msr_definitions::hyperv::HYPERV_MSRS[..], + non_architectural_msrs: &msr_definitions::intel::NON_ARCHITECTURAL_INTEL_MSRS[..], + forbidden_architectural_msrs: &msr_definitions::intel::FORBIDDEN_IA32_MSR_RANGES[..], + }, + msr_data_file, + msr_data_license_file, + ) +} + +struct Files { + cpuid_data_file: File, + cpuid_data_license_file: File, + msr_data_file: File, + msr_data_license_file: File, +} +/// Create empty files with names derived from the name given to the CPU profile. +/// The name will be lowercase and spaces are replaced with "-". +fn create_files(profile_name: &str) -> anyhow::Result { + let profile_file_name = { + let mut name = String::new(); + for part in profile_name.split_whitespace().map(|s| s.to_lowercase()) { + if !name.is_empty() { + name.push('-'); + } + name.push_str(&part); + } + name + }; + + let create_file = |path: PathBuf| { + File::create(path.clone()).with_context(|| { + format!( + "CPU profile generation failed: Could not create file:={}", + path.to_string_lossy() + ) + }) + }; + + let path_with_license = |mut path: PathBuf| { + path.as_mut_os_string().push(".license"); + path + }; + + let current_dir = std::env::current_dir() + .context("CPU profile generation failed: Unable to get the current working directory")?; + + let common_path = format!("arch/src/x86_64/cpu_profiles/{profile_file_name}"); + + let cpuid_profile_file_name = { + let mut path = current_dir.clone(); + path.push(format!("{common_path}.cpuid.json")); + path + }; + + let cpuid_data_file = create_file(cpuid_profile_file_name.clone())?; + + let cpuid_data_license_file_path = path_with_license(cpuid_profile_file_name); + + let cpuid_data_license_file = create_file(cpuid_data_license_file_path)?; + + let msr_profile_file_name = { + let mut path = current_dir; + path.push(format!("{common_path}.msr.json")); + path + }; + + let msr_data_file = create_file(msr_profile_file_name.clone())?; + + let msr_data_license_file_path = path_with_license(msr_profile_file_name); + let msr_data_license_file = create_file(msr_data_license_file_path)?; + + Ok(Files { + cpuid_data_file, + cpuid_data_license_file, + msr_data_file, + msr_data_license_file, + }) +} + +/// Prepare the bytes which the brand string should consist of +fn cpu_brand_string_bytes(cpu_vendor: CpuVendor, profile_name: &str) -> anyhow::Result<[u8; 48]> { + let cpu_vendor_str: String = serde_json::to_string(&cpu_vendor) + .expect("Should be possible to serialize CPU vendor to a string"); + let cpu_vendor_str = cpu_vendor_str.trim_start_matches('"').trim_end_matches('"'); + let mut brand_string_bytes = [0_u8; 4 * 3 * 4]; + if cpu_vendor_str.len() + 1 + profile_name.len() > brand_string_bytes.len() { + return Err(anyhow!( + "The profile name is too long. Try using a shorter name" + )); + } + for (b, brand_byte) in cpu_vendor_str + .as_bytes() + .iter() + .chain(std::iter::once(&b' ')) + .chain(profile_name.as_bytes()) + .zip(brand_string_bytes.iter_mut()) + { + *brand_byte = *b; + } + Ok(brand_string_bytes) +} +/// Computes [`CpuIdProfileData`] based on the given sorted vector of CPUID entries, hypervisor type, cpu_vendor +/// and cpuid_definitions. +/// +/// The computed [`CpuIdProfileData`] is then converted to a string representation, embeddable as Rust code, which is +/// then written by the given `writer`. +/// +// TODO: Consider making a snapshot test or two for this function. +fn generate_cpuid_profile_data_with( + hypervisor_type: HypervisorType, + cpu_vendor: CpuVendor, + supported_cpuid_sorted: &[CpuIdEntry], + processor_cpuid_definitions: &CpuidDefinitions, + hypervisor_cpuid_definitions: &CpuidDefinitions, + mut cpuid_data_file: impl Write, + cpuid_license_file: impl Write, +) -> anyhow::Result<()> { + let mut adjustments: Vec<(Parameters, CpuidOutputRegisterAdjustments)> = Vec::new(); + + for (parameter, values) in processor_cpuid_definitions + .as_slice() + .iter() + .chain(hypervisor_cpuid_definitions.as_slice().iter()) + { + for (sub_leaf_range, maybe_matching_register_output_value) in + extract_parameter_matches(parameter, supported_cpuid_sorted) + { + // If the compatibility target (current host) has multiple sub-leaves matching the parameter's range + // then we want to specialize: + let mut mask: u32 = 0; + let mut replacements: u32 = 0; + for value in values.as_slice() { + // Reality check on the bit range listed in `value` + { + assert!(value.bits_range.0 <= value.bits_range.1); + assert!(value.bits_range.1 < 32); + } + + match value.policy { + ProfilePolicy::Passthrough => { + // The profile should take whatever we get from the host, hence there is no adjustment, but our + // mask needs to retain all bits in the range of bits corresponding to this value + let (first_bit_pos, last_bit_pos) = value.bits_range; + mask |= bit_range_mask::(first_bit_pos, last_bit_pos); + } + ProfilePolicy::Static(overwrite_value) => { + replacements |= overwrite_value << value.bits_range.0; + } + ProfilePolicy::Inherit => { + // The value is supposed to be obtained from the compatibility target if it exists + let (first_bit_pos, last_bit_pos) = value.bits_range; + if let Some(matching_register_value) = maybe_matching_register_output_value + { + let extraction_mask = + bit_range_mask::(first_bit_pos, last_bit_pos); + let value = matching_register_value & extraction_mask; + replacements |= value; + } + } + } + } + adjustments.push(( + Parameters { + leaf: parameter.leaf, + sub_leaf: sub_leaf_range, + register: parameter.register, + }, + CpuidOutputRegisterAdjustments { mask, replacements }, + )); + } + } + + let cpuid_profile_data = CpuIdProfileData { + hypervisor: hypervisor_type, + cpu_vendor, + adjustments, + }; + + serde_json::to_writer_pretty(&mut cpuid_data_file, &cpuid_profile_data) + .context("Cpu profile generation failed: Could not serialize the generated cpuid profile data to the given writer")?; + cpuid_data_file + .flush() + .context("CPU profile generation failed: Unable to flush cpuid profile data")?; + write_license_file(cpuid_license_file, "CPUID") +} + +struct MsrProfileDataParams<'a, const N: usize> { + hypervisor_type: HypervisorType, + cpu_vendor: CpuVendor, + processor_feature_msr_definitions: &'a MsrDefinitions, + + /// MSR-based features supported by the hardware and hypervisor used to + /// generate this CPU profile. + supported_feature_msrs: &'a [MsrEntry], + /// MSRs supported by the hardware and hypervisor used to generate this + /// CPU profile. + supported_msrs: HashSet, + /// A list of all architectural MSRs that are permitted if they are also + /// contained in `supported_msrs`. + permitted_architectural_msrs: &'a [u32], + /// MSRs defined by the hypervisor that are permitted if they are supported + /// by the hardware and hypervisor used when generating this CPU profile + /// + /// We let CHV make the final decision at runtime whether they should be + /// available to guests (currently via CPUID) + permitted_hypervisor_msrs: &'a [u32], + /// Hyper-V related MSRs. + /// + /// NOTE: We can only know if these are truly permitted when the profile is + ///applied at runtime, hence we include them in the profile regardless and + ///let CHV remove them if necessary upon applying the CPU profile. + permitted_hyperv_msrs: &'a [u32], + /// A list of known non-architectural MSRs. This list is only used to help + /// us detect MSRs that we might not be aware of. + non_architectural_msrs: &'a [u32], + /// A list of known ranges of architectural msrs, that should not be + /// permitted by any generated CPU profile. This list is only used to help + /// us detect MSRs that we might not be aware of. + forbidden_architectural_msrs: &'a [(u32, u32)], +} + +fn generate_msr_profile_data_with<'a, const N: usize>( + MsrProfileDataParams { + hypervisor_type, + cpu_vendor, + processor_feature_msr_definitions, + supported_feature_msrs, + supported_msrs, + permitted_architectural_msrs, + permitted_hypervisor_msrs, + permitted_hyperv_msrs, + non_architectural_msrs, + forbidden_architectural_msrs, + }: MsrProfileDataParams<'a, N>, + mut msr_data_file: impl Write, + msr_license_file: impl Write, +) -> anyhow::Result<()> { + const KVM_GET_NOT_SET_MSRS: [RegisterAddress; 6] = [ + RegisterAddress::IA32_VMX_PINBASED_CTLS, + RegisterAddress::IA32_VMX_PROCBASED_CTLS, + RegisterAddress::IA32_VMX_EXIT_CTLS, + RegisterAddress::IA32_VMX_ENTRY_CTLS, + RegisterAddress::IA32_VMX_CR0_FIXED1, + RegisterAddress::IA32_VMX_CR4_FIXED1, + ]; + let mut entries_encountered = 0; + let mut adjustments = Vec::new(); + let mut permitted_msrs = HashSet::new(); + 'table: for (reg_addr, definitions) in processor_feature_msr_definitions.as_slice() { + let Some(entry) = supported_feature_msrs + .iter() + .find(|e| e.index == reg_addr.0) + else { + continue; + }; + entries_encountered += 1; + + // NOTE: For now this tool only supports KVM, but we insert this check so we don't forget + // about (possible) KVM specific behavior. + if hypervisor_type == HypervisorType::Kvm && KVM_GET_NOT_SET_MSRS.contains(reg_addr) { + // In this case we do not want to record an update, but just that the MSR is permitted. + permitted_msrs.insert(reg_addr.0); + continue; + } + + let value = entry.data; + let mut replacements = 0; + let mut mask = 0; + let mut bits_accounted_for = 0; + for msr_definitions::ValueDefinition { + policy, + bits_range: (first_bit_pos, last_bit_pos), + .. + } in definitions.as_slice().iter().copied() + { + let temp_mask = bit_range_mask::(first_bit_pos, last_bit_pos); + bits_accounted_for |= temp_mask; + match policy { + msr_definitions::ProfilePolicy::Deny => { + // This can only be applied to the entire MSR + assert_eq!(first_bit_pos, 0); + assert_eq!(last_bit_pos, 63); + continue 'table; + } + msr_definitions::ProfilePolicy::Inherit => { + replacements |= value & temp_mask; + } + msr_definitions::ProfilePolicy::Passthrough => { + mask |= temp_mask; + } + msr_definitions::ProfilePolicy::Static(overwrite_value) => { + replacements |= (overwrite_value) << (first_bit_pos); + } + } + } + // Reserved bit positions within an MSR value may get assigned meaning by hardware vendors in the future. + // For this reason we decide to have an "inherit" policy for these bits during profile generation. + let reserved_values = value & (!bits_accounted_for); + replacements |= reserved_values; + + permitted_msrs.insert(reg_addr.0); + adjustments.push((*reg_addr, FeatureMsrAdjustment { mask, replacements })); + } + + if entries_encountered != supported_feature_msrs.len() { + let unknown_register_address = supported_feature_msrs.iter().find(|entry| !processor_feature_msr_definitions.as_slice().iter().any(|(reg_addr, _)| reg_addr.0 == entry.index )).expect("We have checked that there should be at least one unknown supported MSR-based feature").index; + Err(anyhow!( + "CPU profile generation failed: The hardware and hypervisor supports MSR-based feature with register address:={unknown_register_address:#x}, but the CPU profile generation tool does not know what to do with this MSR. Please update the appropriate `MsrDefinitions` and try again." + ))?; + } + + for msr in permitted_architectural_msrs + .iter() + .chain(permitted_hypervisor_msrs) + .chain(permitted_hyperv_msrs) + { + if supported_msrs.contains(msr) { + let _ = permitted_msrs.insert(*msr); + } + } + + // Also check to see if there are any MSRs on the system that we are not aware off. In that case + // it might be a sign that this tool needs to update its definitions! + for msr in supported_msrs.difference(&permitted_msrs) { + let is_proc_feat_msr = processor_feature_msr_definitions + .as_slice() + .iter() + .any(|(reg_addr, _)| reg_addr.0 == *msr); + + let is_architectural_msr = forbidden_architectural_msrs + .iter() + .any(|r| (r.0..=r.1).contains(msr)); + + let is_non_architectural_msr = non_architectural_msrs.contains(msr); + + if is_proc_feat_msr || is_architectural_msr || is_non_architectural_msr { + continue; + } + + // TODO: Make this a hard error before upstreaming + warn!( + "Encountered unknown MSR:={:#x} when generating CPU profile. This CPU profile generation tool might not be up-to-date", + *msr + ); + } + + let permitted_msrs: Vec = { + let mut permitted_msrs: Vec = permitted_msrs.into_iter().collect(); + permitted_msrs.sort(); + permitted_msrs.into_iter().map(RegisterAddress).collect() + }; + + let msr_profile_data = MsrProfileData { + hypervisor_type, + cpu_vendor, + adjustments, + permitted_msrs, + }; + + serde_json::to_writer_pretty(&mut msr_data_file, &msr_profile_data) + .context("Cpu profile generation failed: Could not serialize the generated MSR profile data to the given writer")?; + msr_data_file + .flush() + .context("CPU profile generation failed: Unable to flush MSR profile data")?; + write_license_file(msr_license_file, "MSR") +} + +fn write_license_file(mut license_file: impl Write, data_type: &str) -> anyhow::Result<()> { + let license_text = { + r#"SPDX-FileCopyrightText: 2025 Cyberus Technology GmbH + +SPDX-License-Identifier: Apache-2.0 +"# + }; + license_file + .write_all(license_text.as_bytes()) + .with_context(|| { + format!("CPU profile generation failed: Unable to write to {data_type} profile data license file") + })?; + license_file.flush().context(format!( + "CPU profile generation failed: Unable to flush {data_type} profile data license file" + )) +} +/// Get as many of the supported CPUID entries from the hypervisor as possible. +fn supported_cpuid(hypervisor: &dyn Hypervisor) -> anyhow::Result> { + // Check for AMX compatibility. If this is supported we need to call arch_prctl before requesting the supported + // CPUID entries from the hypervisor. We simply call the enable_amx_state_components method on the hypervisor and + // ignore any AMX not supported error to achieve this. + match hypervisor.enable_amx_state_components() { + Ok(()) => {} + Err(HypervisorError::CouldNotEnableAmxStateComponents(amx_err)) => { + if matches!( + amx_err, + hypervisor::arch::x86::AmxGuestSupportError::AmxGuestTileRequest { .. } + ) { + return Err(amx_err).context("Unable to enable AMX state tiles for guests"); + } + } + Err(_) => unreachable!("Unexpected error when checking AMX support"), + } + + hypervisor + .get_supported_cpuid() + .context("CPU profile data generation failed") +} + +/// Overwrite the Processor brand string with the given `brand_string_bytes` +fn overwrite_brand_string( + mut cpuid: Vec, + brand_string_bytes: [u8; 48], +) -> Vec { + let mut iter = brand_string_bytes + .as_chunks::<4>() + .0 + .iter() + .map(|c| u32::from_le_bytes(*c)); + let mut overwrite = |leaf: u32| CpuIdEntry { + function: leaf, + index: 0, + flags: 0, + eax: iter.next().unwrap_or(0), + ebx: iter.next().unwrap_or(0), + ecx: iter.next().unwrap_or(0), + edx: iter.next().unwrap_or(0), + }; + for leaf in [0x80000002, 0x80000003, 0x80000004] { + if let Some(entry) = cpuid + .iter_mut() + .find(|entry| (entry.function == leaf) && (entry.index == 0)) + { + *entry = overwrite(leaf); + } else { + cpuid.push(overwrite(leaf)); + } + } + cpuid +} + +/// Sort the CPUID entries by function and index +fn sort_entries(mut cpuid: Vec) -> Vec { + cpuid.sort_unstable_by(|entry, other_entry| { + let fn_cmp = entry.function.cmp(&other_entry.function); + if fn_cmp == core::cmp::Ordering::Equal { + entry.index.cmp(&other_entry.index) + } else { + fn_cmp + } + }); + cpuid +} + +/// Returns a numeric value where each bit between `first_bit_pos` and `last_bit_pos` is set (including both ends) and all other bits are 0. +fn bit_range_mask(first_bit_pos: u8, last_bit_pos: u8) -> T +where + T: Shl, + T: BitOr, + T: From, +{ + (first_bit_pos..=last_bit_pos).fold(T::from(0_u8), |acc, next| acc | ((T::from(1_u8)) << next)) +} +/// Returns a vector of exact parameter matches ((sub_leaf ..= sub_leaf), register_value) interleaved by +/// the sub_leaf ranges specified by `param` that did not match any cpuid entry. +fn extract_parameter_matches( + param: &Parameters, + supported_cpuid_sorted: &[CpuIdEntry], +) -> Vec<(RangeInclusive, Option)> { + let register_value = |entry: &CpuIdEntry| -> u32 { + match param.register { + CpuidReg::EAX => entry.eax, + CpuidReg::EBX => entry.ebx, + CpuidReg::ECX => entry.ecx, + CpuidReg::EDX => entry.edx, + } + }; + let mut out = Vec::new(); + let param_range = param.sub_leaf.clone(); + let mut range_for_consideration = param_range.clone(); + let range_end = *range_for_consideration.end(); + for sub_leaf_entry in supported_cpuid_sorted + .iter() + .filter(|entry| entry.function == param.leaf && param_range.contains(&entry.index)) + { + let matching_subleaf = sub_leaf_entry.index; + + // If we are in the middle of the range, it means there is no entry matching the first few sub-leaves within the range + let current_range_start = *range_for_consideration.start(); + if current_range_start < matching_subleaf { + let range_not_matching = RangeInclusive::new(current_range_start, matching_subleaf - 1); + out.push((range_not_matching, None)); + } + + out.push(( + RangeInclusive::new(matching_subleaf, matching_subleaf), + Some(register_value(sub_leaf_entry)), + )); + if matching_subleaf == range_end { + return out; + } + // Update range_for_consideration: Note that we must have index + 1 <= range_end + range_for_consideration = RangeInclusive::new(matching_subleaf + 1, range_end); + } + // We did not find the last entry within the range hence we push the final range for consideration together with no matching register value + out.push((range_for_consideration, None)); + out +} diff --git a/arch/src/x86_64/cpu_profiles/sapphire-rapids.cpuid.json b/arch/src/x86_64/cpu_profiles/sapphire-rapids.cpuid.json new file mode 100644 index 0000000000..b0790bb426 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/sapphire-rapids.cpuid.json @@ -0,0 +1,3366 @@ +{ + "hypervisor": "Kvm", + "cpu_vendor": "Intel", + "adjustments": [ + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000020", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x756e6547", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x6c65746e", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x49656e69", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x000806f8", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ff00" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x76fa3223", + "mask": "0x89000000" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x078bfbff", + "mask": "0x08000000" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000004", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000002", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0xf1bf07ab", + "mask": "0x00002040" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x1b415f46", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0xbfc04410", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00001c30", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EDX" + }, + { + "replacements": "0x00000017", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x9", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x000602e7", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x0000001f", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000100", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EBX" + }, + { + "replacements": "0x00000240", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x4" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x4" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x4" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EAX" + }, + { + "replacements": "0x00000040", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x6", + "end": "0x6" + }, + "register": "EAX" + }, + { + "replacements": "0x00000200", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x7", + "end": "0x7" + }, + "register": "EAX" + }, + { + "replacements": "0x00000400", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EBX" + }, + { + "replacements": "0x00000440", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x6", + "end": "0x6" + }, + "register": "EBX" + }, + { + "replacements": "0x00000480", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x7", + "end": "0x7" + }, + "register": "EBX" + }, + { + "replacements": "0x00000680", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x6", + "end": "0x6" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x7", + "end": "0x7" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x9", + "end": "0x9" + }, + "register": "EAX" + }, + { + "replacements": "0x00000008", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x9", + "end": "0x9" + }, + "register": "EBX" + }, + { + "replacements": "0x00000a80", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x9", + "end": "0x9" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xb" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xc", + "end": "0xc" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xb" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xc", + "end": "0xc" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xb" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xc", + "end": "0xc" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xb" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xc", + "end": "0xc" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x11", + "end": "0x11" + }, + "register": "EAX" + }, + { + "replacements": "0x00000040", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x12", + "end": "0x12" + }, + "register": "EAX" + }, + { + "replacements": "0x00002000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x13", + "end": "0x3f" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x11", + "end": "0x11" + }, + "register": "EBX" + }, + { + "replacements": "0x00000ac0", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x12", + "end": "0x12" + }, + "register": "EBX" + }, + { + "replacements": "0x00000b00", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x13", + "end": "0x3f" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x11", + "end": "0x11" + }, + "register": "ECX" + }, + { + "replacements": "0x00000002", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x12", + "end": "0x12" + }, + "register": "ECX" + }, + { + "replacements": "0x00000006", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x13", + "end": "0x3f" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x15", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x15", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x15", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x16", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x16", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x16", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x17", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffff070f" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffff070f" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x03ffc1ff" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x03ffc1ff" + } + ], + [ + { + "leaf": "0x1c", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1c", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1c", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000001", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x04002000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00080040", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000010", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1e", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1e", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00004010", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1e", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x20", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x20", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x21", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x21", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x21", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x24", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x24", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x80000008", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000121", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x2c100800", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x65746e49", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x6153206c", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x69687070", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x52206572", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x64697061", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000073", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000006", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000007", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000100", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000008", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00ffffff" + } + ], + [ + { + "leaf": "0x80000008", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0103feff" + } + ], + [ + { + "leaf": "0x40000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000001" + } + ] + ] +} \ No newline at end of file diff --git a/arch/src/x86_64/cpu_profiles/sapphire-rapids.cpuid.json.license b/arch/src/x86_64/cpu_profiles/sapphire-rapids.cpuid.json.license new file mode 100644 index 0000000000..579657c531 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/sapphire-rapids.cpuid.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Cyberus Technology GmbH + +SPDX-License-Identifier: Apache-2.0 diff --git a/arch/src/x86_64/cpu_profiles/sapphire-rapids.msr.json b/arch/src/x86_64/cpu_profiles/sapphire-rapids.msr.json new file mode 100644 index 0000000000..c9b5d42089 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/sapphire-rapids.msr.json @@ -0,0 +1,206 @@ +{ + "cpu_vendor": "Intel", + "hypervisor_type": "Kvm", + "adjustments": [ + [ + "0x8b", + { + "mask": "0xffffffff00000000", + "replacements": "0x0000000000000000" + } + ], + [ + "0x10a", + { + "mask": "0x4000000000000000", + "replacements": "0x000000000c08e06b" + } + ], + [ + "0x480", + { + "mask": "0x0000000000000000", + "replacements": "0x00d8100011e57ed0" + } + ], + [ + "0x485", + { + "mask": "0x000000000000001f", + "replacements": "0x0000000020000060" + } + ], + [ + "0x486", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000080000021" + } + ], + [ + "0x488", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000000002000" + } + ], + [ + "0x48a", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000000000032" + } + ], + [ + "0x48b", + { + "mask": "0x0000000000000000", + "replacements": "0x06137bff00000000" + } + ], + [ + "0x48c", + { + "mask": "0x0000000000000000", + "replacements": "0x00000f01063340c1" + } + ], + [ + "0x48d", + { + "mask": "0x0000000000000000", + "replacements": "0x000000ff00000016" + } + ], + [ + "0x48e", + { + "mask": "0x0000000000000000", + "replacements": "0xfff9fffe04006172" + } + ], + [ + "0x48f", + { + "mask": "0x0000000000000000", + "replacements": "0x007fefff00036dfb" + } + ], + [ + "0x490", + { + "mask": "0x0000000000000000", + "replacements": "0x0000d3ff000011fb" + } + ], + [ + "0x491", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000000000001" + } + ] + ], + "permitted_msrs": [ + "0x10", + "0x11", + "0x12", + "0x3a", + "0x3b", + "0x48", + "0x8b", + "0x10a", + "0x174", + "0x175", + "0x176", + "0x17a", + "0x1a0", + "0x1c4", + "0x1c5", + "0x200", + "0x201", + "0x202", + "0x203", + "0x204", + "0x205", + "0x206", + "0x207", + "0x208", + "0x209", + "0x20a", + "0x20b", + "0x20c", + "0x20d", + "0x20e", + "0x20f", + "0x250", + "0x258", + "0x259", + "0x268", + "0x269", + "0x26a", + "0x26b", + "0x26c", + "0x26d", + "0x26e", + "0x26f", + "0x277", + "0x2ff", + "0x480", + "0x481", + "0x482", + "0x483", + "0x484", + "0x485", + "0x486", + "0x487", + "0x488", + "0x489", + "0x48a", + "0x48b", + "0x48c", + "0x48d", + "0x48e", + "0x48f", + "0x490", + "0x491", + "0x6e0", + "0x40000000", + "0x40000001", + "0x40000002", + "0x40000003", + "0x40000010", + "0x40000020", + "0x40000021", + "0x40000022", + "0x40000023", + "0x40000073", + "0x40000080", + "0x400000b0", + "0x400000f1", + "0x400000f2", + "0x400000f3", + "0x400000f4", + "0x400000f5", + "0x40000100", + "0x40000101", + "0x40000102", + "0x40000103", + "0x40000104", + "0x40000105", + "0x4b564d00", + "0x4b564d01", + "0x4b564d02", + "0x4b564d03", + "0x4b564d04", + "0x4b564d05", + "0x4b564d06", + "0x4b564d07", + "0xc0000081", + "0xc0000082", + "0xc0000083", + "0xc0000084", + "0xc0000102", + "0xc0000103" + ] +} \ No newline at end of file diff --git a/arch/src/x86_64/cpu_profiles/sapphire-rapids.msr.json.license b/arch/src/x86_64/cpu_profiles/sapphire-rapids.msr.json.license new file mode 100644 index 0000000000..579657c531 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/sapphire-rapids.msr.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Cyberus Technology GmbH + +SPDX-License-Identifier: Apache-2.0 diff --git a/arch/src/x86_64/cpu_profiles/skylake.cpuid.json b/arch/src/x86_64/cpu_profiles/skylake.cpuid.json new file mode 100644 index 0000000000..bbe3ec73a8 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/skylake.cpuid.json @@ -0,0 +1,3184 @@ +{ + "hypervisor": "Kvm", + "cpu_vendor": "Intel", + "adjustments": [ + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000016", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x756e6547", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x6c65746e", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x0", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x49656e69", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00050654", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ff00" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x76fa3223", + "mask": "0x89000000" + } + ], + [ + { + "leaf": "0x1", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x078bfbff", + "mask": "0x08000000" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x2", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffc3ff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x7fffffff" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x4", + "sub_leaf": { + "start": "0x5", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000007" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x5", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000004", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x6", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0xd19f07ab", + "mask": "0x00002040" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000004", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0xbc000400", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x7", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x9", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xa", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xb", + "sub_leaf": { + "start": "0x1", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x000002e7", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x0000000f", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000100", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EBX" + }, + { + "replacements": "0x00000240", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EAX" + }, + { + "replacements": "0x00000040", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x6", + "end": "0x6" + }, + "register": "EAX" + }, + { + "replacements": "0x00000200", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x7", + "end": "0x7" + }, + "register": "EAX" + }, + { + "replacements": "0x00000400", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EBX" + }, + { + "replacements": "0x00000440", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x6", + "end": "0x6" + }, + "register": "EBX" + }, + { + "replacements": "0x00000480", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x7", + "end": "0x7" + }, + "register": "EBX" + }, + { + "replacements": "0x00000680", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x6", + "end": "0x6" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x7", + "end": "0x7" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x8", + "end": "0x8" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x9", + "end": "0x9" + }, + "register": "EAX" + }, + { + "replacements": "0x00000008", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x9", + "end": "0x9" + }, + "register": "EBX" + }, + { + "replacements": "0x00000a80", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x9", + "end": "0x9" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xa", + "end": "0xa" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xc" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xc" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xc" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xb", + "end": "0xc" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xd", + "end": "0xd" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xe", + "end": "0xe" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0xf", + "end": "0xf" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x10", + "end": "0x10" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x11", + "end": "0x3f" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x11", + "end": "0x3f" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xd", + "sub_leaf": { + "start": "0x11", + "end": "0x3f" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0xf", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x10", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x14", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x15", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x15", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x15", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x16", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x16", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x16", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x17", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffff070f" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x18", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x03ffc1ff" + } + ], + [ + { + "leaf": "0x1c", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1c", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1c", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1d", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1e", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1e", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1e", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000001f" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x0000ffff" + } + ], + [ + { + "leaf": "0x1f", + "sub_leaf": { + "start": "0x0", + "end": "0xffffffff" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x20", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x20", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x21", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x21", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x21", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x1", + "end": "0x1" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x2", + "end": "0x2" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x3", + "end": "0x3" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x4", + "end": "0x4" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x23", + "sub_leaf": { + "start": "0x5", + "end": "0x5" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x24", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x24", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x80000008", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000121", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x2c100800", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x65746e49", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x6b53206c", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x6b616c79", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000002", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000065", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000003", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000004", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000006", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x80000007", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000100", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x80000008", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x00ffffff" + } + ], + [ + { + "leaf": "0x80000008", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000000" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EBX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "ECX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000000", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0xffffffff" + } + ], + [ + { + "leaf": "0x40000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EAX" + }, + { + "replacements": "0x00000000", + "mask": "0x0103feff" + } + ], + [ + { + "leaf": "0x40000001", + "sub_leaf": { + "start": "0x0", + "end": "0x0" + }, + "register": "EDX" + }, + { + "replacements": "0x00000000", + "mask": "0x00000001" + } + ] + ] +} \ No newline at end of file diff --git a/arch/src/x86_64/cpu_profiles/skylake.cpuid.json.license b/arch/src/x86_64/cpu_profiles/skylake.cpuid.json.license new file mode 100644 index 0000000000..579657c531 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/skylake.cpuid.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Cyberus Technology GmbH + +SPDX-License-Identifier: Apache-2.0 diff --git a/arch/src/x86_64/cpu_profiles/skylake.msr.json b/arch/src/x86_64/cpu_profiles/skylake.msr.json new file mode 100644 index 0000000000..eceb91fcda --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/skylake.msr.json @@ -0,0 +1,204 @@ +{ + "cpu_vendor": "Intel", + "hypervisor_type": "Kvm", + "adjustments": [ + [ + "0x8b", + { + "mask": "0xffffffff00000000", + "replacements": "0x0000000000000000" + } + ], + [ + "0x10a", + { + "mask": "0x4000000000000000", + "replacements": "0x000000000c00004c" + } + ], + [ + "0x480", + { + "mask": "0x0000000000000000", + "replacements": "0x00d8100011e57ed0" + } + ], + [ + "0x485", + { + "mask": "0x000000000000001f", + "replacements": "0x0000000020000060" + } + ], + [ + "0x486", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000080000021" + } + ], + [ + "0x488", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000000002000" + } + ], + [ + "0x48a", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000000000032" + } + ], + [ + "0x48b", + { + "mask": "0x0000000000000000", + "replacements": "0x02137bff00000000" + } + ], + [ + "0x48c", + { + "mask": "0x0000000000000000", + "replacements": "0x00000f0106334041" + } + ], + [ + "0x48d", + { + "mask": "0x0000000000000000", + "replacements": "0x000000ff00000016" + } + ], + [ + "0x48e", + { + "mask": "0x0000000000000000", + "replacements": "0xfff9fffe04006172" + } + ], + [ + "0x48f", + { + "mask": "0x0000000000000000", + "replacements": "0x007fefff00036dfb" + } + ], + [ + "0x490", + { + "mask": "0x0000000000000000", + "replacements": "0x0000d3ff000011fb" + } + ], + [ + "0x491", + { + "mask": "0x0000000000000000", + "replacements": "0x0000000000000001" + } + ] + ], + "permitted_msrs": [ + "0x10", + "0x11", + "0x12", + "0x3a", + "0x3b", + "0x48", + "0x8b", + "0x10a", + "0x174", + "0x175", + "0x176", + "0x17a", + "0x1a0", + "0x200", + "0x201", + "0x202", + "0x203", + "0x204", + "0x205", + "0x206", + "0x207", + "0x208", + "0x209", + "0x20a", + "0x20b", + "0x20c", + "0x20d", + "0x20e", + "0x20f", + "0x250", + "0x258", + "0x259", + "0x268", + "0x269", + "0x26a", + "0x26b", + "0x26c", + "0x26d", + "0x26e", + "0x26f", + "0x277", + "0x2ff", + "0x480", + "0x481", + "0x482", + "0x483", + "0x484", + "0x485", + "0x486", + "0x487", + "0x488", + "0x489", + "0x48a", + "0x48b", + "0x48c", + "0x48d", + "0x48e", + "0x48f", + "0x490", + "0x491", + "0x6e0", + "0x40000000", + "0x40000001", + "0x40000002", + "0x40000003", + "0x40000010", + "0x40000020", + "0x40000021", + "0x40000022", + "0x40000023", + "0x40000073", + "0x40000080", + "0x400000b0", + "0x400000f1", + "0x400000f2", + "0x400000f3", + "0x400000f4", + "0x400000f5", + "0x40000100", + "0x40000101", + "0x40000102", + "0x40000103", + "0x40000104", + "0x40000105", + "0x4b564d00", + "0x4b564d01", + "0x4b564d02", + "0x4b564d03", + "0x4b564d04", + "0x4b564d05", + "0x4b564d06", + "0x4b564d07", + "0xc0000081", + "0xc0000082", + "0xc0000083", + "0xc0000084", + "0xc0000102", + "0xc0000103" + ] +} \ No newline at end of file diff --git a/arch/src/x86_64/cpu_profiles/skylake.msr.json.license b/arch/src/x86_64/cpu_profiles/skylake.msr.json.license new file mode 100644 index 0000000000..579657c531 --- /dev/null +++ b/arch/src/x86_64/cpu_profiles/skylake.msr.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Cyberus Technology GmbH + +SPDX-License-Identifier: Apache-2.0 diff --git a/arch/src/x86_64/cpuid_definitions/intel.rs b/arch/src/x86_64/cpuid_definitions/intel.rs new file mode 100644 index 0000000000..61517e7e1b --- /dev/null +++ b/arch/src/x86_64/cpuid_definitions/intel.rs @@ -0,0 +1,5306 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This module contains CPUID definitions for Intel CPUs. +use std::ops::RangeInclusive; + +use super::{ + CpuidDefinitions, CpuidReg, Parameters, ProfilePolicy, ValueDefinition, ValueDefinitions, +}; + +/// Contains CPUID definitions described in "Intel Architecture Instruction Set Extensions and Future Features" +/// +/// ## Missing leaves +/// +/// The following known CPUID leaves are left out of this table: +/// - 0x3 (Only relevant for Intel Pentium III), +/// - 0x12 (Only relevant for SGX which is deprecated), +/// - 0x19 (Key locker leaf. These features are not in scope for CPU profiles for the time being) +/// - 0x1a (Native Model ID Enumeration leaf), +/// - 0x1b (PCONFIG Information Sub-leaf. This is not in scope for CPU profiles for the time being), +/// - 0x27 (L3 Cache Intel RDT Monitoring Capability Asymmetric Enumeration), +/// - 0x28 (Intel Resource Director Technology Allocation Asymmetric Enumeration), +/// - 0x21 (Only relevant for Intel TDX which is not in scope fore CPU profiles for the time being), +/// - 0x40000000 - 0x4FFFFFFF (Reserved for hypervisors), +/// +/// ### How we produced this table +/// +/// We first ran the [`cpuidgen` tool](https://gitlab.com/x86-cpuid.org/x86-cpuid-db), whose +/// output is licensed under the SPDX Creative Commons Zero 1.0 Universal License. We then wrote a +/// throw-away Rust script to modify the output into something more similar to Rust code. Following +/// this we used macros and other functionality in the [Helix editor](https://helix-editor.com/) to +/// get actual Rust code. +/// +/// We then read through the CPUID section (1.4) of the Intel Architecture Instruction Set +/// Extensions and Future Features manual and manually inserted several leaf definitions that +/// we noticed were missing from the table we had produced. During this process we also changed +/// a few of the short names and descriptions to be more inline with what is written in the +/// aforementioned Intel manual. Finally we decided on a [`ProfilePolicy`] to be set for every +/// single [`ValueDefinition`] and manually appended those. +pub static INTEL_CPUID_DEFINITIONS: CpuidDefinitions<187> = const { + CpuidDefinitions([ + // ========================================================================================= + // Basic CPUID Information + // ========================================================================================= + ( + Parameters { + leaf: 0x0, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "max_std_leaf", + description: "Maximum Input value for Basic CPUID Information", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x0, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_vendorid_0", + description: "CPU vendor ID string bytes 0 - 3", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x0, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_vendorid_2", + description: "CPU vendor ID string bytes 8 - 11", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x0, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_vendorid_1", + description: "CPU vendor ID string bytes 4 - 7", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + // TODO: Do we really want to inherit these values from the corresponding CPU, or should we zero it out or set something else here? + ( + Parameters { + leaf: 0x1, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "stepping", + description: "Stepping ID", + bits_range: (0, 3), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "base_model", + description: "Base CPU model ID", + bits_range: (4, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "base_family_id", + description: "Base CPU family ID", + bits_range: (8, 11), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cpu_type", + description: "CPU type", + bits_range: (12, 13), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "ext_model", + description: "Extended CPU model ID", + bits_range: (16, 19), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "ext_family", + description: "Extended CPU family ID", + bits_range: (20, 27), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x1, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "brand_id", + description: "Brand index", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "clflush_size", + description: "CLFLUSH instruction cache line size", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + // This is set by cloud hypervisor + ValueDefinition { + short: "n_logical_cpu", + description: "Logical CPU count", + bits_range: (16, 23), + policy: ProfilePolicy::Static(0), + }, + // This is set by cloud hypervisor + ValueDefinition { + short: "local_apic_id", + description: "Initial local APIC physical ID", + bits_range: (24, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x1, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "sse3", + description: "Streaming SIMD Extensions 3 (SSE3)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pclmulqdq", + description: "PCLMULQDQ instruction support", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "dtes64", + description: "64-bit DS save area", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "monitor", + description: "MONITOR/MWAIT support", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ds_cpl", + description: "CPL Qualified Debug Store", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + // TODO: Ideally configurable by the user (host must have this otherwise CHV will not run) + ValueDefinition { + short: "vmx", + description: "Virtual Machine Extensions", + bits_range: (5, 5), + policy: ProfilePolicy::Static(1), + }, + ValueDefinition { + short: "smx", + description: "Safer Mode Extensions", + bits_range: (6, 6), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "est", + description: "Enhanced Intel SpeedStep", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "tm2", + description: "Thermal Monitor 2", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ssse3", + description: "Supplemental SSE3", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "cnxt_id", + description: "L1 Context ID", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "sdbg", + description: "Silicon Debug", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "fma", + description: "FMA extensions using YMM state", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cx16", + description: "CMPXCHG16B instruction support", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "xtpr", + description: "xTPR Update Control", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "pdcm", + description: "Perfmon and Debug Capability", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pcid", + description: "Process-context identifiers", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "dca", + description: "Direct Cache Access", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sse4_1", + description: "SSE4.1", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sse4_2", + description: "SSE4.2", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit, + }, + // Set by Cloud hypervisor + ValueDefinition { + short: "x2apic", + description: "X2APIC support", + bits_range: (21, 21), + policy: ProfilePolicy::Static(1), + }, + ValueDefinition { + short: "movbe", + description: "MOVBE instruction support", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "popcnt", + description: "POPCNT instruction support", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit, + }, + // Set by Cloud hypervisor + ValueDefinition { + short: "tsc_deadline_timer", + description: "APIC timer one-shot operation", + bits_range: (24, 24), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "aes", + description: "AES instructions", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xsave", + description: "XSAVE (and related instructions) support", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "osxsave", + description: "XSAVE (and related instructions) are enabled by OS", + bits_range: (27, 27), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "avx", + description: "AVX instructions support", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "f16c", + description: "Half-precision floating-point conversion support", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "rdrand", + description: "RDRAND instruction support", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit, + }, + // TODO: If set by CHV set to 0 and write comment + ValueDefinition { + short: "guest_status", + description: "System is running as guest; (para-)virtualized system", + bits_range: (31, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x1, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "fpu", + description: "Floating-Point Unit on-chip (x87)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "vme", + description: "Virtual-8086 Mode Extensions", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "de", + description: "Debugging Extensions", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pse", + description: "Page Size Extension", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "tsc", + description: "Time Stamp Counter", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "msr", + description: "Model-Specific Registers (RDMSR and WRMSR support)", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pae", + description: "Physical Address Extensions", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "mce", + description: "Machine Check Exception", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cx8", + description: "CMPXCHG8B instruction", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "apic", + description: "APIC on-chip", + bits_range: (9, 9), + policy: ProfilePolicy::Static(1), + }, + // MSR related + ValueDefinition { + short: "sep", + description: "SYSENTER, SYSEXIT, and associated MSRs", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "mtrr", + description: "Memory Type Range Registers", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pge", + description: "Page Global Extensions", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "mca", + description: "Machine Check Architecture", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cmov", + description: "Conditional Move Instruction", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pat", + description: "Page Attribute Table", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pse36", + description: "Page Size Extension (36-bit)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "psn", + description: "Processor Serial Number", + bits_range: (18, 18), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "clfsh", + description: "CLFLUSH instruction", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "ds", + description: "Debug Store", + bits_range: (21, 21), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "acpi", + description: "Thermal monitor and clock control", + bits_range: (22, 22), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "mmx", + description: "MMX instructions", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "fxsr", + description: "FXSAVE and FXRSTOR instructions", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sse", + description: "SSE instructions", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sse2", + description: "SSE2 instructions", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "ss", + description: "Self Snoop", + bits_range: (27, 27), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "htt", + description: "Hyper-threading", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "tm", + description: "Thermal Monitor", + bits_range: (29, 29), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "pbe", + description: "Pending Break Enable", + bits_range: (31, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // ========================================================================================= + // Cache and TLB Information + // ========================================================================================= + ( + Parameters { + leaf: 0x2, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "iteration_count", + description: "Number of times this leaf must be queried", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc1", + description: "Descriptor #1", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc2", + description: "Descriptor #2", + bits_range: (16, 23), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc3", + description: "Descriptor #3", + bits_range: (24, 30), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "eax_invalid", + description: "Descriptors 1-3 are invalid if set", + bits_range: (31, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x2, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "desc4", + description: "Descriptor #4", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc5", + description: "Descriptor #5", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc6", + description: "Descriptor #6", + bits_range: (16, 23), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc7", + description: "Descriptor #7", + bits_range: (24, 30), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "ebx_invalid", + description: "Descriptors 4-7 are invalid if set", + bits_range: (31, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x2, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "desc8", + description: "Descriptor #8", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc9", + description: "Descriptor #9", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc10", + description: "Descriptor #10", + bits_range: (16, 23), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc11", + description: "Descriptor #11", + bits_range: (24, 30), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "ecx_invalid", + description: "Descriptors 8-11 are invalid if set", + bits_range: (31, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x2, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "desc12", + description: "Descriptor #12", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc13", + description: "Descriptor #13", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc14", + description: "Descriptor #14", + bits_range: (16, 23), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "desc15", + description: "Descriptor #15", + bits_range: (24, 30), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "edx_invalid", + description: "Descriptors 12-15 are invalid if set", + bits_range: (31, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + // ========================================================================================= + // Deterministic Cache Parameters + // ========================================================================================= + ( + Parameters { + leaf: 0x4, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "cache_type", + description: "Cache type field", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "cache_level", + description: "Cache level (1-based)", + bits_range: (5, 7), + policy: ProfilePolicy::Passthrough, + }, + // TODO: Could there be a problem migrating from a CPU with self-initializing cache to one without? + ValueDefinition { + short: "cache_self_init", + description: "Self-initializing cache level", + bits_range: (8, 8), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "fully_associative", + description: "Fully-associative cache", + bits_range: (9, 9), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "num_threads_sharing", + description: "Number logical CPUs sharing this cache", + bits_range: (14, 25), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "num_cores_on_die", + description: "Number of cores in the physical package", + bits_range: (26, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x4, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "cache_linesize", + description: "System coherency line size (0-based)", + bits_range: (0, 11), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "cache_npartitions", + description: "Physical line partitions (0-based)", + bits_range: (12, 21), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "cache_nways", + description: "Ways of associativity (0-based)", + bits_range: (22, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x4, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cache_nsets", + description: "Cache number of sets (0-based)", + bits_range: (0, 30), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x4, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "wbinvd_rll_no_guarantee", + description: "WBINVD/INVD not guaranteed for Remote Lower-Level caches", + bits_range: (0, 0), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "ll_inclusive", + description: "Cache is inclusive of Lower-Level caches", + bits_range: (1, 1), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "complex_indexing", + description: "Not a direct-mapped cache (complex function)", + bits_range: (2, 2), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + // ========================================================================================= + // MONITOR/MWAIT + // ========================================================================================= + ( + Parameters { + leaf: 0x5, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "min_mon_size", + description: "Smallest monitor-line size, in bytes", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x5, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "max_mon_size", + description: "Largest monitor-line size, in bytes", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x5, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "mwait_ext", + description: "Enumeration of MONITOR/MWAIT extensions is supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "mwait_irq_break", + description: "Interrupts as a break-event for MWAIT is supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x5, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "n_c0_substates", + description: "Number of C0 sub C-states supported using MWAIT", + bits_range: (0, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c1_substates", + description: "Number of C1 sub C-states supported using MWAIT", + bits_range: (4, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c2_substates", + description: "Number of C2 sub C-states supported using MWAIT", + bits_range: (8, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c3_substates", + description: "Number of C3 sub C-states supported using MWAIT", + bits_range: (12, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c4_substates", + description: "Number of C4 sub C-states supported using MWAIT", + bits_range: (16, 19), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c5_substates", + description: "Number of C5 sub C-states supported using MWAIT", + bits_range: (20, 23), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c6_substates", + description: "Number of C6 sub C-states supported using MWAIT", + bits_range: (24, 27), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "n_c7_substates", + description: "Number of C7 sub C-states supported using MWAIT", + bits_range: (28, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // ========================================================================================= + // Thermal and Power Management + // ========================================================================================= + ( + Parameters { + leaf: 0x6, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "dtherm", + description: "Digital temperature sensor", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "turbo_boost", + description: "Intel Turbo Boost", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "arat", + description: "Always-Running APIC Timer (not affected by p-state)", + bits_range: (2, 2), + // The timer is emulated by KVM and thus always always-running :) + policy: ProfilePolicy::Static(1), + }, + ValueDefinition { + short: "pln", + description: "Power Limit Notification (PLN) event", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ecmd", + description: "Clock modulation duty cycle extension", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pts", + description: "Package thermal management", + bits_range: (6, 6), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp", + description: "HWP (Hardware P-states) base registers are supported", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_notify", + description: "HWP notification (IA32_HWP_INTERRUPT MSR)", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_act_window", + description: "HWP activity window (IA32_HWP_REQUEST[bits 41:32]) supported", + bits_range: (9, 9), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_epp", + description: "HWP Energy Performance Preference", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_pkg_req", + description: "HWP Package Level Request", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hdc_base_regs", + description: "HDC base registers are supported", + bits_range: (13, 13), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "turbo_boost_3_0", + description: "Intel Turbo Boost Max 3.0", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_capabilities", + description: "HWP Highest Performance change", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_peci_override", + description: "HWP PECI override", + bits_range: (16, 16), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_flexible", + description: "Flexible HWP", + bits_range: (17, 17), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_fast", + description: "IA32_HWP_REQUEST MSR fast access mode", + bits_range: (18, 18), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hfi", + description: "HW_FEEDBACK MSRs supported", + bits_range: (19, 19), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "hwp_ignore_idle", + description: "Ignoring idle logical CPU HWP req is supported", + bits_range: (20, 20), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "thread_director", + description: "Intel thread director support", + bits_range: (23, 23), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "therm_interrupt_bit25", + description: "IA32_THERM_INTERRUPT MSR bit 25 is supported", + bits_range: (24, 24), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x6, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "n_therm_thresholds", + description: "Digital thermometer thresholds", + bits_range: (0, 3), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x6, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + // MSR related + ValueDefinition { + short: "aperfmperf", + description: "MPERF/APERF MSRs (effective frequency interface)", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "epb", + description: "IA32_ENERGY_PERF_BIAS MSR support", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "thrd_director_nclasses", + description: "Number of classes, Intel thread director", + bits_range: (8, 15), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x6, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "perfcap_reporting", + description: "Performance capability reporting", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "encap_reporting", + description: "Energy efficiency capability reporting", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "feedback_sz", + description: "Feedback interface structure size, in 4K pages", + bits_range: (8, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "this_lcpu_hwfdbk_idx", + description: "This logical CPU hardware feedback interface index", + bits_range: (16, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Structured Extended Feature Flags Enumeration Main Leaf + // =================================================================================================================== + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "leaf7_n_subleaves", + description: "Number of leaf 0x7 subleaves", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "fsgsbase", + description: "FSBASE/GSBASE read/write support", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "tsc_adjust", + description: "IA32_TSC_ADJUST MSR supported", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + // SGX is deprecated so we disable it unconditionally for all CPU profiles + ValueDefinition { + short: "sgx", + description: "Intel SGX (Software Guard Extensions)", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "bmi1", + description: "Bit manipulation extensions group 1", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit, + }, + // TSX related which is riddled with CVEs. Consider two profiles, or making it opt-in/out. QEMU always has a CPU model with and without TSX. + ValueDefinition { + short: "hle", + description: "Hardware Lock Elision", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx2", + description: "AVX2 instruction set", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + /*The KVM docs recommend always setting this (https://docs.kernel.org/virt/kvm/x86/errata.html#kvm-get-supported-cpuid-issues). + + Keep in mind however that in my limited understanding this isn't about enabling or disabling a feature, but it describes critical behaviour. + Hence I am wondering whether it should be a hard error if the host does not have this bit set, but the desired CPU profile does? + + TODO: Check what KVM_GET_SUPPORTED_CPUID actually gives here (on the Skylake server) + */ + ValueDefinition { + short: "fdp_excptn_only", + description: "FPU Data Pointer updated only on x87 exceptions", + bits_range: (6, 6), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "smep", + description: "Supervisor Mode Execution Protection", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "bmi2", + description: "Bit manipulation extensions group 2", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "erms", + description: "Enhanced REP MOVSB/STOSB", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit, + }, + /* + The instruction enabled by this seems rather powerful. Are we sure that doesn't have security implications? + I included this because it seems like QEMU does (to the best of my understanding). + */ + ValueDefinition { + short: "invpcid", + description: "INVPCID instruction (Invalidate Processor Context ID)", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit, + }, + // This is TSX related. TSX is riddled with CVEs: Consider two profiles (one with it disabled) or an opt-in/out feature. + ValueDefinition { + short: "rtm", + description: "Intel restricted transactional memory", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "rdt_m", + description: "Supports Intel Resource Director Technology Monitoring Capability if 1", + bits_range: (12, 12), + policy: ProfilePolicy::Static(0), + }, + // The KVM docs recommend always setting this (https://docs.kernel.org/virt/kvm/x86/errata.html#kvm-get-supported-cpuid-issues). TODO: Is it OK to just set this to 1? + ValueDefinition { + short: "zero_fcs_fds", + description: "Deprecates FPU CS and FPU DS values if 1", + bits_range: (13, 13), + policy: ProfilePolicy::Passthrough, + }, + // This has been deprecated + ValueDefinition { + short: "mpx", + description: "Intel memory protection extensions", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0), + }, + // This might be useful for certain high performance applications, but it also seems like a rather niche and advanced feature. QEMU does also not automatically enable this from what we can tell. + // TODO: Should we make this OPT-IN? + ValueDefinition { + short: "rdt_a", + description: "Intel RDT-A. Supports Intel Resource Director Technology Allocation Capability if 1", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + // TODO: Do the wider avx512 zmm registers work out of the box when the hardware supports it? + ValueDefinition { + short: "avx512f", + description: "AVX-512 foundation instructions", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512dq", + description: "AVX-512 double/quadword instructions", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "rdseed", + description: "RDSEED instruction", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "adx", + description: "ADCX/ADOX instructions", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "smap", + description: "Supervisor mode access prevention", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512ifma", + description: "AVX-512 integer fused multiply add", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "clflushopt", + description: "CLFLUSHOPT instruction", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "clwb", + description: "CLWB instruction", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "intel_pt", + description: "Intel processor trace", + bits_range: (25, 25), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512pf", + description: "AVX-512 prefetch instructions", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512er", + description: "AVX-512 exponent/reciprocal instructions", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512cd", + description: "AVX-512 conflict detection instructions", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sha_ni", + description: "SHA/SHA256 instructions", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512bw", + description: "AVX-512 byte/word instructions", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512vl", + description: "AVX-512 VL (128/256 vector length) extensions", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "prefetchwt1", + description: "PREFETCHWT1 (Intel Xeon Phi only)", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512vbmi", + description: "AVX-512 Vector byte manipulation instructions", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + // Also set by QEMU for CPU models from what we can tell + ValueDefinition { + short: "umip", + description: "User mode instruction protection", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + // TODO: This is however set by QEMU for CPU models from what we can tell? + ValueDefinition { + short: "pku", + description: "Protection keys for user-space", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + // NOTE: This field is mutable in principle and can be changed by the OS (TODO: Under which circumstances?) + ValueDefinition { + short: "ospke", + description: "OS protection keys enable", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + // TODO: Revisit this decision. Setting this to 0 for now in order to be compatible with QEMU + ValueDefinition { + short: "waitpkg", + description: "WAITPKG instructions", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512_vbmi2", + description: "AVX-512 vector byte manipulation instructions group 2", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cet_ss", + description: "CET shadow stack features", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "gfni", + description: "Galois field new instructions", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "vaes", + description: "Vector AES instructions", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "vpclmulqdq", + description: "VPCLMULQDQ 256-bit instruction support", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512_vnni", + description: "Vector neural network instructions", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512_bitalg", + description: "AVX-512 bitwise algorithms", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit, + }, + // Seems to be TDX related which is experimental in CHV. We disable this for CPU profiles for now, but could potentially add it as an opt-in feature eventually. + ValueDefinition { + short: "tme", + description: "Intel total memory encryption", + bits_range: (13, 13), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512_vpopcntdq", + description: "AVX-512: POPCNT for vectors of DWORD/QWORD", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "la57", + description: "57-bit linear addresses (five-level paging)", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "mawau_val_lm", + description: "BNDLDX/BNDSTX MAWAU value in 64-bit mode", + bits_range: (17, 21), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "rdpid", + description: "RDPID instruction", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit, + }, + // We leave key locker support out for CPU profiles for the time being. We may want this to be opt-in in the future though + ValueDefinition { + short: "key_locker", + description: "Intel key locker support", + bits_range: (23, 23), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "bus_lock_detect", + description: "OS bus-lock detection", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cldemote", + description: "CLDEMOTE instruction", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "movdiri", + description: "MOVDIRI instruction", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "movdir64b", + description: "MOVDIR64B instruction", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "enqcmd", + description: "Enqueue stores supported (ENQCMD{,S})", + bits_range: (29, 29), + policy: ProfilePolicy::Static(0), + }, + // SGX support is deprecated so we disable it unconditionally for CPU profiles + ValueDefinition { + short: "sgx_lc", + description: "Intel SGX launch configuration", + bits_range: (30, 30), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pks", + description: "Protection keys for supervisor-mode pages", + bits_range: (31, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + // SGX is deprecated + ValueDefinition { + short: "sgx_keys", + description: "Intel SGX attestation services", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512_4vnniw", + description: "AVX-512 neural network instructions (Intel Xeon Phi only)", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512_4fmaps", + description: "AVX-512 multiply accumulation single precision (Intel Xeon Phi only)", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "fsrm", + description: "Fast short REP MOV", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "uintr", + description: "CPU supports user interrupts", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx512_vp2intersect", + description: "VP2INTERSECT{D,Q} instructions", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "srdbs_ctrl", + description: "SRBDS mitigation MSR available: If 1, enumerates support for the IA32_MCU_OPT_CTRL MSR and indicates that its bit 0 (RNGDS_MITG_DIS) is also supported.", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "md_clear", + description: "VERW MD_CLEAR microcode support", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "rtm_always_abort", + description: "XBEGIN (RTM transaction) always aborts", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "tsx_force_abort", + description: "MSR TSX_FORCE_ABORT, RTM_ABORT bit, supported", + bits_range: (13, 13), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "serialize", + description: "SERIALIZE instruction", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "hybrid_cpu", + description: "The CPU is identified as a 'hybrid part'", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit, + }, + // TODO: This is TSX related which is riddled with CVEs. We could consider an additional profile enabling TSX in the future, but we leave it out for now. + ValueDefinition { + short: "tsxldtrk", + description: "TSX suspend/resume load address tracking", + bits_range: (16, 16), + policy: ProfilePolicy::Static(0), + }, + // Might be relevant for confidential computing + ValueDefinition { + short: "pconfig", + description: "PCONFIG instruction", + bits_range: (18, 18), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "arch_lbr", + description: "Intel architectural LBRs", + bits_range: (19, 19), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ibt", + description: "CET indirect branch tracking", + bits_range: (20, 20), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "amx_bf16", + description: "AMX-BF16: tile bfloat16 support", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512_fp16", + description: "AVX-512 FP16 instructions", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_tile", + description: "AMX-TILE: tile architecture support", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_int8", + description: "AMX-INT8: tile 8-bit integer support", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "spec_ctrl", + description: "Speculation Control (IBRS/IBPB: indirect branch restrictions)", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "intel_stibp", + description: "Single thread indirect branch predictors", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit, + }, + // MSR related + // + // TODO: Is passthrough correct? + // If this bit is set then MSR IA32_FLUSH_CMD + // becomes available, otherwise it is not. + ValueDefinition { + short: "flush_l1d", + description: "FLUSH L1D cache: IA32_FLUSH_CMD MSR", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "arch_capabilities", + description: "Intel IA32_ARCH_CAPABILITIES MSR", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "core_capabilities", + description: "IA32_CORE_CAPABILITIES MSR", + bits_range: (30, 30), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "spec_ctrl_ssbd", + description: "Speculative store bypass disable", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // =================================================================================================================== + // Structured Extended Feature Flags Enumeration Sub-Leaf 1 + // =================================================================================================================== + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "sha512", + description: "SHA-512 extensions", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sm3", + description: "SM3 instructions", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "sm4", + description: "SM4 instructions", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + // RAO-INT is deprecated and removed from many compilers as far as we are aware. + // This policy can be changed if requested in the future. + ValueDefinition { + short: "RAO-INT", + description: "RAO-INT instructions", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx_vnni", + description: "AVX-VNNI instructions", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx512_bf16", + description: "AVX-512 bfloat16 instructions", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + /* + Not set in QEMU from what we can tell, but according seems to be fine to expose this to guests + if we understood https://www.phoronix.com/news/Intel-Linux-LASS-KVM correctly. It is also + our understanding that this feature can enable guests opting in to more security (possibly at the cost of some performance). + */ + ValueDefinition { + short: "lass", + description: "Linear address space separation", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "cmpccxadd", + description: "CMPccXADD instructions", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "arch_perfmon_ext", + description: "ArchPerfmonExt: leaf 0x23 is supported", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "fzrm", + description: "Fast zero-length REP MOVSB", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "fsrs", + description: "Fast short REP STOSB", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "fsrc", + description: "Fast Short REP CMPSB/SCASB", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "fred", + description: "FRED: Flexible return and event delivery transitions", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "lkgs", + description: "LKGS: Load 'kernel' (userspace) GS", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "wrmsrns", + description: "WRMSRNS instruction (WRMSR-non-serializing)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "nmi_src", + description: "NMI-source reporting with FRED event data", + bits_range: (20, 20), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "amx_fp16", + description: "AMX-FP16: FP16 tile operations", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "hreset", + description: "History reset support", + bits_range: (22, 22), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx_ifma", + description: "Integer fused multiply add", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "lam", + description: "Linear address masking", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "rd_wr_msrlist", + description: "RDMSRLIST/WRMSRLIST instructions", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "invd_disable_post_bios_done", + description: "If 1, supports INVD execution prevention after BIOS Done", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "movrs", + description: "MOVRS", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "intel_ppin", + description: "Protected processor inventory number (PPIN{,_CTL} MSRs)", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "pbndkb", + description: "PBNDKB instruction supported and enumerates the existence of the IA32_TSE_CAPABILITY MSR", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "asymmetric-rdt-M", + description: "At least one logical processor supports Asymmetrical Intel RDT Monitoring Capability", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "asymmetric-rdt-A", + description: "At least one logical processor supports Asymmetrical Intel RDT Allocation Capability", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "MSR_IMM", + description: "Immediate forms of the RDMSR and WRMSRNS instructions are supported", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "avx_vnni_int8", + description: "AVX-VNNI-INT8 instructions", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx_ne_convert", + description: "AVX-NE-CONVERT instructions", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + // NOTE: AMX currently requires opt-in, even for the host CPU profile. We still inherit this value for profiles as the value will be zeroed out if the user has not opted in for "amx" via CpuFeatures. + ValueDefinition { + short: "amx_complex", + description: "AMX-COMPLEX instructions (starting from Granite Rapids)", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx_vnni_int16", + description: "AVX-VNNI-INT16 instructions", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "utmr", + description: "If 1, supports user-timer events", + bits_range: (13, 13), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "prefetchit_0_1", + description: "PREFETCHIT0/1 instructions", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "user_msr", + description: "If 1, supports the URDMSR and UWRMSR instructions", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "uiret_uif", + description: "If 1, UIRET sets UIF to the value of bit 1 of the RFLAGS image loaded from the stack", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cet_sss", + description: "CET supervisor shadow stacks safe to use", + bits_range: (18, 18), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "avx10", + description: "If 1, supports the Intel AVX10 instructions and indicates the presence of leaf 0x24", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "apx_f", + description: "If 1, the processor provides foundational support for Intel Advanced Performance Extensions", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "mwait", + description: "If 1, MWAIT is supported even if (0x1 ECX bit 3 (monitor) is enumerated as 0)", + bits_range: (23, 23), + policy: ProfilePolicy::Static(0), + }, + // MSR related + ValueDefinition { + short: "slsm", + description: "If 1, indicates bit 0 of the IA32_INTEGRITY_STATUS MSR is supported. Bit 0 of this MSR indicates whether static lockstep is active on this logical processor", + bits_range: (24, 24), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Structured Extended Feature Flags Enumeration Sub-Leaf 2 + // =================================================================================================================== + ( + Parameters { + leaf: 0x7, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + // MSR related + ValueDefinition { + short: "intel_psfd", + description: "If 1, indicates bit 7 of the IA32_SPEC_CTRL_MSR is supported. Bit 7 of this MSR disables fast store forwarding predictor without disabling speculative store bypass", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "ipred_ctrl", + description: "MSR bits IA32_SPEC_CTRL.IPRED_DIS_{U,S}", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "rrsba_ctrl", + description: "MSR bits IA32_SPEC_CTRL.RRSBA_DIS_{U,S}", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "ddp_ctrl", + description: "MSR bit IA32_SPEC_CTRL.DDPD_U", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "bhi_ctrl", + description: "MSR bit IA32_SPEC_CTRL.BHI_DIS_S", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "mcdt_no", + description: "MCDT mitigation not needed", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "uclock_disable", + description: "UC-lock disable is supported", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // =================================================================================================================== + // Direct Cache Access Information + // =================================================================================================================== + ( + Parameters { + leaf: 0x9, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + // MSR related + ValueDefinition { + short: "dca_cap_msr_value", + description: "Value of bits [31:0] of IA32_PLATFORM_DCA_CAP MSR (address 1f8H)", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring + // =================================================================================================================== + // We will just zero out everything to do with PMU for CPU profiles + ( + Parameters { + leaf: 0xa, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "pmu_version", + description: "Performance monitoring unit version ID", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pmu_n_gcounters", + description: "Number of general PMU counters per logical CPU", + bits_range: (8, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pmu_gcounters_nbits", + description: "Bitwidth of PMU general counters", + bits_range: (16, 23), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pmu_cpuid_ebx_bits", + description: "Length of leaf 0xa EBX bit vector", + bits_range: (24, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0xa, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "no_core_cycle_evt", + description: "Core cycle event not available", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_insn_retired_evt", + description: "Instruction retired event not available", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_refcycle_evt", + description: "Reference cycles event not available", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_llc_ref_evt", + description: "LLC-reference event not available", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_llc_miss_evt", + description: "LLC-misses event not available", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_br_insn_ret_evt", + description: "Branch instruction retired event not available", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_br_mispredict_evt", + description: "Branch mispredict retired event not available", + bits_range: (6, 6), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "no_td_slots_evt", + description: "Topdown slots event not available", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0xa, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pmu_fcounters_bitmap", + description: "Fixed-function PMU counters support bitmap", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xa, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "pmu_n_fcounters", + description: "Number of fixed PMU counters", + bits_range: (0, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "pmu_fcounters_nbits", + description: "Bitwidth of PMU fixed counters", + bits_range: (5, 12), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "anythread_depr", + description: "AnyThread deprecation", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Extended Topology Enumeration + // =================================================================================================================== + + // Leaf 0xB must be set by CHV itself (and do all necessary checks) + ( + Parameters { + leaf: 0xb, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "x2apic_id_shift", + description: "Bit width of this level (previous levels inclusive)", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough, + }]), + ), + // Set by VMM/user provided config + ( + Parameters { + leaf: 0xb, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "domain_lcpus_count", + description: "Logical CPUs count across all instances of this domain", + bits_range: (0, 15), + policy: ProfilePolicy::Passthrough, + }]), + ), + // Set by VMM/user provided config + ( + Parameters { + leaf: 0xb, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "domain_nr", + description: "This domain level (subleaf ID)", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "domain_type", + description: "This domain type", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + // Set by VMM/user provided config + ( + Parameters { + leaf: 0xb, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "x2apic_id", + description: "x2APIC ID of current logical CPU", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + // =================================================================================================================== + // Processor Extended State Enumeration Main Leaf + // =================================================================================================================== + // TODO: Implement CPUID compatibility checks in CHV for this leaf + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "xcr0_x87", + description: "XCR0.X87 (bit 0) supported", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xcr0_sse", + description: "XCR0.SEE (bit 1) supported", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xcr0_avx", + description: "XCR0.AVX (bit 2) supported", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + // MPX is deprecated + ValueDefinition { + short: "xcr0_mpx_bndregs", + description: "XCR0.BNDREGS (bit 3) supported (MPX BND0-BND3 registers)", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + // MPX is deprecated + ValueDefinition { + short: "xcr0_mpx_bndcsr", + description: "XCR0.BNDCSR (bit 4) supported (MPX BNDCFGU/BNDSTATUS registers)", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_avx512_opmask", + description: "XCR0.OPMASK (bit 5) supported (AVX-512 k0-k7 registers)", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xcr0_avx512_zmm_hi256", + description: "XCR0.ZMM_Hi256 (bit 6) supported (AVX-512 ZMM0->ZMM7/15 registers)", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xcr0_avx512_hi16_zmm", + description: "XCR0.HI16_ZMM (bit 7) supported (AVX-512 ZMM16->ZMM31 registers)", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "xcr0_ia32_xss", + description: "XCR0.IA32_XSS (bit 8) used for PT in IA32_XSS", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_pkru", + description: "XCR0.PKRU (bit 9) supported (XSAVE PKRU registers)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xcr0_ia32_xss_pasid", + description: "XCR0.IA32_XSS (bit 10) used for PASID in IA32_XSS", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_ia32_xss_cet", + description: "XCR0.IA32_XSS (bits 11 - 12) used for IA32_XSS", + bits_range: (11, 12), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_ia32_xss_hdc", + description: "XCR0.IA32_XSS (bit 13) used for IA32_XSS", + bits_range: (13, 13), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_ia32_xss_UINTR", + description: "XCR0.IA32_XSS (bit 14) used for UINTR in IA32_XSS", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_ia32_xss_LBR", + description: "XCR0.IA32_XSS (bit 15) used for LBR in IA32_XSS", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_ia32_xss_bits_hwp", + description: "XCR0.IA32_XSS (bit 16) used for HWP in IA32_XSS", + bits_range: (16, 16), + policy: ProfilePolicy::Static(0), + }, + // NOTE: AMX currently requires opt-in, even for the host CPU profile. We still inherit this value for profiles and modify this value at runtime if AMX is not enabled by the user. + ValueDefinition { + short: "xcr0_tileconfig", + description: "XCR0.TILECONFIG (bit 17) supported (AMX can manage TILECONFIG)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit, + }, + // NOTE: AMX currently requires opt-in, even for the host CPU profile. We still inherit this value for profiles and modify this value at runtime if AMX is not ebabled by the user. + ValueDefinition { + short: "xcr0_tiledata", + description: "XCR0.TILEDATA (bit 18) supported (AMX can manage TILEDATA)", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + // This value can be changed by the OS and must thus be passthrough + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_sz_xcr0_enabled", + description: "XSAVE/XRSTOR area byte size, for XCR0 enabled features", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + // This may be passthrough because we restrict each individual state component + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_sz_max", + description: "XSAVE/XRSTOR area max byte size, all CPU features", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + // TODO: Do we know of any state components corresponding to the upper bits in XCR0? Perhaps it would be + // better to have `ProfilePolicy::Static(0)` here? + ValueDefinitions::new(&[ValueDefinition { + short: "xcr0_upper_bits", + description: "Reports the valid bit fields of the upper 32 bits of the XCR0 register", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + // =================================================================================================================== + // Processor Extended State Enumeration Sub-leaf 1 + // =================================================================================================================== + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "xsaveopt", + description: "XSAVEOPT instruction", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xsavec", + description: "XSAVEC instruction", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xgetbv1", + description: "XGETBV instruction with ECX = 1", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + // TODO: Can this have security implications in terms of supervisor state getting exposed? + ValueDefinition { + short: "xsaves", + description: "XSAVES/XRSTORS instructions (and XSS MSR)", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xfd", + description: "Extended feature disable support", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + /*NOTE: This will depend on which CPU features (in CHV) are enabled and pre-computation can potentially lead to a combinatorial explosion. Luckily we can deal with each component (and its size) separately, hence we can just passthrough whatever we get from the host here.*/ + ValueDefinition { + short: "xsave_sz_xcr0_xmms_enabled", + description: "XSAVE area size, all XCR0 and IA32_XSS features enabled", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::ECX, + }, + /* Reports the supported bits of the lower IA32_XSS MSR. IA32_XSS[n] can be set to 1 only if ECX[n] = 1*/ + ValueDefinitions::new(&[ + ValueDefinition { + short: "xcr0_7bits", + description: "Used for XCR0", + bits_range: (0, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xss_pt", + description: "PT state, supported", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_bit9", + description: "Used for XCR0", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xss_pasid", + description: "PASID state, supported", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xss_cet_u", + description: "CET user state, supported", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xss_cet_p", + description: "CET supervisor state, supported", + bits_range: (12, 12), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xss_hdc", + description: "HDC state, supported", + bits_range: (13, 13), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xss_uintr", + description: "UINTR state, supported", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xss_lbr", + description: "LBR state, supported", + bits_range: (15, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xss_hwp", + description: "HWP state, supported", + bits_range: (16, 16), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xcr0_bits", + description: "Used for XCR0", + bits_range: (17, 18), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EDX, + }, + /* Reports the supported bits of the upper 32 bits of the IA32_XSS MSR. IA32_XSS[n + 32 ] can be set to 1 only if EDX[n] = 1*/ + ValueDefinitions::new(&[ValueDefinition { + short: "ia32_xss_upper", + description: " Reports the supported bits of the upper 32 bits of the IA32_XSS MSR. IA32_XSS[n + 32 ] can be set to 1 only if EDX[n] = 1", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + // =================================================================================================================== + // Processor Extended State Enumeration Sub-leaves + // =================================================================================================================== + + /* LEAF 0xd sub-leaf n >=2 : + If ECX contains an invalid sub-leaf index, EAX/EBX/ECX/EDX return 0. Sub-leaf n (0 ≤ n ≤ 31) is + invalid if sub-leaf 0 returns 0 in EAX[n] and sub-leaf 1 returns 0 in ECX[n]. Sub-leaf n (32 ≤ n ≤ 63) + is invalid if sub-leaf 0 returns 0 in EDX[n-32] and sub-leaf 1 returns 0 in EDX[n-32]. + */ + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_sz", + description: "Size of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_offset", + description: "Offset of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "is_xss_bit", + description: "Subleaf N describes an XSS bit, otherwise XCR0 bit", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "compacted_xsave_64byte_aligned", + description: "When compacted, subleaf-N feature XSAVE area is 64-byte aligned", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xfd_faulting", + description: "Indicates support for xfd faulting", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // Intel MPX is deprecated hence we zero out these sub-leaves + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(3, 4), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-3-4-eax-mpx-zero", + description: "This leaf has been zeroed out because MPX state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(3, 4), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-3-4-ebx-mpx-zero", + description: "This leaf has been zeroed out because MPX state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(3, 4), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-3-4-ecx-mpx-zero", + description: "This leaf has been zeroed out because MPX state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(3, 4), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-3-4-edx-mpx-zero", + description: "This leaf has been zeroed out because MPX state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(5, 7), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_sz", + description: "Size of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(5, 7), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_offset", + description: "Offset of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(5, 7), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "is_xss_bit", + description: "Subleaf N describes an XSS bit, otherwise XCR0 bit", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "compacted_xsave_64byte_aligned", + description: "When compacted, subleaf-N feature XSAVE area is 64-byte aligned", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xfd_faulting", + description: "Indicates support for xfd faulting", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // Disable PT for CPU profiles + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(8, 8), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-8-eax-pt-zero", + description: "This leaf has been zeroed out because PT state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(8, 8), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-8-ebx-pt-zero", + description: "This leaf has been zeroed out because PT state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(8, 8), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-8-ecx-pt-zero", + description: "This leaf has been zeroed out because PT state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(8, 8), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-8-edx-pt-zero", + description: "This leaf has been zeroed out because PT state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(9, 9), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_sz", + description: "Size of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(9, 9), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_offset", + description: "Offset of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(9, 9), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "is_xss_bit", + description: "Subleaf N describes an XSS bit, otherwise XCR0 bit", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "compacted_xsave_64byte_aligned", + description: "When compacted, subleaf-N feature XSAVE area is 64-byte aligned", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xfd_faulting", + description: "Indicates support for xfd faulting", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // Disable PASID for CPU profiles + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(10, 10), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-10-eax-pasid-zero", + description: "This leaf has been zeroed out because PASID state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(10, 10), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-10-ebx-pasid-zero", + description: "This leaf has been zeroed out because PASID state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(10, 10), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-10-ecx-pasid-zero", + description: "This leaf has been zeroed out because PASID state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(10, 10), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-10-edx-pasid-zero", + description: "This leaf has been zeroed out because PASID state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // We leave CET out of CPU profiles for the time being + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(11, 12), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-11-12-eax-cet-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(11, 12), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-11-12-ebx-cet-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(11, 12), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-11-12-ecx-cet-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(11, 12), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-11-12-edx-cet-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // Disable HDC for CPU profiles + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(13, 13), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-13-eax-edc-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(13, 13), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-13-ebx-hdc-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(13, 13), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-13-ecx-hdc-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(13, 13), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-13-edx-hdc-zero", + description: "This leaf has been zeroed out because CET state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // We decided to disable UINTR for CPU profiles, hence we zero out these sub-leaves + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(14, 14), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-eax-uintr-zero", + description: "This leaf has been zeroed out because UINTR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(14, 14), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-ebx-uintr-zero", + description: "This leaf has been zeroed out because UINTR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(14, 14), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-ecx-uintr-zero", + description: "This leaf has been zeroed out because UINTR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(14, 14), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-edx-uintr-zero", + description: "This leaf has been zeroed out because UINTR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // Disable LBR for CPU Profiles + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(15, 15), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-eax-lbr-zero", + description: "This leaf has been zeroed out because LBR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(15, 15), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-ebx-lbr-zero", + description: "This leaf has been zeroed out because LBR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(15, 15), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-ecx-lbr-zero", + description: "This leaf has been zeroed out because LBR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(15, 15), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-edx-lbr-zero", + description: "This leaf has been zeroed out because LBR state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // Disable HWP for CPU profiles + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(16, 16), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-eax-hwp-zero", + description: "This leaf has been zeroed out because HWP state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(16, 16), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-ebx-hwp-zero", + description: "This leaf has been zeroed out because HWP state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(16, 16), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-ecx-hwp-zero", + description: "This leaf has been zeroed out because HWP state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(16, 16), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "0xd-edx-hwp-zero", + description: "This leaf has been zeroed out because HWP state components are disabled", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // NOTE: Sub-leaves 17 & 18 are AMX related and we will alter the adjustments corresponding to + // the policy declared here at runtime for those values. + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(17, 63), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_sz", + description: "Size of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(17, 63), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "xsave_offset", + description: "Offset of save area for subleaf-N feature, in bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0xd, + sub_leaf: RangeInclusive::new(17, 63), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "is_xss_bit", + description: "Subleaf N describes an XSS bit, otherwise XCR0 bit", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "compacted_xsave_64byte_aligned", + description: "When compacted, subleaf-N feature XSAVE area is 64-byte aligned", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "xfd_faulting", + description: "Indicates support for xfd faulting", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Monitoring Enumeration + // =================================================================================================================== + ( + Parameters { + leaf: 0xf, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "core_rmid_max", + description: "RMID max, within this core, all types (0-based)", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xf, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "l3-cache-rdt-monitoring", + description: "Supports L3 Cache Intel RDT Monitoring if 1", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Monitoring Enumeration Sub-leaf 1 + // =================================================================================================================== + ( + Parameters { + leaf: 0xf, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "l3c_qm_bitwidth", + description: "L3 QoS-monitoring counter bitwidth (24-based)", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "l3c_qm_overflow_bit", + description: "QM_CTR MSR bit 61 is an overflow bit", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "l3c_qm_non_cpu_agent", + description: "If 1, indicates the presence of non-CPU agent Intel RDT CTM support", + bits_range: (9, 9), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "l3c_qm_non_cpu_agent", + description: "If 1, indicates the presence of non-CPU agent Intel RDT MBM support", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0xf, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "l3c_qm_conver_factor", + description: "QM_CTR MSR conversion factor to bytes", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xf, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "l3c_qm_rmid_max", + description: "L3 QoS-monitoring max RMID", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0xf, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "cqm_occup_llc", + description: "L3 QoS occupancy monitoring supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cqm_mbm_total", + description: "L3 QoS total bandwidth monitoring supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cqm_mbm_local", + description: "L3 QoS local bandwidth monitoring supported", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Allocation Enumeration + // =================================================================================================================== + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + //TODO: These features may be good for increased performance. Perhaps there needs to be some mechanism to opt-in for non-host CPU profiles? + ValueDefinitions::new(&[ + ValueDefinition { + short: "cat_l3", + description: "L3 Cache Allocation Technology supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cat_l2", + description: "L2 Cache Allocation Technology supported", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "mba", + description: "Memory Bandwidth Allocation supported", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Allocation Enumeration Sub-leaf (ECX = ResID = 1) + // =================================================================================================================== + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cat_cbm_len", + description: "L3_CAT capacity bitmask length, minus-one notation", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cat_units_bitmap", + description: "L3_CAT bitmap of allocation units", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::ECX, + }, + //TODO: These feature may be good for increased performance. Perhaps there needs to be some mechanism to opt-in for non-host CPU profiles? + ValueDefinitions::new(&[ + ValueDefinition { + short: "l3_cat_non_cpu_agents", + description: "L3_CAT for non-CPU agent is supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cdp_l3", + description: "L3/L2_CAT CDP (Code and Data Prioritization)", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cat_sparse_1s", + description: "L3/L2_CAT non-contiguous 1s value supported", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EDX, + }, + // TODO: We might need some way to opt in to use Intel cache allocation technology in guests with non-host CPU profiles. + ValueDefinitions::new(&[ValueDefinition { + short: "cat_cos_max", + description: "Highest COS number supported for this ResID", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Allocation Enumeration Sub-leaf (ECX = ResID = 2) + // =================================================================================================================== + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cat_cbm_len", + description: "L2_CAT capacity bitmask length, minus-one notation", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cat_units_bitmap", + description: "L2_CAT bitmap of allocation units", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cat_cos_max", + description: "Highest COS number supported for this ResID", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::ECX, + }, + // TODO: We might need some way to opt in to use Intel cache allocation technology in guests with non-host CPU profiles. + ValueDefinitions::new(&[ + ValueDefinition { + short: "cdp_l2", + description: "L2_CAT CDP (Code and Data Prioritization)", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cat_sparse_1s", + description: "L2_CAT non-contiguous 1s value supported", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Allocation Enumeration Sub-leaf (ECX = ResID = 3) + // =================================================================================================================== + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(3, 3), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + // TODO: We might need some way to opt in to use Intel MBA technology in guests with non-host CPU profiles. + ValueDefinition { + short: "mba_max_delay", + description: "Max MBA throttling value; minus-one notation", + bits_range: (0, 11), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(3, 3), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "per_thread_mba", + description: "Per-thread MBA controls are supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "mba_delay_linear", + description: "Delay values are linear", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(3, 3), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "mba_cos_max", + description: "MBA max Class of Service supported", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Intel Resource Director Technology Allocation Enumeration Sub-leaf (ECX = ResID = 5) + // =================================================================================================================== + // + // TODO: We may want to have some way to opt-in to use Intel RDT for guests with non-host CPU profiles. + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "core_max_throttle", + description: "Max Core throttling level supported by the corresponding ResID", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "core_scope", + description: "If 1, indicates the logical processor scope of the IA32_QoS_Core_BW_Thrtl_n MSRs. Other values are reserved", + bits_range: (8, 11), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cba_delay_linear", + description: "The response of the bandwidth control is approximately linear", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x10, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "core_cos_max", + description: "Core max Class of Service supported", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }]), + ), + // SGX is already disabled and deprecated so we don't need to worry about leaf 0x12 and its subleaves + + // =================================================================================================================== + // Intel Processor Trace Enumeration Main Leaf + // =================================================================================================================== + ( + Parameters { + leaf: 0x14, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pt_max_subleaf", + description: "Maximum leaf 0x14 subleaf", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x14, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "cr3_filtering", + description: "IA32_RTIT_CR3_MATCH is accessible", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "psb_cyc", + description: "Configurable PSB and cycle-accurate mode", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ip_filtering", + description: "IP/TraceStop filtering; Warm-reset PT MSRs preservation", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "mtc_timing", + description: "MTC timing packet; COFI-based packets suppression", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ptwrite", + description: "PTWRITE support", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "power_event_trace", + description: "Power Event Trace support", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "psb_pmi_preserve", + description: "PSB and PMI preservation support", + bits_range: (6, 6), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "event_trace", + description: "Event Trace packet generation through IA32_RTIT_CTL.EventEn", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "tnt_disable", + description: "TNT packet generation disable through IA32_RTIT_CTL.DisTNT", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x14, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "topa_output", + description: "ToPA output scheme support", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "topa_multiple_entries", + description: "ToPA tables can hold multiple entries", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "single_range_output", + description: "Single-range output scheme supported", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "trance_transport_output", + description: "Trace Transport subsystem output support", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ip_payloads_lip", + description: "IP payloads have LIP values (CS base included)", + bits_range: (31, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Intel Processor Trace Enumeration Sub-leaf 1 + // =================================================================================================================== + ( + Parameters { + leaf: 0x14, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "num_address_ranges", + description: "Filtering number of configurable Address Ranges", + bits_range: (0, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "mtc_periods_bmp", + description: "Bitmap of supported MTC period encodings", + bits_range: (16, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x14, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "cycle_thresholds_bmp", + description: "Bitmap of supported Cycle Threshold encodings", + bits_range: (0, 15), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "psb_periods_bmp", + description: "Bitmap of supported Configurable PSB frequency encodings", + bits_range: (16, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Time Stamp Counter and Core Crystal Clock Information + // =================================================================================================================== + ( + Parameters { + leaf: 0x15, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tsc_denominator", + description: "Denominator of the TSC/'core crystal clock' ratio", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x15, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tsc_numerator", + description: "Numerator of the TSC/'core crystal clock' ratio", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x15, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_crystal_hz", + description: "Core crystal clock nominal frequency, in Hz", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + // =================================================================================================================== + // Processor Frequency Information + // =================================================================================================================== + ( + Parameters { + leaf: 0x16, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_base_mhz", + description: "Processor base frequency, in MHz", + bits_range: (0, 15), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x16, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_max_mhz", + description: "Processor max frequency, in MHz", + bits_range: (0, 15), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x16, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "bus_mhz", + description: "Bus reference frequency, in MHz", + bits_range: (0, 15), + policy: ProfilePolicy::Passthrough, + }]), + ), + // =================================================================================================================== + // System-On-Chip Vendor Attribute Enumeration Main Leaf + // =================================================================================================================== + + // System-On-Chip should probably not be supported for CPU profiles for the foreseeable feature. + ( + Parameters { + leaf: 0x17, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "soc_max_subleaf", + description: "Maximum leaf 0x17 subleaf", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Deterministic Address Translation Parameters + // =================================================================================================================== + ( + Parameters { + leaf: 0x18, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tlb_max_subleaf", + description: "Maximum leaf 0x18 subleaf", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x18, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "tlb_4k_page", + description: "TLB 4KB-page entries supported", + bits_range: (0, 0), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "tlb_2m_page", + description: "TLB 2MB-page entries supported", + bits_range: (1, 1), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "tlb_4m_page", + description: "TLB 4MB-page entries supported", + bits_range: (2, 2), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "tlb_1g_page", + description: "TLB 1GB-page entries supported", + bits_range: (3, 3), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "hard_partitioning", + description: "(Hard/Soft) partitioning between logical CPUs sharing this structure", + bits_range: (8, 10), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "n_way_associative", + description: "Ways of associativity", + bits_range: (16, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x18, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "n_sets", + description: "Number of sets", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x18, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "tlb_type", + description: "Translation cache type (TLB type)", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "tlb_cache_level", + description: "Translation cache level (1-based)", + bits_range: (5, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "is_fully_associative", + description: "Fully-associative structure", + bits_range: (8, 8), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "tlb_max_addressable_ids", + description: "Max number of addressable IDs for logical CPUs sharing this TLB - 1", + bits_range: (14, 25), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + // We don't support key locker for now (leaf 0x19): Hence we zero out leaf 0x19 for CPU profiles We zero LEAF + // 0x1A (Native Model ID Enumeration) out for CPU profiles LEAF 0x1B (PCONFIG) is zeroed out for CPU profiles + // for now + + // =================================================================================================================== + // Last Branch Records Information + // =================================================================================================================== + ( + Parameters { + leaf: 0x1c, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "lbr_depth_8", + description: "Max stack depth (number of LBR entries) = 8", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_16", + description: "Max stack depth (number of LBR entries) = 16", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_24", + description: "Max stack depth (number of LBR entries) = 24", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_32", + description: "Max stack depth (number of LBR entries) = 32", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_40", + description: "Max stack depth (number of LBR entries) = 40", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_48", + description: "Max stack depth (number of LBR entries) = 48", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_56", + description: "Max stack depth (number of LBR entries) = 56", + bits_range: (6, 6), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_depth_64", + description: "Max stack depth (number of LBR entries) = 64", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_deep_c_reset", + description: "LBRs maybe cleared on MWAIT C-state > C1", + bits_range: (30, 30), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_ip_is_lip", + description: "LBR IP contain Last IP, otherwise effective IP", + bits_range: (31, 31), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x1c, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "lbr_cpl", + description: "CPL filtering (non-zero IA32_LBR_CTL[2:1]) supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_branch_filter", + description: "Branch filtering (non-zero IA32_LBR_CTL[22:16]) supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_call_stack", + description: "Call-stack mode (IA32_LBR_CTL[3] = 1) supported", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x1c, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "lbr_mispredict", + description: "Branch misprediction bit supported (IA32_LBR_x_INFO[63])", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_timed_lbr", + description: "Timed LBRs (CPU cycles since last LBR entry) supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_branch_type", + description: "Branch type field (IA32_LBR_INFO_x[59:56]) supported", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_events_gpc_bmp", + description: "LBR PMU-events logging support; bitmap for first 4 GP (general-purpose) Counters", + bits_range: (16, 19), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Tile Information Main Leaf + // =================================================================================================================== + // NOTE: AMX is opt-in, but there are no problems with inheriting these values. The CHV will take care of zeroing out the bits userspace applications should check for if the user did not opt-in to amx. + ( + Parameters { + leaf: 0x1d, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "amx_max_palette", + description: "Highest palette ID / subleaf ID", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + // =================================================================================================================== + // Tile Palette 1 Sub-leaf + // =================================================================================================================== + // NOTE: AMX is opt-in, but there are no problems with inheriting these values. The CHV will take care of zeroing out the bits userspace applications should check for if the user did not opt-in to amx. + ( + Parameters { + leaf: 0x1d, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "amx_palette_size", + description: "AMX palette total tiles size, in bytes", + bits_range: (0, 15), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_tile_size", + description: "AMX single tile's size, in bytes", + bits_range: (16, 31), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x1d, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "amx_tile_row_size", + description: "AMX tile single row's size, in bytes", + bits_range: (0, 15), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_palette_nr_tiles", + description: "AMX palette number of tiles", + bits_range: (16, 31), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x1d, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "amx_tile_nr_rows", + description: "AMX tile max number of rows", + bits_range: (0, 15), + policy: ProfilePolicy::Inherit, + }]), + ), + // =================================================================================================================== + // TMUL Information Main Leaf + // =================================================================================================================== + // NOTE: AMX is opt-in, but there are no problems with inheriting these values. The CHV will take care of zeroing out the bits userspace applications should check for if the user did not opt-in to amx. + ( + Parameters { + leaf: 0x1e, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tmul_info_max", + description: "Reports the maximum number of sub-leaves that are supported in leaf 0x1e", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x1e, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "tmul_maxk", + description: "TMUL unit maximum height, K (rows or columns)", + bits_range: (0, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "tmul_maxn", + description: "TMUL unit maximum SIMD dimension, N (column bytes)", + bits_range: (8, 23), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // =================================================================================================================== + // TMUL Information Sub-leaf 1 + // =================================================================================================================== + // NOTE: AMX is opt-in, but there are no problems with inheriting these values. The CHV will take care of zeroing out the bits userspace applications should check for if the user did not opt-in to amx. + ( + Parameters { + leaf: 0x1e, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + // NOTE: AMX currently requires opt-in, even for the host CPU profile. We still inherit this value for profiles as the relevant feature bits that userspace applications must check will be zeroed out if the user has not opted in for "amx" via CpuFeatures. + ValueDefinitions::new(&[ + ValueDefinition { + short: "amx_int8", + description: "If 1, the processor supports tile computational operations on 8-bit integers", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_bf16", + description: "If 1, the processor supports tile computational operations on bfloat16 numbers", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_complex", + description: "If 1, the processor supports the AMX-COMPLEX instructions", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_fp16", + description: "If 1, the processor supports tile computational operations on FP16 numbers", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_fp8", + description: "If 1, the processor supports tile computational operations on FP8 numbers", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_transpose", + description: "If 1, the processor supports the AMX-TRANSPOSE instructions", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_tf32", + description: "If 1, the processor supports the AMX-TF32 (FP19) instructions", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_avx512", + description: "If 1, the processor supports the AMX-AVX512 instructions", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "amx_movrs", + description: "If 1, the processor supports the AMX-MOVRS instructions", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // =================================================================================================================== + // V2 Extended Topology Enumeration + // =================================================================================================================== + + // The values in leaf 0x1f must be set by CHV itself. + ( + Parameters { + leaf: 0x1f, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "x2apic_id_shift", + description: "Bit width of this level (previous levels inclusive)", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x1f, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "domain_lcpus_count", + description: "Logical CPUs count across all instances of this domain", + bits_range: (0, 15), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x1f, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "domain_level", + description: "This domain level (subleaf ID)", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "domain_type", + description: "This domain type", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x1f, + sub_leaf: RangeInclusive::new(0, u32::MAX), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "x2apic_id", + description: "x2APIC ID of current logical CPU", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + // =================================================================================================================== + // Processor History Reset + // =================================================================================================================== + ( + Parameters { + leaf: 0x20, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "hreset_nr_subleaves", + description: "CPUID 0x20 max subleaf + 1", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x20, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "hreset_thread_director", + description: "HRESET of Intel thread director is supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // TDX + // =================================================================================================================== + + // TDX is not supported by CPU profiles for now. We just zero out this leaf for CPU profiles for the time being. + ( + Parameters { + leaf: 0x21, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tdx_vendorid_0", + description: "TDX vendor ID string bytes 0 - 3", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x21, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tdx_vendorid_2", + description: "CPU vendor ID string bytes 8 - 11", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x21, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "tdx_vendorid_1", + description: "CPU vendor ID string bytes 4 - 7", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring Extended Main Leaf + // =================================================================================================================== + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "subleaf_0", + description: "If 1, subleaf 0 exists", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "subleaf_1", + description: "If 1, subleaf 1 exists", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "subleaf_2", + description: "If 1, subleaf 2 exists", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "subleaf_3", + description: "If 1, subleaf 3 exists", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "subleaf_4", + description: "If 1, subleaf 4 exists", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "subleaf_5", + description: "If 1, subleaf 5 exists. The processor supports Architectural PEBS. The IA32_PEBS_BASE and IA32_PEBS_INDEX MSRs exist", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "unitmask2", + description: "IA32_PERFEVTSELx MSRs UnitMask2 is supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "eq_bit", + description: "equal flag in the IA32_PERFEVTSELx MSR is supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "RDPMC_USR_DISABLE", + description: "RDPMC_USR_DISABLE", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "num_slots_per_cycle", + description: "Number of slots per cycle. This number can be multiplied by the number of cycles (from CPU_CLK_UNHALTED.THREAD / CPU_CLK_UNHALTED.CORE or IA32_FIXED_CTR1) to determine the total number of slots", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring Extended Sub-leaf 1 + // =================================================================================================================== + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pmu_gp_counters_bitmap", + description: "General-purpose PMU counters bitmap", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(1, 1), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pmu_f_counters_bitmap", + description: "Fixed PMU counters bitmap", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring Extended Sub-leaf 2 + // =================================================================================================================== + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(2, 2), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pmu_acr_bitmap", + description: "Bitmap of Auto Counter Reload (ACR) general-purpose counters that can be reloaded", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring Extended Sub-leaf 3 + // =================================================================================================================== + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(3, 3), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "core_cycles_evt", + description: "Core cycles event supported", + bits_range: (0, 0), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "insn_retired_evt", + description: "Instructions retired event supported", + bits_range: (1, 1), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ref_cycles_evt", + description: "Reference cycles event supported", + bits_range: (2, 2), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "llc_refs_evt", + description: "Last-level cache references event supported", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "llc_misses_evt", + description: "Last-level cache misses event supported", + bits_range: (4, 4), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "br_insn_ret_evt", + description: "Branch instruction retired event supported", + bits_range: (5, 5), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "br_mispr_evt", + description: "Branch mispredict retired event supported", + bits_range: (6, 6), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "td_slots_evt", + description: "Topdown slots event supported", + bits_range: (7, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "td_backend_bound_evt", + description: "Topdown backend bound event supported", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "td_bad_spec_evt", + description: "Topdown bad speculation event supported", + bits_range: (9, 9), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "td_frontend_bound_evt", + description: "Topdown frontend bound event supported", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "td_retiring_evt", + description: "Topdown retiring event support", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr_inserts", + description: "LBR support", + bits_range: (12, 12), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring Extended Sub-leaf 4 + // =================================================================================================================== + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(4, 4), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "allow_in_record", + description: "If 1, indicates that the ALLOW_IN_RECORD bit is available in the IA32_PMC_GPn_CFG_C and IA32_PMC_FXm_CFG_C MSRs", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cntr", + description: "Counters group sub-groups general-purpose counters, fixed-function counters, and performance metrics are available", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr", + description: "LBR group and both bits [41:40] are available", + bits_range: (8, 9), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xer", + description: "These bits correspond to XER group bits [55:49]", + bits_range: (17, 23), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "grp", + description: "If 1, the GRP group is available", + bits_range: (29, 29), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "aux", + description: "If 1, the AUX group is available", + bits_range: (30, 30), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(4, 4), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "allow_in_record", + description: "If 1, indicates that the ALLOW_IN_RECORD bit is available in the IA32_PMC_GPn_CFG_C and IA32_PMC_FXm_CFG_C MSRs", + bits_range: (3, 3), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "cntr", + description: "Counters group sub-groups general-purpose counters, fixed-function counters, and performance metrics are available", + bits_range: (0, 7), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "lbr", + description: "LBR group and both bits [41:40] are available", + bits_range: (8, 9), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "xer", + description: "These bits correspond to XER group bits [55:49]", + bits_range: (17, 23), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "grp", + description: "If 1, the GRP group is available", + bits_range: (29, 29), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "aux", + description: "If 1, the AUX group is available", + bits_range: (30, 30), + policy: ProfilePolicy::Static(0), + }, + ]), + ), + // =================================================================================================================== + // Architectural Performance Monitoring Extended Sub-leaf 5 + // =================================================================================================================== + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "architectural_pebs_counters", + description: "General-purpose counters support Architectural PEBS. Bit vector of general-purpose counters for which the Architectural PEBS mechanism is available", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pebs_pdist_counters", + description: "General-purpose counters for which PEBS support PDIST", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pebs_fixed_function_counters", + description: "Fixed-function counters support Architectural PEBS. Bit vector of fixed-function counters for which the Architectural PEBS mechanism is available. If ECX[x] == 1, then the IA32_PMC_FXm_CFG_C MSR is available, and PEBS is supported", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + ( + Parameters { + leaf: 0x23, + sub_leaf: RangeInclusive::new(5, 5), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "pebs_fixed_function_pdist_counters", + description: "Fixed-function counters for which PEBS supports PDIST", + bits_range: (0, 31), + policy: ProfilePolicy::Static(0), + }]), + ), + // =================================================================================================================== + // Converged Vector ISA Main Leaf + // =================================================================================================================== + ( + Parameters { + leaf: 0x24, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "converged_vector_isa_max_sub_leaves", + description: "Reports the maximum number of sub-leaves that are supported in leaf 0x24", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x24, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "avx_10_version", + description: "Reports the intel AVX10 Converged Vector ISA version", + bits_range: (0, 7), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "avx_10_lengths", + description: "Reserved at 111", + bits_range: (0, 7), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // Hypervisor reserved CPUID leaves are set elsewhere + + // =================================================================================================================== + // Extended Function CPUID Information + // =================================================================================================================== + ( + Parameters { + leaf: 0x80000000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "max_ext_leaf", + description: "Maximum extended CPUID leaf supported", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_vendorid_0", + description: "Vendor ID string bytes 0 - 3", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x80000000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_vendorid_2", + description: "Vendor ID string bytes 8 - 11", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x80000000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_vendorid_1", + description: "Vendor ID string bytes 4 - 7", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + // 0x80000001.EAX and EBX are both Reserved on Intel hence we just zero them out + ( + Parameters { + leaf: 0x80000001, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "lahf_lm", + description: "LAHF and SAHF in 64-bit mode", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "lzcnt", + description: "LZCNT advanced bit manipulation", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "prefetchw", + description: "3DNow PREFETCH/PREFETCHW support", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x80000001, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "syscall", + description: "SYSCALL and SYSRET instructions", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "nx", + description: "Execute Disable Bit available", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "pdpe1gb", + description: "1-GB large page support", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + // MSR related + ValueDefinition { + short: "rdtscp", + description: "RDTSCP instruction and IA32_TSC_AUX are available", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "lm", + description: "Long mode (x86-64, 64-bit support)", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + // The profile generation tool will actually modify the brand id string before + // acting on the policy set here. + ( + Parameters { + leaf: 0x80000002, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_0", + description: "CPU brand ID string, bytes 0 - 3", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000002, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_1", + description: "CPU brand ID string, bytes 4 - 7", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000002, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_2", + description: "CPU brand ID string, bytes 8 - 11", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000002, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_3", + description: "CPU brand ID string, bytes 12 - 15", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000003, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_4", + description: "CPU brand ID string bytes, 16 - 19", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000003, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_5", + description: "CPU brand ID string bytes, 20 - 23", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000003, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_6", + description: "CPU brand ID string bytes, 24 - 27", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000003, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_7", + description: "CPU brand ID string bytes, 28 - 31", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000004, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_8", + description: "CPU brand ID string, bytes 32 - 35", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000004, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_9", + description: "CPU brand ID string, bytes 36 - 39", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000004, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_10", + description: "CPU brand ID string, bytes 40 - 43", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000004, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "cpu_brandid_11", + description: "CPU brand ID string, bytes 44 - 47", + bits_range: (0, 31), + policy: ProfilePolicy::Inherit, + }]), + ), + ( + Parameters { + leaf: 0x80000006, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "l2_line_size", + description: "L2 cache line size, in bytes", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "l2_nlines", + description: "L2 cache number of lines per tag", + bits_range: (8, 11), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "l2_assoc", + description: "L2 cache associativity", + bits_range: (12, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "l2_size_kb", + description: "L2 cache size, in KB", + bits_range: (16, 31), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + // EAX, EBX and ECX of 0x8000_0007 are all reserved (=0) on Intel + ( + Parameters { + leaf: 0x80000007, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ + // TODO: We may want some mechanism to let users opt-in to using an invariant TSC provided by the hardware (when available). + // TODO: Probably unconditionally set by CHV + ValueDefinition { + short: "constant_tsc", + description: "TSC ticks at constant rate across all P and C states", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit, + }, + ]), + ), + ( + Parameters { + leaf: 0x80000008, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "phys_addr_bits", + description: "Max physical address bits", + bits_range: (0, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "virt_addr_bits", + description: "Max virtual address bits", + bits_range: (8, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "guest_phys_addr_bits", + description: "Max nested-paging guest physical address bits", + bits_range: (16, 23), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x80000008, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "wbnoinvd", + description: "WBNOINVD supported", + bits_range: (9, 9), + policy: ProfilePolicy::Static(0), + }]), + ), + ]) +}; + +/// Compile time check that the given `BIT` in the CPUID output register specified by `params` is not +/// declared to be overwritten by `0` for non-host CPU profiles. +pub const fn assert_not_denied_cpuid_feature(params: &Parameters) { + if let Some(defs) = INTEL_CPUID_DEFINITIONS.get(params) + && let Some(def) = defs.find_bit::() + { + assert!(!matches!(def.policy, ProfilePolicy::Static(0))); + } else { + panic!("Unable to lookup CPUID value definition with the given parameters and feature bit"); + } +} + +// TODO: Also include assert_denied_cpuid_feature diff --git a/arch/src/x86_64/cpuid_definitions/kvm.rs b/arch/src/x86_64/cpuid_definitions/kvm.rs new file mode 100644 index 0000000000..9523f4ffab --- /dev/null +++ b/arch/src/x86_64/cpuid_definitions/kvm.rs @@ -0,0 +1,223 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This module contains CPUID definitions for the KVM hypervisor. + +use std::ops::RangeInclusive; + +use crate::x86_64::CpuidReg; +use crate::x86_64::cpuid_definitions::{ + CpuidDefinitions, Parameters, ProfilePolicy, ValueDefinition, ValueDefinitions, +}; + +/// CPUID features defined for the KVM hypervisor. +/// +/// See https://www.kernel.org/doc/html/latest/virt/kvm/x86/cpuid.html +pub const KVM_CPUID_DEFINITIONS: CpuidDefinitions<6> = const { + CpuidDefinitions([ + //===================================================================== + // KVM CPUID Signature + // =================================================================== + ( + Parameters { + leaf: 0x4000_0000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "max_hypervisor_leaf", + description: "The maximum valid leaf between 0x4000_0000 and 0x4FFF_FFF", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x4000_0000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EBX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "hypervisor_string_ebx", + description: "Part of the hypervisor string", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x4000_0000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::ECX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "hypervisor_string_ecx", + description: "Part of the hypervisor string", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + ( + Parameters { + leaf: 0x4000_0000, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "hypervisor_string_edx", + description: "Part of the hypervisor string", + bits_range: (0, 31), + policy: ProfilePolicy::Passthrough, + }]), + ), + //===================================================================== + // KVM CPUID Features + // =================================================================== + ( + Parameters { + leaf: 0x4000_0001, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EAX, + }, + ValueDefinitions::new(&[ + ValueDefinition { + short: "kvm_feature_clocksource", + description: "kvmclock available at MSRs 0x11 and 0x12", + bits_range: (0, 0), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_nop_io_delay", + description: "Not necessary to perform delays on PIO operations", + bits_range: (1, 1), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_mmu_op", + description: "Deprecated", + bits_range: (2, 2), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_clocksource2", + description: "kvmclock available at MSRs 0x4b564d00 and 0x4b564d01", + bits_range: (3, 3), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_async_pf", + description: "async pf can be enabled by writing to MSR 0x4b564d02", + bits_range: (4, 4), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_steal_time", + description: "steal time can be enabled by writing to msr 0x4b564d03", + bits_range: (5, 5), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_pv_eoi", + description: "paravirtualized end of interrupt handler can be enabled by writing to msr 0x4b564d04", + bits_range: (6, 6), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_pv_unhalt", + description: "guest checks this feature bit before enabling paravirtualized spinlock support", + bits_range: (7, 7), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_pv_tlb_flush", + description: "guest checks this feature bit before enabling paravirtualized tlb flush", + bits_range: (9, 9), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_async_pf_vmexit", + description: "paravirtualized async PF VM EXIT can be enabled by setting bit 2 when writing to msr 0x4b564d02", + bits_range: (10, 10), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_pv_send_ipi", + description: "guest checks this feature bit before enabling paravirtualized send IPIs", + bits_range: (11, 11), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_poll_control", + description: "host-side polling on HLT can be disabled by writing to msr 0x4b564d05.", + bits_range: (12, 12), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_pv_sched_yield", + description: "guest checks this feature bit before using paravirtualized sched yield.", + bits_range: (13, 13), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_async_pf_int", + description: "guest checks this feature bit before using the second async pf control msr 0x4b564d06 and async pf acknowledgment msr 0x4b564d07.", + bits_range: (14, 14), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_msi_ext_dest_id", + description: "guest checks this feature bit before using extended destination ID bits in MSI address bits 11-5.", + bits_range: (15, 15), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_hc_map_gpa_range", + description: "guest checks this feature bit before using the map gpa range hypercall to notify the page state change", + bits_range: (16, 16), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_migration_control", + description: "guest checks this feature bit before using MSR_KVM_MIGRATION_CONTROL", + bits_range: (17, 17), + policy: ProfilePolicy::Passthrough, + }, + ValueDefinition { + short: "kvm_feature_clocksource_stable_bit", + description: "host will warn if no guest-side per-cpu warps are expected in kvmclock", + bits_range: (24, 24), + policy: ProfilePolicy::Passthrough, + }, + ]), + ), + ( + Parameters { + leaf: 0x4000_0001, + sub_leaf: RangeInclusive::new(0, 0), + register: CpuidReg::EDX, + }, + ValueDefinitions::new(&[ValueDefinition { + short: "kvm_hints_realtime", + description: "guest checks this feature bit to determine that vCPUs are never preempted for an unlimited time allowing optimizations", + bits_range: (0, 0), + policy: ProfilePolicy::Passthrough, + }]), + ), + ]) +}; + +/// Compile time check that the given `BIT` in the CPUID output register specified by `params` is not +/// declared to be overwritten by `0` for non-host CPU profiles. +pub const fn assert_not_denied_cpuid_feature(params: &Parameters) { + if let Some(defs) = KVM_CPUID_DEFINITIONS.get(params) + && let Some(def) = defs.find_bit::() + { + assert!(!matches!(def.policy, ProfilePolicy::Static(0))); + } else { + panic!("Unable to lookup CPUID value definition with the given parameters and feature bit"); + } +} + +// TODO: Also include assert_denied_cpuid_feature diff --git a/arch/src/x86_64/cpuid_definitions/mod.rs b/arch/src/x86_64/cpuid_definitions/mod.rs new file mode 100644 index 0000000000..f45dc4a9e4 --- /dev/null +++ b/arch/src/x86_64/cpuid_definitions/mod.rs @@ -0,0 +1,233 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::ops::RangeInclusive; + +use serde::{Deserialize, Serialize}; + +use crate::x86_64::CpuidReg; +use crate::{deserialize_u32_hex, serialize_u32_hex}; + +pub mod intel; +#[cfg(feature = "kvm")] +pub mod kvm; + +/// Parameters for inspecting CPUID definitions. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Parameters { + // 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, + // 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( + input: &RangeInclusive, + serializer: S, +) -> Result { + let provisional = ProvisionalRangeInclusive { + start: *input.start(), + end: *input.end(), + }; + provisional.serialize(serializer) +} + +fn deserialize_range_hex<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + let ProvisionalRangeInclusive { start, end } = + ProvisionalRangeInclusive::deserialize(deserializer)?; + Ok(start..=end) +} + +/// Describes a policy for how the corresponding CPUID data should be considered when building +/// a CPU profile. +/// +/// This enum is mostly intended for the CPU profile generation tool, but it's debug representation +/// might also appear in logs if/when CPUID compatibility checks fail at runtime. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ProfilePolicy { + /// Store the corresponding data when building the CPU profile. + /// + /// When the CPU profile gets utilized the corresponding data will be set into the modified + /// CPUID instruction(s). + Inherit, + /// Ignore the corresponding data when building the CPU profile. + /// + /// When the CPU profile gets utilized the corresponding data will then instead get + /// extracted from the host. + /// + /// This variant is typically set for data that has no effect on migration compatibility, + /// but there may be some exceptions such as data which is necessary to run the VM at all, + /// but must coincide with whatever is on the host. + Passthrough, + /// Set the following hardcoded value in the CPU profile. + /// + /// This variant is typically used for features/values that don't work well with live migration (even when using the exact same physical CPU model). + Static(u32), +} + +/// A description of a range of bits in a register populated by the CPUID instruction with specific parameters. +#[derive(Clone, Copy, Debug)] +pub struct ValueDefinition { + /// A short name for the value obtainable through CPUID + pub short: &'static str, + /// A description of the value obtainable through CPUID + pub description: &'static str, + /// The range of bits in the output register corresponding to this feature or value. + /// + /// This is not a `RangeInclusive` because that type does unfortunately not implement `Copy`. + pub bits_range: (u8, u8), + /// The policy corresponding to this value when building CPU profiles. + pub policy: ProfilePolicy, +} + +/// Describes values within a register populated by the CPUID instruction with specific parameters. +pub struct ValueDefinitions(&'static [ValueDefinition]); +impl ValueDefinitions { + /// Constructor permitting at most 32 entries. + const fn new(cpuid_descriptions: &'static [ValueDefinition]) -> Self { + // Note that this function is only called within this module, at compile time, hence it is fine to have some + // additional sanity checks such as the following assert. + assert!(cpuid_descriptions.len() <= 32); + Self(cpuid_descriptions) + } + /// Converts this into a slice representation. This is the only way to read values of this type. + pub const fn as_slice(&self) -> &'static [ValueDefinition] { + self.0 + } + + /// Lookup the [`ValueDefinition`] whose bits range contains the given `BIT`. + pub const fn find_bit(&self) -> Option<&ValueDefinition> { + let mut idx = 0; + let len = self.0.len(); + while idx < len { + let def = &self.0[idx]; + let start = def.bits_range.0; + let end = def.bits_range.1; + if (start <= BIT) & (end >= BIT) { + return Some(def); + } + idx += 1; + } + None + } +} + +/// Describes multiple CPUID outputs. +/// +/// Each wrapped [`ValueDefinitions`] corresponds to the given [`Parameters`] in the same tuple. +/// +pub struct CpuidDefinitions( + [(Parameters, ValueDefinitions); NUM_PARAMETERS], +); + +impl CpuidDefinitions { + pub const fn as_slice(&self) -> &[(Parameters, ValueDefinitions); NUM_PARAMETERS] { + &self.0 + } + + /// Lookup the [`ValueDefinitions`] corresponding to the given `parameters`. + pub const fn get(&self, parameters: &Parameters) -> Option<&ValueDefinitions> { + let mut idx = 0; + let len = self.0.len(); + let leaf = parameters.leaf; + let sub_leaf_start = *parameters.sub_leaf.start(); + let sub_leaf_end = *parameters.sub_leaf.end(); + // Note that as of today const Rust is quite a bit more vorbose than normal Rust. + // This is why the following implementation doesn't look so idiomatic. + let is_eax = matches!(parameters.register, CpuidReg::EAX); + let is_ebx = matches!(parameters.register, CpuidReg::EBX); + let is_ecx = matches!(parameters.register, CpuidReg::ECX); + let is_edx = matches!(parameters.register, CpuidReg::EDX); + while idx < len { + let (param, defs) = &self.0[idx]; + let matching_leaf = leaf == param.leaf; + let matching_sub_leaf = (sub_leaf_start >= *param.sub_leaf.start()) + & (sub_leaf_end <= *param.sub_leaf.end()); + let matching_reg = { + match param.register { + CpuidReg::EAX => is_eax, + CpuidReg::EBX => is_ebx, + CpuidReg::ECX => is_ecx, + CpuidReg::EDX => is_edx, + } + }; + if matching_leaf & matching_sub_leaf & matching_reg { + return Some(defs); + } + idx += 1; + } + None + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::Parameters; + use crate::x86_64::CpuidReg; + + // Check that serializing and then deserializing a value of type `Parameter` results in the + // same value we started with. + // + // Also check that the serialized numeric values are hex strings + proptest! { + #[test] + fn parameter_serialization_roundtrip_works(leaf in any::(), x1 in 0u32..100, x2 in 0u32..100, reg in 0..4) { + let sub_leaf_range_start = std::cmp::min(x1, x2); + let sub_leaf_range_end = std::cmp::max(x1,x2); + let sub_leaf = sub_leaf_range_start..=sub_leaf_range_end; + let register = match reg { + 0 => CpuidReg::EAX, + 1 => CpuidReg::EBX, + 2 => CpuidReg::ECX, + 3 => CpuidReg::EDX, + _ => unreachable!() + }; + let cpuid_parameters = Parameters { + leaf, + sub_leaf, + register + }; + let serialized = serde_json::to_string(&cpuid_parameters).unwrap(); + let deserialized: Parameters = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(&deserialized, &cpuid_parameters); + + // Check that all numeric values are hex strings when serialized to json + let params_json = serde_json::to_value(cpuid_parameters).unwrap(); + prop_assert!(params_json.get("leaf").unwrap().as_str().unwrap().starts_with("0x")); + let sub_leaf_map = params_json.get("sub_leaf").unwrap().as_object().unwrap(); + prop_assert!(sub_leaf_map.get("start").unwrap().as_str().unwrap().starts_with("0x")); + prop_assert!(sub_leaf_map.get("end").unwrap().as_str().unwrap().starts_with("0x")); + } + } +} diff --git a/arch/src/x86_64/interrupts.rs b/arch/src/x86_64/interrupts.rs index 1ca322ec71..70534c8346 100644 --- a/arch/src/x86_64/interrupts.rs +++ b/arch/src/x86_64/interrupts.rs @@ -6,7 +6,6 @@ // found in the LICENSE-BSD-3-Clause file. use std::result; -use std::sync::Arc; pub type Result = result::Result; @@ -24,7 +23,7 @@ pub fn set_apic_delivery_mode(reg: u32, mode: u32) -> u32 { /// /// # Arguments /// * `vcpu` - The VCPU object to configure. -pub fn set_lint(vcpu: &Arc) -> Result<()> { +pub fn set_lint(vcpu: &dyn hypervisor::Vcpu) -> Result<()> { let mut klapic = vcpu.get_lapic()?; let lvt_lint0 = klapic.get_klapic_reg(APIC_LVT0); diff --git a/arch/src/x86_64/mod.rs b/arch/src/x86_64/mod.rs index baa984c94b..0e2e539a5d 100644 --- a/arch/src/x86_64/mod.rs +++ b/arch/src/x86_64/mod.rs @@ -6,43 +6,68 @@ // 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::sync::Arc; +pub mod cpu_profile; +#[cfg(feature = "cpu_profile_generation")] +pub mod cpu_profile_generation; +pub mod cpuid_definitions; pub mod interrupts; pub mod layout; +pub mod msr_definitions; +pub mod regs; + +#[cfg(feature = "tdx")] +pub mod tdx; + mod mpspec; mod mptable; -pub mod regs; -use std::collections::BTreeMap; +mod msr_filter; +mod smbios; + +use std::arch::x86_64; +use std::collections::{HashMap, HashSet}; use std::mem; -use hypervisor::arch::x86::{CpuIdEntry, CPUID_FLAG_VALID_INDEX}; -use hypervisor::{CpuVendor, HypervisorCpuError, HypervisorError}; +use hypervisor::arch::x86::{CPUID_FLAG_VALID_INDEX, CpuIdEntry, MsrEntry}; +use hypervisor::{CpuVendor, HypervisorCpuError, HypervisorError, HypervisorVmError}; use linux_loader::loader::bootparam::{boot_params, setup_header}; use linux_loader::loader::elf::start_info::{ hvm_memmap_table_entry, hvm_modlist_entry, hvm_start_info, }; +use log::{debug, error, info, trace}; +pub use msr_filter::{MAX_BITMAP_SIZE, filter_denied_msrs}; +use serde::{Deserialize, Serialize}; +pub use smbios::{SmbiosChassisConfig, SmbiosConfig, SmbiosSystem}; use thiserror::Error; use vm_memory::{ Address, Bytes, GuestAddress, GuestAddressSpace, GuestMemory, GuestMemoryAtomic, - GuestMemoryRegion, GuestUsize, + GuestMemoryRegion, }; -use crate::{GuestMemoryMmap, InitramfsConfig, RegionType}; -mod smbios; -use std::arch::x86_64; -#[cfg(feature = "tdx")] -pub mod tdx; +use crate::x86_64::cpu_profile::{ + CpuidOutputRegisterAdjustments, FeatureMsrAdjustment, RequiredMsrUpdates, +}; +use crate::x86_64::msr_definitions::RegisterAddress; +use crate::{CpuProfile, GuestMemoryMmap, InitramfsConfig, RegionType}; + +// While modern architectures support more than 255 CPUs via x2APIC, +// legacy devices such as mptable support at most 254 CPUs. +pub const MAX_SUPPORTED_CPUS_LEGACY: u32 = 254; // CPUID feature bits #[cfg(feature = "kvm")] const TSC_DEADLINE_TIMER_ECX_BIT: u8 = 24; // tsc deadline timer ecx bit. const HYPERVISOR_ECX_BIT: u8 = 31; // Hypervisor ecx bit. +const VMX_ECX_BIT: u8 = 5; // VMX for Intel +const SVM_ECX_BIT: u8 = 2; // SVM for AMD const MTRR_EDX_BIT: u8 = 12; // Hypervisor ecx bit. const INVARIANT_TSC_EDX_BIT: u8 = 8; // Invariant TSC bit on 0x8000_0007 EDX const AMX_BF16: u8 = 22; // AMX tile computation on bfloat16 numbers const AMX_TILE: u8 = 24; // AMX tile load/store instructions const AMX_INT8: u8 = 25; // AMX tile computation on 8-bit integers +const AMX_FP16: u8 = 21; // AMX tile computation on fp16 numbers +const AMX_COMPLEX: u8 = 8; // AMX tile computation on complex numbers + // KVM feature bits #[cfg(feature = "tdx")] const KVM_FEATURE_CLOCKSOURCE_BIT: u8 = 0; @@ -57,6 +82,8 @@ const KVM_FEATURE_ASYNC_PF_VMEXIT_BIT: u8 = 10; #[cfg(feature = "tdx")] const KVM_FEATURE_STEAL_TIME_BIT: u8 = 5; +const KVM_FEATURE_MSI_EXT_DEST_ID: u8 = 15; + pub const _NSIG: i32 = 65; #[derive(Debug, Copy, Clone)] @@ -73,60 +100,13 @@ pub struct EntryPoint { const E820_RAM: u32 = 1; const E820_RESERVED: u32 = 2; -#[derive(Clone)] -pub struct SgxEpcSection { - start: GuestAddress, - size: GuestUsize, -} - -impl SgxEpcSection { - pub fn new(start: GuestAddress, size: GuestUsize) -> Self { - SgxEpcSection { start, size } - } - pub fn start(&self) -> GuestAddress { - self.start - } - pub fn size(&self) -> GuestUsize { - self.size - } -} - -#[derive(Clone)] -pub struct SgxEpcRegion { - start: GuestAddress, - size: GuestUsize, - epc_sections: BTreeMap, -} - -impl SgxEpcRegion { - pub fn new(start: GuestAddress, size: GuestUsize) -> Self { - SgxEpcRegion { - start, - size, - epc_sections: BTreeMap::new(), - } - } - pub fn start(&self) -> GuestAddress { - self.start - } - pub fn size(&self) -> GuestUsize { - self.size - } - pub fn epc_sections(&self) -> &BTreeMap { - &self.epc_sections - } - pub fn insert(&mut self, id: String, epc_section: SgxEpcSection) { - self.epc_sections.insert(id, epc_section); - } -} - pub struct CpuidConfig { - pub sgx_epc_sections: Option>, pub phys_bits: u8, pub kvm_hyperv: bool, #[cfg(feature = "tdx")] pub tdx: bool, pub amx: bool, + pub profile: CpuProfile, } #[derive(Debug, Error)] @@ -163,22 +143,50 @@ pub enum Error { #[error("Error setting up SMBIOS table")] SmbiosSetup(#[source] smbios::Error), - /// Could not find any SGX EPC section - #[error("Could not find any SGX EPC section")] - NoSgxEpcSection, - - /// Missing SGX CPU feature - #[error("Missing SGX CPU feature")] - MissingSgxFeature, - - /// Missing SGX_LC CPU feature - #[error("Missing SGX_LC CPU feature")] - MissingSgxLaunchControlFeature, - /// Error getting supported CPUID through the hypervisor (kvm/mshv) API #[error("Error getting supported CPUID through the hypervisor API")] CpuidGetSupported(#[source] HypervisorError), - + /// Error getting the MSR-based features through the hypervisor (kvm) API + #[error("Error getting the MSR-based features through the hypervisor API")] + MsrBasedFeaturesGetSupported(#[source] HypervisorError), + + #[error("Error getting the MSRs supported by the hypervisor")] + MsrIndexList(#[source] HypervisorError), + + #[error( + "The selected CPU profile cannot be utilized because the host's CPUID entries are not compatible with the profile" + )] + CpuProfileCpuidIncompatibility, + + #[error( + "The selected CPU profile cannot be utilized because the host's MSR-based features are not compatible with the profile" + )] + CpuProfileMsrIncompatibility, + + #[error( + "Unable to apply MSR filter: Bitmaps exceed maximum permitted memory usage: {0} > {MAX_BITMAP_SIZE}" + )] + MsrFilterTooLarge(usize), + + #[error("The hypervisor failed to set the given MSR filter")] + MsrFilter(#[source] HypervisorVmError), + + /// Error because TDX cannot be enabled when a custom (non host) CPU profile has been selected + #[error("TDX cannot be enabled when a custom CPU profile has been selected")] + CpuProfileTdxIncompatibility, + #[error( + "The selected CPU profile cannot be utilized because a necessary CPUID entry was not found" + )] + /// Error when trying to apply a CPU profile because a necessary CPUID entry was not found + MissingExpectedCpuidEntry(#[source] cpu_profile::MissingCpuidEntriesError), + /// Error when trying to apply a CPU profile because the host has a CPU from a different vendor + #[error( + "The selected CPU profile cannot be utilized because the host has a CPU from a different vendor: host_vendor:={cpu_vendor_host:?}, expected_vendor:={cpu_vendor_profile:?}" + )] + CpuProfileVendorIncompatibility { + cpu_vendor_profile: CpuVendor, + cpu_vendor_host: CpuVendor, + }, /// Error populating CPUID with KVM HyperV emulation details #[error("Error populating CPUID with KVM HyperV emulation details")] CpuidKvmHyperV(#[source] vmm_sys_util::fam::Error), @@ -209,11 +217,11 @@ pub enum Error { E820Configuration, } -pub fn get_x2apic_id(cpu_id: u32, topology: Option<(u8, u8, u8)>) -> u32 { +pub fn get_x2apic_id(cpu_id: u32, topology: Option<(u16, u16, u16, u16)>) -> u32 { if let Some(t) = topology { - let thread_mask_width = u8::BITS - (t.0 - 1).leading_zeros(); - let core_mask_width = u8::BITS - (t.1 - 1).leading_zeros(); - let die_mask_width = u8::BITS - (t.2 - 1).leading_zeros(); + let thread_mask_width = u16::BITS - (t.0 - 1).leading_zeros(); + let core_mask_width = u16::BITS - (t.1 - 1).leading_zeros(); + let die_mask_width = u16::BITS - (t.2 - 1).leading_zeros(); let thread_id = cpu_id % (t.0 as u32); let core_id = cpu_id / (t.0 as u32) % (t.1 as u32); @@ -229,7 +237,14 @@ pub fn get_x2apic_id(cpu_id: u32, topology: Option<(u8, u8, u8)>) -> u32 { cpu_id } -#[derive(Copy, Clone, Debug)] +pub fn get_max_x2apic_id(topology: (u16, u16, u16, u16)) -> u32 { + get_x2apic_id( + (topology.0 as u32 * topology.1 as u32 * topology.2 as u32 * topology.3 as u32) - 1, + Some(topology), + ) +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum CpuidReg { EAX, EBX, @@ -327,7 +342,7 @@ impl CpuidPatch { } } - pub fn patch_cpuid(cpuid: &mut [CpuIdEntry], patches: Vec) { + pub fn patch_cpuid(cpuid: &mut [CpuIdEntry], patches: &[CpuidPatch]) { for entry in cpuid { for patch in patches.iter() { if entry.function == patch.function && entry.index == patch.index { @@ -454,7 +469,7 @@ impl CpuidFeatureEntry { feature_reg: CpuidReg::EDX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, - // KVM CPUID bits: https://www.kernel.org/doc/html/latest/virt/kvm/cpuid.html + // KVM CPUID bits: https://www.kernel.org/doc/html/latest/virt/kvm/x86/cpuid.html // Leaf 0x4000_0000, EAX/EBX/ECX/EDX, KVM CPUID SIGNATURE CpuidFeatureEntry { function: 0x4000_0000, @@ -551,8 +566,62 @@ impl CpuidFeatureEntry { let src_vm_features = Self::get_features_from_cpuid(src_vm_cpuid, feature_entry_list); let dest_vm_features = Self::get_features_from_cpuid(dest_vm_cpuid, feature_entry_list); - // Loop on feature bit and check if the 'source vm' feature is a subset - // of those of the 'destination vm' feature + // If both processors are Intel then we can use the existing Intel CPUID definitions to log more + // precise information about potential errors + let both_intel = { + // Check if the vendor string is "GenuineIntel". This assumes that `leaf_0` is the entry + // corresponding to CPUID leaf 0. + let is_intel = |leaf_0: &CpuIdEntry| { + leaf_0.ebx == 0x756e_6547 && leaf_0.ecx == 0x6c65_746e && leaf_0.edx == 0x4965_6e69 + }; + let src_0 = src_vm_cpuid + .iter() + .find(|entry| (entry.function == 0x0) & (entry.index == 0x0)); + let dest_0 = dest_vm_cpuid + .iter() + .find(|entry| (entry.function == 0x0) & (entry.index == 0x0)); + src_0 + .zip(dest_0) + .is_some_and(|(src, dest)| is_intel(src) & is_intel(dest)) + }; + let extra_reporting = |entry: &CpuidFeatureEntry, src_reg: u32, dest_reg: u32| { + if let Some((_, defs)) = cpuid_definitions::intel::INTEL_CPUID_DEFINITIONS + .as_slice() + .iter() + .find(|(param, _)| { + (param.leaf == entry.function) + && (param.sub_leaf.contains(&entry.index) + && (param.register == entry.feature_reg)) + }) + { + for def in defs.as_slice() { + let mask = (def.bits_range.0..=def.bits_range.1) + .fold(0, |acc, next| acc | (1 << next)); + + let src_val = src_reg & mask; + let dest_val = dest_reg & mask; + + let is_compatible = match entry.compatible_check { + CpuidCompatibleCheck::BitwiseSubset => (src_val & (!dest_val)) == 0, + CpuidCompatibleCheck::NumNotGreater => src_val <= dest_val, + CpuidCompatibleCheck::Equal => src_val == dest_val, + }; + if !is_compatible { + info!( + "CPUID incompatibility for value definition='{:?}' detected in leaf={:#04x}, sub-leaf={:#04x}, register={:?}, compatibility_check={:?}, source VM value='{:#04x}' destination VM value='{:#04x}'", + def, + entry.function, + entry.index, + entry.feature_reg, + entry.compatible_check, + src_val, + dest_val + ); + } + } + } + }; + let mut compatible = true; for (i, (src_vm_feature, dest_vm_feature)) in src_vm_features .iter() @@ -571,12 +640,18 @@ impl CpuidFeatureEntry { }; if !entry_compatible { error!( - "Detected incompatible CPUID entry: leaf={:#02x} (subleaf={:#02x}), register='{:?}', \ + "Detected incompatible CPUID entry: leaf={:#04x} (subleaf={:#04x}), register='{:?}', \ compatible_check='{:?}', source VM feature='{:#04x}', destination VM feature'{:#04x}'.", - entry.function, entry.index, entry.feature_reg, - entry.compatible_check, src_vm_feature, dest_vm_feature - ); - + entry.function, + entry.index, + entry.feature_reg, + entry.compatible_check, + src_vm_feature, + dest_vm_feature + ); + if both_intel { + extra_reporting(entry, *src_vm_feature, *dest_vm_feature); + } compatible = false; } } @@ -590,10 +665,15 @@ impl CpuidFeatureEntry { } } +/// This function generates the CPUID entries to be set for all CPUs. +/// +/// If the `config` has a CPU profile set (other than host) then the profile +/// will be applied pub fn generate_common_cpuid( - hypervisor: &Arc, + hypervisor: &dyn hypervisor::Hypervisor, config: &CpuidConfig, ) -> super::Result> { + #[allow(unused_unsafe)] // SAFETY: cpuid called with valid leaves if unsafe { x86_64::__cpuid(1) }.ecx & (1 << HYPERVISOR_ECX_BIT) == 1 << HYPERVISOR_ECX_BIT { // SAFETY: cpuid called with valid leaves @@ -655,179 +735,395 @@ pub fn generate_common_cpuid( }); } - // Supported CPUID - let mut cpuid = hypervisor + // Supported CPUID according to the host and hypervisor + let mut host_cpuid = hypervisor .get_supported_cpuid() .map_err(Error::CpuidGetSupported)?; - CpuidPatch::patch_cpuid(&mut cpuid, cpuid_patches); - - if let Some(sgx_epc_sections) = &config.sgx_epc_sections { - update_cpuid_sgx(&mut cpuid, sgx_epc_sections)?; + // Copy CPU identification string + // + // If a CPU profile has been applied then this will get + // overwritten as soon as the profile is applied + for i in 0x8000_0002..=0x8000_0004 { + host_cpuid.retain(|c| c.function != i); + // SAFETY: call cpuid with valid leaves + #[allow(unused_unsafe)] + let leaf = unsafe { std::arch::x86_64::__cpuid(i) }; + host_cpuid.push(CpuIdEntry { + function: i, + eax: leaf.eax, + ebx: leaf.ebx, + ecx: leaf.ecx, + edx: leaf.edx, + ..Default::default() + }); } - #[cfg(feature = "tdx")] - let tdx_capabilities = if config.tdx { - let caps = hypervisor - .tdx_capabilities() - .map_err(Error::TdxCapabilities)?; - info!("TDX capabilities {:#?}", caps); - Some(caps) - } else { - None + let use_custom_profile = config.profile != CpuProfile::Host; + // Obtain cpuid entries that are adjusted to the specified CPU profile and the cpuid entries of the compatibility target + // TODO: Try to write this in a clearer way + let (host_adjusted_to_profile, profile_cpu_vendor) = { + config + .profile + .cpuid_data(config.amx) + .map_or((Ok(None), None), |profile_data| { + ( + CpuidOutputRegisterAdjustments::adjust_cpuid_entries( + host_cpuid.clone(), + &profile_data.adjustments, + ) + .map(Some), + Some(profile_data.cpu_vendor), + ) + }) }; + let mut host_adjusted_to_profile = + host_adjusted_to_profile.map_err(Error::MissingExpectedCpuidEntry)?; + + // There should be relatively few cases where live migration can succeed between hosts from different + // CPU vendors and making our checks account for that possibility would complicate things substantially. + // We thus require that the host's cpu vendor matches the one used to generate the CPU profile. + if let Some(cpu_vendor_profile) = profile_cpu_vendor + && let cpu_vendor_host = hypervisor.get_cpu_vendor() + && cpu_vendor_profile != cpu_vendor_host + { + return Err(Error::CpuProfileVendorIncompatibility { + cpu_vendor_profile, + cpu_vendor_host, + } + .into()); + } + // We now make the modifications according to the config parameters to each of the cpuid entries + // declared above and then perform a compatibility check. + for cpuid_option in [Some(&mut host_cpuid), host_adjusted_to_profile.as_mut()] { + let Some(cpuid) = cpuid_option else { + break; + }; + CpuidPatch::patch_cpuid(cpuid, &cpuid_patches); - // Update some existing CPUID - for entry in cpuid.as_mut_slice().iter_mut() { - match entry.function { - // Clear AMX related bits if the AMX feature is not enabled - 0x7 => { - if !config.amx && entry.index == 0 { - entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8)) - } + #[cfg(feature = "tdx")] + let tdx_capabilities = if config.tdx { + if use_custom_profile { + return Err(Error::CpuProfileTdxIncompatibility.into()); } - 0xd => - { - #[cfg(feature = "tdx")] - if let Some(caps) = &tdx_capabilities { - let xcr0_mask: u64 = 0x82ff; - let xss_mask: u64 = !xcr0_mask; - if entry.index == 0 { - entry.eax &= (caps.xfam_fixed0 as u32) & (xcr0_mask as u32); - entry.eax |= (caps.xfam_fixed1 as u32) & (xcr0_mask as u32); - entry.edx &= ((caps.xfam_fixed0 & xcr0_mask) >> 32) as u32; - entry.edx |= ((caps.xfam_fixed1 & xcr0_mask) >> 32) as u32; - } else if entry.index == 1 { - entry.ecx &= (caps.xfam_fixed0 as u32) & (xss_mask as u32); - entry.ecx |= (caps.xfam_fixed1 as u32) & (xss_mask as u32); - entry.edx &= ((caps.xfam_fixed0 & xss_mask) >> 32) as u32; - entry.edx |= ((caps.xfam_fixed1 & xss_mask) >> 32) as u32; + let caps = hypervisor + .tdx_capabilities() + .map_err(Error::TdxCapabilities)?; + info!("TDX capabilities {caps:#?}"); + Some(caps) + } else { + None + }; + + // Update some existing CPUID + for entry in cpuid.as_mut_slice().iter_mut() { + match entry.function { + // Clear AMX related bits if the AMX feature is not enabled + 0x7 + if !config.amx =>{ + if entry.index == 0 { + entry.edx &= !((1 << AMX_BF16) | (1 << AMX_TILE) | (1 << AMX_INT8)); + } + if entry.index == 1 { + entry.eax &= !(1 << AMX_FP16); + entry.edx &= !(1 << AMX_COMPLEX); + } } - } - } - // Copy host L1 cache details if not populated by KVM - 0x8000_0005 => { - if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 { - // SAFETY: cpuid called with valid leaves - if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 { - // SAFETY: cpuid called with valid leaves - let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) }; - entry.eax = leaf.eax; - entry.ebx = leaf.ebx; - entry.ecx = leaf.ecx; - entry.edx = leaf.edx; + + 0xd => + { + #[cfg(feature = "tdx")] + if let Some(caps) = &tdx_capabilities { + let xcr0_mask: u64 = 0x82ff; + let xss_mask: u64 = !xcr0_mask; + if entry.index == 0 { + entry.eax &= (caps.xfam_fixed0 as u32) & (xcr0_mask as u32); + entry.eax |= (caps.xfam_fixed1 as u32) & (xcr0_mask as u32); + entry.edx &= ((caps.xfam_fixed0 & xcr0_mask) >> 32) as u32; + entry.edx |= ((caps.xfam_fixed1 & xcr0_mask) >> 32) as u32; + } else if entry.index == 1 { + entry.ecx &= (caps.xfam_fixed0 as u32) & (xss_mask as u32); + entry.ecx |= (caps.xfam_fixed1 as u32) & (xss_mask as u32); + entry.edx &= ((caps.xfam_fixed0 & xss_mask) >> 32) as u32; + entry.edx |= ((caps.xfam_fixed1 & xss_mask) >> 32) as u32; + } } } - } - // Copy host L2 cache details if not populated by KVM - 0x8000_0006 => { - if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 { - // SAFETY: cpuid called with valid leaves - if unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 { - // SAFETY: cpuid called with valid leaves - let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) }; - entry.eax = leaf.eax; - entry.ebx = leaf.ebx; - entry.ecx = leaf.ecx; - entry.edx = leaf.edx; + + 0x1d + // Tile Information (purely AMX related). + if !config.amx =>{ + entry.eax = 0; + entry.ebx = 0; + entry.ecx = 0; + entry.edx = 0; } + + 0x1e + // TMUL information (purely AMX related) + if !config.amx =>{ + entry.eax = 0; + entry.ebx = 0; + entry.ecx = 0; + entry.edx = 0; + } + + + // Copy host L1 cache details if not populated by KVM + #[allow(unused_unsafe)] + 0x8000_0005 + if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 + + // SAFETY: cpuid called with valid leaves + && unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 =>{ + #[allow(unused_unsafe)] + // SAFETY: cpuid called with valid leaves + let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0005) }; + entry.eax = leaf.eax; + entry.ebx = leaf.ebx; + entry.ecx = leaf.ecx; + entry.edx = leaf.edx; + } + + // Copy host L2 cache details if not populated by KVM + #[allow(unused_unsafe)] + 0x8000_0006 + if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 + + // SAFETY: cpuid called with valid leaves + && unsafe { std::arch::x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 =>{ + #[allow(unused_unsafe)] + // SAFETY: cpuid called with valid leaves + let leaf = unsafe { std::arch::x86_64::__cpuid(0x8000_0006) }; + entry.eax = leaf.eax; + entry.ebx = leaf.ebx; + entry.ecx = leaf.ecx; + entry.edx = leaf.edx; + } - } - // Set CPU physical bits - 0x8000_0008 => { - entry.eax = (entry.eax & 0xffff_ff00) | (config.phys_bits as u32 & 0xff); - } - 0x4000_0001 => { - // These features are not supported by TDX - #[cfg(feature = "tdx")] - if config.tdx { - entry.eax &= !((1 << KVM_FEATURE_CLOCKSOURCE_BIT) - | (1 << KVM_FEATURE_CLOCKSOURCE2_BIT) - | (1 << KVM_FEATURE_CLOCKSOURCE_STABLE_BIT) - | (1 << KVM_FEATURE_ASYNC_PF_BIT) - | (1 << KVM_FEATURE_ASYNC_PF_VMEXIT_BIT) - | (1 << KVM_FEATURE_STEAL_TIME_BIT)) + // Set CPU physical bits + 0x8000_0008 => { + entry.eax = (entry.eax & 0xffff_ff00) | (config.phys_bits as u32 & 0xff); + } + 0x4000_0001 => { + // Enable KVM_FEATURE_MSI_EXT_DEST_ID. This allows the guest to target + // device interrupts to cpus with APIC IDs > 254 without interrupt remapping. + entry.eax |= 1 << KVM_FEATURE_MSI_EXT_DEST_ID; + + // These features are not supported by TDX + #[cfg(feature = "tdx")] + if config.tdx { + entry.eax &= !((1 << KVM_FEATURE_CLOCKSOURCE_BIT) + | (1 << KVM_FEATURE_CLOCKSOURCE2_BIT) + | (1 << KVM_FEATURE_CLOCKSOURCE_STABLE_BIT) + | (1 << KVM_FEATURE_ASYNC_PF_BIT) + | (1 << KVM_FEATURE_ASYNC_PF_VMEXIT_BIT) + | (1 << KVM_FEATURE_STEAL_TIME_BIT)); + } } + _ => {} } - _ => {} } - } - - // Copy CPU identification string - for i in 0x8000_0002..=0x8000_0004 { - cpuid.retain(|c| c.function != i); - // SAFETY: call cpuid with valid leaves - let leaf = unsafe { std::arch::x86_64::__cpuid(i) }; - cpuid.push(CpuIdEntry { - function: i, - eax: leaf.eax, - ebx: leaf.ebx, - ecx: leaf.ecx, - edx: leaf.edx, - ..Default::default() - }); - } - if config.kvm_hyperv { - // Remove conflicting entries - cpuid.retain(|c| c.function != 0x4000_0000); - cpuid.retain(|c| c.function != 0x4000_0001); - // See "Hypervisor Top Level Functional Specification" for details - // Compliance with "Hv#1" requires leaves up to 0x4000_000a - cpuid.push(CpuIdEntry { - function: 0x40000000, - eax: 0x4000000a, // Maximum cpuid leaf - ebx: 0x756e694c, // "Linu" - ecx: 0x564b2078, // "x KV" - edx: 0x7648204d, // "M Hv" - ..Default::default() - }); - cpuid.push(CpuIdEntry { - function: 0x40000001, - eax: 0x31237648, // "Hv#1" - ..Default::default() - }); - cpuid.push(CpuIdEntry { - function: 0x40000002, - eax: 0x3839, // "Build number" - ebx: 0xa0000, // "Version" - ..Default::default() - }); - cpuid.push(CpuIdEntry { - function: 0x4000_0003, - eax: (1 << 1) // AccessPartitionReferenceCounter + if config.kvm_hyperv { + // Remove conflicting entries + cpuid.retain(|c| c.function != 0x4000_0000); + cpuid.retain(|c| c.function != 0x4000_0001); + // See "Hypervisor Top Level Functional Specification" for details + // Compliance with "Hv#1" requires leaves up to 0x4000_000a + cpuid.push(CpuIdEntry { + function: 0x40000000, + eax: 0x4000000a, // Maximum cpuid leaf + ebx: 0x756e694c, // "Linu" + ecx: 0x564b2078, // "x KV" + edx: 0x7648204d, // "M Hv" + ..Default::default() + }); + cpuid.push(CpuIdEntry { + function: 0x40000001, + eax: 0x31237648, // "Hv#1" + ..Default::default() + }); + cpuid.push(CpuIdEntry { + function: 0x40000002, + eax: 0x3839, // "Build number" + ebx: 0xa0000, // "Version" + ..Default::default() + }); + cpuid.push(CpuIdEntry { + function: 0x4000_0003, + eax: (1 << 1) // AccessPartitionReferenceCounter | (1 << 2) // AccessSynicRegs | (1 << 3) // AccessSyntheticTimerRegs | (1 << 9), // AccessPartitionReferenceTsc - edx: 1 << 3, // CPU dynamic partitioning - ..Default::default() - }); - cpuid.push(CpuIdEntry { - function: 0x4000_0004, - eax: 1 << 5, // Recommend relaxed timing - ..Default::default() - }); - for i in 0x4000_0005..=0x4000_000a { + edx: 1 << 3, // CPU dynamic partitioning + ..Default::default() + }); cpuid.push(CpuIdEntry { - function: i, + function: 0x4000_0004, + eax: 1 << 5, // Recommend relaxed timing ..Default::default() }); + for i in 0x4000_0005..=0x4000_000a { + cpuid.push(CpuIdEntry { + function: i, + ..Default::default() + }); + } } } - Ok(cpuid) + if use_custom_profile { + // Final compatibility checks to ensure that the CPUID values we return are compatible both with the CPU profile and the host we are currently running on. + let host_adjusted_to_profile = host_adjusted_to_profile.expect("The profile adjusted cpuid entries should exist as we checked that we have a custom CPU profile"); + + // Check that the host's cpuid is indeed compatible with the adjusted profile. This is not by construction. + info!("checking compatibility between host adjusted to profile and the host itself"); + CpuidFeatureEntry::check_cpuid_compatibility(&host_adjusted_to_profile, &host_cpuid) + .map_err(|_| Error::CpuProfileCpuidIncompatibility)?; + Ok(host_adjusted_to_profile) + } else { + Ok(host_cpuid) + } } +/// This function computes the [`RequiredMsrUpdates`] according to the +/// given `cpu_profile`, and `kvm_hyperv` parameters. +/// +/// If [`CpuProfile::Host`] is used then this function immediately returns `Ok(None)`, +/// regardless of the other parameters. +/// +/// ## Consistency with CPUID +/// +/// Some MSRs are only present when certain related bits in CPUID leaves are. +/// The CPU profile definition ensures consistency between the MSRs it permits and the +/// CPUID adjustments it prescribes. +/// +/// There are however certain CPUID values that can be modified by the VMM independently of the +/// CPUID profile and there may be corresponding MSRs that should then not be accessible. +/// At this point in time this only concerns the KVM and Hyper-V specific CPUID leaves and we +/// assume that the end user checks CPUID before accessing any of the related MSRs for now. +// TODO: Add `cpuid: &[CpuidEntry]` as a parameter and patch the permitted MSRs accordingly +// before upstreaming. +pub fn compute_required_msr_updates( + hypervisor: &dyn hypervisor::Hypervisor, + cpu_profile: CpuProfile, + kvm_hyperv: bool, +) -> super::Result> { + let Some(data) = cpu_profile.msr_data() else { + return Ok(None); + }; + + let cpu_vendor_host = hypervisor.get_cpu_vendor(); + let cpu_vendor_profile = data.cpu_vendor; + if cpu_vendor_host != cpu_vendor_profile { + return Err(Error::CpuProfileVendorIncompatibility { + cpu_vendor_profile, + cpu_vendor_host, + } + .into()); + } + + let msr_based_features = hypervisor + .get_msr_based_features() + .map_err(Error::MsrBasedFeaturesGetSupported)?; + + let msr_index_list = hypervisor + .get_msr_index_list() + .map_err(Error::MsrIndexList)?; + + let all_host_msrs: HashSet = msr_based_features + .iter() + .map(|entry| entry.index) + .chain(msr_index_list.iter().copied()) + .collect(); + + let mut permitted_msrs: HashSet = data.permitted_msrs.iter().map(|msr| msr.0).collect(); + + if kvm_hyperv { + // Log the Hyper-V MSRs that are not in the list of permitted MSRs. + // Some of these MSRs not being permitted by the profile might be benign or even intentional, + // but it might also indicate a BUG, or misconceptions that lead to bad CPU profiles. We thus + // log this at the info level for now. + for msr in msr_definitions::hyperv::HYPERV_MSRS { + if !permitted_msrs.contains(&msr) { + info!( + "NOTE: Hyper-V MSR: {msr:#x} is not in the list of MSRs supported by the CPU profile" + ); + } + } + } else { + // Remove all HYPER-V MSRs from the list of permitted MSRs + for msr in msr_definitions::hyperv::HYPERV_MSRS { + if permitted_msrs.remove(&msr) { + trace!("Removed Hyper-V MSR {msr:#x} from the set of supported MSRs"); + } + } + } + + let forbidden_msrs: Vec = all_host_msrs + .difference(&permitted_msrs) + .map(|msr| RegisterAddress(*msr)) + .collect(); + + if (all_host_msrs.len() - forbidden_msrs.len()) != permitted_msrs.len() { + error!("Host does not have all the permitted MSRS"); + for msr in permitted_msrs.iter() { + if !all_host_msrs.contains(msr) { + error!("Host is missing the required MSR:={msr:#x}"); + } + } + Err(Error::CpuProfileMsrIncompatibility)?; + } + + // NOTE: It is fine to ignore the inner error because the called function logs any missing MSRs. + let adjusted_msr_based_features = + FeatureMsrAdjustment::adjust_to(&data.adjustments, &msr_based_features) + .map_err(|_| Error::CpuProfileMsrIncompatibility)?; + + // TODO: CPU profiles are only available for Intel CPUs at the moment. We need to branch on the vendor + // once we also have CPU profiles for AMD. + assert!(matches!(cpu_vendor_host, CpuVendor::Intel)); + crate::x86_64::msr_definitions::intel::check_feature_msr_compatibility( + &HashMap::from_iter( + adjusted_msr_based_features + .iter() + .map(|entry| (entry.index, entry.data)), + ), + &HashMap::from_iter( + msr_based_features + .iter() + .map(|entry| (entry.index, entry.data)), + ), + "CPU Profile", + "Host", + ) + .map_err(|_| { + error!("feature-based MSR compatibility check failed"); + Error::CpuProfileMsrIncompatibility + })?; + + let update = RequiredMsrUpdates { + msr_based_features: adjusted_msr_based_features, + denied_msrs: forbidden_msrs, + }; + Ok(Some(update)) +} + +#[allow(clippy::too_many_arguments)] pub fn configure_vcpu( - vcpu: &Arc, - id: u8, + vcpu: &dyn hypervisor::Vcpu, + id: u32, boot_setup: Option<(EntryPoint, &GuestMemoryAtomic)>, cpuid: Vec, + feature_msrs: &[MsrEntry], kvm_hyperv: bool, cpu_vendor: CpuVendor, - topology: Option<(u8, u8, u8)>, + topology: (u16, u16, u16, u16), + nested: bool, + setup_registers: bool, ) -> super::Result<()> { - let x2apic_id = get_x2apic_id(id as u32, topology); + let x2apic_id = get_x2apic_id(id, Some(topology)); // Per vCPU CPUID changes; common are handled via generate_common_cpuid() let mut cpuid = cpuid; @@ -844,45 +1140,50 @@ pub fn configure_vcpu( entry.ebx &= 0xffffff; entry.ebx |= x2apic_id << 24; apic_id_patched = true; + if matches!(cpu_vendor, CpuVendor::Intel) { + if !nested { + // Disable nested virtualization for Intel + entry.ecx &= !(1 << VMX_ECX_BIT); + } + break; + } + } + if entry.function == 0x8000_0001 { + if !nested { + // Disable the nested virtualization for AMD + entry.ecx &= !(1 << SVM_ECX_BIT); + } break; } } assert!(apic_id_patched); - if let Some(t) = topology { - update_cpuid_topology(&mut cpuid, t.0, t.1, t.2, cpu_vendor, id); - } + update_cpuid_topology( + &mut cpuid, topology.0, topology.1, topology.2, topology.3, cpu_vendor, id, + ); // The TSC frequency CPUID leaf should not be included when running with HyperV emulation - if !kvm_hyperv { - if let Some(tsc_khz) = vcpu.tsc_khz().map_err(Error::GetTscFrequency)? { - // Need to check that the TSC doesn't vary with dynamic frequency - // SAFETY: cpuid called with valid leaves - if unsafe { std::arch::x86_64::__cpuid(0x8000_0007) }.edx - & (1u32 << INVARIANT_TSC_EDX_BIT) - > 0 - { - CpuidPatch::set_cpuid_reg( - &mut cpuid, - 0x4000_0000, - None, - CpuidReg::EAX, - 0x4000_0010, - ); - cpuid.retain(|c| c.function != 0x4000_0010); - cpuid.push(CpuIdEntry { - function: 0x4000_0010, - eax: tsc_khz, - ebx: 1000000, /* LAPIC resolution of 1ns (freq: 1GHz) is hardcoded in KVM's - * APIC_BUS_CYCLE_NS */ - ..Default::default() - }); - }; + if !kvm_hyperv && let Some(tsc_khz) = vcpu.tsc_khz().map_err(Error::GetTscFrequency)? { + // Need to check that the TSC doesn't vary with dynamic frequency + #[allow(unused_unsafe)] + // SAFETY: cpuid called with valid leaves + if unsafe { std::arch::x86_64::__cpuid(0x8000_0007) }.edx & (1u32 << INVARIANT_TSC_EDX_BIT) + > 0 + { + CpuidPatch::set_cpuid_reg(&mut cpuid, 0x4000_0000, None, CpuidReg::EAX, 0x4000_0010); + cpuid.retain(|c| c.function != 0x4000_0010); + cpuid.push(CpuIdEntry { + function: 0x4000_0010, + eax: tsc_khz, + ebx: 1000000, /* LAPIC resolution of 1ns (freq: 1GHz) is hardcoded in KVM's + * APIC_BUS_CYCLE_NS */ + ..Default::default() + }); } } for c in &cpuid { - debug!("{}", c); + debug!("{c}"); } vcpu.set_cpuid2(&cpuid) @@ -892,11 +1193,21 @@ pub fn configure_vcpu( vcpu.enable_hyperv_synic().unwrap(); } - regs::setup_msrs(vcpu).map_err(Error::MsrsConfiguration)?; + regs::setup_msrs(vcpu, feature_msrs).map_err(Error::MsrsConfiguration)?; if let Some((kernel_entry_point, guest_memory)) = boot_setup { - regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?; + if setup_registers { + regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?; + + // CPUs are required (by Intel sdm spec) to boot in x2apic mode if any + // of the apic IDs is larger than 255. Experimentally, the Linux kernel + // does not recognize the last vCPU if x2apic is not enabled when + // there are 256 vCPUs in a flat hierarchy (i.e. max x2apic ID is 255), + // so we need to enable x2apic in this case as well. + let enable_x2_apic_mode = get_max_x2apic_id(topology) > MAX_SUPPORTED_CPUS_LEGACY; + regs::setup_sregs(&guest_memory.memory(), vcpu, enable_x2_apic_mode) + .map_err(Error::SregsConfiguration)?; + } regs::setup_fpu(vcpu).map_err(Error::FpuConfiguration)?; - regs::setup_sregs(&guest_memory.memory(), vcpu).map_err(Error::SregsConfiguration)?; } interrupts::set_lint(vcpu).map_err(|e| Error::LocalIntConfiguration(e.into()))?; Ok(()) @@ -946,22 +1257,18 @@ pub fn configure_system( cmdline_addr: GuestAddress, cmdline_size: usize, initramfs: &Option, - _num_cpus: u8, + _num_cpus: u32, setup_header: Option, rsdp_addr: Option, - sgx_epc_region: Option, - serial_number: Option<&str>, - uuid: Option<&str>, - oem_strings: Option<&[&str]>, - topology: Option<(u8, u8, u8)>, + smbios: Option<&SmbiosConfig>, + topology: Option<(u16, u16, u16, u16)>, ) -> super::Result<()> { // Write EBDA address to location where ACPICA expects to find it guest_mem .write_obj((layout::EBDA_START.0 >> 4) as u16, layout::EBDA_POINTER) .map_err(Error::EbdaSetup)?; - let size = smbios::setup_smbios(guest_mem, serial_number, uuid, oem_strings) - .map_err(Error::SmbiosSetup)?; + let size = smbios::setup_smbios(guest_mem, smbios).map_err(Error::SmbiosSetup)?; // Place the MP table after the SMIOS table aligned to 16 bytes let offset = GuestAddress(layout::SMBIOS_START).unchecked_add(size); @@ -969,10 +1276,10 @@ pub fn configure_system( mptable::setup_mptable(offset, guest_mem, _num_cpus, topology).map_err(Error::MpTableSetup)?; // Check that the RAM is not smaller than the RSDP start address - if let Some(rsdp_addr) = rsdp_addr { - if rsdp_addr.0 > guest_mem.last_addr().0 { - return Err(super::Error::RsdpPastRamEnd); - } + if let Some(rsdp_addr) = rsdp_addr + && rsdp_addr.0 > guest_mem.last_addr().0 + { + return Err(super::Error::RsdpPastRamEnd); } match setup_header { @@ -983,15 +1290,8 @@ pub fn configure_system( initramfs, hdr, rsdp_addr, - sgx_epc_region, - ), - None => configure_pvh( - guest_mem, - cmdline_addr, - initramfs, - rsdp_addr, - sgx_epc_region, ), + None => configure_pvh(guest_mem, cmdline_addr, initramfs, rsdp_addr), } } @@ -1048,17 +1348,15 @@ pub fn generate_ram_ranges(guest_mem: &GuestMemoryMmap) -> super::Result, rsdp_addr: Option, - sgx_epc_region: Option, ) -> super::Result<()> { const XEN_HVM_START_MAGIC_VALUE: u32 = 0x336ec578; @@ -1149,15 +1446,6 @@ fn configure_pvh( E820_RESERVED, ); - if let Some(sgx_epc_region) = sgx_epc_region { - add_memmap_entry( - &mut memmap, - sgx_epc_region.start().raw_value(), - sgx_epc_region.size(), - E820_RESERVED, - ); - } - start_info.memmap_entries = memmap.len() as u32; // Copy the vector with the memmap table to the MEMMAP_START address @@ -1204,7 +1492,6 @@ fn configure_32bit_entry( initramfs: &Option, setup_hdr: setup_header, rsdp_addr: Option, - sgx_epc_region: Option, ) -> super::Result<()> { const KERNEL_LOADER_OTHER: u8 = 0xff; @@ -1260,15 +1547,6 @@ fn configure_32bit_entry( E820_RESERVED, )?; - if let Some(sgx_epc_region) = sgx_epc_region { - add_e820_entry( - &mut params, - sgx_epc_region.start().raw_value(), - sgx_epc_region.size(), - E820_RESERVED, - )?; - } - if let Some(rsdp_addr) = rsdp_addr { params.acpi_rsdp_addr = rsdp_addr.0; } @@ -1333,8 +1611,9 @@ pub fn initramfs_load_addr( Ok(aligned_addr) } -pub fn get_host_cpu_phys_bits(hypervisor: &Arc) -> u8 { +pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 { // SAFETY: call cpuid with valid leaves + #[allow(unused_unsafe)] unsafe { let leaf = x86_64::__cpuid(0x8000_0000); @@ -1361,30 +1640,35 @@ pub fn get_host_cpu_phys_bits(hypervisor: &Arc) -> u fn update_cpuid_topology( cpuid: &mut Vec, - threads_per_core: u8, - cores_per_die: u8, - dies_per_package: u8, + threads_per_core: u16, + cores_per_die: u16, + dies_per_package: u16, + packages: u16, cpu_vendor: CpuVendor, - id: u8, + id: u32, ) { let x2apic_id = get_x2apic_id( - id as u32, - Some((threads_per_core, cores_per_die, dies_per_package)), + id, + Some((threads_per_core, cores_per_die, dies_per_package, packages)), ); - let thread_width = 8 - (threads_per_core - 1).leading_zeros(); - let core_width = (8 - (cores_per_die - 1).leading_zeros()) + thread_width; - let die_width = (8 - (dies_per_package - 1).leading_zeros()) + core_width; + // Note: the topology defined here is per "package" (~NUMA node). + let thread_width = u16::BITS - (threads_per_core - 1).leading_zeros(); + let core_width = u16::BITS - (cores_per_die - 1).leading_zeros() + thread_width; + let die_width = u16::BITS - (dies_per_package - 1).leading_zeros() + core_width; + // The very old way: a flat number of logical CPUs per package: CPUID.1H:EBX[23:16] bits. + let core_count = dies_per_package as u32 * cores_per_die as u32 * threads_per_core as u32; let mut cpu_ebx = CpuidPatch::get_cpuid_reg(cpuid, 0x1, None, CpuidReg::EBX).unwrap_or(0); - cpu_ebx |= ((dies_per_package as u32) * (cores_per_die as u32) * (threads_per_core as u32)) - & (0xff << 16); + cpu_ebx &= !(0xff << 16); + cpu_ebx |= (core_count & 0xff) << 16; CpuidPatch::set_cpuid_reg(cpuid, 0x1, None, CpuidReg::EBX, cpu_ebx); let mut cpu_edx = CpuidPatch::get_cpuid_reg(cpuid, 0x1, None, CpuidReg::EDX).unwrap_or(0); cpu_edx |= 1 << 28; CpuidPatch::set_cpuid_reg(cpuid, 0x1, None, CpuidReg::EDX, cpu_edx); + // The legacy way: threads+cores per package. // CPU Topology leaf 0xb CpuidPatch::set_cpuid_reg(cpuid, 0xb, Some(0), CpuidReg::EAX, thread_width); CpuidPatch::set_cpuid_reg( @@ -1407,6 +1691,7 @@ fn update_cpuid_topology( CpuidPatch::set_cpuid_reg(cpuid, 0xb, Some(1), CpuidReg::ECX, 2 << 8); CpuidPatch::set_cpuid_reg(cpuid, 0xb, Some(1), CpuidReg::EDX, x2apic_id); + // The modern way: many-level hierarchy (but we here only support four levels). // CPU Topology leaf 0x1f CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(0), CpuidReg::EAX, thread_width); CpuidPatch::set_cpuid_reg( @@ -1417,6 +1702,7 @@ fn update_cpuid_topology( u32::from(threads_per_core), ); CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(0), CpuidReg::ECX, 1 << 8); + CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(0), CpuidReg::EDX, x2apic_id); CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(1), CpuidReg::EAX, core_width); CpuidPatch::set_cpuid_reg( @@ -1427,6 +1713,7 @@ fn update_cpuid_topology( u32::from(cores_per_die * threads_per_core), ); CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(1), CpuidReg::ECX, 2 << 8); + CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(1), CpuidReg::EDX, x2apic_id); CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(2), CpuidReg::EAX, die_width); CpuidPatch::set_cpuid_reg( @@ -1437,6 +1724,7 @@ fn update_cpuid_topology( u32::from(dies_per_package * cores_per_die * threads_per_core), ); CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(2), CpuidReg::ECX, 5 << 8); + CpuidPatch::set_cpuid_reg(cpuid, 0x1f, Some(2), CpuidReg::EDX, x2apic_id); if matches!(cpu_vendor, CpuVendor::AMD) { CpuidPatch::set_cpuid_reg( @@ -1483,7 +1771,7 @@ fn update_cpuid_topology( edx_bit: Some(28), }, ]; - CpuidPatch::patch_cpuid(cpuid, cpuid_patches); + CpuidPatch::patch_cpuid(cpuid, &cpuid_patches); CpuidPatch::set_cpuid_reg( cpuid, 0x8000_0008, @@ -1497,59 +1785,8 @@ fn update_cpuid_topology( } } } - -// The goal is to update the CPUID sub-leaves to reflect the number of EPC -// sections exposed to the guest. -fn update_cpuid_sgx( - cpuid: &mut Vec, - epc_sections: &[SgxEpcSection], -) -> Result<(), Error> { - // Something's wrong if there's no EPC section. - if epc_sections.is_empty() { - return Err(Error::NoSgxEpcSection); - } - // We can't go further if the hypervisor does not support SGX feature. - if !CpuidPatch::is_feature_enabled(cpuid, 0x7, 0, CpuidReg::EBX, 2) { - return Err(Error::MissingSgxFeature); - } - // We can't go further if the hypervisor does not support SGX_LC feature. - if !CpuidPatch::is_feature_enabled(cpuid, 0x7, 0, CpuidReg::ECX, 30) { - return Err(Error::MissingSgxLaunchControlFeature); - } - - // Get host CPUID for leaf 0x12, subleaf 0x2. This is to retrieve EPC - // properties such as confidentiality and integrity. - // SAFETY: call cpuid with valid leaves - let leaf = unsafe { std::arch::x86_64::__cpuid_count(0x12, 0x2) }; - - for (i, epc_section) in epc_sections.iter().enumerate() { - let subleaf_idx = i + 2; - let start = epc_section.start().raw_value(); - let size = epc_section.size(); - let eax = (start & 0xffff_f000) as u32 | 0x1; - let ebx = (start >> 32) as u32; - let ecx = (size & 0xffff_f000) as u32 | (leaf.ecx & 0xf); - let edx = (size >> 32) as u32; - // CPU Topology leaf 0x12 - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::EAX, eax); - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::EBX, ebx); - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::ECX, ecx); - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::EDX, edx); - } - - // Add one NULL entry to terminate the dynamic list - let subleaf_idx = epc_sections.len() + 2; - // CPU Topology leaf 0x12 - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::EAX, 0); - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::EBX, 0); - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::ECX, 0); - CpuidPatch::set_cpuid_reg(cpuid, 0x12, Some(subleaf_idx as u32), CpuidReg::EDX, 0); - - Ok(()) -} - #[cfg(test)] -mod tests { +mod unit_tests { use linux_loader::loader::bootparam::boot_e820_entry; use super::*; @@ -1576,9 +1813,6 @@ mod tests { Some(layout::RSDP_POINTER), None, None, - None, - None, - None, ); config_err.unwrap_err(); @@ -1601,9 +1835,6 @@ mod tests { None, None, None, - None, - None, - None, ) .unwrap(); @@ -1631,9 +1862,6 @@ mod tests { None, None, None, - None, - None, - None, ) .unwrap(); @@ -1647,9 +1875,6 @@ mod tests { None, None, None, - None, - None, - None, ) .unwrap(); } @@ -1721,22 +1946,27 @@ mod tests { #[test] fn test_get_x2apic_id() { - let x2apic_id = get_x2apic_id(0, Some((2, 3, 1))); + let x2apic_id = get_x2apic_id(0, Some((2, 3, 1, 1))); assert_eq!(x2apic_id, 0); - let x2apic_id = get_x2apic_id(1, Some((2, 3, 1))); + let x2apic_id = get_x2apic_id(1, Some((2, 3, 1, 1))); assert_eq!(x2apic_id, 1); - let x2apic_id = get_x2apic_id(2, Some((2, 3, 1))); + let x2apic_id = get_x2apic_id(2, Some((2, 3, 1, 1))); assert_eq!(x2apic_id, 2); - let x2apic_id = get_x2apic_id(6, Some((2, 3, 1))); + let x2apic_id = get_x2apic_id(6, Some((2, 3, 1, 1))); assert_eq!(x2apic_id, 8); - let x2apic_id = get_x2apic_id(7, Some((2, 3, 1))); + let x2apic_id = get_x2apic_id(7, Some((2, 3, 1, 1))); assert_eq!(x2apic_id, 9); - let x2apic_id = get_x2apic_id(8, Some((2, 3, 1))); + let x2apic_id = get_x2apic_id(8, Some((2, 3, 1, 1))); assert_eq!(x2apic_id, 10); + + let x2apic_id = get_x2apic_id(257, Some((1, 312, 1, 1))); + assert_eq!(x2apic_id, 257); + + assert_eq!(255, get_max_x2apic_id((1, 256, 1, 1))); } } diff --git a/arch/src/x86_64/mptable.rs b/arch/src/x86_64/mptable.rs index aaf6f1ddd7..203d55fa27 100644 --- a/arch/src/x86_64/mptable.rs +++ b/arch/src/x86_64/mptable.rs @@ -8,12 +8,14 @@ use std::{mem, result, slice}; use libc::c_uchar; +use log::{info, warn}; use thiserror::Error; use vm_memory::{Address, ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError}; +use super::MAX_SUPPORTED_CPUS_LEGACY; +use crate::GuestMemoryMmap; use crate::layout::{APIC_START, HIGH_RAM_START, IOAPIC_START}; use crate::x86_64::{get_x2apic_id, mpspec}; -use crate::GuestMemoryMmap; // This is a workaround to the Rust enforcement specifying that any implementation of a foreign // trait (in this case `ByteValued`) where: @@ -61,9 +63,6 @@ pub enum Error { /// Failure while zeroing out the memory for the MP table. #[error("Failure while zeroing out the memory for the MP table")] Clear(#[source] GuestMemoryError), - /// Number of CPUs exceeds the maximum supported CPUs - #[error("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), @@ -89,11 +88,6 @@ pub enum Error { pub type Result = result::Result; -// 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; - // 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"; @@ -107,8 +101,9 @@ const CPU_FEATURE_APIC: u32 = 0x200; const CPU_FEATURE_FPU: u32 = 0x001; fn compute_checksum(v: &T) -> u8 { + let v: *const T = v; // SAFETY: we are only reading the bytes within the size of the `T` reference `v`. - let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::()) }; + let v_slice = unsafe { slice::from_raw_parts(v.cast(), mem::size_of::()) }; let mut checksum: u8 = 0; for i in v_slice.iter() { checksum = checksum.wrapping_add(*i); @@ -121,7 +116,7 @@ fn mpf_intel_compute_checksum(v: &mpspec::mpf_intel) -> u8 { (!checksum).wrapping_add(1) } -fn compute_mp_size(num_cpus: u8) -> usize { +fn compute_mp_size(num_cpus: u32) -> usize { mem::size_of::() + mem::size_of::() + mem::size_of::() * (num_cpus as usize) @@ -135,14 +130,15 @@ fn compute_mp_size(num_cpus: u8) -> usize { pub fn setup_mptable( offset: GuestAddress, mem: &GuestMemoryMmap, - num_cpus: u8, - topology: Option<(u8, u8, u8)>, + 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.into(), topology); - if x2apic_id_max >= MAX_SUPPORTED_CPUS { - return Err(Error::TooManyCpus); + 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(()); } } @@ -157,7 +153,7 @@ pub fn setup_mptable( } let mut checksum: u8 = 0; - let ioapicid: u8 = MAX_SUPPORTED_CPUS as u8 + 1; + let ioapicid: u8 = MAX_SUPPORTED_CPUS_LEGACY as u8 + 1; // The checked_add here ensures the all of the following base_mp.unchecked_add's will be without // overflow. @@ -195,7 +191,7 @@ pub fn setup_mptable( 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 as u32, topology) as u8; + mpc_cpu.0.apicid = get_x2apic_id(cpu_id, topology) as u8; mpc_cpu.0.apicver = APIC_VERSION; mpc_cpu.0.cpuflag = mpspec::CPU_ENABLED as u8 | if cpu_id == 0 { @@ -303,7 +299,7 @@ pub fn setup_mptable( } #[cfg(test)] -mod tests { +mod unit_tests { use vm_memory::bitmap::BitmapSlice; use vm_memory::{GuestUsize, VolatileMemoryError, VolatileSlice, WriteVolatile}; @@ -394,11 +390,11 @@ mod tests { fn cpu_entry_count() { let mem = GuestMemoryMmap::from_ranges(&[( MPTABLE_START, - compute_mp_size(MAX_SUPPORTED_CPUS as u8), + compute_mp_size(MAX_SUPPORTED_CPUS_LEGACY), )]) .unwrap(); - for i in 0..MAX_SUPPORTED_CPUS as u8 { + for i in 0..MAX_SUPPORTED_CPUS_LEGACY { setup_mptable(MPTABLE_START, &mem, i, None).unwrap(); let mpf_intel: MpfIntelWrapper = mem.read_obj(MPTABLE_START).unwrap(); @@ -428,11 +424,9 @@ mod tests { #[test] fn cpu_entry_count_max() { - let cpus = MAX_SUPPORTED_CPUS + 1; - let mem = - GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(cpus as u8))]).unwrap(); + let cpus = MAX_SUPPORTED_CPUS_LEGACY + 1; + let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(cpus))]).unwrap(); - let result = setup_mptable(MPTABLE_START, &mem, cpus as u8, None); - result.unwrap_err(); + setup_mptable(MPTABLE_START, &mem, cpus, None).unwrap(); } } diff --git a/arch/src/x86_64/msr_definitions/hyperv.rs b/arch/src/x86_64/msr_definitions/hyperv.rs new file mode 100644 index 0000000000..8d5b6577e4 --- /dev/null +++ b/arch/src/x86_64/msr_definitions/hyperv.rs @@ -0,0 +1,169 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This module exports a list of all known Hyper-V MSRs that we found in Appendix F in +//! the Microsoft Hypervisor Top Level Functional Specification document from February 2017. + +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_POWER_STATE_TRIGGER_C1: u32 = 0x400000C1; +const HV_X64_MSR_POWER_STATE_TRIGGER_C2: u32 = 0x400000C2; +const HV_X64_MSR_POWER_STATE_TRIGGER_C3: u32 = 0x400000C3; +const HV_X64_MSR_POWER_STATE_CONFIG_C1: u32 = 0x400000D1; +const HV_X64_MSR_POWER_STATE_CONFIG_C2: u32 = 0x400000D2; +const HV_X64_MSR_POWER_STATE_CONFIG_C3: u32 = 0x400000D3; +const HV_X64_MSR_STATS_PARTITION_RETAIL_PAGE: u32 = 0x400000E0; +const HV_X64_MSR_STATS_PARTITION_INTERNAL_PAGE: u32 = 0x400000E1; +const HV_X64_MSR_STATS_VP_RETAIL_PAGE: u32 = 0x400000E2; +const HV_X64_MSR_STATS_VP_INTERNAL_PAGE: u32 = 0x400000E3; +const HV_X64_MSR_GUEST_IDLE: u32 = 0x400000F0; +const HV_X64_MSR_SYNTH_DEBUG_CONTROL: u32 = 0x400000F1; +const HV_X64_MSR_SYNTH_DEBUG_STATUS: u32 = 0x400000F2; +const HV_X64_MSR_SYNTH_DEBUG_SEND_BUFFER: u32 = 0x400000F3; +const HV_X64_MSR_SYNTH_DEBUG_RECEIVE_BUFFER: u32 = 0x400000F4; +const HV_X64_MSR_SYNTH_DEBUG_PENDING_BUFFER: u32 = 0x400000F5; +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; + +/// This is a list of all Hyper-V MSRs that we found in Appendix F in the Microsoft +/// Hypervisor Top Level Functional Specification document from February 2017 +pub(in crate::x86_64) const HYPERV_MSRS: [u32; 64] = [ + 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_POWER_STATE_TRIGGER_C1, + HV_X64_MSR_POWER_STATE_TRIGGER_C2, + HV_X64_MSR_POWER_STATE_TRIGGER_C3, + HV_X64_MSR_POWER_STATE_CONFIG_C1, + HV_X64_MSR_POWER_STATE_CONFIG_C2, + HV_X64_MSR_POWER_STATE_CONFIG_C3, + HV_X64_MSR_STATS_PARTITION_RETAIL_PAGE, + HV_X64_MSR_STATS_PARTITION_INTERNAL_PAGE, + HV_X64_MSR_STATS_VP_RETAIL_PAGE, + HV_X64_MSR_STATS_VP_INTERNAL_PAGE, + HV_X64_MSR_GUEST_IDLE, + HV_X64_MSR_SYNTH_DEBUG_CONTROL, + HV_X64_MSR_SYNTH_DEBUG_STATUS, + HV_X64_MSR_SYNTH_DEBUG_SEND_BUFFER, + HV_X64_MSR_SYNTH_DEBUG_RECEIVE_BUFFER, + HV_X64_MSR_SYNTH_DEBUG_PENDING_BUFFER, + 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, +]; + +#[cfg(all(test, feature = "kvm", feature = "cpu_profile_generation"))] +mod tests { + use super::*; + use crate::x86_64::msr_definitions::intel::{ + INTEL_MSR_FEATURE_DEFINITIONS, PERMITTED_IA32_MSRS, + }; + use crate::x86_64::msr_definitions::kvm::PROFILE_PERMITTED_KVM_MSRS; + + // If this can be assumed than that simplifies some things. + // + // NOTE: It is perfectly possible to make this a compile time check instead, + // but that is more cumbersome hence we leave that for later. + #[test] + fn does_not_intersect_other_permitted_msr_sets() { + for msr in HYPERV_MSRS { + assert!( + !INTEL_MSR_FEATURE_DEFINITIONS + .as_slice() + .iter() + .map(|r| r.0.0) + .chain(PERMITTED_IA32_MSRS) + .chain(PROFILE_PERMITTED_KVM_MSRS) + .any(|other_permitted_msr| other_permitted_msr == msr) + ); + } + } +} diff --git a/arch/src/x86_64/msr_definitions/intel/architectural_msrs.rs b/arch/src/x86_64/msr_definitions/intel/architectural_msrs.rs new file mode 100644 index 0000000000..af7b4e7cc0 --- /dev/null +++ b/arch/src/x86_64/msr_definitions/intel/architectural_msrs.rs @@ -0,0 +1,1654 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// +//! This module contains lists of architectural MSRs (or more accurately MSR register addresses) that +//! are permitted and forbidden for use with CPU profiles. +//! +//! The CPU profile generation tool obtains all MSRS supported by both KVM and the hardware +//! when it runs and uses the permitted list to only record those that are permitted. +//! +//! The list of forbidden architectural MSRs is only used to rule out "false" new MSRs that otherwise +//! would require updating the CPU profile generation tool. + +// We occasionally write doc comments for constants that are defined in private modules. This +// is still helpful for developers as the LSP can then provide information about the constants +// directly at the site(s) where they are being used. +#![allow(unused_doc_comments)] + +pub(in crate::x86_64) use forbidden_architectural_msrs::FORBIDDEN_IA32_MSR_RANGES; +pub(in crate::x86_64) use permitted_architectural_msrs::PERMITTED_IA32_MSRS; + +use crate::x86_64::CpuidReg; +use crate::x86_64::cpuid_definitions::Parameters; +use crate::x86_64::cpuid_definitions::intel::assert_not_denied_cpuid_feature; + +mod permitted_architectural_msrs { + use read_only::READ_ONLY_IA32_MSRS; + use read_write::READ_WRITE_IA32_MSRS; + use write_only::WRITE_ONLY_IA32_MSRS; + + use super::{CpuidReg, Parameters}; + use crate::x86_64::msr_definitions::intel::architectural_msrs::assert_not_denied_cpuid_feature; + + mod read_only { + use super::{CpuidReg, Parameters, assert_not_denied_cpuid_feature}; + /// (R/O) + const IA32_BARRIER: u32 = 0x2f; + const _IA32_BARRIER_CPUID_CHECK: () = const { + assert_not_denied_cpuid_feature::<27>(&Parameters { + leaf: 0x7, + sub_leaf: 0..=0, + register: CpuidReg::EAX, + }); + }; + + /// MTRR Capability (R/O) + const IA32_MTRRCAP: u32 = 0xfe; + + // TODO: Not sure whether the IA32_FZM_* msrs should be permitted + const IA32_FZM_DOMAIN_CONFIG: u32 = 0x83; + const IA32_FZM_RANGE_STARTADDR: u32 = 0x84; + const IA32_FZM_RANGE_ENDADDR: u32 = 0x85; + const IA32_FZM_RANGE_WRITESTATUS: u32 = 0x86; + // NOTE: This is permitted, but will be zeroed out for all non-host CPU profiles. + const IA32_MCG_CAP: u32 = 0x179; + + /// DCA Capability (R) + const IA32_PLATFORM_DCA_CAP: u32 = 0x1f8; + /// If set, CPU supports Prefetch-Hint type + const IA32_CPU_DCA_CAP: u32 = 0x1f9; + + const _IA32_DCA_CAP_CPUID_CHECK: () = assert_not_denied_cpuid_feature::<18>(&Parameters { + leaf: 0x1, + sub_leaf: 0..=0, + register: CpuidReg::ECX, + }); + + // TODO: Can we rather place this MSR in the deny list? + const IA32_MCU_STAGING_MBOX_ADDR: u32 = 0x7a5; + + // NOTE: THE X2APIC related MSRs cannot be filtered by KVM, but we include them here anyway for completeness sake. + const IA32_X2APIC_APICID: u32 = 0x802; + const IA32_X2APIC_VERSION: u32 = 0x803; + const IA32_X2APIC_PPR: u32 = 0x80a; + const IA32_X2APIC_LDR: u32 = 0x80d; + const IA32_X2APIC_ISR0: u32 = 0x810; + const IA32_X2APIC_ISR1: u32 = 0x811; + const IA32_X2APIC_ISR2: u32 = 0x812; + + const IA32_X2APIC_ISR3: u32 = 0x813; + const IA32_X2APIC_ISR4: u32 = 0x814; + const IA32_X2APIC_ISR5: u32 = 0x815; + const IA32_X2APIC_ISR6: u32 = 0x816; + const IA32_X2APIC_ISR7: u32 = 0x817; + const IA32_X2APIC_TMR0: u32 = 0x818; + const IA32_X2APIC_TMR1: u32 = 0x819; + const IA32_X2APIC_TMR2: u32 = 0x81a; + const IA32_X2APIC_TMR3: u32 = 0x81b; + const IA32_X2APIC_TMR4: u32 = 0x81c; + const IA32_X2APIC_TMR5: u32 = 0x81d; + const IA32_X2APIC_TMR6: u32 = 0x81e; + const IA32_X2APIC_TMR7: u32 = 0x81f; + const IA32_X2APIC_IRR0: u32 = 0x820; + const IA32_X2APIC_IRR1: u32 = 0x821; + const IA32_X2APIC_IRR2: u32 = 0x822; + const IA32_X2APIC_IRR3: u32 = 0x823; + const IA32_X2APIC_IRR4: u32 = 0x824; + const IA32_X2APIC_IRR5: u32 = 0x825; + const IA32_X2APIC_IRR6: u32 = 0x826; + const IA32_X2APIC_IRR7: u32 = 0x827; + const IA32_X2APIC_CUR_COUNT: u32 = 0x839; + + pub(super) const READ_ONLY_IA32_MSRS: [u32; 39] = [ + IA32_BARRIER, + IA32_MTRRCAP, + IA32_FZM_DOMAIN_CONFIG, + IA32_FZM_RANGE_STARTADDR, + IA32_FZM_RANGE_ENDADDR, + IA32_FZM_RANGE_WRITESTATUS, + IA32_MCG_CAP, + IA32_PLATFORM_DCA_CAP, + IA32_CPU_DCA_CAP, + IA32_MCU_STAGING_MBOX_ADDR, + IA32_X2APIC_APICID, + IA32_X2APIC_VERSION, + IA32_X2APIC_PPR, + IA32_X2APIC_LDR, + IA32_X2APIC_ISR0, + IA32_X2APIC_ISR1, + IA32_X2APIC_ISR2, + IA32_X2APIC_ISR3, + IA32_X2APIC_ISR4, + IA32_X2APIC_ISR5, + IA32_X2APIC_ISR6, + IA32_X2APIC_ISR7, + IA32_X2APIC_TMR0, + IA32_X2APIC_TMR1, + IA32_X2APIC_TMR2, + IA32_X2APIC_TMR3, + IA32_X2APIC_TMR4, + IA32_X2APIC_TMR5, + IA32_X2APIC_TMR6, + IA32_X2APIC_TMR7, + IA32_X2APIC_IRR0, + IA32_X2APIC_IRR1, + IA32_X2APIC_IRR2, + IA32_X2APIC_IRR3, + IA32_X2APIC_IRR4, + IA32_X2APIC_IRR5, + IA32_X2APIC_IRR6, + IA32_X2APIC_IRR7, + IA32_X2APIC_CUR_COUNT, + ]; + } + + mod read_write { + use super::{CpuidReg, Parameters, assert_not_denied_cpuid_feature}; + + const IA32_TIME_STAMP_COUNTER: u32 = 0x10; + + const IA32_APIC_BASE: u32 = 0x1b; + + const IA32_FEATURE_CONTROL: u32 = 0x3a; + + /// Per Logical Processor TSC Adjust (R/Write to clear) + const IA32_TSC_ADJUST: u32 = 0x3b; + const _IA32_TSC_ADJUST_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<1>(&Parameters { + leaf: 0x7, + sub_leaf: 0..=0, + register: CpuidReg::EBX, + }); + + const IA32_SPEC_CTRL: u32 = 0x48; + const _IA32_SPECT_CTRL_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<26>(&Parameters { + leaf: 0x7, + sub_leaf: 0..=0, + register: CpuidReg::EDX, + }); + + const IA32_MCU_OPT_CTRL: u32 = 0x123; + const _IA32_MCU_OPT_CTRL_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<9>(&Parameters { + leaf: 0x7, + sub_leaf: (0..=0), + register: CpuidReg::EDX, + }); + + /// SYSENTER_CS_MSR + const IA32_SYSENTER_CS: u32 = 0x174; + + /// SYSENTER_ESP_MSR + const IA32_SYSENTER_ESP: u32 = 0x175; + + /// SYSENTER_ESP_MSR + const IA32_SYSENTER_EIP: u32 = 0x176; + + // Technically permitted (as users will expect it given that MCA is available via CPUID), + // but probably not very useful since IA32_MCG_CAP will be zeroed out for all non-host + // CPU profiles + const IA32_MCG_STATUS: u32 = 0x17a; + + // TODO: Does it really make sense to permit this MSR? + const IA32_SMM_MONITOR_CTL: u32 = 0x9b; + const _IA32_SMM_MONITOR_CTL_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<5>(&Parameters { + leaf: 0x1, + sub_leaf: 0..=0, + register: CpuidReg::ECX, + }); + + /// Enable Misc. Processr Features + const IA32_MISC_ENABLE: u32 = 0x1a0; + + const IA32_XFD: u32 = 0x1c4; + const IA32_XFD_ERR: u32 = 0x1c5; + + const IA32_DCA_0_CAP: u32 = 0x1fa; + + const _IA32_DCA_0_CAP_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<18>(&Parameters { + leaf: 0x1, + sub_leaf: 0..=0, + register: CpuidReg::ECX, + }); + + const IA32_MTRR_PHYSBASE0: u32 = 0x200; + const IA32_MTRR_PHYSMASK0: u32 = 0x201; + const IA32_MTRR_PHYSBASE1: u32 = 0x202; + const IA32_MTRR_PHYSMASK1: u32 = 0x203; + const IA32_MTRR_PHYSBASE2: u32 = 0x204; + const IA32_MTRR_PHYSMASK2: u32 = 0x205; + const IA32_MTRR_PHYSBASE3: u32 = 0x206; + const IA32_MTRR_PHYSMASK3: u32 = 0x207; + const IA32_MTRR_PHYSBASE4: u32 = 0x208; + const IA32_MTRR_PHYSMASK4: u32 = 0x209; + const IA32_MTRR_PHYSBASE5: u32 = 0x20a; + const IA32_MTRR_PHYSMASK5: u32 = 0x20b; + const IA32_MTRR_PHYSBASE6: u32 = 0x20c; + const IA32_MTRR_PHYSMASK6: u32 = 0x20d; + const IA32_MTRR_PHYSBASE7: u32 = 0x20e; + const IA32_MTRR_PHYSMASK7: u32 = 0x20f; + const IA32_MTRR_PHYSBASE8: u32 = 0x210; + const IA32_MTRR_PHYSMASK8: u32 = 0x211; + const IA32_MTRR_PHYSBASE9: u32 = 0x212; + const IA32_MTRR_PHYSMASK9: u32 = 0x213; + + const IA32_MTRR_FIX64K_00000: u32 = 0x250; + const IA32_MTRR_FIX16K_80000: u32 = 0x258; + const IA32_MTRR_FIX16K_A0000: u32 = 0x259; + const IA32_MTRR_FIX4K_C0000: u32 = 0x268; + const IA32_MTRR_FIX4K_C8000: u32 = 0x269; + const IA32_MTRR_FIX4K_D0000: u32 = 0x26a; + const IA32_MTRR_FIX4K_D8000: u32 = 0x26b; + const IA32_MTRR_FIX4K_E0000: u32 = 0x26c; + const IA32_MTRR_FIX4K_E8000: u32 = 0x26d; + const IA32_MTRR_FIX4K_F0000: u32 = 0x26e; + const IA32_MTRR_FIX4K_F8000: u32 = 0x26f; + + const _IA32_MTRR_FIX_I_X_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<12>(&Parameters { + leaf: 0x1, + sub_leaf: 0..=0, + register: CpuidReg::EDX, + }); + + const IA32_PAT: u32 = 0x277; + const _IA32_PAT_CPUID_CHECK: () = assert_not_denied_cpuid_feature::<16>(&Parameters { + leaf: 0x1, + sub_leaf: 0..=0, + register: CpuidReg::EDX, + }); + + const IA32_MTRR_DEF_TYPE: u32 = 0x2ff; + + // Error reporting banks. KVM always reports 32 + // of them by default. + // TODO: Consider conditionally compiling this based + // on whether we are using KVM + const IA32_MC0_CTL: u32 = 0x400; + const IA32_MC0_STATUS: u32 = 0x401; + const IA32_MC0_ADDR: u32 = 0x402; + const IA32_MC0_MISC: u32 = 0x403; + const IA32_MC1_CTL: u32 = 0x404; + const IA32_MC1_STATUS: u32 = 0x405; + const IA32_MC1_ADDR: u32 = 0x406; + + const IA32_MC1_MISC: u32 = 0x407; + const IA32_MC2_CTL: u32 = 0x408; + const IA32_MC2_STATUS: u32 = 0x409; + const IA32_MC2_ADDR: u32 = 0x40a; + const IA32_MC2_MISC: u32 = 0x40b; + const IA32_MC3_CTL: u32 = 0x40c; + const IA32_MC3_STATUS: u32 = 0x40d; + const IA32_MC3_ADDR1: u32 = 0x40e; + const IA32_MC3_MISC: u32 = 0x40f; + const IA32_MC4_CTL: u32 = 0x410; + const IA32_MC4_STATUS: u32 = 0x411; + const IA32_MC4_ADDR: u32 = 0x412; + const IA32_MC4_MISC: u32 = 0x413; + const IA32_MC5_CTL: u32 = 0x414; + const IA32_MC5_STATUS: u32 = 0x415; + const IA32_MC5_ADDR: u32 = 0x416; + const IA32_MC5_MISC: u32 = 0x417; + const IA32_MC6_CTL: u32 = 0x418; + + const IA32_MC6_STATUS: u32 = 0x419; + const IA32_MC6_ADDR1: u32 = 0x41a; + const IA32_MC6_MISC: u32 = 0x41b; + const IA32_MC7_CTL: u32 = 0x41c; + const IA32_MC7_STATUS: u32 = 0x41d; + const IA32_MC7_ADDR: u32 = 0x41e; + const IA32_MC7_MISC: u32 = 0x41f; + const IA32_MC8_CTL: u32 = 0x420; + const IA32_MC8_STATUS: u32 = 0x421; + const IA32_MC8_ADDR: u32 = 0x422; + const IA32_MC8_MISC: u32 = 0x423; + const IA32_MC9_CTL: u32 = 0x424; + const IA32_MC9_STATUS: u32 = 0x425; + const IA32_MC9_ADDR: u32 = 0x426; + const IA32_MC9_MISC: u32 = 0x427; + const IA32_MC10_CTL: u32 = 0x428; + const IA32_MC10_STATUS: u32 = 0x429; + const IA32_MC10_ADDR: u32 = 0x42a; + const IA32_MC10_MISC: u32 = 0x42b; + + const IA32_MC11_CTL: u32 = 0x42c; + const IA32_MC11_STATUS: u32 = 0x42d; + const IA32_MC11_ADDR: u32 = 0x42e; + const IA32_MC11_MISC: u32 = 0x42f; + const IA32_MC12_CTL: u32 = 0x430; + const IA32_MC12_STATUS: u32 = 0x431; + const IA32_MC12_ADDR: u32 = 0x432; + const IA32_MC12_MISC: u32 = 0x433; + const IA32_MC13_CTL: u32 = 0x434; + const IA32_MC13_STATUS: u32 = 0x435; + const IA32_MC13_ADDR: u32 = 0x436; + const IA32_MC13_MISC: u32 = 0x437; + const IA32_MC14_CTL: u32 = 0x438; + const IA32_MC14_STATUS: u32 = 0x439; + const IA32_MC14_ADDR: u32 = 0x43a; + const IA32_MC14_MISC: u32 = 0x43b; + const IA32_MC15_CTL: u32 = 0x43c; + const IA32_MC15_STATUS: u32 = 0x43d; + + const IA32_MC15_ADDR: u32 = 0x43e; + const IA32_MC15_MISC: u32 = 0x43f; + const IA32_MC16_CTL: u32 = 0x440; + const IA32_MC16_STATUS: u32 = 0x441; + const IA32_MC16_ADDR: u32 = 0x442; + const IA32_MC16_MISC: u32 = 0x443; + const IA32_MC17_CTL: u32 = 0x444; + const IA32_MC17_STATUS: u32 = 0x445; + const IA32_MC17_ADDR: u32 = 0x446; + const IA32_MC17_MISC: u32 = 0x447; + const IA32_MC18_CTL: u32 = 0x448; + const IA32_MC18_STATUS: u32 = 0x449; + const IA32_MC18_ADDR: u32 = 0x44a; + const IA32_MC18_MISC: u32 = 0x44b; + const IA32_MC19_CTL: u32 = 0x44c; + const IA32_MC19_STATUS: u32 = 0x44d; + const IA32_MC19_ADDR: u32 = 0x44e; + const IA32_MC19_MISC: u32 = 0x44f; + const IA32_MC20_CTL: u32 = 0x450; + + const IA32_MC20_STATUS: u32 = 0x451; + const IA32_MC20_ADDR: u32 = 0x452; + const IA32_MC20_MISC: u32 = 0x453; + const IA32_MC21_CTL: u32 = 0x454; + const IA32_MC21_STATUS: u32 = 0x455; + const IA32_MC21_ADDR: u32 = 0x456; + const IA32_MC21_MISC: u32 = 0x457; + const IA32_MC22_CTL: u32 = 0x458; + const IA32_MC22_STATUS: u32 = 0x459; + const IA32_MC22_ADDR: u32 = 0x45a; + const IA32_MC22_MISC: u32 = 0x45b; + const IA32_MC23_CTL: u32 = 0x45c; + const IA32_MC23_STATUS: u32 = 0x45d; + const IA32_MC23_ADDR: u32 = 0x45e; + const IA32_MC23_MISC: u32 = 0x45f; + const IA32_MC24_CTL: u32 = 0x460; + const IA32_MC24_STATUS: u32 = 0x461; + const IA32_MC24_ADDR: u32 = 0x462; + + const IA32_MC24_MISC: u32 = 0x463; + const IA32_MC25_CTL: u32 = 0x464; + const IA32_MC25_STATUS: u32 = 0x465; + const IA32_MC25_ADDR: u32 = 0x466; + const IA32_MC25_MISC: u32 = 0x467; + const IA32_MC26_CTL: u32 = 0x468; + const IA32_MC26_STATUS: u32 = 0x469; + const IA32_MC26_ADDR: u32 = 0x46a; + const IA32_MC26_MISC: u32 = 0x46b; + const IA32_MC27_CTL: u32 = 0x46c; + const IA32_MC27_STATUS: u32 = 0x46d; + const IA32_MC27_ADDR: u32 = 0x46e; + const IA32_MC27_MISC: u32 = 0x46f; + const IA32_MC28_CTL: u32 = 0x470; + const IA32_MC28_STATUS: u32 = 0x471; + const IA32_MC28_ADDR: u32 = 0x472; + const IA32_MC28_MISC: u32 = 0x473; + const IA32_MC29_CTL: u32 = 0x474; + const IA32_MC29_STATUS: u32 = 0x475; + + const IA32_MC29_ADDR: u32 = 0x476; + const IA32_MC29_MISC: u32 = 0x477; + const IA32_MC30_CTL: u32 = 0x478; + const IA32_MC30_STATUS: u32 = 0x479; + const IA32_MC30_ADDR: u32 = 0x47a; + const IA32_MC30_MISC: u32 = 0x47b; + const IA32_MC31_CTL: u32 = 0x47c; + const IA32_MC31_STATUS: u32 = 0x47d; + const IA32_MC31_ADDR: u32 = 0x47e; + const IA32_MC31_MISC: u32 = 0x47f; + + const IA32_TSC_DEADLINE: u32 = 0x6e0; + const _IA32_TSC_DEADLINE_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<24>(&Parameters { + leaf: 0x1, + sub_leaf: 0..=0, + register: CpuidReg::ECX, + }); + + // NOTE: THE X2APIC related MSRs cannot be filtered by KVM, but we include them here anyway for completeness sake. + const IA32_X2APIC_TPR: u32 = 0x808; + const IA32_X2APIC_SIVR: u32 = 0x80f; + + const IA32_X2APIC_ESR: u32 = 0x828; + const IA32_X2APIC_LVT_CMCI: u32 = 0x82f; + const IA32_X2APIC_ICR: u32 = 0x830; + const IA32_X2APIC_LVT_TIMER: u32 = 0x832; + const IA32_X2APIC_LVT_THERMAL: u32 = 0x833; + const IA32_X2APIC_LVT_PMI: u32 = 0x834; + const IA32_X2APIC_LVT_LINT0: u32 = 0x835; + + const IA32_X2APIC_LVT_LINT1: u32 = 0x836; + const IA32_X2APIC_LVT_ERROR: u32 = 0x837; + const IA32_X2APIC_INIT_COUNT: u32 = 0x838; + const IA32_X2APIC_DIV_CONF: u32 = 0x83e; + + /// Extended Feature Enable + const IA32_EFER: u32 = 0xc0000080; + + const IA32_STAR: u32 = 0xc000_0081; + const IA32_LSTAR: u32 = 0xc000_0082; + const IA32_CSTAR: u32 = 0xc000_0083; + const IA32_FMASK: u32 = 0xc000_0084; + const IA32_FS_BASE: u32 = 0xc000_0100; + const IA32_GS_BASE: u32 = 0xc000_0101; + const IA32_KERNEL_GS_BASE: u32 = 0xc000_0102; + const _IA32_EFER_UPTO_IA32_KERNEL_GS_BASE_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<29>(&Parameters { + leaf: 0x80000001, + sub_leaf: 0..=0, + register: CpuidReg::EDX, + }); + + const IA32_TSC_AUX: u32 = 0xc000_0103; + // NOTE That either the following has to pass, or the same test with 0x80000001.EDX[27] + const _IA32_TSC_AUX_CPUID_CHECK: () = assert_not_denied_cpuid_feature::<22>(&Parameters { + leaf: 0x7, + sub_leaf: 0..=0, + register: CpuidReg::ECX, + }); + + pub(super) const READ_WRITE_IA32_MSRS: [u32; 199] = [ + IA32_TIME_STAMP_COUNTER, + IA32_APIC_BASE, + IA32_FEATURE_CONTROL, + IA32_TSC_ADJUST, + IA32_SPEC_CTRL, + IA32_MCU_OPT_CTRL, + IA32_SYSENTER_CS, + IA32_SYSENTER_ESP, + IA32_SYSENTER_EIP, + IA32_MCG_STATUS, + IA32_SMM_MONITOR_CTL, + IA32_MISC_ENABLE, + IA32_XFD, + IA32_XFD_ERR, + IA32_DCA_0_CAP, + IA32_MTRR_PHYSBASE0, + IA32_MTRR_PHYSMASK0, + IA32_MTRR_PHYSBASE1, + IA32_MTRR_PHYSMASK1, + IA32_MTRR_PHYSBASE2, + IA32_MTRR_PHYSMASK2, + IA32_MTRR_PHYSBASE3, + IA32_MTRR_PHYSMASK3, + IA32_MTRR_PHYSBASE4, + IA32_MTRR_PHYSMASK4, + IA32_MTRR_PHYSBASE5, + IA32_MTRR_PHYSMASK5, + IA32_MTRR_PHYSBASE6, + IA32_MTRR_PHYSMASK6, + IA32_MTRR_PHYSBASE7, + IA32_MTRR_PHYSMASK7, + IA32_MTRR_PHYSBASE8, + IA32_MTRR_PHYSMASK8, + IA32_MTRR_PHYSBASE9, + IA32_MTRR_PHYSMASK9, + IA32_MTRR_FIX64K_00000, + IA32_MTRR_FIX16K_80000, + IA32_MTRR_FIX16K_A0000, + IA32_MTRR_FIX4K_C0000, + IA32_MTRR_FIX4K_C8000, + IA32_MTRR_FIX4K_D0000, + IA32_MTRR_FIX4K_D8000, + IA32_MTRR_FIX4K_E0000, + IA32_MTRR_FIX4K_E8000, + IA32_MTRR_FIX4K_F0000, + IA32_MTRR_FIX4K_F8000, + IA32_PAT, + IA32_MTRR_DEF_TYPE, + IA32_MC0_CTL, + IA32_MC0_STATUS, + IA32_MC0_ADDR, + IA32_MC0_MISC, + IA32_MC1_CTL, + IA32_MC1_STATUS, + IA32_MC1_ADDR, + IA32_MC1_MISC, + IA32_MC2_CTL, + IA32_MC2_STATUS, + IA32_MC2_ADDR, + IA32_MC2_MISC, + IA32_MC3_CTL, + IA32_MC3_STATUS, + IA32_MC3_ADDR1, + IA32_MC3_MISC, + IA32_MC4_CTL, + IA32_MC4_STATUS, + IA32_MC4_ADDR, + IA32_MC4_MISC, + IA32_MC5_CTL, + IA32_MC5_STATUS, + IA32_MC5_ADDR, + IA32_MC5_MISC, + IA32_MC6_CTL, + IA32_MC6_STATUS, + IA32_MC6_ADDR1, + IA32_MC6_MISC, + IA32_MC7_CTL, + IA32_MC7_STATUS, + IA32_MC7_ADDR, + IA32_MC7_MISC, + IA32_MC8_CTL, + IA32_MC8_STATUS, + IA32_MC8_ADDR, + IA32_MC8_MISC, + IA32_MC9_CTL, + IA32_MC9_STATUS, + IA32_MC9_ADDR, + IA32_MC9_MISC, + IA32_MC10_CTL, + IA32_MC10_STATUS, + IA32_MC10_ADDR, + IA32_MC10_MISC, + IA32_MC11_CTL, + IA32_MC11_STATUS, + IA32_MC11_ADDR, + IA32_MC11_MISC, + IA32_MC12_CTL, + IA32_MC12_STATUS, + IA32_MC12_ADDR, + IA32_MC12_MISC, + IA32_MC13_CTL, + IA32_MC13_STATUS, + IA32_MC13_ADDR, + IA32_MC13_MISC, + IA32_MC14_CTL, + IA32_MC14_STATUS, + IA32_MC14_ADDR, + IA32_MC14_MISC, + IA32_MC15_CTL, + IA32_MC15_STATUS, + IA32_MC15_ADDR, + IA32_MC15_MISC, + IA32_MC16_CTL, + IA32_MC16_STATUS, + IA32_MC16_ADDR, + IA32_MC16_MISC, + IA32_MC17_CTL, + IA32_MC17_STATUS, + IA32_MC17_ADDR, + IA32_MC17_MISC, + IA32_MC18_CTL, + IA32_MC18_STATUS, + IA32_MC18_ADDR, + IA32_MC18_MISC, + IA32_MC19_CTL, + IA32_MC19_STATUS, + IA32_MC19_ADDR, + IA32_MC19_MISC, + IA32_MC20_CTL, + IA32_MC20_STATUS, + IA32_MC20_ADDR, + IA32_MC20_MISC, + IA32_MC21_CTL, + IA32_MC21_STATUS, + IA32_MC21_ADDR, + IA32_MC21_MISC, + IA32_MC22_CTL, + IA32_MC22_STATUS, + IA32_MC22_ADDR, + IA32_MC22_MISC, + IA32_MC23_CTL, + IA32_MC23_STATUS, + IA32_MC23_ADDR, + IA32_MC23_MISC, + IA32_MC24_CTL, + IA32_MC24_STATUS, + IA32_MC24_ADDR, + IA32_MC24_MISC, + IA32_MC25_CTL, + IA32_MC25_STATUS, + IA32_MC25_ADDR, + IA32_MC25_MISC, + IA32_MC26_CTL, + IA32_MC26_STATUS, + IA32_MC26_ADDR, + IA32_MC26_MISC, + IA32_MC27_CTL, + IA32_MC27_STATUS, + IA32_MC27_ADDR, + IA32_MC27_MISC, + IA32_MC28_CTL, + IA32_MC28_STATUS, + IA32_MC28_ADDR, + IA32_MC28_MISC, + IA32_MC29_CTL, + IA32_MC29_STATUS, + IA32_MC29_ADDR, + IA32_MC29_MISC, + IA32_MC30_CTL, + IA32_MC30_STATUS, + IA32_MC30_ADDR, + IA32_MC30_MISC, + IA32_MC31_CTL, + IA32_MC31_STATUS, + IA32_MC31_ADDR, + IA32_MC31_MISC, + IA32_TSC_DEADLINE, + IA32_X2APIC_TPR, + IA32_X2APIC_SIVR, + IA32_X2APIC_ESR, + IA32_X2APIC_LVT_CMCI, + IA32_X2APIC_ICR, + IA32_X2APIC_LVT_TIMER, + IA32_X2APIC_LVT_THERMAL, + IA32_X2APIC_LVT_PMI, + IA32_X2APIC_LVT_LINT0, + IA32_X2APIC_LVT_LINT1, + IA32_X2APIC_LVT_ERROR, + IA32_X2APIC_INIT_COUNT, + IA32_X2APIC_DIV_CONF, + IA32_EFER, + IA32_STAR, + IA32_LSTAR, + IA32_CSTAR, + IA32_FMASK, + IA32_FS_BASE, + IA32_GS_BASE, + IA32_KERNEL_GS_BASE, + IA32_TSC_AUX, + ]; + } + + mod write_only { + use super::{CpuidReg, Parameters, assert_not_denied_cpuid_feature}; + + /// Prediction Command (WO) + const IA32_PRED_CMD: u32 = 0x49; + const _IA32_PRED_CMD_CPUID_CHECK: () = assert_not_denied_cpuid_feature::<26>(&Parameters { + leaf: 0x7, + sub_leaf: 0..=0, + register: CpuidReg::EDX, + }); + + /// Flush Command (WO) + const IA32_FLUSH_CMD: u32 = 0x10b; + + // TODO: Should probably use inherit policy here + const _IA32_FLUSH_CMD_CPUID_CHECK: () = + assert_not_denied_cpuid_feature::<28>(&Parameters { + leaf: 0x7, + sub_leaf: 0..=0, + register: CpuidReg::EDX, + }); + + // X2apic related MSRS cannot be filtered by KVM, but we include it here anyway for completeness sake + const IA32_X2APIC_EOI: u32 = 0x80b; + + const IA32_X2APIC_SELF_IPI: u32 = 0x83f; + + pub(super) const WRITE_ONLY_IA32_MSRS: [u32; 4] = [ + IA32_PRED_CMD, + IA32_FLUSH_CMD, + IA32_X2APIC_EOI, + IA32_X2APIC_SELF_IPI, + ]; + } + + /// A list of permitted Intel IA32 MSRs that are not considered MSR-based feature indices + /// by KVM. + /// + /// The MSRs listed here can be studied further in Table 2.2 in Section 2.1 of the Intel SDM + /// Vol. 4 from October 2025 + pub(in crate::x86_64) const PERMITTED_IA32_MSRS: [u32; 242] = const { + let mut permitted = [0u32; 242]; + let read_only_len = READ_ONLY_IA32_MSRS.len(); + let write_only_len = WRITE_ONLY_IA32_MSRS.len(); + let read_write_len = READ_WRITE_IA32_MSRS.len(); + assert!(permitted.len() == (read_only_len + write_only_len + read_write_len)); + let mut idx = 0; + // Insert read only msrs + { + let mut i = 0; + while i < read_only_len { + permitted[idx + i] = READ_ONLY_IA32_MSRS[i]; + i += 1; + } + idx += read_only_len; + } + // Insert write only msrs + { + let mut i = 0; + while i < write_only_len { + permitted[idx + i] = WRITE_ONLY_IA32_MSRS[i]; + i += 1; + } + idx += write_only_len; + } + // Insert read & write msrs + { + let mut i = 0; + while i < read_write_len { + permitted[idx + i] = READ_WRITE_IA32_MSRS[i]; + i += 1; + } + } + permitted + }; +} + +mod forbidden_architectural_msrs { + const IA32_P5_MC_ADDR: (u32, u32) = (0x0, 0x0); + const IA32_P5_MC_TYPE: (u32, u32) = (0x1, 0x1); + + const IA32_MONITOR_FILTER_SIZE: (u32, u32) = (0x6, 0x6); + // TODO: Not sure about this one + const IA32_PLATFORM_ID: (u32, u32) = (0x17, 0x17); + + /// Only available is CPUID 0x7.0x1.EBX[0] = 1, but this is always 0 for non-host CPU profiles + const IA32_PPIN_CTL: (u32, u32) = (0x4e, 0x4e); + + /// Only available is CPUID 0x7.0x1.EBX[0] = 1, but this is always 0 for non-host CPU profiles + const IA32_PPIN: (u32, u32) = (0x4f, 0x4f); + + /// Used for microcode updates. Should not be available for guests. + const IA32_BIOS_UPDT_TRIG: (u32, u32) = (0x79, 0x79); + + /// Currently only related to Secure enclaves/Keylocker which is not available for non-host CPU profiles + const IA32_FEATURE_ACTIVATION: (u32, u32) = (0x7a, 0x7a); + + /// Related to microcode updates + const IA32_MCU_ENUMERATION: (u32, u32) = (0x7b, 0x7b); + + const IA32_MCU_STATUS: (u32, u32) = (0x7c, 0x7c); + + // TODO: Not sure what this does and whether it should be enabled + const IA32_FZM_RANGE_INDEX: (u32, u32) = (0x82, 0x82); + + /// Related to total memory encryption + /// + const IA32_MKTME_KEYID_PARTITIONING: (u32, u32) = (0x87, 0x87); + + const IA32_SGXLEPUBKEYHASH0: (u32, u32) = (0x8c, 0x8c); + + const IA32_SGXLEPUBKEYHASH1: (u32, u32) = (0x8d, 0x8d); + + const IA32_SGXLEPUBKEYHASH2: (u32, u32) = (0x8e, 0x8e); + + const IA32_SGXLEPUBKEYHASH3: (u32, u32) = (0x8f, 0x8f); + + const IA32_SGXLEPUBKEYHASH4: (u32, u32) = (0x90, 0x90); + + const IA32_SGXLEPUBKEYHASH5: (u32, u32) = (0x91, 0x91); + + // TODO: Check this + const IA32_SMBASE: (u32, u32) = (0x9e, 0x9e); + + const IA32_MISC_PACKAGE_CTLS: (u32, u32) = (0xbc, 0xbc); + + /// xAPIC Disable Status + // TODO: Also check consistency with IA32_ARCH_CAPABILITIES[21] + const IA32_XAPIC_DISABLE_STATUS: (u32, u32) = (0xbd, 0xbd); + + const IA32_SMRR_PHYS_BASE_MASK: (u32, u32) = (0x1f2, 0x1f3); + + /// Overclocking Status (R/O) + // TODO: Also check consistency with IA32_ARCH_CAPABILITIES[23] + const IA32_OVERCLOCKING_STATUS: (u32, u32) = (0x195, 0x195); + + /// Clock Modulation Control + /// This is disabled via CPUID for non-host CPU profiles + const IA32_CLOCK_MODULATION: (u32, u32) = (0x19a, 0x19a); + + // IA32_PLI_SSP is disabled via CPUID for non-host profiles + const IA32_PLI_SSP: (u32, u32) = (0x6a4, 0x6a7); + + // This is disabled via CPUID for non-host profiles + const IA32_INTERRUPT_SSP_TABLE_ADDR: (u32, u32) = (0x6a8, 0x6a8); + + const IA32_PECI_HWP_REQUEST_INFO: (u32, u32) = (0x775, 0x775); + const IA32_PMC0: (u32, u32) = (0xc1, 0xc1); + const IA32_PMC1: (u32, u32) = (0xc2, 0xc2); + const IA32_PMC2: (u32, u32) = (0xc3, 0xc3); + const IA32_PMC3: (u32, u32) = (0xc4, 0xc4); + const IA32_PMC4: (u32, u32) = (0xc5, 0xc5); + const IA32_PMC5: (u32, u32) = (0xc6, 0xc6); + const IA32_PMC6: (u32, u32) = (0xc7, 0xc7); + const IA32_PMC7: (u32, u32) = (0xc8, 0xc8); + const IA32_PMC8: (u32, u32) = (0xc9, 0xc9); + const IA32_PMC9: (u32, u32) = (0xca, 0xca); + + const IA32_CORE_CAPABILITIES: (u32, u32) = (0xcf, 0xcf); + + // TODO: Do we really want to forbid this MSR? + const IA32_UMWAIT_CONTROL: (u32, u32) = (0xe1, 0xe1); + + // Disabled by CPUID for non-host CPU profiles + const IA32_MPERF: (u32, u32) = (0xe7, 0xe7); + + const IA32_APERF: (u32, u32) = (0xe8, 0xe8); + + const IA32_TSX_FORCE_ABORT: (u32, u32) = (0x10f, 0x10f); + + // Disabled via static IA32_ARCH_CAPABILITIES bit for non-host CPU profiles + const IA32_TSX_CTRL: (u32, u32) = (0x122, 0x122); + + // NOTE: IA32_MCU_OPT_CTRL must necessarily be available, due to + // what we set in CPUID for some CPU profiles (inherit policy) + + const IA32_MCG_CTL: (u32, u32) = (0x17b, 0x17b); + + // TODO: 0x180- 0x185 is reserved, we should not list these MSRS at all + + /// Disabled via CPUID for all non-host CPU profiles + const IA32_PERFEVTSEL0: (u32, u32) = (0x186, 0x186); + const IA32_PERFEVTSEL1: (u32, u32) = (0x187, 0x187); + const IA32_PERFEVTSEL2: (u32, u32) = (0x188, 0x188); + const IA32_PERFEVTSEL3: (u32, u32) = (0x189, 0x189); + const IA32_PERFEVTSEL4: (u32, u32) = (0x18a, 0x18a); + const IA32_PERFEVTSEL5: (u32, u32) = (0x18b, 0x18b); + const IA32_PERFEVTSEL6: (u32, u32) = (0x18c, 0x18c); + const IA32_PERFEVTSEL7: (u32, u32) = (0x18d, 0x18d); + const IA32_PERFEVTSEL8: (u32, u32) = (0x18e, 0x18e); + const IA32_PERFEVTSEL9: (u32, u32) = (0x18f, 0x18f); + + // TODO: 0x18a - 0x194 is reserved and should not be included in any list + + // TODO: 0x196, 197 is reserved and should not be included in any list + // + + const IA32_PERF_STATUS: (u32, u32) = (0x198, 0x198); + + const IA32_PERF_CTL: (u32, u32) = (0x199, 0x199); + + // Disabled via CPUID for non-host profiles + const IA32_THERM_INTERRUPT: (u32, u32) = (0x19b, 0x19b); + + // Disabled via CPUID for non-host profiles + const IA32_THERM_STATUS: (u32, u32) = (0x19c, 0x19c); + + // Disabled via CPUID for non-host profiles + const IA32_ENERGY_PERF_BIAS: (u32, u32) = (0x1b0, 0x1b0); + + // Disabled via CPUID for non-host profiles + const IA32_PACKAGE_THERM_STATUS: (u32, u32) = (0x1b1, 0x1b1); + + // Disabled via CPUID for non-host profiles + const IA32_PACKAGE_THERM_INTERRUPT: (u32, u32) = (0x1b2, 0x1b2); + + const IA32_DEBUGCTL: (u32, u32) = (0x1d9, 0x1d9); + + const IA32_LER_FROM_IP: (u32, u32) = (0x1dd, 0x1dd); + + const IA32_LER_TO_IP: (u32, u32) = (0x1de, 0x1de); + + const IA32_LER_INFO: (u32, u32) = (0x1e0, 0x1e0); + + const IA32_MC_I_CTL2: (u32, u32) = (0x280, 0x29f); + + // Disabled via CPUID for non-host profiles + const IA32_INTEGRITY_STATUS: (u32, u32) = (0x2dc, 0x2dc); + + const IA32_FIXED_CTRI: (u32, u32) = (0x309, 0x30f); + + // IA32_PERF_CAPABILITIES is an MSR-based feature thus not listed here + + // Disabled via CPUID for non-host profiles + const IA32_FIXED_CTR_CTRL: (u32, u32) = (0x38d, 0x38d); + + // Disabled via CPUID for non-host profiles + const IA32_PERF_GLOBAL_STATUS: (u32, u32) = (0x38e, 0x38e); + + // Disabled via CPUID for non-host profiles + const IA32_PERF_GLOBAL_CTRL: (u32, u32) = (0x38f, 0x38f); + + // Disabled via CPUID for non-host profiles + const IA32_PERF_GLOBAL_STATUS_RESET: (u32, u32) = (0x390, 0x390); + + // Disabled via CPUID for non-host profiles + const IA32_PERF_GLOBAL_STATUS_SET: (u32, u32) = (0x391, 0x391); + + // Disabled via CPUID for non-host profiles + const IA32_PERF_GLOBAL_INUSE: (u32, u32) = (0x392, 0x392); + + // TODO: Not sure about this one, but seems to be related to performance monitoring which + // should be disabled for non-host CPU profiles. + const IA32_PEBS_ENABLE: (u32, u32) = (0x3f1, 0x3f1); + + const IA32_A_PMC0: (u32, u32) = (0x4c1, 0x4c1); + const IA32_A_PMC1: (u32, u32) = (0x4c2, 0x4c2); + const IA32_A_PMC2: (u32, u32) = (0x4c3, 0x4c3); + const IA32_A_PMC3: (u32, u32) = (0x4c4, 0x4c4); + const IA32_A_PMC4: (u32, u32) = (0x4c5, 0x4c5); + const IA32_A_PMC5: (u32, u32) = (0x4c6, 0x4c6); + const IA32_A_PMC6: (u32, u32) = (0x4c7, 0x4c7); + const IA32_A_PMC7: (u32, u32) = (0x4c8, 0x4c8); + const IA32_A_PMC8: (u32, u32) = (0x4c9, 0x4c9); + const IA32_A_PMC9: (u32, u32) = (0x4ca, 0x4ca); + + const IA32_MCG_EXT_CTL: (u32, u32) = (0x4d0, 0x4d0); + + // SGX is disabled via CPUID for non-host CPU profiles + const IA32_SGX_SVN_STATUS: (u32, u32) = (0x500, 0x500); + + // Disabled via CPUID for non-host CPU profiles + const IA32_RTIT_OUTPUT_BASE: (u32, u32) = (0x560, 0x560); + + // Disabled via CPUID for non-host CPU profiles + const IA32_RTIT_OUTPUT_MASK_PTRS: (u32, u32) = (0x561, 0x561); + + // Disabled via CPUID for non-host CPU profiles + const IA32_RTIT_CTL: (u32, u32) = (0x570, 0x570); + + // Disabled via CPUID for non-host CPU profiles + const IA32_RTIT_STATUS: (u32, u32) = (0x571, 0x571); + + // Disabled via CPU profiles + const IA32_RTIT_CR3_MATCH: (u32, u32) = (0x572, 0x572); + + const IA32_RTIT_ADDR0_A: (u32, u32) = (0x580, 0x580); + const IA32_RTIT_ADDR0_B: (u32, u32) = (0x581, 0x581); + const IA32_RTIT_ADDR1_A: (u32, u32) = (0x582, 0x582); + const IA32_RTIT_ADDR1_B: (u32, u32) = (0x583, 0x583); + const IA32_RTIT_ADDR2_A: (u32, u32) = (0x584, 0x584); + const IA32_RTIT_ADDR2_B: (u32, u32) = (0x585, 0x585); + const IA32_RTIT_ADDR3_A: (u32, u32) = (0x586, 0x586); + const IA32_RTIT_ADDR3_B: (u32, u32) = (0x587, 0x587); + + // Disabled via CPUID for non-host CPU profiles + const IA32_DS_AREA: (u32, u32) = (0x600, 0x600); + + // U_CET and S_CET are disabled via CPUID + // TODO: Include compile time checks for that + const IA32_U_CET: (u32, u32) = (0x6a0, 0x6a0); + const IA32_S_CET: (u32, u32) = (0x6a2, 0x6a2); + + // TODO: IA32_TSC_DEADLINE should be available because the TSC_DEADLINE CPUID bit + // is set by CHV unconditionally. The availability of this MSR probably needs to be + // handled by CHV itself and not the CPU profiles + + // Disabled via CPUID for non-host CPU profiles + const IA32_PKRS: (u32, u32) = (0x6e1, 0x6e1); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PM_ENABLE: (u32, u32) = (0x770, 0x770); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HWP_CAPABILITIES: (u32, u32) = (0x771, 0x771); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HWP_REQUEST_PKG: (u32, u32) = (0x772, 0x772); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HWP_INTERRUPT: (u32, u32) = (0x773, 0x773); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HWP_REQUEST: (u32, u32) = (0x774, 0x774); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HWP_CTL: (u32, u32) = (0x776, 0x776); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HWP_STATUS: (u32, u32) = (0x777, 0x777); + + const IA32_MCU_EXT_SERVICE: (u32, u32) = (0x7a3, 0x7a3); + + const IA32_MCU_ROLLBACK_MIN_ID: (u32, u32) = (0x7a4, 0x7a4); + + // TODO: Not sure about IA32_MCU_STAGING_MBOX_ADDR + + const IA32_ROLLBACK_SIGN_ID_0: (u32, u32) = (0x7b0, 0x7b0); + const IA32_ROLLBACK_SIGN_ID_1: (u32, u32) = (0x7b1, 0x7b1); + const IA32_ROLLBACK_SIGN_ID_2: (u32, u32) = (0x7b2, 0x7b2); + const IA32_ROLLBACK_SIGN_ID_3: (u32, u32) = (0x7b3, 0x7b3); + const IA32_ROLLBACK_SIGN_ID_4: (u32, u32) = (0x7b4, 0x7b4); + const IA32_ROLLBACK_SIGN_ID_5: (u32, u32) = (0x7b5, 0x7b5); + const IA32_ROLLBACK_SIGN_ID_6: (u32, u32) = (0x7b6, 0x7b6); + const IA32_ROLLBACK_SIGN_ID_7: (u32, u32) = (0x7b7, 0x7b7); + const IA32_ROLLBACK_SIGN_ID_8: (u32, u32) = (0x7b8, 0x7b8); + const IA32_ROLLBACK_SIGN_ID_9: (u32, u32) = (0x7b9, 0x7b9); + const IA32_ROLLBACK_SIGN_ID_10: (u32, u32) = (0x7ba, 0x7ba); + const IA32_ROLLBACK_SIGN_ID_11: (u32, u32) = (0x7bb, 0x7bb); + const IA32_ROLLBACK_SIGN_ID_12: (u32, u32) = (0x7bc, 0x7bc); + const IA32_ROLLBACK_SIGN_ID_13: (u32, u32) = (0x7bd, 0x7bd); + const IA32_ROLLBACK_SIGN_ID_14: (u32, u32) = (0x7be, 0x7be); + const IA32_ROLLBACK_SIGN_ID_15: (u32, u32) = (0x7bf, 0x7bf); + + // Disabled via CPUID for non-host CPU profiles + const IA32_TME_CAPABILITY: (u32, u32) = (0x981, 0x981); + + // Disabled via CPUID for non-host CPU profiles + const IA32_TME_ACTIVATE: (u32, u32) = (0x982, 0x982); + + // Disabled via CPUID for non-host CPU profiles + const IA32_TME_EXCLUDE_MASK: (u32, u32) = (0x983, 0x983); + + // Disabled via CPUID for non-host CPU profiles + const IA32_TME_EXCLUDE_BASE: (u32, u32) = (0x984, 0x984); + + // Disabled via CPUID for non-host CPU profiles + const IA32_UINTR_RR: (u32, u32) = (0x985, 0x985); + + // Disabled via CPUID for non-host CPU profiles + const IA32_UINTR_HANDLER: (u32, u32) = (0x986, 0x986); + + // Disabled via CPUID for non-host CPU profiles + const IA32_UINTR_STACKADJUST: (u32, u32) = (0x987, 0x987); + + // Disabled via CPUID for non-host CPU profiles + const IA32_UINTR_MISC: (u32, u32) = (0x988, 0x988); + + // Disabled via CPUID for non-host CPU profiles + const IA32_UINTR_PD: (u32, u32) = (0x989, 0x989); + + // Disabled via CPUID for non-host CPU profiles + const IA32_UINTR_TT: (u32, u32) = (0x98a, 0x98a); + + // Disabled via CPUID for non-host CPU profiles + const IA32_COPY_STATUS: (u32, u32) = (0x990, 0x990); + + // Disabled via CPUID for non-host CPU profiles + const IA32_IWKEYBACKUP_STATUS: (u32, u32) = (0x991, 0x991); + + const IA32_TME_CLEAR_SAVED_KEY: (u32, u32) = (0x9fb, 0x9fb); + + // Disabled via CPUID for non-host CPU profiles + const IA32_DEBUG_INTERFACE: (u32, u32) = (0xc80, 0xc80); + + // Disabled via CPUID for non-host CPU profiles + const IA32_L3_QOS_CFG: (u32, u32) = (0xc81, 0xc81); + + // Disabled via CPUID + const IA32_L2_QOS_CFG: (u32, u32) = (0xc82, 0xc82); + + // Disabled via CPUID + const IA32_L3_IO_QOS_CFG: (u32, u32) = (0xc83, 0xc83); + + const IA32_RESOURCE_PRIORITY: (u32, u32) = (0xc88, 0xc88); + const IA32_RESOURCE_PRIORITY_PKG: (u32, u32) = (0xc89, 0xc89); + + // Disabled via CPUID for non-host CPU profiles + const IA32_QM_EVTSEL: (u32, u32) = (0xc8d, 0xc8d); + + // Disabled via CPUID for non-host CPU profiles + const IA32_QM_CTR: (u32, u32) = (0xc8e, 0xc8e); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PQR_ASSOC: (u32, u32) = (0xc8f, 0xc8f); + + // Disabled via CPUID for non-host CPU profiles + const IA32_L3_MASK_0: (u32, u32) = (0xc90, 0xc90); + + const IA32_L3_MASK_N: (u32, u32) = (0xc91, 0xd8f); + + // Disabled via CPUID for non-host CPU profiles + const IA32_L2_MASK_0: (u32, u32) = (0xd10, 0xd10); + + // Disabled via CPUID for non-host CPU profiles + const IA32_L2_MASK_N: (u32, u32) = (0xd11, 0xd4f); + + // Disabled via CPUID for non-host CPU profiles + const IA32_L2_QOS_EXT_BW_THRTL_I: (u32, u32) = (0xd50, 0xd5e); + + // Disabled via CPUID for non-host CPU profiles + const IA32_BNDCFGS: (u32, u32) = (0xd90, 0xd90); + + // Disabled via CPUID for non-host CPU profiles + const IA32_COPY_LOCAL_TO_PLATFORM: (u32, u32) = (0xd91, 0xd91); + + // Disabled via CPUID for non-host CPU profiles + const IA32_COPY_PLATFORM_TO_LOCAL: (u32, u32) = (0xd92, 0xd92); + + const IA32_PASID: (u32, u32) = (0xd93, 0xd93); + + /* + IA32_XSS is a bit problematic: Only never kernels will report it via + KVM_GET_MSR_INDEX_LIST, but CPUID 0xd.0x1.EAX[3] reports that this MSR + exists. + + In order for CPU profiles generated with recent kernels to work with + deployments operating with older kernels, we decide to forbid this MSR + for now even though CPUID indicates that it is available to the guest. + + We consider this OK because we have disabled every single IA32_XSS + related state component in the 0xd CPUID leaves, hence there is no + reason for the guest to want to use this. + */ + const IA32_XSS: (u32, u32) = (0xda0, 0xda0); + // Disabled via CPUID for non-host CPU profiles + const IA32_PKG_HDC_CTL: (u32, u32) = (0xdb0, 0xdb0); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PM_CTL1: (u32, u32) = (0xdb1, 0xdb1); + + // Disabled via CPUID for non-host CPU profiles + const IA32_THREAD_STALL: (u32, u32) = (0xdb2, 0xdb2); + + // Disabled via CPUID for non-host CPU profiles + const IA32_QOS_CORE_BW_THRTL_0: (u32, u32) = (0xe00, 0xe00); + + // Disabled via CPUID for non-host CPU profiles + const IA32_QOS_CORE_BW_THRTL_1: (u32, u32) = (0xe01, 0xe01); + + // Note that we have CPUID 0x7.EDX.[19] = 0 (ARCH_LBR) + const IA32_LBR_X_INFO: (u32, u32) = (0x1200, 0x121f); + + // TDX related. + const IA32_SEAMRR_BASE: (u32, u32) = (0x1400, 0x1400); + + // TDX related. + const IA32_SEAMRR_MASK: (u32, u32) = (0x1401, 0x1401); + + // Disabled via ARCH_CAPABILITIES for non-host CPU profiles + // TODO: Check that deny policy is compatible with + // the policy for IA32_ARCH_COMPATIBILITY[9] + const IA32_MCU_CONTROL: (u32, u32) = (0x1406, 1406); + + const IA32_LBR_CTL: (u32, u32) = (0x14ce, 0x14ce); + + const IA32_LBR_DEPTH: (u32, u32) = (0x14cf, 0x14cf); + + const IA32_LBR_X_FROM_IP: (u32, u32) = (0x1500, 0x151f); + + const IA32_LBR_X_TO_IP: (u32, u32) = (0x1600, 0x161f); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HW_FEEDBACK_PTR: (u32, u32) = (0x17d0, 0x17d0); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HW_FEEDBACK_CONFIG: (u32, u32) = (0x17d1, 0x17d1); + + // Disabled via CPUID for non-host CPU profiles + const IA32_HW_FEEDBACK_THREAD_CHAR: (u32, u32) = (0x17d2, 0x17d2); + + const IA32_HW_FEEDBACK_THREAD_CONFIG: (u32, u32) = (0x17d4, 0x17d4); + + const IA32_HRESET_ENABLE: (u32, u32) = (0x17da, 0x17da); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP0_CTR: (u32, u32) = (0x1900, 0x1900); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP0_CFG_A: (u32, u32) = (0x1901, 0x1901); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP0_CFG_C: (u32, u32) = (0x1903, 0x1903); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP1_CTR: (u32, u32) = (0x1904, 0x1904); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP1_CFG_A: (u32, u32) = (0x1905, 0x1905); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP1_CFG_C: (u32, u32) = (0x1907, 0x1907); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP2_CTR: (u32, u32) = (0x1908, 0x1908); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP2_CFG_A: (u32, u32) = (0x1909, 0x1909); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP2_CFG_B: (u32, u32) = (0x190a, 0x190a); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP2_CFG_C: (u32, u32) = (0x190b, 0x190b); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP3_CTR: (u32, u32) = (0x190c, 0x190c); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP3_CFG_A: (u32, u32) = (0x190d, 0x190d); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP3_CFG_B: (u32, u32) = (0x190e, 0x190e); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP3_CFG_C: (u32, u32) = (0x190f, 0x190f); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP4_CTR: (u32, u32) = (0x1910, 0x1910); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP4_CFG_A: (u32, u32) = (0x1911, 0x1911); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP4_CFG_B: (u32, u32) = (0x1912, 0x1912); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP4_CFG_C: (u32, u32) = (0x1913, 0x1913); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP5_CTR: (u32, u32) = (0x1914, 0x1914); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP5_CFG_A: (u32, u32) = (0x1915, 0x1915); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP5_CFG_B: (u32, u32) = (0x1916, 0x1916); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP5_CFG_C: (u32, u32) = (0x1917, 0x1917); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP6_CTR: (u32, u32) = (0x1918, 0x1918); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP6_CFG_A: (u32, u32) = (0x1919, 0x1919); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP6_CFG_B: (u32, u32) = (0x191a, 0x191a); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP6_CFG_C: (u32, u32) = (0x191b, 0x191b); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP7_CTR: (u32, u32) = (0x191c, 0x191c); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP7_CFG_A: (u32, u32) = (0x191d, 0x191d); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP7_CFG_B: (u32, u32) = (0x191e, 0x191e); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP7_CFG_C: (u32, u32) = (0x191f, 0x191f); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP8_CTR: (u32, u32) = (0x1920, 0x1920); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP8_CFG_A: (u32, u32) = (0x1921, 0x1921); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP9_CTR: (u32, u32) = (0x1924, 0x1924); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_GP9_CFG_A: (u32, u32) = (0x1925, 0x1925); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX0_CTR: (u32, u32) = (0x1980, 0x1980); + + const IA32_PMC_FX0_CFG_B: (u32, u32) = (0x1982, 0x1982); + const IA32_PMC_FX0_CFG_C: (u32, u32) = (0x1983, 0x1983); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX1_CTR: (u32, u32) = (0x1984, 0x1984); + const IA32_PMC_FX1_CFG_B: (u32, u32) = (0x1986, 0x1986); + const IA32_PMC_FX1_CFG_C: (u32, u32) = (0x1987, 0x1987); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX2_CTR: (u32, u32) = (0x1988, 0x1988); + + const IA32_PMC_FX2_CFG_C: (u32, u32) = (0x198b, 0x198b); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX3_CTR: (u32, u32) = (0x198c, 0x198c); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX4_CTR: (u32, u32) = (0x1990, 0x1990); + const IA32_PMC_FX4_CFG_C: (u32, u32) = (0x1993, 0x1993); + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX5_CTR: (u32, u32) = (0x1994, 0x1994); + const IA32_PMC_FX5_CFG_C: (u32, u32) = (0x1997, 0x1997); + + // Disabled via CPUID for non-host CPU profiles + const IA32_PMC_FX6_CTR: (u32, u32) = (0x1998, 0x1998); + const IA32_PMC_FX6_CFG_C: (u32, u32) = (0x199b, 0x199b); + + // TODO: Check against IA32_ARCH_CAPABILITIES[12] + const IA32_UARCH_MISC_CTL: (u32, u32) = (0x1b01, 0x1b01); + /// A list of ARCHITECTURAL MSR register addresses that are forbidden for all non-host CPU profiles and also not + /// considered MSR-based FEATURE indices by KVM. + pub(in crate::x86_64) const FORBIDDEN_IA32_MSR_RANGES: [(u32, u32); 229] = [ + IA32_P5_MC_ADDR, + IA32_P5_MC_TYPE, + // TODO: Not sure about IA32_P5_MC_ADDR & IA32_P5_MC_TYPE + IA32_MONITOR_FILTER_SIZE, + // TODO: Not sure about this one + IA32_PLATFORM_ID, + /// Only available is CPUID 0x7.0x1.EBX[0] = 1, but this is always 0 for non-host CPU profiles + IA32_PPIN_CTL, + /// Only available is CPUID 0x7.0x1.EBX[0] = 1, but this is always 0 for non-host CPU profiles + IA32_PPIN, + /// Used for microcode updates. Should not be available for guests. + IA32_BIOS_UPDT_TRIG, + /// Currently only related to Secure enclaves/Keylocker which is not available for non-host CPU profiles + IA32_FEATURE_ACTIVATION, + IA32_FZM_RANGE_INDEX, + IA32_SMRR_PHYS_BASE_MASK, + IA32_PECI_HWP_REQUEST_INFO, + /// Related to microcode updates + IA32_MCU_ENUMERATION, + IA32_MCU_STATUS, + /// Related to total memory encryption + IA32_MKTME_KEYID_PARTITIONING, + // TODO: Not sure what to do about IA32_BIOS_SIGN_ID (note that it is also a MSR-based feature according to KVM) + IA32_SGXLEPUBKEYHASH0, + IA32_SGXLEPUBKEYHASH1, + IA32_SGXLEPUBKEYHASH2, + IA32_SGXLEPUBKEYHASH3, + IA32_SGXLEPUBKEYHASH4, + IA32_SGXLEPUBKEYHASH5, + // TODO: Check this + IA32_SMBASE, + IA32_MISC_PACKAGE_CTLS, + IA32_XAPIC_DISABLE_STATUS, + IA32_OVERCLOCKING_STATUS, + IA32_PMC0, + IA32_PMC1, + IA32_PMC2, + IA32_PMC3, + IA32_PMC4, + IA32_PMC5, + IA32_PMC6, + IA32_PMC7, + IA32_PMC8, + IA32_PMC9, + IA32_CORE_CAPABILITIES, + IA32_UMWAIT_CONTROL, + IA32_CLOCK_MODULATION, + IA32_PLI_SSP, + IA32_INTERRUPT_SSP_TABLE_ADDR, + // Disabled by CPUID for non-host CPU profiles + IA32_MPERF, + IA32_APERF, + IA32_TSX_FORCE_ABORT, + // Disabled via static IA32_ARCH_CAPABILITIES bit for non-host CPU profiles + IA32_TSX_CTRL, + // NOTE: IA32_MCU_OPT_CTRL must necessarily be available, due to + // what we set in CPUID for some CPU profiles (inherit policy) + + // TODO: Don't know about IA32_SYSENTER_CS, IA32_SYSENTER_ESP, + // IA32_SYSENTER_EIP + // + IA32_MCG_CTL, + // TODO: 0x180- 0x185 is reserved, we should not list these MSRS at all + /// Disabled via CPUID for all non-host CPU profiles + IA32_PERFEVTSEL0, + IA32_PERFEVTSEL1, + IA32_PERFEVTSEL2, + IA32_PERFEVTSEL3, + IA32_PERFEVTSEL4, + IA32_PERFEVTSEL5, + IA32_PERFEVTSEL6, + IA32_PERFEVTSEL7, + IA32_PERFEVTSEL8, + IA32_PERFEVTSEL9, + // TODO: 0x18a - 0x194 is reserved and should not be included in any list + + // TODO: 0x196, 197 is reserved and should not be included in any list + // + IA32_PERF_STATUS, + IA32_PERF_CTL, + // Disabled via CPUID for non-host profiles + IA32_THERM_INTERRUPT, + // Disabled via CPUID for non-host profiles + IA32_THERM_STATUS, + // TODO: Consider disabling IA32_MISC_ENABLE + + // Disabled via CPUID for non-host profiles + IA32_ENERGY_PERF_BIAS, + // Disabled via CPUID for non-host profiles + IA32_PACKAGE_THERM_STATUS, + // Disabled via CPUID for non-host profiles + IA32_PACKAGE_THERM_INTERRUPT, + IA32_DEBUGCTL, + IA32_LER_FROM_IP, + IA32_LER_TO_IP, + IA32_LER_INFO, + // TODO: Not sure about IA32_SMRR_PHYSBASE & IA32_SMRR_PHYSMASK + IA32_MC_I_CTL2, + // Disabled via CPUID for non-host profiles + IA32_INTEGRITY_STATUS, + IA32_FIXED_CTRI, + // IA32_PERF_CAPABILITIES is an MSR-based feature thus not listed here + + // Disabled via CPUID for non-host profiles + IA32_FIXED_CTR_CTRL, + // Disabled via CPUID for non-host profiles + IA32_PERF_GLOBAL_STATUS, + // Disabled via CPUID for non-host profiles + IA32_PERF_GLOBAL_CTRL, + // Disabled via CPUID for non-host profiles + IA32_PERF_GLOBAL_STATUS_RESET, + // Disabled via CPUID for non-host profiles + IA32_PERF_GLOBAL_STATUS_SET, + // Disabled via CPUID for non-host profiles + IA32_PERF_GLOBAL_INUSE, + // TODO: Not sure about this one, but seems to be related to performance monitoring which + // should be disabled for non-host CPU profiles. + IA32_PEBS_ENABLE, + IA32_A_PMC0, + IA32_A_PMC1, + IA32_A_PMC2, + IA32_A_PMC3, + IA32_A_PMC4, + IA32_A_PMC5, + IA32_A_PMC6, + IA32_A_PMC7, + IA32_A_PMC8, + IA32_A_PMC9, + IA32_MCG_EXT_CTL, + // SGX is disabled via CPUID for non-host CPU profiles + IA32_SGX_SVN_STATUS, + // Disabled via CPUID for non-host CPU profiles + IA32_RTIT_OUTPUT_BASE, + // Disabled via CPUID for non-host CPU profiles + IA32_RTIT_OUTPUT_MASK_PTRS, + // Disabled via CPUID for non-host CPU profiles + IA32_RTIT_CTL, + // Disabled via CPUID for non-host CPU profiles + IA32_RTIT_STATUS, + // Disabled via CPU profiles + IA32_RTIT_CR3_MATCH, + IA32_RTIT_ADDR0_A, + IA32_RTIT_ADDR0_B, + IA32_RTIT_ADDR1_A, + IA32_RTIT_ADDR1_B, + IA32_RTIT_ADDR2_A, + IA32_RTIT_ADDR2_B, + IA32_RTIT_ADDR3_A, + IA32_RTIT_ADDR3_B, + // Disabled via CPUID for non-host CPU profiles + IA32_DS_AREA, + IA32_U_CET, + IA32_S_CET, + // Disabled via CPUID for non-host CPU profiles + IA32_PKRS, + // Disabled via CPUID for non-host CPU profiles + IA32_PM_ENABLE, + // Disabled via CPUID for non-host CPU profiles + IA32_HWP_CAPABILITIES, + // Disabled via CPUID for non-host CPU profiles + IA32_HWP_REQUEST_PKG, + // Disabled via CPUID for non-host CPU profiles + IA32_HWP_INTERRUPT, + // Disabled via CPUID for non-host CPU profiles + IA32_HWP_REQUEST, + // TODO: Can we also deny IA32_PECI_HWP_REQUEST_INFO? + + // Disabled via CPUID for non-host CPU profiles + IA32_HWP_CTL, + // Disabled via CPUID for non-host CPU profiles + IA32_HWP_STATUS, + // TODO: Currently permitted via IA32_ARCH_CAPABILITIES (bit 22), + // but that bit should probably have policy Static(0) ? + IA32_MCU_EXT_SERVICE, + IA32_MCU_ROLLBACK_MIN_ID, + // TODO: Not sure about IA32_MCU_STAGING_MBOX_ADDR + IA32_ROLLBACK_SIGN_ID_0, + IA32_ROLLBACK_SIGN_ID_1, + IA32_ROLLBACK_SIGN_ID_2, + IA32_ROLLBACK_SIGN_ID_3, + IA32_ROLLBACK_SIGN_ID_4, + IA32_ROLLBACK_SIGN_ID_5, + IA32_ROLLBACK_SIGN_ID_6, + IA32_ROLLBACK_SIGN_ID_7, + IA32_ROLLBACK_SIGN_ID_8, + IA32_ROLLBACK_SIGN_ID_9, + IA32_ROLLBACK_SIGN_ID_10, + IA32_ROLLBACK_SIGN_ID_11, + IA32_ROLLBACK_SIGN_ID_12, + IA32_ROLLBACK_SIGN_ID_13, + IA32_ROLLBACK_SIGN_ID_14, + IA32_ROLLBACK_SIGN_ID_15, + // Disabled via CPUID for non-host CPU profiles + IA32_TME_CAPABILITY, + // Disabled via CPUID for non-host CPU profiles + IA32_TME_ACTIVATE, + // Disabled via CPUID for non-host CPU profiles + IA32_TME_EXCLUDE_MASK, + // Disabled via CPUID for non-host CPU profiles + IA32_TME_EXCLUDE_BASE, + // Disabled via CPUID for non-host CPU profiles + IA32_UINTR_RR, + // Disabled via CPUID for non-host CPU profiles + IA32_UINTR_HANDLER, + // Disabled via CPUID for non-host CPU profiles + IA32_UINTR_STACKADJUST, + // Disabled via CPUID for non-host CPU profiles + IA32_UINTR_MISC, + // Disabled via CPUID for non-host CPU profiles + IA32_UINTR_PD, + // Disabled via CPUID for non-host CPU profiles + IA32_UINTR_TT, + // Disabled via CPUID for non-host CPU profiles + IA32_COPY_STATUS, + // Disabled via CPUID for non-host CPU profiles + IA32_IWKEYBACKUP_STATUS, + IA32_TME_CLEAR_SAVED_KEY, + // Disabled via CPUID for non-host CPU profiles + IA32_DEBUG_INTERFACE, + // Disabled via CPUID for non-host CPU profiles + IA32_L3_QOS_CFG, + // Disabled via CPUID + IA32_L2_QOS_CFG, + // Disabled via CPUID + IA32_L3_IO_QOS_CFG, + IA32_RESOURCE_PRIORITY, + IA32_RESOURCE_PRIORITY_PKG, + // Disabled via CPUID for non-host CPU profiles + IA32_QM_EVTSEL, + // Disabled via CPUID for non-host CPU profiles + IA32_QM_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PQR_ASSOC, + // Disabled via CPUID for non-host CPU profiles + IA32_L3_MASK_0, + IA32_L3_MASK_N, + // Disabled via CPUID for non-host CPU profiles + IA32_L2_MASK_0, + // Disabled via CPUID for non-host CPU profiles + IA32_L2_MASK_N, + // Disabled via CPUID for non-host CPU profiles + IA32_L2_QOS_EXT_BW_THRTL_I, + // Disabled via CPUID for non-host CPU profiles + IA32_BNDCFGS, + // Disabled via CPUID for non-host CPU profiles + IA32_COPY_LOCAL_TO_PLATFORM, + // Disabled via CPUID for non-host CPU profiles + IA32_COPY_PLATFORM_TO_LOCAL, + IA32_PASID, + IA32_XSS, + // Disabled via CPUID for non-host CPU profiles + IA32_PKG_HDC_CTL, + // Disabled via CPUID for non-host CPU profiles + IA32_PM_CTL1, + // Disabled via CPUID for non-host CPU profiles + IA32_THREAD_STALL, + // Disabled via CPUID for non-host CPU profiles + IA32_QOS_CORE_BW_THRTL_0, + // Disabled via CPUID for non-host CPU profiles + IA32_QOS_CORE_BW_THRTL_1, + // TODO: Is it OK to disable this for CPU profiles? + // Note that we have CPUID 0x7.EDX.[19] = 0 (ARCH_LBR) + IA32_LBR_X_INFO, + // TDX related. + IA32_SEAMRR_BASE, + // TDX related. + IA32_SEAMRR_MASK, + // Disabled via ARCH_CAPABILITIES for non-host CPU profiles + IA32_MCU_CONTROL, + IA32_LBR_CTL, + IA32_LBR_DEPTH, + IA32_LBR_X_FROM_IP, + IA32_LBR_X_TO_IP, + // Disabled via CPUID for non-host CPU profiles + IA32_HW_FEEDBACK_PTR, + // Disabled via CPUID for non-host CPU profiles + IA32_HW_FEEDBACK_CONFIG, + // Disabled via CPUID for non-host CPU profiles + IA32_HW_FEEDBACK_THREAD_CHAR, + IA32_HW_FEEDBACK_THREAD_CONFIG, + IA32_HRESET_ENABLE, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP0_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP0_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP0_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP1_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP1_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP1_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP2_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP2_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP2_CFG_B, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP2_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP3_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP3_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP3_CFG_B, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP3_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP4_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP4_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP4_CFG_B, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP4_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP5_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP5_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP5_CFG_B, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP5_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP6_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP6_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP6_CFG_B, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP6_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP7_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP7_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP7_CFG_B, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP7_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP8_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP8_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP9_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_GP9_CFG_A, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX0_CTR, + IA32_PMC_FX0_CFG_B, + IA32_PMC_FX0_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX1_CTR, + IA32_PMC_FX1_CFG_B, + IA32_PMC_FX1_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX2_CTR, + IA32_PMC_FX2_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX3_CTR, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX4_CTR, + IA32_PMC_FX4_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX5_CTR, + IA32_PMC_FX5_CFG_C, + // Disabled via CPUID for non-host CPU profiles + IA32_PMC_FX6_CTR, + IA32_PMC_FX6_CFG_C, + IA32_UARCH_MISC_CTL, + ]; +} diff --git a/arch/src/x86_64/msr_definitions/intel/mod.rs b/arch/src/x86_64/msr_definitions/intel/mod.rs new file mode 100644 index 0000000000..c8e8a91d5c --- /dev/null +++ b/arch/src/x86_64/msr_definitions/intel/mod.rs @@ -0,0 +1,21 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[cfg(feature = "cpu_profile_generation")] +mod architectural_msrs; + +#[cfg(feature = "cpu_profile_generation")] +mod non_architectural_msrs; + +mod msr_based_features; + +#[cfg(feature = "cpu_profile_generation")] +pub(in crate::x86_64) use architectural_msrs::FORBIDDEN_IA32_MSR_RANGES; +#[cfg(feature = "cpu_profile_generation")] +pub(in crate::x86_64) use architectural_msrs::PERMITTED_IA32_MSRS; +pub use msr_based_features::INTEL_MSR_FEATURE_DEFINITIONS; +pub(in crate::x86_64) use msr_based_features::check_feature_msr_compatibility; +#[cfg(feature = "cpu_profile_generation")] +pub(in crate::x86_64) use non_architectural_msrs::NON_ARCHITECTURAL_INTEL_MSRS; diff --git a/arch/src/x86_64/msr_definitions/intel/msr_based_features.rs b/arch/src/x86_64/msr_definitions/intel/msr_based_features.rs new file mode 100644 index 0000000000..e5cb7b214d --- /dev/null +++ b/arch/src/x86_64/msr_definitions/intel/msr_based_features.rs @@ -0,0 +1,4442 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::collections::HashMap; + +use log::{debug, error, warn}; + +use crate::x86_64::msr_definitions::{ + MsrDefinitions, ProfilePolicy, RegisterAddress, ValueDefinition, ValueDefinitions, +}; + +impl RegisterAddress { + pub const IA32_BIOS_SIGN_ID: Self = Self(0x8b); + pub const IA32_ARCH_CAPABILITIES: Self = Self(0x10a); + pub const IA32_PERF_CAPABILITIES: Self = Self(0x345); + pub const IA32_VMX_BASIC: Self = Self(0x480); + pub const IA32_VMX_PINBASED_CTLS: Self = Self(0x481); + pub const IA32_VMX_PROCBASED_CTLS: Self = Self(0x482); + pub const IA32_VMX_EXIT_CTLS: Self = Self(0x483); + pub const IA32_VMX_ENTRY_CTLS: Self = Self(0x484); + pub const IA32_VMX_MISC: Self = Self(0x485); + pub const IA32_VMX_CR0_FIXED0: Self = Self(0x486); + pub const IA32_VMX_CR0_FIXED1: Self = Self(0x487); + pub const IA32_VMX_CR4_FIXED0: Self = Self(0x488); + pub const IA32_VMX_CR4_FIXED1: Self = Self(0x489); + pub const IA32_VMX_VMCS_ENUM: Self = Self(0x48a); + pub const IA32_VMX_PROCBASED_CTLS2: Self = Self(0x48b); + pub const IA32_VMX_EPT_VPID_CAP: Self = Self(0x48c); + pub const IA32_VMX_TRUE_PINBASED_CTLS: Self = Self(0x48d); + pub const IA32_VMX_TRUE_PROCBASED_CTLS: Self = Self(0x48e); + pub const IA32_VMX_TRUE_EXIT_CTLS: Self = Self(0x48f); + pub const IA32_VMX_TRUE_ENTRY_CTLS: Self = Self(0x490); + pub const IA32_VMX_VMFUNC: Self = Self(0x491); + pub const IA32_VMX_PROCBASED_CTLS3: Self = Self(0x492); + pub const IA32_VMX_EXIT_CTLS2: Self = Self(0x493); + + // =============== Non-architectural MSRs ======== + + // KVM + Intel Skylake reports this as an MSR-based feature + pub const MSR_PLATFORM_INFO: Self = Self(0xce); +} + +/// This table contains descriptions of all the MSRs whose register addresses can be contained in +/// the list returned by `KVM_GET_MSR_FEATURE_INDEX_LIST` when executed on an Intel CPU. +/// +/// The values described here are based on the Intel 64 and IA-32 Architectures Software Developer's +/// Manual Combined Volumes: 1,2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4 from October 2025. +/// +/// We try to use the same short descriptions as Intel, but in the cases where we could not find an +/// official name for the bit field(s) we invented our own based on the description. +/// +/// The descriptions written here are based on those found in the aforementioned manual, but often less +/// detailed. We recommend consulting the official Intel documentation whenever more information +/// is required. +/// +/// +/// ## Future-proofing +/// +/// Future processors and/or KVM versions may of course introduce more MSR-based features than those listed here at this time of writing. +/// In order to make sure that this is taken into account, the CPU profile generation tool will error when this is detected. The person +/// attempting to create a new CPU profile should then update this table accordingly and try again. +pub static INTEL_MSR_FEATURE_DEFINITIONS: MsrDefinitions<24> = const { + MsrDefinitions([ + ( + RegisterAddress::IA32_BIOS_SIGN_ID, + ValueDefinitions::new(&[ + ValueDefinition { + short: "PATCH_SIGN_ID", + description: "Any non-zero value is the microcode update signature patch signature ID", + bits_range: (32, 63), + policy: ProfilePolicy::Passthrough, + } + ]) + ), + + ( + RegisterAddress::IA32_ARCH_CAPABILITIES, + ValueDefinitions::new(&[ + ValueDefinition { + short: "RDCL_NO", + description: "The processor is not susceptible to Rogue Data Cache Load (RDCL)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "IBRS_ALL", + description: "The processor supports enhanced IBRS", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit, + }, + // Skylake has this bit set, but not Sapphire Rapids + // TODO: Is Inherit the right policy here? (Will it still be possible to use the Skylake profile on a Sapphire Rapids machine?) + ValueDefinition { + short: "RSBA", + description: "The processor supports RSB Alternate", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "SKIP_L1DFL_VMENTRY", + description: "A value of 1 indicates the hypervisor need not flush the L1D on VM entry", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "SSB_NO", + description: "Processor is not susceptible to Speculation Store Bypass", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "MDS_NO", + description: "Processor is not susceptible to Microarchitectural Data Sampling (MDS)", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "IF_PSCHANGE_MC_NO", + description: "The processor is not susceptible to a machine check error due to modifying the size of a code page without TLB invalidation", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "TSX_CTRL", + description: "If 1, indicates presence of IA32_TSX_CTRL MSR", + bits_range: (7, 7), + // TSX is riddled with CVEs + // TODO: Check that this is indeed the right policy + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "TAA_NO", + description: "If 1, processor is not affected by TAA", + bits_range: (8, 8), + // This is TSX related which we disable anyway + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "MCU_CONTROL", + description: "If 1, the processor supports the IA32_MCU_CONTROL MSR", + bits_range: (9, 9), + // TODO: Check what the IA32_MCU_CONTROL MSR is + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "MISC_PACKAGE_CTLS", + description: "The processor supports IA32_MISC_PACKAGE_CTLS MSR", + bits_range: (10, 10), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "ENERGY_FILTERING_CTL", + description: "The processor supports setting and reading the IA32_MISC_PACKAGE_CTLS[0] (ENERGY_FILTERING_ENABLE) bit", + bits_range: (11, 11), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "DOITM:", + description: "If 1, the processor supports Data Operand Independent Timing Mode", + bits_range: (12, 12), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "SBDR_SSDP_NO", + description: "The processor is not affected by either the Shared Buffers Data Read (SBDR) vulnerability or the Sideband Stale Data Propagator (SSDP)", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "FBSDP_NO", + description: "The processor is not affected by the Fill Buffer Stale Data Propagator (DBSDP)", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "PSDP_NO", + description: "The processor is not affected by vulnerabilities involving the Primary Stale Data Propagator (PSDP)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "MCU_ENUMERATION", + description: "If 1, the processor supportss the IA32_MCU_ENUMERATION and IA32_MCU_STATUS MSRs", + bits_range: (16, 16), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "FB_CLEAR", + description: "If 1, the processor supports overwrite of fill buffer values as part of MD_CLEAR operations with the VERW instruction. + On these processors L1D_FLUSH does not overwrite fill buffer values", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "FB_CLEAR_CTRL", + description: "If 1, the processor supports the IA32_MCU_OPT_CTRL MSR and allows software to set bit 3 of that MSR (FB_CLEAR_DIS)", + bits_range: (18, 18), + policy: ProfilePolicy::Static(0), + }, + + ValueDefinition { + short: "RRSBA", + description: "A value of 1 indicates the processor may have the RRSBA alternate prediction behavior, if not disabled by RRSBA_DIS_U or RRSBA_DIS_S", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "BHI_NO", + description: "A value of 1 indicates BHI_NO branch prediction behavior, regardless of the value of IA32_SPEC_CTRL[BHI_DIS_S] MSR bit", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "XAPIC_DISABLE_STATUS", + description: "Enumerates that the IA32_XAPIC_DISABLE_STATUS MSR exists, and that bit 0 specifies whether the legacy xAPIC is disabled and APIC state is locked to x2APIC", + bits_range: (21, 21), + policy: ProfilePolicy::Static(0), + }, + + ValueDefinition { + short: "MCU_EXTENDED_SERVICE", + description: "If 1, the processor supports MCU extended servicing - IA32_MCU_EXT_SERVICE MSR", + bits_range: (22, 22), + // TODO: Check + policy: ProfilePolicy::Static(0), + }, + + ValueDefinition { + short: "OVERCLOCKING_STATUS", + description: "If set, the IA32_OVERCLOCKING_STATUS MSR exists", + bits_range: (23, 23), + // TODO: Check + policy: ProfilePolicy::Static(0), + }, + + ValueDefinition { + short: "PBRSB_NO", + description: "If 1, the processor is not affected by issues related to Post-Barrier Return Stack Buffer Predictions", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "GDS_CTRL", + description: "If 1, the processor supports the GDS_MITG_DIS and GDS_MITG_LOCK bits of the IA32_MCU_OPT_CTRL MSR", + bits_range: (25, 25), + // TODO: Check + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "GDS_NO", + description: "If 1, the processor is not affected by Gather Data Sampling", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "RFDS_NO", + description: "If 1, processor is not affected by Register File Data Sampling", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "RFDS_CLEAR", + description: "If 1, when VERW is executed the processor will clear stale data from register files affected by Register File Data Sampling", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "IGN_UMONITOR_SUPPORT", + description: "If 0, IA32_MCU_OPT_CTRL bit 6 (IGN_UMONITOR) is not supported. If 1, it indicates support of IA32_MCU_OPT_CTRL bit 6 (IGN_UMONITOR)", + bits_range: (29, 29), + policy: ProfilePolicy::Static(0), + }, + + ValueDefinition { + short: "MON_UMON_MITG_SUPPORT", + description: "If 1, indicates support for IA32_MCU_OPT_CTRL bit 7 (MON_UMON_MITG), otherwise it is not supported", + bits_range: (30, 30), + policy: ProfilePolicy::Static(0), + }, + + ValueDefinition { + short: "PBOPT_SUPPORT", + description: "If 1, IA32_PBOPT_CTRL bit 0 (Prediction Barrier Option (PBOPT)) is supported, otherwise it is not", + bits_range: (32, 32), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "ITS_NO", + description: "If 0, the hypervisor indicates that the system is not affected by indirect Target Selection. If 1, then the hypervisor + indicates that the system may be affected by indirect Target Selection", + bits_range: (62, 62), + policy: ProfilePolicy::Passthrough, + + }, + + ]), + ), + + ( + RegisterAddress::IA32_PERF_CAPABILITIES, + ValueDefinitions::new(&[ + ValueDefinition { + short: "IA32_PERF_CAPABILITIES", + description: "Read Only MSR that enumerates the existence of performance monitoring features", + bits_range: (0, 63), + // This MSR is only valid if CPUID 0x1.ECX[15] is set, but that bit is always zeroed out for CPU profiles different from host + policy: ProfilePolicy::Deny + } + ]) + ), + + ( + RegisterAddress::IA32_VMX_BASIC, + ValueDefinitions::new(&[ + ValueDefinition { + short: "VMCS_REV_ID", + description: "31-bit VMCS revision identifier. Processors that use the same VMCS revision identifier + use the same size for VMCS regions", + bits_range: (0,31), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "REGION_SIZE", + description: "Number of bytes that software should allocate for the VMXON region and any VMCS region. It is a value greater than + 0 and at most 4096", + bits_range: (32, 44), + policy: ProfilePolicy::Inherit, + }, + + ValueDefinition { + short: "DUAL_MON", + description: " If 1, the logical processor supports the dual-monitor treatment of system-management + interrupts and system-management mode. See Section 33.15 for details of this treatment", + bits_range: (49, 49), + // TODO: Should we have Static(0)? here (I think that might be equivalent to what QEMU does) + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "MEM_TYPE", + description: "The memory type that should be used for the VMCS, for data structures referenced by pointers + in the VMCS (I/O bitmaps, virtual-APIC page, MSR areas for VMX transitions), and for the MSEG header", + bits_range: (50, 53), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "VM_EXIT_INFO_INS_OUTS", + description: " If 1, the processor reports information in the VM-exit instruction-information field on VM exits + due to execution of the INS and OUTS instructions. + ", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "VMX_CTRLS_DEFAULT_MUT", + description: "Any VMX controls that default to 1 may be cleared to 0", + bits_range: (55,55), + policy: ProfilePolicy::Inherit + }, + // This is only available for relatively recent kernels + // TODO: Revisit this policy + ValueDefinition { + short: "VM_ENTRY_HARDWARE_EXCEPTIONS", + description: "If 1, then software can use VM entry to deliver a hardware exception", + bits_range: (56, 56), + policy: ProfilePolicy::Static(0) + } + ]) + ), + + ( + RegisterAddress::IA32_VMX_PINBASED_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short:"ALLOWED_ZERO_EXTERNAL_INTERRUPT_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_1_2", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (1, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_NMI_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_4", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_VIRTUAL_NMIS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_VMX_PREEMPTION_TIMER", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_PROCESS_POSTED_INTERRUPTS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + + + ValueDefinition { + short: "ALLOWED_ZERO", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (8, 31), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short:"ALLOWED_ONE_EXTERNAL_INTERRUPT_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (32, 32), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_1_2", + description: "VM entry allows control X to be 1 if bit X in this MSR is 1", + bits_range: (33, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_NMI_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (35, 35), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_4", + description: "VM entry allows control X to be 1 if bit X in this MSR is 1", + bits_range: (36, 36), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_VIRTUAL_NMIS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (37, 37), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_ACTIVATE_VMX__PREEMPTION_TIMER", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (38, 38), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_PROCESS_POSTED_INTERRUPTS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (39, 39), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (40, 63), + policy: ProfilePolicy::Inherit + } + ]) + ), + + ( + RegisterAddress::IA32_VMX_PROCBASED_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ZERO_0_1", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (0, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_INTERRUPT_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_TSC_OFFSETTING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_4_6", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (4, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_HLT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_8", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_INVLPG_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MWAIT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_RDPMC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_RDTSC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_13_14", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (13, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR3_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR3_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_TERTIARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_18", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR8_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR8_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_TPR_SHADOW", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_NMI_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MOV_DR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_UNCONDITIONAL_I/O_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_I/O_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_26", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MONITOR_TRAP_FLAG", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_MSR_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MONITOR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_PAUSE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_0_1", + description: "Control X is allowed to be 1 if bit 32 + X of this MSR is 1", + bits_range: (32, 33), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_INTERRUPT_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_TSC_OFFSETTING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (35, 35), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_4_6", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (36, 38), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_HLT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (39, 39), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_8", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (40, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_INVLPG_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MWAIT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (42, 42), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_RDPMC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (43, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_RDTSC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (44, 44), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "ALLOWED_ONE_13_14", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (45, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CR3_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CR3_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (48, 48), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ACTIVATE_TERTIARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (49, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_18", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short:"ALLOWED_ONE_CR8_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CR8_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_TPR_SHADOW", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_NMI_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MOV_DR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (55, 55), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_UNCONDITIONAL_I/O_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (56, 56), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_I/O_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_26", + description: "Control X is allowed to be 1 if bit X of this MSR is 1", + bits_range: (58, 58), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short:"ALLOWED_ONE_MONITOR_TRAP_FLAG", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (59, 59), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_MSR_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (60, 60), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MONITOR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (61, 61), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_PAUSE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (62, 62), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (63, 63), + policy: ProfilePolicy::Inherit + }, + + ]) + ), + + ( + RegisterAddress::IA32_VMX_EXIT_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ZERO_0_1", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (0, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_3_8", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (3, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_HOST_ADDRESS_SPACE_SIZE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_10_11", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (10, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_13_14", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (13, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACKNOWLEDGE_INTERRUPT_O_EXIT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_16_17", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (16, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_VMX_PREEMPTION_TIMER_VALUE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (26, 26), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_UINV", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + // TODO: Also determines whether SSP is loaded on VM exit (do we need that?) + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (28, 28), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_IA32_PERF_GLOBAL_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_0_1", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (32, 33), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short:"ALLOWED_ONE_SAVE_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "ALLOWED_ONE_3_8", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (35, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_HOST_ADDRESS_SPACE_SIZE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_10_11", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (42, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (44, 44), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "ALLOWED_ONE_13_14", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (45, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ACKNOWLEDGE_INTERRUPT_O_EXIT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_16_17", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (48, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_VMX_PREEMPTION_TIMER_VALUE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (55, 55), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (56, 56), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (58, 58), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_UINV", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (59, 59), + policy: ProfilePolicy::Inherit + }, + // TODO: Also determines whether SSP is loaded on VM exit (do we need that?) + ValueDefinition { + short:"ALLOWED_ONE_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (60, 60), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (61, 61), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_IA32_PERF_GLOBAL_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (62, 62), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (63, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + ( + RegisterAddress::IA32_VMX_ENTRY_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ZERO_0_1", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (0, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_3_8", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (3, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_IA_32E_MODE_GUES", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENTRY_TO_SMM", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_DEACTIVATE_DUAL__MONITOR_TREATMENT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_12", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_UINV", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + // TODO: Also determines whether SSP is loaded on VM exit (do we need that?) + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_GUEST_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_23_24", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (23, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ALLOW_SEAM_GUEST_TELEMETRY", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_26_31", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (26, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_0_1", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (32, 33), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_3_8", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (35, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_IA_32E_MODE_GUES", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENTRY_TO_SMM", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (42, 42), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_DEACTIVATE_DUAL__MONITOR_TREATMENT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (43, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_12", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (44, 44), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (45, 45), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (46, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (48, 48), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (49, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_UINV", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_GUEST_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_23_24", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (55, 56), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ALLOW_SEAM_GUEST_TELEMETRY", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_26_31", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (58, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + ( + RegisterAddress::IA32_VMX_MISC, + ValueDefinitions::new(&[ + ValueDefinition { + short: "VMX_PREEMPTION_TSC_REL", + description: "specifies the relationship between the rate of the VMX-preemption timer and that of the timestamp counter (TSC)", + bits_range: (0, 4), + policy: ProfilePolicy::Passthrough + }, + ValueDefinition { + short: "IA32_EFER.LMA_STORE", + description: "If 1, then VM exits store the value of IA32_EFER.LMA into the IA32-e mode guest VM-entry control", + bits_range: (5,5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "HLT_STATE", + description: "Activity state 1 (HLT) is supported", + bits_range: (6,6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "SHUTDOWN_STATE", + description: "Activity state 2 (shutdown) is supported", + bits_range: (7,7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "WAIT_FOR_SIPI__STATE", + description: "Activity state 3 (wait-for-SIPI) is supported", + bits_range: (8,8), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "VMX_INTEL_PT", + description: "If 1 then Intel Processor Trace can be used in VMX operation", + bits_range: (14,14), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "RDMSR_SMM", + description: "If 1 then the RDMSR instruction can be used in system management mode (SMM) to read the IA32_SMBASE MSR", + bits_range: (15,15), + // TODO: Is this a reasonable policy? + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "VMX_NUM_CR3", + description: "The number of CR3-target values supported by the processor", + bits_range: (16,24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "MAX_MSR_STORE_LISTS", + description: "If N then 512*(N +1) is the recommended maximum number of MSRs to be included each of the VM-exit MSR-store list, VM-exit-MSR-load-list, VM-entry MSR-load list", + bits_range: (25, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "SMM_MONITOR_CTL_BIT2", + description: "If set then bit 2 of the IA32_SMM_MONITOR_CTL can be set to 1", + // TODO: Check policy. Perhaps this should rather be Static(0) ? + bits_range: (28, 28), + policy: ProfilePolicy::Inherit, + }, + ValueDefinition { + short: "VM_WRITE_EXIT_FIELDS", + description: "If 1 then software can use VMWRITE to write to any supported field in the VMCS", + bits_range: (29,29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "VM_ENTRY_INJECTION", + description: "If 1 then VM entry permits injection of the following: software interrupt, software exception, or privileged software exception with an instruction length of 0", + bits_range: (30,30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "MSEG_REV_ID", + description: "MSEG revision identifier used by the processor", + bits_range: (32,63), + // TODO: Should this be Passthrough? + policy: ProfilePolicy::Inherit + }, + ]) + ), + + ( + RegisterAddress::IA32_VMX_CR0_FIXED0, + // NOTE 1: If any entry in IA32_VMX_CR0_FIXED1 has ProfilePolicy::Stattic(0) then the corresponding entry here must also have ProfilePolicy::Static(0) + // + // NOTE 2: We use the inherit policy for reserved fields. + ValueDefinitions::new(&[ + ValueDefinition { + short: "CR0.PE", + description: "If 0, then bit 0 (Protection Enable) of CR0 is allowed to be 0. bit 0 of CR0 enables real-address mode when clear.", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.MP", + description: "If 0, then bit 1 (Monitor Coprocessor) of CR0 is allowed to be 0. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit + }, + // We expect this to be 0 for all modern processors, but Inherit is fine. + ValueDefinition { + short: "CR0.EM", + description: "If 0, then bit 2 (Emulation) of CR0 is allowed to be 0. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.TS", + description: "If 0, then bit 3 (Task Switched) of CR0 is allowed to be 0. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.ET", + description: "If 0, then bit 4 (Extension Type) of CR0 is allowed to be 0. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.NE", + description: "If 0, then bit 5 (Numeric Error) of CR0 is allowed to be 0. Enables the PC-style x87 FPU error reporting mechanism when clear in CR0.", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_6_15", + description: "Reports bits allowed to be 0 in CR0", + bits_range: (6, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.WP", + description: "If 0, then bit 16 (Write protect) of CR0 is allowed to be 0. If this bit is clear in CR0 then supervisor-level procedures are + allowed to write into read-only pages", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_17_17", + description: "Reports bits allowed to be 0 in CR0", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.AM", + description: "If 0, then bit 18 (Alignment Mask) of CR0 is allowed to be 0. If this bit is clear in CR0 then alignment checking is disabled.", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_19_28", + description: "Reports bits allowed to be 0 in CR0", + bits_range: (19, 28), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.NW", + description: "If 0, then bit 29 (Not Write-through) of CR0 is allowed to be 0. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.CD", + description: "If 0, then bit 30 (Cache disable) of CR0 is allowed to be 0. If CR0 bits 30 and 29 are 0 then caching of memory locations + for the whole of physical memory in the processor's internal (and external) cache is enabled.", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + // TOD0: Disabling paging sounds bad, should we force this to 1? + ValueDefinition { + short: "CR0.PG", + description: "If 0, then bit 31 (Paging) of CR0 is allowed to be 0. If bit 31 of CR0 is cleared then paging is disabled (all linear addresses get treated as physical addresses).", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_32_63", + description: "Reports bits allowed to be 0 in CR0", + bits_range: (32, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + // NOTE: CR0_FIXED1 cannot be set by KVM, but this is OK, because its value is determined by CPUID anyway + ( + RegisterAddress::IA32_VMX_CR0_FIXED1, + ValueDefinitions::new(&[ + + ValueDefinition { + short: "CR0.PE", + description: "If 1, then bit 0 (Protection Enable) of CR0 is allowed to be 1. bit 0 of CR0 enables protected mode when set", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "CR0.MP", + description: "If 1, then bit 1 (Monitor Coprocessor) of CR0 is allowed to be 1. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit + }, + // We expect this to be 0 for all modern processors, but Inherit is fine. + ValueDefinition { + short: "CR0.EM", + description: "If 1, then bit 2 (Emulation) of CR0 is allowed to be 1. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.TS", + description: "If 1, then bit 3 (Task Switched) of CR0 is allowed to be 1. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.ET", + description: "If 1, then bit 4 (Extension Type) of CR0 is allowed to be 1. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.NE", + description: "If 1, then bit 5 (Numeric Error) of CR0 is allowed to be 1. This bit enables the native (internal) mechanism for reporting x87 FPU errors when set in CR0.", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_6_15", + description: "Reports bits allowed to be 1 in CR0", + bits_range: (6, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.WP", + description: "If 1, then bit 16 (Write protect) of CR0 is allowed to be 1. If this bit is set in CR0 then supervisor-level procedures are + inhibited from writing into read-only pages", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_17_17", + description: "Reports bits allowed to be 1 in CR0", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.AM", + description: "If 1, then bit 18 (Alignment Mask) of CR0 is allowed to be 1. If bit 18 of CR0 is set then automatic alignment checking is possible.", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_19_28", + description: "Reports bits allowed to be 1 in CR0", + bits_range: (19, 28), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.NW", + description: "If 1, then bit 29 (Not Write-through) of CR0 is allowed to be 1. See Intel SDM Vol. 3A Section 2.5 for more information", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.CD", + description: "If 1, then bit 30 (Cache disable) of CR0 is allowed to be 1. If CR0 bit 30 is 1 then caching is restricted", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR0.PG", + description: "If 1, then bit 31 (Paging) of CR0 is allowed to be 1 which enables paging", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR0_FIXED1_RESERVED_32_63", + description: "Reports bits allowed to be 1 in CR0", + bits_range: (32, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + ( + RegisterAddress::IA32_VMX_CR4_FIXED0, + ValueDefinitions::new(&[ + ValueDefinition { + short: "CR4.VME", + description: "If 0, then bit 0 (Virtual-8086 Mode Extension) of CR4 is allowed to be 0. Bit 0 of CR4 disables the interrupt and exception-handling extensions in virtual-8086 mode when clear.", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PVI", + description: "If 0, then bit 1 (Protected-Mode Virtual Interrupts) of CR4 is allowed to be 0. Bit 1 of CR4 disables the virtual interrupt flag in protected mode when clear.", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.TSD", + description: "If 0, then bit 2 (Time Stamp Disable) of CR4 is allowed to be 0. Bit 2 of CR4 allows RDTSC instruction to be executed at any privilege level when clear.", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.DE", + description: "If 0, then bit 3 (Debugging extensions) of CR4 is allowed to be 0. When Bit 3 of CR4 is clear the processor aliases references to registers DR4 and DR5 for compatibility with legacy software", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PSE", + description: "If 0, then bit 4 (Page Size Extensions) of CR4 is allowed to be 0. Bit 4 of CR4 restricts 32-bit paging to pages of 4 KBytes when clear.", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PAE", + description: "If 0, then bit 5 (Physical Address Extension) of CR4 is allowed to be 0. Bit 5 of CR4 restricts physical addresses to 32 bits when clear", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.MCE", + description: "If 0, then bit 6 (Machine-Check Enable) of CR4 is allowed to be 0. Bit 6 of CR4 disables the machine-check exception when clear", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PGE", + description: "If 0, then bit 7 (Page Global Enable) of CR4 is allowed to be 0. Bit 7 of CR4 disables the global page feature when clear", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PCE", + description: "If 0, then bit 8 (Performance-Monitoring Counter Enable) of CR4 is allowed to be 0. The RDPMC instruction can only be executed at protection level 0 when bit 8 of CR4 is clear", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.OSFXSR", + description: "If 0, then bit 9 (OS Support for FXSAVE and FXRSTOR) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.OSXMMEXCPT", + description: "If 0, then bit 10 (OS Support for Unmaksed SIMD Floating-Point Exceptions) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.UMIP", + description: "If 0, then bit 11 (User-Mode instruction Prevention) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + // Maybe this could even be passthrogh? CHV is 64-bit only. + ValueDefinition { + short: "CR4.LA57", + description: "If 0, then bit 12 (57-bit linear addresses) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.VMXE", + description: "If 0, then bit 13 (VMX-Enable) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.SMXE", + description: "If 0, then bit 14 (SMX-Enable) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.RESERVED_15", + description: "If 0, then bit 15 (RESERVED) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.FSGSBASE", + description: "If 0, then bit 16 (FSGSBASE-Enable) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + // Probably irrelevant? + ValueDefinition { + short: "CR4.PCIDE", + description: "If 0, then bit 17 (PCID-Enable) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.OSXSAVE", + description: "If 0, then bit 18 (XSAVE and Processor Extended States-Enable) of CR4 is allowed to be 0. See Intel SDM Vol.3A Section 2.5 for more information", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + // CPU Profiles do not support Key locker features for now + ValueDefinition { + short: "CR4.KL", + description: "If 0, then bit 19 (Key-Locker-Enable) of CR4 is allowed to be 0. When bit 19 of CR4 is set, the LOADIWKEY instruction is enabled and CPUID.0x19.EBX[0] is set if support for AES key locker instructions has been activated by system firmware", + bits_range: (19, 19), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.SMEP", + description: "If 0, then bit 20 (SMEP-Enable) of CR4 is allowed to be 0. See Intel SDM Vol 3.A Section 2.5 for more information", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.SMAP", + description: "If 0, then bit 21 (SMAP-Enable) of CR4 is allowed to be 0. See Intel SDM Vol 3.A Section 2.5 for more information", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PKE", + description: "If 0, then bit 22 (Enable protection keys for user-mode pages) of CR4 is allowed to be 0. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (22, 22), + policy: ProfilePolicy::Static(0), + }, + ValueDefinition { + short: "CR4.CET", + description: "If 0, then bit 23 (Control-flow Enforcement Technology) of CR4 is allowed to be 0. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (23, 23), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.PKS", + description: "If 0, then bit 24 (Enable protection keys for supervisor-mode pages) of CR4 is allowed to be 0. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (24, 24), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.UINTR", + description: "If 0, then bit 25 (User Interrupts Enable) of CR4 is allowed to be 0. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (25, 25), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.RESERVED_26", + description: "If 0, then bit 26 (RESERVED) of CR4 is allowed to be 0. See Intel SDM Vol.3.A Section 2.5 for more information.", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.LASS", + description: "If 0, then bit 27 (User Interrupts Enable) of CR4 is allowed to be 0. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.LAM_SUP", + description: "If 0, then bit 28 (Supervisor LAM-enable) of CR4 is allowed to be 0. See Intel SDM Vol. 3.A Section 25 for more information.", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "IA32_VMX_CR4_FIXED0", + description: "Reports bits allowed to be 0 in CR4", + bits_range: (29, 63), + policy: ProfilePolicy::Inherit + } + ]) + ), + + // NOTE: CR4_FIXED1 cannot be set by KVM, but this is OK, because its value is determined by CPUID anyway + ( + RegisterAddress::IA32_VMX_CR4_FIXED1, + ValueDefinitions::new(&[ + ValueDefinition { + short: "CR4.VME", + description: "If 1, then bit 1 (Virtual-8086 Mode Extension) of CR4 is allowed to be 1. Bit 0 of CR4 enables the interrupt and exception-handling extensions in virtual-8086 mode when set.", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PVI", + description: "If 1, then bit 1 (Protected-Mode Virtual Interrupts) of CR4 is allowed to be 1. Bit 1 of CR4 enables hardware support for a virtual interrupt flag in protected mode when set.", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.TSD", + description: "If 1, then bit 2 (Time Stamp Disable) of CR4 is allowed to be 1. Bit 2 of CR4 restricts the execution of the RDTS instruction to procedures running at privilege level 0 when set.", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.DE", + description: "If 1, then bit 3 (Debugging extensions) of CR4 is allowed to be 1. Bit 3 of CR4 make references to debug registers DR4 and DR5 cause an undefined opcode exception when set", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PSE", + description: "If 1, then bit 4 (Page Size Extensions) of CR4 is allowed to be 1. Bit 4 of CR4 enables 4-MByte pages with 32-bit paging when set", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PAE", + description: "If 1, then bit 5 (Physical Address Extension) of CR4 is allowed to be 1. Bit 5 of CR4 enables paging to produce physical addresses of more than 32 bits when set", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.MCE", + description: "If 1, then bit 6 (Machine-Check Enable) of CR4 is allowed to be 1. Bit 6 of CR4 enables the machine-check exception when set", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PGE", + description: "If 1, then bit 7 (Page Global Enable) of CR4 is allowed to be 1. Bit 7 of CR4 enables the global page feature when set", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PCE", + description: "If 1, then bit 8 (Performance-Monitoring Counter Enable) of CR4 is allowed to be 1. The RDPMC instruction can be executed at any protection level when bit 8 of CR4 is set.", + bits_range: (8, 8), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.OSFXSR", + description: "If 1, then bit 9 (OS Support for FXSAVE and FXRSTOR) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.OSXMMEXCPT", + description: "If 1, then bit 10 (OS Support for Unmaksed SIMD Floating-Point Exceptions) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + // TODO: Is this always 0 for QEMU? + ValueDefinition { + short: "CR4.UMIP", + description: "If 1, then bit 11 (User-Mode instruction Prevention) of CR4 is allowed to be 1. If bit 11 of CR4 is set and CPL > 0 then the SGDT,SIDT,SLDT,SMSW and STR instructions cannot be executed.", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + // Maybe this could even be passthrogh? CHV is 64-bit only. + ValueDefinition { + short: "CR4.LA57", + description: "If 1, then bit 12 (57-bit linear addresses) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.VMXE", + description: "If 1, then bit 13 (VMX-Enable) of CR4 is allowed to be 1. Bit 13 of CR4 enables VMX operation when set.", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.SMXE", + description: "If 1, then bit 14 (SMX-Enable) of CR4 is allowed to be 1. Bit 14 of CR4 enables SMX operation when set.", + bits_range: (14, 14), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.RESERVED_15", + description: "If 1, then bit 15 (RESERVED) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.FSGSBASE", + description: "If 1, then bit 16 (FSGSBASE-Enable) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + // Probably irrelevant? + ValueDefinition { + short: "CR4.PCIDE", + description: "If 1, then bit 17 (PCID-Enable) of CR4 is allowed to be 1. Enables process-context identifiers (PCIDs) when bit 17 of CR4 is set. Applies only in IA-32e mode", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.OSXSAVE", + description: "If 1, then bit 18 (XSAVE and Processor Extended States-Enable) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + // CPU Profiles do not support Key locker features for now + ValueDefinition { + short: "CR4.KL", + description: "If 1, then bit 19 (Key-Locker-Enable) of CR4 is allowed to be 1. When bit 19 of CR4 is set, the LOADIWKEY instruction is enabled and CPUID.0x19.EBX[0] is set if support for AES key locker instructions has been activated by system firmware", + bits_range: (19, 19), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.SMEP", + description: "If 1, then bit 20 (SMEP-Enable) of CR4 is allowed to be 1. Bit 20 of CR4 enables supervisor-mode execution prevention when set", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.SMAP", + description: "If 1, then bit 21 (SMAP-Enable) of CR4 is allowed to be 1. Bit 21 of CR4 enables supervisor-mode access prevention when set", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.PKE", + description: "If 1, then bit 22 (Enable protection keys for user-mode pages) of CR4 is allowed to be 1. When bit 22 of CR4 is set, CPUID.0x7.ECX[4] is displayed as 1. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (22, 22), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.CET", + description: "If 1, then bit 23 (Control-flow Enforcement Technology) of CR4 is allowed to be 1. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (23, 23), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.PKS", + description: "If 1, then bit 24 (Enable protection keys for supervisor-mode pages) of CR4 is allowed to be 1. See Intel SDM Vol. 3.A Section 2.5 for more information.", + bits_range: (24, 24), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.UINTR", + description: "If 1, then bit 25 (User Interrupts Enable) of CR4 is allowed to be 1. Bit 25 of CR4 enables user interrupts when set.", + bits_range: (25, 25), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "CR4.RESERVED_26", + description: "If 1, then bit 26 (RESERVED) of CR4 is allowed to be 1. See Intel SDM Vol.3A Section 2.5 for more information.", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.LASS", + description: "If 1, then bit 27 (User Interrupts Enable) of CR4 is allowed to be 1. Bit 27 of CR4 enables LASS (Linear-Address-Space Separation) when set.", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.LAM_SUP", + description: "If 1, then bit 28 (Supervisor LAM-enable) of CR4 is allowed to be 1. Bit 28 of CR4 enables LAM (linear-address masking) for supervisor pointers when set.", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "CR4.RESERVED_29_63", + description: "Reports bits allowed to be 1 in CR4", + bits_range: (29, 63), + policy: ProfilePolicy::Inherit + } + ]) + ), + + ( + RegisterAddress::IA32_VMX_VMCS_ENUM, + ValueDefinitions::new(&[ + ValueDefinition{ + short: "MAX_INDEX", + description: "highest index value used for any VCMS encoding", + bits_range: (1, 9), + policy: ProfilePolicy::Inherit + } + ]) + + ), + + ( + RegisterAddress::IA32_VMX_PROCBASED_CTLS2, + ValueDefinitions::new(&[ + // Intel SDM Vol.3D A.3.3 documents that the ALLOWED_ZERO bits are actually always 0 for this MSR. + ValueDefinition { + short:"ALLOWED_ZERO_VIRTUALIZE_APIC_ACCESSES", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_EPT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (1, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_DESCRIPTOR_TABLE_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_RDTSCP", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_VIRTUALIZE_X2APIC_MODE", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_VPID", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_WBINVD_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_UNRESTRICTED_GUEST", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_APIC_REGISTER_VIRTUALIZATION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_VIRTUAL_INTERRUPT_DELIVERY", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_PAUSE_LOOP_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_RDRAND_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_INVPCID", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_VM_FUNCTIONS", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_VMCS_SHADOWING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_ENCLS_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_RDSEED_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_PML", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_EPT_VIOLATION_#VE", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_XSAVES/XRSTORS", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_PASID_TRANSLATION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MODE_BASED_EXECUTE_CONTROL_FOR_EPT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SUB_PAGE_WRITE_PERMISSIONS_FOR_EPT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_INTEL_PT_USES_GUEST_PHYSICAL_ADDRESSES", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_TSC_SCALING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_USER_WAIT_AND_PAUSE", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENABLE_PCONFIG", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_28_29", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (28, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_VMM_BUS_LOCK_DETECTION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_INSTRUCTION_TIMEOU", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_VIRTUALIZE_APIC_ACCESSES", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (32, 32), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_EPT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (33, 33), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_DESCRIPTOR_TABLE_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_RDTSCP", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (35, 35), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_VIRTUALIZE_X2APIC_MODE", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (36, 36), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_VPID", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (37, 37), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_WBINVD_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (38, 38), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_UNRESTRICTED_GUEST", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (39, 39), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_APIC_REGISTER_VIRTUALIZATION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (40, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_VIRTUAL_INTERRUPT_DELIVERY", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_PAUSE_LOOP_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (42, 42), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_RDRAND_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (43, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_INVPCID", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (44, 44), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_VM_FUNCTIONS", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (45, 45), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_VMCS_SHADOWING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (46, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_ENCLS_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_RDSEED_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (48, 48), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_PML", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (49, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_EPT_VIOLATION_#VE", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_XSAVES/XRSTORS", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_PASID_TRANSLATION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MODE_BASED_EXECUTE_CONTROL_FOR_EPT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SUB_PAGE_WRITE_PERMISSIONS_FOR_EPT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (55, 55), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_INTEL_PT_USES_GUEST_PHYSICAL_ADDRESSES", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (56, 56), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_TSC_SCALING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_USER_WAIT_AND_PAUSE", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (58, 58), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENABLE_PCONFIG", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (59, 59), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_28_29", + description: "Control X is allowed to be 1 if bit X of this MSR is 1", + bits_range: (60, 61), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_VMM_BUS_LOCK_DETECTION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (62, 62), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_INSTRUCTION_TIMEOUT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-7. (Definitions of Secondary Processor-Based VM-Execution Controls)", + bits_range: (63, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + ( + RegisterAddress::IA32_VMX_EPT_VPID_CAP, + ValueDefinitions::new(&[ + ValueDefinition{ + short: "EPT_EXECUTE_ONLY", + description: "The processor supports execute-only translations by EPT", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "PAGE_WALK_LENGTH_4", + description: "Support for Page-walk length of 4", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "PAGE_WALK_LENGTH_5", + description: "Support for Page-walk length of 5", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "EPT_MEM_TYPE_UC", + description: "Software can configure the EPT paging structure to memory type to be unreachable (UC)", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "EPT_MEM_TYPE_WB", + description: "Software can configure the EPT paging structure to memory type to be write-back (WB)", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "EPT_PDE_2M", + description: "Software can configure the EPT PDE to map a 2-Mbyte page", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "EPT_PDPTE_1G", + description: "Software can configure the EPT PDPTE to map a 1-Gbyte page", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "INVEPT", + description: "INVEPT instruction is supported", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "FLAGS_EPT", + description: "Accessed and dirty flags for EPT are supported", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "VM_EXIT_VIOLATIONS_INFO", + description: "If set, the processors advanced VM-exit information for EPT violations", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "SHADOW_STACK_CTL", + description: "Supervisor shadow-stack control is supported", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "SINGLE_CONTEXT_INVEPT", + description: "The single-context INVEPT type is supported", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "ALL_CONTEXT_INVEPT", + description: "The all-context INVEPT type is supported", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "INVVPID", + description: "INVVPID instruction is supported", + bits_range: (32, 32), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "INDIVIDUAL_ADDRESS_INVVPID", + description: "The individual address INVVPID type is supported", + bits_range: (40, 40), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "SINGLE_CONTEXT_INVVPID", + description: "The single-context INVVPID type is supported", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "ALL_CONTEXT_INVVPID", + description: "The all-context INVEPT type is supported", + bits_range: (42, 42), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "SINGLE_CONTEXT_RETAINING_GLOBALS_INVVPID", + description: "The single-context-retaining-globals INVVPID type is supported", + bits_range: (43, 43), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short: "MAX_HLAT_PREFIX", + description: "Enumerates the maximum HLAT prefix size", + bits_range: (48, 53), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + ( + + RegisterAddress::IA32_VMX_TRUE_PINBASED_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short:"ALLOWED_ZERO_EXTERNAL_INTERRUPT_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_1_2", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (1, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_NMI_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_4", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (4, 4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_VIRTUAL_NMIS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (5, 5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_VMX_PREEMPTION_TIMER", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (6, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_PROCESS_POSTED_INTERRUPTS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + + + ValueDefinition { + short: "ALLOWED_ZERO", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (8, 31), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition{ + short:"ALLOWED_ONE_EXTERNAL_INTERRUPT_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (32, 32), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_1_2", + description: "VM entry allows control X to be 1 if bit X in this MSR is 1", + bits_range: (33, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_NMI_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (35, 35), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_4", + description: "VM entry allows control X to be 1 if bit X in this MSR is 1", + bits_range: (36, 36), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_VIRTUAL_NMIS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (37, 37), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_ACTIVATE_VMX__PREEMPTION_TIMER", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (38, 38), + policy: ProfilePolicy::Inherit + }, + ValueDefinition{ + short:"ALLOWED_ONE_PROCESS_POSTED_INTERRUPTS", + description: "See Intel SDM Vol.3C Section 26.6.1 Table 26-5 (Definitions of Pin-Based VM-Execution Controls)", + bits_range: (39, 39), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (40, 63), + policy: ProfilePolicy::Inherit + } + ]) + ), + + ( + RegisterAddress::IA32_VMX_TRUE_PROCBASED_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ZERO_0_1", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (0, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_INTERRUPT_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_TSC_OFFSETTING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_4_6", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (4, 6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_HLT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (7, 7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_8", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (8, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_INVLPG_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MWAIT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_RDPMC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_RDTSC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_13_14", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (13, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR3_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR3_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_TERTIARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_18", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR8_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CR8_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_TPR_SHADOW", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_NMI_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MOV_DR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_UNCONDITIONAL_I/O_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_I/O_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_26", + description: "Control X is allowed to be 0 if bit X of this MSR is 0", + bits_range: (26, 26), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MONITOR_TRAP_FLAG", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_USE_MSR_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (28, 28), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_MONITOR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_PAUSE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_0_1", + description: "Control X is allowed to be 1 if bit 32 + X of this MSR is 1", + bits_range: (32, 33), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_INTERRUPT_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_TSC_OFFSETTING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (35, 35), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_4_6", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (36, 38), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_HLT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (39, 39), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_8", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (40, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_INVLPG_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MWAIT_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (42, 42), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_RDPMC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (43, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_RDTSC_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (44, 44), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "ALLOWED_ONE_13_14", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (45, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CR3_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CR3_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (48, 48), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ACTIVATE_TERTIARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (49, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_18", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short:"ALLOWED_ONE_CR8_LOAD_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CR8_STORE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_TPR_SHADOW", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_NMI_WINDOW_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MOV_DR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (55, 55), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_UNCONDITIONAL_I/O_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (56, 56), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_I/O_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_26", + description: "Control X is allowed to be 1 if bit X of this MSR is 1", + bits_range: (58, 58), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short:"ALLOWED_ONE_MONITOR_TRAP_FLAG", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (59, 59), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_USE_MSR_BITMAPS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (60, 60), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_MONITOR_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (61, 61), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_PAUSE_EXITING", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (62, 62), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM. Vol.3C Section 26.6.2 Table 26-6 (Definitions of Primary Processor-Based VM-Execution Controls)", + bits_range: (63, 63), + policy: ProfilePolicy::Inherit + }, + + ]) + ), + + ( + RegisterAddress::IA32_VMX_TRUE_EXIT_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ZERO_0_1", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (0, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_3_8", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (3, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_HOST_ADDRESS_SPACE_SIZE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_10_11", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (10, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_13_14", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (13, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACKNOWLEDGE_INTERRUPT_O_EXIT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_16_17", + description: "Control X is allowed to be 0 if bit X in this MSR is 0", + bits_range: (16, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_VMX_PREEMPTION_TIMER_VALUE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (23, 23), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (24, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (26, 26), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_CLEAR_UINV", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (27, 27), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (28, 28), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (29, 29), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_SAVE_IA32_PERF_GLOBAL_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (30, 30), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (31, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_0_1", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (32, 33), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short:"ALLOWED_ONE_SAVE_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + + ValueDefinition { + short: "ALLOWED_ONE_3_8", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (35, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_HOST_ADDRESS_SPACE_SIZE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_10_11", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (42, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (44, 44), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short: "ALLOWED_ONE_13_14", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (45, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ACKNOWLEDGE_INTERRUPT_O_EXIT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_16_17", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (48, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_VMX_PREEMPTION_TIMER_VALUE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (55, 55), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (56, 56), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (58, 58), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_CLEAR_UINV", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (59, 59), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (60, 60), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (61, 61), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_SAVE_IA32_PERF_GLOBAL_CTL", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (62, 62), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_ACTIVATE_SECONDARY_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.7.1 Table 26-14 (Definitions of Primary VM-Exit Controls)", + bits_range: (63, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + ( + RegisterAddress::IA32_VMX_TRUE_ENTRY_CTLS, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ZERO_0_1", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (0, 1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (2, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_3_8", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (3, 8), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_IA_32E_MODE_GUES", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (9, 9), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ENTRY_TO_SMM", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (10, 10), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_DEACTIVATE_DUAL__MONITOR_TREATMENT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (11, 11), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_12", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (12, 12), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (13, 13), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (14, 14), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (15, 15), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (16, 16), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (17, 17), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (18, 18), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_UINV", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (19, 19), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (20, 20), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_GUEST_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (21, 21), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ZERO_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (22, 22), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_23_24", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (23, 24), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ZERO_ALLOW_SEAM_GUEST_TELEMETRY", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (25, 25), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ZERO_26_31", + description: "VM entry allows control X to be 0 if bit X in this MSR is zero", + bits_range: (26, 31), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_0_1", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (32, 33), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_DEBUG_CONTROLS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (34, 34), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_3_8", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (35, 40), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_IA_32E_MODE_GUES", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (41, 41), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ENTRY_TO_SMM", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (42, 42), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_DEACTIVATE_DUAL__MONITOR_TREATMENT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (43, 43), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_12", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (44, 44), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PERF_GLOBAL_CTRL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (45, 45), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_PAT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (46, 46), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_EFER", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (47, 47), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_BNDCFGS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (48, 48), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_CONCEAL_VMX_FROM_PT", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (49, 49), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_IA32_RTIT_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (50, 50), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_UINV", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (51, 51), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_CET_STATE", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (52, 52), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_GUEST_IA32_LBR_CTL", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (53, 53), + policy: ProfilePolicy::Static(0) + }, + ValueDefinition { + short:"ALLOWED_ONE_LOAD_PKRS", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (54, 54), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_23_24", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (55, 56), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_ALLOW_SEAM_GUEST_TELEMETRY", + description: "See Intel SDM Vol.3C Section 26.8.1 Table 26-17. (Definitions of VM-Entry Controls)", + bits_range: (57, 57), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_26_31", + description:"VM entry allows control X to be 1 if bit X + 32 in this MSR is 1", + bits_range: (58, 63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + ( + RegisterAddress::IA32_VMX_VMFUNC, + ValueDefinitions::new(&[ + ValueDefinition { + short:"ALLOWED_ONE_EPTP_SWITCHING", + description: "See Intel SDM Vol.3C Section 26.6.14 Table 26-10. (Definitions of VM-Function Controls)", + bits_range: (0, 0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short:"ALLOWED_ONE_1_63", + description: "See Intel SDM Vol.3C Section 26.6.14 Table 26-10. (Definitions of VM-Function Controls)", + bits_range: (1, 63), + policy: ProfilePolicy::Inherit + }, + + ]) + ), + + // NOTE: This MSR is currently not supported by KVM. We keep the definition here regardless. (TODO: Maybe it would be better to remove it?) + ( + RegisterAddress::IA32_VMX_PROCBASED_CTLS3, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ONE_LOADIWKEY_EXITING", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (0,0), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_ENABLE_HLAT", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (1,1), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_EPT_PAGING_WRITE_CONTROL", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (2,2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_GUEST_PAGING_VERIFICATION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (3,3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_IPI_VIRTUALIZATION", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (4,4), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_SEAM_GUEST_PHYSICAL_ADDRESS_WIDTH", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (5,5), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_ENABLE_MSR_LIST_INSTRUCTIONS", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (6,6), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_VIRTUALIZE_IA32_SPEC_CTRL", + description: "See Intel SDM Vol.3C Section 26.6.2 Table 26-8 (Definitions of Tertiary Processor-Based VM-Execution Controls)", + bits_range: (7,7), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_8_63", + description: "Control X is allowed to be 1 if bit X in this MSR is 1", + bits_range: (8,63), + policy: ProfilePolicy::Inherit + }, + ]) + ), + + // NOTE: This MSR is currently not supported by KVM. We keep the definition here regardless. (TODO: Maybe it would be better to remove it?) + ( + RegisterAddress::IA32_VMX_EXIT_CTLS2, + ValueDefinitions::new(&[ + ValueDefinition { + short: "ALLOWED_ONE_0_2", + description:"VM entry allows control X to be 1 if bit X is 1", + bits_range: (0, 2), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_PREMATURELY_BUSY_SHADOW_STACK", + description:"See Intel SDM Vol.3C Section 26.7.1", + bits_range: (3, 3), + policy: ProfilePolicy::Inherit + }, + ValueDefinition { + short: "ALLOWED_ONE_4_63", + description:"VM entry allows control X to be 1 if bit X is 1", + bits_range: (4, 63), + policy: ProfilePolicy::Inherit + } + ]) + ), + ( + RegisterAddress::MSR_PLATFORM_INFO, + ValueDefinitions::new(&[ + ValueDefinition { + short: "PLATFORM_INFORMATION", + description: "Contains power management and other model specific features enumeration. In reality bits 15:8 describe the maximum frequency that does not require turbo. All other bits are reserved", + bits_range: (0, 63), + policy: ProfilePolicy::Deny + } + ]) + ) + ]) +}; + +/// Convenience function to lookup value definitions corresponding to the given MSR register address (as a const parameter). +#[cold] +#[inline(never)] +pub(in crate::x86_64) const fn msr_definitions() -> &'static [ValueDefinition] +{ + const { + let mut out = [].as_slice(); + let intel_definitions = INTEL_MSR_FEATURE_DEFINITIONS.as_slice(); + let mut i = 0; + let length = intel_definitions.len(); + while i < length { + let (addr, definitions) = intel_definitions[i]; + if addr.0 == REG_ADDR { + out = definitions.as_slice(); + break; + } + i += 1; + } + if out.is_empty() { + panic!("MSR definition not found"); + } + out + } +} + +/// Check that the `src_feature_msrs` are compatible with those given in `dest_feature_msrs`. +/// +/// If this check fails, then software that works under the `src_feature_msrs`, may no longer +/// behave correctly with `dest_feature_msrs`. +/// +/// The `src_id` and `dest_id` strings are only used for logging purposes to identify what +/// is being compared (e.g. CPU profile vs host where the profile should be applied, etc). +/// +/// NOTE: This function assumes CPUID compatibility. +/// +/// All register addresses/keys in [`INTEL_MSR_FEATURE_DEFINITIONS`] are checked, except for: +/// - IA32_BIOS_SIGN_ID, +/// - IA32_PERF_CAPABILITIES, +/// - MSR_PLATFORM_INFO +/// +/// IA32_PERF_CAPABILITIES are inherently incompatible between different VMs and we do not +/// think it makes much sense to compare IA32_BIOS_SIGN_ID or MSR_PLATFORM_INFO in this context. +/// +/// # Errors +/// +/// This function does not return early upon error, but rather attempts all MSR-based feature +/// checks while logging errors it encounters. If any of these checks fail an error is returned +/// at the end. +/// +/// We also just use the unit type as the error variant for now, as not much can be done to +/// recover from these errors at runtime and the logs should provide the user with enough +/// information to debug the problem. +/// +/// At this moment in time we prefer the aforementioned approach over designing a complex +/// error type capable of tracking everything that might fail. +pub(in crate::x86_64) fn check_feature_msr_compatibility( + src_feature_msrs: &HashMap, + dest_feature_msrs: &HashMap, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + let mut is_err = false; + // First check IA32_ARCH_CAPABILITIES + // Since we are assuming CPUID to be compatible we + // may assume that either both src and dest have this + // MSR or none of them do + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_ARCH_CAPABILITIES.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_ARCH_CAPABILITIES.0)) + { + is_err |= + check_arch_capabilities_compatibility(*src_val, *dest_val, src_id, dest_id).is_err(); + } + + // Next let us consider IA32_VMX_BASIC + let mut true_ctls_exist_src = false; + let mut true_ctls_exist_dest = false; + // Since we assume compatibility of CPUID we can again check that either both src and dest + // have the IA32_VMX_BASIC MSR or none of them do + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_BASIC.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_BASIC.0)) + { + true_ctls_exist_src = (*src_val & (1 << 55)) != 0; + true_ctls_exist_dest = (*dest_val & (1 << 55)) != 0; + is_err |= check_vmx_basic_compatibility(*src_val, *dest_val, src_id, dest_id).is_err(); + } + // The following closure saves us some boiler plate when checking the various VMX CTLS that have a default1 class + let check_vmx_ctls_with_default1_class = |vmx_ctrl_reg_address: RegisterAddress, + vmx_true_ctrl_reg_address: RegisterAddress, + check_id: &str, + src_id: &str, + dest_id: &str| + -> Result<(), ()> { + let mut is_err = false; + let src_reg_address = { + conditional_select( + vmx_ctrl_reg_address.0, + vmx_true_ctrl_reg_address.0, + true_ctls_exist_src, + ) + }; + + let dest_reg_address = { + conditional_select( + vmx_ctrl_reg_address.0, + vmx_true_ctrl_reg_address.0, + true_ctls_exist_dest, + ) + }; + + let src_val = src_feature_msrs.get(&src_reg_address); + let dest_val = dest_feature_msrs.get(&dest_reg_address); + if src_val.is_some() && dest_val.is_none() { + error!( + "{check_id} compatibility check failed: unable to compare value of MSR {src_reg_address:#x} of {src_id} with value of MSR {dest_reg_address:#x} of {dest_id}, because the latter value was not found" + ); + is_err = true; + } + if let Some((src_val, dest_val)) = src_val.zip(dest_val) + && let Err(CtlsCheck { + bitset_only_zero_src_lo, + bitset_only_one_src_hi, + }) = check_negative_subset_lo_and_subset_hi(*src_val, *dest_val) + { + is_err = true; + if let Some(bitset) = bitset_only_zero_src_lo { + for_each_bitpos(bitset, |bit_pos| { + debug!( + "{check_id} compatibility check failed: bit {bit_pos} is 0 in MSR:={src_reg_address:#x} of {src_id}, but 1 in MSR:={dest_reg_address:#x} of {dest_id}" + ); + }); + } + + if let Some(bitset) = bitset_only_one_src_hi { + for_each_bitpos(bitset, |bit_pos| { + debug!( + "{check_id} compatibility check failed: bit {bit_pos} is 1 in MSR:={src_reg_address:#x} of {src_id}, but 0 in MSR:={dest_reg_address:#x} of {dest_id}" + ); + }); + } + } + + if is_err { + if let Some(src_val) = src_val + && let Some(dest_val) = dest_val + { + error!( + "{check_id} compatibility check failed: {src_id} register address:={src_reg_address:#x}, {src_id} value:={:#x}, {dest_id} register address:={dest_reg_address:#x}, {dest_id} value:={:#x}", + *src_val, *dest_val + ); + } + Err(()) + } else { + Ok(()) + } + }; + + // Now we consider IA32_VMX_PINBASED_CTLS and/or IA32_VMX_TRUE_BINBASED_CTLS + // (Intel SDM Vol.3D A.3.1) + is_err |= check_vmx_ctls_with_default1_class( + RegisterAddress::IA32_VMX_PINBASED_CTLS, + RegisterAddress::IA32_VMX_TRUE_PINBASED_CTLS, + "IA32_VMX_PINBASED_CTLS", + src_id, + dest_id, + ) + .is_err(); + + // Next up is IA32_VMX_PROCBASED_CTLS and/or IA32_VMX_TRUE_PROCBASED_CTLS + // (Intel SDM Vol.3D A.3.2.) + is_err |= check_vmx_ctls_with_default1_class( + RegisterAddress::IA32_VMX_PROCBASED_CTLS, + RegisterAddress::IA32_VMX_TRUE_PROCBASED_CTLS, + "IA32_PROCBASED_CTLS", + src_id, + dest_id, + ) + .is_err(); + // Check IA32_VMX_EXIT_CTLS and/or IA32_VMX_TRUE_EXIT_CTLS + // (Intel SDM Vol.3D A.4) + is_err |= check_vmx_ctls_with_default1_class( + RegisterAddress::IA32_VMX_EXIT_CTLS, + RegisterAddress::IA32_VMX_TRUE_EXIT_CTLS, + "IA32_VMX_EXIT_CTLS", + src_id, + dest_id, + ) + .is_err(); + // Check IA32_VMX_ENTRY_CTLS and/or IA32_VMX_TRUE_ENTRY_CTLS + // (Intel SDM Vol.3D A.5) + is_err |= check_vmx_ctls_with_default1_class( + RegisterAddress::IA32_VMX_ENTRY_CTLS, + RegisterAddress::IA32_VMX_TRUE_ENTRY_CTLS, + "IA32_VMX_ENTRY_CTLS", + src_id, + dest_id, + ) + .is_err(); + // Check IA32_VMX_MISC + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_MISC.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_MISC.0)) + { + is_err |= check_vmx_misc_msr(*src_val, *dest_val, src_id, dest_id).is_err(); + } + // Check IA32_VMX_CR0_FIXED0 + if let Some((src_fixed0, dest_fixed0)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_CR0_FIXED0.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_CR0_FIXED0.0)) + { + is_err |= + check_cr_i_compatibility::<0>(*src_fixed0, *dest_fixed0, src_id, dest_id).is_err(); + } + + // Check IA32_VMX_CR4_FIXED0 + if let Some((src_fixed0, dest_fixed0)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_CR4_FIXED0.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_CR4_FIXED0.0)) + { + is_err |= + check_cr_i_compatibility::<4>(*src_fixed0, *dest_fixed0, src_id, dest_id).is_err(); + } + + // Check IA32_VMX_VMCS_ENUM + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_VMCS_ENUM.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_VMCS_ENUM.0)) + { + is_err |= check_vmx_vmcs_enum_compatibility(*src_val, *dest_val, src_id, dest_id).is_err(); + } + + // Check IA32_VMX_PROCBASED_CTLS2 + // This MSR exists only if bit 63 of IA32_VMX_PROCBASED_CTLS is set + // (note that if it is set on src then our IA32_VMX_PROCBASED_CTLS check + // ensures that it is also set on dest) + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_PROCBASED_CTLS2.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_PROCBASED_CTLS2.0)) + { + let src_val = *src_val; + let dest_val = *dest_val; + // First verify that the first 32 bits are indeed 0 as documented by Intel, otherwise we have misunderstood the documentation + // and we should not continue. + let lo_mask = u64::from(u32::MAX); + assert_eq!( + src_val & lo_mask, + 0, + "BUG: The 32-first bits of the IA32_VMX_PROCBASED_CTLS2 MSR were not zero for src" + ); + assert_eq!( + dest_val & lo_mask, + 0, + "BUG: The 32-first bits of the IA32_VMX_PROCBASED_CTLS2 MSR were not zero for dest" + ); + // Note that the 32-first bits are documented to always be 0 + if let Err(bits_only_in_src) = check_subset(src_val, dest_val) { + is_err = true; + error!( + "IA32_VMX_PROCBASED_CTLS2 compatibility check failed: {src_id} value:={src_val:#x}, {dest_id} value:={dest_val:#x}" + ); + for_each_bitpos(bits_only_in_src, |bit_pos| { + debug!( + "IA32_VMX_PROCBASED_CTLS2 check failed: VM entry allows control X:={bit_pos} to be 1 for {src_id}, but not for {dest_id}" + ); + }); + } + } + + // Check IA32_VMX_PROCBASED_CTLS3 + // This MSR exists only if bit 49 of IA32_VMX_PROCBASED_CTLS is set + // (note that if it is set on src then our IA32_VMX_PROCBASED_CTLS check + // ensures that it is also set on dest) + + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_PROCBASED_CTLS3.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_PROCBASED_CTLS3.0)) + && let Err(bits_only_in_src) = check_subset(*src_val, *dest_val) + { + is_err = true; + error!( + "IA32_VMX_PROCBASED_CTLS3 compatibility check failed: {src_id} value:= {:#x}, {dest_id} value:={:#x}", + *src_val, *dest_val + ); + + for_each_bitpos(bits_only_in_src, |bit_pos| { + debug!( + "IA32_VMX_PROCBASED_CTLS3 compatibility check failed: VM entry allows control X:={bit_pos} for {src_id}, but not for {dest_id}" + ); + }); + } + + // Check IA32_VMX_EXIT_CTLS2 + // This MSR exists only if bit 63 of the IA32_VMX_EXIT_CTLS is set + // (note that if it is set on src then our IA32_VMX_EXIT_CTLS check + // ensures that it is also set on dest) + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_EXIT_CTLS2.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_EXIT_CTLS2.0)) + && let Err(bits_only_in_src) = check_subset(*src_val, *dest_val) + { + is_err = true; + error!( + "IA32_VMX_EXIT_CTLS2 compatibility check failed: {src_id} value:={:#x}, {dest_id} value:={:#x}", + *src_val, *dest_val + ); + for_each_bitpos(bits_only_in_src, |bit_pos| { + debug!( + "IA32_VMX_EXIT_CTLS2 compatibility check failed: bit {bit_pos} is set for {src_id}, but not for {dest_id}" + ); + }); + } + + // Check IA32_VMX_EPT_VPID_CAP (Intel SDM Vol.3D A.10) + // + // This MSR is only available on processors where bit 63 of IA32_VMX_PROCBASED_CTLS is 1 and that either + // have bit 33 of IA32_VMX_PROCBASED_CTLS2 set, or bit 37 of IA32_VMX_PROC_BASED_CTLS2 set. Since we + // already check for compatibility of those bits, we may assume that if this MSR is available for src, then + // it is also available for dest. + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_EPT_VPID_CAP.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_EPT_VPID_CAP.0)) + { + is_err |= check_vpid_and_ept_capabilities(*src_val, *dest_val, src_id, dest_id).is_err(); + } + + if let Some((src_val, dest_val)) = src_feature_msrs + .get(&RegisterAddress::IA32_VMX_VMFUNC.0) + .zip(dest_feature_msrs.get(&RegisterAddress::IA32_VMX_VMFUNC.0)) + && let Err(bits_only_in_src) = check_subset(*src_val, *dest_val) + { + is_err = true; + error!( + "IA32_VMX_VMFUNC compatibility check failed: {src_id} value:={:#x}, {dest_id} value:={:#x}", + *src_val, *dest_val + ); + for_each_bitpos(bits_only_in_src, |bit_pos| { + debug!( + "IA32_VMX_VMFUNC compatibility check failed: VM entry allows bit X:={bit_pos} of the VM-function controls to be 1 for {src_id}, but not for {dest_id}" + ); + }); + } + + if is_err { Err(()) } else { Ok(()) } +} + +/// `a` if `condition` else `b` +fn conditional_select(a: u32, b: u32, condition: bool) -> u32 { + let a_mask = u32::from(condition).wrapping_neg(); + let b_mask = !a_mask; + (a & a_mask) | (b & b_mask) +} + +/// Check that the values of MSR IA32_ARCH_CAPABILITIES are compatible. +/// +/// 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`. +/// +/// See: Ch.2 Table 2-2. IA-32 Architectural MSRs in Intel SDM Vol.4 +fn check_arch_capabilities_compatibility( + src_val: u64, + dest_val: u64, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + // Make a mask out of + const RDCL_NO: u64 = 1 << 0; + const IBRS_ALL: u64 = 1 << 1; + const SKIP_L1_DFL_VMENTRY: u64 = 1 << 3; + const SSB_NO: u64 = 1 << 4; + const MDS_NO: u64 = 1 << 5; + const TSX_CONTROL: u64 = 1 << 7; + const TAA_NO: u64 = 1 << 8; + const MCU_CONTROL: u64 = 1 << 9; + const MISC_PACKAGE_CTLS: u64 = 1 << 10; + const ENERGY_FILTERING_CTL: u64 = 1 << 11; + const DOITM: u64 = 1 << 12; + const MCU_ENUMERATION: u64 = 1 << 16; + const FB_CLEAR: u64 = 1 << 17; + const FB_CLEAR_CTRL: u64 = 1 << 18; + const BHI_NO: u64 = 1 << 20; + const XAPIC_DISABLE_STATUS: u64 = 1 << 21; + const MCU_EXTENDED_SERVICE: u64 = 1 << 22; + const OVERCLOCKING_STATUS: u64 = 1 << 23; + const PBRSB_NO: u64 = 1 << 24; + const GDS_CTRL: u64 = 1 << 25; + const GDS_NO: u64 = 1 << 26; + const RFDS_NO: u64 = 1 << 27; + // TODO: Should we perhaps ignore checking this (is it too strict)? + const RFDS_CLEAR: u64 = 1 << 28; + const IGN_UMONITOR_SUPPORT: u64 = 1 << 29; + const MON_UMON_MITG_SUPPORT: u64 = 1 << 30; + const PBOPT_SUPPORT: u64 = 1 << 32; + + let mask: u64 = { + RDCL_NO + | IBRS_ALL + | SKIP_L1_DFL_VMENTRY + | SSB_NO + | MDS_NO + | TAA_NO + | TSX_CONTROL + | MCU_CONTROL + | MISC_PACKAGE_CTLS + | ENERGY_FILTERING_CTL + | DOITM + | MCU_ENUMERATION + | FB_CLEAR + | FB_CLEAR_CTRL + | XAPIC_DISABLE_STATUS + | MCU_EXTENDED_SERVICE + | OVERCLOCKING_STATUS + | GDS_CTRL + | IGN_UMONITOR_SUPPORT + | MON_UMON_MITG_SUPPORT + | PBOPT_SUPPORT + | RFDS_CLEAR + | PBRSB_NO + | GDS_NO + | RFDS_NO + | BHI_NO + }; + if let Err(only_in_src) = check_subset(src_val & mask, dest_val & mask) { + error!( + "IA32_ARCH_CAPABILITIES compatibility check failed: {src_id} value:={src_val:#x}, {dest_id} value:={dest_val:#x}" + ); + let definitions = msr_definitions::<{ RegisterAddress::IA32_ARCH_CAPABILITIES.0 }>(); + log_features_only_in_src(only_in_src, src_id, definitions, "IA32_ARCH_CAPABILITIES"); + Err(()) + } else { + Ok(()) + } +} + +/// Check that the values of MSR IA32_VMX_BASIC are compatible. +/// +/// See Intel SDM Vol.3D A.1 for more information about the IA32_VMX_BASIC MSR +fn check_vmx_basic_compatibility( + src_val: u64, + dest_val: u64, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + let mut is_err = false; + // All bits between 0 and 53 are expected to be equal (except bit 49) + let req_eq_mask: u64 = ((1 << 54) - 1) & (!(1 << 49)); + let src_req_eq = src_val & req_eq_mask; + let dest_req_eq = dest_val & req_eq_mask; + if src_req_eq != dest_req_eq { + is_err = true; + let definitions = msr_definitions::<{ RegisterAddress::IA32_VMX_BASIC.0 }>(); + log_inequalities( + src_req_eq, + dest_req_eq, + definitions, + src_id, + dest_id, + "IA32_VMX_BASIC compatibility", + ); + } + // bits 49, 54, 55, and 56 indicate some form of capability and we need to check + // that these bits in the `src_value` are a subset of those in `dest_value` + let req_subset_eq_mask: u64 = (1 << 54) | (1 << 55) | (1 << 56) | (1 << 49); + let src_val_seq = req_subset_eq_mask & src_val; + let dest_val_seq = req_subset_eq_mask & dest_val; + is_err |= check_subset(src_val_seq, dest_val_seq).is_err(); + + if is_err { + error!( + "IA32_VMX_BASIC compatibility check failed: {src_id} value:={src_val:#x}, {dest_id} value:={dest_val:#x}" + ); + Err(()) + } else { + Ok(()) + } +} + +/// Check that no values are only in a +/// +/// Upon error a bitset is returned with the +/// bits that are only available in `src_val` +fn check_subset(src_val: u64, dest_val: u64) -> Result<(), u64> { + let only_in_src_val = src_val & (src_val ^ dest_val); + if only_in_src_val != 0 { + Err(only_in_src_val) + } else { + Ok(()) + } +} + +/// Checks the following: +/// 1. For any X < 32; If bit X of src_val is 0 then bit X of dest_val is also 0 +/// 2. For any X >= 32; If bit X of src_val is 1 then bit X of dest_val is also 1 +struct CtlsCheck { + bitset_only_zero_src_lo: Option, + bitset_only_one_src_hi: Option, +} + +fn check_negative_subset_lo_and_subset_hi(src_val: u64, dest_val: u64) -> Result<(), CtlsCheck> { + let lo_mask = (1_u64 << 32) - 1; + let hi_mask = !lo_mask; + + let lo_check = check_subset((!src_val) & lo_mask, (!dest_val) & lo_mask); + + let hi_check = check_subset(src_val & hi_mask, dest_val & hi_mask); + + if lo_check.is_ok() && hi_check.is_ok() { + Ok(()) + } else { + Err(CtlsCheck { + bitset_only_zero_src_lo: lo_check.err(), + bitset_only_one_src_hi: hi_check.err(), + }) + } +} + +/// Check that the values of MSR IA32_VMX_MISC are compatible. +/// +/// See Intel SDM Vol.3D A.6 for more information about the IA32_VMX_MISC MSR +fn check_vmx_misc_msr( + src_value: u64, + dest_value: u64, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + let mut is_err = false; + let subset_eq_check_mask: u64 = { + (1 << 5) + | (1 << 6) + | (1 << 7) + | (1 << 8) + | (1 << 14) + | (1 << 15) + | (1 << 28) + | (1 << 29) + | (1 << 30) + }; + if let Err(only_in_src) = check_subset( + subset_eq_check_mask & src_value, + subset_eq_check_mask & dest_value, + ) { + is_err = true; + let definitions = msr_definitions::<{ RegisterAddress::IA32_VMX_MISC.0 }>(); + log_features_only_in_src(only_in_src, src_id, definitions, "IA32_VMX_MISC"); + } + + let eq_mask: u64 = { + // TODO: Do we also need to check that the MSEG revisions match? + (16..=24).fold(0_u64, |acc, next| acc | (1 << next)) + }; + + let src_req_eq_val = src_value & eq_mask; + let dest_req_eq_val = dest_value & eq_mask; + if src_req_eq_val != dest_req_eq_val { + is_err = true; + let definitions = msr_definitions::<{ RegisterAddress::IA32_VMX_MISC.0 }>(); + log_inequalities( + src_req_eq_val, + dest_req_eq_val, + definitions, + src_id, + dest_id, + "IA32_VMX_MISC", + ); + } + + let leq_mask: u64 = { (25..=27).fold(0_u64, |acc, next| acc | (1 << next)) }; + + let src_req_leq = src_value & leq_mask; + let dest_req_leq = dest_value & leq_mask; + if src_req_leq > dest_req_leq { + is_err = true; + debug!( + "IA32_VMX_MISC compatibility check failed when checking definition: {:?}, {src_id} has value:={src_req_leq}, {dest_id} has value:={dest_req_leq}", + max_msr_store_lists_def(), + ); + } + + if is_err { + error!( + "IA32_VMX_MISC compatibility check failed: {src_id} value:={src_value:#x}, {dest_id} value:={dest_value:#x}" + ); + Err(()) + } else { + Ok(()) + } +} + +/// Check compatibility of MSRs IA32_VMX_CR{I}_FIXED0 for I = 0, 4. +/// +/// See Intel SDM Vol.3D A.7 & A.8 for more information about these MSRs. +/// +/// NOTE: We don't need to check compatibility for CR{I}_FIXED1 because +/// that is ensured by CPUID. +fn check_cr_i_compatibility( + src_fixed0: u64, + dest_fixed0: u64, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + let cri = const { + match I { + 0 => "CR0", + 4 => "CR4", + _ => { + panic!("only 0 and 4 may be used") + } + } + }; + + // Need to ensure that there are no bits that are only 0 in src_fixed0 and also no bits + // that are only 1 in src_fixed1. + + if let Err(only_zero_in_src) = check_subset(!src_fixed0, !dest_fixed0) { + error!( + "IA32_VMX_{cri}_FIXED0 compatibility check failed: {src_id} value:={src_fixed0:#x}, {dest_id} value:={dest_fixed0:#x}" + ); + for_each_bitpos(only_zero_in_src, |bit_pos| { + debug!( + "IA32_VMX_{cri}_FIXED0 compatibility check failed: bit {bit_pos} is allowed to be 0 in {cri} for {src_id}, but not for {dest_id}" + ); + }); + Err(()) + } else { + Ok(()) + } +} + +/// Check compatibility of MSRs IA32_VMX_VMCS_ENUM. +/// +/// See Intel SDM Vol.3D A.9 for more information about IA32_VMX_VMCS_ENUM. +fn check_vmx_vmcs_enum_compatibility( + src_value: u64, + dest_value: u64, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + let mask = (1..=9).fold(0_u64, |acc, next| acc | (1 << next)); + let src_req_leq = src_value & mask; + let dest_req_leq = dest_value & mask; + if src_req_leq > dest_req_leq { + error!( + "VMX_VMCS_ENUM compatibility check failed: MAX_INDEX for {src_id}:={src_req_leq} is greater than MAX_INDEX:={dest_req_leq} for {dest_id}" + ); + Err(()) + } else { + Ok(()) + } +} + +/// Check compatibility of MSRs IA32_VMX_EPT_VPID_CAP. +/// +/// See (Intel TODO:) Vol. 3D A.10 for more information about IA32_VMX_EPT_VPID_CAP. +// Only if IA32_VMX_PROCBASED_CTLS[63] & (IA32_VMX_PROCBASED_CTLS2[33] | IA32_VMX_PROCBASED_CTLS2[37]) +fn check_vpid_and_ept_capabilities( + src_value: u64, + dest_value: u64, + src_id: &str, + dest_id: &str, +) -> Result<(), ()> { + let mut is_err = false; + let subset_eq_mask = { (1 << 44) - 1 }; + + if let Err(bits_only_in_src) = + check_subset(src_value & subset_eq_mask, dest_value & subset_eq_mask) + { + is_err = true; + let definitions = msr_definitions::<{ RegisterAddress::IA32_VMX_EPT_VPID_CAP.0 }>(); + log_features_only_in_src( + bits_only_in_src, + src_id, + definitions, + "IA32_VMX_EPT_VPID_CAP", + ); + } + + let leq_mask = { (48..=53).fold(0_u64, |acc, next| acc | (1 << next)) }; + let src_req_leq = src_value & leq_mask; + let dest_req_leq = dest_value & leq_mask; + if src_req_leq > dest_req_leq { + is_err = true; + debug!( + "IA32_VMX_EPT_VPID_CAP compatibility check failed: maximum HLAT prefix size is {src_req_leq} for {src_id}, but {dest_req_leq} for {dest_id}" + ); + } + if is_err { + error!( + "IA32_VMX_EPT_VPID_CAP compatibility check failed: {src_id} value:={src_value:#x}, {dest_id} value:={dest_value:#x}" + ); + Err(()) + } else { + Ok(()) + } +} + +fn for_each_bitpos(bits: u64, mut cb: impl FnMut(u8)) { + let mut bits = bits; + while bits != 0 { + let pos = bits.trailing_zeros() as u8; + cb(pos); + let lsb = bits & bits.wrapping_neg(); + bits ^= lsb; + } +} + +#[inline(never)] +#[cold] +fn log_features_only_in_src( + only_in_src: u64, + src_id: &str, + definitions: &[ValueDefinition], + check_id: &str, +) { + for_each_bitpos(only_in_src, |bit_pos| { + let Some(def) = definitions + .iter() + .find(|def| (def.bits_range.0..=def.bits_range.1).contains(&bit_pos)) + else { + debug!( + "{check_id} compatibility check failed: bit:={bit_pos} is only set for {src_id}" + ); + warn!( + "unable to produce proper debug log: No MSR value definition found for bit:={bit_pos} check:={check_id} compatibility" + ); + return; + }; + debug!( + "{check_id} compatibility check failed: feature bit {bit_pos} only set for {src_id}: feature definition:={def:?}" + ); + }); +} + +#[inline(never)] +#[cold] +fn log_inequalities( + src_val: u64, + dest_val: u64, + definitions: &[ValueDefinition], + src_id: &str, + dest_id: &str, + check_id: &str, +) { + for def in definitions { + let mask = + (def.bits_range.0..=def.bits_range.1).fold(0_u64, |acc, next| acc | (1_u64 << next)); + let val_src = mask & src_val; + let val_dest = mask & dest_val; + if src_val != dest_val { + debug!( + "Check: {check_id} compatibility failed: on definition:={def:?}, values are required to be equal, but we have {src_id} value:={val_src:#x}, {dest_id} value:={val_dest:#x}" + ); + } + } +} + +#[inline(never)] +#[cold] +const fn max_msr_store_lists_def() -> &'static ValueDefinition { + const { + let defs = msr_definitions::<{ RegisterAddress::IA32_VMX_MISC.0 }>(); + // Currently stored at index = 8, if this changes we make sure that we fail at compile time. + // We do not perform a search as the order is unlikely to change frequently and we want to keep + // compile times down. + let def = &defs[8]; + assert!( + def.bits_range.0 == 25, + "MAX_MSR_STORE_LISTS definition is no longer at index 8 in the ValueDefinitions corresponding to IA32_VMX_MISC, please update the index" + ); + assert!( + def.bits_range.1 == 27, + "MAX_MSR_STORE_LISTS definition is no longer at index 8 in the ValueDefinitions corresponding to IA32_VMX_MISC, please update the index" + ); + def + } +} diff --git a/arch/src/x86_64/msr_definitions/intel/non_architectural_msrs.rs b/arch/src/x86_64/msr_definitions/intel/non_architectural_msrs.rs new file mode 100644 index 0000000000..b1f88aa809 --- /dev/null +++ b/arch/src/x86_64/msr_definitions/intel/non_architectural_msrs.rs @@ -0,0 +1,113 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This module contains a list of all known non-architectural MSRS for various Intel +//! CPUs. This list only helps us detect new MSRs that we are not (yet) aware of when +//! generating CPU profiles, but has no importance beyond that. + +/// A list of known non-architectural MSRs +/// +/// Note: KVM_GET_MSR_FEATURE_INDEX_LIST may return non-architectural MSRS. We append those +/// to [`crate::x86_64::msr_definitions_intel::INTEL_MSR_FEATURE_DEFINITIONS`] and not here. +pub(in crate::x86_64) const NON_ARCHITECTURAL_INTEL_MSRS: [u32; 872] = [ + 0x11, 0x12, 0x13, 0x2a, 0x2b, 0x2c, 0x33, 0x34, 0x35, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x53, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x80, 0x88, 0x89, 0x8a, 0x98, + 0x99, 0x9a, 0xa0, 0xa1, 0xa5, 0xa7, 0xcd, 0xe2, 0xe4, 0xed, 0xee, 0xef, 0xf0, 0x105, 0x116, + 0x118, 0x119, 0x11a, 0x11b, 0x11e, 0x13c, 0x140, 0x151, 0x17d, 0x17f, 0x180, 0x181, 0x182, + 0x183, 0x184, 0x185, 0x190, 0x191, 0x192, 0x193, 0x194, 0x196, 0x197, 0x19d, 0x1a1, 0x1a2, + 0x1a4, 0x1a6, 0x1a7, 0x1aa, 0x1ac, 0x1ad, 0x1ae, 0x1af, 0x1c8, 0x1c9, 0x1d7, 0x1d8, 0x1da, + 0x1db, 0x1dc, 0x1f1, 0x1f4, 0x1f5, 0x1fb, 0x1fc, 0x2a0, 0x2a1, 0x2a2, 0x2a3, 0x2a4, 0x2a5, + 0x2a6, 0x2a7, 0x2b8, 0x2b9, 0x2ba, 0x2bb, 0x2bc, 0x2bd, 0x2be, 0x2bf, 0x2c2, 0x2c3, 0x2c4, + 0x2c5, 0x2c6, 0x2c7, 0x2c8, 0x2c9, 0x2d6, 0x2d7, 0x2d9, 0x2f4, 0x2f5, 0x300, 0x301, 0x302, + 0x303, 0x304, 0x305, 0x306, 0x307, 0x308, 0x310, 0x311, 0x329, 0x350, 0x351, 0x354, 0x355, + 0x360, 0x361, 0x362, 0x363, 0x364, 0x365, 0x366, 0x367, 0x368, 0x369, 0x36a, 0x36b, 0x36c, + 0x36d, 0x36e, 0x36f, 0x370, 0x371, 0x393, 0x394, 0x395, 0x396, 0x39c, 0x3a0, 0x3a1, 0x3a2, + 0x3a3, 0x3a4, 0x3a5, 0x3a6, 0x3a7, 0x3a8, 0x3a9, 0x3aa, 0x3ab, 0x3ac, 0x3ad, 0x3ae, 0x3af, + 0x3b0, 0x3b1, 0x3b2, 0x3b3, 0x3b4, 0x3b5, 0x3b6, 0x3b7, 0x3b8, 0x3b9, 0x3ba, 0x3bb, 0x3bc, + 0x3bd, 0x3be, 0x3c0, 0x3c1, 0x3c2, 0x3c3, 0x3c4, 0x3c5, 0x3c6, 0x3c7, 0x3c8, 0x3c9, 0x3ca, + 0x3cb, 0x3cc, 0x3cd, 0x3e0, 0x3e1, 0x3f0, 0x3f2, 0x3f6, 0x3f7, 0x3f8, 0x3f9, 0x3fa, 0x3fc, + 0x3fd, 0x3fe, 0x3ff, 0x4e0, 0x4e2, 0x4e3, 0x4f0, 0x4f8, 0x540, 0x541, 0x601, 0x606, 0x60a, + 0x60b, 0x60c, 0x60d, 0x610, 0x611, 0x612, 0x613, 0x614, 0x618, 0x619, 0x61b, 0x61c, 0x61e, + 0x620, 0x630, 0x631, 0x632, 0x638, 0x639, 0x63a, 0x640, 0x641, 0x642, 0x648, 0x649, 0x64a, + 0x64b, 0x64c, 0x64d, 0x64e, 0x64f, 0x650, 0x651, 0x652, 0x653, 0x655, 0x656, 0x657, 0x658, + 0x659, 0x65a, 0x65b, 0x65c, 0x65e, 0x65f, 0x660, 0x662, 0x664, 0x665, 0x666, 0x668, 0x669, + 0x66e, 0x680, 0x681, 0x682, 0x683, 0x684, 0x685, 0x686, 0x687, 0x688, 0x689, 0x68a, 0x68b, + 0x68c, 0x68d, 0x68e, 0x68f, 0x690, 0x691, 0x692, 0x693, 0x694, 0x695, 0x696, 0x697, 0x698, + 0x699, 0x69a, 0x69b, 0x69c, 0x69d, 0x69e, 0x69f, 0x6b0, 0x6b1, 0x6c0, 0x6c1, 0x6c2, 0x6c3, + 0x6c4, 0x6c5, 0x6c6, 0x6c7, 0x6c8, 0x6c9, 0x6ca, 0x6cb, 0x6cc, 0x6cd, 0x6ce, 0x6cf, 0x6d0, + 0x6d1, 0x6d2, 0x6d3, 0x6d4, 0x6d5, 0x6d6, 0x6d7, 0x6d8, 0x6d9, 0x6da, 0x6db, 0x6dc, 0x6dd, + 0x6de, 0x6df, 0x700, 0x701, 0x702, 0x703, 0x704, 0x705, 0x706, 0x707, 0x708, 0x709, 0x70a, + 0x70b, 0x710, 0x711, 0x712, 0x713, 0x714, 0x715, 0x716, 0x717, 0x718, 0x719, 0x71a, 0x71b, + 0x720, 0x721, 0x722, 0x723, 0x724, 0x725, 0x726, 0x727, 0x728, 0x729, 0x72a, 0x72b, 0x72c, + 0x72d, 0x72e, 0x72f, 0x730, 0x731, 0x732, 0x733, 0x734, 0x735, 0x736, 0x737, 0x738, 0x739, + 0x73a, 0x73b, 0x73c, 0x73d, 0x73e, 0x73f, 0x740, 0x741, 0x742, 0x743, 0x744, 0x745, 0x746, + 0x747, 0x748, 0x749, 0x9ff, 0xc00, 0xc01, 0xc02, 0xc06, 0xc08, 0xc09, 0xc10, 0xc11, 0xc16, + 0xc17, 0xc20, 0xc21, 0xc22, 0xc24, 0xc30, 0xc31, 0xc32, 0xc33, 0xc34, 0xc35, 0xc36, 0xc37, + 0xc38, 0xc39, 0xc40, 0xc41, 0xc42, 0xc50, 0xc51, 0xc52, 0xc53, 0xc54, 0xc55, 0xc56, 0xc57, + 0xc60, 0xc61, 0xc62, 0xc70, 0xc71, 0xc72, 0xc73, 0xc74, 0xc75, 0xc76, 0xc77, 0xc84, 0xd94, + 0xd95, 0xd96, 0xd97, 0xd98, 0xd99, 0xd9a, 0xd9b, 0xda1, 0xda2, 0xda4, 0xdb3, 0xdb4, 0xdb5, + 0xdb6, 0xdb7, 0xdb8, 0xdb9, 0xdba, 0xdbb, 0xdc0, 0xdc1, 0xdc2, 0xdc3, 0xdc4, 0xdc5, 0xdc6, + 0xdc7, 0xdc8, 0xdc9, 0xdca, 0xdcb, 0xdcc, 0xdcd, 0xdce, 0xdcf, 0xdd0, 0xdd1, 0xdd2, 0xdd3, + 0xdd4, 0xdd5, 0xdd6, 0xdd7, 0xdd8, 0xdd9, 0xdda, 0xddb, 0xddc, 0xddd, 0xdde, 0xddf, 0xde0, + 0xde1, 0xde2, 0xde4, 0xdf0, 0xdf1, 0xdf2, 0xdf3, 0xdf4, 0xdf5, 0xdf6, 0xdf7, 0xdf8, 0xdf9, + 0xdfa, 0xdfb, 0xe02, 0xe03, 0xe04, 0xe05, 0xe06, 0xe07, 0xe08, 0xe09, 0xe0a, 0xe0b, 0xe0c, + 0xe0d, 0xe0e, 0xe0f, 0xe10, 0xe11, 0xe12, 0xe13, 0xe14, 0xe15, 0xe16, 0xe17, 0xe18, 0xe19, + 0xe1a, 0xe1b, 0xe1c, 0xe1d, 0xe1e, 0xe1f, 0xe20, 0xe21, 0xe22, 0xe23, 0xe24, 0xe25, 0xe26, + 0xe27, 0xe28, 0xe29, 0xe2a, 0xe2b, 0xe2c, 0xe2d, 0xe2e, 0xe2f, 0xe30, 0xe31, 0xe32, 0xe33, + 0xe34, 0xe35, 0xe36, 0xe37, 0xe38, 0xe39, 0xe3a, 0xe3b, 0xe3c, 0xe3d, 0xe3e, 0xe3f, 0xe40, + 0xe41, 0xe42, 0xe43, 0xe44, 0xe45, 0xe46, 0xe47, 0xe48, 0xe49, 0xe4a, 0xe4b, 0xe4d, 0xe4e, + 0xe50, 0xe51, 0xe52, 0xe53, 0xe54, 0xe55, 0xe56, 0xe57, 0xe58, 0xe59, 0xe5a, 0xe5c, 0xe5d, + 0xe5e, 0xe60, 0xe61, 0xe62, 0xe63, 0xe64, 0xe65, 0xe66, 0xe67, 0xe68, 0xe69, 0xe6a, 0xe6b, + 0xe70, 0xe71, 0xe72, 0xe73, 0xe74, 0xe75, 0xe76, 0xe77, 0xe78, 0xe79, 0xe7a, 0xe7b, 0xe80, + 0xe81, 0xe82, 0xe83, 0xe84, 0xe85, 0xe86, 0xe87, 0xe88, 0xe89, 0xe8b, 0xe90, 0xe91, 0xe92, + 0xe93, 0xe94, 0xe95, 0xe96, 0xe97, 0xe98, 0xe99, 0xe9a, 0xe9b, 0xea0, 0xea1, 0xea2, 0xea3, + 0xea4, 0xea5, 0xea6, 0xea7, 0xea8, 0xea9, 0xeaa, 0xeab, 0xeb0, 0xeb1, 0xeb2, 0xeb3, 0xeb4, + 0xeb5, 0xeb6, 0xeb7, 0xeb8, 0xeb9, 0xeba, 0xebb, 0xec0, 0xec1, 0xec2, 0xec3, 0xec4, 0xec5, + 0xec6, 0xec7, 0xec8, 0xec9, 0xeca, 0xecb, 0xed0, 0xed1, 0xed2, 0xed3, 0xed4, 0xed5, 0xed6, + 0xed7, 0xed8, 0xed9, 0xeda, 0xedb, 0xee0, 0xee1, 0xee2, 0xee3, 0xee4, 0xee5, 0xee6, 0xee7, + 0xee8, 0xee9, 0xeea, 0xeeb, 0xef0, 0xef1, 0xef2, 0xef3, 0xef4, 0xef5, 0xef6, 0xef7, 0xef8, + 0xef9, 0xefa, 0xefb, 0xf00, 0xf01, 0xf02, 0xf03, 0xf04, 0xf05, 0xf06, 0xf07, 0xf08, 0xf09, + 0xf0a, 0xf0b, 0xf10, 0xf11, 0xf12, 0xf13, 0xf14, 0xf15, 0xf16, 0xf17, 0xf18, 0xf19, 0xf1a, + 0xf1b, 0xf40, 0xf41, 0xf42, 0xf50, 0xf51, 0xf52, 0xf53, 0xf54, 0xf55, 0xf56, 0xf57, 0xf58, + 0xf59, 0xf5a, 0xf5b, 0xfc0, 0xfc1, 0xfc2, 0xfd0, 0xfd1, 0xfd2, 0xfd3, 0xfd4, 0xfd5, 0xfd6, + 0xfd7, 0xfd8, 0xfd9, 0xfda, 0xfdb, 0x1309, 0x130a, 0x130b, 0x14c1, 0x14c2, 0x14c3, 0x14c4, + 0x14c5, 0x14c6, 0x14c7, 0x14c8, 0x1878, 0x1a8e, 0x1a8f, 0x2000, 0x2001, 0x2002, 0x2003, 0x2008, + 0x2009, 0x200a, 0x200b, 0x2010, 0x2011, 0x2012, 0x2013, 0x2018, 0x2019, 0x201a, 0x201b, 0x2020, + 0x2021, 0x2022, 0x2023, 0x2028, 0x2029, 0x202a, 0x202b, 0x2030, 0x2031, 0x2032, 0x2033, 0x2038, + 0x2039, 0x203a, 0x203b, 0x2040, 0x2041, 0x2042, 0x2043, 0x2048, 0x2049, 0x204a, 0x204b, 0x2fd0, + 0x2fd1, 0x2fd2, 0x2fd3, 0x2fd4, 0x2fd5, 0x2fd8, 0x2fd9, 0x2fda, 0x2fdb, 0x2fdc, 0x2fdd, 0x2fde, + 0x2fdf, 0x2ff0, 0x2ff2, 0x107cc, 0x107cd, 0x107ce, 0x107cf, 0x107d0, 0x107d1, 0x107d2, 0x107d3, + 0x107d8, +]; + +// TODO: Look out for 0x13c (used to check for AES instruction on Intel Atom and ..)? +// TODO: 0x35 gives THREAD_COUNT will some programs stop working if we deny this MSR? + +// TODO: It is perfectly possible to convert the following test into compile time checks. +// We take care of that later. +#[cfg(test)] +mod tests { + use super::super::msr_based_features::INTEL_MSR_FEATURE_DEFINITIONS; + use super::super::{FORBIDDEN_IA32_MSR_RANGES, PERMITTED_IA32_MSRS}; + use super::NON_ARCHITECTURAL_INTEL_MSRS; + #[test] + fn disjoint_from_others() { + let mut unique_count = 0; + for msr in NON_ARCHITECTURAL_INTEL_MSRS { + if (!PERMITTED_IA32_MSRS.contains(&msr)) + && (!FORBIDDEN_IA32_MSR_RANGES + .iter() + .any(|r| (r.0..=r.1).contains(&msr))) + && (!INTEL_MSR_FEATURE_DEFINITIONS + .as_slice() + .iter() + .any(|(address, _)| address.0 == msr)) + { + unique_count += 1; + } + } + assert_eq!(unique_count, NON_ARCHITECTURAL_INTEL_MSRS.len()); + } +} diff --git a/arch/src/x86_64/msr_definitions/kvm.rs b/arch/src/x86_64/msr_definitions/kvm.rs new file mode 100644 index 0000000000..d85cc22f98 --- /dev/null +++ b/arch/src/x86_64/msr_definitions/kvm.rs @@ -0,0 +1,93 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! This module lists KVM defined MSRS. It is currently only used when generating CPU profiles +//! (hence feature gated), but may possibly be extended and utilized for better debug logs in +//! the future. +pub(in crate::x86_64) use permitted_msrs::PROFILE_PERMITTED_KVM_MSRS; + +use crate::x86_64::CpuidReg; +use crate::x86_64::cpuid_definitions::Parameters; + +mod permitted_msrs { + use super::{CpuidReg, Parameters}; + use crate::x86_64::cpuid_definitions::kvm::assert_not_denied_cpuid_feature; + + const MSR_KVM_WALL_CLOCK: u32 = 0x11; + const MSR_KVM_SYSTEM_TIME: u32 = 0x12; + const _KVM_CLOCKSOURCE_CPUID_CHECK: () = assert_not_denied_cpuid_feature::<0>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_WALL_CLOCK_NEW: u32 = 0x4b564d00; + const MSR_KVM_SYSTEM_TIME_NEW: u32 = 0x4b564d01; + const _KVM_CLOCKSOURCE2_CHECK: () = assert_not_denied_cpuid_feature::<3>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_ASYNC_PF_EN: u32 = 0x4b564d02; + const _KVM_ASYNC_PF_CHECK: () = assert_not_denied_cpuid_feature::<4>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_STEAL_TIME: u32 = 0x4b564d03; + const _KVM_STEAL_TIME_CHECK: () = assert_not_denied_cpuid_feature::<5>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_EOI_EN: u32 = 0x4b564d04; + const _KVM_EOI_EN_CHECK: () = assert_not_denied_cpuid_feature::<6>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_POLL_CONTROL: u32 = 0x4b564d05; + const _KVM_POLL_CONTROL_CHECK: () = assert_not_denied_cpuid_feature::<12>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_ASYNC_PF_INT: u32 = 0x4b564d06; + const MSR_KVM_ASYNC_PF_ACK: u32 = 0x4b564d07; + const _KVM_ASYNC_PF_INT_ACK_CHECK: () = assert_not_denied_cpuid_feature::<14>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + const MSR_KVM_MIGRATION_CONTROL: u32 = 0x4b564d08; + const _KVM_MIGRATION_CONTROL_CHECK: () = assert_not_denied_cpuid_feature::<17>(&Parameters { + leaf: 0x4000_0001, + sub_leaf: (0..=0), + register: CpuidReg::EAX, + }); + + /// KVM defined MSRS that CPU profiles may inclide in their permitted MSR definitions. + /// + /// This list is (currently) only utilized when generating CPU profiles. + pub(in crate::x86_64) const PROFILE_PERMITTED_KVM_MSRS: [u32; 11] = [ + MSR_KVM_WALL_CLOCK, + MSR_KVM_SYSTEM_TIME, + MSR_KVM_WALL_CLOCK_NEW, + MSR_KVM_SYSTEM_TIME_NEW, + MSR_KVM_ASYNC_PF_EN, + MSR_KVM_STEAL_TIME, + MSR_KVM_EOI_EN, + MSR_KVM_POLL_CONTROL, + MSR_KVM_ASYNC_PF_INT, + MSR_KVM_ASYNC_PF_ACK, + MSR_KVM_MIGRATION_CONTROL, + ]; +} diff --git a/arch/src/x86_64/msr_definitions/mod.rs b/arch/src/x86_64/msr_definitions/mod.rs new file mode 100644 index 0000000000..805b83c863 --- /dev/null +++ b/arch/src/x86_64/msr_definitions/mod.rs @@ -0,0 +1,101 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use serde::{Deserialize, Serialize}; +pub mod intel; +#[cfg(all(feature = "kvm", feature = "cpu_profile_generation"))] +pub mod kvm; + +pub mod hyperv; + +use crate::{deserialize_u32_hex, serialize_u32_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, +); + +/// Describes a policy for how the corresponding MSR data should be considered when building +/// a CPU profile. +/// +/// This is the MSR analogue of [cpuid_definitions::ProfilePolicy](crate::x86_64::cpuid_definitions::ProfilePolicy) +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ProfilePolicy { + /// Store the corresponding data when building the CPU profile. + /// + /// When the CPU profile gets utilized the corresponding data will be set into the modified + /// MSR(s) + Inherit, + /// Ignore the corresponding data when building the CPU profile. + /// + /// When the CPU profile gets utilized the corresponding data will then instead get + /// extracted from the host. + /// + /// This variant is typically set for data that has no effect on migration compatibility, + /// but there may be some exceptions such as data which is necessary to run the VM at all, + /// but must coincide with whatever is on the host. + Passthrough, + /// Set the following hardcoded value in the CPU profile. + /// + /// This variant is typically used for features/values that don't work well with live migration (even when using the exact same physical CPU model). + Static(u64), + /// Deny read and write accesses to this MSR. + /// + /// This can only be applied to an MSR in its entirety and not to individual bit ranges + Deny, +} + +/// A description of a range of bits in an MSR. +/// +/// This is the MSR analogue of [cpuid_definitions::ValueDefinition](crate::x86_64::cpuid_definitions::ValueDefinition) +#[derive(Clone, Copy, Debug)] +pub struct ValueDefinition { + /// A short name for the value. + pub short: &'static str, + /// A description of the value. + pub description: &'static str, + /// The range of bits in the MSR corresponding to this feature or value. + /// + /// This is not a `RangeInclusive` because that type does unfortunately not implement `Copy`. + pub bits_range: (u8, u8), + /// The policy corresponding to this value when building CPU profiles. + pub policy: ProfilePolicy, +} + +/// Describes values within an MSR. +/// +/// NOTE: The only way to interact with this value (beyond this crate) is via the const [`Self::as_slice()`](Self::as_slice) method. +/// +/// This is the MSR analogue of [cpuid_definitions::ValueDefinitions](crate::x86_64::cpuid_definitions::ValueDefinitions) +#[derive(Clone, Copy, Debug)] +pub struct ValueDefinitions(&'static [ValueDefinition]); +impl ValueDefinitions { + /// Constructor permitting at most 64 entries. + const fn new(msr_descriptions: &'static [ValueDefinition]) -> Self { + // Note that this function is only called within this module, at compile time, hence it is fine to have some + // additional sanity checks such as the following assert. + assert!(msr_descriptions.len() <= 64); + Self(msr_descriptions) + } + /// Converts this into a slice representation. This is the only way to read values of this type. + pub const fn as_slice(&self) -> &'static [ValueDefinition] { + self.0 + } +} + +/// Describes multiple MSRs. +/// +/// Each wrapped [`ValueDefinitions`] corresponds to the given [`RegisterAddress`] in the same tuple. +pub struct MsrDefinitions([(RegisterAddress, ValueDefinitions); NUM]); + +impl MsrDefinitions { + pub const fn as_slice(&self) -> &[(RegisterAddress, ValueDefinitions); NUM] { + &self.0 + } +} diff --git a/arch/src/x86_64/msr_filter.rs b/arch/src/x86_64/msr_filter.rs new file mode 100644 index 0000000000..0edad0e364 --- /dev/null +++ b/arch/src/x86_64/msr_filter.rs @@ -0,0 +1,361 @@ +// Copyright © 2025 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::cell::Cell; +use std::fmt::Write; + +use hypervisor::MsrFilterRange; + +use super::Error; + +/// The maximum number of MSR filter ranges an MSR filter may consist of. +const MAX_FILTERS: usize = { + #[cfg(feature = "kvm")] + { + hypervisor::kvm::KVM_MSR_FILTER_MAX_RANGES + } + #[cfg(not(feature = "kvm"))] + { + // TODO: Change this when adding support for CPU profiles with MSHV + 16 + } +}; + +/// THE maximum number of bytes the bitmap arena used for the filter may occupy. +/// This is to ensure that we do not allocate too much memory for the bitmaps in +/// the filter ranges. +pub const MAX_BITMAP_SIZE: usize = MAX_FILTERS * 1024 * 1024; + +/// Apply a filter which denies guests any kind of access to the MSRs in `denied_msrs`. +/// +/// # Assumptions +/// +/// This function may explicitly mark certain MSRs different from those in `denied_msrs` as +/// both READ + Write permitted. We assume that the hypervisor will permit this filter being set +/// regardless and rather injects an exception if guests attempt to read/modify these MSRs in anyway +/// that is incompatible with the hardware and/or hypervisor. +/// +/// # Errors +/// +/// This errors if any of the following conditions hold: +/// +/// 1. Too much memory is required to construct the MSR filter that covers all of the denied MSRs. +/// 2. The VM/Hypervisor fails to apply the MSR filter. +pub fn filter_denied_msrs( + mut denied_msrs: Vec, + vm: &dyn hypervisor::Vm, +) -> Result<(), crate::Error> { + if denied_msrs.is_empty() { + return Ok(()); + } + denied_msrs.sort_unstable(); + + for msr in &denied_msrs { + log::debug!("MSR:={msr:#x} is set to be denied"); + } + + let mut bitmap_arena = Vec::new(); + let (filter, num_filter_ranges) = denied_to_filter(&denied_msrs, &mut bitmap_arena)?; + + if let Err(e) = vm.msr_filter(&filter[..num_filter_ranges], false) { + // Log more details at the debug level. Note that this error is likely to be reproducible and happens close to startup, hence + // it should be relatively easy to set the necessary log level if/when debugging becomes desirable. + for filter_range in &filter[..num_filter_ranges] { + // We want to encode the bitmap as a string of the form "[, ,... ]" + let mut bitmap_hex_encoded = String::with_capacity((4 * filter_range.bitmap.len()) + 2); + let _ = write!(&mut bitmap_hex_encoded, "["); + for b in filter_range.bitmap.iter() { + let _ = write!(&mut bitmap_hex_encoded, "{b:#x},"); + } + // Remove the final "," from the string + bitmap_hex_encoded.pop(); + let _ = write!(&mut bitmap_hex_encoded, "]"); + log::debug!( + "Failed to set MSR filter containing filter range: base:={:#x}, nmsrs:={:#x}, bitmap:={}", + filter_range.base, + filter_range.nmsrs, + bitmap_hex_encoded + ); + } + Err(Into::into(Error::MsrFilter(e))) + } else { + Ok(()) + } +} + +/// Essentially partitions `denied_sorted` into up to [`MAX_FILTERS`] ranges of +/// indices. +/// +/// These ranges may then be used to place the MSRs into distinct [`MsrFilterRanges`](MsrFilterRange). +/// In other words; If (a,b) is an entry in the output of this function, then all MSRs in +/// `denied_sorted[a..=b]` are intended to be placed in the same filter range. +/// +/// This partition minimizes the amount of memory necessary to construct the bitmaps for each +/// MSR filter range, that collectively cover all MSRs in `denied_sorted`, under the constraint +/// that none of the MSR filter ranges can intersect the x2APIC-related MSR range (0x801..=0x8ff). +/// +/// ## Performance +/// +/// This function has complexity` O(MAX_FILTERS * denied_sorted.len())` and does not allocate. +fn denied_to_range_indices<'a>( + denied_sorted: &[u32], + r_buff: &'a mut [(usize, usize); MAX_FILTERS], +) -> &'a [(usize, usize)] { + let mut d_prevs = [u32::MAX; MAX_FILTERS]; + let mut r_cnt = 0; + let mut min_dprev = u32::MAX; + let mut min_pos = 0_usize; + + let compute_dprev = |p: u32, n: u32| { + // Make dprev impractically large if it overlaps the x2apic MSR range + if (p <= 0x8ff) && (n > 0x800) { + u32::MAX + } else { + n - p + } + }; + + // Called as soon as we discover a full contiguous range of MSRs to be denied + // `r_s` is the index of the first MSR in this range and `r_e` the last. + let mut eval_deny_range = |r_s: usize, r_e: usize| { + const LAST_IDX: usize = MAX_FILTERS - 1; + let is_first = r_cnt == 0; + + let d_prev = if is_first { + u32::MAX + } else { + let l_prev_idx = r_buff[r_cnt - 1].1; + let l_prev = denied_sorted[l_prev_idx]; + compute_dprev(l_prev, denied_sorted[r_s]) + }; + + if r_cnt < MAX_FILTERS { + d_prevs[r_cnt] = d_prev; + r_buff[r_cnt] = (r_s, r_e); + if d_prev < min_dprev { + min_dprev = d_prev; + min_pos = r_cnt; + } + r_cnt += 1; + } else { + // Need to join ranges to find space + // The idea is to merge the range groups closest to each other + if d_prev <= min_dprev { + // Make the final range group cover this range + r_buff[LAST_IDX].1 = r_e; + } else { + // Merge some previously gathered range groups to make space + r_buff[min_pos - 1].1 = r_buff[min_pos].1; + // shift every thing after min_pos left + { + shift_left(&mut r_buff[min_pos..]); + shift_left(&mut d_prevs[min_pos..]); + } + // Now we have space for the new entry + r_buff[LAST_IDX] = (r_s, r_e); + d_prevs[LAST_IDX] = d_prev; + // Recompute minimum meta data + min_dprev = *d_prevs.iter().min().unwrap(); + min_pos = d_prevs.iter().position(|d| *d == min_dprev).unwrap(); + } + } + }; + // Produce all range groups + let mut offset = 0_usize; + let mut deny_slice = denied_sorted; + while let Some(deny_slice_skip1) = deny_slice.get(1..) { + let Some(pos) = deny_slice_skip1 + .iter() + .zip(deny_slice) + .position(|(n, p)| (n - p) > 1) + else { + break; + }; + let r_s = offset; + let r_e = offset + pos; + eval_deny_range(r_s, r_e); + offset = r_e + 1; + deny_slice = &denied_sorted[offset..]; + } + // Since there is no gap beyond the last element, we have one final deny range to + // evaluate + eval_deny_range(offset, denied_sorted.len() - 1); + &r_buff[..r_cnt] +} + +/// Construct `range_indices.len() (<= MAX_FILTERS)` [`MsrFilterRanges`](MsrFilterRange) +/// to deny all MSRs in `denied_sorted`. +/// +/// For each pair `(r_s, r_e)` in `range_indices` there will be a corresponding +/// filter range denying the MSRs in [`denied_sorted[r_s..=r_e]`]. +/// +/// # Errors +/// +/// This function can only error if more than [`MAX_BITMAP_SIZE`] bytes are required +/// to construct the filters. +/// +/// # Performance +/// +/// This function allocates once (but a possibly large allocation) and has otherwise +/// computational complexity `O(MAX_FILTERS * denied_sorted.len())`. +fn range_indices_to_filter<'a>( + denied_sorted: &[u32], + range_indices: &[(usize, usize)], + bitmap_arena: &'a mut Vec, +) -> Result<[MsrFilterRange<'a>; MAX_FILTERS], Error> { + let mut out = [MsrFilterRange::default().with_read_write_flags(); MAX_FILTERS]; + let bytes_to_allocate: usize = range_indices + .iter() + .copied() + .map(|(s, e)| ((denied_sorted[e] - denied_sorted[s]) + 1).div_ceil(8)) + .map(|v| v as usize) + .sum(); + + if bytes_to_allocate > MAX_BITMAP_SIZE { + return Err(Error::MsrFilterTooLarge(bytes_to_allocate)); + } + + bitmap_arena.extend(std::iter::repeat_n(u8::MAX, bytes_to_allocate)); + + let mut arena_slice = &mut bitmap_arena[..]; + for (idx, (r_s, r_e)) in range_indices.iter().enumerate() { + let base = denied_sorted[*r_s]; + let nmsrs = (denied_sorted[*r_e] - denied_sorted[*r_s]) + 1; + let (bm, rest) = arena_slice.split_at_mut(nmsrs.div_ceil(8) as usize); + arena_slice = rest; + for msr in &denied_sorted[*r_s..=*r_e] { + let d_base = *msr - base; + let byte_idx = (d_base) / 8; + let bit = 1 << (d_base % 8); + bm[byte_idx as usize] ^= bit; + } + // Set the fields in the range filter + { + let filter_range = &mut out[idx]; + filter_range.base = base; + filter_range.nmsrs = nmsrs; + filter_range.bitmap = bm; + } + } + + Ok(out) +} + +/// Prepare up to [`MAX_FILTERS`] [`MsrFilterRanges`](MsrFilterRange) +/// that collectively deny each of the MSRs specified in `denied_sorted`. +/// +/// The second component returned from this function is the number of +/// valid entries in the returned array. +/// +/// # Errors +/// +/// This function can only error if more than [`MAX_BITMAP_SIZE`] bytes are required +/// to construct the filters. +fn denied_to_filter<'a>( + denied_sorted: &[u32], + bitmap_arena: &'a mut Vec, +) -> Result<([MsrFilterRange<'a>; MAX_FILTERS], usize), Error> { + let mut range_indices_buffer = [(0, 0); MAX_FILTERS]; + let range_indices = denied_to_range_indices(denied_sorted, &mut range_indices_buffer); + + range_indices_to_filter(denied_sorted, range_indices, bitmap_arena) + .map(|filter| (filter, range_indices.len())) +} + +/// Convenience function that moves all elements apart from the first and last left by one. +/// +/// The slice's first element will be removed from the slice, while the modified +/// slice's last element will be equal to the second last (prior to calling this method). +fn shift_left(slice: &mut [T]) { + for w in Cell::from_mut(slice).as_slice_of_cells().windows(2) { + Cell::swap(&w[0], &w[1]); + } +} + +#[cfg(test)] +mod unit_tests { + use hypervisor::MsrFilterRange; + use proptest::prelude::*; + + use super::{MAX_BITMAP_SIZE, MAX_FILTERS, denied_to_filter}; + + /// transforms entries out of the x2apic MSR range and sorts + dedups the vector + fn prepare(bases: Vec) -> Vec { + // Remove bases in the x2apic MSR range + let mut v: Vec = bases + .into_iter() + .map(|b| { + if (0x800..=0x8ff).contains(&b) { + b % 0x800 + } else { + b + } + }) + .collect(); + v.sort_unstable(); + v.dedup(); + v + } + + fn filter_to_msrs(filter: &[MsrFilterRange<'_>]) -> Vec { + let mut out = Vec::new(); + for filter_range in filter { + let base = filter_range.base; + let mut num_msrs: u32 = 0; + for byte in filter_range.bitmap { + let mut inverse = !(*byte); + while inverse != 0 { + let idx = inverse.trailing_zeros(); + if num_msrs + idx > filter_range.nmsrs { + break; + } + out.push(base + num_msrs + idx); + let lsb = inverse & inverse.wrapping_neg(); + inverse ^= lsb; + } + num_msrs += 8; + } + } + out + } + + proptest! { + #[test] + fn denied_to_filer_works_short(prepared_msrs in (prop::collection::vec(0..u32::MAX, 1..MAX_FILTERS)).prop_map(prepare)) { + let mut bitmap_arena = Vec::new(); + let Ok((filter, num_filter_ranges)) = denied_to_filter(&prepared_msrs, &mut bitmap_arena) else { + return Ok(()); + }; + let mut recomputed_msrs = filter_to_msrs(&filter[..num_filter_ranges]); + recomputed_msrs.sort_unstable(); + prop_assert_eq!(prepared_msrs, recomputed_msrs); + } + } + + proptest! { + #[test] + fn denied_to_filer_works(prepared_msrs in (prop::collection::vec(0..u32::MAX, 17..70)).prop_map(prepare)) { + let mut bitmap_arena = Vec::new(); + let Ok((filter, num_filter_ranges)) = denied_to_filter(&prepared_msrs, &mut bitmap_arena) else { + return Ok(()); + }; + let mut recomputed_msrs = filter_to_msrs(&filter[..num_filter_ranges]); + recomputed_msrs.sort_unstable(); + prop_assert_eq!(prepared_msrs, recomputed_msrs); + } + } + + // Simple test that doesn't take too long to execute. We can + // include a more thorough test later if desired. + #[test] + fn catches_attempt_to_allocate_too_much_memory() { + let mut bitmap_arena = Vec::new(); + let denied_msrs: Vec = (0..MAX_FILTERS * 8 * 2) + .map(|i| i * MAX_BITMAP_SIZE) + .map(|v| u32::try_from(v).unwrap()) + .collect(); + let _ = denied_to_filter(&denied_msrs, &mut bitmap_arena).unwrap_err(); + } +} diff --git a/arch/src/x86_64/regs.rs b/arch/src/x86_64/regs.rs index 3826fdb6ce..baaedf57ed 100644 --- a/arch/src/x86_64/regs.rs +++ b/arch/src/x86_64/regs.rs @@ -6,12 +6,11 @@ // 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::sync::Arc; use std::{mem, result}; use hypervisor::arch::x86::gdt::{gdt_entry, segment_from_gdt}; use hypervisor::arch::x86::regs::CR0_PE; -use hypervisor::arch::x86::{FpuState, SpecialRegisters}; +use hypervisor::arch::x86::{FpuState, MsrEntry, SpecialRegisters}; use thiserror::Error; use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError}; @@ -34,6 +33,8 @@ pub enum Error { /// Setting up MSRs failed. #[error("Setting up MSRs failed")] SetModelSpecificRegisters(#[source] hypervisor::HypervisorCpuError), + #[error("Setting up MSRs failed: Not all MSRs could be set. See logs for more info.")] + SetModelSpecificRegistersPartial, /// Failed to set SREGs for this CPU. #[error("Failed to set SREGs for this CPU")] SetStatusRegisters(#[source] hypervisor::HypervisorCpuError), @@ -67,7 +68,7 @@ pub type Result = result::Result; /// # Arguments /// /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. -pub fn setup_fpu(vcpu: &Arc) -> Result<()> { +pub fn setup_fpu(vcpu: &dyn hypervisor::Vcpu) -> Result<()> { let fpu: FpuState = FpuState { fcw: 0x37f, mxcsr: 0x1f80, @@ -82,11 +83,35 @@ pub fn setup_fpu(vcpu: &Arc) -> Result<()> { /// # Arguments /// /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. -pub fn setup_msrs(vcpu: &Arc) -> Result<()> { - vcpu.set_msrs(&vcpu.boot_msr_entries()) - .map_err(Error::SetModelSpecificRegisters)?; +/// * `feature_msr_updates` - A (possibly empty) slice of MSR-based features +/// that should be set as as part of the setup. If the slice is empty then +/// only boot msr entries are set, otherwise the given slice will also be +/// included in the setup. +pub fn setup_msrs(vcpu: &dyn hypervisor::Vcpu, feature_msr_updates: &[MsrEntry]) -> Result<()> { + let boot_entries = vcpu.boot_msr_entries(); + let mut entries_for_update = Vec::new(); + let setup_entries: &mut &[MsrEntry] = &mut (&boot_entries[..]); - Ok(()) + if !feature_msr_updates.is_empty() { + entries_for_update.extend_from_slice(feature_msr_updates); + entries_for_update.extend_from_slice(boot_entries); + *setup_entries = &entries_for_update[..]; + } + let num_msrs_written = vcpu + .set_msrs(setup_entries) + .map_err(Error::SetModelSpecificRegisters)?; + if num_msrs_written < setup_entries.len() { + for msr in &setup_entries[num_msrs_written..] { + log::error!( + "Could not set MSR with register address:={:#x} and value:={:#x}", + msr.index, + msr.data + ); + } + Err(Into::into(Error::SetModelSpecificRegistersPartial)) + } else { + Ok(()) + } } /// Configure base registers for a given CPU. @@ -95,7 +120,7 @@ pub fn setup_msrs(vcpu: &Arc) -> Result<()> { /// /// * `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: &Arc, entry_point: EntryPoint) -> Result<()> { +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 => { @@ -109,7 +134,7 @@ pub fn setup_regs(vcpu: &Arc, entry_point: EntryPoint) -> regs.set_rsp(BOOT_STACK_POINTER.raw_value()); regs.set_rsi(ZERO_PAGE_START.raw_value()); } - }; + } vcpu.set_regs(®s).map_err(Error::SetBaseRegisters) } @@ -119,9 +144,13 @@ pub fn setup_regs(vcpu: &Arc, entry_point: EntryPoint) -> /// /// * `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: &Arc) -> Result<()> { +pub fn setup_sregs( + mem: &GuestMemoryMmap, + vcpu: &dyn hypervisor::Vcpu, + enable_x2_apic_mode: bool, +) -> Result<()> { let mut sregs: SpecialRegisters = vcpu.get_sregs().map_err(Error::GetStatusRegisters)?; - configure_segments_and_sregs(mem, &mut sregs)?; + configure_segments_and_sregs(mem, &mut sregs, enable_x2_apic_mode)?; vcpu.set_sregs(&sregs).map_err(Error::SetStatusRegisters) } @@ -148,6 +177,7 @@ 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 @@ -183,11 +213,16 @@ 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 tests { +mod unit_tests { use vm_memory::GuestAddress; use super::*; @@ -204,7 +239,7 @@ mod tests { fn segments_and_sregs() { let mut sregs: SpecialRegisters = Default::default(); let gm = create_guest_mem(); - configure_segments_and_sregs(&gm, &mut sregs).unwrap(); + configure_segments_and_sregs(&gm, &mut sregs, false).unwrap(); assert_eq!(0x0, read_u64(&gm, BOOT_GDT_START)); assert_eq!( 0xcf9b000000ffff, diff --git a/arch/src/x86_64/smbios.rs b/arch/src/x86_64/smbios.rs index 55a7df1e72..02965f4685 100644 --- a/arch/src/x86_64/smbios.rs +++ b/arch/src/x86_64/smbios.rs @@ -12,8 +12,8 @@ use thiserror::Error; use uuid::Uuid; use vm_memory::{Address, ByteValued, Bytes, GuestAddress}; -use crate::layout::SMBIOS_START; use crate::GuestMemoryMmap; +use crate::layout::SMBIOS_START; #[derive(Debug, Error)] pub enum Error { @@ -33,24 +33,64 @@ pub enum Error { #[error("Failure to write additional data to memory")] WriteData, /// Failure to parse uuid, uuid format may be error - #[error("Failure to parse uuid")] - ParseUuid(#[source] uuid::Error), + #[error("Failure to parse uuid: {1}")] + ParseUuid(#[source] uuid::Error, String), + /// SMBIOS string index overflow (u8 limit reached). + #[error("SMBIOS string index overflow (u8 limit reached: {})", u8::MAX)] + TooManyStrings, } pub type Result = result::Result; -// Constants sourced from SMBIOS Spec 3.2.0. +// Constants sourced from SMBIOS Spec 3.9.0. const SM3_MAGIC_IDENT: &[u8; 5usize] = b"_SM3_"; const BIOS_INFORMATION: u8 = 0; const SYSTEM_INFORMATION: u8 = 1; const OEM_STRINGS: u8 = 11; +const SYSTEM_ENCLOSURE: u8 = 3; const END_OF_TABLE: u8 = 127; +const SYSTEM_WAKE_UP_TYPE_UNKNOWN: u8 = 0x02; +const CHASSIS_TYPE_UNKNOWN: u8 = 0x02; +const CHASSIS_STATE_UNKNOWN: u8 = 0x02; +const CHASSIS_SECURITY_STATUS_NONE: u8 = 0x03; const PCI_SUPPORTED: u64 = 1 << 7; const IS_VIRTUAL_MACHINE: u8 = 1 << 4; +pub const DEFAULT_SYSTEM_MANUFACTURER: &str = "Cloud Hypervisor"; +pub const DEFAULT_SYSTEM_PRODUCT_NAME: &str = "cloud-hypervisor"; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmbiosConfig { + pub system: Option, + pub chassis: Option, + pub oem_strings: Box<[String]>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmbiosSystem { + pub manufacturer: Option, + pub product_name: Option, + pub version: Option, + pub serial_number: Option, + pub uuid: Option, + pub sku_number: Option, + pub family: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmbiosChassisConfig { + pub asset_tag: Option, +} + +impl SmbiosConfig { + pub fn is_empty(&self) -> bool { + *self == Self::default() + } +} fn compute_checksum(v: &T) -> u8 { + let v: *const T = v; // SAFETY: we are only reading the bytes within the size of the `T` reference `v`. - let v_slice = unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::()) }; + let v_slice = unsafe { slice::from_raw_parts(v.cast(), mem::size_of::()) }; let mut checksum: u8 = 0; for i in v_slice.iter() { checksum = checksum.wrapping_add(*i); @@ -58,8 +98,7 @@ fn compute_checksum(v: &T) -> u8 { (!checksum).wrapping_add(1) } -#[repr(C)] -#[repr(packed)] +#[repr(C, packed)] #[derive(Default, Copy, Clone)] struct Smbios30Entrypoint { signature: [u8; 5usize], @@ -74,8 +113,7 @@ struct Smbios30Entrypoint { physptr: u64, } -#[repr(C)] -#[repr(packed)] +#[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosBiosInfo { r#type: u8, @@ -91,8 +129,7 @@ struct SmbiosBiosInfo { characteristics_ext2: u8, } -#[repr(C)] -#[repr(packed)] +#[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosSysInfo { r#type: u8, @@ -108,8 +145,7 @@ struct SmbiosSysInfo { family: u8, } -#[repr(C)] -#[repr(packed)] +#[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosOemStrings { r#type: u8, @@ -118,8 +154,34 @@ struct SmbiosOemStrings { count: u8, } -#[repr(C)] -#[repr(packed)] +/// SMBIOS Chassis Table (Type 3) as defined in DMTF SMBIOS 3.9.0: +/// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf +/// Note: trailing fields are omitted, so this structure is not complete. +#[repr(C, packed)] +#[derive(Default, Copy, Clone)] +struct SmbiosChassis { + r#type: u8, + length: u8, + handle: u16, + manufacturer: u8, + chassis_type: u8, + version: u8, + serial_number: u8, + asset_tag: u8, + bootup_state: u8, + power_supply_state: u8, + thermal_state: u8, + security_status: u8, + oem_defined: u32, + height: u8, + number_of_power_cords: u8, + contained_element_count: u8, + contained_element_record_length: u8, + // followed by contained element records (optional, variable-length) + // followed by sku_number: u8, rack_type: u8, rack_height: u8 +} + +#[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosEndOfTable { r#type: u8, @@ -136,6 +198,8 @@ unsafe impl ByteValued for SmbiosSysInfo {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosOemStrings {} // SAFETY: data structure only contain a series of integers +unsafe impl ByteValued for SmbiosChassis {} +// SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosEndOfTable {} fn write_and_incr( @@ -162,12 +226,153 @@ fn write_string( Ok(curptr) } -pub fn setup_smbios( +fn write_opt_string( + mem: &GuestMemoryMmap, + s: Option<&str>, + cur: GuestAddress, +) -> Result { + 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 { + // 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 { + 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, - serial_number: Option<&str>, - uuid: Option<&str>, - oem_strings: Option<&[&str]>, -) -> Result { + 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: mem::size_of::() 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: mem::size_of::() 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 { + let system = smbios.and_then(|cfg| cfg.system.as_ref()); + let chassis = smbios.and_then(|cfg| cfg.chassis.as_ref()); + let oem_strings: &[String] = smbios.map_or(&[], |cfg| &cfg.oem_strings); let physptr = GuestAddress(SMBIOS_START) .checked_add(mem::size_of::() as u64) .ok_or(Error::NotEnoughMemory)?; @@ -192,34 +397,13 @@ pub fn setup_smbios( curptr = write_and_incr(mem, 0u8, curptr)?; } - { - handle += 1; + write_type1_system(mem, &mut curptr, &mut handle, system)?; - let uuid_number = uuid - .map(Uuid::parse_str) - .transpose() - .map_err(Error::ParseUuid)? - .unwrap_or(Uuid::nil()); - let smbios_sysinfo = SmbiosSysInfo { - r#type: SYSTEM_INFORMATION, - length: mem::size_of::() as u8, - handle, - manufacturer: 1, // First string written in this section - product_name: 2, // Second string written in this section - serial_number: serial_number.map(|_| 3).unwrap_or_default(), // 3rd string - uuid: uuid_number.to_bytes_le(), // set uuid - ..Default::default() - }; - curptr = write_and_incr(mem, smbios_sysinfo, curptr)?; - curptr = write_string(mem, "Cloud Hypervisor", curptr)?; - curptr = write_string(mem, "cloud-hypervisor", curptr)?; - if let Some(serial_number) = serial_number { - curptr = write_string(mem, serial_number, curptr)?; - } - curptr = write_and_incr(mem, 0u8, curptr)?; + if let Some(chassis) = chassis { + write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?; } - if let Some(oem_strings) = oem_strings { + if !oem_strings.is_empty() { handle += 1; let smbios_oemstrings = SmbiosOemStrings { @@ -235,7 +419,7 @@ pub fn setup_smbios( curptr = write_string(mem, s, curptr)?; } - curptr = write_and_incr(mem, 0u8, curptr)?; + curptr = write_string_terminator(mem, curptr, true)?; } { @@ -272,11 +456,58 @@ pub fn setup_smbios( } #[cfg(test)] -mod tests { +mod unit_tests { use super::*; + /// Collects all strings after a SMBIOS structure, stopping at the double-NUL terminator and returns next addr. + fn read_string_set(mem: &GuestMemoryMmap, addr: GuestAddress) -> (Vec, 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 struct_size() { + fn entrypoint_struct_size() { assert_eq!( mem::size_of::(), 0x18usize, @@ -295,13 +526,184 @@ mod tests { } #[test] - fn entrypoint_checksum() { + fn smbios_chassis_empty_string_set_has_double_null() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); + let smbios = SmbiosConfig { + chassis: Some(SmbiosChassisConfig::default()), + ..Default::default() + }; - setup_smbios(&mem, None, None, None).unwrap(); + setup_smbios(&mem, Some(&smbios)).unwrap(); let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap(); + let mut cur = GuestAddress(smbios_ep.physptr); + + let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap(); + cur = cur.checked_add(bios.length as u64).unwrap(); + let (_, next) = read_string_set(&mem, cur); + cur = next; + + let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap(); + cur = cur.checked_add(sys.length as u64).unwrap(); + let (_, next) = read_string_set(&mem, cur); + cur = next; + + let chassis: SmbiosChassis = mem.read_obj(cur).unwrap(); + cur = cur.checked_add(chassis.length as u64).unwrap(); + // SMBIOS DSP0134 §6.1.3: empty string-set ends with double NUL. + let b0: u8 = mem.read_obj(cur).unwrap(); + let b1: u8 = mem.read_obj(cur.checked_add(1).unwrap()).unwrap(); + assert_eq!(b0, 0); + assert_eq!(b1, 0); + cur = cur.checked_add(2).unwrap(); + + let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap(); + assert_eq!(end.r#type, END_OF_TABLE); + } - assert_eq!(compute_checksum(&smbios_ep), 0); + #[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), + mem::size_of::(), + )]) + .unwrap(); + + let err = setup_smbios(&mem, None).unwrap_err(); + assert!(matches!(err, Error::WriteData)); } } diff --git a/arch/src/x86_64/tdx/mod.rs b/arch/src/x86_64/tdx/mod.rs index 8a95f1d6d5..53e004c5ba 100644 --- a/arch/src/x86_64/tdx/mod.rs +++ b/arch/src/x86_64/tdx/mod.rs @@ -5,6 +5,7 @@ use std::fs::File; 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}; @@ -108,7 +109,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().to_string(), + entry_uuid.hyphenated(), entry_size ); @@ -162,7 +163,7 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec, bool), // SAFETY: we read exactly the size of the descriptor header file.read_exact(unsafe { std::slice::from_raw_parts_mut( - &mut descriptor as *mut _ as *mut u8, + (&raw mut descriptor).cast(), std::mem::size_of::(), ) }) @@ -189,7 +190,7 @@ pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec, bool), // SAFETY: we read exactly the advertised sections file.read_exact(unsafe { std::slice::from_raw_parts_mut( - sections.as_mut_ptr() as *mut u8, + sections.as_mut_ptr().cast(), descriptor.num_sections as usize * std::mem::size_of::(), ) }) @@ -304,7 +305,7 @@ fn align_hob(v: u64) -> u64 { impl TdHob { fn update_offset(&mut self) { - self.current_offset = align_hob(self.current_offset + std::mem::size_of::() as u64) + self.current_offset = align_hob(self.current_offset + std::mem::size_of::() as u64); } pub fn start(offset: u64) -> TdHob { @@ -519,7 +520,7 @@ impl TdHob { } #[cfg(test)] -mod tests { +mod unit_tests { use super::*; #[test] @@ -528,7 +529,7 @@ mod tests { 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?}"); } } } diff --git a/block/Cargo.toml b/block/Cargo.toml index 02bf37eb03..22ea206de2 100644 --- a/block/Cargo.toml +++ b/block/Cargo.toml @@ -1,7 +1,8 @@ [package] authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"] -edition = "2021" +edition.workspace = true name = "block" +rust-version.workspace = true version = "0.1.0" [features] @@ -9,17 +10,19 @@ default = [] io_uring = ["dep:io-uring"] [dependencies] -byteorder = "1.5.0" +bitflags = { workspace = true } +byteorder = { workspace = true } crc-any = "2.5.0" -io-uring = { version = "0.6.4", optional = true } -libc = "0.2.167" -log = "0.4.22" +flate2 = "1.1" +io-uring = { version = "0.7.12", optional = true } +libc = { workspace = true } +log = { workspace = true } remain = "0.2.15" -serde = { version = "1.0.208", features = ["derive"] } -smallvec = "1.13.2" +serde = { workspace = true, features = ["derive"] } +smallvec = "1.15.1" thiserror = { workspace = true } uuid = { workspace = true, features = ["v4"] } -virtio-bindings = { workspace = true, features = ["virtio-v5_0_0"] } +virtio-bindings = { workspace = true } virtio-queue = { workspace = true } vm-memory = { workspace = true, features = [ "backend-atomic", @@ -28,3 +31,10 @@ vm-memory = { workspace = true, features = [ ] } vm-virtio = { path = "../vm-virtio" } vmm-sys-util = { workspace = true } +zstd = "0.13" + +[dev-dependencies] +cfg-if = { workspace = true } + +[lints] +workspace = true diff --git a/block/src/aligned_operation.rs b/block/src/aligned_operation.rs new file mode 100644 index 0000000000..6096a4f936 --- /dev/null +++ b/block/src/aligned_operation.rs @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Meta Platforms, Inc. and affiliates. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +use std::alloc::{Layout, alloc_zeroed, dealloc}; +use std::io; + +use vm_memory::GuestAddress; + +/// Owns an aligned bounce buffer used when a guest descriptor's host VA +/// does not meet the disk backend's alignment requirement. +#[derive(Debug)] +pub struct AlignedOperation { + data_addr: GuestAddress, + aligned_ptr: *mut u8, + size: usize, + layout: Layout, +} + +impl AlignedOperation { + /// Allocate a zero-initialized buffer of `size` bytes aligned to + /// `alignment`. Returns `InvalidInput` if `size` is zero; + /// `alignment` must be a power of two and not exceed `isize::MAX` + /// after rounding up. + pub fn new(data_addr: GuestAddress, size: usize, alignment: usize) -> io::Result { + if size == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "AlignedOperation requires a non-zero size", + )); + } + let layout = Layout::from_size_align(size, alignment) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + // SAFETY: size is non-zero (checked above) and Layout::from_size_align + // rejects alignments that are not a power of two or that overflow. + let aligned_ptr = unsafe { alloc_zeroed(layout) }; + if aligned_ptr.is_null() { + return Err(io::Error::last_os_error()); + } + Ok(Self { + data_addr, + aligned_ptr, + size, + layout, + }) + } + + /// Gets the raw pointer to the aligned buffer. + pub fn as_mut_ptr(&mut self) -> *mut u8 { + self.aligned_ptr + } + + /// Returns the aligned buffer as a slice. + pub fn as_bytes(&self) -> &[u8] { + // SAFETY: `new` allocates `size` bytes via alloc_zeroed (so they + // are initialized) and AlignedOperation owns the buffer + // exclusively. + unsafe { std::slice::from_raw_parts(self.aligned_ptr, self.size) } + } + + /// Returns the aligned buffer as a mutable slice. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + // SAFETY: same invariant as as_bytes; &mut self rules out other + // simultaneous borrows. + unsafe { std::slice::from_raw_parts_mut(self.aligned_ptr, self.size) } + } + + /// Returns the guest address for this op. + pub fn data_addr(&self) -> GuestAddress { + self.data_addr + } +} + +impl Drop for AlignedOperation { + fn drop(&mut self) { + // SAFETY: `new` is the only constructor, and it stores a pointer + // returned by `alloc_zeroed` paired with the exact `layout` used + // for that allocation. Ownership has not escaped (the type is + // neither `Clone` nor `Copy`). + unsafe { + dealloc(self.aligned_ptr, self.layout); + } + } +} + +// SAFETY: AlignedOperation owns its heap allocation exclusively (no Clone/ +// Copy, no shared aliases) and the allocation's lifetime is tied to the +// value's. Moving an AlignedOperation between threads transfers that +// ownership — the same rationale Box uses for its Send impl. +unsafe impl Send for AlignedOperation {} diff --git a/block/src/async_io.rs b/block/src/async_io.rs index 3f37bd6e34..bbdd77779d 100644 --- a/block/src/async_io.rs +++ b/block/src/async_io.rs @@ -8,7 +8,7 @@ use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use thiserror::Error; use vmm_sys_util::eventfd::EventFd; -use crate::DiskTopology; +use crate::{BatchRequest, SECTOR_SIZE}; #[derive(Error, Debug)] pub enum DiskFileError { @@ -18,14 +18,22 @@ pub enum DiskFileError { /// Failed creating a new AsyncIo. #[error("Failed creating a new AsyncIo")] NewAsyncIo(#[source] std::io::Error), + /// Unsupported operation. + #[error("Unsupported operation")] + Unsupported, + /// Resize failed + #[error("Resize failed")] + ResizeError(#[source] std::io::Error), + #[error("Failed cloning disk file")] + Clone(#[source] std::io::Error), } pub type DiskFileResult = std::result::Result; -/// A wrapper for [`RawFd`] capturing the lifetime of a corresponding [`DiskFile`]. +/// 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 implementations of [`DiskFile`], which wrap the effective [`File`] +/// by some disk implementations, which wrap the effective [`File`] /// in an `Arc>`, making the use of [`BorrowedFd`] impossible. /// /// [`BorrowedFd`]: std::os::fd::BorrowedFd @@ -50,24 +58,6 @@ impl AsRawFd for BorrowedDiskFd<'_> { } } -/// Abstraction over the effective [`File`] backing up a block device, -/// with support for synchronous and asynchronous I/O. -/// -/// This allows abstracting over raw image formats as well as structured -/// image formats. -pub trait DiskFile: Send { - fn size(&mut self) -> DiskFileResult; - fn new_async_io(&self, ring_depth: u32) -> DiskFileResult>; - fn topology(&mut self) -> DiskTopology { - DiskTopology::default() - } - /// Returns the file descriptor of the underlying disk image file. - /// - /// The file descriptor is supposed to be used for `fcntl()` calls but no - /// other operation. - fn fd(&mut self) -> BorrowedDiskFd<'_>; -} - #[derive(Error, Debug)] pub enum AsyncIoError { /// Failed vectored reading from file. @@ -79,6 +69,15 @@ pub enum AsyncIoError { /// Failed synchronizing file. #[error("Failed synchronizing file")] Fsync(#[source] std::io::Error), + /// Failed punching hole. + #[error("Failed punching hole")] + PunchHole(#[source] std::io::Error), + /// Failed writing zeroes. + #[error("Failed writing zeroes")] + WriteZeroes(#[source] std::io::Error), + /// Failed submitting batch requests. + #[error("Failed submitting batch requests")] + SubmitBatchRequests(#[source] std::io::Error), } pub type AsyncIoResult = std::result::Result; @@ -98,5 +97,16 @@ pub trait AsyncIo: Send { user_data: u64, ) -> AsyncIoResult<()>; fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()>; + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>; + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>; fn next_completed_request(&mut self) -> Option<(u64, i32)>; + fn batch_requests_enabled(&self) -> bool { + false + } + fn submit_batch_requests(&mut self, _batch_request: &[BatchRequest]) -> AsyncIoResult<()> { + Ok(()) + } + fn alignment(&self) -> u64 { + SECTOR_SIZE + } } diff --git a/block/src/disk_file.rs b/block/src/disk_file.rs new file mode 100644 index 0000000000..7f044ea7e3 --- /dev/null +++ b/block/src/disk_file.rs @@ -0,0 +1,158 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Composable disk capability traits for the block crate. +//! +//! Small traits define individual capabilities: +//! +//! - [`DiskSize`] - reported capacity (logical size) +//! - [`PhysicalSize`] - host allocation size +//! - [`DiskFd`] - backing file descriptor access +//! - [`Geometry`] - sector/cluster geometry (default 512B) +//! - [`SparseCapable`] - sparse and zero flag support +//! - [`Resizable`] - online resize +//! +//! [`DiskFile`] is a supertrait that bundles the universal capabilities +//! (`DiskSize` + `Geometry`). [`FullDiskFile`] adds all optional +//! capabilities. [`AsyncDiskFile`] extends `DiskFile` with async I/O +//! construction for virtio queue workers. [`AsyncFullDiskFile`] +//! combines both axes. +//! +//! ```text +//! DiskFile: DiskSize + Geometry + Sync +//! / \ +//! FullDiskFile: AsyncDiskFile: +//! DiskFile + PhysicalSize + DiskFile + Unpin +//! DiskFd + SparseCapable + try_clone, create_async_io +//! Resizable +//! \ / +//! AsyncFullDiskFile: FullDiskFile + AsyncDiskFile +//! ``` +//! +//! Readonly accessors take `&self`. Only [`Resizable::resize`] requires +//! `&mut self`. Errors are returned as [`BlockResult`]. + +use std::fmt::Debug; + +use crate::async_io::{AsyncIo, BorrowedDiskFd}; +use crate::{BlockResult, DiskTopology}; + +/// Reported capacity of a disk image. +pub trait DiskSize: Send + Debug { + /// Virtual size of the disk image in bytes (reported capacity). + fn logical_size(&self) -> BlockResult; +} + +/// 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; +} + +/// Backing file descriptor access for disk images backed by a file. +pub trait DiskFd: Send + Debug { + /// Borrows the underlying file descriptor. + fn fd(&self) -> BorrowedDiskFd<'_>; +} + +/// Sector and cluster geometry of a disk image. +/// +/// Default returns `DiskTopology::default()` (512B logical/physical). +pub trait Geometry: Send + Debug { + /// Returns the disk topology. + fn topology(&self) -> DiskTopology { + DiskTopology::default() + } +} + +/// Sparse and zero flag support for thin provisioned disk images. +pub trait SparseCapable: Send + Debug { + /// Indicates support for sparse operations (punch hole, write zeroes, discard). + fn supports_sparse_operations(&self) -> bool { + false + } + + /// Indicates support for a metadata level zero flag optimization in + /// virtio `VIRTIO_BLK_T_WRITE_ZEROES` requests. When true, the format + /// can mark regions as reading zeros via a metadata bit rather than + /// writing actual zero bytes to disk. + fn supports_zero_flag(&self) -> bool { + false + } +} + +/// Live disk resize support. +/// +/// Implementations may return an error if the backend does not +/// support resizing (e.g. fixed size formats). +pub trait Resizable: Send + Debug { + /// Resizes the disk image to the given size in bytes, if the backend supports it. + fn resize(&mut self, size: u64) -> BlockResult<()>; +} + +/// Supertrait bundling universal disk capabilities. +/// +/// Every disk format implements `DiskSize` and `Geometry`. +/// `Sync` is required so that `Arc` can be shared +/// across threads for concurrent readonly access. +pub trait DiskFile: DiskSize + Geometry + Sync {} + +/// Full capability disk file trait. +/// +/// Bundles all optional capabilities on top of [`DiskFile`]: +/// file descriptor access, physical size, sparse operations, and resize. +/// Used by consumers that need feature negotiation without async I/O +/// (e.g. vhost user block). +pub trait FullDiskFile: DiskFile + PhysicalSize + DiskFd + SparseCapable + Resizable {} + +/// Blanket implementation: any type implementing all constituent traits +/// automatically satisfies [`FullDiskFile`]. +impl 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`) 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` (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>; + + /// 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>; +} + +/// 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 AsyncFullDiskFile for T {} diff --git a/block/src/error.rs b/block/src/error.rs new file mode 100644 index 0000000000..645057005e --- /dev/null +++ b/block/src/error.rs @@ -0,0 +1,245 @@ +// 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, + pub offset: Option, + pub op: Option, +} + +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>, + ctx: Option, +} + +impl BlockError { + /// Create a new `BlockError` from a kind and a source error. + pub fn new(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(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) -> 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(&self) -> Option<&T> { + self.source.as_ref()?.downcast_ref::() + } + + /// Consume the error and return the boxed source, if any. + pub fn into_source(self) -> Option> { + 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 for BlockError { + fn from(e: io::Error) -> Self { + Self::new(BlockErrorKind::Io, e) + } +} + +pub type BlockResult = Result; diff --git a/block/src/factory.rs b/block/src/factory.rs new file mode 100644 index 0000000000..ffe65f7d9f --- /dev/null +++ b/block/src/factory.rs @@ -0,0 +1,292 @@ +// 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::fixed_vhd_disk::FixedVhdDisk; +use crate::qcow_disk::QcowDisk; +use crate::raw_disk::{RawBackend, RawDisk}; +use crate::vhdx_sync::VhdxDiskSync; +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, +} + +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 = 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 = 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 { + 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 = match image_type { + ImageType::FixedVhd => open_fixed_vhd(file, options)?, + ImageType::Raw => open_raw(file, options)?, + ImageType::Qcow2 => open_qcow2(file, options)?, + ImageType::Vhdx => open_vhdx(file, options)?, + ImageType::Unknown => { + return Err( + BlockError::from_kind(BlockErrorKind::UnsupportedFeature).with_path(options.path) + ); + } + }; + + Ok(OpenedDisk { image_type, disk }) +} + +fn open_vhdx( + file: fs::File, + options: &DiskOpenOptions<'_>, +) -> BlockResult> { + info!("Opening VHDX disk file with synchronous backend"); + Ok(Box::new( + VhdxDiskSync::new(file).map_err(|e| e.with_path(options.path))?, + )) +} + +fn open_fixed_vhd( + file: fs::File, + options: &DiskOpenOptions<'_>, +) -> BlockResult> { + #[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( + FixedVhdDisk::new(file, true).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( + FixedVhdDisk::new(file, false).map_err(|e| e.with_path(options.path))?, + )) +} + +fn open_raw( + file: fs::File, + options: &DiskOpenOptions<'_>, +) -> BlockResult> { + 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))); + } + 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))); + } + 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))) +} + +fn open_qcow2( + file: fs::File, + options: &DiskOpenOptions<'_>, +) -> BlockResult> { + #[cfg(feature = "io_uring")] + if !options.disable_io_uring { + if io_uring_supported() { + info!("Opening QCOW2 disk file with io_uring backend"); + return Ok(Box::new( + QcowDisk::new( + file, + options.direct, + options.backing_files, + options.sparse, + true, + ) + .map_err(|e| e.with_path(options.path))?, + )); + } + info!("io_uring runtime probe failed for QCOW2, using synchronous backend"); + } + + info!("Opening QCOW2 disk file with synchronous backend"); + Ok(Box::new( + QcowDisk::new( + file, + options.direct, + options.backing_files, + options.sparse, + false, + ) + .map_err(|e| e.with_path(options.path))?, + )) +} + +#[cfg(test)] +mod unit_tests { + use std::io::Write; + use std::path::Path; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::qcow::{QcowFile, RawFile}; + + 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 = TempFile::new().unwrap(); + { + let raw = RawFile::new(tmp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, 100 * 1024 * 1024, true).unwrap(); + qcow.flush().unwrap(); + } + let 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); + } +} diff --git a/block/src/fcntl.rs b/block/src/fcntl.rs index 2e34de1d6a..f5cb626c00 100644 --- a/block/src/fcntl.rs +++ b/block/src/fcntl.rs @@ -16,6 +16,7 @@ use std::fmt::Debug; use std::io; use std::os::fd::{AsRawFd, RawFd}; +use std::str::FromStr; use thiserror::Error; @@ -23,8 +24,6 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum LockError { /// The file is already locked. - /// - /// A call to [`get_lock_state`] can help to identify the reason. #[error("The file is already locked")] AlreadyLocked, /// IO error. @@ -42,12 +41,36 @@ enum FcntlArg<'a> { } /// Wrapper for [`libc::fcntl`] that properly sets the function arguments. -fn fcntl(fd: RawFd, arg: FcntlArg) -> libc::c_int { - // SAFETY: We use a valid FD. - unsafe { - match arg { - FcntlArg::F_OFD_SETLK(flock) => libc::fcntl(fd, libc::F_OFD_SETLK, flock), - FcntlArg::F_OFD_GETLK(flock) => libc::fcntl(fd, libc::F_OFD_GETLK, flock), +fn fcntl(fd: RawFd, mut arg: FcntlArg) -> Result<(), LockError> { + loop { + // SAFETY: + // - `F_OFD_SETLK` and `F_OFD_GETLK` fcntl calls handle invalid file descriptors. + // - `F_OFD_SETLK` does not modify `flock`. + // - `F_OFD_GETLK` uses a mutable pointer to `flock`. + let result = unsafe { + match &mut arg { + FcntlArg::F_OFD_SETLK(flock) => { + libc::fcntl(fd, libc::F_OFD_SETLK, *flock as *const libc::flock) + } + FcntlArg::F_OFD_GETLK(flock) => { + libc::fcntl(fd, libc::F_OFD_GETLK, *flock as *mut libc::flock) + } + } + }; + match result { + 0 => return Ok(()), + -1 => { + let io_error = io::Error::last_os_error(); + let errno = io_error.raw_os_error().unwrap(); + match errno { + // See man page for error code: + // + libc::EAGAIN | libc::EACCES => return Err(LockError::AlreadyLocked), + libc::EINTR => continue, + _ => return Err(LockError::Io(io_error)), + } + } + val => panic!("Unexpected return value from fcntl(): {val}"), } } } @@ -73,101 +96,276 @@ impl LockType { } } -/// Describes the current state of a lock. -#[derive(Debug)] -pub enum LockState { - /// No lock set. - Unlocked, - /// Locked for reading (non-exclusive). - SharedRead, - /// Locked for writing (exclusive mode). - ExclusiveWrite, +/// Amount of bytes by which the first lock is offset from the start of the file. +const QEMU_LOCK_OFFSET: u64 = 100; +/// Amount of bytes by which the first unshared lock is offset from the start of the file. +/// +/// Unsharing is equivalent to marking lock as exclusive. +/// +/// # Example +/// +/// Setting `QEMU_LOCK_OFFSET` + `QEMU_READ_BYTE` indicates a reader lock that may be shared with +/// other readers. +/// Setting `QEMU_UNSHARE_LOCK_OFFSET` + `QEMU_READ_BYTE` additionally indicates, that the reader +/// lock is "unshared" (exclusive) and may not be shared with others. +const QEMU_UNSHARE_LOCK_OFFSET: u64 = 200; + +/// Read permission lock index for QEMU. +const QEMU_READ_BYTE: u64 = 0; +/// Write permission lock index for QEMU. +const QEMU_WRITE_BYTE: u64 = 1; + +/// The granularity of the advisory lock. +/// +/// The granularity has significant implications in typical cloud deployments +/// with network storage. The Linux kernel will sync advisory locks to network +/// file systems, but these backends may have different policies and handle +/// locks differently. For example, Netapp speaks a NFS API but will treat +/// advisory OFD locks for the whole file as mandatory locks, whereas byte-range +/// locks for the whole file will remain advisory [0]. +/// +/// As it is a valid use case to prevent multiple CHV instances from accessing +/// the same disk but disk management software (e.g., Cinder in OpenStack) +/// should be able to snapshot disks while VMs are running, we need special +/// control over the lock granularity. Therefore, it is a valid use case to lock +/// the whole byte range of a disk image without technically locking the whole +/// file - to get the best of both worlds. +/// +/// [0] https://kb.netapp.com/on-prem/ontap/da/NAS/NAS-KBs/How_is_Mandatory_Locking_supported_for_NFSv4_on_ONTAP_9 +#[derive(Clone, Copy, Debug)] +pub enum LockGranularity { + WholeFile, + ByteRange(u64 /* from, inclusive */, u64 /* len */), + QemuCompatible, } -impl LockState { - fn new(value: libc::c_int) -> Self { - const F_UNLCK: libc::c_int = libc::F_UNLCK as libc::c_int; - const F_WRLCK: libc::c_int = libc::F_WRLCK as libc::c_int; - const F_RDLCK: libc::c_int = libc::F_RDLCK as libc::c_int; - match value { - F_UNLCK => Self::Unlocked, - F_WRLCK => Self::ExclusiveWrite, - F_RDLCK => Self::SharedRead, - // This is so unlikely that we want to avoid the complexity of - // coping with this error case. Can only fail if either Linux - // is broken or memory is messed up. - other => panic!("Unexpected lock state: {other}"), +impl LockGranularity { + const fn l_len(self) -> u64 { + match self { + LockGranularity::WholeFile => 0, /* EOF */ + LockGranularity::ByteRange(_, len) => len, + // QEMU uses multiple one byte long locks. + LockGranularity::QemuCompatible => 1, } } -} -/// Returns a [`struct@libc::flock`] structure for the whole file. -const fn get_flock(lock_type: LockType) -> libc::flock { - libc::flock { - l_type: lock_type.to_libc_val() as libc::c_short, - l_whence: libc::SEEK_SET as libc::c_short, - l_start: 0, - l_len: 0, /* EOF */ - l_pid: 0, /* filled by callee */ + /// Internal implementation of [`Self::try_acquire_lock`] for [`LockGranularity::WholeFile`] and + /// [`LockGranularity::ByteRange`]. + fn try_acquire_lock_file( + self, + file: &Fd, + lock_type: LockType, + l_start: u64, + ) -> Result<(), LockError> { + let flock = self.flock(lock_type.to_libc_val(), l_start); + + fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock)) } -} -/// Tries to acquire a lock using [`fcntl`] with respect to the given -/// parameters. -/// -/// Please note that `fcntl()` OFD locks are **advisory locks**, which do not -/// prevent to `open()` a file if a lock is already placed. -/// -/// # Parameters -/// - `file`: The file to acquire a lock for [`LockType`]. The file's state will -/// be logically mutated, but not technically. -/// - `lock_type`: The [`LockType`] -pub fn try_acquire_lock(file: Fd, lock_type: LockType) -> Result<(), LockError> { - let flock = get_flock(lock_type); - - let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock)); - match res { - 0 => Ok(()), - -1 => { - let io_error = io::Error::last_os_error(); - let errno = io_error.raw_os_error().unwrap(); - match errno { - // See man page for error code: - // - libc::EAGAIN | libc::EACCES => Err(LockError::AlreadyLocked), - _ => Err(LockError::Io(io_error)), + /// Releases all locks not required for `lock_type`. + /// + /// Used to roll back a lock acquisition attempt to a previously acquired lock. + fn release_unneeded_locks_qemu( + self, + file: &Fd, + lock_type: LockType, + ) -> Result<(), LockError> { + let flocks = match lock_type { + LockType::Unlock => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_LOCK_OFFSET + QEMU_WRITE_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + LockType::Write => vec![], + LockType::Read => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + }; + + let mut first_error = None; + for flock in flocks { + if let Err(error) = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock)) { + first_error.get_or_insert(error); + } + } + if let Some(first_error) = first_error { + return Err(first_error); + } + Ok(()) + } + + /// Internal implementation of [`Self::try_acquire_lock`] for [`LockGranularity::QemuCompatible`]. + fn try_acquire_lock_qemu( + self, + file: &Fd, + lock_type: LockType, + current_lock_status: LockType, + ) -> Result<(), LockError> { + let flocks = match lock_type { + LockType::Unlock => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_LOCK_OFFSET + QEMU_WRITE_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_UNLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + LockType::Write => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_RDLCK, QEMU_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_RDLCK, QEMU_LOCK_OFFSET + QEMU_WRITE_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_RDLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + LockType::Read => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_RDLCK, QEMU_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_RDLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + }; + + for flock in flocks { + if let Err(error) = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock)) { + if let LockType::Unlock = lock_type { + return Err(error); + } + let _ = self.release_unneeded_locks_qemu(file, current_lock_status); + return Err(error); + } + } + + if let Err(error) = self.check_lock_success_qemu(file, lock_type) { + let _ = self.release_unneeded_locks_qemu(file, current_lock_status); + return Err(error); + } + Ok(()) + } + + /// Tries to acquire a lock using [`fcntl`] with respect to the given + /// parameters. + /// + /// Please note that `fcntl()` OFD locks are **advisory locks**, which do not + /// prevent to `open()` a file if a lock is already placed. + /// + /// # Parameters + /// - `file`: The file to acquire a lock for [`LockType`]. The file's state will + /// be logically mutated, but not technically. + /// - `lock_type`: The [`LockType`] + /// - `current_lock_status`: Already held locks on this `file`. + /// Used for [`LockGranularity::QemuCompatible`] to roll back to if locking fails. + pub fn try_acquire_lock( + self, + file: &Fd, + lock_type: LockType, + current_lock_status: LockType, + ) -> Result<(), LockError> { + match self { + LockGranularity::WholeFile => self.try_acquire_lock_file(file, lock_type, 0), + LockGranularity::ByteRange(start, _) => { + self.try_acquire_lock_file(file, lock_type, start) + } + LockGranularity::QemuCompatible => { + self.try_acquire_lock_qemu(file, lock_type, current_lock_status) } } - val => panic!("Unexpected return value from fcntl(): {val}"), + } + + /// Clears a lock. + /// + /// # Parameters + /// - `file`: The file to clear all locks for [`LockType`]. + pub fn clear_lock(self, file: &Fd) -> Result<(), LockError> { + self.try_acquire_lock(file, LockType::Unlock, LockType::Unlock) + } + + /// Checks whether any conflicting locks are set. + /// + /// Returns an error if a conflicting lock is set. + fn check_lock_success_qemu( + &self, + file: &Fd, + lock_type: LockType, + ) -> Result<(), LockError> { + let flocks = match lock_type { + LockType::Unlock => vec![], + LockType::Write => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_WRLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_WRLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_WRITE_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_WRLCK, QEMU_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + LockType::Read => vec![ + LockGranularity::QemuCompatible + .flock(libc::F_WRLCK, QEMU_UNSHARE_LOCK_OFFSET + QEMU_READ_BYTE), + LockGranularity::QemuCompatible + .flock(libc::F_WRLCK, QEMU_LOCK_OFFSET + QEMU_WRITE_BYTE), + ], + }; + + for mut flock in flocks { + fcntl(file.as_raw_fd(), FcntlArg::F_OFD_GETLK(&mut flock))?; + + if flock.l_type as libc::c_int != libc::F_UNLCK { + return Err(LockError::AlreadyLocked); + } + } + Ok(()) + } + + /// Returns a [`struct@libc::flock`] structure. + const fn flock(self, lock_type: libc::c_int, l_start: u64) -> libc::flock { + libc::flock { + l_type: lock_type as libc::c_short, + l_whence: libc::SEEK_SET as libc::c_short, + l_start: l_start as libc::c_long, + l_len: self.l_len() as libc::c_long, + l_pid: 0, /* filled by callee */ + } } } -/// Clears a lock. +/// User-facing choice for the lock granularity. /// -/// # Parameters -/// - `file`: The file to clear all locks for [`LockType`]. -pub fn clear_lock(file: Fd) -> Result<(), LockError> { - try_acquire_lock(file, LockType::Unlock) +/// This allows external management software to create snapshots of the disk +/// image. Without a byte-range lock, some NFS implementations may treat the +/// entire file as exclusively locked and prevent such operations (e.g. NetApp). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub enum LockGranularityChoice { + /// Byte-range lock covering [0, size). + ByteRange, + /// Whole-file lock (l_start=0, l_len=0) - original OFD whole-file lock behavior. + Full, + /// Locking scheme that mimics QEMU's marker byte based locking scheme. + #[default] + QemuCompatible, } -/// Returns the current lock state using [`fcntl`] with respect to the given -/// parameters. -/// -/// # Parameters -/// - `file`: The file for which to get the lock state. -pub fn get_lock_state(file: Fd) -> Result { - let mut flock = get_flock(LockType::Write); - let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_GETLK(&mut flock)); - match res { - 0 => { - let state = flock.l_type as libc::c_int; - let state = LockState::new(state); - Ok(state) - } - -1 => { - let io_error = io::Error::last_os_error(); - Err(LockError::Io(io_error)) +/// Error returned when parsing a [`LockGranularityChoice`] from a string. +#[derive(Error, Debug)] +#[error("Invalid lock granularity value: {0}, expected 'byte-range', 'full' or 'qemu-compatible'")] +pub struct LockGranularityParseError(String); + +impl FromStr for LockGranularityChoice { + type Err = LockGranularityParseError; + + fn from_str(s: &str) -> Result { + match s { + "byte-range" => Ok(LockGranularityChoice::ByteRange), + "full" => Ok(LockGranularityChoice::Full), + "qemu-compatible" => Ok(Self::QemuCompatible), + _ => Err(LockGranularityParseError(s.to_owned())), } - val => panic!("Unexpected return value from fcntl(): {val}"), } } diff --git a/block/src/fixed_vhd.rs b/block/src/fixed_vhd.rs index 22ef4dd80d..aa9bd95303 100644 --- a/block/src/fixed_vhd.rs +++ b/block/src/fixed_vhd.rs @@ -6,8 +6,8 @@ use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; use std::os::unix::io::{AsRawFd, RawFd}; -use crate::vhd::VhdFooter; use crate::BlockBackend; +use crate::vhd::VhdFooter; #[derive(Debug)] pub struct FixedVhd { @@ -75,9 +75,17 @@ impl Seek for FixedVhd { } impl BlockBackend for FixedVhd { - fn size(&self) -> std::result::Result { + fn logical_size(&self) -> Result { Ok(self.size) } + + /// Returns the physical size of the underlying file. + fn physical_size(&self) -> Result { + self.file + .metadata() + .map(|m| m.len()) + .map_err(crate::Error::GetFileMetadata) + } } impl Clone for FixedVhd { diff --git a/block/src/fixed_vhd_async.rs b/block/src/fixed_vhd_async.rs index 6b51d070f8..58dbc9a93c 100644 --- a/block/src/fixed_vhd_async.rs +++ b/block/src/fixed_vhd_async.rs @@ -2,42 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 -use std::fs::File; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::RawFd; use vmm_sys_util::eventfd::EventFd; -use crate::async_io::{ - AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, -}; -use crate::fixed_vhd::FixedVhd; +use crate::BatchRequest; +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; +use crate::error::BlockResult; use crate::raw_async::RawFileAsync; -use crate::BlockBackend; - -pub struct FixedVhdDiskAsync(FixedVhd); - -impl FixedVhdDiskAsync { - pub fn new(file: File) -> std::io::Result { - Ok(Self(FixedVhd::new(file)?)) - } -} - -impl DiskFile for FixedVhdDiskAsync { - fn size(&mut self) -> DiskFileResult { - Ok(self.0.size().unwrap()) - } - - fn new_async_io(&self, ring_depth: u32) -> DiskFileResult> { - Ok(Box::new( - FixedVhdAsync::new(self.0.as_raw_fd(), ring_depth, self.0.size().unwrap()) - .map_err(DiskFileError::NewAsyncIo)?, - ) as Box) - } - - fn fd(&mut self) -> BorrowedDiskFd<'_> { - BorrowedDiskFd::new(self.0.as_raw_fd()) - } -} pub struct FixedVhdAsync { raw_file_async: RawFileAsync, @@ -45,7 +17,7 @@ pub struct FixedVhdAsync { } impl FixedVhdAsync { - pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> std::io::Result { + pub fn new(fd: RawFd, ring_depth: u32, size: u64) -> BlockResult { let raw_file_async = RawFileAsync::new(fd, ring_depth)?; Ok(FixedVhdAsync { @@ -106,4 +78,24 @@ impl AsyncIo for FixedVhdAsync { fn next_completed_request(&mut self) -> Option<(u64, i32)> { self.raw_file_async.next_completed_request() } + + fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::PunchHole(std::io::Error::other( + "punch_hole not supported for fixed VHD", + ))) + } + + fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::WriteZeroes(std::io::Error::other( + "write_zeroes not supported for fixed VHD", + ))) + } + + fn batch_requests_enabled(&self) -> bool { + true + } + + fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> { + self.raw_file_async.submit_batch_requests(batch_request) + } } diff --git a/block/src/fixed_vhd_disk.rs b/block/src/fixed_vhd_disk.rs new file mode 100644 index 0000000000..8a27cdf963 --- /dev/null +++ b/block/src/fixed_vhd_disk.rs @@ -0,0 +1,222 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::io; +use std::os::unix::io::AsRawFd; + +use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; +use crate::disk_file::DiskSize; +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::fixed_vhd::FixedVhd; +#[cfg(feature = "io_uring")] +use crate::fixed_vhd_async::FixedVhdAsync; +use crate::fixed_vhd_sync::FixedVhdSync; +use crate::{BlockBackend, Error, disk_file}; + +#[derive(Debug)] +pub struct FixedVhdDisk { + inner: FixedVhd, + use_io_uring: bool, +} + +impl FixedVhdDisk { + pub fn new(file: File, use_io_uring: bool) -> BlockResult { + #[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, + }) + } +} + +impl disk_file::DiskSize for FixedVhdDisk { + fn logical_size(&self) -> BlockResult { + self.inner + .logical_size() + .map_err(|e| BlockError::new(BlockErrorKind::Io, e)) + } +} + +impl disk_file::PhysicalSize for FixedVhdDisk { + fn physical_size(&self) -> BlockResult { + 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 FixedVhdDisk { + fn fd(&self) -> BorrowedDiskFd<'_> { + BorrowedDiskFd::new(self.inner.as_raw_fd()) + } +} + +impl disk_file::Geometry for FixedVhdDisk {} + +impl disk_file::SparseCapable for FixedVhdDisk {} + +impl disk_file::Resizable for FixedVhdDisk { + fn resize(&mut self, _size: u64) -> BlockResult<()> { + Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + DiskFileError::ResizeError(io::Error::other("resize not supported for fixed VHD")), + ) + .with_op(ErrorOp::Resize)) + } +} + +impl disk_file::DiskFile for FixedVhdDisk {} + +impl disk_file::AsyncDiskFile for FixedVhdDisk { + fn try_clone(&self) -> BlockResult> { + Ok(Box::new(FixedVhdDisk { + inner: self.inner.clone(), + use_io_uring: self.use_io_uring, + })) + } + + fn create_async_io(&self, ring_depth: u32) -> BlockResult> { + let size = self.logical_size()?; + + if self.use_io_uring { + #[cfg(feature = "io_uring")] + { + return Ok(Box::new(FixedVhdAsync::new( + self.inner.as_raw_fd(), + 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(self.inner.as_raw_fd(), size).map_err(|e| { + BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e)) + .with_op(ErrorOp::Open) + })?, + )) + } +} + +#[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; + 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 = FixedVhdDisk::new(file, 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 = disk.create_async_io(128).unwrap(); + assert_eq!(io.batch_requests_enabled(), expect_batch); + } + + fn assert_async_io(disk: &FixedVhdDisk, expect_batch: bool) { + assert_async_io_from_dyn(disk, expect_batch); + } + + #[test] + fn sync_backend_disables_batch_requests() { + let file = make_vhd_file(); + let disk = FixedVhdDisk::new(file, false).unwrap(); + assert_async_io(&disk, false); + } + + #[cfg(feature = "io_uring")] + #[test] + fn io_uring_backend_enables_batch_requests() { + let file = make_vhd_file(); + let disk = FixedVhdDisk::new(file, true).unwrap(); + assert_async_io(&disk, true); + } + + #[test] + fn try_clone_preserves_sync_dispatch() { + let file = make_vhd_file(); + let disk = FixedVhdDisk::new(file, 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 = FixedVhdDisk::new(file, true).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 = FixedVhdDisk::new(file, false).unwrap(); + assert!(disk.resize(0x2000_0000).is_err()); + } + + #[test] + fn physical_size_includes_footer() { + let file = make_vhd_file(); + let disk = FixedVhdDisk::new(file, false).unwrap(); + // Data region (0x1122_3344) + VHD footer (0x200). + assert_eq!(disk.physical_size().unwrap(), 0x1122_3344 + 0x200); + } +} diff --git a/block/src/fixed_vhd_sync.rs b/block/src/fixed_vhd_sync.rs index b1f2118f19..bcf16f4f5b 100644 --- a/block/src/fixed_vhd_sync.rs +++ b/block/src/fixed_vhd_sync.rs @@ -2,42 +2,12 @@ // // SPDX-License-Identifier: Apache-2.0 -use std::fs::File; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::RawFd; use vmm_sys_util::eventfd::EventFd; -use crate::async_io::{ - AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, -}; -use crate::fixed_vhd::FixedVhd; +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; use crate::raw_sync::RawFileSync; -use crate::BlockBackend; - -pub struct FixedVhdDiskSync(FixedVhd); - -impl FixedVhdDiskSync { - pub fn new(file: File) -> std::io::Result { - Ok(Self(FixedVhd::new(file)?)) - } -} - -impl DiskFile for FixedVhdDiskSync { - fn size(&mut self) -> DiskFileResult { - Ok(self.0.size().unwrap()) - } - - fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult> { - Ok(Box::new( - FixedVhdSync::new(self.0.as_raw_fd(), self.0.size().unwrap()) - .map_err(DiskFileError::NewAsyncIo)?, - ) as Box) - } - - fn fd(&mut self) -> BorrowedDiskFd<'_> { - BorrowedDiskFd::new(self.0.as_raw_fd()) - } -} pub struct FixedVhdSync { raw_file_sync: RawFileSync, @@ -103,4 +73,16 @@ impl AsyncIo for FixedVhdSync { fn next_completed_request(&mut self) -> Option<(u64, i32)> { self.raw_file_sync.next_completed_request() } + + fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::PunchHole(std::io::Error::other( + "punch_hole not supported for fixed VHD", + ))) + } + + fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::WriteZeroes(std::io::Error::other( + "write_zeroes not supported for fixed VHD", + ))) + } } diff --git a/block/src/lib.rs b/block/src/lib.rs index 1424848ba3..9d688f5ff4 100644 --- a/block/src/lib.rs +++ b/block/src/lib.rs @@ -8,63 +8,70 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -#[macro_use] -extern crate log; - +mod aligned_operation; pub mod async_io; +pub mod disk_file; +pub mod error; +pub mod factory; pub mod fcntl; pub mod fixed_vhd; #[cfg(feature = "io_uring")] /// Enabled with the `"io_uring"` feature pub mod fixed_vhd_async; +pub mod fixed_vhd_disk; pub mod fixed_vhd_sync; pub mod qcow; -pub mod qcow_sync; #[cfg(feature = "io_uring")] -/// Async primitives based on `io-uring` -/// -/// Enabled with the `"io_uring"` feature -pub mod raw_async; -pub mod raw_async_aio; -pub mod raw_sync; +pub(crate) mod qcow_async; +pub(crate) mod qcow_common; +pub mod qcow_disk; +pub(crate) mod qcow_sync; +#[cfg(feature = "io_uring")] +pub(crate) mod raw_async; +pub(crate) mod raw_async_aio; +#[cfg(test)] +mod raw_async_io_tests; +pub mod raw_disk; +pub(crate) mod raw_sync; +mod request; pub mod vhd; pub mod vhdx; pub mod vhdx_sync; -use std::alloc::{alloc_zeroed, dealloc, Layout}; +use std::alloc::{Layout, alloc_zeroed}; use std::collections::VecDeque; -use std::fmt::Debug; -use std::fs::File; +use std::fmt::{self, Debug}; +use std::fs::{File, OpenOptions}; use std::io::{self, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write}; use std::os::linux::fs::MetadataExt; +use std::os::unix::fs::FileTypeExt; use std::os::unix::io::AsRawFd; use std::path::Path; -use std::sync::{Arc, MutexGuard}; -use std::time::Instant; -use std::{cmp, result}; +use std::str::FromStr; +use std::{cmp, mem, result}; +pub use aligned_operation::AlignedOperation; #[cfg(feature = "io_uring")] -use io_uring::{opcode, IoUring, Probe}; -use libc::{ioctl, S_IFBLK, S_IFMT}; +use io_uring::{IoUring, Probe, opcode}; +use libc::{ + FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE, S_IFBLK, S_IFMT, ioctl, +}; +use log::{debug, info, warn}; +pub use request::{BatchRequest, ExecuteAsync, MAX_DISCARD_WRITE_ZEROES_SEG, Request, RequestType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use thiserror::Error; use virtio_bindings::virtio_blk::*; -use virtio_queue::DescriptorChain; use vm_memory::bitmap::Bitmap; -use vm_memory::{ - ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryLoadGuard, -}; -use vm_virtio::{AccessPlatform, Translatable}; +use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError}; use vmm_sys_util::eventfd::EventFd; -use vmm_sys_util::{aio, ioctl_io_nr, ioctl_ioc_nr}; +use vmm_sys_util::{aio, ioctl_io_nr, ioctl_ior_nr}; -use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; +use crate::async_io::{AsyncIoError, AsyncIoResult}; +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::request::{DEFAULT_DESCRIPTOR_VEC_SIZE, SECTOR_SIZE}; use crate::vhdx::VhdxError; -const SECTOR_SHIFT: u8 = 9; -pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT; - #[derive(Error, Debug)] pub enum Error { #[error("Guest gave us bad memory addresses")] @@ -83,8 +90,8 @@ pub enum Error { DetectImageType(#[source] std::io::Error), #[error("Failure in fixed vhd")] FixedVhdError(#[source] std::io::Error), - #[error("Getting a block's metadata fails for any reason")] - GetFileMetadata, + #[error("Getting a block's metadata failed")] + GetFileMetadata(#[source] std::io::Error), #[error("The requested operation would cause a seek beyond disk end")] InvalidOffset, #[error("Failure in qcow")] @@ -93,13 +100,15 @@ pub enum Error { RawFileError(#[source] std::io::Error), #[error("The requested operation does not support multiple descriptors")] TooManyDescriptors, + #[error("Request contains too many segments ({0}, max {MAX_DISCARD_WRITE_ZEROES_SEG})")] + TooManySegments(u32), #[error("Failure in vhdx")] VhdxError(#[source] VhdxError), } fn build_device_id(disk_path: &Path) -> result::Result { let blk_metadata = match disk_path.metadata() { - Err(_) => return Err(Error::GetFileMetadata), + Err(e) => return Err(Error::GetFileMetadata(e)), Ok(m) => m, }; // This is how kvmtool does it. @@ -123,7 +132,7 @@ pub fn build_serial(disk_path: &Path) -> Vec { // This will also zero out any leftover bytes. let disk_id = m.as_bytes(); let bytes_to_copy = cmp::min(disk_id.len(), VIRTIO_BLK_ID_BYTES as usize); - default_serial[..bytes_to_copy].clone_from_slice(&disk_id[..bytes_to_copy]) + default_serial[..bytes_to_copy].clone_from_slice(&disk_id[..bytes_to_copy]); } } default_serial @@ -139,6 +148,8 @@ pub enum ExecuteError { Read(#[source] GuestMemoryError), #[error("Failed to read_exact")] ReadExact(#[source] io::Error), + #[error("Can't execute an operation other than `read` or `get_id` on a read-only device")] + ReadOnly, #[error("Failed to seek")] Seek(#[source] io::Error), #[error("Failed to write")] @@ -147,6 +158,8 @@ pub enum ExecuteError { WriteAll(#[source] io::Error), #[error("Unsupported request: {0}")] Unsupported(u32), + #[error("Unsupported flags {flags:#x} for request type {request_type}")] + UnsupportedFlags { request_type: u32, flags: u32 }, #[error("Failed to submit io uring")] SubmitIoUring(#[source] io::Error), #[error("Failed to get guest address")] @@ -157,6 +170,10 @@ pub enum ExecuteError { AsyncWrite(#[source] AsyncIoError), #[error("failed to async flush")] AsyncFlush(#[source] AsyncIoError), + #[error("Failed to async punch hole")] + AsyncPunchHole(#[source] AsyncIoError), + #[error("Failed to async write zeroes")] + AsyncWriteZeroes(#[source] AsyncIoError), #[error("Failed allocating a temporary buffer")] TemporaryBufferAllocation(#[source] io::Error), } @@ -168,30 +185,25 @@ impl ExecuteError { ExecuteError::Flush(_) => VIRTIO_BLK_S_IOERR, ExecuteError::Read(_) => VIRTIO_BLK_S_IOERR, ExecuteError::ReadExact(_) => VIRTIO_BLK_S_IOERR, + ExecuteError::ReadOnly => VIRTIO_BLK_S_IOERR, ExecuteError::Seek(_) => VIRTIO_BLK_S_IOERR, ExecuteError::Write(_) => VIRTIO_BLK_S_IOERR, ExecuteError::WriteAll(_) => VIRTIO_BLK_S_IOERR, ExecuteError::Unsupported(_) => VIRTIO_BLK_S_UNSUPP, + ExecuteError::UnsupportedFlags { .. } => VIRTIO_BLK_S_UNSUPP, ExecuteError::SubmitIoUring(_) => VIRTIO_BLK_S_IOERR, ExecuteError::GetHostAddress(_) => VIRTIO_BLK_S_IOERR, ExecuteError::AsyncRead(_) => VIRTIO_BLK_S_IOERR, ExecuteError::AsyncWrite(_) => VIRTIO_BLK_S_IOERR, ExecuteError::AsyncFlush(_) => VIRTIO_BLK_S_IOERR, + ExecuteError::AsyncPunchHole(_) => VIRTIO_BLK_S_IOERR, + ExecuteError::AsyncWriteZeroes(_) => VIRTIO_BLK_S_IOERR, ExecuteError::TemporaryBufferAllocation(_) => VIRTIO_BLK_S_IOERR, }; status as u8 } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RequestType { - In, - Out, - Flush, - GetDeviceId, - Unsupported(u32), -} - pub fn request_type( mem: &vm_memory::GuestMemoryMmap, desc_addr: GuestAddress, @@ -202,6 +214,8 @@ pub fn request_type( VIRTIO_BLK_T_OUT => Ok(RequestType::Out), VIRTIO_BLK_T_FLUSH => Ok(RequestType::Flush), VIRTIO_BLK_T_GET_ID => Ok(RequestType::GetDeviceId), + VIRTIO_BLK_T_DISCARD => Ok(RequestType::Discard), + VIRTIO_BLK_T_WRITE_ZEROES => Ok(RequestType::WriteZeroes), t => Ok(RequestType::Unsupported(t)), } } @@ -219,334 +233,6 @@ fn sector( mem.read_obj(addr).map_err(Error::GuestMemory) } -const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32; - -#[derive(Debug)] -pub struct AlignedOperation { - origin_ptr: u64, - aligned_ptr: u64, - size: usize, - layout: Layout, -} - -#[derive(Debug)] -pub struct Request { - pub request_type: RequestType, - pub sector: u64, - pub data_descriptors: SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]>, - pub status_addr: GuestAddress, - pub writeback: bool, - pub aligned_operations: SmallVec<[AlignedOperation; DEFAULT_DESCRIPTOR_VEC_SIZE]>, - pub start: Instant, -} - -impl Request { - pub fn parse( - desc_chain: &mut DescriptorChain>>, - access_platform: Option<&Arc>, - ) -> result::Result { - let hdr_desc = desc_chain - .next() - .ok_or(Error::DescriptorChainTooShort) - .inspect_err(|_| { - error!("Missing head descriptor"); - })?; - - // The head contains the request type which MUST be readable. - if hdr_desc.is_write_only() { - return Err(Error::UnexpectedWriteOnlyDescriptor); - } - - let hdr_desc_addr = hdr_desc - .addr() - .translate_gva(access_platform, hdr_desc.len() as usize); - - let mut req = Request { - request_type: request_type(desc_chain.memory(), hdr_desc_addr)?, - sector: sector(desc_chain.memory(), hdr_desc_addr)?, - data_descriptors: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE), - status_addr: GuestAddress(0), - writeback: true, - aligned_operations: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE), - start: Instant::now(), - }; - - let status_desc; - let mut desc = desc_chain - .next() - .ok_or(Error::DescriptorChainTooShort) - .inspect_err(|_| { - error!("Only head descriptor present: request = {:?}", req); - })?; - - if !desc.has_next() { - status_desc = desc; - // Only flush requests are allowed to skip the data descriptor. - if req.request_type != RequestType::Flush { - error!("Need a data descriptor: request = {:?}", req); - return Err(Error::DescriptorChainTooShort); - } - } else { - req.data_descriptors.reserve_exact(1); - while desc.has_next() { - if desc.is_write_only() && req.request_type == RequestType::Out { - return Err(Error::UnexpectedWriteOnlyDescriptor); - } - if !desc.is_write_only() && req.request_type == RequestType::In { - return Err(Error::UnexpectedReadOnlyDescriptor); - } - if !desc.is_write_only() && req.request_type == RequestType::GetDeviceId { - return Err(Error::UnexpectedReadOnlyDescriptor); - } - - req.data_descriptors.push(( - desc.addr() - .translate_gva(access_platform, desc.len() as usize), - desc.len(), - )); - desc = desc_chain - .next() - .ok_or(Error::DescriptorChainTooShort) - .inspect_err(|_| { - error!("DescriptorChain corrupted: request = {:?}", req); - })?; - } - status_desc = desc; - } - - // The status MUST always be writable. - if !status_desc.is_write_only() { - return Err(Error::UnexpectedReadOnlyDescriptor); - } - - if status_desc.len() < 1 { - return Err(Error::DescriptorLengthTooSmall); - } - - req.status_addr = status_desc - .addr() - .translate_gva(access_platform, status_desc.len() as usize); - - Ok(req) - } - - pub fn execute( - &self, - disk: &mut T, - disk_nsectors: u64, - mem: &vm_memory::GuestMemoryMmap, - serial: &[u8], - ) -> result::Result { - disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT)) - .map_err(ExecuteError::Seek)?; - let mut len = 0; - for (data_addr, data_len) in &self.data_descriptors { - let mut top: u64 = u64::from(*data_len) / SECTOR_SIZE; - if u64::from(*data_len) % SECTOR_SIZE != 0 { - top += 1; - } - top = top - .checked_add(self.sector) - .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; - if top > disk_nsectors { - return Err(ExecuteError::BadRequest(Error::InvalidOffset)); - } - - match self.request_type { - RequestType::In => { - let mut buf = vec![0u8; *data_len as usize]; - disk.read_exact(&mut buf).map_err(ExecuteError::ReadExact)?; - mem.read_exact_volatile_from( - *data_addr, - &mut buf.as_slice(), - *data_len as usize, - ) - .map_err(ExecuteError::Read)?; - len += data_len; - } - RequestType::Out => { - let mut buf: Vec = Vec::new(); - mem.write_all_volatile_to(*data_addr, &mut buf, *data_len as usize) - .map_err(ExecuteError::Write)?; - disk.write_all(&buf).map_err(ExecuteError::WriteAll)?; - if !self.writeback { - disk.flush().map_err(ExecuteError::Flush)?; - } - } - RequestType::Flush => disk.flush().map_err(ExecuteError::Flush)?, - RequestType::GetDeviceId => { - if (*data_len as usize) < serial.len() { - return Err(ExecuteError::BadRequest(Error::InvalidOffset)); - } - mem.write_slice(serial, *data_addr) - .map_err(ExecuteError::Write)?; - } - RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)), - }; - } - Ok(len) - } - - pub fn execute_async( - &mut self, - mem: &vm_memory::GuestMemoryMmap, - disk_nsectors: u64, - disk_image: &mut dyn AsyncIo, - serial: &[u8], - user_data: u64, - ) -> result::Result { - let sector = self.sector; - let request_type = self.request_type; - let offset = (sector << SECTOR_SHIFT) as libc::off_t; - - let mut iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]> = - SmallVec::with_capacity(self.data_descriptors.len()); - for &(data_addr, data_len) in &self.data_descriptors { - let _: u32 = data_len; // compiler-checked documentation - const _: () = assert!( - core::mem::size_of::() <= core::mem::size_of::(), - "unsupported platform" - ); - if data_len == 0 { - continue; - } - let mut top: u64 = u64::from(data_len) / SECTOR_SIZE; - if u64::from(data_len) % SECTOR_SIZE != 0 { - top += 1; - } - let data_len = data_len as usize; - top = top - .checked_add(sector) - .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; - if top > disk_nsectors { - return Err(ExecuteError::BadRequest(Error::InvalidOffset)); - } - - let origin_ptr = mem - .get_slice(data_addr, data_len) - .map_err(ExecuteError::GetHostAddress)? - .ptr_guard(); - - // Verify the buffer alignment. - // In case it's not properly aligned, an intermediate buffer is - // created with the correct alignment, and a copy from/to the - // origin buffer is performed, depending on the type of operation. - let iov_base = if (origin_ptr.as_ptr() as u64) % SECTOR_SIZE != 0 { - let layout = Layout::from_size_align(data_len, SECTOR_SIZE as usize).unwrap(); - // SAFETY: layout has non-zero size - let aligned_ptr = unsafe { alloc_zeroed(layout) }; - if aligned_ptr.is_null() { - return Err(ExecuteError::TemporaryBufferAllocation( - io::Error::last_os_error(), - )); - } - - // We need to perform the copy beforehand in case we're writing - // data out. - if request_type == RequestType::Out { - // SAFETY: destination buffer has been allocated with - // the proper size. - unsafe { std::ptr::copy(origin_ptr.as_ptr(), aligned_ptr, data_len) }; - } - - // Store both origin and aligned pointers for complete_async() - // to process them. - self.aligned_operations.push(AlignedOperation { - origin_ptr: origin_ptr.as_ptr() as u64, - aligned_ptr: aligned_ptr as u64, - size: data_len, - layout, - }); - - aligned_ptr as *mut libc::c_void - } else { - origin_ptr.as_ptr() as *mut libc::c_void - }; - - let iovec = libc::iovec { - iov_base, - iov_len: data_len as libc::size_t, - }; - iovecs.push(iovec); - } - - // Queue operations expected to be submitted. - match request_type { - RequestType::In => { - for (data_addr, data_len) in &self.data_descriptors { - mem.get_slice(*data_addr, *data_len as usize) - .map_err(ExecuteError::GetHostAddress)? - .bitmap() - .mark_dirty(0, *data_len as usize); - } - disk_image - .read_vectored(offset, &iovecs, user_data) - .map_err(ExecuteError::AsyncRead)?; - } - RequestType::Out => { - disk_image - .write_vectored(offset, &iovecs, user_data) - .map_err(ExecuteError::AsyncWrite)?; - } - RequestType::Flush => { - disk_image - .fsync(Some(user_data)) - .map_err(ExecuteError::AsyncFlush)?; - } - RequestType::GetDeviceId => { - let (data_addr, data_len) = if self.data_descriptors.len() == 1 { - (self.data_descriptors[0].0, self.data_descriptors[0].1) - } else { - return Err(ExecuteError::BadRequest(Error::TooManyDescriptors)); - }; - if (data_len as usize) < serial.len() { - return Err(ExecuteError::BadRequest(Error::InvalidOffset)); - } - mem.write_slice(serial, data_addr) - .map_err(ExecuteError::Write)?; - return Ok(false); - } - RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)), - } - - Ok(true) - } - - pub fn complete_async(&mut self) -> result::Result<(), Error> { - for aligned_operation in self.aligned_operations.drain(..) { - // We need to perform the copy after the data has been read inside - // the aligned buffer in case we're reading data in. - if self.request_type == RequestType::In { - // SAFETY: origin buffer has been allocated with the - // proper size. - unsafe { - std::ptr::copy( - aligned_operation.aligned_ptr as *const u8, - aligned_operation.origin_ptr as *mut u8, - aligned_operation.size, - ) - }; - } - - // Free the temporary aligned buffer. - // SAFETY: aligned_ptr was allocated by alloc_zeroed with the same - // layout - unsafe { - dealloc( - aligned_operation.aligned_ptr as *mut u8, - aligned_operation.layout, - ) - }; - } - - Ok(()) - } - - pub fn set_writeback(&mut self, writeback: bool) { - self.writeback = writeback - } -} - #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] #[repr(C, packed)] pub struct VirtioBlockConfig { @@ -606,7 +292,7 @@ pub fn block_io_uring_is_supported() -> bool { let io_uring = match IoUring::new(1) { Ok(io_uring) => io_uring, Err(e) => { - info!("{} failed to create io_uring instance: {}", error_msg, e); + info!("{error_msg} failed to create io_uring instance: {e}"); return false; } }; @@ -619,26 +305,26 @@ pub fn block_io_uring_is_supported() -> bool { match submitter.register_probe(&mut probe) { Ok(_) => {} Err(e) => { - info!("{} failed to register a probe: {}", error_msg, e); + info!("{error_msg} failed to register a probe: {e}"); return false; } } // Check IORING_OP_FSYNC is supported if !probe.is_supported(opcode::Fsync::CODE) { - info!("{} IORING_OP_FSYNC operation not supported", error_msg); + info!("{error_msg} IORING_OP_FSYNC operation not supported"); return false; } // Check IORING_OP_READV is supported if !probe.is_supported(opcode::Readv::CODE) { - info!("{} IORING_OP_READV operation not supported", error_msg); + info!("{error_msg} IORING_OP_READV operation not supported"); return false; } // Check IORING_OP_WRITEV is supported if !probe.is_supported(opcode::Writev::CODE) { - info!("{} IORING_OP_WRITEV operation not supported", error_msg); + info!("{error_msg} IORING_OP_WRITEV operation not supported"); return false; } @@ -646,10 +332,127 @@ pub fn block_io_uring_is_supported() -> bool { } } -pub trait AsyncAdaptor -where - F: Read + Write + Seek, -{ +/// Probe whether the file/device supports punch hole and zero range +pub fn probe_sparse_support(file: &File) -> bool { + let fd = file.as_raw_fd(); + + let is_block_device = { + let mut stat = std::mem::MaybeUninit::::uninit(); + // SAFETY: FFI call with valid fd and buffer + let ret = unsafe { libc::fstat(fd, stat.as_mut_ptr()) }; + if ret != 0 { + warn!( + "Failed to stat file descriptor for sparse probe: {}", + io::Error::last_os_error() + ); + return false; + } + // SAFETY: stat result is valid at this point + unsafe { (*stat.as_ptr()).st_mode & S_IFMT == S_IFBLK } + }; + + if is_block_device { + probe_block_device_sparse_support(fd) + } else { + probe_file_sparse_support(fd) + } +} + +/// Probe sparse support for a regular file using fallocate(). +fn probe_file_sparse_support(fd: libc::c_int) -> bool { + // SAFETY: FFI call with valid fd + let file_size = unsafe { libc::lseek(fd, 0, libc::SEEK_END) }; + if file_size < 0 { + let err = io::Error::last_os_error(); + warn!("Failed to get file size for sparse probe: {err}"); + return false; + } + + // SAFETY: FFI call with valid fd, probing past EOF is safe with KEEP_SIZE + let punch_hole = + unsafe { libc::fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, file_size, 1) } + == 0; + + if !punch_hole { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::EOPNOTSUPP) { + debug!("File does not support FALLOC_FL_PUNCH_HOLE: {err}"); + } else { + debug!("PUNCH_HOLE probe returned unexpected error: {err}"); + } + } + + // SAFETY: FFI call with valid fd, probing past EOF is safe with KEEP_SIZE + let zero_range = + unsafe { libc::fallocate(fd, FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE, file_size, 1) } + == 0; + + if !zero_range { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::EOPNOTSUPP) { + debug!("File does not support FALLOC_FL_ZERO_RANGE: {err}"); + } + } + + let supported = punch_hole || zero_range; + info!( + "Probed file sparse support: punch_hole={punch_hole}, zero_range={zero_range} => {supported}" + ); + supported +} + +/// Probe sparse support for a block device. +/// +/// Block devices always report sparse support. `BLKZEROOUT` is guaranteed to +/// succeed as the kernel provides a software fallback writing explicit zeros +/// when the hardware lacks a native write zeroes command. `BLKDISCARD` may fail +/// at runtime with `EOPNOTSUPP` on devices without trim or discard support, but +/// Linux guests handle this gracefully by ceasing discard requests. +/// +/// There is no non destructive read only ioctl to query block device discard +/// or write zeroes capabilities. +fn probe_block_device_sparse_support(_fd: libc::c_int) -> bool { + info!("Block device: assuming sparse support"); + true +} + +/// Preallocate disk space for a disk image file. +/// +/// Uses `fallocate()` to allocate all disk space upfront, ensuring storage +/// availability and reducing fragmentation. Allocating all blocks upfront is +/// more likely to place them contiguously than allocating on demand during +/// random writes. +pub fn preallocate_disk>(file: &File, path: P) { + let size = match file.metadata() { + Ok(m) => m.len(), + Err(e) => { + warn!("Failed to get metadata for {:?}: {}", path.as_ref(), e); + return; + } + }; + + if size == 0 { + return; + } + + // SAFETY: FFI call with valid file descriptor and size + let ret = unsafe { libc::fallocate(file.as_raw_fd(), 0, 0, size as libc::off_t) }; + + if ret != 0 { + warn!( + "Failed to preallocate disk space for {:?}: {}", + path.as_ref(), + io::Error::last_os_error() + ); + } else { + debug!( + "Preallocated {size} bytes for disk image {:?}", + path.as_ref() + ); + } +} + +pub trait AsyncAdaptor { fn read_vectored_sync( &mut self, offset: libc::off_t, @@ -657,7 +460,10 @@ where user_data: u64, eventfd: &EventFd, completion_list: &mut VecDeque<(u64, i32)>, - ) -> AsyncIoResult<()> { + ) -> AsyncIoResult<()> + where + Self: Read + Seek, + { // Convert libc::iovec into IoSliceMut let mut slices: SmallVec<[IoSliceMut; DEFAULT_DESCRIPTOR_VEC_SIZE]> = SmallVec::with_capacity(iovecs.len()); @@ -669,15 +475,13 @@ where } let result = { - let mut file = self.file(); - // Move the cursor to the right offset - file.seek(SeekFrom::Start(offset as u64)) + self.seek(SeekFrom::Start(offset as u64)) .map_err(AsyncIoError::ReadVectored)?; let mut r = 0; for b in slices.iter_mut() { - r += file.read(b).map_err(AsyncIoError::ReadVectored)?; + r += self.read(b).map_err(AsyncIoError::ReadVectored)?; } r }; @@ -695,7 +499,10 @@ where user_data: u64, eventfd: &EventFd, completion_list: &mut VecDeque<(u64, i32)>, - ) -> AsyncIoResult<()> { + ) -> AsyncIoResult<()> + where + Self: Write + Seek, + { // Convert libc::iovec into IoSlice let mut slices: SmallVec<[IoSlice; DEFAULT_DESCRIPTOR_VEC_SIZE]> = SmallVec::with_capacity(iovecs.len()); @@ -707,15 +514,13 @@ where } let result = { - let mut file = self.file(); - // Move the cursor to the right offset - file.seek(SeekFrom::Start(offset as u64)) + self.seek(SeekFrom::Start(offset as u64)) .map_err(AsyncIoError::WriteVectored)?; let mut r = 0; for b in slices.iter() { - r += file.write(b).map_err(AsyncIoError::WriteVectored)?; + r += self.write(b).map_err(AsyncIoError::WriteVectored)?; } r }; @@ -731,12 +536,13 @@ where user_data: Option, eventfd: &EventFd, completion_list: &mut VecDeque<(u64, i32)>, - ) -> AsyncIoResult<()> { + ) -> AsyncIoResult<()> + where + Self: Write, + { let result: i32 = { - let mut file = self.file(); - // Flush - file.flush().map_err(AsyncIoError::Fsync)?; + self.flush().map_err(AsyncIoError::Fsync)?; 0 }; @@ -748,15 +554,46 @@ where Ok(()) } - - fn file(&mut self) -> MutexGuard<'_, F>; } +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum ImageType { FixedVhd, Qcow2, Raw, Vhdx, + #[default] + Unknown, +} + +impl fmt::Display for ImageType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ImageType::FixedVhd => write!(f, "vhd"), + ImageType::Qcow2 => write!(f, "qcow2"), + ImageType::Raw => write!(f, "raw"), + ImageType::Vhdx => write!(f, "vhdx"), + ImageType::Unknown => write!(f, "unknown"), + } + } +} + +pub enum ImageTypeParseError { + InvalidValue(String), +} + +impl FromStr for ImageType { + type Err = ImageTypeParseError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "vhd" => Ok(ImageType::FixedVhd), + "qcow2" => Ok(ImageType::Qcow2), + "raw" => Ok(ImageType::Raw), + "vhdx" => Ok(ImageType::Vhdx), + _ => Err(ImageTypeParseError::InvalidValue(s.to_string())), + } + } } const QCOW_MAGIC: u32 = 0x5146_49fb; @@ -779,14 +616,27 @@ pub fn read_aligned_block_size(f: &mut File) -> std::io::Result> { Ok(data) } +/// Open a disk image file, returning a [`BlockError`] with path context +/// on failure. +pub fn open_disk_image(path: &Path, options: &OpenOptions) -> BlockResult { + options.open(path).map_err(|e| { + BlockError::new(BlockErrorKind::Io, e) + .with_op(ErrorOp::Open) + .with_path(path) + }) +} + /// Determine image type through file parsing. -pub fn detect_image_type(f: &mut File) -> std::io::Result { - let block = read_aligned_block_size(f)?; +pub fn detect_image_type(f: &mut File) -> BlockResult { + let block = read_aligned_block_size(f) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?; // Check 4 first bytes to get the header value and determine the image type let image_type = if u32::from_be_bytes(block[0..4].try_into().unwrap()) == QCOW_MAGIC { ImageType::Qcow2 - } else if vhd::is_fixed_vhd(f)? { + } else if vhd::is_fixed_vhd(f) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))? + { ImageType::FixedVhd } else if u64::from_le_bytes(block[0..8].try_into().unwrap()) == VHDX_SIGN { ImageType::Vhdx @@ -798,7 +648,14 @@ pub fn detect_image_type(f: &mut File) -> std::io::Result { } pub trait BlockBackend: Read + Write + Seek + Send + Debug { - fn size(&self) -> Result; + /// Returns the logical disk size a guest will see. + /// + /// For raw formats, this is equal to [`Self::physical_size`]. For file formats + /// that wrap disk images in a container (e.g. QCOW2), this refers to the + /// effective size that the guest will see. + fn logical_size(&self) -> Result; + /// Returns the physical size of the underlying file. + fn physical_size(&self) -> Result; } #[derive(Debug)] @@ -824,7 +681,38 @@ ioctl_io_nr!(BLKSSZGET, 0x12, 104); ioctl_io_nr!(BLKPBSZGET, 0x12, 123); ioctl_io_nr!(BLKIOMIN, 0x12, 120); ioctl_io_nr!(BLKIOOPT, 0x12, 121); +ioctl_ior_nr!(BLKGETSIZE64, 0x12, 114, u64); +/// Returns `(logical_size, physical_size)` in bytes for regular files and block devices. +/// +/// For regular files, logical size is `st_size` and physical size is +/// `st_blocks * 512` (actual host allocation). For block devices both +/// values equal the `BLKGETSIZE64` result. +pub fn query_device_size(file: &File) -> io::Result<(u64, u64)> { + let m = file.metadata()?; + if m.is_file() { + // st_blocks is always in 512-byte units on Linux + Ok((m.len(), m.st_blocks() * 512)) + } else if m.file_type().is_block_device() { + let mut size: u64 = 0; + // SAFETY: BLKGETSIZE64 reads the device size into a u64 pointer. + let ret = unsafe { libc::ioctl(file.as_raw_fd(), BLKGETSIZE64() as _, &mut size) }; + if ret != 0 { + return Err(io::Error::last_os_error()); + } + Ok((size, size)) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "disk image must be a regular file or block device, is: {:?}", + m.file_type() + ), + )) + } +} + +#[derive(Copy, Clone)] enum BlockSize { LogicalBlock, PhysicalBlock, @@ -864,13 +752,78 @@ impl DiskTopology { }; if ret != 0 { return Err(std::io::Error::last_os_error()); - }; + } Ok(block_size) } + /// Query the O_DIRECT alignment requirement for a regular file. + /// + /// Uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain the exact + /// memory and offset alignment the kernel requires for direct I/O on + /// this specific file. Unlike `fstatvfs().f_bsize`, which only returns + /// the filesystem's preferred I/O block size, `STATX_DIOALIGN` reports + /// the true per-file DIO constraints accounting for the filesystem, + /// underlying block device, and any stacking (loop, dm, etc.). + fn query_file_alignment(f: &File) -> u64 { + // The libc crate does not expose statx / STATX_DIOALIGN on all + // targets (e.g. musl), so define the constant and a minimal repr(C) + // struct locally and invoke the syscall directly. + const STATX_DIOALIGN: u32 = 0x2000; + + // Minimal statx layout, only the needed fields, + // everything else is padding. + #[repr(C)] + struct Statx { + stx_mask: u32, + _pad: [u8; 148], + stx_dio_mem_align: u32, + stx_dio_offset_align: u32, + _pad2: [u8; 96], + } + + let mut stx = mem::MaybeUninit::::zeroed(); + // SAFETY: FFI syscall with valid fd and correctly sized buffer. + let ret = unsafe { + libc::syscall( + libc::SYS_statx, + f.as_raw_fd(), + c"".as_ptr(), + libc::AT_EMPTY_PATH, + STATX_DIOALIGN, + stx.as_mut_ptr(), + ) + }; + if ret == 0 { + // SAFETY: statx succeeded, the struct is fully initialized. + let stx = unsafe { stx.assume_init() }; + if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 { + let align = cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64; + debug!("statx(STATX_DIOALIGN) returned alignment {align}"); + return align; + } + } + + debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}"); + SECTOR_SIZE + } + pub fn probe(f: &File) -> std::io::Result { if !Self::is_block_device(f)? { + // For regular files opened with O_DIRECT, the logical block size + // must reflect the filesystem DIO alignment so the guest issues + // correctly sized I/O. + // SAFETY: fcntl(F_GETFL) is always safe on a valid fd. + let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) }; + if flags >= 0 && (flags & libc::O_DIRECT) != 0 { + let alignment = Self::query_file_alignment(f); + return Ok(DiskTopology { + logical_block_size: alignment, + physical_block_size: alignment, + minimum_io_size: alignment, + optimal_io_size: 0, + }); + } return Ok(DiskTopology::default()); } @@ -882,3 +835,192 @@ impl DiskTopology { }) } } + +#[cfg(test)] +mod unit_tests { + use std::alloc::{Layout, alloc_zeroed, dealloc}; + use std::fs::OpenOptions; + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + use std::{ptr, slice}; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + + #[test] + fn test_probe_regular_file_returns_valid_alignment() { + let temp_file = TempFile::new().unwrap(); + let mut f = temp_file.into_file(); + f.write_all(&[0u8; 4096]).unwrap(); + f.sync_all().unwrap(); + + let topo = DiskTopology::probe(&f).unwrap(); + + assert_eq!( + topo.logical_block_size, SECTOR_SIZE, + "probe() should return {SECTOR_SIZE} for regular files without O_DIRECT, got {}", + topo.logical_block_size + ); + } + + #[test] + fn test_probe_regular_file_with_direct_returns_dio_alignment() { + let temp_file = TempFile::new().unwrap(); + let path = temp_file.as_path().to_owned(); + { + let f = temp_file.as_file(); + f.set_len(1 << 20).unwrap(); // 1 MiB + f.sync_all().unwrap(); + } + + let f = OpenOptions::new() + .read(true) + .write(true) + .custom_flags(libc::O_DIRECT) + .open(&path) + .unwrap(); + let topo = DiskTopology::probe(&f).unwrap(); + + assert!( + topo.logical_block_size.is_power_of_two(), + "logical_block_size {} is not a power of two", + topo.logical_block_size + ); + assert!( + topo.logical_block_size >= SECTOR_SIZE, + "logical_block_size {} is less than SECTOR_SIZE ({SECTOR_SIZE})", + topo.logical_block_size + ); + + let alignment = topo.logical_block_size as usize; + let layout = Layout::from_size_align(4096, alignment); + assert!( + layout.is_ok(), + "Layout::from_size_align(4096, {alignment}) failed: {:?}", + layout.err() + ); + } + + #[test] + fn test_dio_write_read_with_probed_alignment() { + let temp_file = TempFile::new().unwrap(); + let path = temp_file.as_path().to_owned(); + { + let f = temp_file.as_file(); + f.set_len(1 << 20).unwrap(); // 1 MiB + f.sync_all().unwrap(); + } + + let f = OpenOptions::new() + .read(true) + .write(true) + .custom_flags(libc::O_DIRECT) + .open(&path) + .unwrap(); + let topo = DiskTopology::probe(&f).unwrap(); + let alignment = topo.logical_block_size as usize; + + let layout = Layout::from_size_align(alignment, alignment).unwrap(); + // SAFETY: layout is valid (non-zero, power-of-two alignment). + let buf = unsafe { alloc_zeroed(layout) }; + assert!(!buf.is_null()); + + // SAFETY: buf is valid for `alignment` bytes. + unsafe { ptr::write_bytes(buf, 0xAB, alignment) }; + + // SAFETY: buf is aligned and sized for O_DIRECT; fd is valid. + let written = unsafe { libc::pwrite(f.as_raw_fd(), buf.cast(), alignment, 0) }; + assert_eq!( + written as usize, + alignment, + "O_DIRECT pwrite failed: {}", + io::Error::last_os_error() + ); + + // SAFETY: buf is valid for `alignment` bytes. + unsafe { ptr::write_bytes(buf, 0x00, alignment) }; + // SAFETY: buf is aligned and sized for O_DIRECT; fd is valid. + let read = unsafe { libc::pread(f.as_raw_fd(), buf.cast(), alignment, 0) }; + assert_eq!( + read as usize, + alignment, + "O_DIRECT pread failed: {}", + io::Error::last_os_error() + ); + + // SAFETY: buf is valid for `alignment` bytes after successful pread. + let slice = unsafe { slice::from_raw_parts(buf, alignment) }; + assert!( + slice.iter().all(|&b| b == 0xAB), + "Data mismatch after O_DIRECT roundtrip" + ); + + // SAFETY: buf was allocated with this layout via alloc_zeroed. + unsafe { dealloc(buf, layout) }; + } + + #[test] + fn test_query_device_size_regular_file() { + let temp_file = TempFile::new().unwrap(); + let mut f = temp_file.into_file(); + // 5 sectors + 13 extra bytes - not page aligned, not sectoraligned + f.write_all(&[0xAB; 5 * 512 + 13]).unwrap(); + f.sync_all().unwrap(); + + let (logical, physical) = query_device_size(&f).unwrap(); + assert_eq!(logical, 5 * 512 + 13); + assert!(physical > 0); + } + + #[test] + fn test_query_device_size_sparse_file_punch_hole() { + let temp_file = TempFile::new().unwrap(); + let f = temp_file.as_file(); + // Allocate 1 MiB + let size: i64 = 1 << 20; + f.set_len(size as u64).unwrap(); + // SAFETY: fd is valid, range is within file size. + let ret = unsafe { + libc::fallocate( + f.as_raw_fd(), + 0, // allocate + 0, + size, + ) + }; + assert_eq!(ret, 0, "fallocate failed: {}", io::Error::last_os_error()); + f.sync_all().unwrap(); + + let (log_before, phys_before) = query_device_size(f).unwrap(); + assert_eq!(log_before, size as u64); + assert_eq!(phys_before, size as u64); + + // Punch a hole in the middle 512 KiB + // SAFETY: fd is valid, range is within file size. + let ret = unsafe { + libc::fallocate( + f.as_raw_fd(), + libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE, + size / 4, + size / 2, + ) + }; + assert_eq!(ret, 0, "punch hole failed: {}", io::Error::last_os_error()); + f.sync_all().unwrap(); + + let (logical, physical) = query_device_size(f).unwrap(); + assert_eq!(logical, size as u64, "logical size must not change"); + assert!( + physical < logical, + "physical ({physical}) should be less than logical ({logical}) after punch hole" + ); + } + + #[test] + fn test_query_device_size_rejects_char_device() { + let f = std::fs::File::open("/dev/zero").unwrap(); + let err = query_device_size(&f).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } +} diff --git a/block/src/qcow/backing.rs b/block/src/qcow/backing.rs new file mode 100644 index 0000000000..6b8448861f --- /dev/null +++ b/block/src/qcow/backing.rs @@ -0,0 +1,184 @@ +// 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::io; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; +use std::sync::Arc; + +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::qcow::decoder::Decoder; +use crate::qcow::metadata::{BackingRead, ClusterReadMapping, QcowMetadata}; +use crate::qcow::{BackingFile, BackingKind, Error as QcowError}; +use crate::qcow_common::{decompress_cluster, pread_alloc, pread_exact}; + +/// Raw backing file using pread64 on a duplicated fd. +pub(crate) struct RawBacking { + pub(crate) fd: OwnedFd, + pub(crate) virtual_size: u64, +} + +// SAFETY: The only I/O operation is pread64 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() { + pread_exact(self.fd.as_raw_fd(), buf, address) + } else { + pread_exact(self.fd.as_raw_fd(), &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, + pub(crate) data_fd: OwnedFd, + pub(crate) backing_file: Option>, + pub(crate) cluster_size: u64, + pub(crate) decoder: Arc, +} + +// SAFETY: All reads go through QcowMetadata which uses RwLock +// and pread64 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 { + /// Resolve cluster mappings via metadata then read allocated clusters + /// with pread64. + fn read_clusters(&self, address: u64, buf: &mut [u8]) -> io::Result<()> { + let total_len = buf.len(); + let has_backing = self.backing_file.is_some(); + + 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, + } => { + pread_exact( + self.data_fd.as_raw_fd(), + &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 compressed = + pread_alloc(self.data_fd.as_raw_fd(), host_offset, compressed_size)?; + let decompressed = decompress_cluster( + &compressed, + self.cluster_size as usize, + &*self.decoder, + )?; + buf[buf_offset..buf_offset + length] + .copy_from_slice(&decompressed[cluster_offset..cluster_offset + length]); + buf_offset += length; + } + ClusterReadMapping::Backing { + offset: backing_offset, + length, + } => { + self.backing_file.as_ref().unwrap().read_at( + backing_offset, + &mut buf[buf_offset..buf_offset + length as usize], + )?; + buf_offset += length as usize; + } + } + } + Ok(()) + } +} + +impl Drop for Qcow2Backing { + fn drop(&mut self) { + self.metadata.shutdown(); + } +} + +/// Construct a thread safe backing file reader. +pub fn shared_backing_from(bf: BackingFile) -> BlockResult> { + let (kind, virtual_size) = bf.into_kind(); + + let dup_fd = |fd: BorrowedFd<'_>| -> BlockResult { + 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 fd = dup_fd(raw_file.as_fd())?; + Ok(Arc::new(RawBacking { fd, virtual_size })) + } + BackingKind::Qcow { inner, backing } => { + let data_fd = dup_fd(inner.raw_file.as_fd())?; + let metadata = Arc::new(QcowMetadata::new(*inner)); + Ok(Arc::new(Qcow2Backing { + cluster_size: metadata.cluster_size(), + decoder: metadata.decoder(), + metadata, + data_fd, + backing_file: backing.map(|bf| shared_backing_from(*bf)).transpose()?, + })) + } + #[cfg(test)] + BackingKind::QcowFile(_) => { + unreachable!("QcowFile variant is only used by set_backing_file() in tests") + } + } +} diff --git a/block/src/qcow/decoder.rs b/block/src/qcow/decoder.rs new file mode 100644 index 0000000000..f9510baf93 --- /dev/null +++ b/block/src/qcow/decoder.rs @@ -0,0 +1,87 @@ +// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +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] std::io::Error), + #[error("Zstd: failed to fill buffer")] + ZstdFillBuffer(#[source] std::io::Error), +} + +pub type Result = std::result::Result; + +/// Generic trait for decoding zlib/zstd formats +pub trait Decoder: Send + Sync { + fn decode(&self, input: &[u8], output: &mut [u8]) -> Result; +} + +#[derive(Default)] +pub struct ZlibDecoder {} + +impl Decoder for ZlibDecoder { + fn decode(&self, input: &[u8], output: &mut [u8]) -> Result { + 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 struct ZstdDecoder {} + +impl Decoder for ZstdDecoder { + fn decode(&self, input: &[u8], output: &mut [u8]) -> Result { + 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(); + } +} diff --git a/block/src/qcow/header.rs b/block/src/qcow/header.rs new file mode 100644 index 0000000000..22a5492b19 --- /dev/null +++ b/block/src/qcow/header.rs @@ -0,0 +1,605 @@ +// 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::io::{Read, Seek, SeekFrom, Write}; +use std::mem::size_of; +use std::str::FromStr; + +use bitflags::bitflags; +use vmm_sys_util::file_traits::FileSync; + +use super::decoder::{Decoder, ZlibDecoder, ZstdDecoder}; +use super::qcow_raw_file::BeUint; +use super::raw_file::RawFile; +use super::{Error, Result, div_round_up_u32, div_round_up_u64}; +use 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 { + 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, +} + +// 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 +const COMPRESSION_TYPE_ZSTD: u64 = 1; // 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 = (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, +} + +impl QcowHeader { + /// Read header extensions, optionally collecting feature names for error reporting. + pub(super) fn read_header_extensions( + f: &mut RawFile, + header: &mut QcowHeader, + mut feature_table: Option<&mut Vec<(u8, String)>>, + ) -> Result<()> { + // Extensions start directly after the header + f.seek(SeekFrom::Start(header.header_size as u64)) + .map_err(Error::ReadingHeader)?; + + loop { + let ext_type = u32::read_be(f).map_err(Error::ReadingHeader)?; + if ext_type == HEADER_EXT_END { + break; + } + + let ext_length = u32::read_be(f).map_err(Error::ReadingHeader)?; + + match ext_type { + HEADER_EXT_BACKING_FORMAT => { + let mut format_bytes = vec![0u8; ext_length as usize]; + f.read_exact(&mut format_bytes) + .map_err(Error::ReadingHeader)?; + 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(&mut data).map_err(Error::ReadingHeader)?; + let table = feature_table.as_mut().unwrap(); + for entry in data.chunks_exact(FEATURE_NAME_ENTRY_SIZE) { + 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 + f.seek(SeekFrom::Current(ext_length as i64)) + .map_err(Error::ReadingHeader)?; + } + } + + // Skip to the next 8 byte boundary + let padding = (8 - (ext_length % 8)) % 8; + f.seek(SeekFrom::Current(padding as i64)) + .map_err(Error::ReadingHeader)?; + } + + Ok(()) + } + + /// Creates a QcowHeader from a reference to a file. + pub fn new(f: &mut RawFile) -> Result { + f.rewind().map_err(Error::ReadingHeader)?; + let magic = u32::read_be(f).map_err(Error::ReadingHeader)?; + if magic != QCOW_MAGIC { + return Err(Error::InvalidMagic); + } + + // Reads the next u32 from the file. + fn read_u32_be(f: &mut RawFile) -> Result { + u32::read_be(f).map_err(Error::ReadingHeader) + } + + // Reads the next u64 from the file. + fn read_u64_be(f: &mut RawFile) -> Result { + u64::read_be(f).map_err(Error::ReadingHeader) + } + + let version = read_u32_be(f)?; + + let mut header = QcowHeader { + magic, + version, + backing_file_offset: read_u64_be(f)?, + backing_file_size: read_u32_be(f)?, + cluster_bits: read_u32_be(f)?, + size: read_u64_be(f)?, + crypt_method: read_u32_be(f)?, + l1_size: read_u32_be(f)?, + l1_table_offset: read_u64_be(f)?, + refcount_table_offset: read_u64_be(f)?, + refcount_table_clusters: read_u32_be(f)?, + nb_snapshots: read_u32_be(f)?, + snapshots_offset: read_u64_be(f)?, + incompatible_features: if version == 2 { 0 } else { read_u64_be(f)? }, + compatible_features: if version == 2 { 0 } else { read_u64_be(f)? }, + autoclear_features: if version == 2 { 0 } else { read_u64_be(f)? }, + refcount_order: if version == 2 { + DEFAULT_REFCOUNT_ORDER + } else { + read_u32_be(f)? + }, + header_size: if version == 2 { + V2_BARE_HEADER_SIZE + } else { + read_u32_be(f)? + }, + compression_type: CompressionType::Zlib, + backing_file: None, + }; + if version == 3 && header.header_size > V3_BARE_HEADER_SIZE { + let raw_compression_type = read_u64_be(f)? >> (64 - 8); + header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB { + Ok(CompressionType::Zlib) + } else if raw_compression_type == COMPRESSION_TYPE_ZSTD { + Ok(CompressionType::Zstd) + } else { + Err(Error::UnsupportedCompressionType) + }?; + } + if header.backing_file_size > MAX_BACKING_FILE_SIZE { + return Err(Error::BackingFileTooLong(header.backing_file_size as usize)); + } + if header.backing_file_offset != 0 { + f.seek(SeekFrom::Start(header.backing_file_offset)) + .map_err(Error::ReadingHeader)?; + let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize]; + f.read_exact(&mut backing_file_name_bytes) + .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 { + 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 { + 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::() 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::() 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::() 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 `file`. + pub fn write_to(&self, file: &mut F) -> Result<()> { + // Writes the next u32 to the file. + fn write_u32_be(f: &mut F, value: u32) -> Result<()> { + u32::write_be(f, value).map_err(Error::WritingHeader) + } + + // Writes the next u64 to the file. + fn write_u64_be(f: &mut F, value: u64) -> Result<()> { + u64::write_be(f, value).map_err(Error::WritingHeader) + } + + write_u32_be(file, self.magic)?; + write_u32_be(file, self.version)?; + write_u64_be(file, self.backing_file_offset)?; + write_u32_be(file, self.backing_file_size)?; + write_u32_be(file, self.cluster_bits)?; + write_u64_be(file, self.size)?; + write_u32_be(file, self.crypt_method)?; + write_u32_be(file, self.l1_size)?; + write_u64_be(file, self.l1_table_offset)?; + write_u64_be(file, self.refcount_table_offset)?; + write_u32_be(file, self.refcount_table_clusters)?; + write_u32_be(file, self.nb_snapshots)?; + write_u64_be(file, self.snapshots_offset)?; + + if self.version == 3 { + write_u64_be(file, self.incompatible_features)?; + write_u64_be(file, self.compatible_features)?; + write_u64_be(file, self.autoclear_features)?; + write_u32_be(file, self.refcount_order)?; + write_u32_be(file, self.header_size)?; + + if self.header_size > V3_BARE_HEADER_SIZE { + write_u64_be(file, 0)?; // no compression + } + + write_u32_be(file, 0)?; // header extension type: end of header extension area + write_u32_be(file, 0)?; // length of header extension data: 0 + } + + if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) { + if self.backing_file_offset > 0 { + file.seek(SeekFrom::Start(self.backing_file_offset)) + .map_err(Error::WritingHeader)?; + } + write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?; + } + + // Set the file length by seeking and writing a zero to the last byte. This avoids needing + // a `File` instead of anything that implements seek as the `file` argument. + // Zeros out the l1 and refcount table clusters. + let cluster_size = 0x01u64 << self.cluster_bits; + let refcount_blocks_size = u64::from(self.refcount_table_clusters) * cluster_size; + file.seek(SeekFrom::Start( + self.refcount_table_offset + refcount_blocks_size - 2, + )) + .map_err(Error::WritingHeader)?; + file.write(&[0u8]).map_err(Error::WritingHeader)?; + + Ok(()) + } + + /// Write only the incompatible_features field to the file at its fixed offset. + fn write_incompatible_features(&self, file: &mut F) -> BlockResult<()> { + if self.version != 3 { + return Ok(()); + } + file.seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?; + u64::write_be(file, self.incompatible_features) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?; + 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 F, + 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 F) -> 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 F, + ) -> Result<()> { + if self.version == 3 && self.autoclear_features != 0 { + self.autoclear_features = 0; + file.seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET)) + .map_err(Error::WritingHeader)?; + u64::write_be(file, 0).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(()) +} diff --git a/block/src/qcow/metadata.rs b/block/src/qcow/metadata.rs new file mode 100644 index 0000000000..d7c38c9fc5 --- /dev/null +++ b/block/src/qcow/metadata.rs @@ -0,0 +1,1080 @@ +// 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 metadata with lock based synchronization. +//! +//! QcowMetadata wraps the in memory QCOW2 metadata tables behind a single +//! coarse RwLock. This separates metadata lookup from data I/O, allowing +//! data reads and writes to proceed without holding the metadata lock. +//! +//! On L2 cache hit, map_clusters_for_read only needs a read lock with +//! pure shared reference access on the cache. Cache misses and all write +//! operations upgrade to a write lock. + +use std::cmp::min; +use std::io::{self, Seek}; +use std::mem; +use std::sync::{Arc, RwLock}; + +use libc::{EINVAL, EIO}; + +use super::decoder::Decoder; +use super::qcow_raw_file::QcowRawFile; +use super::refcount::RefCount; +use super::util::{ + div_round_up_u64, l1_entry_make, l2_entry_compressed_cluster_layout, l2_entry_is_compressed, + l2_entry_is_empty, l2_entry_is_zero, l2_entry_make_std, l2_entry_make_zero, + l2_entry_std_cluster_addr, +}; +use super::vec_cache::{CacheMap, Cacheable, VecCache}; +use super::{QcowHeader, refcount}; + +/// Describes how to satisfy a guest read for a single cluster region. +/// +/// Returned by QcowMetadata::map_clusters_for_read. The caller performs +/// the actual data I/O using its own per queue file descriptor without +/// holding the metadata lock. +#[derive(Debug)] +pub enum ClusterReadMapping { + /// The cluster is not allocated and the guest should see zeros. + /// This covers both truly unallocated clusters where the L1 or L2 + /// entry is zero and clusters with the ZERO flag set. + Zero { length: u64 }, + + /// The cluster is allocated at the given host file offset. + /// The offset is the exact byte position combining cluster base and + /// intra cluster offset. The length is the number of bytes to read, + /// bounded by cluster boundary and guest request. + Allocated { offset: u64, length: u64 }, + + /// The cluster is compressed. The host file offset and compressed byte + /// count are extracted from the L2 entry under the read lock. The caller + /// reads the compressed data with pread on its own fd, decompresses + /// into a cluster sized buffer, then slices the requested range. + Compressed { + host_offset: u64, + compressed_size: usize, + cluster_offset: usize, + length: usize, + }, + + /// The cluster is not allocated in this layer but may exist in a backing + /// file. The caller should delegate to the backing file at the given + /// guest offset for the specified length in bytes. + Backing { offset: u64, length: u64 }, +} + +/// Describes how to satisfy a guest write for a single cluster region. +/// +/// Returned by QcowMetadata::map_cluster_for_write. The caller performs +/// the actual data I/O using its own per queue file descriptor without +/// holding the metadata lock. +#[derive(Debug)] +pub enum ClusterWriteMapping { + /// The write target is at the given host file offset. + /// This covers both already allocated clusters and freshly allocated ones. + /// The offset is the exact byte position combining cluster base and + /// intra cluster offset. + Allocated { offset: u64 }, +} + +/// Trait for reading from a backing file in a thread safe manner. +/// +/// Used by QcowMetadata::deallocate_bytes so it can read COW data +/// from the backing file without knowing the concrete backing type. +pub(crate) trait BackingRead: Send + Sync { + fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()>; +} + +/// Action that the caller must perform after deallocate_bytes. +#[derive(Debug)] +pub enum DeallocAction { + /// Punch a hole at the given host file offset for a full cluster. + PunchHole { host_offset: u64, length: u64 }, + /// Write zeros at the given host file offset for a partial cluster. + WriteZeroes { host_offset: u64, length: usize }, +} + +/// Shared QCOW2 metadata protected by a coarse RwLock. +/// +/// Holds the L1 table, L2 cache and refcount state in memory. L2 table +/// entries and refcount blocks are read from disk on cache miss and +/// written back on eviction or when dirty. +/// +/// One instance is shared via Arc across all virtio blk queues. Each +/// queue holds its own QcowRawFile clone for data I/O. +/// +/// Steady state guest I/O is read dominant at the metadata level. Every +/// read and every write to an already allocated cluster only needs an +/// L1 to L2 lookup, which completes under a shared read lock. Only +/// cluster allocation, L2 cache eviction and resize take the exclusive +/// write lock, so contention stays low and queues scale. +pub struct QcowMetadata { + inner: RwLock, + decoder: Arc, +} + +/// The actual metadata state, accessible only through the RwLock. +pub(crate) struct QcowState { + pub(crate) header: QcowHeader, + pub(crate) l1_table: VecCache, + pub(crate) l2_entries: u64, + pub(crate) l2_cache: CacheMap>, + pub(crate) refcounts: RefCount, + pub(crate) avail_clusters: Vec, + pub(crate) unref_clusters: Vec, + /// Dedicated file descriptor for metadata I/O covering L2 table reads, + /// refcount block reads and dirty eviction writes. This is a dup clone + /// of the original fd, separate from the per queue data I/O fds. + pub(crate) raw_file: QcowRawFile, +} + +impl QcowMetadata { + pub(crate) fn new(inner: QcowState) -> Self { + QcowMetadata { + decoder: Arc::from(inner.header.get_decoder()), + inner: RwLock::new(inner), + } + } + + /// Maps a multicluster guest read range to a list of read mappings. + /// + /// This walks the range in cluster sized steps under a single lock + /// acquisition, reducing lock roundtrips for large reads. The returned + /// mappings are ordered by guest address and ready for io_uring + /// submission. The caller can coalesce adjacent allocated entries into + /// fewer submissions. + /// + /// On the read lock fast path, if all L2 tables are cached, the lookup + /// is pure memory access with no I/O and concurrent readers are allowed. + /// + /// On the write lock slow path, if an L2 cache miss occurs, the L2 + /// table is read from disk via the metadata fd, the cache is populated + /// and the mapping is returned. + /// + /// The has_backing_file flag indicates whether a backing file exists, + /// needed to distinguish zero versus backing for unallocated clusters. + pub fn map_clusters_for_read( + &self, + address: u64, + total_length: usize, + has_backing_file: bool, + ) -> io::Result> { + let inner = self.inner.read().unwrap(); + let cluster_size = inner.raw_file.cluster_size(); + let mut mappings = Vec::new(); + let mut mapped = 0usize; + let mut need_write_lock = false; + + // Fast path, try all chunks under read lock + while mapped < total_length { + let curr_addr = address + mapped as u64; + let offset_in_cluster = inner.raw_file.cluster_offset(curr_addr) as usize; + let count = min( + total_length - mapped, + cluster_size as usize - offset_in_cluster, + ); + + match inner.try_map_read(curr_addr, count, has_backing_file)? { + Some(mapping) => mappings.push(mapping), + None => { + need_write_lock = true; + break; + } + } + mapped += count; + } + + if !need_write_lock { + return Ok(mappings); + } + + // Slow path, drop read lock, take write lock, redo from where we stopped + drop(inner); + let mut inner = self.inner.write().unwrap(); + + // Remap everything under write lock for consistency since the L2 cache + // may have been evicted between the read to write lock transition. + mappings.clear(); + mapped = 0; + + while mapped < total_length { + let curr_addr = address + mapped as u64; + let offset_in_cluster = inner.raw_file.cluster_offset(curr_addr) as usize; + let count = min( + total_length - mapped, + cluster_size as usize - offset_in_cluster, + ); + + mappings.push(inner.map_read_with_populate(curr_addr, count, has_backing_file)?); + mapped += count; + } + + Ok(mappings) + } + + /// Maps a guest write address to a write mapping. + /// + /// Always takes a write lock since writes may need to allocate clusters, + /// update L2 entries and update refcounts. + /// + /// The backing_data parameter is the COW source. If the cluster is + /// unallocated and a backing file exists, the caller should have already + /// read the backing cluster data and pass it here. If None, the new + /// cluster is zeroed. + pub fn map_cluster_for_write( + &self, + address: u64, + backing_data: Option>, + ) -> io::Result { + let mut inner = self.inner.write().unwrap(); + inner.map_write(address, backing_data) + } + + pub fn flush(&self) -> io::Result<()> { + let mut inner = self.inner.write().unwrap(); + inner.sync_caches()?; + let mut unref = mem::take(&mut inner.unref_clusters); + inner.avail_clusters.append(&mut unref); + Ok(()) + } + + /// Flushes dirty metadata caches and clears the dirty bit for + /// clean shutdown. + pub fn shutdown(&self) { + let mut inner = self.inner.write().unwrap(); + let _ = inner.sync_caches(); + let QcowState { + ref mut header, + ref mut raw_file, + .. + } = *inner; + if raw_file.file().is_writable() { + let _ = header.set_dirty_bit(raw_file.file_mut(), false); + } + } + + /// Resizes the QCOW2 image to the given new size. Only grow is + /// supported, shrink would require walking all L2 tables to reclaim + /// clusters beyond the new size and risks data loss. + /// + /// Returns an error if the new size is smaller than the current size. + pub fn resize(&self, new_size: u64) -> io::Result<()> { + let mut inner = self.inner.write().unwrap(); + inner.resize(new_size) + } + + /// Deallocates a range of bytes. Full clusters are deallocated via metadata. + /// Partial clusters need the caller to write zeros. This method returns a + /// list of actions the caller should take. + pub(crate) fn deallocate_bytes( + &self, + address: u64, + length: usize, + sparse: bool, + virtual_size: u64, + cluster_size: u64, + backing_file: Option<&dyn BackingRead>, + ) -> io::Result> { + if address.checked_add(length as u64).is_none() { + return Ok(Vec::new()); + } + let mut inner = self.inner.write().unwrap(); + let mut actions = Vec::new(); + + let file_end = virtual_size; + let remaining_in_file = file_end.saturating_sub(address); + let write_count = min(length as u64, remaining_in_file) as usize; + + let mut nwritten = 0usize; + while nwritten < write_count { + let curr_addr = address + nwritten as u64; + let offset_in_cluster = inner.raw_file.cluster_offset(curr_addr) as usize; + let count = min( + write_count - nwritten, + cluster_size as usize - offset_in_cluster, + ); + + if count == cluster_size as usize { + let punch_offset = inner.deallocate_cluster(curr_addr, sparse)?; + if let Some(host_offset) = punch_offset { + actions.push(DeallocAction::PunchHole { + host_offset, + length: cluster_size, + }); + } + } else { + // Partial cluster - COW from backing to preserve non zeroed bytes, + // then the caller writes zeros to the partial range. + let backing_data = if let Some(backing) = backing_file { + let cluster_begin = curr_addr - offset_in_cluster as u64; + let mut data = vec![0u8; cluster_size as usize]; + backing.read_at(cluster_begin, &mut data)?; + Some(data) + } else { + None + }; + let mapping = inner.map_write(curr_addr, backing_data)?; + let ClusterWriteMapping::Allocated { offset } = mapping; + actions.push(DeallocAction::WriteZeroes { + host_offset: offset, + length: count, + }); + } + + nwritten += count; + } + Ok(actions) + } + + pub fn virtual_size(&self) -> u64 { + self.inner.read().unwrap().header.size + } + + pub fn cluster_size(&self) -> u64 { + self.inner.read().unwrap().raw_file.cluster_size() + } + + /// Returns the shared decoder matching the image compression type. + pub fn decoder(&self) -> Arc { + Arc::clone(&self.decoder) + } +} + +impl QcowState { + /// Fast path read mapping under read lock only. Returns None on cache + /// miss. + /// + /// All access here is through shared reference. CacheMap::get, + /// VecCache::get and index operations are all shared reference compatible. + fn try_map_read( + &self, + address: u64, + count: usize, + has_backing_file: bool, + ) -> io::Result> { + if address >= self.header.size { + return Err(io::Error::from_raw_os_error(EINVAL)); + } + + let l1_index = self.l1_table_index(address) as usize; + let l2_addr_disk = match self.l1_table.get(l1_index) { + Some(&addr) => addr, + None => return Err(io::Error::from_raw_os_error(EINVAL)), + }; + + if l2_addr_disk == 0 { + return Ok(Some(self.unallocated_read_mapping( + address, + count, + has_backing_file, + ))); + } + + let l2_table = match self.l2_cache.get(l1_index) { + Some(table) => table, + None => return Ok(None), // cache miss, need write lock + }; + + let l2_index = self.l2_table_index(address) as usize; + let l2_entry = l2_table[l2_index]; + + // Compressed entries: extract layout from L2 entry under read lock. + // The caller reads and decompresses on its own fd without holding + // the metadata lock. + if l2_entry_is_compressed(l2_entry) { + let (host_offset, compressed_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + let cluster_offset = self.raw_file.cluster_offset(address) as usize; + return Ok(Some(ClusterReadMapping::Compressed { + host_offset, + compressed_size, + cluster_offset, + length: count, + })); + } + + if l2_entry_is_empty(l2_entry) { + Ok(Some(self.unallocated_read_mapping( + address, + count, + has_backing_file, + ))) + } else if l2_entry_is_zero(l2_entry) { + // Match original QcowFile::file_read semantics where zero flagged + // entries fall through to backing file when one exists or return + // zeros otherwise. + Ok(Some(self.unallocated_read_mapping( + address, + count, + has_backing_file, + ))) + } else { + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + let cluster_size = self.raw_file.cluster_size(); + if cluster_addr & (cluster_size - 1) != 0 { + // Fall through to write lock path which sets the corrupt bit + return Ok(None); + } + let intra_offset = self.raw_file.cluster_offset(address); + Ok(Some(ClusterReadMapping::Allocated { + offset: cluster_addr + intra_offset, + length: count as u64, + })) + } + } + + /// Slow path read mapping. Requires exclusive access to populate cache. + fn map_read_with_populate( + &mut self, + address: u64, + count: usize, + has_backing_file: bool, + ) -> io::Result { + if address >= self.header.size { + return Err(io::Error::from_raw_os_error(EINVAL)); + } + + let l1_index = self.l1_table_index(address) as usize; + let l2_addr_disk = match self.l1_table.get(l1_index) { + Some(&addr) => addr, + None => return Err(io::Error::from_raw_os_error(EINVAL)), + }; + + if l2_addr_disk == 0 { + return Ok(self.unallocated_read_mapping(address, count, has_backing_file)); + } + + // Populate cache if needed as this does I/O via the metadata raw file + self.cache_l2_cluster(l1_index, l2_addr_disk)?; + + let l2_index = self.l2_table_index(address) as usize; + let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; + + if l2_entry_is_empty(l2_entry) { + Ok(self.unallocated_read_mapping(address, count, has_backing_file)) + } else if l2_entry_is_compressed(l2_entry) { + let (host_offset, compressed_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + let cluster_offset = self.raw_file.cluster_offset(address) as usize; + Ok(ClusterReadMapping::Compressed { + host_offset, + compressed_size, + cluster_offset, + length: count, + }) + } else if l2_entry_is_zero(l2_entry) { + // Match original QcowFile::file_read semantics where zero flagged + // entries fall through to backing file when one exists or return + // zeros otherwise. + Ok(self.unallocated_read_mapping(address, count, has_backing_file)) + } else { + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + let cluster_size = self.raw_file.cluster_size(); + if cluster_addr & (cluster_size - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + let intra_offset = self.raw_file.cluster_offset(address); + Ok(ClusterReadMapping::Allocated { + offset: cluster_addr + intra_offset, + length: count as u64, + }) + } + } + + fn unallocated_read_mapping( + &self, + address: u64, + count: usize, + has_backing_file: bool, + ) -> ClusterReadMapping { + if has_backing_file { + ClusterReadMapping::Backing { + offset: address, + length: count as u64, + } + } else { + ClusterReadMapping::Zero { + length: count as u64, + } + } + } + + /// Maps a single cluster region for a sequential read. + pub(crate) fn map_cluster_read( + &mut self, + address: u64, + count: usize, + has_backing_file: bool, + ) -> io::Result { + match self.try_map_read(address, count, has_backing_file)? { + Some(mapping) => Ok(mapping), + None => self.map_read_with_populate(address, count, has_backing_file), + } + } + + /// Write path mapping. Always called under write lock. + fn map_write( + &mut self, + address: u64, + backing_data: Option>, + ) -> io::Result { + if address >= self.header.size { + return Err(io::Error::from_raw_os_error(EINVAL)); + } + + let l1_index = self.l1_table_index(address) as usize; + let l2_addr_disk = match self.l1_table.get(l1_index) { + Some(&addr) => addr, + None => return Err(io::Error::from_raw_os_error(EINVAL)), + }; + let l2_index = self.l2_table_index(address) as usize; + + let mut set_refcounts = Vec::new(); + + if let Some(new_addr) = self.cache_l2_cluster_alloc(l1_index, l2_addr_disk)? { + set_refcounts.push((new_addr, 1)); + } + + let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; + let cluster_addr = if l2_entry_is_compressed(l2_entry) { + let decompressed_cluster = self.decompress_l2_cluster(l2_entry)?; + let cluster_addr = self.append_data_cluster(None)?; + self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; + self.raw_file + .file_mut() + .seek(io::SeekFrom::Start(cluster_addr))?; + let nwritten = io::Write::write(self.raw_file.file_mut(), &decompressed_cluster)?; + if nwritten != decompressed_cluster.len() { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + self.deallocate_compressed_cluster(l2_entry)?; + cluster_addr + } else if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) { + let cluster_addr = self.append_data_cluster(backing_data)?; + self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; + cluster_addr + } else { + // Already allocated - validate alignment + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + if cluster_addr & (self.raw_file.cluster_size() - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + cluster_addr + }; + + // Apply deferred refcount updates + for (addr, refcount) in set_refcounts { + self.set_cluster_refcount_track_freed(addr, refcount)?; + } + + let intra_offset = self.raw_file.cluster_offset(address); + Ok(ClusterWriteMapping::Allocated { + offset: cluster_addr + intra_offset, + }) + } + + // -- Address computation helpers -- + + fn l1_table_index(&self, address: u64) -> u64 { + (address / self.raw_file.cluster_size()) / self.l2_entries + } + + fn l2_table_index(&self, address: u64) -> u64 { + (address / self.raw_file.cluster_size()) % self.l2_entries + } + + // -- Cache and allocation operations requiring exclusive access -- + + /// Populates the L2 cache for read operations without allocation. + fn cache_l2_cluster(&mut self, l1_index: usize, l2_addr_disk: u64) -> io::Result<()> { + if !self.l2_cache.contains_key(l1_index) { + let cluster_size = self.raw_file.cluster_size(); + if l2_addr_disk & (cluster_size - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + let l2_table = + VecCache::from_vec(self.raw_file.read_pointer_cluster(l2_addr_disk, None)?); + let l1_table = &self.l1_table; + let raw_file = &mut self.raw_file; + self.l2_cache.insert(l1_index, l2_table, |index, evicted| { + raw_file.write_pointer_table_direct(l1_table[index], evicted.iter()) + })?; + } + Ok(()) + } + + /// Populates the L2 cache for write operations and may allocate a new + /// L2 table. Returns the address of the newly allocated cluster if any. + fn cache_l2_cluster_alloc( + &mut self, + l1_index: usize, + l2_addr_disk: u64, + ) -> io::Result> { + let mut new_cluster: Option = None; + if !self.l2_cache.contains_key(l1_index) { + let l2_table = if l2_addr_disk == 0 { + // Allocate a new cluster to store the L2 table + let new_addr = self.get_new_cluster(None)?; + new_cluster = Some(new_addr); + self.l1_table[l1_index] = new_addr; + VecCache::new(self.l2_entries as usize) + } else { + let cluster_size = self.raw_file.cluster_size(); + if l2_addr_disk & (cluster_size - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + VecCache::from_vec(self.raw_file.read_pointer_cluster(l2_addr_disk, None)?) + }; + let l1_table = &self.l1_table; + let raw_file = &mut self.raw_file; + self.l2_cache.insert(l1_index, l2_table, |index, evicted| { + raw_file.write_pointer_table_direct(l1_table[index], evicted.iter()) + })?; + } + Ok(new_cluster) + } + + /// Allocates a new cluster from the free list or by extending the file. + fn get_new_cluster(&mut self, initial_data: Option>) -> io::Result { + if let Some(free_cluster) = self.avail_clusters.pop() { + if free_cluster == 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + if let Some(initial_data) = initial_data { + self.raw_file.write_cluster(free_cluster, &initial_data)?; + } else { + self.raw_file.zero_cluster(free_cluster)?; + } + return Ok(free_cluster); + } + + let max_valid = self.refcounts.max_valid_cluster_offset(); + if let Some(new_cluster) = self.raw_file.add_cluster_end(max_valid)? { + if new_cluster == 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + if let Some(initial_data) = initial_data { + self.raw_file.write_cluster(new_cluster, &initial_data)?; + } + Ok(new_cluster) + } else { + log::error!("No free clusters in get_new_cluster()"); + Err(io::Error::from_raw_os_error(libc::ENOSPC)) + } + } + + /// Allocates a data cluster and sets its refcount to 1. + fn append_data_cluster(&mut self, initial_data: Option>) -> io::Result { + let new_addr = self.get_new_cluster(initial_data)?; + self.set_cluster_refcount_track_freed(new_addr, 1)?; + Ok(new_addr) + } + + /// Updates the L1 and L2 tables to point to a new cluster address. + fn update_cluster_addr( + &mut self, + l1_index: usize, + l2_index: usize, + cluster_addr: u64, + set_refcounts: &mut Vec<(u64, u64)>, + ) -> io::Result<()> { + if !self.l2_cache.get(l1_index).unwrap().dirty() { + // Free the previously used cluster if one exists. Modified tables are always + // written to new clusters so the L1 table can be committed to disk after they + // are and L1 never points at an invalid table. + let addr = self.l1_table[l1_index]; + if addr != 0 { + self.unref_clusters.push(addr); + set_refcounts.push((addr, 0)); + } + + // Allocate a new cluster to store the L2 table and update the L1 table to point + // to the new table. The cluster will be written when the cache is flushed. + let new_addr = self.get_new_cluster(None)?; + set_refcounts.push((new_addr, 1)); + self.l1_table[l1_index] = new_addr; // marks l1_table dirty via IndexMut + } + // Write the L2 entry - IndexMut marks the L2 table dirty automatically. + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = l2_entry_make_std(cluster_addr); + Ok(()) + } + + /// Resizes the image to the given new size. Only grow is supported, + /// shrink would require walking all L2 tables to reclaim clusters + /// beyond the new size and risks data loss. + fn resize(&mut self, new_size: u64) -> io::Result<()> { + let current_size = self.header.size; + + if new_size == current_size { + return Ok(()); + } + + if new_size < current_size { + return Err(io::Error::other("shrinking QCOW2 images is not supported")); + } + + let cluster_size = self.raw_file.cluster_size(); + let entries_per_cluster = cluster_size / size_of::() as u64; + let new_clusters = div_round_up_u64(new_size, cluster_size); + let needed_l1_entries = div_round_up_u64(new_clusters, entries_per_cluster) as u32; + + if needed_l1_entries > self.header.l1_size { + self.grow_l1_table(needed_l1_entries)?; + } + + self.header.size = new_size; + + self.raw_file.file_mut().rewind()?; + self.header + .write_to(self.raw_file.file_mut()) + .map_err(|e| io::Error::other(format!("failed to write header during resize: {e}")))?; + + self.raw_file.file_mut().sync_all()?; + + Ok(()) + } + + /// Grows the L1 table to accommodate at least the requested number of entries. + fn grow_l1_table(&mut self, new_l1_size: u32) -> io::Result<()> { + let old_l1_size = self.header.l1_size; + let old_l1_offset = self.header.l1_table_offset; + let cluster_size = self.raw_file.cluster_size(); + + let new_l1_bytes = new_l1_size as u64 * size_of::() as u64; + let new_l1_clusters = div_round_up_u64(new_l1_bytes, cluster_size); + + // Allocate contiguous clusters at file end for new L1 table + let file_size = self.raw_file.file_mut().seek(io::SeekFrom::End(0))?; + let new_l1_offset = self.raw_file.cluster_address(file_size + cluster_size - 1); + + let new_file_end = new_l1_offset + new_l1_clusters * cluster_size; + self.raw_file.file_mut().set_len(new_file_end)?; + + // Set refcounts for the contiguous range + for i in 0..new_l1_clusters { + self.set_cluster_refcount_track_freed(new_l1_offset + i * cluster_size, 1)?; + } + + let mut new_l1_data = vec![0u64; new_l1_size as usize]; + let old_entries = self.l1_table.get_values(); + new_l1_data[..old_entries.len()].copy_from_slice(old_entries); + + for l2_addr in new_l1_data.iter_mut() { + if *l2_addr != 0 { + let refcount = self + .refcounts + .get_cluster_refcount(&mut self.raw_file, *l2_addr) + .map_err(|e| { + io::Error::other(format!("failed to get refcount during resize: {e}")) + })?; + *l2_addr = l1_entry_make(*l2_addr, refcount == 1); + } + } + + // Write the new L1 table to disk + self.raw_file + .write_pointer_table_direct(new_l1_offset, new_l1_data.iter())?; + + self.raw_file.file_mut().sync_all()?; + + self.header.l1_size = new_l1_size; + self.header.l1_table_offset = new_l1_offset; + + self.raw_file.file_mut().rewind()?; + self.header + .write_to(self.raw_file.file_mut()) + .map_err(|e| io::Error::other(format!("failed to write header during resize: {e}")))?; + + self.raw_file.file_mut().sync_all()?; + + // Free old L1 table clusters + let old_l1_bytes = old_l1_size as u64 * size_of::() as u64; + let old_l1_clusters = div_round_up_u64(old_l1_bytes, cluster_size); + for i in 0..old_l1_clusters { + let cluster_addr = old_l1_offset + i * cluster_size; + // Best effort: the old L1 clusters are no longer reachable, + // so a refcount update failure just leaks space. + let _ = self.set_cluster_refcount(cluster_addr, 0); + } + + // Update L1 table cache + self.l1_table.extend(new_l1_size as usize); + + Ok(()) + } + + /// Deallocates a cluster at the given guest address. + /// + /// If sparse is true, fully deallocates and returns the host offset if + /// the underlying storage should be punched after the refcount dropped + /// to zero. If sparse is false, uses the zero flag optimization when + /// possible. + /// + /// Returns None if no host punch_hole is needed. + pub(super) fn deallocate_cluster( + &mut self, + address: u64, + sparse: bool, + ) -> io::Result> { + if address >= self.header.size { + return Err(io::Error::from_raw_os_error(EINVAL)); + } + + let l1_index = self.l1_table_index(address) as usize; + let l2_addr_disk = match self.l1_table.get(l1_index) { + Some(&addr) => addr, + None => return Err(io::Error::from_raw_os_error(EINVAL)), + }; + let l2_index = self.l2_table_index(address) as usize; + + if l2_addr_disk == 0 { + return Ok(None); + } + + self.cache_l2_cluster(l1_index, l2_addr_disk)?; + + let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; + if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) { + return Ok(None); + } + + if l2_entry_is_compressed(l2_entry) { + self.deallocate_compressed_cluster(l2_entry)?; + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; + return Ok(None); + } + + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + let refcount = self + .refcounts + .get_cluster_refcount(&mut self.raw_file, cluster_addr) + .map_err(|e| { + if matches!(e, refcount::Error::RefblockUnaligned(_)) { + self.set_corrupt_bit_best_effort(); + } + io::Error::new( + io::ErrorKind::InvalidData, + format!("failed to get cluster refcount: {e}"), + ) + })?; + if refcount == 0 { + return Err(io::Error::from_raw_os_error(EINVAL)); + } + + if sparse { + let new_refcount = refcount - 1; + self.set_cluster_refcount_track_freed(cluster_addr, new_refcount)?; + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; + if new_refcount == 0 { + self.unref_clusters.push(cluster_addr); + return Ok(Some(cluster_addr)); + } + } else if refcount == 1 { + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = l2_entry_make_zero(cluster_addr); + } else { + self.set_cluster_refcount_track_freed(cluster_addr, refcount - 1)?; + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; + } + Ok(None) + } + + /// Sets refcount for a cluster, tracking any newly freed clusters. + fn set_cluster_refcount_track_freed(&mut self, address: u64, refcount: u64) -> io::Result<()> { + let mut newly_unref = self.set_cluster_refcount(address, refcount)?; + self.unref_clusters.append(&mut newly_unref); + Ok(()) + } + + /// Sets the refcount for a cluster. Returns freed cluster addresses. + fn set_cluster_refcount(&mut self, address: u64, refcount: u64) -> io::Result> { + let mut added_clusters = Vec::new(); + let mut unref_clusters = Vec::new(); + let mut refcount_set = false; + let mut new_cluster = None; + + while !refcount_set { + match self.refcounts.set_cluster_refcount( + &mut self.raw_file, + address, + refcount, + new_cluster.take(), + ) { + Ok(None) => { + refcount_set = true; + } + Ok(Some(freed_cluster)) => { + let mut freed = self.set_cluster_refcount(freed_cluster, 0)?; + unref_clusters.append(&mut freed); + refcount_set = true; + } + Err(refcount::Error::EvictingRefCounts(e)) => { + return Err(e); + } + Err(refcount::Error::InvalidIndex) => { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EINVAL)); + } + Err(refcount::Error::NeedCluster(addr)) => { + new_cluster = Some(( + addr, + VecCache::from_vec(self.raw_file.read_refcount_block(addr)?), + )); + } + Err(refcount::Error::NeedNewCluster) => { + let addr = self.get_new_cluster(None)?; + added_clusters.push(addr); + new_cluster = Some(( + addr, + VecCache::new(self.refcounts.refcounts_per_block() as usize), + )); + } + Err(refcount::Error::ReadingRefCounts(e)) => { + return Err(e); + } + Err(refcount::Error::RefcountOverflow { .. }) => { + return Err(io::Error::from_raw_os_error(EINVAL)); + } + Err(refcount::Error::RefblockUnaligned(_)) => { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + } + } + + for addr in added_clusters { + self.set_cluster_refcount(addr, 1)?; + } + Ok(unref_clusters) + } + + /// Flushes all dirty metadata to disk. + pub(super) fn sync_caches(&mut self) -> io::Result<()> { + // Write out all dirty L2 tables. + for (l1_index, l2_table) in self.l2_cache.iter_mut().filter(|(_k, v)| v.dirty()) { + let addr = self.l1_table[*l1_index]; + if addr != 0 { + self.raw_file + .write_pointer_table_direct(addr, l2_table.iter())?; + } else { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EINVAL)); + } + l2_table.mark_clean(); + } + // Write the modified refcount blocks. + self.refcounts.flush_blocks(&mut self.raw_file)?; + // Sync metadata and data clusters. + self.raw_file.file_mut().sync_all()?; + + // Push L1 table and refcount table last. + let mut sync_required = if self.l1_table.dirty() { + let refcounts = &mut self.refcounts; + self.raw_file.write_pointer_table( + self.header.l1_table_offset, + self.l1_table.iter(), + |raw_file, l2_addr| { + if l2_addr == 0 { + Ok(0) + } else { + let refcount = refcounts + .get_cluster_refcount(raw_file, l2_addr) + .map_err(|e| io::Error::other(super::Error::GettingRefcount(e)))?; + Ok(l1_entry_make(l2_addr, refcount == 1)) + } + }, + )?; + self.l1_table.mark_clean(); + true + } else { + false + }; + sync_required |= self.refcounts.flush_table(&mut self.raw_file)?; + if sync_required { + self.raw_file.file_mut().sync_data()?; + } + + Ok(()) + } + + /// Decompresses a compressed cluster, returning the raw decompressed bytes. + fn decompress_l2_cluster(&mut self, l2_entry: u64) -> io::Result> { + let (compressed_addr, compressed_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + self.raw_file + .file_mut() + .seek(io::SeekFrom::Start(compressed_addr))?; + let mut compressed = vec![0u8; compressed_size]; + io::Read::read_exact(self.raw_file.file_mut(), &mut compressed)?; + let decoder = self.header.get_decoder(); + let cluster_size = self.raw_file.cluster_size() as usize; + let mut decompressed = vec![0u8; cluster_size]; + let decompressed_size = decoder + .decode(&compressed, &mut decompressed) + .map_err(|_| { + self.set_corrupt_bit_best_effort(); + io::Error::from_raw_os_error(EIO) + })?; + if decompressed_size as u64 != self.raw_file.cluster_size() { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + Ok(decompressed) + } + + /// Deallocates the clusters spanned by a compressed L2 entry. + fn deallocate_compressed_cluster(&mut self, l2_entry: u64) -> io::Result<()> { + let (compressed_addr, compressed_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + let cluster_size = self.raw_file.cluster_size(); + + // Calculate the end of the compressed data region + let compressed_clusters_end = self.raw_file.cluster_address( + compressed_addr // Start of compressed data + + compressed_size as u64 // Add size to get end address + + cluster_size + - 1, // Catch possibly partially used last cluster + ); + + // Decrement refcount for each cluster spanned by the compressed data + let mut addr = self.raw_file.cluster_address(compressed_addr); + while addr < compressed_clusters_end { + let refcount = self + .refcounts + .get_cluster_refcount(&mut self.raw_file, addr) + .map_err(|e| { + if matches!(e, refcount::Error::RefblockUnaligned(_)) { + self.set_corrupt_bit_best_effort(); + } + io::Error::new( + io::ErrorKind::InvalidData, + format!("failed to get cluster refcount: {e}"), + ) + })?; + if refcount > 0 { + self.set_cluster_refcount_track_freed(addr, refcount - 1)?; + } + addr += cluster_size; + } + Ok(()) + } + + /// Best effort attempt to mark the image corrupt. + fn set_corrupt_bit_best_effort(&mut self) { + if let Err(e) = self.header.set_corrupt_bit(self.raw_file.file_mut()) { + log::warn!("Failed to persist corrupt bit: {e}"); + } + } +} diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index 6d74232ddf..b9a63d1f95 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -4,46 +4,74 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -mod qcow_raw_file; +pub(crate) mod backing; +pub(crate) mod decoder; +mod header; +pub(crate) mod metadata; +pub(crate) mod qcow_raw_file; mod raw_file; mod refcount; +mod util; mod vec_cache; use std::cmp::{max, min}; -use std::fs::OpenOptions; +use std::fmt::{Debug, Formatter, Result as FmtResult}; +use std::fs::{OpenOptions, read_link}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::mem::size_of; use std::os::fd::{AsRawFd, RawFd}; use std::str; -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; -use libc::{EINVAL, ENOSPC, ENOTSUP}; +#[cfg(test)] +use header::{ + AUTOCLEAR_FEATURES_OFFSET, DEFAULT_REFCOUNT_ORDER, HEADER_EXT_BACKING_FORMAT, HEADER_EXT_END, + V2_BARE_HEADER_SIZE, V3_BARE_HEADER_SIZE, +}; +pub use header::{ + BackingFileConfig, CompressionType, ImageType, IncompatFeatures, MissingFeatureError, + QcowHeader, +}; +use header::{ + COMPATIBLE_FEATURES_LAZY_REFCOUNTS, MAX_CLUSTER_BITS, MAX_QCOW_FILE_SIZE, + MAX_RAM_POINTER_TABLE_SIZE, MIN_CLUSTER_BITS, QCOW_MAGIC, max_refcount_clusters, + offset_is_cluster_boundary, +}; +use libc::{EINVAL, EIO, ENOSPC}; +use log::{error, warn}; +use metadata::ClusterReadMapping; use remain::sorted; use thiserror::Error; +pub(crate) use util::MAX_NESTING_DEPTH; +use util::{ + L1_TABLE_OFFSET_MASK, L2_TABLE_OFFSET_MASK, div_round_up_u32, div_round_up_u64, l1_entry_make, + l2_entry_compressed_cluster_layout, l2_entry_is_compressed, l2_entry_is_empty, + l2_entry_is_zero, l2_entry_make_std, l2_entry_make_zero, l2_entry_std_cluster_addr, +}; use vmm_sys_util::file_traits::{FileSetLen, FileSync}; use vmm_sys_util::seek_hole::SeekHole; use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; -use crate::qcow::qcow_raw_file::QcowRawFile; +use crate::BlockBackend; +use crate::error::{BlockError, BlockErrorKind, BlockResult}; +use crate::qcow::qcow_raw_file::{BeUint, QcowRawFile}; pub use crate::qcow::raw_file::RawFile; use crate::qcow::refcount::RefCount; use crate::qcow::vec_cache::{CacheMap, Cacheable, VecCache}; -use crate::BlockBackend; - -/// Nesting depth limit for disk formats that can open other disk files. -const MAX_NESTING_DEPTH: u32 = 10; +use crate::qcow_common::decompress_cluster; #[sorted] #[derive(Debug, Error)] pub enum Error { - #[error("Backing file io error")] - BackingFileIo(#[source] io::Error), - #[error("Backing file open error")] - BackingFileOpen(#[source] Box), + #[error("Backing file I/O error: {0}")] + BackingFileIo(String /* path */, #[source] io::Error), + #[error("Backing file open error: {0}")] + BackingFileOpen(String /* path */, #[source] Box), + #[error("Backing file support is disabled")] + BackingFilesDisabled, #[error("Backing file name is too long: {0} bytes over")] BackingFileTooLong(usize), - #[error("Compressed blocks not supported")] - CompressedBlocksNotSupported, + #[error("Image is marked corrupt and cannot be opened for writing")] + CorruptImage, #[error("Failed to evict cache")] EvictingCache(#[source] io::Error), #[error("File larger than max of {MAX_QCOW_FILE_SIZE}: {0}")] @@ -94,22 +122,38 @@ pub enum Error { ReadingRefCounts(#[source] io::Error), #[error("Failed to rebuild ref counts")] RebuildingRefCounts(#[source] io::Error), + #[error("Refcount overflow")] + RefcountOverflow(#[source] refcount::Error), #[error("Refcount table offset past file end")] RefcountTableOffEnd, #[error("Too many clusters specified for refcount")] RefcountTableTooLarge, + #[error("Failed to resize")] + ResizeIo(#[source] io::Error), + #[error("Resize not supported with backing file")] + ResizeWithBackingFile, #[error("Failed to seek file")] SeekingFile(#[source] io::Error), #[error("Failed to set file size")] SettingFileSize(#[source] io::Error), #[error("Failed to set refcount refcount")] SettingRefcountRefcount(#[source] io::Error), + #[error("Shrinking QCOW images is not supported")] + ShrinkNotSupported, #[error("Size too small for number of clusters")] SizeTooSmallForNumberOfClusters, + #[error("Failed to sync header")] + SyncingHeader(#[source] io::Error), #[error("L1 entry table too large: {0}")] TooManyL1Entries(u64), #[error("Ref count table too large: {0}")] TooManyRefcounts(u64), + #[error("Unsupported backing file format: {0}")] + UnsupportedBackingFileFormat(String), + #[error("Unsupported compression type")] + UnsupportedCompressionType, + #[error("Unsupported qcow2 feature(s)")] + UnsupportedFeature(#[source] MissingFeatureError), #[error("Unsupported refcount order")] UnsupportedRefcountOrder, #[error("Unsupported version: {0}")] @@ -122,290 +166,480 @@ pub enum Error { pub type Result = std::result::Result; -pub enum ImageType { - Raw, - Qcow2, +/// Concrete backing file variants. +pub(crate) enum BackingKind { + /// Raw backing file. + Raw(RawFile), + /// QCOW2 backing parsed into metadata and raw file. + Qcow { + inner: Box, + backing: Option>, + }, + /// Full QcowFile used as backing, only in tests. + #[cfg(test)] + QcowFile(Box), } - -// Maximum data size supported. -const MAX_QCOW_FILE_SIZE: u64 = 0x01 << 44; // 16 TB. - -// QCOW magic constant that starts the header. -const QCOW_MAGIC: u32 = 0x5146_49fb; -// Default to a cluster size of 2^DEFAULT_CLUSTER_BITS -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. -const MIN_CLUSTER_BITS: u32 = 9; -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. -const MAX_RAM_POINTER_TABLE_SIZE: u64 = 35_000_000; -// Only support 2 byte refcounts, 2^refcount_order bits. -const DEFAULT_REFCOUNT_ORDER: u32 = 4; - -const V2_BARE_HEADER_SIZE: u32 = 72; -const V3_BARE_HEADER_SIZE: u32 = 104; - -// bits 0-8 and 56-63 are reserved. -const L1_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00; -const L2_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00; -// Flags -const COMPRESSED_FLAG: u64 = 1 << 62; -const CLUSTER_USED_FLAG: u64 = 1 << 63; -const COMPATIBLE_FEATURES_LAZY_REFCOUNTS: u64 = 1; - -// 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, - - // Post-header entries - pub backing_file_path: Option, +/// Backing file wrapper +pub(crate) struct BackingFile { + kind: BackingKind, + virtual_size: u64, } -impl QcowHeader { - /// Creates a QcowHeader from a reference to a file. - pub fn new(f: &mut RawFile) -> Result { - f.rewind().map_err(Error::ReadingHeader)?; - let magic = f.read_u32::().map_err(Error::ReadingHeader)?; - if magic != QCOW_MAGIC { - return Err(Error::InvalidMagic); - } +impl BackingFile { + fn new( + backing_file_config: Option<&BackingFileConfig>, + direct_io: bool, + max_nesting_depth: u32, + sparse: bool, + ) -> BlockResult> { + let Some(config) = backing_file_config else { + return Ok(None); + }; - // Reads the next u32 from the file. - fn read_u32_from_file(f: &mut RawFile) -> Result { - f.read_u32::().map_err(Error::ReadingHeader) + // Check nesting depth - applies to any backing file + if max_nesting_depth == 0 { + return Err(BlockError::new( + BlockErrorKind::Overflow, + Error::MaxNestingDepthExceeded, + )); } - // Reads the next u64 from the file. - fn read_u64_from_file(f: &mut RawFile) -> Result { - f.read_u64::().map_err(Error::ReadingHeader) - } + let backing_raw_file = OpenOptions::new() + .read(true) + .open(&config.path) + .map_err(|e| { + BlockError::new( + BlockErrorKind::Io, + Error::BackingFileIo(config.path.clone(), e), + ) + })?; - let version = read_u32_from_file(f)?; + let mut raw_file = RawFile::new(backing_raw_file, direct_io); - let mut header = QcowHeader { - magic, - version, - backing_file_offset: read_u64_from_file(f)?, - backing_file_size: read_u32_from_file(f)?, - cluster_bits: read_u32_from_file(f)?, - size: read_u64_from_file(f)?, - crypt_method: read_u32_from_file(f)?, - l1_size: read_u32_from_file(f)?, - l1_table_offset: read_u64_from_file(f)?, - refcount_table_offset: read_u64_from_file(f)?, - refcount_table_clusters: read_u32_from_file(f)?, - nb_snapshots: read_u32_from_file(f)?, - snapshots_offset: read_u64_from_file(f)?, - incompatible_features: if version == 2 { - 0 - } else { - read_u64_from_file(f)? - }, - compatible_features: if version == 2 { - 0 - } else { - read_u64_from_file(f)? - }, - autoclear_features: if version == 2 { - 0 - } else { - read_u64_from_file(f)? - }, - refcount_order: if version == 2 { - DEFAULT_REFCOUNT_ORDER - } else { - read_u32_from_file(f)? - }, - header_size: if version == 2 { - V2_BARE_HEADER_SIZE - } else { - read_u32_from_file(f)? - }, - backing_file_path: None, + // Determine backing file format from header extension or auto-detect + let backing_format = match config.format { + Some(format) => format, + None => detect_image_type(&mut raw_file)?, }; - 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 { - f.seek(SeekFrom::Start(header.backing_file_offset)) - .map_err(Error::ReadingHeader)?; - let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize]; - f.read_exact(&mut backing_file_name_bytes) - .map_err(Error::ReadingHeader)?; - header.backing_file_path = Some( - String::from_utf8(backing_file_name_bytes) - .map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?, - ); - } - Ok(header) + + let (kind, virtual_size) = match backing_format { + ImageType::Raw => { + let size = raw_file.seek(SeekFrom::End(0)).map_err(|e| { + BlockError::new( + BlockErrorKind::Io, + Error::BackingFileIo(config.path.clone(), e), + ) + })?; + raw_file.rewind().map_err(|e| { + BlockError::new( + BlockErrorKind::Io, + Error::BackingFileIo(config.path.clone(), e), + ) + })?; + (BackingKind::Raw(raw_file), size) + } + ImageType::Qcow2 => { + let (inner, nested_backing, _sparse) = + parse_qcow(raw_file, max_nesting_depth - 1, sparse).map_err(|e| { + let kind = e.kind(); + let source = e + .into_source() + .and_then(|s| s.downcast::().ok()) + .map(|qcow_err| Error::BackingFileOpen(config.path.clone(), qcow_err)); + match source { + Some(err) => BlockError::new(kind, err), + None => BlockError::from_kind(kind), + } + })?; + let size = inner.header.size; + ( + BackingKind::Qcow { + inner: Box::new(inner), + backing: nested_backing.map(Box::new), + }, + size, + ) + } + }; + + Ok(Some(Self { kind, virtual_size })) } - pub fn create_for_size_and_path( - version: u32, - size: u64, - backing_file: Option<&str>, - ) -> Result { - let header_size = if version == 2 { - V2_BARE_HEADER_SIZE + /// Consume and return the kind and virtual size. + pub(crate) fn into_kind(self) -> (BackingKind, u64) { + (self.kind, self.virtual_size) + } + + /// Read from backing file, returning zeros for any portion beyond backing file size. + #[inline] + pub(crate) fn read_at(&mut self, address: u64, buf: &mut [u8]) -> std::io::Result<()> { + if address >= self.virtual_size { + buf.fill(0); + return Ok(()); + } + + let available = (self.virtual_size - address) as usize; + let (target, overflow) = if available >= buf.len() { + (buf, &mut [][..]) } else { - V3_BARE_HEADER_SIZE + QCOW_EMPTY_HEADER_EXTENSION_SIZE + buf.split_at_mut(available) }; - 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 { - if path.len() > max_length { - return Err(Error::BackingFileTooLong(path.len() - max_length)); + Self::read_at_inner(&mut self.kind, address, target)?; + overflow.fill(0); + Ok(()) + } + + fn read_at_inner(kind: &mut BackingKind, address: u64, buf: &mut [u8]) -> std::io::Result<()> { + match kind { + BackingKind::Raw(file) => { + file.seek(SeekFrom::Start(address))?; + file.read_exact(buf) + } + #[cfg(test)] + BackingKind::QcowFile(qcow) => { + qcow.seek(SeekFrom::Start(address))?; + qcow.read_exact(buf) + } + BackingKind::Qcow { inner, backing } => { + let has_backing = backing.is_some(); + let cluster_size = inner.raw_file.cluster_size(); + let mut pos = 0usize; + while pos < buf.len() { + let curr_addr = address + pos as u64; + let intra = inner.raw_file.cluster_offset(curr_addr) as usize; + let count = min(buf.len() - pos, cluster_size as usize - intra); + let mapping = inner.map_cluster_read(curr_addr, count, has_backing)?; + match mapping { + ClusterReadMapping::Zero { length } => { + buf[pos..pos + length as usize].fill(0); + } + ClusterReadMapping::Allocated { + offset: host_off, + length, + } => { + inner.raw_file.file_mut().seek(SeekFrom::Start(host_off))?; + inner + .raw_file + .file_mut() + .read_exact(&mut buf[pos..pos + length as usize])?; + } + ClusterReadMapping::Compressed { + host_offset, + compressed_size, + cluster_offset, + length, + } => { + let mut compressed = vec![0u8; compressed_size]; + inner + .raw_file + .file_mut() + .seek(SeekFrom::Start(host_offset))?; + inner.raw_file.file_mut().read_exact(&mut compressed)?; + let decompressed = decompress_cluster( + &compressed, + cluster_size as usize, + &*inner.header.get_decoder(), + )?; + buf[pos..pos + length].copy_from_slice( + &decompressed[cluster_offset..cluster_offset + length], + ); + } + ClusterReadMapping::Backing { + offset: backing_off, + length, + } => { + if let Some(bf) = backing.as_mut() { + bf.read_at(backing_off, &mut buf[pos..pos + length as usize])?; + } else { + buf[pos..pos + length as usize].fill(0); + } + } + } + pos += count; + } + Ok(()) } } - // L2 blocks are always one cluster long. They contain cluster_size/sizeof(u64) addresses. - let entries_per_cluster: u32 = cluster_size / size_of::() 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::() as u32, cluster_size); - Ok(QcowHeader { - magic: QCOW_MAGIC, - version, - backing_file_offset: (if backing_file.is_none() { - 0 - } else { - header_size - }) 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::() 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, - backing_file_path: backing_file.map(String::from), - }) } +} - /// Write the header to `file`. - pub fn write_to(&self, file: &mut F) -> Result<()> { - // Writes the next u32 to the file. - fn write_u32_to_file(f: &mut F, value: u32) -> Result<()> { - f.write_u32::(value) - .map_err(Error::WritingHeader) - } - - // Writes the next u64 to the file. - fn write_u64_to_file(f: &mut F, value: u64) -> Result<()> { - f.write_u64::(value) - .map_err(Error::WritingHeader) - } - - write_u32_to_file(file, self.magic)?; - write_u32_to_file(file, self.version)?; - write_u64_to_file(file, self.backing_file_offset)?; - write_u32_to_file(file, self.backing_file_size)?; - write_u32_to_file(file, self.cluster_bits)?; - write_u64_to_file(file, self.size)?; - write_u32_to_file(file, self.crypt_method)?; - write_u32_to_file(file, self.l1_size)?; - write_u64_to_file(file, self.l1_table_offset)?; - write_u64_to_file(file, self.refcount_table_offset)?; - write_u32_to_file(file, self.refcount_table_clusters)?; - write_u32_to_file(file, self.nb_snapshots)?; - write_u64_to_file(file, self.snapshots_offset)?; - - if self.version == 3 { - write_u64_to_file(file, self.incompatible_features)?; - write_u64_to_file(file, self.compatible_features)?; - write_u64_to_file(file, self.autoclear_features)?; - write_u32_to_file(file, self.refcount_order)?; - write_u32_to_file(file, self.header_size)?; - write_u32_to_file(file, 0)?; // header extension type: end of header extension area - write_u32_to_file(file, 0)?; // length of header extension data: 0 - } - - if let Some(backing_file_path) = self.backing_file_path.as_ref() { - write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?; - } - - // Set the file length by seeking and writing a zero to the last byte. This avoids needing - // a `File` instead of anything that implements seek as the `file` argument. - // Zeros out the l1 and refcount table clusters. - let cluster_size = 0x01u64 << self.cluster_bits; - let refcount_blocks_size = u64::from(self.refcount_table_clusters) * cluster_size; - file.seek(SeekFrom::Start( - self.refcount_table_offset + refcount_blocks_size - 2, - )) - .map_err(Error::WritingHeader)?; - file.write(&[0u8]).map_err(Error::WritingHeader)?; - - Ok(()) +impl Debug for BackingFile { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("BackingFile").finish() } } -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_bytes = (0x01 << u64::from(refcount_order)) / 8; - let for_data = div_round_up_u64( - u64::from(num_clusters) * refcount_bytes, - u64::from(cluster_size), +/// Parses and validates a QCOW2 image file, returning the metadata, backing +/// file and sparse flag. +/// +/// Used by [`QcowFile`] and [`QcowDisk`] constructors. +pub(crate) fn parse_qcow( + mut file: RawFile, + max_nesting_depth: u32, + sparse: bool, +) -> BlockResult<(metadata::QcowState, Option, bool)> { + let mut header = QcowHeader::new(&mut file).map_err(|e| { + let kind = match &e { + Error::InvalidMagic + | Error::BackingFileTooLong(_) + | Error::InvalidBackingFileName(_) => BlockErrorKind::InvalidFormat, + Error::UnsupportedFeature(_) | Error::UnsupportedCompressionType => { + BlockErrorKind::UnsupportedFeature + } + _ => BlockErrorKind::Io, + }; + BlockError::new(kind, e) + })?; + + // Only v2 and v3 files are supported. + if header.version != 2 && header.version != 3 { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + Error::UnsupportedVersion(header.version), + )); + } + + // Make sure that the L1 table fits in RAM. + if u64::from(header.l1_size) > MAX_RAM_POINTER_TABLE_SIZE { + return Err(BlockError::new( + BlockErrorKind::InvalidFormat, + Error::InvalidL1TableSize(header.l1_size), + )); + } + + let cluster_bits: u32 = header.cluster_bits; + if !(MIN_CLUSTER_BITS..=MAX_CLUSTER_BITS).contains(&cluster_bits) { + return Err(BlockError::new( + BlockErrorKind::InvalidFormat, + Error::InvalidClusterSize, + )); + } + let cluster_size = 0x01u64 << cluster_bits; + + // Limit the total size of the disk. + if header.size > MAX_QCOW_FILE_SIZE { + return Err(BlockError::new( + BlockErrorKind::InvalidFormat, + Error::FileTooBig(header.size), + )); + } + + let direct_io = file.is_direct(); + + let backing_file = BackingFile::new( + header.backing_file.as_ref(), + direct_io, + max_nesting_depth, + sparse, + )?; + + // Validate refcount order to be 0..6 + let refcount_bits: u64 = 0x01u64.checked_shl(header.refcount_order).ok_or_else(|| { + BlockError::new( + BlockErrorKind::UnsupportedFeature, + Error::UnsupportedRefcountOrder, + ) + })?; + if refcount_bits > 64 { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + Error::UnsupportedRefcountOrder, + )); + } + + // Need at least one refcount cluster + if header.refcount_table_clusters == 0 { + return Err(BlockError::new( + BlockErrorKind::InvalidFormat, + Error::NoRefcountClusters, + )); + } + offset_is_cluster_boundary(header.l1_table_offset, header.cluster_bits) + .map_err(|e| BlockError::new(BlockErrorKind::CorruptImage, e))?; + offset_is_cluster_boundary(header.snapshots_offset, header.cluster_bits) + .map_err(|e| BlockError::new(BlockErrorKind::CorruptImage, e))?; + // refcount table must be a cluster boundary, and within the file's virtual or actual size. + offset_is_cluster_boundary(header.refcount_table_offset, header.cluster_bits) + .map_err(|e| BlockError::new(BlockErrorKind::CorruptImage, e))?; + let file_size = file + .metadata() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))? + .len(); + if header.refcount_table_offset > max(file_size, header.size) { + return Err(BlockError::new( + BlockErrorKind::CorruptImage, + Error::RefcountTableOffEnd, + )); + } + + // The first cluster should always have a non-zero refcount, so if it is 0, + // this is an old file with broken refcounts, which requires a rebuild. + let mut refcount_rebuild_required = true; + file.seek(SeekFrom::Start(header.refcount_table_offset)) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + let first_refblock_addr = u64::read_be(&mut file) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?; + if first_refblock_addr != 0 { + file.seek(SeekFrom::Start(first_refblock_addr)) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + let first_cluster_refcount = u16::read_be(&mut file) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?; + if first_cluster_refcount != 0 { + refcount_rebuild_required = false; + } + } + + if (header.compatible_features & COMPATIBLE_FEATURES_LAZY_REFCOUNTS) != 0 { + refcount_rebuild_required = true; + } + + let mut raw_file = QcowRawFile::from(file, cluster_size, refcount_bits) + .ok_or_else(|| BlockError::new(BlockErrorKind::InvalidFormat, Error::InvalidClusterSize))?; + let is_writable = raw_file.file().is_writable(); + + if header.is_corrupt() { + if is_writable { + return Err(BlockError::new( + BlockErrorKind::CorruptImage, + Error::CorruptImage, + )); + } + let path = read_link(format!("/proc/self/fd/{}", raw_file.file().as_raw_fd())) + .map_or_else(|_| "".to_string(), |p| p.display().to_string()); + warn!("QCOW2 image is marked corrupt, opening read-only: {path}"); + } + + // Image already has dirty bit set. Refcounts may be invalid. + if IncompatFeatures::from_bits_truncate(header.incompatible_features) + .contains(IncompatFeatures::DIRTY) + { + log::warn!("QCOW2 image not cleanly closed, rebuilding refcounts"); + refcount_rebuild_required = true; + } + + // Skip refcount rebuilding for readonly files. + if refcount_rebuild_required && is_writable { + QcowFile::rebuild_refcounts(&mut raw_file, header.clone())?; + } + + let entries_per_cluster = cluster_size / size_of::() as u64; + let num_clusters = div_round_up_u64(header.size, cluster_size); + let num_l2_clusters = div_round_up_u64(num_clusters, entries_per_cluster); + let l1_clusters = div_round_up_u64(num_l2_clusters, entries_per_cluster); + let header_clusters = div_round_up_u64(size_of::() as u64, cluster_size); + if num_l2_clusters > MAX_RAM_POINTER_TABLE_SIZE { + return Err(BlockError::new( + BlockErrorKind::CorruptImage, + Error::TooManyL1Entries(num_l2_clusters), + )); + } + let l1_table = VecCache::from_vec( + raw_file + .read_pointer_table( + header.l1_table_offset, + num_l2_clusters, + Some(L1_TABLE_OFFSET_MASK), + ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?, ); - let for_refcounts = div_round_up_u64(for_data * refcount_bytes, u64::from(cluster_size)); - for_data + for_refcounts + + let num_clusters = div_round_up_u64(header.size, cluster_size); + let refcount_clusters = max_refcount_clusters( + header.refcount_order, + cluster_size as u32, + (num_clusters + l1_clusters + num_l2_clusters + header_clusters) as u32, + ); + // Check that the given header doesn't have a suspiciously sized refcount table. + if u64::from(header.refcount_table_clusters) > 2 * refcount_clusters { + return Err(BlockError::new( + BlockErrorKind::CorruptImage, + Error::RefcountTableTooLarge, + )); + } + if l1_clusters + refcount_clusters > MAX_RAM_POINTER_TABLE_SIZE { + return Err(BlockError::new( + BlockErrorKind::InvalidFormat, + Error::TooManyRefcounts(refcount_clusters), + )); + } + let refcount_block_entries = cluster_size * 8 / refcount_bits; + let mut refcounts = RefCount::new( + &mut raw_file, + header.refcount_table_offset, + refcount_clusters, + refcount_block_entries, + cluster_size, + refcount_bits, + ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingRefCounts(e)))?; + + let l2_entries = cluster_size / size_of::() as u64; + + // Check that the L1 and refcount tables fit in a 64bit address space. + let l1_index = (header.size / cluster_size) / l2_entries; + header + .l1_table_offset + .checked_add(l1_index * size_of::() as u64) + .ok_or_else(|| { + BlockError::new(BlockErrorKind::CorruptImage, Error::InvalidL1TableOffset) + })?; + header + .refcount_table_offset + .checked_add(u64::from(header.refcount_table_clusters) * cluster_size) + .ok_or_else(|| { + BlockError::new( + BlockErrorKind::CorruptImage, + Error::InvalidRefcountTableOffset, + ) + })?; + + // Find available (refcount == 0) clusters for the free list. + let file_size = raw_file + .file_mut() + .metadata() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))? + .len(); + let mut avail_clusters = Vec::new(); + for i in (0..file_size).step_by(cluster_size as usize) { + let refcount = refcounts + .get_cluster_refcount(&mut raw_file, i) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingRefcount(e)))?; + if refcount == 0 { + avail_clusters.push(i); + } + } + + if is_writable { + if !IncompatFeatures::from_bits_truncate(header.incompatible_features) + .contains(IncompatFeatures::DIRTY) + { + header + .set_dirty_bit(raw_file.file_mut(), true) + .map_err(|e| { + BlockError::new( + BlockErrorKind::Io, + Error::WritingHeader(io::Error::other(e)), + ) + })?; + } + + header + .clear_autoclear_features(raw_file.file_mut()) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + } + + let inner = metadata::QcowState { + raw_file, + header, + l1_table, + l2_entries, + l2_cache: CacheMap::new(100), + refcounts, + avail_clusters, + unref_clusters: Vec::new(), + }; + + Ok((inner, backing_file, sparse)) } /// Represents a qcow2 file. This is a sparse file format maintained by the qemu project. @@ -438,231 +672,108 @@ pub struct QcowFile { // List of unreferenced clusters available to be used. unref clusters become available once the // removal of references to them have been synced to disk. avail_clusters: Vec, - backing_file: Option>, + backing_file: Option, + sparse: bool, } impl QcowFile { /// Creates a QcowFile from `file`. File must be a valid qcow2 image. /// /// Additionally, max nesting depth of this qcow2 image will be set to default value 10. - pub fn from(file: RawFile) -> Result { - Self::from_with_nesting_depth(file, MAX_NESTING_DEPTH) + pub fn from(file: RawFile) -> BlockResult { + Self::from_with_nesting_depth(file, MAX_NESTING_DEPTH, true) } /// Creates a QcowFile from `file` and with a max nesting depth. File must be a valid qcow2 /// image. - pub fn from_with_nesting_depth(mut file: RawFile, max_nesting_depth: u32) -> Result { - let header = QcowHeader::new(&mut file)?; - - // Only v2 and v3 files are supported. - if header.version != 2 && header.version != 3 { - return Err(Error::UnsupportedVersion(header.version)); - } - - // Make sure that the L1 table fits in RAM. - if u64::from(header.l1_size) > MAX_RAM_POINTER_TABLE_SIZE { - return Err(Error::InvalidL1TableSize(header.l1_size)); - } - - let cluster_bits: u32 = header.cluster_bits; - if !(MIN_CLUSTER_BITS..=MAX_CLUSTER_BITS).contains(&cluster_bits) { - return Err(Error::InvalidClusterSize); - } - let cluster_size = 0x01u64 << cluster_bits; - - // Limit the total size of the disk. - if header.size > MAX_QCOW_FILE_SIZE { - return Err(Error::FileTooBig(header.size)); - } - - let direct_io = file.is_direct(); - - let backing_file = if let Some(backing_file_path) = header.backing_file_path.as_ref() { - if max_nesting_depth == 0 { - return Err(Error::MaxNestingDepthExceeded); - } - let path = backing_file_path.clone(); - let backing_raw_file = OpenOptions::new() - .read(true) - .open(path) - .map_err(Error::BackingFileIo)?; - let backing_file = Self::from_with_nesting_depth( - RawFile::new(backing_raw_file, direct_io), - max_nesting_depth - 1, - ) - .map_err(|e| Error::BackingFileOpen(Box::new(e)))?; - Some(Box::new(backing_file)) - } else { - None - }; - - // Only support two byte refcounts. - let refcount_bits: u64 = 0x01u64 - .checked_shl(header.refcount_order) - .ok_or(Error::UnsupportedRefcountOrder)?; - if refcount_bits != 16 { - return Err(Error::UnsupportedRefcountOrder); - } - let refcount_bytes = refcount_bits.div_ceil(8); - - // Need at least one refcount cluster - if header.refcount_table_clusters == 0 { - return Err(Error::NoRefcountClusters); - } - offset_is_cluster_boundary(header.l1_table_offset, header.cluster_bits)?; - offset_is_cluster_boundary(header.snapshots_offset, header.cluster_bits)?; - // refcount table must be a cluster boundary, and within the file's virtual or actual size. - offset_is_cluster_boundary(header.refcount_table_offset, header.cluster_bits)?; - let file_size = file.metadata().map_err(Error::GettingFileSize)?.len(); - if header.refcount_table_offset > max(file_size, header.size) { - return Err(Error::RefcountTableOffEnd); - } - - // The first cluster should always have a non-zero refcount, so if it is 0, - // this is an old file with broken refcounts, which requires a rebuild. - let mut refcount_rebuild_required = true; - file.seek(SeekFrom::Start(header.refcount_table_offset)) - .map_err(Error::SeekingFile)?; - let first_refblock_addr = file.read_u64::().map_err(Error::ReadingHeader)?; - if first_refblock_addr != 0 { - file.seek(SeekFrom::Start(first_refblock_addr)) - .map_err(Error::SeekingFile)?; - let first_cluster_refcount = - file.read_u16::().map_err(Error::ReadingHeader)?; - if first_cluster_refcount != 0 { - refcount_rebuild_required = false; - } - } - - if (header.compatible_features & COMPATIBLE_FEATURES_LAZY_REFCOUNTS) != 0 { - refcount_rebuild_required = true; - } - - let mut raw_file = - QcowRawFile::from(file, cluster_size).ok_or(Error::InvalidClusterSize)?; - if refcount_rebuild_required { - QcowFile::rebuild_refcounts(&mut raw_file, header.clone())?; - } - - let entries_per_cluster = cluster_size / size_of::() as u64; - let num_clusters = div_round_up_u64(header.size, cluster_size); - let num_l2_clusters = div_round_up_u64(num_clusters, entries_per_cluster); - let l1_clusters = div_round_up_u64(num_l2_clusters, entries_per_cluster); - let header_clusters = div_round_up_u64(size_of::() as u64, cluster_size); - if num_l2_clusters > MAX_RAM_POINTER_TABLE_SIZE { - return Err(Error::TooManyL1Entries(num_l2_clusters)); - } - let l1_table = VecCache::from_vec( - raw_file - .read_pointer_table( - header.l1_table_offset, - num_l2_clusters, - Some(L1_TABLE_OFFSET_MASK), - ) - .map_err(Error::ReadingHeader)?, - ); - - let num_clusters = div_round_up_u64(header.size, cluster_size); - let refcount_clusters = max_refcount_clusters( - header.refcount_order, - cluster_size as u32, - (num_clusters + l1_clusters + num_l2_clusters + header_clusters) as u32, - ); - // Check that the given header doesn't have a suspiciously sized refcount table. - if u64::from(header.refcount_table_clusters) > 2 * refcount_clusters { - return Err(Error::RefcountTableTooLarge); - } - if l1_clusters + refcount_clusters > MAX_RAM_POINTER_TABLE_SIZE { - return Err(Error::TooManyRefcounts(refcount_clusters)); - } - let refcount_block_entries = cluster_size / refcount_bytes; - let refcounts = RefCount::new( - &mut raw_file, - header.refcount_table_offset, - refcount_clusters, - refcount_block_entries, - cluster_size, - ) - .map_err(Error::ReadingRefCounts)?; - - let l2_entries = cluster_size / size_of::() as u64; - - // Check for compressed blocks - for l2_addr_disk in l1_table.get_values() { - if *l2_addr_disk != 0 { - if let Err(e) = Self::read_l2_cluster(&mut raw_file, *l2_addr_disk) { - if let Some(os_error) = e.raw_os_error() { - if os_error == ENOTSUP { - return Err(Error::CompressedBlocksNotSupported); - } - } - } - } - } - - let mut qcow = QcowFile { + pub fn from_with_nesting_depth( + file: RawFile, + max_nesting_depth: u32, + sparse: bool, + ) -> BlockResult { + let (inner, backing_file, sparse) = parse_qcow(file, max_nesting_depth, sparse)?; + let metadata::QcowState { raw_file, header, l1_table, l2_entries, - l2_cache: CacheMap::new(100), + l2_cache, + refcounts, + avail_clusters, + unref_clusters, + } = inner; + Ok(QcowFile { + raw_file, + header, + l1_table, + l2_entries, + l2_cache, refcounts, current_offset: 0, - unref_clusters: Vec::new(), - avail_clusters: Vec::new(), + unref_clusters, + avail_clusters, backing_file, - }; - - // Check that the L1 and refcount tables fit in a 64bit address space. - qcow.header - .l1_table_offset - .checked_add(qcow.l1_address_offset(qcow.virtual_size())) - .ok_or(Error::InvalidL1TableOffset)?; - qcow.header - .refcount_table_offset - .checked_add(u64::from(qcow.header.refcount_table_clusters) * cluster_size) - .ok_or(Error::InvalidRefcountTableOffset)?; - - qcow.find_avail_clusters()?; - - Ok(qcow) + sparse, + }) } /// Creates a new QcowFile at the given path. - pub fn new(file: RawFile, version: u32, virtual_size: u64) -> Result { - let header = QcowHeader::create_for_size_and_path(version, virtual_size, None)?; - QcowFile::new_from_header(file, header) + pub fn new( + file: RawFile, + version: u32, + virtual_size: u64, + sparse: bool, + ) -> BlockResult { + let header = + QcowHeader::create_for_size_and_path(version, virtual_size, None).map_err(|e| { + let kind = match &e { + Error::BackingFileTooLong(_) => BlockErrorKind::InvalidFormat, + _ => BlockErrorKind::Io, + }; + BlockError::new(kind, e) + })?; + QcowFile::new_from_header(file, &header, sparse) } - /// Creates a new QcowFile at the given path. + /// Creates a new QcowFile at the given path with a backing file. pub fn new_from_backing( file: RawFile, version: u32, - backing_file_name: &str, - backing_file_max_nesting_depth: u32, - ) -> Result { - let direct_io = file.is_direct(); - let backing_raw_file = OpenOptions::new() - .read(true) - .open(backing_file_name) - .map_err(Error::BackingFileIo)?; - let backing_file = Self::from_with_nesting_depth( - RawFile::new(backing_raw_file, direct_io), - backing_file_max_nesting_depth, + backing_file_size: u64, + backing_config: &BackingFileConfig, + sparse: bool, + ) -> BlockResult { + let mut header = QcowHeader::create_for_size_and_path( + version, + backing_file_size, + Some(&backing_config.path), ) - .map_err(|e| Error::BackingFileOpen(Box::new(e)))?; - let size = backing_file.virtual_size(); - let header = QcowHeader::create_for_size_and_path(version, size, Some(backing_file_name))?; - let mut result = QcowFile::new_from_header(file, header)?; - result.backing_file = Some(Box::new(backing_file)); - Ok(result) + .map_err(|e| { + let kind = match &e { + Error::BackingFileTooLong(_) => BlockErrorKind::InvalidFormat, + _ => BlockErrorKind::Io, + }; + BlockError::new(kind, e) + })?; + if let Some(backing_file) = &mut header.backing_file { + backing_file.format = backing_config.format; + } + QcowFile::new_from_header(file, &header, sparse) + // backing_file is loaded by new_from_header -> Self::from() based on the header } - fn new_from_header(mut file: RawFile, header: QcowHeader) -> Result { - file.rewind().map_err(Error::SeekingFile)?; - header.write_to(&mut file)?; + fn new_from_header( + mut file: RawFile, + header: &QcowHeader, + sparse: bool, + ) -> BlockResult { + file.rewind() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + header + .write_to(&mut file) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; - let mut qcow = Self::from(file)?; + let mut qcow = Self::from_with_nesting_depth(file, MAX_NESTING_DEPTH, sparse)?; // Set the refcount for each refcount table cluster. let cluster_size = 0x01u64 << qcow.header.cluster_bits; @@ -672,9 +783,9 @@ impl QcowFile { let mut cluster_addr = 0; while cluster_addr < end_cluster_addr { - let mut unref_clusters = qcow - .set_cluster_refcount(cluster_addr, 1) - .map_err(Error::SettingRefcountRefcount)?; + let mut unref_clusters = qcow.set_cluster_refcount(cluster_addr, 1).map_err(|e| { + BlockError::new(BlockErrorKind::Io, Error::SettingRefcountRefcount(e)) + })?; qcow.unref_clusters.append(&mut unref_clusters); cluster_addr += cluster_size; } @@ -682,8 +793,15 @@ impl QcowFile { Ok(qcow) } + #[cfg(test)] pub fn set_backing_file(&mut self, backing: Option>) { - self.backing_file = backing; + self.backing_file = backing.map(|b| { + let virtual_size = b.virtual_size(); + BackingFile { + kind: BackingKind::QcowFile(b), + virtual_size, + } + }); } /// Returns the `QcowHeader` for this file. @@ -697,8 +815,11 @@ impl QcowFile { } /// Returns an L2_table of cluster addresses, only used for debugging. - pub fn l2_table(&mut self, l1_index: usize) -> Result> { - let l2_addr_disk = *self.l1_table.get(l1_index).ok_or(Error::InvalidIndex)?; + pub fn l2_table(&mut self, l1_index: usize) -> BlockResult> { + let l2_addr_disk = *self + .l1_table + .get(l1_index) + .ok_or_else(|| BlockError::new(BlockErrorKind::OutOfBounds, Error::InvalidIndex))?; if l2_addr_disk == 0 { // Reading from an unallocated cluster will return zeros. @@ -709,19 +830,15 @@ impl QcowFile { // Not in the cache. let table = VecCache::from_vec( Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk) - .map_err(Error::ReadingPointers)?, + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingPointers(e)))?, ); let l1_table = &self.l1_table; let raw_file = &mut self.raw_file; self.l2_cache .insert(l1_index, table, |index, evicted| { - raw_file.write_pointer_table( - l1_table[index], - evicted.get_values(), - CLUSTER_USED_FLAG, - ) + raw_file.write_pointer_table_direct(l1_table[index], evicted.iter()) }) - .map_err(Error::EvictingCache)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::EvictingCache(e)))?; } // The index must exist as it was just inserted if it didn't already. @@ -734,19 +851,19 @@ impl QcowFile { } /// Returns the `index`th refcount block from the file. - pub fn refcount_block(&mut self, index: usize) -> Result> { + pub fn refcount_block(&mut self, index: usize) -> BlockResult> { self.refcounts .refcount_block(&mut self.raw_file, index) - .map_err(Error::ReadingRefCountBlock) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingRefCountBlock(e))) } /// Returns the first cluster in the file with a 0 refcount. Used for testing. - pub fn first_zero_refcount(&mut self) -> Result> { + pub fn first_zero_refcount(&mut self) -> BlockResult> { let file_size = self .raw_file .file_mut() .metadata() - .map_err(Error::GettingFileSize)? + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))? .len(); let cluster_size = 0x01u64 << self.header.cluster_bits; @@ -755,7 +872,7 @@ impl QcowFile { let cluster_refcount = self .refcounts .get_cluster_refcount(&mut self.raw_file, cluster_addr) - .map_err(Error::GettingRefcount)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingRefcount(e)))?; if cluster_refcount == 0 { return Ok(Some(cluster_addr)); } @@ -764,66 +881,219 @@ impl QcowFile { Ok(None) } - fn find_avail_clusters(&mut self) -> Result<()> { + /// Resize the virtual size of the QCOW2 image. + /// + /// This supports growing the image, including growing the L1 table + /// if needed. Shrinking is not supported, as it could lead to data + /// loss. Not supported when a backing file is present in that case + /// an error is returned. + pub fn resize(&mut self, new_size: u64) -> BlockResult<()> { + let current_size = self.virtual_size(); + + if new_size == current_size { + return Ok(()); + } + + if new_size < current_size { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + Error::ShrinkNotSupported, + )); + } + + if self.backing_file.is_some() { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + Error::ResizeWithBackingFile, + )); + } + + // Grow the L1 table if needed let cluster_size = self.raw_file.cluster_size(); + let entries_per_cluster = cluster_size / size_of::() as u64; + let new_clusters = div_round_up_u64(new_size, cluster_size); + let needed_l1_entries = div_round_up_u64(new_clusters, entries_per_cluster) as u32; + if needed_l1_entries > self.header.l1_size { + self.grow_l1_table(needed_l1_entries)?; + } + + self.header.size = new_size; + + self.raw_file + .file_mut() + .rewind() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + self.header + .write_to(self.raw_file.file_mut()) + .map_err(|e| match e { + Error::WritingHeader(io_err) => { + BlockError::new(BlockErrorKind::Io, Error::ResizeIo(io_err)) + } + other => BlockError::new(BlockErrorKind::Io, other), + })?; + + self.raw_file + .file_mut() + .sync_all() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?; + + Ok(()) + } + + /// Grow the L1 table to accommodate at least `new_l1_size` entries. + /// + /// This allocates a new L1 table at file end (guaranteeing contiguity), + /// copies existing entries, updates refcounts, and atomically switches + /// to the new table. + fn grow_l1_table(&mut self, new_l1_size: u32) -> BlockResult<()> { + let old_l1_size = self.header.l1_size; + let old_l1_offset = self.header.l1_table_offset; + let cluster_size = self.raw_file.cluster_size(); + + let new_l1_bytes = new_l1_size as u64 * size_of::() as u64; + let new_l1_clusters = div_round_up_u64(new_l1_bytes, cluster_size); + + // Allocate contiguous clusters at file end for new L1 table let file_size = self .raw_file .file_mut() - .metadata() - .map_err(Error::GettingFileSize)? - .len(); + .seek(SeekFrom::End(0)) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ResizeIo(e)))?; + let new_l1_offset = self.raw_file.cluster_address(file_size + cluster_size - 1); - for i in (0..file_size).step_by(cluster_size as usize) { - let refcount = self - .refcounts - .get_cluster_refcount(&mut self.raw_file, i) - .map_err(Error::GettingRefcount)?; - if refcount == 0 { - self.avail_clusters.push(i); + // Extend file to fit all L1 clusters + let new_file_end = new_l1_offset + new_l1_clusters * cluster_size; + self.raw_file + .file_mut() + .set_len(new_file_end) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SettingFileSize(e)))?; + + // Set refcounts for the contiguous range + for i in 0..new_l1_clusters { + self.set_cluster_refcount(new_l1_offset + i * cluster_size, 1) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ResizeIo(e)))?; + } + + let mut new_l1_data = vec![0u64; new_l1_size as usize]; + let old_entries = self.l1_table.get_values(); + new_l1_data[..old_entries.len()].copy_from_slice(old_entries); + + for (i, l2_addr) in new_l1_data.iter_mut().enumerate() { + if *l2_addr != 0 && i < old_entries.len() { + let refcount = self + .refcounts + .get_cluster_refcount(&mut self.raw_file, *l2_addr) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingRefcount(e)))?; + *l2_addr = l1_entry_make(*l2_addr, refcount == 1); } } + // Write the new L1 table to the file. + self.raw_file + .write_pointer_table_direct(new_l1_offset, new_l1_data.iter()) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ResizeIo(e)))?; + + self.raw_file + .file_mut() + .sync_all() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?; + + self.header.l1_size = new_l1_size; + self.header.l1_table_offset = new_l1_offset; + + self.raw_file + .file_mut() + .rewind() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + self.header + .write_to(self.raw_file.file_mut()) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + + self.raw_file + .file_mut() + .sync_all() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?; + + // Free old L1 table clusters + let old_l1_bytes = old_l1_size as u64 * size_of::() as u64; + let old_l1_clusters = div_round_up_u64(old_l1_bytes, cluster_size); + for i in 0..old_l1_clusters { + let cluster_addr = old_l1_offset + i * cluster_size; + let _ = self.set_cluster_refcount(cluster_addr, 0); + } + + // Update L1 table cache + self.l1_table.extend(new_l1_size as usize); + Ok(()) } /// Rebuild the reference count tables. - fn rebuild_refcounts(raw_file: &mut QcowRawFile, header: QcowHeader) -> Result<()> { - fn add_ref(refcounts: &mut [u16], cluster_size: u64, cluster_address: u64) -> Result<()> { + fn rebuild_refcounts(raw_file: &mut QcowRawFile, header: QcowHeader) -> BlockResult<()> { + fn add_ref( + refcounts: &mut [u64], + cluster_size: u64, + cluster_address: u64, + max_refcount: u64, + refcount_bits: u64, + ) -> Result<()> { let idx = (cluster_address / cluster_size) as usize; if idx >= refcounts.len() { return Err(Error::InvalidClusterIndex); } + if refcounts[idx] >= max_refcount { + return Err(Error::RefcountOverflow(refcount::Error::RefcountOverflow { + value: refcounts[idx] + 1, + max: max_refcount, + refcount_bits, + })); + } refcounts[idx] += 1; Ok(()) } // Add a reference to the first cluster (header plus extensions). - fn set_header_refcount(refcounts: &mut [u16], cluster_size: u64) -> Result<()> { - add_ref(refcounts, cluster_size, 0) + fn set_header_refcount( + refcounts: &mut [u64], + cluster_size: u64, + max_refcount: u64, + refcount_bits: u64, + ) -> Result<()> { + add_ref(refcounts, cluster_size, 0, max_refcount, refcount_bits) } // Add references to the L1 table clusters. fn set_l1_refcounts( - refcounts: &mut [u16], - header: QcowHeader, + refcounts: &mut [u64], + header: &QcowHeader, cluster_size: u64, + max_refcount: u64, + refcount_bits: u64, ) -> Result<()> { let entries_per_cluster = cluster_size / size_of::() as u64; let l1_clusters = div_round_up_u64(u64::from(header.l1_size), entries_per_cluster); let l1_table_offset = header.l1_table_offset; for i in 0..l1_clusters { - add_ref(refcounts, cluster_size, l1_table_offset + i * cluster_size)?; + add_ref( + refcounts, + cluster_size, + l1_table_offset + i * cluster_size, + max_refcount, + refcount_bits, + )?; } Ok(()) } // Traverse the L1 and L2 tables to find all reachable data clusters. fn set_data_refcounts( - refcounts: &mut [u16], - header: QcowHeader, + refcounts: &mut [u64], + header: &QcowHeader, cluster_size: u64, raw_file: &mut QcowRawFile, + max_refcount: u64, + refcount_bits: u64, ) -> Result<()> { let l1_table = raw_file .read_pointer_table( @@ -836,7 +1106,13 @@ impl QcowFile { let l2_addr_disk = *l1_table.get(l1_index).ok_or(Error::InvalidIndex)?; if l2_addr_disk != 0 { // Add a reference to the L2 table cluster itself. - add_ref(refcounts, cluster_size, l2_addr_disk)?; + add_ref( + refcounts, + cluster_size, + l2_addr_disk, + max_refcount, + refcount_bits, + )?; // Read the L2 table and find all referenced data clusters. let l2_table = raw_file @@ -848,7 +1124,13 @@ impl QcowFile { .map_err(Error::ReadingPointers)?; for data_cluster_addr in l2_table { if data_cluster_addr != 0 { - add_ref(refcounts, cluster_size, data_cluster_addr)?; + add_ref( + refcounts, + cluster_size, + data_cluster_addr, + max_refcount, + refcount_bits, + )?; } } } @@ -859,9 +1141,11 @@ impl QcowFile { // Add references to the top-level refcount table clusters. fn set_refcount_table_refcounts( - refcounts: &mut [u16], - header: QcowHeader, + refcounts: &mut [u64], + header: &QcowHeader, cluster_size: u64, + max_refcount: u64, + refcount_bits: u64, ) -> Result<()> { let refcount_table_offset = header.refcount_table_offset; for i in 0..u64::from(header.refcount_table_clusters) { @@ -869,6 +1153,8 @@ impl QcowFile { refcounts, cluster_size, refcount_table_offset + i * cluster_size, + max_refcount, + refcount_bits, )?; } Ok(()) @@ -878,9 +1164,11 @@ impl QcowFile { // This needs to be done last so that we have the correct refcounts for all other // clusters. fn alloc_refblocks( - refcounts: &mut [u16], + refcounts: &mut [u64], cluster_size: u64, refblock_clusters: u64, + max_refcount: u64, + refcount_bits: u64, ) -> Result> { let mut ref_table = vec![0; refblock_clusters as usize]; let mut first_free_cluster: u64 = 0; @@ -896,7 +1184,13 @@ impl QcowFile { } *refblock_addr = first_free_cluster * cluster_size; - add_ref(refcounts, cluster_size, *refblock_addr)?; + add_ref( + refcounts, + cluster_size, + *refblock_addr, + max_refcount, + refcount_bits, + )?; first_free_cluster += 1; } @@ -906,7 +1200,7 @@ impl QcowFile { // Write the updated reference count blocks and reftable. fn write_refblocks( - refcounts: &[u16], + refcounts: &[u64], mut header: QcowHeader, ref_table: &[u64], raw_file: &mut QcowRawFile, @@ -932,19 +1226,18 @@ impl QcowFile { // If this is the last (partial) cluster, pad it out to a full refblock cluster. if refblock.len() < refcount_block_entries as usize { let refblock_padding = - vec![0u16; refcount_block_entries as usize - refblock.len()]; + vec![0u64; refcount_block_entries as usize - refblock.len()]; + let byte_offset = + refblock.len() as u64 * raw_file.cluster_size() / refcount_block_entries; raw_file - .write_refcount_block( - *refblock_addr + refblock.len() as u64 * 2, - &refblock_padding, - ) + .write_refcount_block(*refblock_addr + byte_offset, &refblock_padding) .map_err(Error::WritingHeader)?; } } // Rewrite the top-level refcount table. raw_file - .write_pointer_table(header.refcount_table_offset, ref_table, 0) + .write_pointer_table_direct(header.refcount_table_offset, ref_table.iter()) .map_err(Error::WritingHeader)?; // Rewrite the header again, now with lazy refcounts disabled. @@ -960,12 +1253,16 @@ impl QcowFile { let file_size = raw_file .file_mut() .metadata() - .map_err(Error::GettingFileSize)? + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))? .len(); let refcount_bits = 1u64 << header.refcount_order; - let refcount_bytes = div_round_up_u64(refcount_bits, 8); - let refcount_block_entries = cluster_size / refcount_bytes; + let max_refcount = if refcount_bits == 64 { + u64::MAX + } else { + (1u64 << refcount_bits) - 1 + }; + let refcount_block_entries = cluster_size * 8 / refcount_bits; let pointers_per_cluster = cluster_size / size_of::() as u64; let data_clusters = div_round_up_u64(header.size, cluster_size); let l2_clusters = div_round_up_u64(data_clusters, pointers_per_cluster); @@ -986,24 +1283,60 @@ impl QcowFile { max_valid_cluster_index += refblocks_for_refs + reftable_clusters_for_refs; if max_valid_cluster_index > MAX_RAM_POINTER_TABLE_SIZE { - return Err(Error::InvalidRefcountTableSize(max_valid_cluster_index)); + return Err(BlockError::new( + BlockErrorKind::CorruptImage, + Error::InvalidRefcountTableSize(max_valid_cluster_index), + )); } let max_valid_cluster_offset = max_valid_cluster_index * cluster_size; if max_valid_cluster_offset < file_size - cluster_size { - return Err(Error::InvalidRefcountTableSize(max_valid_cluster_offset)); + return Err(BlockError::new( + BlockErrorKind::CorruptImage, + Error::InvalidRefcountTableSize(max_valid_cluster_offset), + )); } let mut refcounts = vec![0; max_valid_cluster_index as usize]; // Find all references clusters and rebuild refcounts. - set_header_refcount(&mut refcounts, cluster_size)?; - set_l1_refcounts(&mut refcounts, header.clone(), cluster_size)?; - set_data_refcounts(&mut refcounts, header.clone(), cluster_size, raw_file)?; - set_refcount_table_refcounts(&mut refcounts, header.clone(), cluster_size)?; + set_header_refcount(&mut refcounts, cluster_size, max_refcount, refcount_bits) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + set_l1_refcounts( + &mut refcounts, + &header, + cluster_size, + max_refcount, + refcount_bits, + ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + set_data_refcounts( + &mut refcounts, + &header, + cluster_size, + raw_file, + max_refcount, + refcount_bits, + ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + set_refcount_table_refcounts( + &mut refcounts, + &header, + cluster_size, + max_refcount, + refcount_bits, + ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; // Allocate clusters to store the new reference count blocks. - let ref_table = alloc_refblocks(&mut refcounts, cluster_size, refblock_clusters)?; + let ref_table = alloc_refblocks( + &mut refcounts, + cluster_size, + refblock_clusters, + max_refcount, + refcount_bits, + ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; // Write updated reference counts and point the reftable at them. write_refblocks( @@ -1013,6 +1346,7 @@ impl QcowFile { raw_file, refcount_block_entries, ) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e)) } // Limits the range so that it doesn't exceed the virtual size of the file. @@ -1035,12 +1369,6 @@ impl QcowFile { self.header.size } - // Gets the offset of `address` in the L1 table. - fn l1_address_offset(&self, address: u64) -> u64 { - let l1_index = self.l1_table_index(address); - l1_index * size_of::() as u64 - } - // Gets the offset of `address` in the L1 table. fn l1_table_index(&self, address: u64) -> u64 { (address / self.raw_file.cluster_size()) / self.l2_entries @@ -1051,11 +1379,57 @@ impl QcowFile { (address / self.raw_file.cluster_size()) % self.l2_entries } - // Gets the offset of the given guest address in the host file. If L1, L2, or data clusters have - // yet to be allocated, return None. - fn file_offset_read(&mut self, address: u64) -> std::io::Result> { + /// Attempts to set the corrupt bit, logging failures without propagating them. + /// + /// This is "best effort" because the write may fail due to various reasons like + /// disk full, readonly storage, etc. This method is called just before returning + /// EIO to the caller. The error is not propagated because the original corruption + /// error is more important to return to the call site than a secondary I/O + /// failure from marking the image. + fn set_corrupt_bit_best_effort(&mut self) { + if let Err(e) = self.header.set_corrupt_bit(self.raw_file.file_mut()) { + warn!("Failed to persist corrupt bit: {e}"); + } + } + + // Decompress the cluster, return EIO on failure + fn decompress_l2_cluster(&mut self, l2_entry: u64) -> std::io::Result> { + let (compressed_cluster_addr, compressed_cluster_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + // Read compressed cluster from raw file + self.raw_file + .file_mut() + .seek(SeekFrom::Start(compressed_cluster_addr))?; + let mut compressed_cluster = vec![0; compressed_cluster_size]; + self.raw_file + .file_mut() + .read_exact(&mut compressed_cluster)?; + let decoder = self.header.get_decoder(); + // Decompress + let cluster_size = self.raw_file.cluster_size() as usize; + let mut decompressed_cluster = vec![0; cluster_size]; + let decompressed_size = decoder + .decode(&compressed_cluster, &mut decompressed_cluster) + .map_err(|_| { + self.set_corrupt_bit_best_effort(); + io::Error::from_raw_os_error(EIO) + })?; + if decompressed_size as u64 != self.raw_file.cluster_size() { + self.set_corrupt_bit_best_effort(); + return Err(std::io::Error::from_raw_os_error(EIO)); + } + Ok(decompressed_cluster) + } + + fn file_read( + &mut self, + address: u64, + count: usize, + buf: &mut [u8], + ) -> std::io::Result> { + let err_inval = std::io::Error::from_raw_os_error(EINVAL); if address >= self.virtual_size() { - return Err(std::io::Error::from_raw_os_error(EINVAL)); + return Err(err_inval); } let l1_index = self.l1_table_index(address) as usize; @@ -1071,27 +1445,38 @@ impl QcowFile { let l2_index = self.l2_table_index(address) as usize; - if !self.l2_cache.contains_key(l1_index) { - // Not in the cache. - let table = - VecCache::from_vec(Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)?); - - let l1_table = &self.l1_table; - let raw_file = &mut self.raw_file; - self.l2_cache.insert(l1_index, table, |index, evicted| { - raw_file.write_pointer_table( - l1_table[index], - evicted.get_values(), - CLUSTER_USED_FLAG, - ) - })?; - }; + self.cache_l2_cluster(l1_index, l2_addr_disk, false)?; - let cluster_addr = self.l2_cache.get(l1_index).unwrap()[l2_index]; - if cluster_addr == 0 { + let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; + if l2_entry_is_empty(l2_entry) { + // Reading from an unallocated cluster will return zeros. + return Ok(None); + } else if l2_entry_is_compressed(l2_entry) { + // Compressed cluster. + // Read it, decompress, then return slice from decompressed data. + let mut decompressed_cluster = self.decompress_l2_cluster(l2_entry)?; + decompressed_cluster.resize(self.raw_file.cluster_size() as usize, 0); + let start = self.raw_file.cluster_offset(address) as usize; + let end = start.checked_add(count); + if end.is_none() || end.unwrap() > decompressed_cluster.len() { + return Err(err_inval); + } + buf[..count].copy_from_slice(&decompressed_cluster[start..end.unwrap()]); + } else if l2_entry_is_zero(l2_entry) { + // Cluster with zero flag reads as zeros without accessing disk. return Ok(None); + } else { + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + if cluster_addr & (self.raw_file.cluster_size() - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + let start = cluster_addr + self.raw_file.cluster_offset(address); + let raw_file = self.raw_file.file_mut(); + raw_file.seek(SeekFrom::Start(start))?; + raw_file.read_exact(buf)?; } - Ok(Some(cluster_addr + self.raw_file.cluster_offset(address))) + Ok(Some(())) } // Gets the offset of the given guest address in the host file. If L1, L2, or data clusters need @@ -1110,53 +1495,58 @@ impl QcowFile { let mut set_refcounts = Vec::new(); - if !self.l2_cache.contains_key(l1_index) { - // Not in the cache. - let l2_table = if l2_addr_disk == 0 { - // Allocate a new cluster to store the L2 table and update the L1 table to point - // to the new table. - let new_addr: u64 = self.get_new_cluster(None)?; - // The cluster refcount starts at one meaning it is used but doesn't need COW. - set_refcounts.push((new_addr, 1)); - self.l1_table[l1_index] = new_addr; - VecCache::new(self.l2_entries as usize) - } else { - VecCache::from_vec(Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)?) - }; - let l1_table = &self.l1_table; - let raw_file = &mut self.raw_file; - self.l2_cache.insert(l1_index, l2_table, |index, evicted| { - raw_file.write_pointer_table( - l1_table[index], - evicted.get_values(), - CLUSTER_USED_FLAG, - ) - })?; + if let Some(new_addr) = self.cache_l2_cluster(l1_index, l2_addr_disk, true)? { + // The cluster refcount starts at one meaning it is used but doesn't need COW. + set_refcounts.push((new_addr, 1)); } - let cluster_addr = match self.l2_cache.get(l1_index).unwrap()[l2_index] { - 0 => { - let initial_data = if let Some(backing) = self.backing_file.as_mut() { - let cluster_size = self.raw_file.cluster_size(); - let cluster_begin = address - (address % cluster_size); - let mut cluster_data = vec![0u8; cluster_size as usize]; - backing.seek(SeekFrom::Start(cluster_begin))?; - backing.read_exact(&mut cluster_data)?; - Some(cluster_data) - } else { - None - }; - // Need to allocate a data cluster - let cluster_addr = self.append_data_cluster(initial_data)?; - self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; - cluster_addr + let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; + let cluster_addr = if l2_entry_is_compressed(l2_entry) { + // Writing to compressed cluster. + + // Allocate new cluster, decompress into new cluster, then use + // offset of new cluster. + let decompressed_cluster = self.decompress_l2_cluster(l2_entry)?; + let cluster_addr = self.append_data_cluster(None)?; + self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; + self.raw_file + .file_mut() + .seek(SeekFrom::Start(cluster_addr))?; + let nwritten = self.raw_file.file_mut().write(&decompressed_cluster)?; + if nwritten != decompressed_cluster.len() { + self.set_corrupt_bit_best_effort(); + return Err(std::io::Error::from_raw_os_error(EIO)); + } + + // Decrement refcount for each cluster spanned by the old compressed data + self.deallocate_compressed_cluster(l2_entry)?; + + cluster_addr + } else if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) { + let initial_data = if let Some(backing) = self.backing_file.as_mut() { + let cluster_size = self.raw_file.cluster_size(); + let cluster_begin = address - (address % cluster_size); + let mut cluster_data = vec![0u8; cluster_size as usize]; + backing.read_at(cluster_begin, &mut cluster_data)?; + Some(cluster_data) + } else { + None + }; + // Need to allocate a data cluster + let cluster_addr = self.append_data_cluster(initial_data)?; + self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; + cluster_addr + } else { + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + if cluster_addr & (self.raw_file.cluster_size() - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); } - a => a, + cluster_addr }; for (addr, count) in set_refcounts { - let mut newly_unref = self.set_cluster_refcount(addr, count)?; - self.unref_clusters.append(&mut newly_unref); + self.set_cluster_refcount_track_freed(addr, count)?; } Ok(cluster_addr + self.raw_file.cluster_offset(address)) @@ -1168,7 +1558,7 @@ impl QcowFile { l1_index: usize, l2_index: usize, cluster_addr: u64, - set_refcounts: &mut Vec<(u64, u16)>, + set_refcounts: &mut Vec<(u64, u64)>, ) -> io::Result<()> { if !self.l2_cache.get(l1_index).unwrap().dirty() { // Free the previously used cluster if one exists. Modified tables are always @@ -1191,7 +1581,7 @@ impl QcowFile { self.l1_table[l1_index] = new_addr; } // 'unwrap' is OK because it was just added. - self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = cluster_addr; + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = l2_entry_make_std(cluster_addr); Ok(()) } @@ -1199,8 +1589,12 @@ impl QcowFile { fn get_new_cluster(&mut self, initial_data: Option>) -> std::io::Result { // First use a pre allocated cluster if one is available. if let Some(free_cluster) = self.avail_clusters.pop() { + if free_cluster == 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } if let Some(initial_data) = initial_data { - self.raw_file.write_cluster(free_cluster, initial_data)?; + self.raw_file.write_cluster(free_cluster, &initial_data)?; } else { self.raw_file.zero_cluster(free_cluster)?; } @@ -1209,8 +1603,12 @@ impl QcowFile { let max_valid_cluster_offset = self.refcounts.max_valid_cluster_offset(); if let Some(new_cluster) = self.raw_file.add_cluster_end(max_valid_cluster_offset)? { + if new_cluster == 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } if let Some(initial_data) = initial_data { - self.raw_file.write_cluster(new_cluster, initial_data)?; + self.raw_file.write_cluster(new_cluster, &initial_data)?; } Ok(new_cluster) } else { @@ -1224,8 +1622,7 @@ impl QcowFile { fn append_data_cluster(&mut self, initial_data: Option>) -> std::io::Result { let new_addr: u64 = self.get_new_cluster(initial_data)?; // The cluster refcount starts at one indicating it is used but doesn't need COW. - let mut newly_unref = self.set_cluster_refcount(new_addr, 1)?; - self.unref_clusters.append(&mut newly_unref); + self.set_cluster_refcount_track_freed(new_addr, 1)?; Ok(new_addr) } @@ -1248,20 +1645,7 @@ impl QcowFile { return Ok(false); } - if !self.l2_cache.contains_key(l1_index) { - // Not in the cache. - let table = - VecCache::from_vec(Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)?); - let l1_table = &self.l1_table; - let raw_file = &mut self.raw_file; - self.l2_cache.insert(l1_index, table, |index, evicted| { - raw_file.write_pointer_table( - l1_table[index], - evicted.get_values(), - CLUSTER_USED_FLAG, - ) - })?; - } + self.cache_l2_cluster(l1_index, l2_addr_disk, false)?; let cluster_addr = self.l2_cache.get(l1_index).unwrap()[l2_index]; // If cluster_addr != 0, the cluster is allocated. @@ -1300,6 +1684,43 @@ impl QcowFile { Ok(None) } + // Deallocate compressed cluster and all related clusters spanned by compressed data. + fn deallocate_compressed_cluster(&mut self, l2_entry: u64) -> std::io::Result<()> { + let (compressed_cluster_addr, compressed_cluster_size) = + l2_entry_compressed_cluster_layout(l2_entry, self.header.cluster_bits); + + // Calculate the end of the compressed data region + let compressed_clusters_end = self.raw_file.cluster_address( + compressed_cluster_addr // Start of compressed data + + compressed_cluster_size as u64 // Add size to get end address + + self.raw_file.cluster_size() + - 1, // Catch possibly partially used last cluster + ); + + // Decrement refcount for each cluster spanned by the compressed data + let mut addr = self.raw_file.cluster_address(compressed_cluster_addr); + while addr < compressed_clusters_end { + let refcount = self + .refcounts + .get_cluster_refcount(&mut self.raw_file, addr) + .map_err(|e| { + if matches!(e, refcount::Error::RefblockUnaligned(_)) { + self.set_corrupt_bit_best_effort(); + } + io::Error::new( + io::ErrorKind::InvalidData, + format!("failed to get cluster refcount: {e}"), + ) + })?; + if refcount > 0 { + self.set_cluster_refcount_track_freed(addr, refcount - 1)?; + } + addr += self.raw_file.cluster_size(); + } + + Ok(()) + } + // Deallocate the storage for the cluster starting at `address`. // Any future reads of this cluster will return all zeroes. fn deallocate_cluster(&mut self, address: u64) -> std::io::Result<()> { @@ -1320,32 +1741,31 @@ impl QcowFile { return Ok(()); } - if !self.l2_cache.contains_key(l1_index) { - // Not in the cache. - let table = - VecCache::from_vec(Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)?); - let l1_table = &self.l1_table; - let raw_file = &mut self.raw_file; - self.l2_cache.insert(l1_index, table, |index, evicted| { - raw_file.write_pointer_table( - l1_table[index], - evicted.get_values(), - CLUSTER_USED_FLAG, - ) - })?; + self.cache_l2_cluster(l1_index, l2_addr_disk, false)?; + + let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; + if l2_entry_is_empty(l2_entry) || l2_entry_is_zero(l2_entry) { + // Already unallocated or zero. + return Ok(()); } - let cluster_addr = self.l2_cache.get(l1_index).unwrap()[l2_index]; - if cluster_addr == 0 { - // This cluster is already unallocated; nothing to do. + // Compressed clusters cannot use the zero flag optimization, thus fully deallocate instead. + if l2_entry_is_compressed(l2_entry) { + self.deallocate_compressed_cluster(l2_entry)?; + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; return Ok(()); } + let cluster_addr = l2_entry_std_cluster_addr(l2_entry); + // Decrement the refcount. let refcount = self .refcounts .get_cluster_refcount(&mut self.raw_file, cluster_addr) .map_err(|e| { + if matches!(e, refcount::Error::RefblockUnaligned(_)) { + self.set_corrupt_bit_best_effort(); + } io::Error::new( io::ErrorKind::InvalidData, format!("failed to get cluster refcount: {e}"), @@ -1355,24 +1775,38 @@ impl QcowFile { return Err(std::io::Error::from_raw_os_error(EINVAL)); } - let new_refcount = refcount - 1; - let mut newly_unref = self.set_cluster_refcount(cluster_addr, new_refcount)?; - self.unref_clusters.append(&mut newly_unref); - - // Rewrite the L2 entry to remove the cluster mapping. - // unwrap is safe as we just checked/inserted this entry. - self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; - - if new_refcount == 0 { - let cluster_size = self.raw_file.cluster_size(); - // This cluster is no longer in use; deallocate the storage. - // The underlying FS may not support FALLOC_FL_PUNCH_HOLE, - // so don't treat an error as fatal. Future reads will return zeros anyways. - let _ = self - .raw_file - .file_mut() - .punch_hole(cluster_addr, cluster_size); - self.unref_clusters.push(cluster_addr); + if self.sparse { + // Fully deallocate to reclaim storage space. + let new_refcount = refcount - 1; + self.set_cluster_refcount_track_freed(cluster_addr, new_refcount)?; + + // Rewrite the L2 entry to remove the cluster mapping (full deallocation). + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; + + if new_refcount == 0 { + let cluster_size = self.raw_file.cluster_size(); + // This cluster is no longer in use; deallocate the storage. + // The underlying FS may not support FALLOC_FL_PUNCH_HOLE, + // so don't treat an error as fatal. Future reads will return zeros anyways. + let _ = self + .raw_file + .file_mut() + .punch_hole(cluster_addr, cluster_size); + self.unref_clusters.push(cluster_addr); + } + } else { + // Zero flag optimization - mark cluster as reading zeros without deallocating. + // Only safe if refcount == 1 (no other references to this cluster). + if refcount == 1 { + // Single reference - safe to use zero flag optimization + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = + l2_entry_make_zero(cluster_addr); + } else { + // Multiple references - must decrement refcount and unmap this entry. + // Cannot use zero flag because other L2 entries still need the real data. + self.set_cluster_refcount_track_freed(cluster_addr, refcount - 1)?; + self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = 0; + } } Ok(()) } @@ -1394,10 +1828,9 @@ impl QcowFile { // Partial cluster - zero out the relevant bytes if it was allocated. // Any space in unallocated clusters can be left alone, since // unallocated clusters already read back as zeroes. - if let Some(offset) = self.file_offset_read(curr_addr)? { - // Partial cluster - zero it out. - self.raw_file.file_mut().write_zeroes_at(offset, count)?; - } + let offset = self.file_offset_write(curr_addr)?; + // Partial cluster - zero it out. + self.raw_file.file_mut().write_zeroes_at(offset, count)?; } nwritten += count; @@ -1408,20 +1841,61 @@ impl QcowFile { // Reads an L2 cluster from the disk, returning an error if the file can't be read or if any // cluster is compressed. fn read_l2_cluster(raw_file: &mut QcowRawFile, cluster_addr: u64) -> std::io::Result> { - let file_values = raw_file.read_pointer_cluster(cluster_addr, None)?; - if file_values.iter().any(|entry| entry & COMPRESSED_FLAG != 0) { - return Err(std::io::Error::from_raw_os_error(ENOTSUP)); + let l2_table = raw_file.read_pointer_cluster(cluster_addr, None)?; + Ok(l2_table) + } + + // Put an L2 cluster to the cache with evicting less-used cluster + // The new cluster may be allocated if necessary + // (may_alloc argument is true and l2_addr_disk == 0) + fn cache_l2_cluster( + &mut self, + l1_index: usize, + l2_addr_disk: u64, + may_alloc: bool, + ) -> std::io::Result> { + let mut new_cluster: Option = None; + if !self.l2_cache.contains_key(l1_index) { + // Not in the cache. + let l2_table = if may_alloc && l2_addr_disk == 0 { + // Allocate a new cluster to store the L2 table and update the L1 table to point + // to the new table. + let new_addr: u64 = self.get_new_cluster(None)?; + new_cluster = Some(new_addr); + self.l1_table[l1_index] = new_addr; + VecCache::new(self.l2_entries as usize) + } else { + let cluster_size = self.raw_file.cluster_size(); + if l2_addr_disk & (cluster_size - 1) != 0 { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } + VecCache::from_vec(Self::read_l2_cluster(&mut self.raw_file, l2_addr_disk)?) + }; + let l1_table = &self.l1_table; + let raw_file = &mut self.raw_file; + self.l2_cache.insert(l1_index, l2_table, |index, evicted| { + raw_file.write_pointer_table_direct(l1_table[index], evicted.iter()) + })?; } - Ok(file_values - .iter() - .map(|entry| *entry & L2_TABLE_OFFSET_MASK) - .collect()) + Ok(new_cluster) + } + + // Set the refcount for a cluster and add any unreferenced clusters to the unref list. + fn set_cluster_refcount_track_freed( + &mut self, + address: u64, + refcount: u64, + ) -> std::io::Result<()> { + let mut newly_unref = self.set_cluster_refcount(address, refcount)?; + self.unref_clusters.append(&mut newly_unref); + Ok(()) } // Set the refcount for a cluster with the given address. // Returns a list of any refblocks that can be reused, this happens when a refblock is moved, // the old location can be reused. - fn set_cluster_refcount(&mut self, address: u64, refcount: u16) -> std::io::Result> { + fn set_cluster_refcount(&mut self, address: u64, refcount: u64) -> std::io::Result> { let mut added_clusters = Vec::new(); let mut unref_clusters = Vec::new(); let mut refcount_set = false; @@ -1438,13 +1912,16 @@ impl QcowFile { refcount_set = true; } Ok(Some(freed_cluster)) => { - unref_clusters.push(freed_cluster); + // Recursively set the freed refcount block's refcount to 0 + let mut freed = self.set_cluster_refcount(freed_cluster, 0)?; + unref_clusters.append(&mut freed); refcount_set = true; } Err(refcount::Error::EvictingRefCounts(e)) => { return Err(e); } Err(refcount::Error::InvalidIndex) => { + self.set_corrupt_bit_best_effort(); return Err(std::io::Error::from_raw_os_error(EINVAL)); } Err(refcount::Error::NeedCluster(addr)) => { @@ -1466,6 +1943,13 @@ impl QcowFile { Err(refcount::Error::ReadingRefCounts(e)) => { return Err(e); } + Err(refcount::Error::RefcountOverflow { .. }) => { + return Err(std::io::Error::from_raw_os_error(EINVAL)); + } + Err(refcount::Error::RefblockUnaligned(_)) => { + self.set_corrupt_bit_best_effort(); + return Err(io::Error::from_raw_os_error(EIO)); + } } } @@ -1481,12 +1965,10 @@ impl QcowFile { // The index must be valid from when we inserted it. let addr = self.l1_table[*l1_index]; if addr != 0 { - self.raw_file.write_pointer_table( - addr, - l2_table.get_values(), - CLUSTER_USED_FLAG, - )?; + self.raw_file + .write_pointer_table_direct(addr, l2_table.iter())?; } else { + self.set_corrupt_bit_best_effort(); return Err(std::io::Error::from_raw_os_error(EINVAL)); } l2_table.mark_clean(); @@ -1499,10 +1981,21 @@ impl QcowFile { // Push L1 table and refcount table last as all the clusters they point to are now // guaranteed to be valid. let mut sync_required = if self.l1_table.dirty() { + // Write L1 table with OFLAG_COPIED bits + let refcounts = &mut self.refcounts; self.raw_file.write_pointer_table( self.header.l1_table_offset, - self.l1_table.get_values(), - 0, + self.l1_table.iter(), + |raw_file, l2_addr| { + if l2_addr == 0 { + Ok(0) + } else { + let refcount = refcounts + .get_cluster_refcount(raw_file, l2_addr) + .map_err(|e| std::io::Error::other(Error::GettingRefcount(e)))?; + Ok(l1_entry_make(l2_addr, refcount == 1)) + } + }, )?; self.l1_table.mark_clean(); true @@ -1513,6 +2006,7 @@ impl QcowFile { if sync_required { self.raw_file.file_mut().sync_data()?; } + Ok(()) } } @@ -1526,6 +2020,9 @@ impl AsRawFd for QcowFile { impl Drop for QcowFile { fn drop(&mut self) { let _ = self.sync_caches(); + if self.raw_file.file().is_writable() { + let _ = self.header.set_dirty_bit(self.raw_file.file_mut(), false); + } } } @@ -1537,22 +2034,15 @@ impl Read for QcowFile { let mut nread: usize = 0; while nread < read_count { let curr_addr = address + nread as u64; - let file_offset = self.file_offset_read(curr_addr)?; let count = self.limit_range_cluster(curr_addr, read_count - nread); - if let Some(offset) = file_offset { - self.raw_file.file_mut().seek(SeekFrom::Start(offset))?; - self.raw_file - .file_mut() - .read_exact(&mut buf[nread..(nread + count)])?; + if (self.file_read(curr_addr, count, &mut buf[nread..(nread + count)])?).is_some() { + // Data is successfully read from the cluster } else if let Some(backing) = self.backing_file.as_mut() { - backing.seek(SeekFrom::Start(curr_addr))?; - backing.read_exact(&mut buf[nread..(nread + count)])?; + backing.read_at(curr_addr, &mut buf[nread..(nread + count)])?; } else { // Previously unwritten region, return zeros - for b in &mut buf[nread..(nread + count)] { - *b = 0; - } + buf[nread..(nread + count)].fill(0); } nread += count; @@ -1584,11 +2074,11 @@ impl Seek for QcowFile { } }; - if let Some(o) = new_offset { - if o <= self.virtual_size() { - self.current_offset = o; - return Ok(o); - } + if let Some(o) = new_offset + && o <= self.virtual_size() + { + self.current_offset = o; + return Ok(o); } Err(std::io::Error::from_raw_os_error(EINVAL)) } @@ -1690,30 +2180,18 @@ impl SeekHole for QcowFile { } impl BlockBackend for QcowFile { - fn size(&self) -> std::result::Result { + fn logical_size(&self) -> std::result::Result { Ok(self.virtual_size()) } -} -// Returns an Error if the given offset doesn't align to a cluster boundary. -fn offset_is_cluster_boundary(offset: u64, cluster_bits: u32) -> Result<()> { - if offset & ((0x01 << cluster_bits) - 1) != 0 { - return Err(Error::InvalidOffset(offset)); + fn physical_size(&self) -> std::result::Result { + self.raw_file + .physical_size() + .map_err(crate::Error::GetFileMetadata) } - Ok(()) -} - -// Ceiling of the division of `dividend`/`divisor`. -fn div_round_up_u64(dividend: u64, divisor: u64) -> u64 { - dividend / divisor + u64::from(dividend % divisor != 0) } -// Ceiling of the division of `dividend`/`divisor`. -fn div_round_up_u32(dividend: u32, divisor: u32) -> u32 { - dividend / divisor + u32::from(dividend % divisor != 0) -} - -fn convert_copy(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> Result<()> +fn convert_copy(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> BlockResult<()> where R: Read + Seek, W: Write + Seek, @@ -1723,16 +2201,18 @@ where let mut read_count = 0; reader .seek(SeekFrom::Start(offset)) - .map_err(Error::SeekingFile)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; writer .seek(SeekFrom::Start(offset)) - .map_err(Error::SeekingFile)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; loop { let this_count = min(CHUNK_SIZE as u64, size - read_count) as usize; let nread = reader .read(&mut buf[..this_count]) - .map_err(Error::ReadingData)?; - writer.write(&buf[..nread]).map_err(Error::WritingData)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingData(e)))?; + writer + .write(&buf[..nread]) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingData(e)))?; read_count += nread as u64; if nread == 0 || read_count == size { break; @@ -1742,7 +2222,7 @@ where Ok(()) } -fn convert_reader_writer(reader: &mut R, writer: &mut W, size: u64) -> Result<()> +fn convert_reader_writer(reader: &mut R, writer: &mut W, size: u64) -> BlockResult<()> where R: Read + Seek + SeekHole, W: Write + Seek, @@ -1750,19 +2230,28 @@ where let mut offset = 0; while offset < size { // Find the next range of data. - let next_data = match reader.seek_data(offset).map_err(Error::SeekingFile)? { + let next_data = match reader + .seek_data(offset) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))? + { Some(o) => o, None => { // No more data in the file. break; } }; - let next_hole = match reader.seek_hole(next_data).map_err(Error::SeekingFile)? { + let next_hole = match reader + .seek_hole(next_data) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))? + { Some(o) => o, None => { // This should not happen - there should always be at least one hole // after any data. - return Err(Error::SeekingFile(io::Error::from_raw_os_error(EINVAL))); + return Err(BlockError::new( + BlockErrorKind::Io, + Error::SeekingFile(io::Error::from_raw_os_error(EINVAL)), + )); } }; let count = next_hole - next_data; @@ -1773,19 +2262,26 @@ where Ok(()) } -fn convert_reader(reader: &mut R, dst_file: RawFile, dst_type: ImageType) -> Result<()> +fn convert_reader(reader: &mut R, dst_file: RawFile, dst_type: ImageType) -> BlockResult<()> where R: Read + Seek + SeekHole, { - let src_size = reader.seek(SeekFrom::End(0)).map_err(Error::SeekingFile)?; - reader.rewind().map_err(Error::SeekingFile)?; + let src_size = reader + .seek(SeekFrom::End(0)) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + reader + .rewind() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; // Ensure the destination file is empty before writing to it. - dst_file.set_len(0).map_err(Error::SettingFileSize)?; + dst_file + .set_len(0) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SettingFileSize(e)))?; match dst_type { ImageType::Qcow2 => { - let mut dst_writer = QcowFile::new(dst_file, 3, src_size)?; + let mut dst_writer = QcowFile::new(dst_file, 3, src_size, true) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; convert_reader_writer(reader, &mut dst_writer, src_size) } ImageType::Raw => { @@ -1794,7 +2290,7 @@ where // of the desired size. dst_writer .set_len(src_size) - .map_err(Error::SettingFileSize)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SettingFileSize(e)))?; convert_reader_writer(reader, &mut dst_writer, src_size) } } @@ -1808,12 +2304,13 @@ pub fn convert( dst_file: RawFile, dst_type: ImageType, src_max_nesting_depth: u32, -) -> Result<()> { +) -> BlockResult<()> { let src_type = detect_image_type(&mut src_file)?; match src_type { ImageType::Qcow2 => { let mut src_reader = - QcowFile::from_with_nesting_depth(src_file, src_max_nesting_depth)?; + QcowFile::from_with_nesting_depth(src_file, src_max_nesting_depth, true) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; convert_reader(&mut src_reader, dst_file, dst_type) } ImageType::Raw => { @@ -1825,22 +2322,27 @@ pub fn convert( } /// Detect the type of an image file by checking for a valid qcow2 header. -pub fn detect_image_type(file: &mut RawFile) -> Result { - let orig_seek = file.stream_position().map_err(Error::SeekingFile)?; - file.rewind().map_err(Error::SeekingFile)?; - let magic = file.read_u32::().map_err(Error::ReadingHeader)?; +pub fn detect_image_type(file: &mut RawFile) -> BlockResult { + let orig_seek = file + .stream_position() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + file.rewind() + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; + let magic = u32::read_be(file) + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?; let image_type = if magic == QCOW_MAGIC { ImageType::Qcow2 } else { ImageType::Raw }; file.seek(SeekFrom::Start(orig_seek)) - .map_err(Error::SeekingFile)?; + .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; Ok(image_type) } #[cfg(test)] -mod tests { +mod unit_tests { + use std::error::Error as StdError; use std::fs::File; use std::path::Path; @@ -1848,7 +2350,9 @@ mod tests { use vmm_sys_util::tempfile::TempFile; use vmm_sys_util::write_zeroes::WriteZeroes; + use super::util::{COMPRESSED_FLAG, ZERO_FLAG}; use super::*; + use crate::qcow_common::unit_tests::compress_allocated_clusters; fn valid_header_v3() -> Vec { vec![ @@ -1935,7 +2439,7 @@ mod tests { F: FnMut(QcowFile), { let tmp: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), direct); - let qcow_file = QcowFile::new(tmp, 3, file_size).unwrap(); + let qcow_file = QcowFile::new(tmp, 3, file_size, true).unwrap(); testfn(qcow_file); // File closed when the function exits. } @@ -2023,10 +2527,13 @@ mod tests { disk_file.rewind().unwrap(); let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header."); assert_eq!( - header.backing_file_path, + header.backing_file.as_ref().map(|bf| bf.path.clone()), Some(String::from("/my/path/to/a/file")) ); - assert_eq!(read_header.backing_file_path, header.backing_file_path); + assert_eq!( + read_header.backing_file.as_ref().map(|bf| &bf.path), + header.backing_file.as_ref().map(|bf| &bf.path) + ); } #[test] @@ -2040,10 +2547,125 @@ mod tests { disk_file.rewind().unwrap(); let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header."); assert_eq!( - header.backing_file_path, + header.backing_file.as_ref().map(|bf| bf.path.clone()), Some(String::from("/my/path/to/a/file")) ); - assert_eq!(read_header.backing_file_path, header.backing_file_path); + assert_eq!( + read_header.backing_file.as_ref().map(|bf| &bf.path), + header.backing_file.as_ref().map(|bf| &bf.path) + ); + } + + /// Helper to create a test file with header extensions + fn create_header_with_extension(ext_type: u32, ext_data: &[u8]) -> (RawFile, QcowHeader) { + let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None) + .expect("Failed to create header."); + + let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false); + header.write_to(&mut disk_file).unwrap(); + + // Write extension + disk_file + .seek(SeekFrom::Start(header.header_size as u64)) + .unwrap(); + u32::write_be(&mut disk_file, ext_type).unwrap(); + u32::write_be(&mut disk_file, ext_data.len() as u32).unwrap(); + disk_file.write_all(ext_data).unwrap(); + + // Add padding to 8-byte boundary + let padding = (8 - (ext_data.len() % 8)) % 8; + if padding > 0 { + disk_file.write_all(&vec![0u8; padding]).unwrap(); + } + + u32::write_be(&mut disk_file, HEADER_EXT_END).unwrap(); + + disk_file.rewind().unwrap(); + + (disk_file, header) + } + + #[test] + fn read_header_extensions_unknown_extension() { + let (mut disk_file, mut header) = create_header_with_extension( + 0x12345678, // unknown type + "test".as_bytes(), + ); + + // Extension parsing needs a backing file to set format on + header.backing_file = Some(BackingFileConfig { + path: "/test/backing".to_string(), + format: None, + }); + + QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap(); + assert_eq!(header.backing_file.as_ref().and_then(|bf| bf.format), None); + } + + #[test] + fn read_header_extensions_raw_format() { + let (mut disk_file, mut header) = + create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "raw".as_bytes()); + + header.backing_file = Some(BackingFileConfig { + path: "/test/backing".to_string(), + format: None, + }); + + QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap(); + assert_eq!( + header.backing_file.as_ref().and_then(|bf| bf.format), + Some(ImageType::Raw) + ); + } + + #[test] + fn read_header_extensions_qcow2_format() { + let (mut disk_file, mut header) = + create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "qcow2".as_bytes()); + + header.backing_file = Some(BackingFileConfig { + path: "/test/backing".to_string(), + format: None, + }); + + QcowHeader::read_header_extensions(&mut disk_file, &mut header, None).unwrap(); + assert_eq!( + header.backing_file.as_ref().and_then(|bf| bf.format), + Some(ImageType::Qcow2) + ); + } + + #[test] + fn read_header_extensions_invalid_format() { + let (mut disk_file, mut header) = + create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "vmdk".as_bytes()); + + header.backing_file = Some(BackingFileConfig { + path: "/test/backing".to_string(), + format: None, + }); + + let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header, None); + assert!(matches!( + result.unwrap_err(), + Error::UnsupportedBackingFileFormat(_) + )); + } + + #[test] + fn read_header_extensions_invalid_utf8() { + let (mut disk_file, mut header) = create_header_with_extension( + HEADER_EXT_BACKING_FORMAT, + &[0xFF, 0xFE, 0xFD], // invalid UTF-8 + ); + + let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header, None); + // Should fail with InvalidBackingFileName error + assert!(matches!( + result.unwrap_err(), + Error::InvalidBackingFileName(_) + )); } #[test] @@ -2057,7 +2679,7 @@ mod tests { .expect("Failed to write header to shm."); disk_file.rewind().unwrap(); // The maximum nesting depth is 0, which means backing file is not allowed. - QcowFile::from_with_nesting_depth(disk_file, 0).unwrap(); + QcowFile::from_with_nesting_depth(disk_file, 0, true).unwrap(); } #[test] @@ -2072,8 +2694,12 @@ mod tests { .expect("Failed to write header to shm."); disk_file.rewind().unwrap(); // The maximum nesting depth is 0, which means backing file is not allowed. - let res = QcowFile::from_with_nesting_depth(disk_file, 0); - assert!(matches!(res.unwrap_err(), Error::MaxNestingDepthExceeded)); + let res = QcowFile::from_with_nesting_depth(disk_file, 0, true); + let err = res.unwrap_err(); + assert!(matches!(err.kind(), BlockErrorKind::Overflow)); + let source = StdError::source(&err).unwrap(); + let qcow_err = source.downcast_ref::().unwrap(); + assert!(matches!(qcow_err, Error::MaxNestingDepthExceeded)); } /// Create a qcow2 file with itself as its backing file. @@ -2103,6 +2729,7 @@ mod tests { false, ), MAX_NESTING_DEPTH, + true, ) .expect_err("Opening qcow file with itself as backing file should fail."); @@ -2131,12 +2758,226 @@ mod tests { #[test] fn invalid_refcount_order() { let mut header = valid_header_v3(); - header[99] = 2; + header[99] = 7; with_basic_file(&header, |disk_file: RawFile| { QcowFile::from(disk_file).expect_err("Invalid refcount order worked."); }); } + /// Test all valid refcount orders (0-6) can be opened. + #[test] + fn refcount_all_orders() { + for order in 0..=6u8 { + let mut header = valid_header_v3(); + header[99] = order; + with_basic_file(&header, |disk_file: RawFile| { + QcowFile::from(disk_file).expect("refcount order should work"); + }); + } + } + + /// Test write/read roundtrip for all refcount orders. + #[test] + fn refcount_all_orders_write_read() { + for order in 0..=6u8 { + let mut header = valid_header_v3(); + header[99] = order; + with_basic_file(&header, |disk_file: RawFile| { + let mut q = QcowFile::from(disk_file).unwrap(); + let test_data = b"test data for refcount"; + + // Write and read back + q.write_all(test_data).unwrap(); + q.rewind().unwrap(); + let mut buf = vec![0u8; test_data.len()]; + q.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, test_data); + + // Write to another cluster + q.seek(SeekFrom::Start(0x10000)).unwrap(); + q.write_all(test_data).unwrap(); + q.seek(SeekFrom::Start(0x10000)).unwrap(); + q.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, test_data); + }); + } + } + + /// Test overwrite and multi-cluster allocation for all refcount orders. + #[test] + fn refcount_all_orders_overwrite() { + for order in 0..=6u8 { + let mut header = valid_header_v3(); + header[99] = order; + with_basic_file(&header, |disk_file: RawFile| { + let mut q = QcowFile::from(disk_file).unwrap(); + + // Write then overwrite + q.write_all(b"initial data here!!!").unwrap(); + q.rewind().unwrap(); + let new_data = b"overwritten data!!!!"; + q.write_all(new_data).unwrap(); + q.rewind().unwrap(); + let mut buf = vec![0u8; new_data.len()]; + q.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, new_data); + + // Allocate multiple clusters + let cluster_size = 0x10000u64; + for i in 1..4u64 { + q.seek(SeekFrom::Start(i * cluster_size)).unwrap(); + q.write_all(b"cluster data").unwrap(); + } + for i in 1..4u64 { + let mut cluster_buf = vec![0u8; 12]; + q.seek(SeekFrom::Start(i * cluster_size)).unwrap(); + q.read_exact(&mut cluster_buf).unwrap(); + assert_eq!(&cluster_buf, b"cluster data"); + } + }); + } + } + + /// Test L2 cache eviction for all refcount orders. + #[test] + fn refcount_all_orders_l2_eviction() { + for order in 0..=6u8 { + let mut header = valid_header_v3(); + header[99] = order; + with_basic_file(&header, |disk_file: RawFile| { + let mut q = QcowFile::from(disk_file).unwrap(); + + // L2 cache has 100 entries. Write to >100 regions to force eviction. + let cluster_size = 0x10000u64; + let l2_coverage = cluster_size * (cluster_size / 8); + + for i in 0..110u64 { + q.seek(SeekFrom::Start(i * l2_coverage)).unwrap(); + q.write_all(b"eviction test").unwrap(); + } + + // Verify evicted regions can be re-read + for i in [0u64, 1, 50, 100, 109] { + let mut buf = vec![0u8; 13]; + q.seek(SeekFrom::Start(i * l2_coverage)).unwrap(); + q.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"eviction test"); + } + }); + } + } + + /// Test sub-byte refcount read/write roundtrip with max values. + #[test] + fn refcount_subbyte_max_values() { + for (bits, max_val) in [(1u64, 1u64), (2, 3), (4, 15)] { + let file = vmm_sys_util::tempfile::TempFile::new().unwrap().into_file(); + let cluster_size = 0x10000u64; + file.set_len(cluster_size * 2).unwrap(); + let raw = RawFile::new(file, false); + let mut qcow_raw = QcowRawFile::from(raw, cluster_size, bits).unwrap(); + + let entries = (cluster_size * 8 / bits) as usize; + let mut table: Vec = (0..entries as u64).map(|i| i % (max_val + 1)).collect(); + table[0] = max_val; + table[entries - 1] = max_val; + + qcow_raw.write_refcount_block(cluster_size, &table).unwrap(); + let read_table = qcow_raw.read_refcount_block(cluster_size).unwrap(); + + assert_eq!(read_table.len(), entries); + for (i, (&written, &read)) in table.iter().zip(read_table.iter()).enumerate() { + assert_eq!(read, written & max_val, "{bits}-bit entry {i} mismatch"); + } + } + } + + /// Test byte-aligned refcounts with max values. + #[test] + fn refcount_byte_aligned_large_values() { + for (bits, test_val) in [ + (8u64, 0xFFu64), + (16, 0xFFFFu64), + (32, 0xFFFF_FFFFu64), + (64, u64::MAX), + ] { + let file = vmm_sys_util::tempfile::TempFile::new().unwrap().into_file(); + let cluster_size = 0x10000u64; + file.set_len(cluster_size * 2).unwrap(); + let raw = RawFile::new(file, false); + let mut qcow_raw = QcowRawFile::from(raw, cluster_size, bits).unwrap(); + + let entries = (cluster_size * 8 / bits) as usize; + let mut table: Vec = vec![0; entries]; + table[0] = test_val; + table[1] = 1; + table[entries - 1] = test_val; + + qcow_raw.write_refcount_block(cluster_size, &table).unwrap(); + let read_table = qcow_raw.read_refcount_block(cluster_size).unwrap(); + + assert_eq!(read_table[0], test_val); + assert_eq!(read_table[1], 1); + assert_eq!(read_table[entries - 1], test_val); + } + } + + /// Test RefcountOverflow error when exceeding max refcount value. + #[test] + fn refcount_overflow_returns_error() { + use super::refcount::Error as RefcountError; + + for (refcount_bits, max_val) in [(1u64, 1u64), (2, 3), (4, 15)] { + let file = vmm_sys_util::tempfile::TempFile::new().unwrap().into_file(); + let cluster_size = 0x10000u64; + let refcount_block_entries = cluster_size * 8 / refcount_bits; + file.set_len(cluster_size * 3).unwrap(); + + let raw = RawFile::new(file, false); + let mut qcow_raw = QcowRawFile::from(raw, cluster_size, refcount_bits).unwrap(); + + // Set up refcount table pointing to refcount block + let refcount_table_offset = cluster_size; + qcow_raw + .file_mut() + .seek(SeekFrom::Start(refcount_table_offset)) + .unwrap(); + qcow_raw + .file_mut() + .write_all(&(cluster_size * 2).to_be_bytes()) + .unwrap(); + + let zeros = vec![0u64; refcount_block_entries as usize]; + qcow_raw + .write_refcount_block(cluster_size * 2, &zeros) + .unwrap(); + + let mut refcount = RefCount::new( + &mut qcow_raw, + refcount_table_offset, + 1, + refcount_block_entries, + cluster_size, + refcount_bits, + ) + .unwrap(); + + // Overflow should fail + let result = refcount.set_cluster_refcount(&mut qcow_raw, 0, max_val + 1, None); + assert!( + matches!(result, Err(RefcountError::RefcountOverflow { .. })), + "{refcount_bits}-bit: expected overflow error" + ); + + // Max value should not overflow + let result = refcount.set_cluster_refcount(&mut qcow_raw, 0, max_val, None); + assert!( + !matches!(result, Err(RefcountError::RefcountOverflow { .. })), + "{refcount_bits}-bit: max value should not overflow" + ); + } + } + #[test] fn invalid_cluster_bits() { let mut header = valid_header_v3(); @@ -2184,6 +3025,26 @@ mod tests { }); } + #[test] + fn test_l2_entry_zero_flag() { + let empty_entry: u64 = 0; + let standard_entry: u64 = 0x1000; + let zero_flag_entry: u64 = 0x1000 | ZERO_FLAG; + let compressed_entry: u64 = COMPRESSED_FLAG; + + assert!(l2_entry_is_empty(empty_entry)); + assert!(!l2_entry_is_empty(standard_entry)); + + assert!(!l2_entry_is_compressed(standard_entry)); + assert!(l2_entry_is_compressed(compressed_entry)); + + assert!(!l2_entry_is_zero(standard_entry)); + assert!(l2_entry_is_zero(zero_flag_entry)); + + // Note: l2_entry_is_zero() only checks bit 0, so compressed entries + // must be checked first as the code does in file_read. + } + #[test] fn test_header_1_tb_file() { let mut header = test_huge_header(); @@ -2255,19 +3116,183 @@ mod tests { } #[test] - fn offset_write_read() { - with_basic_file(&valid_header_v3(), |disk_file: RawFile| { - let mut q = QcowFile::from(disk_file).unwrap(); - let b = [0x55u8; 0x1000]; - q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek."); - q.write_all(&b).expect("Failed to write test string."); - let mut buf = [0u8; 4]; - q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek."); - q.read_exact(&mut buf).expect("Failed to read."); - assert_eq!(buf[0], 0x55); + fn offset_write_read() { + with_basic_file(&valid_header_v3(), |disk_file: RawFile| { + let mut q = QcowFile::from(disk_file).unwrap(); + let b = [0x55u8; 0x1000]; + q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek."); + q.write_all(&b).expect("Failed to write test string."); + let mut buf = [0u8; 4]; + q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek."); + q.read_exact(&mut buf).expect("Failed to read."); + assert_eq!(buf[0], 0x55); + }); + } + + #[test] + fn resize_grow_within_l1() { + with_default_file(0x10_0000, false, |mut q| { + let original_size = q.virtual_size(); + assert_eq!(original_size, 0x10_0000); + + q.resize(original_size) + .expect("Resize to same size should succeed"); + assert_eq!(q.virtual_size(), original_size); + }); + } + + #[test] + fn resize_grow_with_l1_growth() { + let initial_size = 1024 * 1024; // 1 MB + let new_size = 600 * 1024 * 1024; // 600 MB + + let tmp: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false); + let mut q = QcowFile::new(tmp, 3, initial_size, true).unwrap(); + + let original_l1_size = q.header().l1_size; + assert_eq!(q.virtual_size(), initial_size); + + let test_data = b"Hello, QCOW resize test!"; + q.rewind().unwrap(); + q.write_all(test_data).unwrap(); + + q.resize(new_size).expect("Resize should succeed"); + assert_eq!(q.virtual_size(), new_size); + + assert!(q.header().l1_size > original_l1_size); + + // Verify original data is still intact + let mut buf = vec![0u8; test_data.len()]; + q.rewind().unwrap(); + q.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, test_data); + + let new_offset = new_size - 0x10000; // 64KB before end + q.seek(SeekFrom::Start(new_offset)).unwrap(); + let new_data = b"Data at new end!"; + q.write_all(new_data).unwrap(); + + let mut buf2 = vec![0u8; new_data.len()]; + q.seek(SeekFrom::Start(new_offset)).unwrap(); + q.read_exact(&mut buf2).unwrap(); + assert_eq!(&buf2, new_data); + } + + #[test] + fn resize_shrink_fails() { + with_default_file(0x10_0000, false, |mut q| { + let original_size = q.virtual_size(); + let smaller_size = original_size / 2; + + let result = q.resize(smaller_size); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature)); + assert!(matches!( + err.downcast_ref::(), + Some(Error::ShrinkNotSupported) + )); + + assert_eq!(q.virtual_size(), original_size); }); } + #[test] + fn resize_with_backing_file_fails() { + let backing_temp = TempFile::new().unwrap(); + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + let backing_size = 1024 * 1024; // 1 MB + + { + let backing_raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let _backing_qcow = QcowFile::new(backing_raw, 3, backing_size, true).unwrap(); + } + + let overlay_file = TempFile::new().unwrap(); + let overlay_raw = RawFile::new(overlay_file.into_file(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let mut overlay = + QcowFile::new_from_backing(overlay_raw, 3, backing_size, &backing_config, true) + .unwrap(); + + assert_eq!(overlay.virtual_size(), backing_size); + + let result = overlay.resize(backing_size * 2); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature)); + assert!(matches!( + err.downcast_ref::(), + Some(Error::ResizeWithBackingFile) + )); + + assert_eq!(overlay.virtual_size(), backing_size); + } + + #[test] + fn read_beyond_backing_file_returns_zeros() { + let backing_temp = TempFile::new().unwrap(); + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + let backing_size = 1024 * 1024; + + { + let backing_raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut backing_qcow = QcowFile::new(backing_raw, 3, backing_size, true).unwrap(); + let data = b"BACKING_DATA"; + backing_qcow.rewind().unwrap(); + backing_qcow.write_all(data).unwrap(); + let boundary_data = [0xAAu8; 512]; + backing_qcow + .seek(SeekFrom::Start(backing_size - 512)) + .unwrap(); + backing_qcow.write_all(&boundary_data).unwrap(); + backing_qcow.flush().unwrap(); + } + + let overlay_file = TempFile::new().unwrap(); + let overlay_raw = RawFile::new(overlay_file.into_file(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let overlay_size = backing_size * 2; // 2x the backing size + let mut overlay = + QcowFile::new_from_backing(overlay_raw, 3, overlay_size, &backing_config, true) + .unwrap(); + + assert_eq!(overlay.virtual_size(), overlay_size); + + let mut buf = vec![0u8; 12]; + overlay.rewind().unwrap(); + overlay.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"BACKING_DATA"); + + let offset_beyond = backing_size + 4096; + let mut beyond_buf = vec![0xFFu8; 4096]; + overlay.seek(SeekFrom::Start(offset_beyond)).unwrap(); + overlay.read_exact(&mut beyond_buf).unwrap(); + assert!( + beyond_buf.iter().all(|&b| b == 0), + "Read beyond backing file should return zeros" + ); + + let offset_at_boundary = backing_size - 512; + let mut boundary_buf = vec![0xFFu8; 1024]; // 512 in backing, 512 beyond + overlay.seek(SeekFrom::Start(offset_at_boundary)).unwrap(); + overlay.read_exact(&mut boundary_buf).unwrap(); + assert!( + boundary_buf[..512].iter().all(|&b| b == 0xAA), + "Portion within backing file should contain backing data" + ); + assert!( + boundary_buf[512..].iter().all(|&b| b == 0), + "Portion beyond backing file should be zeros" + ); + } + #[test] fn write_zeroes_read() { with_basic_file(&valid_header_v3(), |disk_file: RawFile| { @@ -2315,6 +3340,48 @@ mod tests { }); } + #[test] + fn discard_sets_zero_flag() { + with_basic_file(&valid_header_v3(), |disk_file: RawFile| { + let mut q = QcowFile::from(disk_file).unwrap(); + + // Write some test data to allocate a cluster + let test_data = [0x42u8; 4096]; + q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek."); + q.write_all(&test_data).expect("Failed to write test data."); + + // Verify data was written + let mut buf = [0u8; 4096]; + q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek."); + q.read_exact(&mut buf).expect("Failed to read."); + assert_eq!(buf[0], 0x42); + assert_eq!(buf[4095], 0x42); + + // DISCARD the full cluster (via write_zeroes which calls punch_hole) + q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek."); + let nwritten = q.write_zeroes(4096).expect("Failed to discard cluster."); + assert_eq!(nwritten, 4096); + + // Verify reads now return zeros (due to zero flag) + q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek."); + q.read_exact(&mut buf).expect("Failed to read."); + assert_eq!(buf[0], 0); + assert_eq!(buf[4095], 0); + + // Write new data to the trimmed cluster + let new_data = [0x99u8; 4096]; + q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek."); + q.write_all(&new_data) + .expect("Failed to write to trimmed cluster."); + + // Verify new data can be read (cluster was reallocated) + q.seek(SeekFrom::Start(0x10000)).expect("Failed to seek."); + q.read_exact(&mut buf).expect("Failed to read."); + assert_eq!(buf[0], 0x99); + assert_eq!(buf[4095], 0x99); + }); + } + #[test] fn test_header() { with_basic_file(&valid_header_v2(), |disk_file: RawFile| { @@ -2768,8 +3835,6 @@ mod tests { assert_eq!(orig, read); } } - - assert_eq!(qcow_file.first_zero_refcount().unwrap(), None); }); } @@ -2907,10 +3972,645 @@ mod tests { with_basic_file(&valid_header_v3(), |mut disk_file: RawFile| { let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header."); let cluster_size = 65536; - let mut raw_file = - QcowRawFile::from(disk_file, cluster_size).expect("Failed to create QcowRawFile."); + let refcount_bits = 1u64 << header.refcount_order; + let mut raw_file = QcowRawFile::from(disk_file, cluster_size, refcount_bits) + .expect("Failed to create QcowRawFile."); QcowFile::rebuild_refcounts(&mut raw_file, header) .expect("Failed to rebuild recounts."); }); } + + // Helper to create a v3 header with specific incompatible feature bits set + fn header_v3_with_incompat_features(features: u64) -> Vec { + let mut header = valid_header_v3(); + // incompatible_features is at offset 72, big-endian u64 + header[72..80].copy_from_slice(&features.to_be_bytes()); + header + } + + // Helper to create a v3 header with specific autoclear feature bits set + fn header_v3_with_autoclear_features(features: u64) -> Vec { + let mut header = valid_header_v3(); + let offset = AUTOCLEAR_FEATURES_OFFSET as usize; + header[offset..offset + 8].copy_from_slice(&features.to_be_bytes()); + header + } + + #[test] + fn accept_incompat_dirty_bit() { + let header = header_v3_with_incompat_features(1 << 0); + with_basic_file(&header, |disk_file: RawFile| { + let result = QcowFile::from(disk_file); + assert!( + result.is_ok(), + "Expected dirty bit to be accepted, got: {result:?}" + ); + }); + } + + #[test] + fn reject_corrupt_bit_for_writable_open() { + // Bit 1: corrupt - image metadata is corrupted + let header = header_v3_with_incompat_features(1 << 1); + with_basic_file(&header, |disk_file: RawFile| { + let result = QcowFile::from(disk_file); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err.kind(), BlockErrorKind::CorruptImage), + "Expected CorruptImage error, got: {err:?}" + ); + }); + } + + #[test] + fn reject_unsupported_incompat_external_data_bit() { + // Bit 2: external data file + let header = header_v3_with_incompat_features(1 << 2); + with_basic_file(&header, |disk_file: RawFile| { + let result = QcowFile::from(disk_file); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature)); + let source = StdError::source(&err).unwrap(); + let qcow_err = source.downcast_ref::().unwrap(); + assert!( + matches!(qcow_err, Error::UnsupportedFeature(v) if v.to_string().contains("external")), + "Expected UnsupportedFeature error mentioning external, got: {err:?}" + ); + }); + } + + #[test] + fn reject_unsupported_incompat_extended_l2_bit() { + // Bit 4: extended L2 entries + let header = header_v3_with_incompat_features(1 << 4); + with_basic_file(&header, |disk_file: RawFile| { + let result = QcowFile::from(disk_file); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature)); + let source = StdError::source(&err).unwrap(); + let qcow_err = source.downcast_ref::().unwrap(); + assert!( + matches!(qcow_err, Error::UnsupportedFeature(v) if v.to_string().contains("extended")), + "Expected UnsupportedFeature error mentioning extended, got: {err:?}" + ); + }); + } + + #[test] + fn reject_multiple_unsupported_incompat_bits() { + // Multiple unsupported bits: external data (2) + extended L2 (4) + let header = header_v3_with_incompat_features((1 << 2) | (1 << 4)); + with_basic_file(&header, |disk_file: RawFile| { + let result = QcowFile::from(disk_file); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err().kind(), + BlockErrorKind::UnsupportedFeature + )); + }); + } + + #[test] + fn reject_unknown_incompat_bit() { + // Unknown bit 5 (not defined in spec) + let header = header_v3_with_incompat_features(1 << 5); + with_basic_file(&header, |disk_file: RawFile| { + let result = QcowFile::from(disk_file); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature)); + let source = StdError::source(&err).unwrap(); + let qcow_err = source.downcast_ref::().unwrap(); + assert!( + matches!(qcow_err, Error::UnsupportedFeature(v) if v.to_string().contains("unknown")), + "Expected UnsupportedFeature error mentioning unknown, got: {err:?}" + ); + }); + } + + #[test] + fn dirty_bit_set_on_open_cleared_on_close_v3() { + // Test that the dirty bit is set when a v3 image is opened and cleared when it's closed + let header = valid_header_v3(); + with_basic_file(&header, |mut disk_file: RawFile| { + // Verify dirty bit is not set initially + disk_file + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + let features_before = u64::read_be(&mut disk_file).unwrap(); + assert_eq!( + features_before & IncompatFeatures::DIRTY.bits(), + 0, + "Dirty bit should not be set initially" + ); + + // Open the file - this should set the dirty bit + disk_file.rewind().unwrap(); + { + let qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + + // Verify dirty bit is set while file is open + disk_file + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + let features_during = u64::read_be(&mut disk_file).unwrap(); + assert_ne!( + features_during & IncompatFeatures::DIRTY.bits(), + 0, + "Dirty bit should be set while file is open" + ); + + drop(qcow); // Close the file + } + + // Verify dirty bit is cleared after close + disk_file + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + let features_after = u64::read_be(&mut disk_file).unwrap(); + assert_eq!( + features_after & IncompatFeatures::DIRTY.bits(), + 0, + "Dirty bit should be cleared after close" + ); + }); + } + + #[test] + fn dirty_bit_not_used_for_v2() { + // Test that v2 images don't use the dirty bit (no incompatible_features field) + let header = valid_header_v2(); + with_basic_file(&header, |mut disk_file: RawFile| { + // Open and close v2 file - should work without touching offset 72 + disk_file.rewind().unwrap(); + let qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + assert_eq!(qcow.header.version, 2, "Should be a v2 file"); + drop(qcow); + }); + } + + #[test] + fn dirty_bit_not_set_for_readonly_v3() { + // Test that read-only v3 files don't set the dirty bit (e.g., backing files) + let header = valid_header_v3(); + + // Create a temp file with a valid v3 qcow header + let temp_file = TempFile::new().unwrap(); + let temp_path = temp_file.as_path().to_owned(); + { + let mut file = temp_file.as_file().try_clone().unwrap(); + file.write_all(&header).unwrap(); + file.set_len(0x1_0000_0000).unwrap(); + } + + // Open the file read-only + let readonly_file = OpenOptions::new() + .read(true) + .write(false) + .open(&temp_path) + .unwrap(); + let raw_file = RawFile::new(readonly_file, false); + + // Verify the file is detected as read-only + assert!( + !raw_file.is_writable(), + "File should be detected as read-only" + ); + + // Open as QcowFile - should not set dirty bit for read-only files + let qcow = QcowFile::from(raw_file).unwrap(); + assert!( + !qcow.raw_file.file().is_writable(), + "File should be read-only" + ); + + // Verify dirty bit was not written to disk + let verify_file = OpenOptions::new().read(true).open(&temp_path).unwrap(); + let mut verify_raw = RawFile::new(verify_file, false); + verify_raw + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + let features = u64::read_be(&mut verify_raw).unwrap(); + assert_eq!( + features & IncompatFeatures::DIRTY.bits(), + 0, + "Dirty bit should not be written for read-only files" + ); + } + + #[test] + fn autoclear_features_cleared_on_open() { + let header = header_v3_with_autoclear_features(0xFFFF_FFFF_FFFF_FFFF); + with_basic_file(&header, |mut disk_file: RawFile| { + disk_file + .seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET)) + .unwrap(); + let features_before = u64::read_be(&mut disk_file).unwrap(); + assert_eq!( + features_before, 0xFFFF_FFFF_FFFF_FFFF, + "Autoclear features should be set initially" + ); + + disk_file.rewind().unwrap(); + { + let _qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + } + + disk_file + .seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET)) + .unwrap(); + let features_after = u64::read_be(&mut disk_file).unwrap(); + assert_eq!( + features_after, 0, + "Autoclear features should be cleared after open for write" + ); + }); + } + + #[test] + fn autoclear_features_not_cleared_for_readonly() { + let header = header_v3_with_autoclear_features(0xFFFF_FFFF_FFFF_FFFF); + + let temp_file = TempFile::new().unwrap(); + let temp_path = temp_file.as_path().to_owned(); + { + let mut file = temp_file.as_file().try_clone().unwrap(); + file.write_all(&header).unwrap(); + file.set_len(0x1_0000_0000).unwrap(); + } + + let readonly_file = OpenOptions::new() + .read(true) + .write(false) + .open(&temp_path) + .unwrap(); + let raw_file = RawFile::new(readonly_file, false); + let _qcow = QcowFile::from(raw_file).unwrap(); + drop(_qcow); + + let verify_file = OpenOptions::new().read(true).open(&temp_path).unwrap(); + let mut verify_raw = RawFile::new(verify_file, false); + verify_raw + .seek(SeekFrom::Start(AUTOCLEAR_FEATURES_OFFSET)) + .unwrap(); + let features = u64::read_be(&mut verify_raw).unwrap(); + assert_eq!( + features, 0xFFFF_FFFF_FFFF_FFFF, + "Autoclear features should NOT be cleared for read-only files" + ); + } + + #[test] + fn autoclear_features_v2_ignored() { + let header = valid_header_v2(); + with_basic_file(&header, |mut disk_file: RawFile| { + disk_file.rewind().unwrap(); + let qcow = QcowFile::from(disk_file).unwrap(); + assert_eq!(qcow.header.version, 2); + assert_eq!(qcow.header.autoclear_features, 0); + }); + } + + #[test] + fn corrupt_image_rejected_for_write() { + // Test that a corrupt image cannot be opened for writing + let header = header_v3_with_incompat_features(IncompatFeatures::CORRUPT.bits()); + with_basic_file(&header, |disk_file: RawFile| { + assert!(disk_file.is_writable(), "File should be writable"); + + let result = QcowFile::from(disk_file); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err.kind(), BlockErrorKind::CorruptImage), + "Expected CorruptImage error, got: {err:?}" + ); + }); + } + + #[test] + fn corrupt_image_allowed_readonly() { + // Test that a corrupt image can be opened read-only + let header = header_v3_with_incompat_features(IncompatFeatures::CORRUPT.bits()); + + // Create a temp file with the corrupt header + let temp_file = TempFile::new().unwrap(); + let temp_path = temp_file.as_path().to_owned(); + { + let mut file = temp_file.as_file().try_clone().unwrap(); + file.write_all(&header).unwrap(); + file.set_len(0x1_0000_0000).unwrap(); + } + + let readonly_file = OpenOptions::new() + .read(true) + .write(false) + .open(&temp_path) + .unwrap(); + let raw_file = RawFile::new(readonly_file, false); + assert!(!raw_file.is_writable(), "File should be read-only"); + + let result = QcowFile::from(raw_file); + assert!( + result.is_ok(), + "Corrupt image should be openable read-only, got: {:?}", + result.err() + ); + + let qcow = result.unwrap(); + assert!(qcow.header.is_corrupt(), "Corrupt bit should be set"); + } + + #[test] + fn set_corrupt_bit() { + // Test that set_corrupt_bit correctly sets the corrupt bit + let header = valid_header_v3(); + with_basic_file(&header, |mut disk_file: RawFile| { + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + + assert!(!qcow.header.is_corrupt(), "Should not be corrupt initially"); + + qcow.header + .set_corrupt_bit(qcow.raw_file.file_mut()) + .unwrap(); + + // Verify in memory + assert!(qcow.header.is_corrupt(), "Should be corrupt after set"); + + // Verify on disk + disk_file + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + let features = u64::read_be(&mut disk_file).unwrap(); + assert!( + IncompatFeatures::from_bits_retain(features).contains(IncompatFeatures::CORRUPT), + "Corrupt bit should be set on disk" + ); + }); + } + + #[test] + fn corrupt_bit_persists_with_dirty() { + // Test that both corrupt and dirty bits can coexist + let header = header_v3_with_incompat_features( + IncompatFeatures::CORRUPT.bits() | IncompatFeatures::DIRTY.bits(), + ); + + let temp_file = TempFile::new().unwrap(); + let temp_path = temp_file.as_path().to_owned(); + { + let mut file = temp_file.as_file().try_clone().unwrap(); + file.write_all(&header).unwrap(); + file.set_len(0x1_0000_0000).unwrap(); + } + + // Writable would be rejected due to corrupt bit + let readonly_file = OpenOptions::new() + .read(true) + .write(false) + .open(&temp_path) + .unwrap(); + let raw_file = RawFile::new(readonly_file, false); + + let qcow = QcowFile::from(raw_file).unwrap(); + + let features = IncompatFeatures::from_bits_truncate(qcow.header.incompatible_features); + assert!( + features.contains(IncompatFeatures::CORRUPT), + "Corrupt bit should be set" + ); + assert!( + features.contains(IncompatFeatures::DIRTY), + "Dirty bit should also be set" + ); + } + + /// Helper to check if corrupt bit is set on disk by re-reading the header + fn is_corrupt_on_disk(disk_file: &mut RawFile) -> bool { + disk_file.rewind().unwrap(); + QcowHeader::new(disk_file).unwrap().is_corrupt() + } + + /// Helper to clear the corrupt bit on disk while preserving other bits + fn clear_corrupt_bit_on_disk(disk_file: &mut RawFile) { + disk_file + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + let features = u64::read_be(disk_file).unwrap(); + let mut flags = IncompatFeatures::from_bits_retain(features); + flags.remove(IncompatFeatures::CORRUPT); + disk_file + .seek(SeekFrom::Start(V2_BARE_HEADER_SIZE as u64)) + .unwrap(); + u64::write_be(disk_file, flags.bits()).unwrap(); + assert!( + !is_corrupt_on_disk(disk_file), + "Corrupt bit should be cleared" + ); + } + + /// Helper to corrupt L1 entry by making L2 table address unaligned. + /// + /// Returns true if corruption was applied, i.e. the L1 entry was allocated. + fn corrupt_l1_entry(disk_file: &mut RawFile) -> bool { + let l1_table_offset = 0x0004_0000u64; + disk_file.seek(SeekFrom::Start(l1_table_offset)).unwrap(); + let l1_entry = u64::read_be(disk_file).unwrap(); + if l1_entry != 0 { + let unaligned = l1_entry | 0x200; // Make unaligned + disk_file.seek(SeekFrom::Start(l1_table_offset)).unwrap(); + u64::write_be(disk_file, unaligned).unwrap(); + disk_file.sync_all().unwrap(); + true + } else { + false + } + } + + /// Helper to corrupt L2 entry by making cluster address unaligned. + /// + /// Returns true if corruption was applied, i.e. an allocated non-compressed + /// L2 entry was found. + fn corrupt_l2_entry(disk_file: &mut RawFile) -> bool { + let l1_table_offset = 0x0004_0000u64; + disk_file.seek(SeekFrom::Start(l1_table_offset)).unwrap(); + let l1_entry = u64::read_be(disk_file).unwrap(); + if l1_entry == 0 { + return false; + } + let l2_table_addr = l1_entry & L1_TABLE_OFFSET_MASK; + disk_file.seek(SeekFrom::Start(l2_table_addr)).unwrap(); + let l2_entry = u64::read_be(disk_file).unwrap(); + if l2_entry != 0 && !l2_entry_is_compressed(l2_entry) { + let unaligned = l2_entry | 0x200; + disk_file.seek(SeekFrom::Start(l2_table_addr)).unwrap(); + u64::write_be(disk_file, unaligned).unwrap(); + disk_file.sync_all().unwrap(); + true + } else { + false + } + } + + /// Asserts that read on corrupted disk sets the corrupt bit + fn assert_corruption_on_read(disk_file: &mut RawFile) { + clear_corrupt_bit_on_disk(disk_file); + + disk_file.rewind().unwrap(); + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + let mut buf = [0u8; 16]; + let result = qcow.read(&mut buf); + + assert_eq!( + result.map_err(|e| e.raw_os_error()), + Err(Some(libc::EIO)), + "read should fail with EIO on corrupted image" + ); + assert!( + is_corrupt_on_disk(disk_file), + "Corrupt bit should be set after read" + ); + } + + /// Asserts that write on corrupted disk sets the corrupt bit + fn assert_corruption_on_write(disk_file: &mut RawFile) { + clear_corrupt_bit_on_disk(disk_file); + + disk_file.rewind().unwrap(); + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + let result = qcow.write_all(b"overwrite"); + + assert_eq!( + result.map_err(|e| e.raw_os_error()), + Err(Some(libc::EIO)), + "write should fail with EIO on corrupted image" + ); + assert!( + is_corrupt_on_disk(disk_file), + "Corrupt bit should be set after write" + ); + } + + #[test] + fn corrupt_bit_on_unaligned_l2_address() { + let header = valid_header_v3(); + with_basic_file(&header, |mut disk_file: RawFile| { + { + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + qcow.write_all(b"test data").unwrap(); + } + + assert!( + corrupt_l1_entry(&mut disk_file), + "Failed to corrupt L1 entry - was data written?" + ); + assert_corruption_on_read(&mut disk_file); + }); + } + + #[test] + fn corrupt_bit_on_unaligned_cluster_address_read() { + let header = valid_header_v3(); + with_basic_file(&header, |mut disk_file: RawFile| { + { + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + qcow.write_all(b"test data to allocate cluster").unwrap(); + } + + assert!( + corrupt_l2_entry(&mut disk_file), + "Failed to corrupt L2 entry - was cluster allocated?" + ); + assert_corruption_on_read(&mut disk_file); + }); + } + + #[test] + fn corrupt_bit_on_unaligned_cluster_address_write() { + let header = valid_header_v3(); + with_basic_file(&header, |mut disk_file: RawFile| { + { + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + qcow.write_all(b"test data to allocate cluster").unwrap(); + } + + assert!( + corrupt_l2_entry(&mut disk_file), + "Failed to corrupt L2 entry - was cluster allocated?" + ); + assert_corruption_on_write(&mut disk_file); + }); + } + + #[test] + fn corrupt_bit_not_set_on_normal_operations() { + let header = valid_header_v3(); + with_basic_file(&header, |mut disk_file: RawFile| { + { + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + + qcow.write_all(b"test data 1234567890").unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + + let mut buf = [0u8; 20]; + qcow.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"test data 1234567890"); + + qcow.seek(SeekFrom::Start(0x10000)).unwrap(); + qcow.write_all(b"more data").unwrap(); + + qcow.flush().unwrap(); + } + + assert!( + !is_corrupt_on_disk(&mut disk_file), + "Corrupt bit should NOT be set after normal operations" + ); + }); + } + + #[test] + fn corrupt_bit_v2_image_not_affected() { + let header = valid_header_v2(); + with_basic_file(&header, |mut disk_file: RawFile| { + { + let mut qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + qcow.write_all(b"test data").unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + let mut buf = [0u8; 9]; + qcow.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"test data"); + } + + disk_file.rewind().unwrap(); + let qcow = QcowFile::from(disk_file.try_clone().unwrap()).unwrap(); + assert_eq!(qcow.header.version, 2); + }); + } + + #[test] + fn test_compressed_read() { + let cluster_size = 65536usize; + let data: Vec = (0..=255).cycle().take(cluster_size).collect(); + let temp = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw_file, 3, 100 * 1024 * 1024, false).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&data).unwrap(); + qcow.flush().unwrap(); + } + + compress_allocated_clusters(&mut temp.as_file().try_clone().unwrap()); + + let raw_file = RawFile::new(temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::from(raw_file).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + let mut buf = vec![0u8; cluster_size]; + qcow.read_exact(&mut buf).unwrap(); + assert_eq!(buf, data); + } } diff --git a/block/src/qcow/qcow_raw_file.rs b/block/src/qcow/qcow_raw_file.rs index bb4f849ac8..232f6b5a5c 100644 --- a/block/src/qcow/qcow_raw_file.rs +++ b/block/src/qcow/qcow_raw_file.rs @@ -4,34 +4,194 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use std::io::{self, BufWriter, Seek, SeekFrom, Write}; +use std::fmt::Debug; +use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write}; use std::mem::size_of; -use std::os::fd::{AsRawFd, RawFd}; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use vmm_sys_util::write_zeroes::WriteZeroes; use super::RawFile; +// Type aliases for the refcount read/write function pointers +type RefcountReader = fn(&mut RawFile, usize) -> io::Result>; +type RefcountWriter = fn(&mut RawFile, &[u64]) -> io::Result<()>; + +/// Big-endian file access trait. +pub(super) trait BeUint: Sized + Copy { + fn from_be_slice(bytes: &[u8]) -> u64; + fn read_be(r: &mut R) -> io::Result; + fn write_be(w: &mut W, val: Self) -> io::Result<()>; +} + +impl BeUint for u8 { + #[inline(always)] + fn from_be_slice(bytes: &[u8]) -> u64 { + bytes[0] as u64 + } + #[inline(always)] + fn read_be(r: &mut R) -> io::Result { + r.read_u8() + } + #[inline(always)] + fn write_be(w: &mut W, val: Self) -> io::Result<()> { + w.write_u8(val) + } +} + +impl BeUint for u16 { + #[inline(always)] + fn from_be_slice(bytes: &[u8]) -> u64 { + u16::from_be_bytes([bytes[0], bytes[1]]) as u64 + } + #[inline(always)] + fn read_be(r: &mut R) -> io::Result { + r.read_u16::() + } + #[inline(always)] + fn write_be(w: &mut W, val: Self) -> io::Result<()> { + w.write_u16::(val) + } +} + +impl BeUint for u32 { + #[inline(always)] + fn from_be_slice(bytes: &[u8]) -> u64 { + u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64 + } + #[inline(always)] + fn read_be(r: &mut R) -> io::Result { + r.read_u32::() + } + #[inline(always)] + fn write_be(w: &mut W, val: Self) -> io::Result<()> { + w.write_u32::(val) + } +} + +impl BeUint for u64 { + #[inline(always)] + fn from_be_slice(bytes: &[u8]) -> u64 { + u64::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]) + } + #[inline(always)] + fn read_be(r: &mut R) -> io::Result { + r.read_u64::() + } + #[inline(always)] + fn write_be(w: &mut W, val: Self) -> io::Result<()> { + w.write_u64::(val) + } +} + +/// Read byte-aligned refcounts. +fn read_refcount(file: &mut RawFile, count: usize) -> io::Result> { + let bytes_per_entry = size_of::(); + let mut data = vec![0u8; count * bytes_per_entry]; + file.read_exact(&mut data)?; + Ok(data + .chunks_exact(bytes_per_entry) + .map(T::from_be_slice) + .collect()) +} + +/// Write byte-aligned refcounts. +fn write_refcount>(file: &mut RawFile, table: &[u64]) -> io::Result<()> +where + >::Error: Debug, +{ + let bytes_per_entry = size_of::(); + let mut buffer = BufWriter::with_capacity(table.len() * bytes_per_entry, file); + for &val in table { + let converted = T::try_from(val).expect("refcount values are validated on increment"); + T::write_be(&mut buffer, converted)?; + } + buffer.flush() +} + +/// Read sub-byte refcounts. Bit 0 is the least significant bit. +fn read_refcount_subbyte( + file: &mut RawFile, + count: usize, +) -> io::Result> { + const { assert!(BITS == 1 || BITS == 2 || BITS == 4) }; + let entries_per_byte = 8 / BITS; + let mask = (1u64 << BITS) - 1; + let bytes_needed = count.div_ceil(entries_per_byte); + let mut bytes = vec![0u8; bytes_needed]; + file.read_exact(&mut bytes)?; + + let mut table = vec![0u64; count]; + for (i, val) in table.iter_mut().enumerate() { + let byte_idx = i / entries_per_byte; + let bit_offset = (i % entries_per_byte) * BITS; + *val = (bytes[byte_idx] as u64 >> bit_offset) & mask; + } + Ok(table) +} + +/// Write sub-byte refcounts. Bit 0 is the least significant bit. +fn write_refcount_subbyte(file: &mut RawFile, table: &[u64]) -> io::Result<()> { + const { assert!(BITS == 1 || BITS == 2 || BITS == 4) }; + let entries_per_byte = 8 / BITS; + let mask = (1u64 << BITS) - 1; + let mut buffer = BufWriter::with_capacity(table.len().div_ceil(entries_per_byte), file); + + for chunk in table.chunks(entries_per_byte) { + let mut byte = 0u8; + for (i, &val) in chunk.iter().enumerate() { + let bit_offset = i * BITS; + byte |= ((val & mask) << bit_offset) as u8; + } + buffer.write_u8(byte)?; + } + buffer.flush() +} + /// A qcow file. Allows reading/writing clusters and appending clusters. #[derive(Debug)] pub struct QcowRawFile { file: RawFile, cluster_size: u64, cluster_mask: u64, + refcount_block_entries: u64, + read_refcount_fn: RefcountReader, + write_refcount_fn: RefcountWriter, } impl QcowRawFile { /// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not - /// a power of two. - pub fn from(file: RawFile, cluster_size: u64) -> Option { + /// a power of two or refcount_bits is invalid. + pub fn from(file: RawFile, cluster_size: u64, refcount_bits: u64) -> Option { 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::, write_refcount::), + 16 => (read_refcount::, write_refcount::), + 32 => (read_refcount::, write_refcount::), + 64 => (read_refcount::, write_refcount::), + _ => 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, }) } @@ -61,47 +221,65 @@ impl QcowRawFile { self.read_pointer_table(offset, count, mask) } - /// Writes `table` of u64 pointers to `offset` in the file. - /// `non_zero_flags` will be ORed with all non-zero values in `table`. - /// writing. - pub fn write_pointer_table( + /// Internal helper for creating a buffered writer for pointer tables. + #[inline] + fn setup_pointer_table_writer( &mut self, offset: u64, - table: &[u64], - non_zero_flags: u64, - ) -> io::Result<()> { + entries: &impl Iterator, + ) -> io::Result> { self.file.seek(SeekFrom::Start(offset))?; - let mut buffer = BufWriter::with_capacity(std::mem::size_of_val(table), &mut self.file); - for addr in table { - let val = if *addr == 0 { - 0 - } else { - *addr | non_zero_flags - }; - buffer.write_u64::(val)?; + let my_file = self.file.try_clone()?; + let capacity = entries.size_hint().0 * size_of::(); + Ok(BufWriter::with_capacity(capacity, my_file)) + } + + /// Writes a pointer table to `offset` in the file. + /// Entries are computed on-the-fly by the callback. + pub fn write_pointer_table<'a, T: Copy + 'a>( + &mut self, + offset: u64, + entries: impl Iterator, + mut f: impl FnMut(&mut QcowRawFile, T) -> io::Result, + ) -> io::Result<()> { + let mut buffer = self.setup_pointer_table_writer(offset, &entries)?; + + for addr in entries { + let entry = f(self, *addr)?; + u64::write_be(&mut buffer, entry)?; } + buffer.flush()?; + Ok(()) + } + + /// Writes a pointer table directly without transforming values. + pub fn write_pointer_table_direct<'a>( + &mut self, + offset: u64, + entries: impl Iterator, + ) -> io::Result<()> { + let mut buffer = self.setup_pointer_table_writer(offset, &entries)?; + + for &entry in entries { + u64::write_be(&mut buffer, entry)?; + } + buffer.flush()?; Ok(()) } /// Read a refcount block from the file and returns a Vec containing the block. /// Always returns a cluster's worth of data. - pub fn read_refcount_block(&mut self, offset: u64) -> io::Result> { - let count = self.cluster_size / size_of::() as u64; - let mut table = vec![0; count as usize]; + #[inline] + pub fn read_refcount_block(&mut self, offset: u64) -> io::Result> { self.file.seek(SeekFrom::Start(offset))?; - self.file.read_u16_into::(&mut table)?; - Ok(table) + (self.read_refcount_fn)(&mut self.file, self.refcount_block_entries as usize) } /// Writes a refcount block to the file. - pub fn write_refcount_block(&mut self, offset: u64, table: &[u16]) -> io::Result<()> { + #[inline] + pub fn write_refcount_block(&mut self, offset: u64, table: &[u64]) -> io::Result<()> { self.file.seek(SeekFrom::Start(offset))?; - let mut buffer = BufWriter::with_capacity(std::mem::size_of_val(table), &mut self.file); - - for count in table { - buffer.write_u16::(*count)?; - } - Ok(()) + (self.write_refcount_fn)(&mut self.file, table) } /// Allocates a new cluster at the end of the current file, return the address. @@ -120,6 +298,11 @@ impl QcowRawFile { Ok(Some(new_cluster_address)) } + /// Returns a reference to the underlying file. + pub fn file(&self) -> &RawFile { + &self.file + } + /// Returns a mutable reference to the underlying file. pub fn file_mut(&mut self) -> &mut RawFile { &mut self.file @@ -135,6 +318,11 @@ impl QcowRawFile { address & self.cluster_mask } + /// Returns the base address of the cluster containing `address`. + pub fn cluster_address(&self, address: u64) -> u64 { + address & !self.cluster_mask + } + /// Zeros out a cluster in the file. pub fn zero_cluster(&mut self, address: u64) -> io::Result<()> { let cluster_size = self.cluster_size as usize; @@ -144,11 +332,15 @@ impl QcowRawFile { } /// Writes - pub fn write_cluster(&mut self, address: u64, data: Vec) -> io::Result<()> { + pub fn write_cluster(&mut self, address: u64, data: &[u8]) -> io::Result<()> { let cluster_size = self.cluster_size as usize; self.file.seek(SeekFrom::Start(address))?; self.file.write_all(&data[0..cluster_size]) } + + pub fn physical_size(&self) -> Result { + self.file.metadata().map(|m| m.len()) + } } impl Clone for QcowRawFile { @@ -157,6 +349,9 @@ impl Clone for 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, } } } @@ -166,3 +361,9 @@ impl AsRawFd for QcowRawFile { self.file.as_raw_fd() } } + +impl AsFd for QcowRawFile { + fn as_fd(&self) -> BorrowedFd<'_> { + self.file.as_fd() + } +} diff --git a/block/src/qcow/raw_file.rs b/block/src/qcow/raw_file.rs index cb96376015..fa33478bf9 100644 --- a/block/src/qcow/raw_file.rs +++ b/block/src/qcow/raw_file.rs @@ -8,17 +8,18 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use std::alloc::{alloc_zeroed, dealloc, Layout}; +use std::alloc::{Layout, alloc_zeroed, dealloc}; use std::fs::{File, Metadata}; use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::io::{AsRawFd, RawFd}; use std::slice; -use libc::c_void; +use vmm_sys_util::file_traits::FileSync; use vmm_sys_util::seek_hole::SeekHole; use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; -use crate::BlockBackend; +use crate::{BlockBackend, query_device_size}; #[derive(Debug)] pub struct RawFile { @@ -37,14 +38,7 @@ fn is_valid_alignment(fd: RawFd, alignment: usize) -> bool { assert!(!ptr.is_null()); // SAFETY: FFI call - let ret = unsafe { - ::libc::pread( - fd, - ptr as *mut c_void, - alignment, - alignment.try_into().unwrap(), - ) - }; + let ret = unsafe { ::libc::pread(fd, ptr.cast(), alignment, alignment.try_into().unwrap()) }; // SAFETY: ptr was allocated by alloc_zeroed with layout unsafe { dealloc(ptr, layout) }; @@ -89,9 +83,9 @@ impl RawFile { let align64: u64 = self.alignment.try_into().unwrap(); - (self.position % align64 == 0) - && ((buf.as_ptr() as usize) % self.alignment == 0) - && (buf.len() % self.alignment == 0) + self.position.is_multiple_of(align64) + && (buf.as_ptr() as usize).is_multiple_of(self.alignment) + && buf.len().is_multiple_of(self.alignment) } pub fn set_len(&self, size: u64) -> std::io::Result<()> { @@ -122,6 +116,21 @@ impl RawFile { pub fn is_direct(&self) -> bool { self.direct_io } + + pub fn alignment(&self) -> usize { + self.alignment + } + + /// Returns true if the file was opened with write access. + pub fn is_writable(&self) -> bool { + // SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor + let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) }; + if flags < 0 { + return false; + } + let access_mode = flags & libc::O_ACCMODE; + access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR + } } impl Read for RawFile { @@ -170,7 +179,7 @@ impl Read for RawFile { let ret = unsafe { ::libc::pread64( self.file.as_raw_fd(), - tmp_buf.as_mut_ptr() as *mut c_void, + tmp_buf.as_mut_ptr().cast(), tmp_buf.len(), rounded_pos.try_into().unwrap(), ) @@ -250,7 +259,7 @@ impl Write for RawFile { let ret = unsafe { ::libc::pread64( self.file.as_raw_fd(), - tmp_buf.as_mut_ptr() as *mut c_void, + tmp_buf.as_mut_ptr().cast(), tmp_buf.len(), rounded_pos.try_into().unwrap(), ) @@ -259,7 +268,7 @@ impl Write for RawFile { // SAFETY: tmp_ptr was allocated by alloc_zeroed with layout unsafe { dealloc(tmp_ptr, layout) }; return Err(io::Error::last_os_error()); - }; + } tmp_buf[file_offset..(file_offset + buf_len)].copy_from_slice(buf); @@ -269,7 +278,7 @@ impl Write for RawFile { let ret = unsafe { ::libc::pwrite64( self.file.as_raw_fd(), - tmp_buf.as_ptr() as *const c_void, + tmp_buf.as_ptr().cast(), tmp_buf.len(), rounded_pos.try_into().unwrap(), ) @@ -327,6 +336,12 @@ impl PunchHole for RawFile { } } +impl FileSync for RawFile { + fn fsync(&mut self) -> std::io::Result<()> { + self.file.fsync() + } +} + impl SeekHole for RawFile { fn seek_hole(&mut self, offset: u64) -> std::io::Result> { match self.file.seek_hole(offset) { @@ -354,8 +369,16 @@ impl SeekHole for RawFile { } impl BlockBackend for RawFile { - fn size(&self) -> std::result::Result { - Ok(self.metadata().map_err(crate::Error::RawFileError)?.len()) + fn logical_size(&self) -> std::result::Result { + Ok(query_device_size(&self.file) + .map_err(crate::Error::RawFileError)? + .0) + } + + fn physical_size(&self) -> std::result::Result { + Ok(query_device_size(&self.file) + .map_err(crate::Error::RawFileError)? + .1) } } @@ -375,3 +398,9 @@ impl AsRawFd for RawFile { self.file.as_raw_fd() } } + +impl AsFd for RawFile { + fn as_fd(&self) -> BorrowedFd<'_> { + self.file.as_fd() + } +} diff --git a/block/src/qcow/refcount.rs b/block/src/qcow/refcount.rs index 12ab30202e..5cd61c09b2 100644 --- a/block/src/qcow/refcount.rs +++ b/block/src/qcow/refcount.rs @@ -20,6 +20,9 @@ pub enum Error { /// `InvalidIndex` - Address requested isn't within the range of the disk. #[error("Address requested is not within the range of the disk")] InvalidIndex, + /// `RefblockUnaligned` - Refcount block offset is not cluster aligned. + #[error("Refcount block offset {0:#x} is not cluster aligned")] + RefblockUnaligned(u64), /// `NeedCluster` - Handle this error by reading the cluster and calling the function again. #[error("Cluster with addr={0} needs to be read")] NeedCluster(u64), @@ -29,6 +32,13 @@ pub enum Error { /// `ReadingRefCounts` - Error reading the file into the refcount cache. #[error("Failed to read the file into the refcount cache")] ReadingRefCounts(#[source] io::Error), + /// `RefcountOverflow` - Refcount value exceeds maximum for the refcount width. + #[error("Refcount value {value} exceeds {refcount_bits}-bit max ({max})")] + RefcountOverflow { + value: u64, + max: u64, + refcount_bits: u64, + }, } pub type Result = std::result::Result; @@ -38,16 +48,19 @@ pub type Result = std::result::Result; pub struct RefCount { ref_table: VecCache, refcount_table_offset: u64, - refblock_cache: CacheMap>, + refblock_cache: CacheMap>, refcount_block_entries: u64, // number of refcounts in a cluster. cluster_size: u64, max_valid_cluster_offset: u64, + max_refcount: u64, // maximum refcount value for this image's refcount_order + refcount_bits: u64, // number of bits per refcount entry } impl RefCount { /// Creates a `RefCount` from `file`, reading the refcount table from `refcount_table_offset`. /// `refcount_table_entries` specifies the number of refcount blocks used by this image. /// `refcount_block_entries` indicates the number of refcounts in each refcount block. + /// `refcount_bits` is the number of bits per refcount (1, 2, 4, 8, 16, 32, or 64). /// Each refcount table entry points to a refcount block. pub fn new( raw_file: &mut QcowRawFile, @@ -55,6 +68,7 @@ impl RefCount { refcount_table_entries: u64, refcount_block_entries: u64, cluster_size: u64, + refcount_bits: u64, ) -> io::Result { let ref_table = VecCache::from_vec(raw_file.read_pointer_table( refcount_table_offset, @@ -63,6 +77,11 @@ impl RefCount { )?); let max_valid_cluster_index = (ref_table.len() as u64) * refcount_block_entries - 1; let max_valid_cluster_offset = max_valid_cluster_index * cluster_size; + let max_refcount = if refcount_bits >= 64 { + u64::MAX + } else { + (1u64 << refcount_bits) - 1 + }; Ok(RefCount { ref_table, refcount_table_offset, @@ -70,6 +89,8 @@ impl RefCount { refcount_block_entries, cluster_size, max_valid_cluster_offset, + max_refcount, + refcount_bits, }) } @@ -92,9 +113,17 @@ impl RefCount { &mut self, raw_file: &mut QcowRawFile, cluster_address: u64, - refcount: u16, - mut new_cluster: Option<(u64, VecCache)>, + refcount: u64, + mut new_cluster: Option<(u64, VecCache)>, ) -> Result> { + if refcount > self.max_refcount { + return Err(Error::RefcountOverflow { + value: refcount, + max: self.max_refcount, + refcount_bits: self.refcount_bits, + }); + } + let (table_index, block_index) = self.get_refcount_index(cluster_address); let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?; @@ -119,7 +148,9 @@ impl RefCount { } // Unwrap is safe here as the entry was filled directly above. - let dropped_cluster = if !self.refblock_cache.get(table_index).unwrap().dirty() { + let dropped_cluster = if self.refblock_cache.get(table_index).unwrap().dirty() { + None + } else { // Free the previously used block and use a new one. Writing modified counts to new // blocks keeps the on-disk state consistent even if it's out of date. if let Some((addr, _)) = new_cluster.take() { @@ -128,8 +159,6 @@ impl RefCount { } else { return Err(Error::NeedNewCluster); } - } else { - None }; self.refblock_cache.get_mut(table_index).unwrap()[block_index] = refcount; @@ -156,11 +185,8 @@ impl RefCount { /// Returns true if the table changed since the previous `flush_table()` call. pub fn flush_table(&mut self, raw_file: &mut QcowRawFile) -> io::Result { if self.ref_table.dirty() { - raw_file.write_pointer_table( - self.refcount_table_offset, - self.ref_table.get_values(), - 0, - )?; + raw_file + .write_pointer_table_direct(self.refcount_table_offset, self.ref_table.iter())?; self.ref_table.mark_clean(); Ok(true) } else { @@ -173,12 +199,15 @@ impl RefCount { &mut self, raw_file: &mut QcowRawFile, address: u64, - ) -> Result { + ) -> Result { let (table_index, block_index) = self.get_refcount_index(address); let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?; if block_addr_disk == 0 { return Ok(0); } + if block_addr_disk & (self.cluster_size - 1) != 0 { + return Err(Error::RefblockUnaligned(block_addr_disk)); + } if !self.refblock_cache.contains_key(table_index) { let table = VecCache::from_vec( raw_file @@ -205,7 +234,7 @@ impl RefCount { &mut self, raw_file: &mut QcowRawFile, table_index: usize, - ) -> Result> { + ) -> Result> { let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?; if block_addr_disk == 0 { return Ok(None); diff --git a/block/src/qcow/util.rs b/block/src/qcow/util.rs new file mode 100644 index 0000000000..bc8d017725 --- /dev/null +++ b/block/src/qcow/util.rs @@ -0,0 +1,79 @@ +// 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 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)) +} diff --git a/block/src/qcow/vec_cache.rs b/block/src/qcow/vec_cache.rs index 67068fdded..0646421873 100644 --- a/block/src/qcow/vec_cache.rs +++ b/block/src/qcow/vec_cache.rs @@ -4,10 +4,10 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use std::collections::hash_map::IterMut; use std::collections::HashMap; +use std::collections::hash_map::IterMut; use std::io; -use std::ops::{Index, IndexMut}; +use std::ops::{Deref, Index, IndexMut}; use std::slice::SliceIndex; /// Trait that allows for checking if an implementor is dirty. Useful for types that are cached so @@ -62,6 +62,21 @@ impl VecCache { pub fn len(&self) -> usize { self.vec.len() } + + /// Extends the cache capacity to `new_len` elements. + /// + /// No-op if `new_len <= self.len()`. Allocates a new buffer, copies + /// existing data, and fills new elements with default values. + /// Marks the cache as dirty. + pub fn extend(&mut self, new_len: usize) { + if new_len <= self.vec.len() { + return; + } + let mut new_vec = vec![Default::default(); new_len]; + new_vec[..self.vec.len()].copy_from_slice(&self.vec); + self.vec = new_vec.into_boxed_slice(); + self.dirty = true; + } } impl Cacheable for VecCache { @@ -85,6 +100,14 @@ impl IndexMut for VecCache { } } +impl Deref for VecCache { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec + } +} + #[derive(Clone, Debug)] pub struct CacheMap { capacity: usize, @@ -123,10 +146,10 @@ impl CacheMap { if self.map.len() == self.capacity { // TODO(dgreid) - smarter eviction strategy. let to_evict = *self.map.iter().next().unwrap().0; - if let Some(evicted) = self.map.remove(&to_evict) { - if evicted.dirty() { - write_callback(to_evict, evicted)?; - } + if let Some(evicted) = self.map.remove(&to_evict) + && evicted.dirty() + { + write_callback(to_evict, evicted)?; } } self.map.insert(index, block); @@ -135,7 +158,7 @@ impl CacheMap { } #[cfg(test)] -mod tests { +mod unit_tests { use super::*; struct NumCache(()); diff --git a/block/src/qcow_async.rs b/block/src/qcow_async.rs new file mode 100644 index 0000000000..870095aa66 --- /dev/null +++ b/block/src/qcow_async.rs @@ -0,0 +1,1086 @@ +// Copyright © 2021 Intel Corporation +// +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +//! QCOW2 async disk backend. + +use std::cmp::{max, min}; +use std::collections::VecDeque; +use std::io; +use std::os::unix::io::AsRawFd; +use std::sync::Arc; + +use io_uring::{IoUring, opcode, types}; +use vmm_sys_util::eventfd::EventFd; +use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; + +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; +use crate::qcow::decoder::Decoder; +use crate::qcow::metadata::{ + BackingRead, ClusterReadMapping, ClusterWriteMapping, DeallocAction, QcowMetadata, +}; +use crate::qcow::qcow_raw_file::QcowRawFile; +use crate::qcow_common::{ + AlignedBuf, aligned_pread, aligned_pwrite, decompress_cluster, gather_from_iovecs_into, + pread_alloc, pread_exact, pwrite_all, scatter_to_iovecs, zero_fill_iovecs, +}; +use crate::{BatchRequest, RequestType, SECTOR_SIZE}; + +/// 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 struct QcowAsync { + metadata: Arc, + data_file: QcowRawFile, + backing_file: Option>, + sparse: bool, + /// O_DIRECT alignment requirement (0 = no alignment needed). + alignment: usize, + /// I/O alignment for the AsyncIo trait (at least SECTOR_SIZE). + io_alignment: u64, + cluster_size: u64, + decoder: Arc, + io_uring: IoUring, + eventfd: EventFd, + completion_list: VecDeque<(u64, i32)>, +} + +impl QcowAsync { + pub(crate) fn new( + metadata: Arc, + data_file: QcowRawFile, + backing_file: Option>, + sparse: bool, + ring_depth: u32, + ) -> io::Result { + let alignment = data_file.file().alignment(); + let io_alignment = max(alignment as u64, SECTOR_SIZE); + let io_uring = IoUring::new(ring_depth)?; + let eventfd = EventFd::new(libc::EFD_NONBLOCK)?; + io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?; + + Ok(QcowAsync { + cluster_size: metadata.cluster_size(), + decoder: metadata.decoder(), + metadata, + data_file, + backing_file, + sparse, + alignment, + io_alignment, + io_uring, + eventfd, + completion_list: VecDeque::new(), + }) + } + + fn apply_dealloc_action(&mut self, action: &DeallocAction) { + match action { + DeallocAction::PunchHole { + host_offset, + length, + } => { + let _ = self.data_file.file_mut().punch_hole(*host_offset, *length); + } + DeallocAction::WriteZeroes { + host_offset, + length, + } => { + let _ = self + .data_file + .file_mut() + .write_zeroes_at(*host_offset, *length); + } + } + } +} + +impl AsyncIo for QcowAsync { + fn notifier(&self) -> &EventFd { + &self.eventfd + } + + fn read_vectored( + &mut self, + offset: libc::off_t, + iovecs: &[libc::iovec], + user_data: u64, + ) -> AsyncIoResult<()> { + let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum(); + + if let Some(host_offset) = Self::resolve_read( + &self.metadata, + &self.data_file, + &self.backing_file, + offset as u64, + iovecs, + total_len, + self.alignment, + self.cluster_size, + &*self.decoder, + )? { + let fd = self.data_file.as_raw_fd(); + let (submitter, mut sq, _) = self.io_uring.split(); + + // SAFETY: fd is valid and iovecs point to valid guest memory. + unsafe { + sq.push( + &opcode::Readv::new(types::Fd(fd), iovecs.as_ptr(), iovecs.len() as u32) + .offset(host_offset) + .build() + .user_data(user_data), + ) + .map_err(|_| { + AsyncIoError::ReadVectored(io::Error::other("Submission queue is full")) + })?; + }; + + sq.sync(); + submitter.submit().map_err(AsyncIoError::ReadVectored)?; + } else { + self.completion_list + .push_back((user_data, total_len as i32)); + self.eventfd.write(1).unwrap(); + } + Ok(()) + } + + // 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. + fn write_vectored( + &mut self, + offset: libc::off_t, + iovecs: &[libc::iovec], + user_data: u64, + ) -> AsyncIoResult<()> { + Self::cow_write_sync( + offset as u64, + iovecs, + &self.metadata, + &self.data_file, + &self.backing_file, + self.alignment, + self.cluster_size, + )?; + + let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum(); + self.completion_list + .push_back((user_data, total_len as i32)); + self.eventfd.write(1).unwrap(); + Ok(()) + } + + fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { + self.metadata.flush().map_err(AsyncIoError::Fsync)?; + if let Some(user_data) = user_data { + self.completion_list.push_back((user_data, 0)); + self.eventfd.write(1).unwrap(); + } + Ok(()) + } + + fn next_completed_request(&mut self) -> Option<(u64, i32)> { + // Drain io_uring completions first, then synthetic ones. + self.io_uring + .completion() + .next() + .map(|entry| (entry.user_data(), entry.result())) + .or_else(|| self.completion_list.pop_front()) + } + + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + let virtual_size = self.metadata.virtual_size(); + let cluster_size = self.cluster_size; + + let result = self + .metadata + .deallocate_bytes( + offset, + length as usize, + self.sparse, + virtual_size, + cluster_size, + self.backing_file.as_deref(), + ) + .map_err(AsyncIoError::PunchHole); + + match result { + Ok(actions) => { + for action in &actions { + self.apply_dealloc_action(action); + } + self.completion_list.push_back((user_data, 0)); + self.eventfd.write(1).unwrap(); + Ok(()) + } + Err(e) => { + let errno = if let AsyncIoError::PunchHole(ref io_err) = e { + -io_err.raw_os_error().unwrap_or(libc::EIO) + } else { + -libc::EIO + }; + self.completion_list.push_back((user_data, errno)); + self.eventfd.write(1).unwrap(); + Ok(()) + } + } + } + + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + // For QCOW2, zeroing and hole punching are the same operation. + // Both discard guest data so the range reads back as zero. + self.punch_hole(offset, length, user_data) + } + + fn batch_requests_enabled(&self) -> bool { + true + } + + fn alignment(&self) -> u64 { + self.io_alignment + } + + fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> { + let (submitter, mut sq, _) = self.io_uring.split(); + let mut needs_submit = false; + let mut sync_completions: Vec<(u64, i32)> = Vec::new(); + + for req in batch_request { + match req.request_type { + RequestType::In => { + let total_len: usize = req.iovecs.iter().map(|v| v.iov_len).sum(); + + if let Some(host_offset) = Self::resolve_read( + &self.metadata, + &self.data_file, + &self.backing_file, + req.offset as u64, + &req.iovecs, + total_len, + self.alignment, + self.cluster_size, + &*self.decoder, + )? { + let fd = self.data_file.as_raw_fd(); + // SAFETY: fd is valid and iovecs point to valid guest memory. + unsafe { + sq.push( + &opcode::Readv::new( + types::Fd(fd), + req.iovecs.as_ptr(), + req.iovecs.len() as u32, + ) + .offset(host_offset) + .build() + .user_data(req.user_data), + ) + .map_err(|_| { + AsyncIoError::ReadVectored(io::Error::other( + "Submission queue is full", + )) + })?; + } + needs_submit = true; + } else { + sync_completions.push((req.user_data, total_len as i32)); + } + } + RequestType::Out => { + let total_len: usize = req.iovecs.iter().map(|v| v.iov_len).sum(); + Self::cow_write_sync( + req.offset as u64, + &req.iovecs, + &self.metadata, + &self.data_file, + &self.backing_file, + self.alignment, + self.cluster_size, + )?; + sync_completions.push((req.user_data, total_len as i32)); + } + _ => { + unreachable!("Unexpected batch request type: {:?}", req.request_type) + } + } + } + + if needs_submit { + sq.sync(); + submitter + .submit() + .map_err(AsyncIoError::SubmitBatchRequests)?; + } + + if !sync_completions.is_empty() { + for c in sync_completions { + self.completion_list.push_back(c); + } + self.eventfd.write(1).unwrap(); + } + + 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`. + #[allow(clippy::too_many_arguments)] + fn resolve_read( + metadata: &QcowMetadata, + data_file: &QcowRawFile, + backing_file: &Option>, + address: u64, + iovecs: &[libc::iovec], + total_len: usize, + alignment: usize, + cluster_size: u64, + decoder: &dyn Decoder, + ) -> AsyncIoResult> { + 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 alignment == 0 + && mappings.len() == 1 + && let ClusterReadMapping::Allocated { + offset: host_offset, + length, + } = &mappings[0] + && *length as usize == total_len + { + return Ok(Some(*host_offset)); + } + + Self::scatter_read_sync( + mappings, + iovecs, + data_file, + backing_file, + alignment, + cluster_size, + decoder, + )?; + Ok(None) + } + + /// Scatter-read cluster mappings synchronously into iovec buffers. + fn scatter_read_sync( + mappings: Vec, + iovecs: &[libc::iovec], + data_file: &QcowRawFile, + backing_file: &Option>, + alignment: usize, + cluster_size: u64, + decoder: &dyn Decoder, + ) -> AsyncIoResult<()> { + let mut buf_offset = 0usize; + for mapping in mappings { + match mapping { + ClusterReadMapping::Zero { length } => { + // SAFETY: iovecs point to valid guest memory buffers. + unsafe { + zero_fill_iovecs(iovecs, buf_offset, length as usize); + } + buf_offset += length as usize; + } + ClusterReadMapping::Allocated { + offset: host_offset, + length, + } => { + let len = length as usize; + if alignment > 0 { + let mut abuf = + AlignedBuf::new(len, alignment).map_err(AsyncIoError::ReadVectored)?; + aligned_pread( + data_file.as_raw_fd(), + abuf.as_mut_slice(len), + host_offset, + alignment, + ) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers. + unsafe { scatter_to_iovecs(iovecs, buf_offset, abuf.as_slice(len)) }; + } else { + let mut buf = vec![0u8; len]; + pread_exact(data_file.as_raw_fd(), &mut buf, host_offset) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers. + unsafe { scatter_to_iovecs(iovecs, buf_offset, &buf) }; + } + buf_offset += len; + } + ClusterReadMapping::Compressed { + host_offset, + compressed_size, + cluster_offset, + length, + } => { + let compressed = + pread_alloc(data_file.as_raw_fd(), host_offset, compressed_size) + .map_err(AsyncIoError::ReadVectored)?; + let decompressed = + decompress_cluster(&compressed, cluster_size as usize, decoder) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers. + unsafe { + scatter_to_iovecs( + iovecs, + buf_offset, + &decompressed[cluster_offset..cluster_offset + length], + ); + } + 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)?; + // SAFETY: iovecs point to valid guest memory buffers. + unsafe { scatter_to_iovecs(iovecs, buf_offset, &buf) }; + buf_offset += length as usize; + } + } + } + Ok(()) + } + + /// Write iovec data cluster-by-cluster with COW from backing file. + fn cow_write_sync( + address: u64, + iovecs: &[libc::iovec], + metadata: &QcowMetadata, + data_file: &QcowRawFile, + backing_file: &Option>, + alignment: usize, + cluster_size: u64, + ) -> AsyncIoResult<()> { + let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum(); + 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, + } => { + if alignment > 0 { + // O_DIRECT, gather directly into aligned buffer. + let mut abuf = AlignedBuf::new(count, alignment) + .map_err(AsyncIoError::WriteVectored)?; + // SAFETY: iovecs point to valid guest memory buffers + unsafe { + gather_from_iovecs_into(iovecs, buf_offset, abuf.as_mut_slice(count)); + } + aligned_pwrite( + data_file.as_raw_fd(), + abuf.as_slice(count), + host_offset, + alignment, + ) + .map_err(AsyncIoError::WriteVectored)?; + } else { + // No O_DIRECT, plain buffer is fine. + let mut buf = vec![0u8; count]; + // SAFETY: iovecs point to valid guest memory buffers. + unsafe { + gather_from_iovecs_into(iovecs, buf_offset, &mut buf); + } + pwrite_all(data_file.as_raw_fd(), &buf, host_offset) + .map_err(AsyncIoError::WriteVectored)?; + } + } + } + buf_offset += count; + } + Ok(()) + } +} + +#[cfg(test)] +mod unit_tests { + use std::io::{Seek, SeekFrom, Write}; + use std::sync::Arc; + use std::thread; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::disk_file::AsyncDiskFile; + use crate::qcow::{QcowFile, RawFile}; + use crate::qcow_common::unit_tests::compress_allocated_clusters; + use crate::qcow_disk::QcowDisk; + use crate::{BatchRequest, RequestType, SECTOR_SIZE}; + + fn create_disk_with_data( + file_size: u64, + data: &[u8], + offset: u64, + sparse: bool, + ) -> (TempFile, QcowDisk) { + let temp_file = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + let mut qcow_file = QcowFile::new(raw_file, 3, file_size, sparse).unwrap(); + qcow_file.seek(SeekFrom::Start(offset)).unwrap(); + qcow_file.write_all(data).unwrap(); + qcow_file.flush().unwrap(); + } + let disk = QcowDisk::new( + temp_file.as_file().try_clone().unwrap(), + false, + false, + sparse, + true, + ) + .unwrap(); + (temp_file, disk) + } + + fn wait_for_completion(async_io: &mut dyn AsyncIo) -> (u64, i32) { + 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 async_write(disk: &QcowDisk, offset: u64, data: &[u8]) { + let mut async_io = disk.create_async_io(1).unwrap(); + let iovec = libc::iovec { + iov_base: data.as_ptr().cast::().cast_mut(), + iov_len: data.len(), + }; + async_io + .write_vectored(offset as libc::off_t, &[iovec], 2) + .unwrap(); + let (user_data, result) = wait_for_completion(async_io.as_mut()); + 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 { + let mut async_io = disk.create_async_io(1).unwrap(); + let mut buf = vec![0xFFu8; len]; + let iovec = libc::iovec { + iov_base: buf.as_mut_ptr().cast(), + iov_len: buf.len(), + }; + async_io + .read_vectored(offset as libc::off_t, &[iovec], 1) + .unwrap(); + let (user_data, result) = wait_for_completion(async_io.as_mut()); + assert_eq!(user_data, 1); + assert_eq!(result as usize, len, "read should return requested length"); + buf + } + + #[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 (user_data, result) = async_io.next_completed_request().unwrap(); + 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 (user_data, result) = async_io.next_completed_request().unwrap(); + 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_read_roundtrip() { + let file_size = 100 * 1024 * 1024; + let temp_file = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw_file, 3, file_size, true).unwrap(); + } + let disk = QcowDisk::new( + temp_file.as_file().try_clone().unwrap(), + false, + false, + true, + true, + ) + .unwrap(); + + let pattern: Vec = (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_batch_mixed_requests() { + let file_size = 100 * 1024 * 1024; + let temp_file = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw_file, 3, file_size, true).unwrap(); + } + 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 iov_a = libc::iovec { + iov_base: write_a.as_ptr().cast::().cast_mut(), + iov_len: write_a.len(), + }; + let iov_b = libc::iovec { + iov_base: write_b.as_ptr().cast::().cast_mut(), + iov_len: write_b.len(), + }; + + let batch = vec![ + BatchRequest { + offset: offset_a as libc::off_t, + iovecs: smallvec::smallvec![iov_a], + user_data: 10, + request_type: RequestType::Out, + }, + BatchRequest { + offset: offset_b as libc::off_t, + iovecs: smallvec::smallvec![iov_b], + user_data: 20, + request_type: RequestType::Out, + }, + ]; + + async_io.submit_batch_requests(&batch).unwrap(); + + let mut completions = [ + wait_for_completion(async_io.as_mut()), + 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 read_a = vec![0u8; 4096]; + let mut read_b = vec![0u8; 4096]; + let riov_a = libc::iovec { + iov_base: read_a.as_mut_ptr().cast(), + iov_len: read_a.len(), + }; + let riov_b = libc::iovec { + iov_base: read_b.as_mut_ptr().cast(), + iov_len: read_b.len(), + }; + + let mut async_io = disk.create_async_io(8).unwrap(); + let read_batch = vec![ + BatchRequest { + offset: offset_a as libc::off_t, + iovecs: smallvec::smallvec![riov_a], + user_data: 30, + request_type: RequestType::In, + }, + BatchRequest { + offset: offset_b as libc::off_t, + iovecs: smallvec::smallvec![riov_b], + user_data: 40, + request_type: RequestType::In, + }, + ]; + + async_io.submit_batch_requests(&read_batch).unwrap(); + + let mut completions = [ + wait_for_completion(async_io.as_mut()), + wait_for_completion(async_io.as_mut()), + ]; + completions.sort_by_key(|c| c.0); + assert_eq!(completions[0], (30, 4096)); + assert_eq!(completions[1], (40, 4096)); + + 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 = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw_file, 3, file_size, true).unwrap(); + } + 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 = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw_file, 3, file_size, true).unwrap(); + } + 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()); + 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 = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw_file, 3, file_size, true).unwrap(); + } + 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); + } + + /// Returns None if O_DIRECT is not supported (e.g. tmpfs). + fn try_create_direct_io_disk(temp_file: &TempFile, file_size: u64) -> Option { + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw_file, 3, file_size, true).unwrap(); + } + QcowDisk::new( + temp_file.as_file().try_clone().unwrap(), + true, + false, + true, + true, + ) + .ok() + } + + #[test] + fn test_qcow_async_alignment_with_direct_io() { + let temp_file = TempFile::new().unwrap(); + let disk = match try_create_direct_io_disk(&temp_file, 100 * 1024 * 1024) { + Some(d) => d, + None => { + eprintln!("skipping: O_DIRECT not supported on this filesystem"); + return; + } + }; + let async_io = disk.create_async_io(1).unwrap(); + assert!(async_io.alignment() >= SECTOR_SIZE); + } + + #[test] + fn test_qcow_async_sub_sector_read_with_direct_io() { + let temp_file = TempFile::new().unwrap(); + let disk = match try_create_direct_io_disk(&temp_file, 100 * 1024 * 1024) { + Some(d) => d, + None => { + eprintln!("skipping: O_DIRECT not supported on this filesystem"); + return; + } + }; + + let pattern = vec![0xAB; 65536]; + async_write(&disk, 0, &pattern); + + let buf = async_read(&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 temp_file = TempFile::new().unwrap(); + let disk = match try_create_direct_io_disk(&temp_file, 100 * 1024 * 1024) { + Some(d) => d, + None => { + eprintln!("skipping: O_DIRECT not supported on this filesystem"); + return; + } + }; + + let pattern: Vec = (0..128 * 1024).map(|i| (i % 251) as u8).collect(); + async_write(&disk, 0, &pattern); + + let buf = async_read(&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 = (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(); + let mut buf = vec![0xFFu8; cluster_size]; + let iovec = libc::iovec { + iov_base: buf.as_mut_ptr().cast(), + iov_len: buf.len(), + }; + async_io.read_vectored(0, &[iovec], 1).unwrap(); + let (_, result) = wait_for_completion(async_io.as_mut()); + assert_eq!(result as usize, cluster_size); + assert_eq!(buf, expected); + }) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } + } +} diff --git a/block/src/qcow_common.rs b/block/src/qcow_common.rs new file mode 100644 index 0000000000..49cae1f79a --- /dev/null +++ b/block/src/qcow_common.rs @@ -0,0 +1,446 @@ +// Copyright © 2021 Intel Corporation +// +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +//! Shared helpers for QCOW2 sync and async backends. +//! +//! Position-independent I/O (`pread_exact`, `pwrite_all`) and iovec +//! scatter/gather helpers used by both `qcow_sync` and `qcow_async`. + +use std::alloc::{Layout, alloc_zeroed, dealloc}; +use std::cmp::min; +use std::os::fd::RawFd; +use std::{io, ptr, slice}; + +use crate::qcow::decoder::Decoder; + +// -- Position independent I/O helpers -- +// +// Duplicated file descriptors share the kernel file description and thus the +// file position. Using seek then read from multiple queues races on that +// shared position. pread64 and pwrite64 are atomic and never touch the position. + +/// Read exactly the requested bytes at offset, looping on short reads. +pub fn pread_exact(fd: RawFd, buf: &mut [u8], offset: u64) -> io::Result<()> { + let mut total = 0usize; + while total < buf.len() { + // SAFETY: buf and fd are valid for the lifetime of the call. + let ret = unsafe { + libc::pread64( + fd, + buf[total..].as_mut_ptr().cast(), + buf.len() - total, + (offset + total as u64) as libc::off_t, + ) + }; + if ret < 0 { + return Err(io::Error::last_os_error()); + } + if ret == 0 { + return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); + } + total += ret as usize; + } + Ok(()) +} + +/// Allocate a buffer and pread exactly `len` bytes at `offset`. +pub fn pread_alloc(fd: RawFd, offset: u64, len: usize) -> io::Result> { + let mut buf = vec![0u8; len]; + pread_exact(fd, &mut buf, offset)?; + Ok(buf) +} + +/// Decompress a full QCOW2 cluster from compressed data. +/// +/// Returns a `cluster_size` byte buffer with the decompressed cluster +/// content. Fails if the decoder does not produce exactly `cluster_size` +/// bytes. +pub fn decompress_cluster( + compressed: &[u8], + cluster_size: usize, + decoder: &dyn Decoder, +) -> io::Result> { + let mut decompressed = vec![0u8; cluster_size]; + let n = decoder + .decode(compressed, &mut decompressed) + .map_err(|_| io::Error::from_raw_os_error(libc::EIO))?; + if n != cluster_size { + return Err(io::Error::from_raw_os_error(libc::EIO)); + } + Ok(decompressed) +} + +/// Write all bytes to fd at offset, looping on short writes. +pub fn pwrite_all(fd: RawFd, buf: &[u8], offset: u64) -> io::Result<()> { + let mut total = 0usize; + while total < buf.len() { + // SAFETY: buf and fd are valid for the lifetime of the call. + let ret = unsafe { + libc::pwrite64( + fd, + buf[total..].as_ptr().cast(), + buf.len() - total, + (offset + total as u64) as libc::off_t, + ) + }; + if ret < 0 { + return Err(io::Error::last_os_error()); + } + if ret == 0 { + return Err(io::Error::other("pwrite64 wrote 0 bytes")); + } + total += ret as usize; + } + Ok(()) +} + +/// RAII wrapper for an aligned heap buffer required by O_DIRECT. +pub struct AlignedBuf { + ptr: *mut u8, + layout: Layout, +} + +impl AlignedBuf { + pub fn new(size: usize, alignment: usize) -> io::Result { + let size = size.max(1).next_multiple_of(alignment); + let layout = Layout::from_size_align(size, alignment) + .map_err(|e| io::Error::other(format!("invalid aligned layout: {e}")))?; + // SAFETY: layout has non-zero size. + let ptr = unsafe { alloc_zeroed(layout) }; + if ptr.is_null() { + return Err(io::Error::new( + io::ErrorKind::OutOfMemory, + "aligned allocation failed", + )); + } + Ok(AlignedBuf { ptr, layout }) + } + + pub fn as_mut_slice(&mut self, len: usize) -> &mut [u8] { + let len = len.min(self.layout.size()); + // SAFETY: ptr is valid for layout.size() bytes; len <= layout.size(). + unsafe { slice::from_raw_parts_mut(self.ptr, len) } + } + + pub fn as_slice(&self, len: usize) -> &[u8] { + let len = len.min(self.layout.size()); + // SAFETY: ptr is valid for layout.size() bytes; len <= layout.size(). + unsafe { slice::from_raw_parts(self.ptr, len) } + } + + #[cfg(test)] + pub fn layout(&self) -> &Layout { + &self.layout + } + + #[cfg(test)] + pub fn ptr(&self) -> *const u8 { + self.ptr + } +} + +impl Drop for AlignedBuf { + fn drop(&mut self) { + // SAFETY: ptr was allocated by alloc_zeroed with self.layout. + unsafe { dealloc(self.ptr, self.layout) }; + } +} + +/// Read into `buf` via an aligned bounce buffer when O_DIRECT requires it. +pub fn aligned_pread(fd: RawFd, buf: &mut [u8], offset: u64, alignment: usize) -> io::Result<()> { + if alignment == 0 + || ((buf.as_ptr() as usize).is_multiple_of(alignment) + && buf.len().is_multiple_of(alignment) + && (offset as usize).is_multiple_of(alignment)) + { + return pread_exact(fd, buf, offset); + } + + let aligned_offset = offset & !(alignment as u64 - 1); + let head = (offset - aligned_offset) as usize; + let aligned_len = (head + buf.len()).next_multiple_of(alignment); + let mut bounce = AlignedBuf::new(aligned_len, alignment)?; + pread_exact(fd, bounce.as_mut_slice(aligned_len), aligned_offset)?; + buf.copy_from_slice(&bounce.as_slice(aligned_len)[head..head + buf.len()]); + Ok(()) +} + +/// Write `buf` via an aligned bounce buffer when O_DIRECT requires it. +pub fn aligned_pwrite(fd: RawFd, buf: &[u8], offset: u64, alignment: usize) -> io::Result<()> { + if alignment == 0 + || ((buf.as_ptr() as usize).is_multiple_of(alignment) + && buf.len().is_multiple_of(alignment) + && (offset as usize).is_multiple_of(alignment)) + { + return pwrite_all(fd, buf, offset); + } + + let aligned_offset = offset & !(alignment as u64 - 1); + let head = (offset - aligned_offset) as usize; + let aligned_len = (head + buf.len()).next_multiple_of(alignment); + let mut bounce = AlignedBuf::new(aligned_len, alignment)?; + + // Read-modify-write: read the existing aligned region, overlay our data. + pread_exact(fd, bounce.as_mut_slice(aligned_len), aligned_offset)?; + bounce.as_mut_slice(aligned_len)[head..head + buf.len()].copy_from_slice(buf); + pwrite_all(fd, bounce.as_slice(aligned_len), aligned_offset) +} + +// -- iovec helper functions -- +// +// Operate on the iovec array as a flat byte stream. + +/// Copy data into iovecs starting at the given byte offset. +/// +/// # Safety +/// Caller must ensure iovecs point to valid, writable memory of sufficient size. +pub unsafe fn scatter_to_iovecs(iovecs: &[libc::iovec], start: usize, data: &[u8]) { + let mut remaining = data; + let mut pos = 0usize; + for iov in iovecs { + let iov_end = pos + iov.iov_len; + if iov_end <= start || remaining.is_empty() { + pos = iov_end; + continue; + } + let iov_start = start.saturating_sub(pos); + let available = iov.iov_len - iov_start; + let count = min(available, remaining.len()); + // SAFETY: iov_base is valid for iov_len bytes per caller contract. + unsafe { + let dst = iov.iov_base.cast::().add(iov_start); + ptr::copy_nonoverlapping(remaining.as_ptr(), dst, count); + } + remaining = &remaining[count..]; + if remaining.is_empty() { + break; + } + pos = iov_end; + } +} + +/// Zero fill iovecs starting at the given byte offset for the given length. +/// +/// # Safety +/// Caller must ensure iovecs point to valid, writable memory of sufficient size. +pub unsafe fn zero_fill_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) { + let mut remaining = len; + let mut pos = 0usize; + for iov in iovecs { + let iov_end = pos + iov.iov_len; + if iov_end <= start || remaining == 0 { + pos = iov_end; + continue; + } + let iov_start = start.saturating_sub(pos); + let available = iov.iov_len - iov_start; + let count = min(available, remaining); + // SAFETY: iov_base is valid for iov_len bytes per caller contract. + unsafe { + let dst = iov.iov_base.cast::().add(iov_start); + ptr::write_bytes(dst, 0, count); + } + remaining -= count; + if remaining == 0 { + break; + } + pos = iov_end; + } +} + +/// Gather bytes from iovecs starting at the given byte offset into `dst`. +/// +/// # Safety +/// Caller must ensure iovecs point to valid, readable memory of sufficient size. +pub unsafe fn gather_from_iovecs_into(iovecs: &[libc::iovec], start: usize, dst: &mut [u8]) { + let len = dst.len(); + let mut written = 0usize; + let mut pos = 0usize; + for iov in iovecs { + let iov_end = pos + iov.iov_len; + if iov_end <= start || written == len { + pos = iov_end; + continue; + } + let iov_start = start.saturating_sub(pos); + let available = iov.iov_len - iov_start; + let count = min(available, len - written); + // SAFETY: iov_base is valid for iov_len bytes per caller contract. + unsafe { + let src = iov.iov_base.cast::().add(iov_start); + ptr::copy_nonoverlapping(src, dst.as_mut_ptr().add(written), count); + } + written += count; + if written == len { + break; + } + pos = iov_end; + } +} + +/// Gather bytes from iovecs starting at the given byte offset into a Vec. +/// +/// # Safety +/// Caller must ensure iovecs point to valid, readable memory of sufficient size. +pub unsafe fn gather_from_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) -> Vec { + let mut result = vec![0u8; len]; + // SAFETY: caller guarantees iovecs are valid; result has len bytes. + unsafe { gather_from_iovecs_into(iovecs, start, &mut result) }; + result +} + +#[cfg(test)] +pub(crate) mod unit_tests { + use std::fs::File; + use std::io::{Read, Seek, SeekFrom, Write}; + use std::os::unix::fs::FileExt; + use std::os::unix::io::AsRawFd; + + use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + use flate2::Compression; + use flate2::write::DeflateEncoder; + use vmm_sys_util::tempfile::TempFile; + + use super::{decompress_cluster, pread_alloc}; + use crate::qcow::decoder::ZlibDecoder; + + const COMPRESSED_FLAG: u64 = 1 << 62; + const CLUSTER_USED_FLAG: u64 = 1 << 63; + const COMPRESSED_SECTOR_SIZE: u64 = 512; + + const HEADER_CLUSTER_BITS_OFFSET: u64 = 20; + const HEADER_L1_SIZE_OFFSET: u64 = 36; + const HEADER_L1_TABLE_OFFSET: u64 = 40; + + const L1_L2_ADDR_MASK: u64 = 0x00ff_ffff_ffff_fe00; + + fn make_compressed_l2_entry(host_offset: u64, compressed_len: usize, cluster_bits: u32) -> u64 { + let compressed_size_shift = 62 - (cluster_bits - 8); + let intra_sector_offset = host_offset & (COMPRESSED_SECTOR_SIZE - 1); + let total_bytes = compressed_len as u64 + intra_sector_offset; + let nsectors = total_bytes.div_ceil(COMPRESSED_SECTOR_SIZE); + let addr_part = host_offset & ((1 << compressed_size_shift) - 1); + let size_part = (nsectors - 1) << compressed_size_shift; + COMPRESSED_FLAG | size_part | addr_part + } + + /// Compress every allocated cluster in a QCOW2 image file in place. + /// + /// Walks L1 -> L2 tables, compresses each standard cluster with raw + /// deflate, appends the compressed payload at the end of the file, + /// and rewrites the L2 entry with the compressed layout. + pub fn compress_allocated_clusters(file: &mut File) { + file.seek(SeekFrom::Start(HEADER_CLUSTER_BITS_OFFSET)) + .unwrap(); + let cluster_bits = file.read_u32::().unwrap(); + let cluster_size = 1u64 << cluster_bits; + + file.seek(SeekFrom::Start(HEADER_L1_SIZE_OFFSET)).unwrap(); + let l1_size = file.read_u32::().unwrap(); + + file.seek(SeekFrom::Start(HEADER_L1_TABLE_OFFSET)).unwrap(); + let l1_table_offset = file.read_u64::().unwrap(); + + let entries_per_l2 = cluster_size / 8; + + let mut append_offset = file.seek(SeekFrom::End(0)).unwrap(); + append_offset = (append_offset + 511) & !511; + + for l1_idx in 0..l1_size as u64 { + let l1_entry_offset = l1_table_offset + l1_idx * 8; + file.seek(SeekFrom::Start(l1_entry_offset)).unwrap(); + let l1_entry = file.read_u64::().unwrap(); + + let l2_table_addr = l1_entry & L1_L2_ADDR_MASK; + if l2_table_addr == 0 { + continue; + } + + for l2_idx in 0..entries_per_l2 { + let l2_entry_offset = l2_table_addr + l2_idx * 8; + file.seek(SeekFrom::Start(l2_entry_offset)).unwrap(); + let l2_entry = file.read_u64::().unwrap(); + + if l2_entry & CLUSTER_USED_FLAG == 0 || l2_entry & COMPRESSED_FLAG != 0 { + continue; + } + + let host_cluster_addr = l2_entry & L1_L2_ADDR_MASK; + if host_cluster_addr == 0 { + continue; + } + + let mut cluster_data = vec![0u8; cluster_size as usize]; + file.seek(SeekFrom::Start(host_cluster_addr)).unwrap(); + file.read_exact(&mut cluster_data).unwrap(); + + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&cluster_data).unwrap(); + let compressed = encoder.finish().unwrap(); + + file.seek(SeekFrom::Start(append_offset)).unwrap(); + file.write_all(&compressed).unwrap(); + + // The L2 entry encodes the compressed size in units of + // 512 byte sectors. The reader decodes the sector count + // back and computes: nsectors * 512 - (addr & 511). + // Because addr is 512 aligned, this yields nsectors * 512 + // which rounds up to the next sector boundary. The file + // must contain enough bytes for that rounded up pread. + let padded_len = (compressed.len() + 511) & !511; + if padded_len > compressed.len() { + let padding = vec![0u8; padded_len - compressed.len()]; + file.write_all(&padding).unwrap(); + } + + let new_entry = + make_compressed_l2_entry(append_offset, compressed.len(), cluster_bits); + file.seek(SeekFrom::Start(l2_entry_offset)).unwrap(); + file.write_u64::(new_entry).unwrap(); + + append_offset += padded_len as u64; + } + } + + file.flush().unwrap(); + } + + #[test] + fn test_pread_alloc() { + let temp = TempFile::new().unwrap(); + let file = temp.as_file(); + let data: Vec = (0..=255).cycle().take(4096).collect(); + file.write_all_at(&data, 0).unwrap(); + + let buf = pread_alloc(file.as_raw_fd(), 0, 4096).unwrap(); + assert_eq!(buf, data); + + let buf = pread_alloc(file.as_raw_fd(), 100, 200).unwrap(); + assert_eq!(buf, &data[100..300]); + + pread_alloc(file.as_raw_fd(), 4000, 200).unwrap_err(); + } + + #[test] + fn test_decompress_cluster() { + let cluster_size = 65536; + let original: Vec = (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)); + } +} diff --git a/block/src/qcow_disk.rs b/block/src/qcow_disk.rs new file mode 100644 index 0000000000..ef27305264 --- /dev/null +++ b/block/src/qcow_disk.rs @@ -0,0 +1,266 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::os::unix::io::AsRawFd; +use std::sync::Arc; +use std::{fmt, io}; + +use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; +use crate::disk_file; +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::qcow::backing::shared_backing_from; +use crate::qcow::metadata::{BackingRead, QcowMetadata}; +use crate::qcow::qcow_raw_file::QcowRawFile; +use crate::qcow::{MAX_NESTING_DEPTH, RawFile, parse_qcow}; +#[cfg(feature = "io_uring")] +use crate::qcow_async::QcowAsync; +use crate::qcow_sync::QcowSync; + +/// Unified DiskFile wrapper for QCOW2 disk images. +/// +/// 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, + backing_file: Option>, + 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 { + #[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 = RawFile::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, + }) + } +} + +impl Drop for QcowDisk { + fn drop(&mut self) { + self.metadata.shutdown(); + } +} + +impl disk_file::DiskSize for QcowDisk { + fn logical_size(&self) -> BlockResult { + Ok(self.metadata.virtual_size()) + } +} + +impl disk_file::PhysicalSize for QcowDisk { + fn physical_size(&self) -> BlockResult { + Ok(self.data_raw_file.physical_size()?) + } +} + +impl disk_file::DiskFd for QcowDisk { + fn fd(&self) -> BorrowedDiskFd<'_> { + BorrowedDiskFd::new(self.data_raw_file.as_raw_fd()) + } +} + +impl disk_file::Geometry for QcowDisk {} + +impl disk_file::SparseCapable for QcowDisk { + fn supports_sparse_operations(&self) -> bool { + true + } + + fn supports_zero_flag(&self) -> bool { + true + } +} + +impl disk_file::Resizable for QcowDisk { + fn resize(&mut self, size: u64) -> BlockResult<()> { + if self.backing_file.is_some() { + return Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + DiskFileError::ResizeError(io::Error::other( + "resize not supported with backing files", + )), + ) + .with_op(ErrorOp::Resize)); + } + self.metadata.resize(size).map_err(|e| { + BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)) + .with_op(ErrorOp::Resize) + }) + } +} + +impl disk_file::DiskFile for QcowDisk {} + +impl disk_file::AsyncDiskFile for QcowDisk { + fn try_clone(&self) -> BlockResult> { + 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> { + 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 vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::async_io::AsyncIo; + use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize}; + use crate::qcow::{QcowFile, RawFile}; + + const TEST_SIZE: u64 = 0x5566_7788; + + fn make_qcow_file() -> File { + let temp_file = TempFile::new().unwrap(); + { + let raw = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + QcowFile::new(raw, 3, TEST_SIZE, true).unwrap(); + } + temp_file.into_file() + } + + #[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 = disk.create_async_io(128).unwrap(); + assert_eq!(io.batch_requests_enabled(), expect_batch); + } + + fn assert_async_io(disk: &QcowDisk, expect_batch: bool) { + assert_async_io_from_dyn(disk, expect_batch); + } + + #[test] + fn sync_backend_disables_batch_requests() { + let file = make_qcow_file(); + let disk = QcowDisk::new(file, false, false, true, false).unwrap(); + assert_async_io(&disk, false); + } + + #[cfg(feature = "io_uring")] + #[test] + fn io_uring_backend_enables_batch_requests() { + let file = make_qcow_file(); + let disk = QcowDisk::new(file, false, false, true, true).unwrap(); + assert_async_io(&disk, true); + } + + #[test] + fn try_clone_preserves_sync_dispatch() { + let file = make_qcow_file(); + let disk = QcowDisk::new(file, false, false, true, false).unwrap(); + let cloned = disk.try_clone().unwrap(); + assert_async_io_from_dyn(cloned.as_ref(), false); + } + + #[cfg(feature = "io_uring")] + #[test] + fn try_clone_preserves_io_uring_dispatch() { + let file = make_qcow_file(); + let disk = QcowDisk::new(file, false, false, true, true).unwrap(); + let cloned = disk.try_clone().unwrap(); + assert_async_io_from_dyn(cloned.as_ref(), true); + } + + #[test] + fn physical_size_less_than_logical() { + // make_qcow_file() writes no guest data, so the file on disk + // only contains QCOW2 headers and metadata tables. + let file = make_qcow_file(); + let disk = QcowDisk::new(file, false, false, true, false).unwrap(); + assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap()); + } +} diff --git a/block/src/qcow_sync.rs b/block/src/qcow_sync.rs index f07e245e01..6a81c4ff99 100644 --- a/block/src/qcow_sync.rs +++ b/block/src/qcow_sync.rs @@ -2,59 +2,56 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause +use std::cmp::min; use std::collections::VecDeque; -use std::fs::File; -use std::io::{Seek, SeekFrom}; -use std::os::fd::AsRawFd; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::os::unix::io::AsRawFd; +use std::sync::Arc; use vmm_sys_util::eventfd::EventFd; +use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; -use crate::async_io::{ - AsyncIo, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; +use crate::qcow::decoder::Decoder; +use crate::qcow::metadata::{ + BackingRead, ClusterReadMapping, ClusterWriteMapping, DeallocAction, QcowMetadata, +}; +use crate::qcow::qcow_raw_file::QcowRawFile; +use crate::qcow_common::{ + AlignedBuf, aligned_pread, aligned_pwrite, decompress_cluster, gather_from_iovecs, + gather_from_iovecs_into, pread_alloc, pread_exact, pwrite_all, scatter_to_iovecs, + zero_fill_iovecs, }; -use crate::qcow::{QcowFile, RawFile, Result as QcowResult}; -use crate::AsyncAdaptor; - -pub struct QcowDiskSync { - qcow_file: Arc>, -} - -impl QcowDiskSync { - pub fn new(file: File, direct_io: bool) -> QcowResult { - Ok(QcowDiskSync { - qcow_file: Arc::new(Mutex::new(QcowFile::from(RawFile::new(file, direct_io))?)), - }) - } -} - -impl DiskFile for QcowDiskSync { - fn size(&mut self) -> DiskFileResult { - let mut file = self.qcow_file.lock().unwrap(); - - file.seek(SeekFrom::End(0)).map_err(DiskFileError::Size) - } - - fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult> { - Ok(Box::new(QcowSync::new(self.qcow_file.clone())) as Box) - } - - fn fd(&mut self) -> BorrowedDiskFd<'_> { - let lock = self.qcow_file.lock().unwrap(); - BorrowedDiskFd::new(lock.as_raw_fd()) - } -} pub struct QcowSync { - qcow_file: Arc>, + metadata: Arc, + data_file: QcowRawFile, + /// See the backing_file field on QcowDisk. + backing_file: Option>, + sparse: bool, + /// O_DIRECT alignment requirement (0 = no alignment needed). + alignment: usize, + cluster_size: u64, + decoder: Arc, eventfd: EventFd, completion_list: VecDeque<(u64, i32)>, } impl QcowSync { - pub fn new(qcow_file: Arc>) -> Self { + pub(crate) fn new( + metadata: Arc, + data_file: QcowRawFile, + backing_file: Option>, + sparse: bool, + ) -> Self { + let alignment = data_file.file().alignment(); QcowSync { - qcow_file, + cluster_size: metadata.cluster_size(), + decoder: metadata.decoder(), + metadata, + data_file, + backing_file, + sparse, + alignment, eventfd: EventFd::new(libc::EFD_NONBLOCK) .expect("Failed creating EventFd for QcowSync"), completion_list: VecDeque::new(), @@ -62,12 +59,6 @@ impl QcowSync { } } -impl AsyncAdaptor for Arc> { - fn file(&mut self) -> MutexGuard<'_, QcowFile> { - self.lock().unwrap() - } -} - impl AsyncIo for QcowSync { fn notifier(&self) -> &EventFd { &self.eventfd @@ -79,13 +70,94 @@ impl AsyncIo for QcowSync { iovecs: &[libc::iovec], user_data: u64, ) -> AsyncIoResult<()> { - self.qcow_file.read_vectored_sync( - offset, - iovecs, - user_data, - &self.eventfd, - &mut self.completion_list, - ) + let address = offset as u64; + let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum(); + + let has_backing = self.backing_file.is_some(); + let mappings = self + .metadata + .map_clusters_for_read(address, total_len, has_backing) + .map_err(AsyncIoError::ReadVectored)?; + + let mut buf_offset = 0usize; + for mapping in mappings { + match mapping { + ClusterReadMapping::Zero { length } => { + // SAFETY: iovecs point to valid guest memory buffers + unsafe { zero_fill_iovecs(iovecs, buf_offset, length as usize) }; + buf_offset += length as usize; + } + ClusterReadMapping::Allocated { + offset: host_offset, + length, + } => { + let len = length as usize; + if self.alignment > 0 { + // O_DIRECT, aligned buffer avoids bounce copy. + let mut abuf = AlignedBuf::new(len, self.alignment) + .map_err(AsyncIoError::ReadVectored)?; + aligned_pread( + self.data_file.as_raw_fd(), + abuf.as_mut_slice(len), + host_offset, + self.alignment, + ) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers + unsafe { scatter_to_iovecs(iovecs, buf_offset, abuf.as_slice(len)) }; + } else { + // No O_DIRECT, plain buffer is fine. + let mut buf = vec![0u8; len]; + pread_exact(self.data_file.as_raw_fd(), &mut buf, host_offset) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers + unsafe { scatter_to_iovecs(iovecs, buf_offset, &buf) }; + } + buf_offset += len; + } + ClusterReadMapping::Compressed { + host_offset, + compressed_size, + cluster_offset, + length, + } => { + let compressed = + pread_alloc(self.data_file.as_raw_fd(), host_offset, compressed_size) + .map_err(AsyncIoError::ReadVectored)?; + let decompressed = + decompress_cluster(&compressed, self.cluster_size as usize, &*self.decoder) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers + unsafe { + scatter_to_iovecs( + iovecs, + buf_offset, + &decompressed[cluster_offset..cluster_offset + length], + ); + } + buf_offset += length; + } + ClusterReadMapping::Backing { + offset: backing_offset, + length, + } => { + let mut buf = vec![0u8; length as usize]; + self.backing_file + .as_ref() + .unwrap() + .read_at(backing_offset, &mut buf) + .map_err(AsyncIoError::ReadVectored)?; + // SAFETY: iovecs point to valid guest memory buffers + unsafe { scatter_to_iovecs(iovecs, buf_offset, &buf) }; + buf_offset += length as usize; + } + } + } + + self.completion_list + .push_back((user_data, total_len as i32)); + self.eventfd.write(1).unwrap(); + Ok(()) } fn write_vectored( @@ -94,21 +166,1674 @@ impl AsyncIo for QcowSync { iovecs: &[libc::iovec], user_data: u64, ) -> AsyncIoResult<()> { - self.qcow_file.write_vectored_sync( - offset, - iovecs, - user_data, - &self.eventfd, - &mut self.completion_list, - ) + let address = offset as u64; + let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum(); + let mut buf_offset = 0usize; + + while buf_offset < total_len { + let curr_addr = address + buf_offset as u64; + let intra_offset = curr_addr & (self.cluster_size - 1); + let remaining_in_cluster = (self.cluster_size - intra_offset) as usize; + let count = min(total_len - buf_offset, remaining_in_cluster); + + // Read backing data for COW if this is a partial cluster + // write to an unallocated cluster with a backing file. + let backing_data = if let Some(backing) = self + .backing_file + .as_ref() + .filter(|_| intra_offset != 0 || count < self.cluster_size as usize) + { + let cluster_begin = curr_addr - intra_offset; + let mut data = vec![0u8; self.cluster_size as usize]; + backing + .read_at(cluster_begin, &mut data) + .map_err(AsyncIoError::WriteVectored)?; + Some(data) + } else { + None + }; + + let mapping = self + .metadata + .map_cluster_for_write(curr_addr, backing_data) + .map_err(AsyncIoError::WriteVectored)?; + + match mapping { + ClusterWriteMapping::Allocated { + offset: host_offset, + } => { + if self.alignment > 0 { + // O_DIRECT, gather directly into aligned buffer. + let mut abuf = AlignedBuf::new(count, self.alignment) + .map_err(AsyncIoError::WriteVectored)?; + // SAFETY: iovecs point to valid guest memory buffers + unsafe { + gather_from_iovecs_into(iovecs, buf_offset, abuf.as_mut_slice(count)); + } + aligned_pwrite( + self.data_file.as_raw_fd(), + abuf.as_slice(count), + host_offset, + self.alignment, + ) + .map_err(AsyncIoError::WriteVectored)?; + } else { + // No O_DIRECT, plain buffer is fine. + // SAFETY: iovecs point to valid guest memory buffers + let buf = unsafe { gather_from_iovecs(iovecs, buf_offset, count) }; + pwrite_all(self.data_file.as_raw_fd(), &buf, host_offset) + .map_err(AsyncIoError::WriteVectored)?; + } + } + } + buf_offset += count; + } + + self.completion_list + .push_back((user_data, total_len as i32)); + self.eventfd.write(1).unwrap(); + Ok(()) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { - self.qcow_file - .fsync_sync(user_data, &self.eventfd, &mut self.completion_list) + self.metadata.flush().map_err(AsyncIoError::Fsync)?; + if let Some(user_data) = user_data { + self.completion_list.push_back((user_data, 0)); + self.eventfd.write(1).unwrap(); + } + Ok(()) } fn next_completed_request(&mut self) -> Option<(u64, i32)> { self.completion_list.pop_front() } + + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + let virtual_size = self.metadata.virtual_size(); + let cluster_size = self.cluster_size; + + let result = self + .metadata + .deallocate_bytes( + offset, + length as usize, + self.sparse, + virtual_size, + cluster_size, + self.backing_file.as_deref(), + ) + .map_err(AsyncIoError::PunchHole); + + match result { + Ok(actions) => { + for action in actions { + match action { + DeallocAction::PunchHole { + host_offset, + length, + } => { + let _ = self.data_file.file_mut().punch_hole(host_offset, length); + } + DeallocAction::WriteZeroes { + host_offset, + length, + } => { + let _ = self + .data_file + .file_mut() + .write_zeroes_at(host_offset, length); + } + } + } + self.completion_list.push_back((user_data, 0)); + self.eventfd.write(1).unwrap(); + Ok(()) + } + Err(e) => { + let errno = if let AsyncIoError::PunchHole(ref io_err) = e { + -io_err.raw_os_error().unwrap_or(libc::EIO) + } else { + -libc::EIO + }; + self.completion_list.push_back((user_data, errno)); + self.eventfd.write(1).unwrap(); + Ok(()) + } + } + } + + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + // For QCOW2 write_zeroes uses cluster deallocation, same as punch_hole. + // Unallocated clusters inherently read as zero in the QCOW2 format. + self.punch_hole(offset, length, user_data) + } +} + +#[cfg(test)] +mod unit_tests { + use std::io::{Seek, SeekFrom, Write}; + use std::os::fd::RawFd; + use std::thread; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::disk_file::{AsyncDiskFile, DiskSize, Resizable}; + use crate::qcow::{BackingFileConfig, ImageType, QcowFile, RawFile}; + use crate::qcow_common::unit_tests::compress_allocated_clusters; + use crate::qcow_disk::QcowDisk; + + fn create_disk_with_data( + file_size: u64, + data: &[u8], + offset: u64, + sparse: bool, + direct_io: bool, + ) -> (TempFile, QcowDisk) { + let temp_file = TempFile::new().unwrap(); + { + let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false); + let mut qcow_file = QcowFile::new(raw_file, 3, file_size, sparse).unwrap(); + qcow_file.seek(SeekFrom::Start(offset)).unwrap(); + qcow_file.write_all(data).unwrap(); + qcow_file.flush().unwrap(); + } + let disk = QcowDisk::new( + temp_file.as_file().try_clone().unwrap(), + direct_io, + false, + sparse, + false, + ) + .unwrap(); + (temp_file, disk) + } + + fn async_read(disk: &QcowDisk, offset: u64, len: usize) -> Vec { + let mut async_io = disk.create_async_io(1).unwrap(); + let mut buf = vec![0xFFu8; len]; + let iovec = libc::iovec { + iov_base: buf.as_mut_ptr().cast(), + iov_len: buf.len(), + }; + async_io + .read_vectored(offset as libc::off_t, &[iovec], 1) + .unwrap(); + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 1); + assert_eq!(result as usize, len, "read should return requested length"); + buf + } + + fn async_write(disk: &QcowDisk, offset: u64, data: &[u8]) { + let mut async_io = disk.create_async_io(1).unwrap(); + let iovec = libc::iovec { + iov_base: data.as_ptr().cast::().cast_mut(), + iov_len: data.len(), + }; + async_io + .write_vectored(offset as libc::off_t, &[iovec], 1) + .unwrap(); + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 1); + assert_eq!(result as usize, data.len()); + } + + #[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, false); + + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.punch_hole(offset, data.len() as u64, 100).unwrap(); + let (user_data, result) = async_io.next_completed_request().unwrap(); + 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![0xEE; 256 * 1024]; + let offset = 64 * 1024u64; + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true, false); + + let mut async_io = disk.create_async_io(1).unwrap(); + async_io + .write_zeroes(offset, data.len() as u64, 200) + .unwrap(); + let (user_data, result) = async_io.next_completed_request().unwrap(); + 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), + "Zeroed region should read as zeros" + ); + } + + #[test] + fn test_qcow_async_multiple_operations() { + let data = vec![0xFF; 64 * 1024]; + let (_temp, _) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true, false); + + // Write data at multiple offsets via QcowFile first, then punch + { + let temp_file = _temp.as_file().try_clone().unwrap(); + let raw_file = RawFile::new(temp_file, false); + let mut qcow_file = QcowFile::from(raw_file).unwrap(); + for i in 0..4u64 { + let off = i * 128 * 1024; + qcow_file.seek(SeekFrom::Start(off)).unwrap(); + qcow_file.write_all(&data).unwrap(); + } + qcow_file.flush().unwrap(); + } + + let disk = QcowDisk::new( + _temp.as_file().try_clone().unwrap(), + false, + false, + true, + false, + ) + .unwrap(); + + let mut async_io = disk.create_async_io(1).unwrap(); + + async_io.punch_hole(0, 64 * 1024, 1).unwrap(); + async_io.punch_hole(128 * 1024, 64 * 1024, 2).unwrap(); + async_io.punch_hole(256 * 1024, 64 * 1024, 3).unwrap(); + + let (ud, res) = async_io.next_completed_request().unwrap(); + assert_eq!(ud, 1); + assert_eq!(res, 0); + let (ud, res) = async_io.next_completed_request().unwrap(); + assert_eq!(ud, 2); + assert_eq!(res, 0); + let (ud, res) = async_io.next_completed_request().unwrap(); + assert_eq!(ud, 3); + assert_eq!(res, 0); + assert!(async_io.next_completed_request().is_none()); + } + + #[test] + fn test_qcow_punch_hole_then_read() { + // Verify that after punch_hole, a second async_io sees zeros. + let data = vec![0xAB; 128 * 1024]; + let offset = 0u64; + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true, false); + + let mut async_io1 = disk.create_async_io(1).unwrap(); + async_io1 + .punch_hole(offset, data.len() as u64, 100) + .unwrap(); + let (user_data, result) = async_io1.next_completed_request().unwrap(); + assert_eq!(user_data, 100); + assert_eq!(result, 0); + drop(async_io1); + + // Read via second async_io, should see zeros + let read_buf = async_read(&disk, offset, data.len()); + assert!( + read_buf.iter().all(|&b| b == 0), + "After punch_hole, read should return zeros" + ); + } + + #[test] + fn test_qcow_disk_sync_punch_hole_with_create_async_io() { + // Simulates the real usage pattern of write data, punch hole, then read back. + let data = vec![0xCD; 64 * 1024]; // one cluster + let offset = 1024 * 1024u64; // 1MB offset + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true, false); + + // Punch hole to simulate DISCARD + let mut async_io1 = disk.create_async_io(1).unwrap(); + async_io1.punch_hole(offset, data.len() as u64, 1).unwrap(); + let (user_data, result) = async_io1.next_completed_request().unwrap(); + assert_eq!(user_data, 1); + assert_eq!(result, 0, "punch_hole should succeed"); + drop(async_io1); + + // Read from the same location to verify + let read_buf = async_read(&disk, offset, data.len()); + assert!( + read_buf.iter().all(|&b| b == 0), + "After punch_hole via create_async_io, read should return zeros" + ); + } + + fn test_qcow_async_read_write_roundtrip_impl(direct_io: bool) { + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true, direct_io); + + let data = vec![0x42u8; 64 * 1024]; + let offset = 0u64; + + async_write(&disk, offset, &data); + + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.fsync(Some(10)).unwrap(); + let (ud, res) = async_io.next_completed_request().unwrap(); + assert_eq!(ud, 10); + assert_eq!(res, 0); + drop(async_io); + + let read_buf = async_read(&disk, offset, data.len()); + assert_eq!(read_buf, data, "Read-back should match written data"); + } + + #[test] + fn test_qcow_async_read_write_roundtrip() { + test_qcow_async_read_write_roundtrip_impl(false); + } + + #[test] + fn test_qcow_async_read_write_roundtrip_direct_io() { + test_qcow_async_read_write_roundtrip_impl(true); + } + + fn test_qcow_async_read_unallocated_impl(direct_io: bool) { + // Reading from an unallocated region should return zeros. + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true, direct_io); + let read_buf = async_read(&disk, 0, 64 * 1024); + assert!( + read_buf.iter().all(|&b| b == 0), + "Unallocated region should read as zeros" + ); + } + + #[test] + fn test_qcow_async_read_unallocated() { + test_qcow_async_read_unallocated_impl(false); + } + + #[test] + fn test_qcow_async_read_unallocated_direct_io() { + test_qcow_async_read_unallocated_impl(true); + } + + fn test_qcow_async_cross_cluster_read_write_impl(direct_io: bool) { + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true, direct_io); + + // Default cluster size is 64KB. Write 96KB starting at 32KB to cross the boundary. + let data: Vec = (0..96 * 1024).map(|i| (i % 251) as u8).collect(); + let offset = 32 * 1024u64; + + async_write(&disk, offset, &data); + + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.fsync(Some(99)).unwrap(); + drop(async_io); + + let read_buf = async_read(&disk, offset, data.len()); + assert_eq!( + read_buf, data, + "Cross cluster read should match written data" + ); + } + + #[test] + fn test_qcow_async_cross_cluster_read_write() { + test_qcow_async_cross_cluster_read_write_impl(false); + } + + #[test] + fn test_qcow_async_cross_cluster_read_write_direct_io() { + test_qcow_async_cross_cluster_read_write_impl(true); + } + + fn test_backing_file_read_impl(direct_io: bool) { + let backing_temp = TempFile::new().unwrap(); + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + backing_temp.as_file().write_all(&pattern).unwrap(); + backing_temp.as_file().sync_all().unwrap(); + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Raw), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Read first cluster - should come from backing file + let buf = async_read(&disk, 0, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[..cluster_size as usize], + "First cluster should match backing file data" + ); + + let buf = async_read(&disk, cluster_size, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[cluster_size as usize..2 * cluster_size as usize], + "Second cluster should match backing file data" + ); + + // Read a partial range spanning cluster boundary + let mid = cluster_size - 512; + let len = 1024usize; + let buf = async_read(&disk, mid, len); + assert_eq!( + &buf[..], + &pattern[mid as usize..mid as usize + len], + "Cross cluster read from backing should match" + ); + + let buf = async_read(&disk, 0, file_size as usize); + assert_eq!( + &buf[..], + &pattern[..], + "Full file read from backing should match" + ); + } + + #[test] + fn test_backing_file_read() { + test_backing_file_read_impl(false); + } + + #[test] + fn test_backing_file_read_direct_io() { + test_backing_file_read_impl(true); + } + + fn test_backing_file_read_qcow2_backing_impl(direct_io: bool) { + let backing_temp = TempFile::new().unwrap(); + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + { + let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&pattern).unwrap(); + qcow.flush().unwrap(); + } + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Read first cluster - should come from QCOW2 backing + let buf = async_read(&disk, 0, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[..cluster_size as usize], + "First cluster from QCOW2 backing should match" + ); + + let buf = async_read(&disk, 0, file_size as usize); + assert_eq!( + &buf[..], + &pattern[..], + "Full file from QCOW2 backing should match" + ); + + // Write to first cluster, then verify second cluster still reads from backing + let new_data = vec![0xAB; cluster_size as usize]; + async_write(&disk, 0, &new_data); + { + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.fsync(Some(99)).unwrap(); + } + + let buf = async_read(&disk, 0, cluster_size as usize); + assert_eq!( + &buf[..], + &new_data[..], + "Written cluster should be new data" + ); + + let buf = async_read(&disk, cluster_size, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[cluster_size as usize..2 * cluster_size as usize], + "Unwritten cluster should still come from backing" + ); + } + + #[test] + fn test_backing_file_read_qcow2_backing() { + test_backing_file_read_qcow2_backing_impl(false); + } + + #[test] + fn test_backing_file_read_qcow2_backing_direct_io() { + test_backing_file_read_qcow2_backing_impl(true); + } + + fn test_multi_queue_concurrent_reads_impl(direct_io: bool) { + // Verify that multiple queues (threads) can read simultaneously. + // This exercises the RwLock + pread64 design: concurrent L2 cache hits + // proceed in parallel and data reads are position independent. + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 16; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + let (_temp, disk) = create_disk_with_data(file_size, &pattern, 0, true, direct_io); + let disk = Arc::new(disk); + + let threads: Vec<_> = (0..8) + .map(|t| { + let disk = Arc::clone(&disk); + let pattern = pattern.clone(); + thread::spawn(move || { + for i in 0..16u64 { + // Each thread reads clusters in a different order + let cluster_idx = (i + t * 2) % 16; + let offset = cluster_idx * cluster_size; + let buf = async_read(&disk, offset, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[offset as usize..(offset + cluster_size) as usize], + "Thread {t} cluster {cluster_idx} mismatch" + ); + } + }) + }) + .collect(); + + for t in threads { + t.join().unwrap(); + } + } + + #[test] + fn test_multi_queue_concurrent_reads() { + test_multi_queue_concurrent_reads_impl(false); + } + + #[test] + fn test_multi_queue_concurrent_reads_direct_io() { + test_multi_queue_concurrent_reads_impl(true); + } + + fn test_multi_queue_concurrent_reads_qcow2_backing_impl(direct_io: bool) { + // Same as above but reads go through a Qcow2Backing, + // exercising concurrent metadata resolution + pread64 in the backing. + let backing_temp = TempFile::new().unwrap(); + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 16; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + { + let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&pattern).unwrap(); + qcow.flush().unwrap(); + } + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = Arc::new(QcowDisk::new(file, direct_io, true, true, false).unwrap()); + + let threads: Vec<_> = (0..8) + .map(|t| { + let disk = Arc::clone(&disk); + let pattern = pattern.clone(); + thread::spawn(move || { + for i in 0..16u64 { + let cluster_idx = (i + t * 2) % 16; + let offset = cluster_idx * cluster_size; + let buf = async_read(&disk, offset, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[offset as usize..(offset + cluster_size) as usize], + "Thread {t} cluster {cluster_idx} mismatch (qcow2 backing)" + ); + } + }) + }) + .collect(); + + for t in threads { + t.join().unwrap(); + } + } + + #[test] + fn test_multi_queue_concurrent_reads_qcow2_backing() { + test_multi_queue_concurrent_reads_qcow2_backing_impl(false); + } + + #[test] + fn test_multi_queue_concurrent_reads_qcow2_backing_direct_io() { + test_multi_queue_concurrent_reads_qcow2_backing_impl(true); + } + + fn test_three_layer_backing_chain_impl(direct_io: bool) { + // raw base -> qcow2 mid -> qcow2 overlay + // Tests recursive shared_backing_from() with nested backing. + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + let base_pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + + // Layer 0: raw base + let base_temp = TempFile::new().unwrap(); + base_temp.as_file().write_all(&base_pattern).unwrap(); + base_temp.as_file().sync_all().unwrap(); + let base_path = base_temp.as_path().to_str().unwrap().to_string(); + + // Layer 1: qcow2 mid pointing at raw base, write to cluster 0 only + let mid_temp = TempFile::new().unwrap(); + let mid_pattern = vec![0xBBu8; cluster_size as usize]; + { + let raw = RawFile::new(mid_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: base_path, + format: Some(ImageType::Raw), + }; + let mut mid = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + mid.seek(SeekFrom::Start(0)).unwrap(); + mid.write_all(&mid_pattern).unwrap(); + mid.flush().unwrap(); + } + let mid_path = mid_temp.as_path().to_str().unwrap().to_string(); + + // Layer 2: qcow2 overlay pointing at qcow2 mid, write to cluster 1 only + let overlay_temp = TempFile::new().unwrap(); + let overlay_pattern = vec![0xCCu8; cluster_size as usize]; + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: mid_path, + format: Some(ImageType::Qcow2), + }; + let mut overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + overlay.seek(SeekFrom::Start(cluster_size)).unwrap(); + overlay.write_all(&overlay_pattern).unwrap(); + overlay.flush().unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Cluster 0: mid wrote 0xBB + let buf = async_read(&disk, 0, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0xBB), + "Cluster 0 should come from mid layer" + ); + + // Cluster 1: overlay wrote 0xCC + let buf = async_read(&disk, cluster_size, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0xCC), + "Cluster 1 should come from overlay" + ); + + // Cluster 2: falls through mid (unwritten) to raw base + let buf = async_read(&disk, cluster_size * 2, cluster_size as usize); + let expected_start = (cluster_size * 2) as usize; + assert_eq!( + &buf[..], + &base_pattern[expected_start..expected_start + cluster_size as usize], + "Cluster 2 should come from raw base" + ); + + // Cluster 3: also falls through to raw base + let buf = async_read(&disk, cluster_size * 3, cluster_size as usize); + let expected_start = (cluster_size * 3) as usize; + assert_eq!( + &buf[..], + &base_pattern[expected_start..expected_start + cluster_size as usize], + "Cluster 3 should come from raw base" + ); + } + + #[test] + fn test_three_layer_backing_chain() { + test_three_layer_backing_chain_impl(false); + } + + #[test] + fn test_three_layer_backing_chain_direct_io() { + test_three_layer_backing_chain_impl(true); + } + + fn test_backing_cow_preserves_all_unwritten_clusters_impl(direct_io: bool) { + // Write to specific clusters in the overlay, verify all others still + // read from the qcow2 backing correctly. + let cluster_size = 1u64 << 16; + let num_clusters = 8u64; + let file_size = cluster_size * num_clusters; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + + let backing_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&pattern).unwrap(); + qcow.flush().unwrap(); + } + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + let written = vec![0xFFu8; cluster_size as usize]; + for &idx in &[0u64, 3, 7] { + async_write(&disk, idx * cluster_size, &written); + } + { + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.fsync(Some(99)).unwrap(); + } + + for &idx in &[0u64, 3, 7] { + let buf = async_read(&disk, idx * cluster_size, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0xFF), + "Cluster {idx} should be written data" + ); + } + + // Verify unwritten clusters read from backing + for idx in 0..num_clusters { + if idx == 0 || idx == 3 || idx == 7 { + continue; + } + let offset = idx * cluster_size; + let buf = async_read(&disk, offset, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[offset as usize..(offset + cluster_size) as usize], + "Cluster {idx} should come from backing" + ); + } + } + + #[test] + fn test_backing_cow_preserves_all_unwritten_clusters() { + test_backing_cow_preserves_all_unwritten_clusters_impl(false); + } + + #[test] + fn test_backing_cow_preserves_all_unwritten_clusters_direct_io() { + test_backing_cow_preserves_all_unwritten_clusters_impl(true); + } + + fn test_qcow2_backing_read_beyond_virtual_size_impl(direct_io: bool) { + // Read starting past the backing file virtual_size should return zeros. + let cluster_size = 1u64 << 16; + let backing_size = cluster_size * 2; + let overlay_size = cluster_size * 4; // overlay is larger than backing + + let backing_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, backing_size, true).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&vec![0xAA; backing_size as usize]).unwrap(); + qcow.flush().unwrap(); + } + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, overlay_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Read cluster 2 (past backing virtual_size) - should be zeros + let buf = async_read(&disk, backing_size, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0), + "Read beyond backing virtual_size should return zeros" + ); + } + + #[test] + fn test_qcow2_backing_read_beyond_virtual_size() { + test_qcow2_backing_read_beyond_virtual_size_impl(false); + } + + #[test] + fn test_qcow2_backing_read_beyond_virtual_size_direct_io() { + test_qcow2_backing_read_beyond_virtual_size_impl(true); + } + + fn test_qcow2_backing_read_spanning_virtual_size_impl(direct_io: bool) { + // Read that starts within backing bounds but extends past virtual_size. + // First part should have backing data, remainder should be zeros. + let cluster_size = 1u64 << 16; + let backing_size = cluster_size * 2; + let overlay_size = cluster_size * 4; + + let backing_temp = TempFile::new().unwrap(); + let backing_data = vec![0xBBu8; backing_size as usize]; + { + let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, backing_size, true).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&backing_data).unwrap(); + qcow.flush().unwrap(); + } + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, overlay_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Read 2 clusters starting at cluster 1 (spans backing boundary) + let read_len = cluster_size as usize * 2; + let buf = async_read(&disk, cluster_size, read_len); + + // First cluster should be backing data + assert!( + buf[..cluster_size as usize].iter().all(|&b| b == 0xBB), + "First half should come from backing" + ); + + // Second cluster is past backing virtual_size - zeros + assert!( + buf[cluster_size as usize..].iter().all(|&b| b == 0), + "Second half should be zeros (past backing virtual_size)" + ); + } + + #[test] + fn test_qcow2_backing_read_spanning_virtual_size() { + test_qcow2_backing_read_spanning_virtual_size_impl(false); + } + + #[test] + fn test_qcow2_backing_read_spanning_virtual_size_direct_io() { + test_qcow2_backing_read_spanning_virtual_size_impl(true); + } + + fn test_raw_backing_read_beyond_virtual_size_impl(direct_io: bool) { + // Read past raw backing file virtual_size should return zeros. + let cluster_size = 1u64 << 16; + let backing_size = cluster_size * 2; + let overlay_size = cluster_size * 4; + + let backing_temp = TempFile::new().unwrap(); + let backing_data = vec![0xDD; backing_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 overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Raw), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, overlay_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Read cluster 2 (past backing size) - should be zeros + let buf = async_read(&disk, backing_size, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0), + "Read beyond raw backing virtual_size should return zeros" + ); + + // Read spanning boundary: cluster 1 has data, cluster 2 zeros + let read_len = cluster_size as usize * 2; + let buf = async_read(&disk, cluster_size, read_len); + assert!( + buf[..cluster_size as usize].iter().all(|&b| b == 0xDD), + "First half should come from raw backing" + ); + assert!( + buf[cluster_size as usize..].iter().all(|&b| b == 0), + "Second half should be zeros (past raw backing size)" + ); + } + + #[test] + fn test_raw_backing_read_beyond_virtual_size() { + test_raw_backing_read_beyond_virtual_size_impl(false); + } + + #[test] + fn test_raw_backing_read_beyond_virtual_size_direct_io() { + test_raw_backing_read_beyond_virtual_size_impl(true); + } + + fn test_qcow2_backing_cross_cluster_read_impl(direct_io: bool) { + // Read spanning a cluster boundary through qcow2 backing. + // Exercises the read_clusters loop in Qcow2Backing. + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + + let backing_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false); + let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap(); + qcow.seek(SeekFrom::Start(0)).unwrap(); + qcow.write_all(&pattern).unwrap(); + qcow.flush().unwrap(); + } + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Qcow2), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Read spanning clusters 1-2 boundary: 512 bytes before + 512 after + let mid = cluster_size - 512; + let len = 1024usize; + let buf = async_read(&disk, mid, len); + assert_eq!( + &buf[..], + &pattern[mid as usize..mid as usize + len], + "Cross cluster read through qcow2 backing should match" + ); + + // Read spanning clusters 0-1-2 (3 clusters worth) + let start = cluster_size / 2; + let len = cluster_size as usize * 2; + let buf = async_read(&disk, start, len); + assert_eq!( + &buf[..], + &pattern[start as usize..start as usize + len], + "Multi cluster read through qcow2 backing should match" + ); + } + + #[test] + fn test_qcow2_backing_cross_cluster_read() { + test_qcow2_backing_cross_cluster_read_impl(false); + } + + #[test] + fn test_qcow2_backing_cross_cluster_read_direct_io() { + test_qcow2_backing_cross_cluster_read_impl(true); + } + + fn test_punch_hole_with_backing_fallthrough_impl(direct_io: bool) { + // Write to overlay, then punch hole. After punch, the cluster should + // fall through to backing data (not zeros). + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + + let backing_temp = TempFile::new().unwrap(); + backing_temp.as_file().write_all(&pattern).unwrap(); + backing_temp.as_file().sync_all().unwrap(); + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Raw), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + let written = vec![0xFFu8; cluster_size as usize]; + async_write(&disk, 0, &written); + { + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.fsync(Some(99)).unwrap(); + } + + let buf = async_read(&disk, 0, cluster_size as usize); + assert!(buf.iter().all(|&b| b == 0xFF), "Should read written data"); + + // Punch hole on cluster 0 - should deallocate and fall through to backing + { + let mut async_io = disk.create_async_io(1).unwrap(); + async_io.punch_hole(0, cluster_size, 42).unwrap(); + let (ud, res) = async_io.next_completed_request().unwrap(); + assert_eq!(ud, 42); + assert_eq!(res, 0); + } + + // Now read should return backing data, not zeros + let buf = async_read(&disk, 0, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[..cluster_size as usize], + "After punch_hole with backing, should read backing data" + ); + + // Cluster 1 should still be backing data throughout + let buf = async_read(&disk, cluster_size, cluster_size as usize); + assert_eq!( + &buf[..], + &pattern[cluster_size as usize..2 * cluster_size as usize], + "Untouched cluster should read from backing" + ); + } + + #[test] + fn test_punch_hole_with_backing_fallthrough() { + test_punch_hole_with_backing_fallthrough_impl(false); + } + + #[test] + fn test_punch_hole_with_backing_fallthrough_direct_io() { + test_punch_hole_with_backing_fallthrough_impl(true); + } + + fn test_rewrite_allocated_cluster_impl(direct_io: bool) { + // Write to a cluster, then overwrite it. The second write should hit + // the already allocated path in map_write (no new cluster allocation). + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true, direct_io); + let cluster_size = 1u64 << 16; + + let data1 = vec![0xAAu8; cluster_size as usize]; + async_write(&disk, 0, &data1); + { + let mut aio = disk.create_async_io(1).unwrap(); + aio.fsync(Some(1)).unwrap(); + } + let buf = async_read(&disk, 0, cluster_size as usize); + assert!(buf.iter().all(|&b| b == 0xAA), "First write should stick"); + + let data2 = vec![0xBBu8; cluster_size as usize]; + async_write(&disk, 0, &data2); + { + let mut aio = disk.create_async_io(1).unwrap(); + aio.fsync(Some(2)).unwrap(); + } + let buf = async_read(&disk, 0, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0xBB), + "Overwrite should replace data" + ); + } + + #[test] + fn test_rewrite_allocated_cluster() { + test_rewrite_allocated_cluster_impl(false); + } + + #[test] + fn test_rewrite_allocated_cluster_direct_io() { + test_rewrite_allocated_cluster_impl(true); + } + + fn test_partial_cluster_write_with_backing_cow_impl(direct_io: bool) { + // Partial cluster write to an overlay with a backing file triggers COW. + // The unwritten part of the cluster must be copied from backing. + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); + + let backing_temp = TempFile::new().unwrap(); + backing_temp.as_file().write_all(&pattern).unwrap(); + backing_temp.as_file().sync_all().unwrap(); + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Raw), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let disk = QcowDisk::new(file, direct_io, true, true, false).unwrap(); + + // Write 4KB at offset 4KB within cluster 0 (partial cluster) + let write_offset = 4096u64; + let write_len = 4096usize; + let write_data = vec![0xEEu8; write_len]; + async_write(&disk, write_offset, &write_data); + { + let mut aio = disk.create_async_io(1).unwrap(); + aio.fsync(Some(1)).unwrap(); + } + + let buf = async_read(&disk, 0, cluster_size as usize); + + // Before the write: should be COW'd from backing + assert_eq!( + &buf[..write_offset as usize], + &pattern[..write_offset as usize], + "Pre write region should be COW from backing" + ); + + assert_eq!( + &buf[write_offset as usize..write_offset as usize + write_len], + &write_data[..], + "Written region should be new data" + ); + + // After the write: should be COW'd from backing + let after_offset = write_offset as usize + write_len; + assert_eq!( + &buf[after_offset..cluster_size as usize], + &pattern[after_offset..cluster_size as usize], + "Post write region should be COW from backing" + ); + } + + #[test] + fn test_partial_cluster_write_with_backing_cow() { + test_partial_cluster_write_with_backing_cow_impl(false); + } + + #[test] + fn test_partial_cluster_write_with_backing_cow_direct_io() { + test_partial_cluster_write_with_backing_cow_impl(true); + } + + #[test] + fn test_partial_cluster_deallocate() { + // Punch hole on a partial cluster range. The deallocate_bytes path + // should produce WriteZeroes actions for partial clusters. + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + + let data: Vec = (0..2 * cluster_size as usize) + .map(|i| (i % 251) as u8) + .collect(); + let (_temp, disk) = create_disk_with_data(file_size, &data, 0, true, false); + + // Punch a partial range: last 4KB of cluster 0 + first 4KB of cluster 1 + let punch_offset = cluster_size - 4096; + let punch_len = 8192u64; + { + let mut aio = disk.create_async_io(1).unwrap(); + aio.punch_hole(punch_offset, punch_len, 10).unwrap(); + let (ud, res) = aio.next_completed_request().unwrap(); + assert_eq!(ud, 10); + assert_eq!(res, 0); + } + + let buf = async_read(&disk, 0, 2 * cluster_size as usize); + + // Before punch: unchanged + assert_eq!( + &buf[..punch_offset as usize], + &data[..punch_offset as usize], + "Data before punch should be unchanged" + ); + + // Punched region: zeros + assert!( + buf[punch_offset as usize..(punch_offset + punch_len) as usize] + .iter() + .all(|&b| b == 0), + "Punched region should be zeros" + ); + + // After punch: unchanged + let after = (punch_offset + punch_len) as usize; + assert_eq!( + &buf[after..2 * cluster_size as usize], + &data[after..2 * cluster_size as usize], + "Data after punch should be unchanged" + ); + } + + #[test] + fn test_resize_grow() { + let cluster_size = 1u64 << 16; + let initial_size = cluster_size * 4; + let data = vec![0xAA; cluster_size as usize]; + let (_temp, mut disk) = create_disk_with_data(initial_size, &data, 0, true, false); + + assert_eq!(disk.logical_size().unwrap(), initial_size); + + let new_size = cluster_size * 8; + disk.resize(new_size).unwrap(); + assert_eq!(disk.logical_size().unwrap(), new_size); + + // Original data intact + let buf = async_read(&disk, 0, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0xAA), + "Original data should survive resize" + ); + + // New region reads as zeros + let buf = async_read(&disk, initial_size, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0), + "Newly grown region should read as zeros" + ); + + // Can write to newly grown region + let new_data = vec![0xBB; cluster_size as usize]; + async_write(&disk, initial_size, &new_data); + { + let mut aio = disk.create_async_io(1).unwrap(); + aio.fsync(Some(1)).unwrap(); + } + let buf = async_read(&disk, initial_size, cluster_size as usize); + assert!( + buf.iter().all(|&b| b == 0xBB), + "Write to grown region should work" + ); + } + + #[test] + fn test_resize_with_backing_file_rejected() { + let backing_temp = TempFile::new().unwrap(); + let cluster_size = 1u64 << 16; + let file_size = cluster_size * 4; + backing_temp + .as_file() + .write_all(&vec![0u8; file_size as usize]) + .unwrap(); + backing_temp.as_file().sync_all().unwrap(); + let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); + + let overlay_temp = TempFile::new().unwrap(); + { + let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false); + let backing_config = BackingFileConfig { + path: backing_path, + format: Some(ImageType::Raw), + }; + let _overlay = + QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap(); + } + + let file = overlay_temp.as_file().try_clone().unwrap(); + let mut disk = QcowDisk::new(file, false, true, true, false).unwrap(); + + assert_eq!(disk.logical_size().unwrap(), file_size); + let result = disk.resize(file_size * 2); + assert!(result.is_err(), "resize with backing file should fail"); + assert_eq!( + disk.logical_size().unwrap(), + file_size, + "size should be unchanged after failed resize" + ); + } + + fn test_multi_iovec_read_write_impl(direct_io: bool) { + // Exercise scatter/gather with multiple iovecs per operation. + let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true, direct_io); + + // Write: 3 iovecs with distinct patterns + let a = vec![0xAAu8; 16 * 1024]; + let b = vec![0xBBu8; 32 * 1024]; + let c = vec![0xCCu8; 16 * 1024]; + let iovecs_w = [ + libc::iovec { + iov_base: a.as_ptr().cast::().cast_mut(), + iov_len: a.len(), + }, + libc::iovec { + iov_base: b.as_ptr().cast::().cast_mut(), + iov_len: b.len(), + }, + libc::iovec { + iov_base: c.as_ptr().cast::().cast_mut(), + iov_len: c.len(), + }, + ]; + let total = a.len() + b.len() + c.len(); + + let mut aio = disk.create_async_io(1).unwrap(); + aio.write_vectored(0, &iovecs_w, 1).unwrap(); + let (ud, res) = aio.next_completed_request().unwrap(); + assert_eq!(ud, 1); + assert_eq!(res as usize, total); + aio.fsync(Some(2)).unwrap(); + drop(aio); + + // Read back into 3 iovecs of different sizes + let mut r1 = vec![0u8; 8 * 1024]; + let mut r2 = vec![0u8; 48 * 1024]; + let mut r3 = vec![0u8; 8 * 1024]; + let iovecs_r = [ + libc::iovec { + iov_base: r1.as_mut_ptr().cast(), + iov_len: r1.len(), + }, + libc::iovec { + iov_base: r2.as_mut_ptr().cast(), + iov_len: r2.len(), + }, + libc::iovec { + iov_base: r3.as_mut_ptr().cast(), + iov_len: r3.len(), + }, + ]; + + let mut aio = disk.create_async_io(1).unwrap(); + aio.read_vectored(0, &iovecs_r, 10).unwrap(); + let (ud, res) = aio.next_completed_request().unwrap(); + assert_eq!(ud, 10); + assert_eq!(res as usize, total); + drop(aio); + + // Reassemble the read buffers into a flat vec + let mut got = Vec::with_capacity(total); + got.extend_from_slice(&r1); + got.extend_from_slice(&r2); + got.extend_from_slice(&r3); + + // Build expected from the write buffers + let mut expected = Vec::with_capacity(total); + expected.extend_from_slice(&a); + expected.extend_from_slice(&b); + expected.extend_from_slice(&c); + + assert_eq!(got, expected, "Multi iovec read should match written data"); + } + + #[test] + fn test_multi_iovec_read_write() { + test_multi_iovec_read_write_impl(false); + } + + #[test] + fn test_multi_iovec_read_write_direct_io() { + test_multi_iovec_read_write_impl(true); + } + + // -- Low level aligned I/O function tests -- + // + // Test aligned_pread and aligned_pwrite directly with controlled + // alignment values on a plain temp file. + + /// Create a temp file filled with a repeating pattern of the given size. + /// Returns the TempFile (must be kept alive) and the raw fd. + fn create_pattern_file(size: usize) -> (TempFile, RawFd) { + let tf = TempFile::new().unwrap(); + let pattern: Vec = (0..size).map(|i| (i % 251) as u8).collect(); + tf.as_file().write_all(&pattern).unwrap(); + tf.as_file().sync_all().unwrap(); + let fd = tf.as_file().as_raw_fd(); + (tf, fd) + } + + #[test] + fn test_aligned_pread_pass_through() { + // When buffer address, length, and offset are all aligned, + // aligned_pread should take the fast path (no bounce buffer). + let size = 4096usize; + let (_tf, fd) = create_pattern_file(size); + let alignment = 512; + + // Use AlignedBuf to guarantee buffer address alignment. + let mut abuf = AlignedBuf::new(size, alignment).unwrap(); + aligned_pread(fd, abuf.as_mut_slice(size), 0, alignment).unwrap(); + + let expected: Vec = (0..size).map(|i| (i % 251) as u8).collect(); + assert_eq!(abuf.as_slice(size), &expected[..]); + } + + #[test] + fn test_aligned_pread_bounce_unaligned_buffer() { + // Force a misaligned buffer so aligned_pread must take the + // bounce path. A plain vec![0u8; 4096] is often page-aligned + // by the allocator, which would skip the bounce entirely. + let size = 4096usize; + let (_tf, fd) = create_pattern_file(size); + let alignment = 512; + + let mut backing = vec![0u8; size + 1]; + let buf = &mut backing[1..size + 1]; + aligned_pread(fd, buf, 0, alignment).unwrap(); + + let expected: Vec = (0..size).map(|i| (i % 251) as u8).collect(); + assert_eq!(buf, &expected[..]); + } + + #[test] + fn test_aligned_pread_unaligned_offset() { + // Read at an offset that is not a multiple of alignment. + // aligned_pread should round down the offset, read an aligned + // region, then copy the correct slice into the caller buffer. + let file_size = 8192usize; + let (_tf, fd) = create_pattern_file(file_size); + let alignment = 512; + + let offset = 100u64; + let len = 200usize; + let mut buf = vec![0u8; len]; + aligned_pread(fd, &mut buf, offset, alignment).unwrap(); + + let expected: Vec = (offset as usize..offset as usize + len) + .map(|i| (i % 251) as u8) + .collect(); + assert_eq!(buf, expected); + } + + #[test] + fn test_aligned_pwrite_pass_through() { + // When buffer address, length, and offset are all aligned, + // aligned_pwrite should take the fast path. + let size = 4096usize; + let (_tf, fd) = create_pattern_file(size); + let alignment = 512; + + let data: Vec = (0..size).map(|i| ((i + 1) % 251) as u8).collect(); + let mut abuf = AlignedBuf::new(size, alignment).unwrap(); + abuf.as_mut_slice(size).copy_from_slice(&data); + aligned_pwrite(fd, abuf.as_slice(size), 0, alignment).unwrap(); + + let mut readback = vec![0u8; size]; + pread_exact(fd, &mut readback, 0).unwrap(); + assert_eq!(readback, data); + } + + #[test] + fn test_aligned_pwrite_bounce_unaligned_buffer() { + // Force a misaligned buffer so aligned_pwrite must take the + // bounce path. A plain vec![0u8; 4096] is often page-aligned + // by the allocator, which would skip the bounce entirely. + let size = 4096usize; + let (_tf, fd) = create_pattern_file(size); + let alignment = 512; + + let backing: Vec = (0..size + 1).map(|i| ((i + 1) % 251) as u8).collect(); + let data = &backing[1..size + 1]; + aligned_pwrite(fd, data, 0, alignment).unwrap(); + + let mut readback = vec![0u8; size]; + pread_exact(fd, &mut readback, 0).unwrap(); + assert_eq!(readback, data); + } + + #[test] + fn test_aligned_pwrite_unaligned_offset() { + // Write at an offset that is not a multiple of alignment. + // aligned_pwrite should do read-modify-write and preserve + // surrounding data. + let file_size = 8192usize; + let (_tf, fd) = create_pattern_file(file_size); + let alignment = 512; + + let offset = 100u64; + let len = 200usize; + let data: Vec = (0..len).map(|i| ((i + 1) % 239) as u8).collect(); + aligned_pwrite(fd, &data, offset, alignment).unwrap(); + + // Read entire file and verify the written region plus untouched areas. + let mut whole = vec![0u8; file_size]; + pread_exact(fd, &mut whole, 0).unwrap(); + + // Before the write region: original pattern. + let before: Vec = (0..offset as usize).map(|i| (i % 251) as u8).collect(); + assert_eq!(&whole[..offset as usize], &before[..]); + + // The written region. + assert_eq!(&whole[offset as usize..offset as usize + len], &data[..]); + + // After the write region: original pattern. + let after_start = offset as usize + len; + let after: Vec = (after_start..file_size).map(|i| (i % 251) as u8).collect(); + assert_eq!(&whole[after_start..], &after[..]); + } + + #[test] + fn test_aligned_pread_pwrite_4096_alignment() { + // Exercise aligned I/O with 4096 byte alignment. + let file_size = 16384usize; + let (_tf, fd) = create_pattern_file(file_size); + let alignment = 4096; + + // Write 4096 bytes at offset 4096 via unaligned Vec. + let offset = 4096u64; + let len = 4096usize; + let data: Vec = (0..len).map(|i| ((i + 1) % 239) as u8).collect(); + aligned_pwrite(fd, &data, offset, alignment).unwrap(); + + // Read back the written region via unaligned Vec. + let mut buf = vec![0u8; len]; + aligned_pread(fd, &mut buf, offset, alignment).unwrap(); + assert_eq!(buf, data); + + // Verify untouched regions. + let mut whole = vec![0u8; file_size]; + pread_exact(fd, &mut whole, 0).unwrap(); + let before: Vec = (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 = (after_start..file_size).map(|i| (i % 251) as u8).collect(); + assert_eq!(&whole[after_start..], &after[..]); + } + + #[test] + fn test_aligned_buf_allocation_and_access() { + for alignment in [512, 4096] { + let size = 1024usize; + let mut abuf = AlignedBuf::new(size, alignment).unwrap(); + let aligned_size = size.next_multiple_of(alignment); + + assert!( + (abuf.ptr() as usize).is_multiple_of(alignment), + "ptr not aligned to {alignment}" + ); + assert!(abuf.as_slice(aligned_size).iter().all(|&b| b == 0)); + + let pattern: Vec = (0..size).map(|i| (i % 251) as u8).collect(); + abuf.as_mut_slice(size).copy_from_slice(&pattern); + assert_eq!(abuf.as_slice(size), &pattern[..]); + } + } + + #[test] + fn test_aligned_buf_size_rounds_up() { + let abuf = AlignedBuf::new(1, 512).unwrap(); + assert_eq!(abuf.layout().size(), 512); + + let abuf = AlignedBuf::new(513, 512).unwrap(); + assert_eq!(abuf.layout().size(), 1024); + } + + #[test] + fn test_compressed_read() { + let cluster_size = 65536usize; + let data: Vec = (0..=255).cycle().take(cluster_size).collect(); + let (temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, 0, false, false); + drop(disk); + + compress_allocated_clusters(&mut temp.as_file().try_clone().unwrap()); + + let disk = QcowDisk::new( + temp.as_file().try_clone().unwrap(), + false, + false, + false, + false, + ) + .unwrap(); + + let buf = async_read(&disk, 0, cluster_size); + assert_eq!(buf, data); + } } diff --git a/block/src/raw_async.rs b/block/src/raw_async.rs index 496445c6ad..79c84c05ae 100644 --- a/block/src/raw_async.rs +++ b/block/src/raw_async.rs @@ -2,75 +2,43 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use std::fs::File; -use std::io::{Error, Seek, SeekFrom}; +use std::io::Error; use std::os::unix::io::{AsRawFd, RawFd}; -use io_uring::{opcode, types, IoUring}; +use io_uring::{IoUring, opcode, types}; +use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE}; use vmm_sys_util::eventfd::EventFd; -use crate::async_io::{ - AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, -}; -use crate::DiskTopology; - -pub struct RawFileDisk { - file: File, -} - -impl RawFileDisk { - pub fn new(file: File) -> Self { - RawFileDisk { file } - } -} - -impl DiskFile for RawFileDisk { - fn size(&mut self) -> DiskFileResult { - self.file - .seek(SeekFrom::End(0)) - .map_err(DiskFileError::Size) - } - - fn new_async_io(&self, ring_depth: u32) -> DiskFileResult> { - Ok(Box::new( - RawFileAsync::new(self.file.as_raw_fd(), ring_depth) - .map_err(DiskFileError::NewAsyncIo)?, - ) as Box) - } - - fn topology(&mut self) -> DiskTopology { - if let Ok(topology) = DiskTopology::probe(&self.file) { - topology - } else { - warn!("Unable to get device topology. Using default topology"); - DiskTopology::default() - } - } - - fn fd(&mut self) -> BorrowedDiskFd<'_> { - BorrowedDiskFd::new(self.file.as_raw_fd()) - } -} +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; +use crate::error::{BlockError, BlockErrorKind, BlockResult}; +use crate::{BatchRequest, RequestType, SECTOR_SIZE}; pub struct RawFileAsync { fd: RawFd, io_uring: IoUring, eventfd: EventFd, + alignment: u64, } impl RawFileAsync { - pub fn new(fd: RawFd, ring_depth: u32) -> std::io::Result { - let io_uring = IoUring::new(ring_depth)?; - let eventfd = EventFd::new(libc::EFD_NONBLOCK)?; + pub fn new(fd: RawFd, ring_depth: u32) -> BlockResult { + let io_uring = + IoUring::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + let eventfd = + EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; // Register the io_uring eventfd that will notify when something in // the completion queue is ready. - io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?; + io_uring + .submitter() + .register_eventfd(eventfd.as_raw_fd()) + .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; Ok(RawFileAsync { fd, io_uring, eventfd, + alignment: SECTOR_SIZE, }) } } @@ -80,6 +48,10 @@ impl AsyncIo for RawFileAsync { &self.eventfd } + fn alignment(&self) -> u64 { + self.alignment + } + fn read_vectored( &mut self, offset: libc::off_t, @@ -97,7 +69,9 @@ impl AsyncIo for RawFileAsync { .build() .user_data(user_data), ) - .map_err(|_| AsyncIoError::ReadVectored(Error::other("Submission queue is full")))? + .map_err(|e| { + AsyncIoError::ReadVectored(Error::other(format!("Submission queue is full: {e:?}"))) + })?; }; // Update the submission queue and submit new operations to the @@ -125,7 +99,11 @@ impl AsyncIo for RawFileAsync { .build() .user_data(user_data), ) - .map_err(|_| AsyncIoError::WriteVectored(Error::other("Submission queue is full")))? + .map_err(|e| { + AsyncIoError::WriteVectored(Error::other(format!( + "Submission queue is full: {e:?}" + ))) + })?; }; // Update the submission queue and submit new operations to the @@ -147,7 +125,9 @@ impl AsyncIo for RawFileAsync { .build() .user_data(user_data), ) - .map_err(|_| AsyncIoError::Fsync(Error::other("Submission queue is full")))? + .map_err(|e| { + AsyncIoError::Fsync(Error::other(format!("Submission queue is full: {e:?}"))) + })?; }; // Update the submission queue and submit new operations to the @@ -168,4 +148,139 @@ impl AsyncIo for RawFileAsync { .next() .map(|entry| (entry.user_data(), entry.result())) } + + fn batch_requests_enabled(&self) -> bool { + true + } + + fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> { + if !self.batch_requests_enabled() { + return Ok(()); + } + + let (submitter, mut sq, _) = self.io_uring.split(); + let mut submitted = false; + + // Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially + // successful push. + if batch_request.len() > sq.capacity() - sq.len() { + return Err(AsyncIoError::SubmitBatchRequests(Error::other( + "io_uring submission queue is full", + ))); + } + + for req in batch_request { + match req.request_type { + RequestType::In => { + // SAFETY: we know the file descriptor is valid and we + // relied on vm-memory to provide the buffer address. + unsafe { + sq.push( + &opcode::Readv::new( + types::Fd(self.fd), + req.iovecs.as_ptr(), + req.iovecs.len() as u32, + ) + .offset(req.offset as u64) + .build() + .user_data(req.user_data), + ) + .map_err(|e| { + AsyncIoError::ReadVectored(Error::other(format!( + "Submission queue is full: {e:?}" + ))) + })?; + }; + submitted = true; + } + RequestType::Out => { + // SAFETY: we know the file descriptor is valid and we + // relied on vm-memory to provide the buffer address. + unsafe { + sq.push( + &opcode::Writev::new( + types::Fd(self.fd), + req.iovecs.as_ptr(), + req.iovecs.len() as u32, + ) + .offset(req.offset as u64) + .build() + .user_data(req.user_data), + ) + .map_err(|e| { + AsyncIoError::WriteVectored(Error::other(format!( + "Submission queue is full: {e:?}" + ))) + })?; + }; + submitted = true; + } + _ => { + unreachable!("Unexpected batch request type: {:?}", req.request_type) + } + } + } + + // Only submit if we actually queued something + if submitted { + // Update the submission queue and submit new operations to the + // io_uring instance. + sq.sync(); + submitter + .submit() + .map_err(AsyncIoError::SubmitBatchRequests)?; + } + + Ok(()) + } + + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + let (submitter, mut sq, _) = self.io_uring.split(); + + let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: The file descriptor is known to be valid. + unsafe { + sq.push( + &opcode::Fallocate::new(types::Fd(self.fd), length) + .offset(offset) + .mode(mode) + .build() + .user_data(user_data), + ) + .map_err(|e| { + AsyncIoError::PunchHole(Error::other(format!("Submission queue is full: {e:?}"))) + })?; + }; + + sq.sync(); + submitter.submit().map_err(AsyncIoError::PunchHole)?; + + Ok(()) + } + + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + let (submitter, mut sq, _) = self.io_uring.split(); + + let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: The file descriptor is known to be valid. + unsafe { + sq.push( + &opcode::Fallocate::new(types::Fd(self.fd), length) + .offset(offset) + .mode(mode) + .build() + .user_data(user_data), + ) + .map_err(|e| { + AsyncIoError::WriteZeroes(Error::other(format!("Submission queue is full: {e:?}"))) + })?; + }; + + sq.sync(); + submitter.submit().map_err(AsyncIoError::WriteZeroes)?; + + Ok(()) + } } diff --git a/block/src/raw_async_aio.rs b/block/src/raw_async_aio.rs index 9ef0c62619..3636fd7fc1 100644 --- a/block/src/raw_async_aio.rs +++ b/block/src/raw_async_aio.rs @@ -5,68 +5,39 @@ // Copyright © 2023 Crusoe Energy Systems LLC // -use std::fs::File; -use std::io::{Seek, SeekFrom}; +use std::collections::VecDeque; use std::os::unix::io::{AsRawFd, RawFd}; +use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE}; use vmm_sys_util::aio; use vmm_sys_util::eventfd::EventFd; -use crate::async_io::{ - AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, -}; -use crate::DiskTopology; - -pub struct RawFileDiskAio { - file: File, -} - -impl RawFileDiskAio { - pub fn new(file: File) -> Self { - RawFileDiskAio { file } - } -} - -impl DiskFile for RawFileDiskAio { - fn size(&mut self) -> DiskFileResult { - self.file - .seek(SeekFrom::End(0)) - .map_err(DiskFileError::Size) - } - - fn new_async_io(&self, ring_depth: u32) -> DiskFileResult> { - Ok(Box::new( - RawFileAsyncAio::new(self.file.as_raw_fd(), ring_depth) - .map_err(DiskFileError::NewAsyncIo)?, - ) as Box) - } - - fn topology(&mut self) -> DiskTopology { - if let Ok(topology) = DiskTopology::probe(&self.file) { - topology - } else { - warn!("Unable to get device topology. Using default topology"); - DiskTopology::default() - } - } - - fn fd(&mut self) -> BorrowedDiskFd<'_> { - BorrowedDiskFd::new(self.file.as_raw_fd()) - } -} +use crate::SECTOR_SIZE; +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; +use crate::error::{BlockError, BlockErrorKind, BlockResult}; pub struct RawFileAsyncAio { fd: RawFd, ctx: aio::IoContext, eventfd: EventFd, + alignment: u64, + completion_list: VecDeque<(u64, i32)>, } impl RawFileAsyncAio { - pub fn new(fd: RawFd, queue_depth: u32) -> std::io::Result { - let eventfd = EventFd::new(libc::EFD_NONBLOCK)?; - let ctx = aio::IoContext::new(queue_depth)?; - - Ok(RawFileAsyncAio { fd, ctx, eventfd }) + pub fn new(fd: RawFd, queue_depth: u32) -> BlockResult { + let eventfd = + EventFd::new(libc::EFD_NONBLOCK).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + let ctx = + aio::IoContext::new(queue_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; + + Ok(RawFileAsyncAio { + fd, + ctx, + eventfd, + alignment: SECTOR_SIZE, + completion_list: VecDeque::new(), + }) } } @@ -75,6 +46,10 @@ impl AsyncIo for RawFileAsyncAio { &self.eventfd } + fn alignment(&self) -> u64 { + self.alignment + } + fn read_vectored( &mut self, offset: libc::off_t, @@ -145,12 +120,99 @@ impl AsyncIo for RawFileAsyncAio { } fn next_completed_request(&mut self) -> Option<(u64, i32)> { - let mut events: [aio::IoEvent; 1] = [aio::IoEvent::default()]; - let rc = self.ctx.get_events(0, &mut events, None).unwrap(); - if rc == 0 { - None - } else { - Some((events[0].data, events[0].res as i32)) + if self.completion_list.is_empty() { + // Drain pending AIO completions batched into the same queue. + let mut events = [aio::IoEvent::default(); 32]; + let rc = self.ctx.get_events(0, &mut events, None).unwrap(); + for event in &events[..rc] { + self.completion_list + .push_back((event.data, event.res as i32)); + } + } + self.completion_list.pop_front() + } + + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + // Linux AIO has no IOCB command for fallocate, so perform the operation + // synchronously and signal completion via the completion list, matching + // the pattern used by the sync backend (RawFileSync). + let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: FFI call with valid arguments + let result = unsafe { + libc::fallocate( + self.fd as libc::c_int, + mode, + offset as libc::off_t, + length as libc::off_t, + ) + }; + if result < 0 { + return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error())); } + + self.completion_list.push_back((user_data, result)); + self.eventfd.write(1).unwrap(); + + Ok(()) + } + + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + // Linux AIO has no IOCB command for fallocate, so perform the operation + // synchronously and signal completion via the completion list, matching + // the pattern used by the sync backend (RawFileSync). + let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: FFI call with valid arguments + let result = unsafe { + libc::fallocate( + self.fd as libc::c_int, + mode, + offset as libc::off_t, + length as libc::off_t, + ) + }; + if result < 0 { + return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error())); + } + + self.completion_list.push_back((user_data, result)); + self.eventfd.write(1).unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod unit_tests { + use std::os::unix::io::AsRawFd; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::raw_async_io_tests; + + #[test] + fn test_punch_hole() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap(); + raw_async_io_tests::test_punch_hole(&mut async_io, &mut file); + } + + #[test] + fn test_write_zeroes() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap(); + raw_async_io_tests::test_write_zeroes(&mut async_io, &mut file); + } + + #[test] + fn test_punch_hole_multiple_operations() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + let mut async_io = RawFileAsyncAio::new(file.as_raw_fd(), 128).unwrap(); + raw_async_io_tests::test_punch_hole_multiple_operations(&mut async_io, &mut file); } } diff --git a/block/src/raw_async_io_tests.rs b/block/src/raw_async_io_tests.rs new file mode 100644 index 0000000000..560e41e334 --- /dev/null +++ b/block/src/raw_async_io_tests.rs @@ -0,0 +1,162 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +//! Shared test helpers for [`AsyncIo`] backends. +//! +//! Each helper takes a `&mut dyn AsyncIo` together with the [`File`] handle +//! that backs the I/O object, so the same logic exercises every backend with +//! only the constructor differing. + +use std::fs::File; +use std::io::{Read, Seek, SeekFrom, Write}; + +use crate::async_io::{AsyncIo, AsyncIoError}; + +/// 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) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 1); + assert_eq!(result, 0); + + // Verify the hole reads as zeros + file.seek(SeekFrom::Start(offset)).unwrap(); + let mut read_buf = vec![0; length as usize]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0), + "Punched hole should read as zeros" + ); + + // Verify data before hole is intact + file.seek(SeekFrom::Start(0)).unwrap(); + let mut read_buf = vec![0; 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0xAA), + "Data before hole should be intact" + ); + + // Verify data after hole is intact + file.seek(SeekFrom::Start(offset + length)).unwrap(); + let mut read_buf = vec![0; 1024]; + file.read_exact(&mut read_buf).unwrap(); + assert!( + read_buf.iter().all(|&b| b == 0xAA), + "Data after hole should be intact" + ); +} + +/// Tests writing zeroes to a 512 KB region inside a 4 MB file and verifying +/// surrounding data is preserved. Gracefully skips when the filesystem does +/// not support `FALLOC_FL_ZERO_RANGE`. +pub fn test_write_zeroes(async_io: &mut dyn AsyncIo, file: &mut File) { + // Write 4MB of data + let data = vec![0xBB; 4 * 1024 * 1024]; + file.write_all(&data).unwrap(); + file.sync_all().unwrap(); + + // Write zeros in the middle (512KB at offset 2MB) + let offset = 2 * 1024 * 1024; + let length = 512 * 1024; + let write_zeroes_result = async_io.write_zeroes(offset, length, 2); + + // FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs) + // If it fails with ENOTSUP, skip the test + if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result + && (e.raw_os_error() == Some(libc::EOPNOTSUPP) || e.raw_os_error() == Some(libc::ENOTSUP)) + { + eprintln!("Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE"); + return; + } + write_zeroes_result.unwrap(); + + // Check completion + let (user_data, result) = async_io.next_completed_request().unwrap(); + 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) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 10); + assert_eq!(result, 0); + + let (user_data, result) = async_io.next_completed_request().unwrap(); + assert_eq!(user_data, 11); + assert_eq!(result, 0); + + let (user_data, result) = async_io.next_completed_request().unwrap(); + 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)); +} diff --git a/block/src/raw_disk.rs b/block/src/raw_disk.rs new file mode 100644 index 0000000000..82dd3c5302 --- /dev/null +++ b/block/src/raw_disk.rs @@ -0,0 +1,265 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +use std::fs::File; +use std::io; +use std::os::unix::fs::FileTypeExt; +use std::os::unix::io::AsRawFd; + +use log::warn; + +use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; +use crate::error::{BlockError, BlockErrorKind, BlockResult}; +#[cfg(feature = "io_uring")] +use crate::raw_async::RawFileAsync; +use crate::raw_async_aio::RawFileAsyncAio; +use crate::raw_sync::RawFileSync; +use crate::{DiskTopology, disk_file, probe_sparse_support, query_device_size}; + +/// 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, +} + +impl RawDisk { + pub fn new(file: File, backend: RawBackend) -> Self { + Self { file, backend } + } +} + +impl disk_file::DiskSize for RawDisk { + fn logical_size(&self) -> BlockResult { + 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 { + query_device_size(&self.file) + .map(|(_, physical_size)| physical_size) + .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Size(e))) + } +} + +impl disk_file::DiskFd for RawDisk { + fn fd(&self) -> BorrowedDiskFd<'_> { + BorrowedDiskFd::new(self.file.as_raw_fd()) + } +} + +impl disk_file::Geometry for RawDisk { + fn topology(&self) -> DiskTopology { + DiskTopology::probe(&self.file).unwrap_or_else(|_| { + warn!("Unable to get device topology. Using default topology"); + DiskTopology::default() + }) + } +} + +impl disk_file::SparseCapable for RawDisk { + fn supports_sparse_operations(&self) -> bool { + probe_sparse_support(&self.file) + } +} + +impl disk_file::Resizable for RawDisk { + fn resize(&mut self, size: u64) -> BlockResult<()> { + let fd_metadata = self + .file + .metadata() + .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?; + + if fd_metadata.file_type().is_block_device() { + // Block devices cannot be resized via ftruncate; they are resized + // externally (LVM, losetup, etc.). Verify the size matches. + let (actual_size, _) = query_device_size(&self.file) + .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e)))?; + if actual_size != size { + return Err(BlockError::new( + BlockErrorKind::Io, + DiskFileError::ResizeError(io::Error::other(format!( + "Block device size {actual_size} does not match requested size {size}" + ))), + )); + } + Ok(()) + } else { + self.file + .set_len(size) + .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e))) + } + } +} + +impl disk_file::DiskFile for RawDisk {} + +impl disk_file::AsyncDiskFile for RawDisk { + fn try_clone(&self) -> BlockResult> { + let file = self + .file + .try_clone() + .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?; + Ok(Box::new(RawDisk { + file, + backend: self.backend, + })) + } + + fn create_async_io(&self, ring_depth: u32) -> BlockResult> { + match self.backend { + RawBackend::Sync => Ok(Box::new(RawFileSync::new(self.file.as_raw_fd()))), + #[cfg(feature = "io_uring")] + RawBackend::IoUring => Ok(Box::new(RawFileAsync::new( + self.file.as_raw_fd(), + ring_depth, + )?)), + RawBackend::Aio => Ok(Box::new(RawFileAsyncAio::new( + self.file.as_raw_fd(), + ring_depth, + )?)), + } + } +} + +#[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); + assert_eq!(disk.logical_size().unwrap(), TEST_SIZE); + } + + fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_backend: RawBackend) { + let io: Box = 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); + assert_sync_backend(&disk); + } + + #[test] + fn aio_backend_disables_batch_requests() { + let file = make_raw_file(); + let disk = RawDisk::new(file, RawBackend::Aio); + 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); + 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); + 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); + 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); + assert_try_clone(&disk, RawBackend::IoUring); + } + + #[test] + fn resize_changes_file_size() { + let file = make_raw_file(); + let mut disk = RawDisk::new(file, RawBackend::Aio); + let 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); + // Sparse file: physical size is less than logical size. + assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap()); + } +} diff --git a/block/src/raw_sync.rs b/block/src/raw_sync.rs index 54ba1acca6..659693f29c 100644 --- a/block/src/raw_sync.rs +++ b/block/src/raw_sync.rs @@ -3,56 +3,19 @@ // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::collections::VecDeque; -use std::fs::File; -use std::io::{Seek, SeekFrom}; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::RawFd; +use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE}; use vmm_sys_util::eventfd::EventFd; -use crate::async_io::{ - AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, -}; -use crate::DiskTopology; - -pub struct RawFileDiskSync { - file: File, -} - -impl RawFileDiskSync { - pub fn new(file: File) -> Self { - RawFileDiskSync { file } - } -} - -impl DiskFile for RawFileDiskSync { - fn size(&mut self) -> DiskFileResult { - self.file - .seek(SeekFrom::End(0)) - .map_err(DiskFileError::Size) - } - - fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult> { - Ok(Box::new(RawFileSync::new(self.file.as_raw_fd())) as Box) - } - - fn topology(&mut self) -> DiskTopology { - if let Ok(topology) = DiskTopology::probe(&self.file) { - topology - } else { - warn!("Unable to get device topology. Using default topology"); - DiskTopology::default() - } - } - - fn fd(&mut self) -> BorrowedDiskFd<'_> { - BorrowedDiskFd::new(self.file.as_raw_fd()) - } -} +use crate::SECTOR_SIZE; +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult}; pub struct RawFileSync { fd: RawFd, eventfd: EventFd, completion_list: VecDeque<(u64, i32)>, + alignment: u64, } impl RawFileSync { @@ -61,6 +24,7 @@ impl RawFileSync { fd, eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"), completion_list: VecDeque::new(), + alignment: SECTOR_SIZE, } } } @@ -70,6 +34,10 @@ impl AsyncIo for RawFileSync { &self.eventfd } + fn alignment(&self) -> u64 { + self.alignment + } + fn read_vectored( &mut self, offset: libc::off_t, @@ -138,4 +106,82 @@ impl AsyncIo for RawFileSync { fn next_completed_request(&mut self) -> Option<(u64, i32)> { self.completion_list.pop_front() } + + fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: FFI call with valid arguments + let result = unsafe { + libc::fallocate( + self.fd as libc::c_int, + mode, + offset as libc::off_t, + length as libc::off_t, + ) + }; + if result < 0 { + return Err(AsyncIoError::PunchHole(std::io::Error::last_os_error())); + } + + self.completion_list.push_back((user_data, result)); + self.eventfd.write(1).unwrap(); + + Ok(()) + } + + fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { + let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE; + + // SAFETY: FFI call with valid arguments + let result = unsafe { + libc::fallocate( + self.fd as libc::c_int, + mode, + offset as libc::off_t, + length as libc::off_t, + ) + }; + if result < 0 { + return Err(AsyncIoError::WriteZeroes(std::io::Error::last_os_error())); + } + + self.completion_list.push_back((user_data, result)); + self.eventfd.write(1).unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod unit_tests { + use std::os::unix::io::AsRawFd; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::raw_async_io_tests; + + #[test] + fn test_punch_hole() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + let mut async_io = RawFileSync::new(file.as_raw_fd()); + raw_async_io_tests::test_punch_hole(&mut async_io, &mut file); + } + + #[test] + fn test_write_zeroes() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + let mut async_io = RawFileSync::new(file.as_raw_fd()); + raw_async_io_tests::test_write_zeroes(&mut async_io, &mut file); + } + + #[test] + fn test_punch_hole_multiple_operations() { + let temp_file = TempFile::new().unwrap(); + let mut file = temp_file.into_file(); + let mut async_io = RawFileSync::new(file.as_raw_fd()); + raw_async_io_tests::test_punch_hole_multiple_operations(&mut async_io, &mut file); + } } diff --git a/block/src/request.rs b/block/src/request.rs new file mode 100644 index 0000000000..ab6685f6b6 --- /dev/null +++ b/block/src/request.rs @@ -0,0 +1,573 @@ +// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Portions Copyright 2017 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE-BSD-3-Clause file. +// +// Copyright © 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +use std::io::{Read, Seek, SeekFrom, Write}; +use std::mem; +use std::time::Instant; + +use log::{error, warn}; +use smallvec::SmallVec; +use virtio_bindings::virtio_blk::{ + VIRTIO_BLK_T_DISCARD, VIRTIO_BLK_T_WRITE_ZEROES, VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP, + virtio_blk_discard_write_zeroes, +}; +use virtio_queue::DescriptorChain; +use vm_memory::bitmap::Bitmap; +use vm_memory::{ + Address as _, Bytes as _, GuestAddress, GuestMemory as _, GuestMemoryError, + GuestMemoryLoadGuard, +}; +use vm_virtio::{AccessPlatform, Translatable as _}; + +use crate::aligned_operation::AlignedOperation; +use crate::async_io::AsyncIo; +use crate::{Error, ExecuteError, request_type, sector}; + +const SECTOR_SHIFT: u8 = 9; +pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT; + +/// Maximum number of segments per DISCARD or WRITE_ZEROES request. +pub const MAX_DISCARD_WRITE_ZEROES_SEG: u32 = 1; +/// Size and field offsets within `struct virtio_blk_discard_write_zeroes`. +const DISCARD_WZ_SEG_SIZE: u32 = mem::size_of::() as u32; +const DISCARD_WZ_MAX_PAYLOAD: u32 = DISCARD_WZ_SEG_SIZE * MAX_DISCARD_WRITE_ZEROES_SEG; +const DISCARD_WZ_SECTOR_OFFSET: u64 = + mem::offset_of!(virtio_blk_discard_write_zeroes, sector) as u64; +const DISCARD_WZ_NUM_SECTORS_OFFSET: u64 = + mem::offset_of!(virtio_blk_discard_write_zeroes, num_sectors) as u64; +const DISCARD_WZ_FLAGS_OFFSET: u64 = mem::offset_of!(virtio_blk_discard_write_zeroes, flags) as u64; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RequestType { + In, + Out, + Flush, + GetDeviceId, + Discard, + WriteZeroes, + Unsupported(u32), +} + +pub const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32; +pub struct BatchRequest { + pub offset: libc::off_t, + pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>, + pub user_data: u64, + pub request_type: RequestType, +} + +pub struct ExecuteAsync { + // `true` if the execution will complete asynchronously + pub async_complete: bool, + // request need to be batched for submission if any + pub batch_request: Option, +} + +#[derive(Debug)] +pub struct Request { + request_type: RequestType, + sector: u64, + data_descriptors: SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]>, + status_addr: GuestAddress, + pub writeback: bool, + aligned_operations: SmallVec<[AlignedOperation; DEFAULT_DESCRIPTOR_VEC_SIZE]>, + start: Instant, +} + +impl Request { + pub fn parse( + desc_chain: &mut DescriptorChain>>, + access_platform: Option<&dyn AccessPlatform>, + ) -> Result { + let hdr_desc = desc_chain + .next() + .ok_or(Error::DescriptorChainTooShort) + .inspect_err(|_| { + error!("Missing head descriptor"); + })?; + + // The head contains the request type which MUST be readable. + if hdr_desc.is_write_only() { + return Err(Error::UnexpectedWriteOnlyDescriptor); + } + + let hdr_desc_addr = hdr_desc + .addr() + .translate_gva(access_platform, hdr_desc.len() as usize) + .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?; + + let mut req = Request { + request_type: request_type(desc_chain.memory(), hdr_desc_addr)?, + sector: sector(desc_chain.memory(), hdr_desc_addr)?, + data_descriptors: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE), + status_addr: GuestAddress(0), + writeback: true, + aligned_operations: SmallVec::with_capacity(DEFAULT_DESCRIPTOR_VEC_SIZE), + start: Instant::now(), + }; + + let status_desc; + let mut desc = desc_chain + .next() + .ok_or(Error::DescriptorChainTooShort) + .inspect_err(|_| { + error!("Only head descriptor present: request = {req:?}"); + })?; + + if desc.has_next() { + req.data_descriptors.reserve_exact(1); + while desc.has_next() { + if desc.is_write_only() && req.request_type == RequestType::Out { + return Err(Error::UnexpectedWriteOnlyDescriptor); + } + if desc.is_write_only() && req.request_type == RequestType::Discard { + return Err(Error::UnexpectedWriteOnlyDescriptor); + } + if desc.is_write_only() && req.request_type == RequestType::WriteZeroes { + return Err(Error::UnexpectedWriteOnlyDescriptor); + } + if !desc.is_write_only() && req.request_type == RequestType::In { + return Err(Error::UnexpectedReadOnlyDescriptor); + } + if !desc.is_write_only() && req.request_type == RequestType::GetDeviceId { + return Err(Error::UnexpectedReadOnlyDescriptor); + } + + req.data_descriptors.push(( + desc.addr() + .translate_gva(access_platform, desc.len() as usize) + .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?, + desc.len(), + )); + desc = desc_chain + .next() + .ok_or(Error::DescriptorChainTooShort) + .inspect_err(|_| { + error!("DescriptorChain corrupted: request = {req:?}"); + })?; + } + status_desc = desc; + } else { + status_desc = desc; + // Only flush requests are allowed to skip the data descriptor. + if req.request_type != RequestType::Flush { + error!("Need a data descriptor: request = {req:?}"); + return Err(Error::DescriptorChainTooShort); + } + } + + // The status MUST always be writable. + if !status_desc.is_write_only() { + return Err(Error::UnexpectedReadOnlyDescriptor); + } + + if status_desc.len() < 1 { + return Err(Error::DescriptorLengthTooSmall); + } + + req.status_addr = status_desc + .addr() + .translate_gva(access_platform, status_desc.len() as usize) + .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?; + + Ok(req) + } + + pub fn execute( + &self, + disk: &mut T, + disk_nsectors: u64, + mem: &vm_memory::GuestMemoryMmap, + serial: &[u8], + ) -> Result { + self.check_data_bounds(disk_nsectors)?; + + disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT)) + .map_err(ExecuteError::Seek)?; + let mut len = 0; + for (data_addr, data_len) in &self.data_descriptors { + match self.request_type { + RequestType::In => { + let mut buf = vec![0u8; *data_len as usize]; + disk.read_exact(&mut buf).map_err(ExecuteError::ReadExact)?; + mem.read_exact_volatile_from( + *data_addr, + &mut buf.as_slice(), + *data_len as usize, + ) + .map_err(ExecuteError::Read)?; + len += data_len; + } + RequestType::Out => { + let mut buf: Vec = Vec::new(); + mem.write_all_volatile_to(*data_addr, &mut buf, *data_len as usize) + .map_err(ExecuteError::Write)?; + disk.write_all(&buf).map_err(ExecuteError::WriteAll)?; + if !self.writeback { + disk.flush().map_err(ExecuteError::Flush)?; + } + } + RequestType::Flush => disk.flush().map_err(ExecuteError::Flush)?, + RequestType::GetDeviceId => { + if (*data_len as usize) < serial.len() { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + mem.write_slice(serial, *data_addr) + .map_err(ExecuteError::Write)?; + } + RequestType::Discard => { + return Err(ExecuteError::Unsupported(VIRTIO_BLK_T_DISCARD)); + } + RequestType::WriteZeroes => { + return Err(ExecuteError::Unsupported(VIRTIO_BLK_T_WRITE_ZEROES)); + } + RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)), + } + } + Ok(len) + } + + pub fn execute_async( + &mut self, + mem: &vm_memory::GuestMemoryMmap, + disk_nsectors: u64, + disk_image: &mut dyn AsyncIo, + serial: &[u8], + disable_sector0_writes: bool, + user_data: u64, + ) -> Result { + let sector = self.sector; + let request_type = self.request_type; + let offset = (sector << SECTOR_SHIFT) as libc::off_t; + let alignment = disk_image.alignment(); + + self.check_data_bounds(disk_nsectors)?; + + let mut iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]> = + SmallVec::with_capacity(self.data_descriptors.len()); + for &(data_addr, data_len) in &self.data_descriptors { + let _: u32 = data_len; // compiler-checked documentation + const _: () = assert!( + core::mem::size_of::() <= core::mem::size_of::(), + "unsupported platform" + ); + if data_len == 0 { + continue; + } + let data_len = data_len as usize; + + let origin_ptr = mem + .get_slice(data_addr, data_len) + .map_err(ExecuteError::GetHostAddress)?; + assert!(origin_ptr.len() >= data_len); + let origin_ptr = origin_ptr.ptr_guard_mut(); + + // O_DIRECT requires buffer addresses to be aligned to the + // backend device's logical block size. In case it's not properly + // aligned, an intermediate buffer is created with the correct + // alignment, and a copy from/to the origin buffer is performed, + // depending on the type of operation. + let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(alignment) { + origin_ptr.as_ptr().cast() + } else { + let mut aligned_op = AlignedOperation::new(data_addr, data_len, alignment as usize) + .map_err(ExecuteError::TemporaryBufferAllocation)?; + + // We need to perform the copy beforehand in case we're writing + // data out. + if request_type == RequestType::Out { + mem.read_slice(aligned_op.as_bytes_mut(), data_addr) + .map_err(ExecuteError::Read)?; + } + + let aligned_ptr = aligned_op.as_mut_ptr(); + self.aligned_operations.push(aligned_op); + + aligned_ptr.cast() + }; + + let iovec = libc::iovec { + iov_base, + iov_len: data_len as libc::size_t, + }; + iovecs.push(iovec); + } + + let mut ret = ExecuteAsync { + async_complete: true, + batch_request: None, + }; + // Queue operations expected to be submitted. + match request_type { + RequestType::In => { + for (data_addr, data_len) in &self.data_descriptors { + mem.get_slice(*data_addr, *data_len as usize) + .map_err(ExecuteError::GetHostAddress)? + .bitmap() + .mark_dirty(0, *data_len as usize); + } + if disk_image.batch_requests_enabled() { + ret.batch_request = Some(BatchRequest { + offset, + iovecs, + user_data, + request_type, + }); + } else { + disk_image + .read_vectored(offset, &iovecs, user_data) + .map_err(ExecuteError::AsyncRead)?; + } + } + RequestType::Out => { + if disk_image.batch_requests_enabled() { + ret.batch_request = Some(BatchRequest { + offset, + iovecs, + user_data, + request_type, + }); + } else { + disk_image + .write_vectored(offset, &iovecs, user_data) + .map_err(ExecuteError::AsyncWrite)?; + } + } + RequestType::Flush => { + disk_image + .fsync(Some(user_data)) + .map_err(ExecuteError::AsyncFlush)?; + } + RequestType::GetDeviceId => { + let (data_addr, data_len) = if self.data_descriptors.len() == 1 { + (self.data_descriptors[0].0, self.data_descriptors[0].1) + } else { + return Err(ExecuteError::BadRequest(Error::TooManyDescriptors)); + }; + if (data_len as usize) < serial.len() { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + mem.write_slice(serial, data_addr) + .map_err(ExecuteError::Write)?; + ret.async_complete = false; + return Ok(ret); + } + RequestType::Discard => { + let (data_addr, data_len) = if self.data_descriptors.len() == 1 { + (self.data_descriptors[0].0, self.data_descriptors[0].1) + } else { + return Err(ExecuteError::BadRequest(Error::TooManyDescriptors)); + }; + + if data_len < DISCARD_WZ_SEG_SIZE { + return Err(ExecuteError::BadRequest(Error::DescriptorLengthTooSmall)); + } + if data_len > DISCARD_WZ_MAX_PAYLOAD { + return Err(ExecuteError::BadRequest(Error::TooManySegments( + data_len.div_ceil(DISCARD_WZ_SEG_SIZE), + ))); + } + + let mut discard_sector = [0u8; 8]; + let mut discard_num_sectors = [0u8; 4]; + let mut discard_flags = [0u8; 4]; + + let sector_addr = data_addr.checked_add(DISCARD_WZ_SECTOR_OFFSET).unwrap(); + mem.read_slice(&mut discard_sector, sector_addr) + .map_err(ExecuteError::Read)?; + + let num_sectors_addr = data_addr + .checked_add(DISCARD_WZ_NUM_SECTORS_OFFSET) + .unwrap(); + mem.read_slice(&mut discard_num_sectors, num_sectors_addr) + .map_err(ExecuteError::Read)?; + + let flags_addr = data_addr.checked_add(DISCARD_WZ_FLAGS_OFFSET).unwrap(); + mem.read_slice(&mut discard_flags, flags_addr) + .map_err(ExecuteError::Read)?; + + let discard_flags = u32::from_le_bytes(discard_flags); + // Per virtio spec v1.2 reject discard if any flag is set, including unmap. + if discard_flags != 0 { + warn!("Unsupported flags {discard_flags:#x} in discard request"); + return Err(ExecuteError::UnsupportedFlags { + request_type: VIRTIO_BLK_T_DISCARD, + flags: discard_flags, + }); + } + + let discard_sector = u64::from_le_bytes(discard_sector); + + if discard_sector == 0 && disable_sector0_writes { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + + let discard_num_sectors = u32::from_le_bytes(discard_num_sectors); + + let top = discard_sector + .checked_add(discard_num_sectors as u64) + .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; + if top > disk_nsectors { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + + let discard_offset = discard_sector * SECTOR_SIZE; + let discard_length = (discard_num_sectors as u64) * SECTOR_SIZE; + + disk_image + .punch_hole(discard_offset, discard_length, user_data) + .map_err(ExecuteError::AsyncPunchHole)?; + } + RequestType::WriteZeroes => { + let (data_addr, data_len) = if self.data_descriptors.len() == 1 { + (self.data_descriptors[0].0, self.data_descriptors[0].1) + } else { + return Err(ExecuteError::BadRequest(Error::TooManyDescriptors)); + }; + + if data_len < DISCARD_WZ_SEG_SIZE { + return Err(ExecuteError::BadRequest(Error::DescriptorLengthTooSmall)); + } + if data_len > DISCARD_WZ_MAX_PAYLOAD { + return Err(ExecuteError::BadRequest(Error::TooManySegments( + data_len.div_ceil(DISCARD_WZ_SEG_SIZE), + ))); + } + + let mut wz_sector = [0u8; 8]; + let mut wz_num_sectors = [0u8; 4]; + let mut wz_flags = [0u8; 4]; + + let sector_addr = data_addr.checked_add(DISCARD_WZ_SECTOR_OFFSET).unwrap(); + mem.read_slice(&mut wz_sector, sector_addr) + .map_err(ExecuteError::Read)?; + + let num_sectors_addr = data_addr + .checked_add(DISCARD_WZ_NUM_SECTORS_OFFSET) + .unwrap(); + mem.read_slice(&mut wz_num_sectors, num_sectors_addr) + .map_err(ExecuteError::Read)?; + + let flags_addr = data_addr.checked_add(DISCARD_WZ_FLAGS_OFFSET).unwrap(); + mem.read_slice(&mut wz_flags, flags_addr) + .map_err(ExecuteError::Read)?; + + let wz_sector = u64::from_le_bytes(wz_sector); + let wz_num_sectors = u32::from_le_bytes(wz_num_sectors); + + let wz_flags = u32::from_le_bytes(wz_flags); + // Per virtio spec v1.2 reject write zeroes if any unknown flag is set. + if (wz_flags & !VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP) != 0 { + warn!("Unsupported flags {wz_flags:#x} in write zeroes request"); + return Err(ExecuteError::UnsupportedFlags { + request_type: VIRTIO_BLK_T_WRITE_ZEROES, + flags: wz_flags, + }); + } + + let wz_offset = wz_sector * SECTOR_SIZE; + if wz_offset == 0 && disable_sector0_writes { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + + let top = wz_sector + .checked_add(wz_num_sectors as u64) + .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; + if top > disk_nsectors { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + + let wz_length = (wz_num_sectors as u64) * SECTOR_SIZE; + + if wz_flags & VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP != 0 { + disk_image + .punch_hole(wz_offset, wz_length, user_data) + .map_err(ExecuteError::AsyncPunchHole)?; + } else { + disk_image + .write_zeroes(wz_offset, wz_length, user_data) + .map_err(ExecuteError::AsyncWriteZeroes)?; + } + } + RequestType::Unsupported(t) => return Err(ExecuteError::Unsupported(t)), + } + + Ok(ret) + } + + pub fn complete_async( + &mut self, + mem: &vm_memory::GuestMemoryMmap, + ) -> Result<(), Error> { + for aligned_op in self.aligned_operations.drain(..) { + // We need to perform the copy after the data has been read inside + // the aligned buffer in case we're reading data in. + if self.request_type == RequestType::In { + mem.write_slice(aligned_op.as_bytes(), aligned_op.data_addr()) + .map_err(Error::GuestMemory)?; + } + } + + Ok(()) + } + + #[inline] + pub fn data_descriptors( + &self, + ) -> &SmallVec<[(GuestAddress, u32); DEFAULT_DESCRIPTOR_VEC_SIZE]> { + &self.data_descriptors + } + + #[inline] + pub fn status_addr(&self) -> GuestAddress { + self.status_addr + } + + #[inline] + pub fn start(&self) -> Instant { + self.start + } + + #[inline] + pub fn sector(&self) -> u64 { + self.sector + } + + #[inline] + pub fn request_type(&self) -> RequestType { + self.request_type + } + + /// For In and Out requests, checks that the descriptors collectively fit in a backing disk of + /// the given size. Returns `Ok(())` if they fit, or `ExecuteError::BadRequest` otherwise. + fn check_data_bounds(&self, disk_nsectors: u64) -> Result<(), ExecuteError> { + if !matches!(self.request_type, RequestType::In | RequestType::Out) { + return Ok(()); + } + let mut total_bytes: u64 = 0; + for (_, data_len) in &self.data_descriptors { + total_bytes = total_bytes + .checked_add(u64::from(*data_len)) + .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; + } + if total_bytes == 0 { + return Ok(()); + } + let total_sectors = total_bytes.div_ceil(SECTOR_SIZE); + let end_sector = self + .sector + .checked_add(total_sectors) + .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; + if end_sector > disk_nsectors { + return Err(ExecuteError::BadRequest(Error::InvalidOffset)); + } + Ok(()) + } +} diff --git a/block/src/vhd.rs b/block/src/vhd.rs index 2cc65ca0b9..5a8e59de39 100644 --- a/block/src/vhd.rs +++ b/block/src/vhd.rs @@ -5,7 +5,7 @@ use std::fs::File; use std::io::{Seek, SeekFrom}; -use crate::{read_aligned_block_size, DiskTopology}; +use crate::{DiskTopology, read_aligned_block_size}; #[derive(Clone, Copy)] pub struct VhdFooter { @@ -117,13 +117,13 @@ pub fn is_fixed_vhd(f: &mut File) -> std::io::Result { } #[cfg(test)] -mod tests { +mod unit_tests { use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use vmm_sys_util::tempfile::TempFile; - use super::{is_fixed_vhd, VhdFooter}; + use super::{VhdFooter, is_fixed_vhd}; fn valid_fixed_vhd_footer() -> Vec { vec![ diff --git a/block/src/vhdx/mod.rs b/block/src/vhdx/mod.rs index 45974c5a3f..f8d404fc5b 100644 --- a/block/src/vhdx/mod.rs +++ b/block/src/vhdx/mod.rs @@ -12,11 +12,11 @@ use remain::sorted; use thiserror::Error; use uuid::Uuid; +use crate::BlockBackend; use crate::vhdx::vhdx_bat::{BatEntry, VhdxBatError}; use crate::vhdx::vhdx_header::{RegionInfo, RegionTableEntry, VhdxHeader, VhdxHeaderError}; use crate::vhdx::vhdx_io::VhdxIoError; use crate::vhdx::vhdx_metadata::{DiskSpec, VhdxMetadataError}; -use crate::BlockBackend; mod vhdx_bat; mod vhdx_header; @@ -187,11 +187,11 @@ impl Seek for Vhdx { } }; - if let Some(o) = new_offset { - if o <= self.virtual_disk_size() { - self.current_offset = o; - return Ok(o); - } + if let Some(o) = new_offset + && o <= self.virtual_disk_size() + { + self.current_offset = o; + return Ok(o); } Err(std::io::Error::new( @@ -202,9 +202,16 @@ impl Seek for Vhdx { } impl BlockBackend for Vhdx { - fn size(&self) -> std::result::Result { + fn logical_size(&self) -> std::result::Result { Ok(self.virtual_disk_size()) } + + fn physical_size(&self) -> std::result::Result { + self.file + .metadata() + .map(|m| m.len()) + .map_err(crate::Error::GetFileMetadata) + } } impl Clone for Vhdx { diff --git a/block/src/vhdx/vhdx_header.rs b/block/src/vhdx/vhdx_header.rs index bed5418d83..5c8c3e37d8 100644 --- a/block/src/vhdx/vhdx_header.rs +++ b/block/src/vhdx/vhdx_header.rs @@ -2,8 +2,6 @@ // // SPDX-License-Identifier: Apache-2.0 -extern crate log; - use std::collections::btree_map::BTreeMap; use std::fs::File; use std::io::{self, Read, Seek, SeekFrom, Write}; @@ -137,7 +135,7 @@ impl Header { .map_err(VhdxHeaderError::ReadHeader)?; // SAFETY: buffer is of correct size and has been successfully filled. - let header = unsafe { *(buffer.as_ptr() as *mut Header) }; + let header: Header = unsafe { *(buffer.as_ptr().cast()) }; if header.signature != HEADER_SIGN { return Err(VhdxHeaderError::InvalidHeaderSign); } @@ -153,9 +151,8 @@ impl Header { /// Converts the header structure into a buffer fn write_to_buffer(&self, buffer: &mut [u8; HEADER_SIZE as usize]) { // SAFETY: self is a valid header. - let reference = unsafe { - std::slice::from_raw_parts(self as *const Header as *const u8, HEADER_SIZE as usize) - }; + let reference = + unsafe { std::slice::from_raw_parts((&raw const *self).cast(), HEADER_SIZE as usize) }; *buffer = reference.try_into().unwrap(); } @@ -224,7 +221,7 @@ impl RegionTableHeader { .map_err(VhdxHeaderError::ReadRegionTableHeader)?; // SAFETY: buffer is of correct size and has been successfully filled. - let region_table_header = unsafe { *(buffer.as_ptr() as *mut RegionTableHeader) }; + let region_table_header: RegionTableHeader = unsafe { *(buffer.as_ptr().cast()) }; if region_table_header.signature != REGION_SIGN { return Err(VhdxHeaderError::InvalidRegionSign); } @@ -342,7 +339,7 @@ impl RegionTableEntry { pub fn new(buffer: &[u8]) -> Result { assert!(buffer.len() == std::mem::size_of::()); // SAFETY: the assertion above makes sure the buffer size is correct. - let mut region_table_entry = unsafe { *(buffer.as_ptr() as *mut RegionTableEntry) }; + let mut region_table_entry: RegionTableEntry = unsafe { *(buffer.as_ptr().cast()) }; let uuid = crate::vhdx::uuid_from_guid(buffer); region_table_entry.guid = uuid; diff --git a/block/src/vhdx/vhdx_io.rs b/block/src/vhdx/vhdx_io.rs index 30e3837876..96ce4ef4ce 100644 --- a/block/src/vhdx/vhdx_io.rs +++ b/block/src/vhdx/vhdx_io.rs @@ -35,9 +35,7 @@ pub enum VhdxIoError { pub type Result = std::result::Result; macro_rules! align { - ($n:expr, $align:expr) => {{ - $n.div_ceil($align) * $align - }}; + ($n:expr, $align:expr) => {{ $n.div_ceil($align) * $align }}; } #[derive(Default)] @@ -130,7 +128,7 @@ pub fn read( _ => { return Err(VhdxIoError::InvalidBatEntryState); } - }; + } sector_count -= sector.free_sectors; sector_index += sector.free_sectors; read_count += sector.free_bytes as usize; @@ -212,7 +210,7 @@ pub fn write( _ => { return Err(VhdxIoError::InvalidBatEntryState); } - }; + } sector_count -= sector.free_sectors; sector_index += sector.free_sectors; write_count += sector.free_bytes as usize; diff --git a/block/src/vhdx/vhdx_metadata.rs b/block/src/vhdx/vhdx_metadata.rs index 47cc2ff68d..0410d9af93 100644 --- a/block/src/vhdx/vhdx_metadata.rs +++ b/block/src/vhdx/vhdx_metadata.rs @@ -280,7 +280,7 @@ impl MetadataTableHeader { pub fn new(buffer: &[u8]) -> Result { assert!(buffer.len() == std::mem::size_of::()); // SAFETY: the assertion above makes sure the buffer size is correct. - let metadata_table_header = unsafe { *(buffer.as_ptr() as *mut MetadataTableHeader) }; + let metadata_table_header: MetadataTableHeader = unsafe { *(buffer.as_ptr().cast()) }; if metadata_table_header.signature != METADATA_SIGN { return Err(VhdxMetadataError::InvalidMetadataSign); @@ -313,7 +313,7 @@ impl MetadataTableEntry { fn new(buffer: &[u8]) -> Result { assert!(buffer.len() == std::mem::size_of::()); // SAFETY: the assertion above makes sure the buffer size is correct. - let mut metadata_table_entry = unsafe { *(buffer.as_ptr() as *mut MetadataTableEntry) }; + let mut metadata_table_entry: MetadataTableEntry = unsafe { *(buffer.as_ptr().cast()) }; let uuid = crate::vhdx::uuid_from_guid(buffer); metadata_table_entry.item_id = uuid; diff --git a/block/src/vhdx_sync.rs b/block/src/vhdx_sync.rs index d832f5e3cc..da9b3e1fb8 100644 --- a/block/src/vhdx_sync.rs +++ b/block/src/vhdx_sync.rs @@ -5,43 +5,98 @@ use std::collections::VecDeque; use std::fs::File; use std::os::fd::AsRawFd; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex}; use vmm_sys_util::eventfd::EventFd; -use crate::async_io::{ - AsyncIo, AsyncIoResult, BorrowedDiskFd, DiskFile, DiskFileError, DiskFileResult, -}; -use crate::vhdx::{Result as VhdxResult, Vhdx}; -use crate::AsyncAdaptor; +use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFileError}; +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::vhdx::{Vhdx, VhdxError}; +use crate::{AsyncAdaptor, BlockBackend, Error, disk_file}; +#[derive(Debug)] pub struct VhdxDiskSync { + // FIXME: The Mutex serializes all VHDX I/O operations across queues, which + // is necessary for correctness but eliminates any parallelism benefit from + // multiqueue. Vhdx::clone() shares the underlying file description across + // threads, so concurrent I/O from multiple queues races on the file offset + // causing data corruption. + // + // A proper fix would require restructuring the VHDX I/O path so that data + // operations can proceed in parallel with independent file descriptors. vhdx_file: Arc>, } impl VhdxDiskSync { - pub fn new(f: File) -> VhdxResult { + pub fn new(f: File) -> BlockResult { Ok(VhdxDiskSync { - vhdx_file: Arc::new(Mutex::new(Vhdx::new(f)?)), + vhdx_file: Arc::new(Mutex::new(Vhdx::new(f).map_err(|e| { + let kind = match &e { + VhdxError::NotVhdx(_) + | VhdxError::ParseVhdxHeader(_) + | VhdxError::ParseVhdxMetadata(_) + | VhdxError::ParseVhdxRegionEntry(_) => BlockErrorKind::InvalidFormat, + VhdxError::ReadBatEntry(_) => BlockErrorKind::CorruptImage, + VhdxError::ReadFailed(_) | VhdxError::WriteFailed(_) => BlockErrorKind::Io, + }; + BlockError::new(kind, e).with_op(ErrorOp::Open) + })?)), }) } } -impl DiskFile for VhdxDiskSync { - fn size(&mut self) -> DiskFileResult { +impl disk_file::DiskSize for VhdxDiskSync { + fn logical_size(&self) -> BlockResult { Ok(self.vhdx_file.lock().unwrap().virtual_disk_size()) } +} - fn new_async_io(&self, _ring_depth: u32) -> DiskFileResult> { - Ok( - Box::new(VhdxSync::new(self.vhdx_file.clone()).map_err(DiskFileError::NewAsyncIo)?) - as Box, +impl disk_file::PhysicalSize for VhdxDiskSync { + fn physical_size(&self) -> BlockResult { + self.vhdx_file + .lock() + .unwrap() + .physical_size() + .map_err(|e| match e { + Error::GetFileMetadata(io) => { + BlockError::new(BlockErrorKind::Io, Error::GetFileMetadata(io)) + } + _ => unreachable!("unexpected error from Vhdx::physical_size(): {e}"), + }) + } +} + +impl disk_file::DiskFd for VhdxDiskSync { + fn fd(&self) -> BorrowedDiskFd<'_> { + BorrowedDiskFd::new(self.vhdx_file.lock().unwrap().as_raw_fd()) + } +} + +impl disk_file::Geometry for VhdxDiskSync {} + +impl disk_file::SparseCapable for VhdxDiskSync {} + +impl disk_file::Resizable for VhdxDiskSync { + fn resize(&mut self, _size: u64) -> BlockResult<()> { + Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + DiskFileError::ResizeError(std::io::Error::other("resize not supported for VHDX")), ) + .with_op(ErrorOp::Resize)) + } +} + +impl disk_file::DiskFile for VhdxDiskSync {} + +impl disk_file::AsyncDiskFile for VhdxDiskSync { + fn try_clone(&self) -> BlockResult> { + Ok(Box::new(VhdxDiskSync { + vhdx_file: Arc::clone(&self.vhdx_file), + })) } - fn fd(&mut self) -> BorrowedDiskFd<'_> { - let lock = self.vhdx_file.lock().unwrap(); - BorrowedDiskFd::new(lock.as_raw_fd()) + fn create_async_io(&self, _ring_depth: u32) -> BlockResult> { + Ok(Box::new(VhdxSync::new(Arc::clone(&self.vhdx_file)))) } } @@ -52,20 +107,17 @@ pub struct VhdxSync { } impl VhdxSync { - pub fn new(vhdx_file: Arc>) -> std::io::Result { - Ok(VhdxSync { + pub fn new(vhdx_file: Arc>) -> Self { + VhdxSync { vhdx_file, - eventfd: EventFd::new(libc::EFD_NONBLOCK)?, + eventfd: EventFd::new(libc::EFD_NONBLOCK) + .expect("Failed creating EventFd for VhdxSync"), completion_list: VecDeque::new(), - }) + } } } -impl AsyncAdaptor for Arc> { - fn file(&mut self) -> MutexGuard<'_, Vhdx> { - self.lock().unwrap() - } -} +impl AsyncAdaptor for Vhdx {} impl AsyncIo for VhdxSync { fn notifier(&self) -> &EventFd { @@ -78,7 +130,7 @@ impl AsyncIo for VhdxSync { iovecs: &[libc::iovec], user_data: u64, ) -> AsyncIoResult<()> { - self.vhdx_file.read_vectored_sync( + self.vhdx_file.lock().unwrap().read_vectored_sync( offset, iovecs, user_data, @@ -93,7 +145,7 @@ impl AsyncIo for VhdxSync { iovecs: &[libc::iovec], user_data: u64, ) -> AsyncIoResult<()> { - self.vhdx_file.write_vectored_sync( + self.vhdx_file.lock().unwrap().write_vectored_sync( offset, iovecs, user_data, @@ -103,11 +155,26 @@ impl AsyncIo for VhdxSync { } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { - self.vhdx_file - .fsync_sync(user_data, &self.eventfd, &mut self.completion_list) + self.vhdx_file.lock().unwrap().fsync_sync( + user_data, + &self.eventfd, + &mut self.completion_list, + ) } fn next_completed_request(&mut self) -> Option<(u64, i32)> { self.completion_list.pop_front() } + + fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::PunchHole(std::io::Error::other( + "punch_hole not supported for VHDX", + ))) + } + + fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::WriteZeroes(std::io::Error::other( + "write_zeroes not supported for VHDX", + ))) + } } diff --git a/chv.nix b/chv.nix new file mode 100644 index 0000000000..83b331ed8b --- /dev/null +++ b/chv.nix @@ -0,0 +1,69 @@ +# Builds Cloud Hypervisor with using crane. +# +# Uses a pragmatic release profile with debug-ability and faster +# compilation times in mind without sacrificing too much performance. + +{ + # helper from nixpkgs + lib, + openssl, + pkg-config, + # other helper + craneLib, + # other + meta, # meta of pkgs.cloud-hypervisor + src, # clean source + chExtraVersion, # Additional information to be appended to the version string. +}: +let + commonArgs = { + inherit meta src; + # Since Nov 2025 (v50), Cloud Hypervisor has a virtual manifest and the + # main package was moved into a sub directory. + cargoToml = "${src}/cloud-hypervisor/Cargo.toml"; + + # Pragmatic release profile with debug-ability and faster + # compilation times in mind. + env = { + CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS = "true"; + CARGO_PROFILE_RELEASE_OPT_LEVEL = 2; + CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS = "true"; + CARGO_PROFILE_RELEASE_LTO = "thin"; + + # Fix build. Reference: + # - https://github.com/sfackler/rust-openssl/issues/1430 + # - https://docs.rs/openssl/latest/openssl/ + OPENSSL_NO_VENDOR = true; + + # Sets additional information to be appended to the version string. + CH_EXTRA_VERSION = chExtraVersion; + }; + + nativeBuildInputs = [ + pkg-config + ]; + buildInputs = [ + openssl + ]; + }; + + # Downloaded and compiled dependencies. + cargoArtifacts = craneLib.buildDepsOnly ( + commonArgs + // { + doCheck = false; + } + ); + + cargoPackageKvm = craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + # Don't execute tests here. Too expensive for local development with + # frequent rebuilds + little benefit. + doCheck = false; + cargoExtraArgs = "--features kvm"; + } + ); +in +cargoPackageKvm diff --git a/ci/README.auto.approve.md b/ci/README.auto.approve.md new file mode 100644 index 0000000000..a38d3e6e7e --- /dev/null +++ b/ci/README.auto.approve.md @@ -0,0 +1,43 @@ +# Flake bump auto approve + +## Description + +We add a github workflow `Flake bump`. +First job of this workflow checks if a merge request contains only one commit which updates the `flake.lock` file. +If this condition is met the second job approve this merge request and automatically merge it. +The approval is done with a dedicated GitHubApp. + +## Install + +* Follow this guide: https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app +* Create a GitHub app `auto-approve-app` in your GH organization + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> New GitHub App + * Add a name and Homepage URL + * Add Repository Permissions + * Actions: RO + * Contents: RW + * Metadata: RO + * Pull Requests: RW + * Workflows: RW + +* Install this app into your organization + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` -> Install App + * Only select repositories: + * repository-name + +* Find app_id + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` + * you find the app_id in the `General` section + +* Create app client secret + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` -> Client secrets + * The private key will be downloaded using your browser + * Save it in 1Password or vault + +* Create two organization secrets: + * GH_AUTO_APPROVE_APP_ID + * GH_AUTO_APPROVE_APP_PRIVATE_KEY + +* Add Github App `auto-approve-app` to your branch ruleset. + * github.com/github-organization/repository -> Settings -> Rules -> Rulesets -> rule name -> Bypass list -> Add bypass + * This allows the Github App `auto-approve-app` to merge the MRs even if other conditions of the ruleset are not met. diff --git a/ci/gitlint/rules_auto_approve/only-flake.py b/ci/gitlint/rules_auto_approve/only-flake.py new file mode 100644 index 0000000000..567323869f --- /dev/null +++ b/ci/gitlint/rules_auto_approve/only-flake.py @@ -0,0 +1,61 @@ +# Copyright © 2026 Cyberus Technology GmbH +# +# SPDX-License-Identifier: Apache-2.0 +# +from gitlint.options import ListOption, StrOption +from gitlint.rules import CommitRule, RuleViolation + +class SingleSpecificFile(CommitRule): + """Reject commits which modifies files other than those specified""" + id = "UC-flake" + name = "body-require-single-specific-file" + description = "Commit must change exactly one specific file" + target = None # Applies to entire commit + options_spec = [ + StrOption( + "filepath", + "flake.lock", + "The file path to check" + ) + ] + + def validate(self, commit): + changed_files = getattr(commit, "changed_files", None) + if changed_files is None: + # Newer gitlint commit objects expose the touched paths directly via + # `changed_files`. Older variants may only expose + # `changed_files_stats`, a mapping keyed by changed path, so we fall + # back to its keys when `changed_files` is unavailable. + changed_files_stats = getattr(commit, "changed_files_stats", {}) + changed_files = list(changed_files_stats.keys()) + + if len(changed_files) != 1: + return [RuleViolation("commit-changes-multiple-files-or-none", f"Commit changes {len(changed_files)} files, expected exactly 1: {', '.join(changed_files)}")] + + filepath = self.options["filepath"].value + if changed_files[0] != filepath: + return [RuleViolation("commit-wrong-file", f"Commit changes '{changed_files[0]}', expected only '{filepath}'")] + +#################### +# Usage of this rule +#################### +# +# .gitlint_auto_approve file +# [general] +# extra-path=ci/gitlint/rules_auto_approve +# regex-style-search=true +# ignore=body-is-missing,body-max-line-length + +# # default 72 +# [title-max-length] +# line-length=72 + +# # Empty bodies are fine +# [body-min-length] +# min-length=0 + +# [UC-flake] +# filepath=flake.lock + +## run with +# nix run nixpkgs#gitlint -- --commits origin/main.. -C .gitlint_auto_approve diff --git a/cloud-hypervisor/Cargo.toml b/cloud-hypervisor/Cargo.toml new file mode 100644 index 0000000000..be61df65f6 --- /dev/null +++ b/cloud-hypervisor/Cargo.toml @@ -0,0 +1,65 @@ +[package] +authors = ["The Cloud Hypervisor Authors"] +build = "build.rs" +default-run = "cloud-hypervisor" +description = "Open source Virtual Machine Monitor (VMM) that runs on top of KVM & MSHV" +edition = "2024" +homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor" +license = "Apache-2.0 AND BSD-3-Clause" +name = "cloud-hypervisor" +rust-version.workspace = true +version = "52.0.0" + +[dependencies] +anyhow = { workspace = true } +api_client = { path = "../api_client" } +clap = { workspace = true, features = ["string"] } +dhat = { workspace = true, optional = true } +env_logger = { workspace = true } +epoll = { workspace = true } +event_monitor = { path = "../event_monitor" } +hypervisor = { path = "../hypervisor" } +jiff = { workspace = true } +libc = { workspace = true } +log = { workspace = true, features = ["std"] } +option_parser = { path = "../option_parser" } +seccompiler = { workspace = true } +serde_json = { workspace = true } +signal-hook = { workspace = true } +thiserror = { workspace = true } +tpm = { path = "../tpm" } +tracer = { path = "../tracer" } +vm-memory = { workspace = true } +vm-migration = { path = "../vm-migration" } +vmm = { path = "../vmm" } +vmm-sys-util = { workspace = true } +zbus = { version = "5.15.0", optional = true } + +[dev-dependencies] +block = { path = "../block" } +dirs = { workspace = true } +net_util = { path = "../net_util" } +serde_json = { workspace = true } +test_infra = { path = "../test_infra" } +wait-timeout = { workspace = true } + +# Please adjust `vmm::feature_list()` accordingly when changing the +# feature list below +[features] +dbus_api = ["vmm/dbus_api", "zbus"] +default = ["io_uring", "kvm"] +dhat-heap = ["dhat", "vmm/dhat-heap"] # For heap profiling +fw_cfg = ["vmm/fw_cfg"] +guest_debug = ["vmm/guest_debug"] +igvm = ["mshv", "vmm/igvm"] +io_uring = ["vmm/io_uring"] +ivshmem = ["vmm/ivshmem"] +kvm = ["vmm/kvm"] +mshv = ["vmm/mshv"] +pvmemcontrol = ["vmm/pvmemcontrol"] +sev_snp = ["igvm", "mshv", "vmm/sev_snp"] +tdx = ["vmm/tdx"] +tracing = ["tracer/tracing", "vmm/tracing"] + +[lints] +workspace = true diff --git a/build.rs b/cloud-hypervisor/build.rs similarity index 75% rename from build.rs rename to cloud-hypervisor/build.rs index 37a5ffd9fa..080c625599 100644 --- a/build.rs +++ b/cloud-hypervisor/build.rs @@ -9,14 +9,13 @@ use std::process::Command; fn main() { let mut version = "v".to_owned() + env!("CARGO_PKG_VERSION"); - if let Ok(git_out) = Command::new("git").args(["describe", "--dirty"]).output() { - if git_out.status.success() { - if let Ok(git_out_str) = String::from_utf8(git_out.stdout) { - version = git_out_str; - // Pop the trailing newline. - version.pop(); - } - } + if let Ok(git_out) = Command::new("git").args(["describe", "--dirty"]).output() + && git_out.status.success() + && let Ok(git_out_str) = String::from_utf8(git_out.stdout) + { + version = git_out_str; + // Pop the trailing newline. + version.pop(); } // Append CH_EXTRA_VERSION to version if it is set. diff --git a/src/bin/ch-remote.rs b/cloud-hypervisor/src/bin/ch-remote.rs similarity index 81% rename from src/bin/ch-remote.rs rename to cloud-hypervisor/src/bin/ch-remote.rs index 9d8c4f68b4..dd2eefb79e 100644 --- a/src/bin/ch-remote.rs +++ b/cloud-hypervisor/src/bin/ch-remote.rs @@ -11,18 +11,24 @@ use std::io::Read; use std::marker::PhantomData; use std::os::unix::net::UnixStream; use std::process; +use std::thread::sleep; +use std::time::Duration; use api_client::{ - simple_api_command, simple_api_command_with_fds, simple_api_full_command, - Error as ApiClientError, + Error as ApiClientError, StatusCode, simple_api_command, simple_api_command_with_fds, + simple_api_full_command, simple_api_full_command_and_response, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; +#[cfg(feature = "dbus_api")] +use clap::ArgAction; +use clap::{Arg, ArgMatches, Command}; +use log::{error, info}; use option_parser::{ByteSized, ByteSizedParseError}; use thiserror::Error; +use vm_migration::progress::{MigrationProgress, MigrationState}; use vmm::config::RestoreConfig; use vmm::vm_config::{ - DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig, - VsockConfig, + DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig, + UserDeviceConfig, VdpaConfig, VsockConfig, }; #[cfg(feature = "dbus_api")] use zbus::{proxy, zvariant::Optional}; @@ -48,6 +54,8 @@ enum Error { AddDiskConfig(#[source] vmm::config::Error), #[error("Error parsing filesystem syntax")] AddFsConfig(#[source] vmm::config::Error), + #[error("Error parsing generic vhost-user syntax")] + AddGenericVhostUserConfig(#[source] vmm::config::Error), #[error("Error parsing persistent memory syntax")] AddPmemConfig(#[source] vmm::config::Error), #[error("Error parsing network syntax")] @@ -64,6 +72,12 @@ enum Error { ReadingStdin(#[source] std::io::Error), #[error("Error reading from file")] ReadingFile(#[source] std::io::Error), + #[error("Invalid disk size")] + InvalidDiskSize(#[source] ByteSizedParseError), + #[error("Error parsing receive migration configuration")] + ReceiveMigrationConfig(#[from] vmm::api::VmReceiveMigrationConfigError), + #[error("Error parsing send migration configuration")] + SendMigrationConfig(#[from] vmm::api::VmSendMigrationConfigError), } enum TargetApi<'a> { @@ -80,6 +94,10 @@ trait DBusApi1 { fn vm_add_device(&self, device_config: &str) -> zbus::Result>; fn vm_add_disk(&self, disk_config: &str) -> zbus::Result>; fn vm_add_fs(&self, fs_config: &str) -> zbus::Result>; + fn vm_add_generic_vhost_user( + &self, + generic_vhost_user_config: &str, + ) -> zbus::Result>; fn vm_add_net(&self, net_config: &str) -> zbus::Result>; fn vm_add_pmem(&self, pmem_config: &str) -> zbus::Result>; fn vm_add_user_device(&self, vm_add_user_device: &str) -> zbus::Result>; @@ -92,6 +110,7 @@ trait DBusApi1 { fn vm_delete(&self) -> zbus::Result<()>; fn vm_info(&self) -> zbus::Result; fn vm_pause(&self) -> zbus::Result<()>; + fn vm_post_migration_announce(&self) -> zbus::Result<()>; fn vm_power_button(&self) -> zbus::Result<()>; fn vm_reboot(&self) -> zbus::Result<()>; fn vm_remove_device(&self, vm_remove_device: &str) -> zbus::Result<()>; @@ -152,6 +171,10 @@ impl<'a> DBusApi1ProxyBlocking<'a> { self.print_response(self.vm_add_fs(fs_config)) } + fn api_vm_add_generic_vhost_user(&self, generic_vhost_user_config: &str) -> ApiResult { + self.print_response(self.vm_add_generic_vhost_user(generic_vhost_user_config)) + } + fn api_vm_add_net(&self, net_config: &str) -> ApiResult { self.print_response(self.vm_add_net(net_config)) } @@ -203,6 +226,11 @@ impl<'a> DBusApi1ProxyBlocking<'a> { self.vm_pause().map_err(Error::DBusApiClient) } + fn api_vm_post_migration_announce(&self) -> ApiResult { + self.vm_post_migration_announce() + .map_err(Error::DBusApiClient) + } + fn api_vm_power_button(&self) -> ApiResult { self.vm_power_button().map_err(Error::DBusApiClient) } @@ -277,6 +305,10 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu Some("resume") => { simple_api_command(socket, "PUT", "resume", None).map_err(Error::HttpApiClient) } + Some("post-migration-announce") => { + simple_api_command(socket, "PUT", "post-migration-announce", None) + .map_err(Error::HttpApiClient) + } Some("power-button") => { simple_api_command(socket, "PUT", "power-button", None).map_err(Error::HttpApiClient) } @@ -298,6 +330,8 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu Some("shutdown") => { simple_api_command(socket, "PUT", "shutdown", None).map_err(Error::HttpApiClient) } + Some("migration-progress") => simple_api_command(socket, "GET", "migration-progress", None) + .map_err(Error::HttpApiClient), Some("nmi") => simple_api_command(socket, "PUT", "nmi", None).map_err(Error::HttpApiClient), Some("resize") => { let resize = resize_config( @@ -319,6 +353,22 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu )?; simple_api_command(socket, "PUT", "resize", Some(&resize)).map_err(Error::HttpApiClient) } + Some("resize-disk") => { + let resize_disk = resize_disk_config( + matches + .subcommand_matches("resize-disk") + .unwrap() + .get_one::("disk") + .unwrap(), + matches + .subcommand_matches("resize-disk") + .unwrap() + .get_one::("size") + .unwrap(), + )?; + simple_api_command(socket, "PUT", "resize-disk", Some(&resize_disk)) + .map_err(Error::HttpApiClient) + } Some("resize-zone") => { let resize_zone = resize_zone_config( matches @@ -379,6 +429,22 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu simple_api_command(socket, "PUT", "add-fs", Some(&fs_config)) .map_err(Error::HttpApiClient) } + Some("add-generic-vhost-user") => { + let device_config = add_generic_vhost_user_config( + matches + .subcommand_matches("add-generic-vhost-user") + .unwrap() + .get_one::("generic_vhost_user_config") + .unwrap(), + )?; + simple_api_command( + socket, + "PUT", + "add-generic-vhost-user", + Some(&device_config), + ) + .map_err(Error::HttpApiClient) + } Some("add-pmem") => { let pmem_config = add_pmem_config( matches @@ -398,7 +464,7 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .get_one::("net_config") .unwrap(), )?; - simple_api_command_with_fds(socket, "PUT", "add-net", Some(&net_config), fds) + simple_api_command_with_fds(socket, "PUT", "add-net", Some(&net_config), &fds) .map_err(Error::HttpApiClient) } Some("add-user-device") => { @@ -453,7 +519,7 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .get_one::("restore_config") .unwrap(), )?; - simple_api_command_with_fds(socket, "PUT", "restore", Some(&restore_config), fds) + simple_api_command_with_fds(socket, "PUT", "restore", Some(&restore_config), &fds) .map_err(Error::HttpApiClient) } Some("coredump") => { @@ -468,19 +534,81 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .map_err(Error::HttpApiClient) } Some("send-migration") => { + let just_dispatch = matches + .subcommand_matches("send-migration") + .unwrap() + .get_one::("dispatch") + .cloned() + .unwrap_or(false); + let wait_for_migration = !just_dispatch; + let send_migration_data = send_migration_data( matches .subcommand_matches("send-migration") .unwrap() .get_one::("send_migration_config") .unwrap(), - matches - .subcommand_matches("send-migration") - .unwrap() - .get_flag("send_migration_local"), - ); + )?; simple_api_command(socket, "PUT", "send-migration", Some(&send_migration_data)) - .map_err(Error::HttpApiClient) + .map_err(Error::HttpApiClient)?; + + if !wait_for_migration { + return Ok(()); + } + loop { + let response = simple_api_full_command_and_response( + socket, + "GET", + "vm.migration-progress", + None, + ) + .map_err(Error::HttpApiClient)? + // should have response + .ok_or(Error::HttpApiClient(ApiClientError::ServerResponse( + StatusCode::Ok, + None, + )))?; + + // This is guaranteed by the SendMigration call + assert_ne!( + response, "null", + "migration progress should be there immediately when the migration was dispatched" + ); + + let progress = serde_json::from_slice::(response.as_bytes()) + .map_err(|e| { + error!("failed to parse response as MigrationProgress: {e}"); + Error::HttpApiClient(ApiClientError::ServerResponse( + StatusCode::Ok, + Some(response), + )) + })?; + + match progress.state { + MigrationState::Cancelled { .. } => { + info!("Migration was cancelled"); + break; + } + MigrationState::Failed { + error_msg, + error_msg_debug, + } => { + error!("Migration failed! {error_msg}\n{error_msg_debug}"); + break; + } + MigrationState::Finished { .. } => { + info!("Migration finished successfully. Shutting down Cloud Hypervisor"); + simple_api_full_command(socket, "PUT", "vmm.shutdown", None) + .map_err(Error::HttpApiClient)?; + break; + } + MigrationState::Ongoing { .. } => { + sleep(Duration::from_millis(50)); + continue; + } + } + } + Ok(()) } Some("receive-migration") => { let receive_migration_data = receive_migration_data( @@ -489,7 +617,7 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .unwrap() .get_one::("receive_migration_config") .unwrap(), - ); + )?; simple_api_command( socket, "PUT", @@ -508,6 +636,8 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu )?; simple_api_command(socket, "PUT", "create", Some(&data)).map_err(Error::HttpApiClient) } + Some("cancel-migration") => simple_api_command(socket, "PUT", "cancel-migration", None) + .map_err(Error::HttpApiClient), _ => unreachable!(), } } @@ -519,6 +649,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) Some("delete") => proxy.api_vm_delete(), Some("shutdown-vmm") => proxy.api_vmm_shutdown(), Some("resume") => proxy.api_vm_resume(), + Some("post-migration-announce") => proxy.api_vm_post_migration_announce(), Some("power-button") => proxy.api_vm_power_button(), Some("reboot") => proxy.api_vm_reboot(), Some("pause") => proxy.api_vm_pause(), @@ -601,6 +732,16 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) )?; proxy.api_vm_add_fs(&fs_config) } + Some("add-generic-vhost-user") => { + let generic_vhost_user_config = add_generic_vhost_user_config( + matches + .subcommand_matches("add-generic-vhost-user") + .unwrap() + .get_one::("generic_vhost_user_config") + .unwrap(), + )?; + proxy.api_vm_add_generic_vhost_user(&generic_vhost_user_config) + } Some("add-pmem") => { let pmem_config = add_pmem_config( matches @@ -688,11 +829,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) .unwrap() .get_one::("send_migration_config") .unwrap(), - matches - .subcommand_matches("send-migration") - .unwrap() - .get_flag("send_migration_local"), - ); + )?; proxy.api_vm_send_migration(&send_migration_data) } Some("receive-migration") => { @@ -702,7 +839,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) .unwrap() .get_one::("receive_migration_config") .unwrap(), - ); + )?; proxy.api_vm_receive_migration(&receive_migration_data) } Some("create") => { @@ -724,7 +861,7 @@ fn resize_config( memory: Option<&str>, balloon: Option<&str>, ) -> Result { - let desired_vcpus: Option = if let Some(cpus) = cpus { + let desired_vcpus: Option = if let Some(cpus) = cpus { Some(cpus.parse().map_err(Error::InvalidCpuCount)?) } else { None @@ -761,6 +898,15 @@ fn resize_config( Ok(serde_json::to_string(&resize).unwrap()) } +fn resize_disk_config(id: &str, size: &str) -> Result { + let resize_disk = vmm::api::VmResizeDiskData { + id: id.to_owned(), + desired_size: size.parse::().map_err(Error::InvalidDiskSize)?.0, + }; + + Ok(serde_json::to_string(&resize_disk).unwrap()) +} + fn resize_zone_config(id: &str, size: &str) -> Result { let resize_zone = vmm::api::VmResizeZoneData { id: id.to_owned(), @@ -807,6 +953,14 @@ fn add_fs_config(config: &str) -> Result { Ok(fs_config) } +fn add_generic_vhost_user_config(config: &str) -> Result { + let generic_vhost_user_config = + GenericVhostUserConfig::parse(config).map_err(Error::AddGenericVhostUserConfig)?; + let generic_vhost_user_config = serde_json::to_string(&generic_vhost_user_config).unwrap(); + + Ok(generic_vhost_user_config) +} + fn add_pmem_config(config: &str) -> Result { let pmem_config = PmemConfig::parse(config).map_err(Error::AddPmemConfig)?; let pmem_config = serde_json::to_string(&pmem_config).unwrap(); @@ -873,21 +1027,17 @@ fn coredump_config(destination_url: &str) -> String { serde_json::to_string(&coredump_config).unwrap() } -fn receive_migration_data(url: &str) -> String { - let receive_migration_data = vmm::api::VmReceiveMigrationData { - receiver_url: url.to_owned(), - }; - - serde_json::to_string(&receive_migration_data).unwrap() +fn receive_migration_data(config: &str) -> Result { + let receive_migration_data = + vmm::api::VmReceiveMigrationData::parse(config).map_err(Error::ReceiveMigrationConfig)?; + Ok(serde_json::to_string(&receive_migration_data).unwrap()) } -fn send_migration_data(url: &str, local: bool) -> String { - let send_migration_data = vmm::api::VmSendMigrationData { - destination_url: url.to_owned(), - local, - }; - - serde_json::to_string(&send_migration_data).unwrap() +fn send_migration_data(config: &str) -> Result { + let send_migration_data = + vmm::api::VmSendMigrationData::parse(config).map_err(Error::SendMigrationConfig)?; + let send_migration_config = serde_json::to_string(&send_migration_data).unwrap(); + Ok(send_migration_config) } fn create_data(path: &str) -> Result { @@ -953,6 +1103,13 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .index(1) .help(vmm::vm_config::FsConfig::SYNTAX), ), + Command::new("add-generic-vhost-user") + .about("Add generic vhost-user device") + .arg( + Arg::new("generic_vhost_user_config") + .index(1) + .help(vmm::vm_config::GenericVhostUserConfig::SYNTAX), + ), Command::new("add-net") .about("Add network device") .arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)), @@ -977,6 +1134,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .about("Add vsock device") .arg(Arg::new("vsock_config").index(1).help(VsockConfig::SYNTAX)), Command::new("boot").about("Boot a created VM"), + Command::new("cancel-migration").about("Cancel any ongoing migration"), Command::new("coredump") .about("Create a coredump from VM") .arg(Arg::new("coredump_config").index(1).help("")), @@ -986,9 +1144,11 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .arg(Arg::new("path").index(1).default_value("-")), Command::new("delete").about("Delete a VM"), Command::new("info").about("Info on the VM"), + Command::new("migration-progress"), Command::new("nmi").about("Trigger NMI"), Command::new("pause").about("Pause the VM"), Command::new("ping").about("Ping the VMM to check for API server availability"), + Command::new("post-migration-announce").about("Trigger post-migration announcements"), Command::new("power-button").about("Trigger a power button in the VM"), Command::new("reboot").about("Reboot the VM"), Command::new("receive-migration") @@ -996,7 +1156,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .arg( Arg::new("receive_migration_config") .index(1) - .help(""), + .help(vmm::api::VmReceiveMigrationData::SYNTAX), ), Command::new("remove-device") .about("Remove VFIO and PCI device") @@ -1021,6 +1181,20 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .help("New memory size in bytes (supports K/M/G suffix)") .num_args(1), ), + Command::new("resize-disk") + .about("Resize an attached disk") + .arg( + Arg::new("disk") + .long("disk") + .help("Disk identifier") + .num_args(1), + ) + .arg( + Arg::new("size") + .long("size") + .help("New disk size") + .num_args(1), + ), Command::new("resize-zone") .about("Resize a memory zone") .arg( @@ -1046,15 +1220,15 @@ fn get_cli_commands_sorted() -> Box<[Command]> { Command::new("send-migration") .about("Initiate a VM migration") .arg( - Arg::new("send_migration_config") - .index(1) - .help(""), + Arg::new("dispatch") + .long("dispatch") + .help("just dispatch the migration without waiting for it to finish") + .num_args(0), ) .arg( - Arg::new("send_migration_local") - .long("local") - .num_args(0) - .action(ArgAction::SetTrue), + Arg::new("send_migration_config") + .index(1) + .help(vmm::api::VmSendMigrationData::SYNTAX), ), Command::new("shutdown").about("Shutdown the VM"), Command::new("shutdown-vmm").about("Shutdown the VMM"), @@ -1071,6 +1245,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> { } fn main() { + env_logger::init(); let app = Command::new("ch-remote") .author(env!("CARGO_PKG_AUTHORS")) .version(env!("BUILD_VERSION")) @@ -1092,7 +1267,7 @@ fn main() { #[cfg(not(feature = "dbus_api"))] (Some(api_sock),) => TargetApi::HttpApi( UnixStream::connect(api_sock).unwrap_or_else(|e| { - eprintln!("Error opening HTTP socket: {e}"); + error!("Error opening HTTP socket: {e}"); process::exit(1) }), PhantomData, @@ -1100,7 +1275,7 @@ fn main() { #[cfg(feature = "dbus_api")] (Some(api_sock), None, None) => TargetApi::HttpApi( UnixStream::connect(api_sock).unwrap_or_else(|e| { - eprintln!("Error opening HTTP socket: {e}"); + error!("Error opening HTTP socket: {e}"); process::exit(1) }), PhantomData, @@ -1114,25 +1289,28 @@ fn main() { ) .map_err(Error::DBusApiClient) .unwrap_or_else(|e| { - eprintln!("Error creating D-Bus proxy: {e}"); + error!("Error creating D-Bus proxy: {e}"); process::exit(1) }), ), #[cfg(feature = "dbus_api")] (Some(_), Some(_) | None, Some(_) | None) => { - println!( + error!( "`api-socket` and (dbus-service-name or dbus-object-path) are mutually exclusive" ); process::exit(1); } _ => { - println!("Please either provide the api-socket option or dbus-service-name and dbus-object-path options"); + error!( + "Please either provide the api-socket option or dbus-service-name and dbus-object-path options" + ); process::exit(1); } }; if let Err(top_error) = target_api.do_command(&matches) { // Helper to join strings with a newline. + #[allow(clippy::needless_pass_by_value)] fn join_strs(mut acc: String, next: String) -> String { if !acc.is_empty() { acc.push('\n'); @@ -1157,7 +1335,7 @@ fn main() { if let Some(api_client::Error::ServerResponse(status_code, body)) = error.downcast_ref::() { - let body = body.as_ref().map(|body| body.as_str()).unwrap_or(""); + let body = body.as_ref().map_or("", |body| body.as_str()); // Retrieve the list of error messages back. let lines: Vec<&str> = match serde_json::from_str(body) { @@ -1197,11 +1375,11 @@ fn main() { server_api_error_display_modifier, ); process::exit(1) - }; + } } #[cfg(test)] -mod tests { +mod unit_tests { use std::cmp::Ordering; use super::*; diff --git a/src/lib.rs b/cloud-hypervisor/src/lib.rs similarity index 94% rename from src/lib.rs rename to cloud-hypervisor/src/lib.rs index 1596a13f47..836ed8e2ed 100644 --- a/src/lib.rs +++ b/cloud-hypervisor/src/lib.rs @@ -4,6 +4,8 @@ use std::error::Error; +use log::error; + /// Prints a chain of errors to the user in a consistent manner. /// The user will see a clear chain of errors, followed by debug output /// for opening issues. @@ -17,6 +19,9 @@ pub fn cli_print_error_chain<'a>( &'a (dyn Error + 'static), ) -> Option, ) { + // Debug info. + error!("Fatal error: {top_error:?}"); + eprint!("Error: {component} exited with the following "); if top_error.source().is_none() { eprintln!("error:"); @@ -38,7 +43,4 @@ pub fn cli_print_error_chain<'a>( } }); } - - eprintln!(); - eprintln!("Debug Info: {top_error:?}"); } diff --git a/cloud-hypervisor/src/logger.rs b/cloud-hypervisor/src/logger.rs new file mode 100644 index 0000000000..2f73d386ec --- /dev/null +++ b/cloud-hypervisor/src/logger.rs @@ -0,0 +1,700 @@ +// Copyright © 2026 Cloud Hypervisor Contributors +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::io::Write; +use std::str::FromStr; +use std::sync::Mutex; +use std::time::Instant; + +use jiff::tz::TimeZone; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Unterminated '{{' in format string")] + UnterminatedBrace, + #[error("Unmatched '}}' in format string")] + UnmatchedBrace, + #[error("Unknown format token '{{{0}}}'")] + UnknownToken(String), +} + +/// Which time source a date/time field should be read from. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Zone { + Utc, + Local, +} + +/// An individual broken-down date/time field. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum TimeField { + Year, + Month, + Day, + Hour, + Minute, + Second, + Micros, + /// Timezone offset like `-08:00` (always `+00:00` for `Zone::Utc`). + Offset, +} + +enum Token { + Literal(String), + BootTime, + /// Wallclock using RFC 3339 formatting. + WallClock, + /// UTC glog-style timestamp (e.g. `0521 08:02:15.542701`). + Glog, + /// Local-time glog-style timestamp (e.g. `0521 08:02:15.542701`). + LocalGlog, + Pid, + Tid, + Thread, + /// Full level word (e.g. `INFO`). + Level, + /// Single-letter level character, glog style (e.g. `I`). + LevelChar, + Location, + Msg, + /// A broken-down date/time field from either UTC or local wallclock. + Time(TimeField, Zone), +} + +impl FromStr for Token { + type Err = Error; + + fn from_str(s: &str) -> Result { + // Detect `local`-prefixed variants for the broken-down time fields. + let (name, zone) = match s.strip_prefix("local") { + Some(rest) => (rest, Zone::Local), + None => (s, Zone::Utc), + }; + + match name { + "year" => return Ok(Self::Time(TimeField::Year, zone)), + "month" => return Ok(Self::Time(TimeField::Month, zone)), + "day" => return Ok(Self::Time(TimeField::Day, zone)), + "hour" => return Ok(Self::Time(TimeField::Hour, zone)), + "minute" => return Ok(Self::Time(TimeField::Minute, zone)), + "second" => return Ok(Self::Time(TimeField::Second, zone)), + "micros" => return Ok(Self::Time(TimeField::Micros, zone)), + "offset" => return Ok(Self::Time(TimeField::Offset, zone)), + _ => {} + } + + // Fall back to tokens that don't take a `local` prefix. + match s { + "boottime" => Ok(Self::BootTime), + "wallclock" => Ok(Self::WallClock), + "glog" => Ok(Self::Glog), + "localglog" => Ok(Self::LocalGlog), + "pid" => Ok(Self::Pid), + "tid" => Ok(Self::Tid), + "thread" => Ok(Self::Thread), + "level" => Ok(Self::Level), + "levelchar" => Ok(Self::LevelChar), + "location" => Ok(Self::Location), + "msg" => Ok(Self::Msg), + _ => Err(Error::UnknownToken(s.to_string())), + } + } +} + +/// Convert a `log::Level` to its glog single-letter abbreviation. +fn level_char(level: log::Level) -> char { + match level { + log::Level::Error => 'E', + log::Level::Warn => 'W', + log::Level::Info => 'I', + log::Level::Debug => 'D', + log::Level::Trace => 'T', + } +} + +fn write_time_field( + out: &mut W, + field: TimeField, + zoned: &jiff::Zoned, +) -> std::io::Result<()> { + match field { + TimeField::Year => write!(out, "{:04}", zoned.year()), + TimeField::Month => write!(out, "{:02}", zoned.month()), + TimeField::Day => write!(out, "{:02}", zoned.day()), + TimeField::Hour => write!(out, "{:02}", zoned.hour()), + TimeField::Minute => write!(out, "{:02}", zoned.minute()), + TimeField::Second => write!(out, "{:02}", zoned.second()), + TimeField::Micros => write!(out, "{:06}", zoned.subsec_nanosecond() / 1000), + TimeField::Offset => write!(out, "{}", zoned.strftime("%:z")), + } +} + +fn parse_format(fmt: &str) -> Result, Error> { + let mut tokens = Vec::new(); + let mut literal = String::new(); + let mut chars = fmt.chars().peekable(); + + while let Some(c) = chars.next() { + match c { + '{' => { + if chars.peek() == Some(&'{') { + chars.next(); + literal.push('{'); + continue; + } + + if !literal.is_empty() { + tokens.push(Token::Literal(std::mem::take(&mut literal))); + } + + let mut name = String::new(); + loop { + match chars.next() { + Some('}') => break, + Some(ch) => name.push(ch), + None => return Err(Error::UnterminatedBrace), + } + } + + tokens.push(name.parse()?); + } + '}' => { + if chars.peek() == Some(&'}') { + chars.next(); + literal.push('}'); + } else { + return Err(Error::UnmatchedBrace); + } + } + _ => literal.push(c), + } + } + if !literal.is_empty() { + tokens.push(Token::Literal(literal)); + } + Ok(tokens) +} + +pub const DEFAULT_FORMAT: &str = + "cloud-hypervisor: {boottime}s: <{thread}> {level}:{location} -- {msg}"; + +pub struct Logger { + output: Mutex>, + start: Instant, + pid: u32, + tokens: Vec, + // Saving the timezone when Logger is constructed avoids potential seccomp violations when the + // internal libc timezone cache expires as the affected thread is unpredictable. + local_tz: TimeZone, +} + +impl Logger { + pub fn new(output: Box, format: &str) -> Result { + Ok(Self { + output: Mutex::new(output), + start: Instant::now(), + pid: std::process::id(), + tokens: parse_format(format)?, + local_tz: TimeZone::try_system().unwrap_or(TimeZone::UTC), + }) + } +} + +impl log::Log for Logger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let duration_s = Instant::now().duration_since(self.start).as_secs_f32(); + // Compute the wallclock timestamps lazily, but at most once per record so + // that multiple `{hour}`/`{minute}`/`{second}`/etc. fields stay coherent. + let mut zoned_utc: Option = None; + let mut zoned_local: Option = None; + let mut out = self.output.lock().unwrap(); + for token in &self.tokens { + let _ = match token { + Token::Literal(s) => out.write_all(s.as_bytes()), + // 10: 6 decimal places + sep => whole seconds in range `0..=999` properly aligned + Token::BootTime => write!(&mut *out, "{duration_s:>10.6?}"), + Token::WallClock => { + let zoned = zoned_utc.get_or_insert_with(|| { + jiff::Timestamp::now().to_zoned(jiff::tz::TimeZone::UTC) + }); + write!(&mut *out, "{:.6}", zoned.timestamp()) + } + Token::Glog => { + let zoned = zoned_utc.get_or_insert_with(|| { + jiff::Timestamp::now().to_zoned(jiff::tz::TimeZone::UTC) + }); + write!(&mut *out, "{}", zoned.strftime("%m%d %H:%M:%S%.6f")) + } + Token::LocalGlog => { + let zoned = zoned_local.get_or_insert_with(|| { + jiff::Timestamp::now().to_zoned(self.local_tz.clone()) + }); + write!(&mut *out, "{}", zoned.strftime("%m%d %H:%M:%S%.6f")) + } + Token::Pid => write!(&mut *out, "{}", self.pid), + // SAFETY: gettid(2) always succeeds + Token::Tid => write!(&mut *out, "{}", unsafe { libc::gettid() }), + Token::Thread => write!( + &mut *out, + "{}", + std::thread::current().name().unwrap_or("anonymous") + ), + Token::Level => write!(&mut *out, "{}", record.level()), + Token::LevelChar => write!(&mut *out, "{}", level_char(record.level())), + Token::Location => match (record.file(), record.line()) { + (Some(file), Some(line)) => write!(&mut *out, "{file}:{line}"), + _ => write!(&mut *out, "{}", record.target()), + }, + Token::Msg => write!(&mut *out, "{}", record.args()), + Token::Time(field, zone) => { + let zoned = match zone { + Zone::Utc => zoned_utc.get_or_insert_with(|| { + jiff::Timestamp::now().to_zoned(jiff::tz::TimeZone::UTC) + }), + Zone::Local => zoned_local.get_or_insert_with(|| { + jiff::Timestamp::now().to_zoned(self.local_tz.clone()) + }), + }; + write_time_field(&mut *out, *field, zoned) + } + }; + } + let _ = out.write_all(b"\r\n"); + } + + fn flush(&self) {} +} + +#[cfg(test)] +mod tests { + use std::io; + use std::sync::Arc; + + use log::Log; + + use super::*; + + /// A `Write` sink that appends to a shared byte buffer so tests can + /// inspect what the logger wrote. + #[derive(Clone, Default)] + struct SharedBuffer(Arc>>); + + impl SharedBuffer { + fn contents(&self) -> String { + String::from_utf8(self.0.lock().unwrap().clone()).unwrap() + } + } + + impl Write for SharedBuffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + fn render(tokens: &[Token]) -> String { + tokens + .iter() + .map(|t| match t { + Token::Literal(s) => format!("L({s})"), + Token::BootTime => "B".to_string(), + Token::WallClock => "W".to_string(), + Token::Glog => "G".to_string(), + Token::LocalGlog => "LG".to_string(), + Token::Pid => "P".to_string(), + Token::Tid => "I".to_string(), + Token::Thread => "T".to_string(), + Token::Level => "V".to_string(), + Token::LevelChar => "VC".to_string(), + Token::Location => "O".to_string(), + Token::Msg => "M".to_string(), + Token::Time(field, zone) => { + let z = match zone { + Zone::Utc => "u", + Zone::Local => "l", + }; + let f = match field { + TimeField::Year => "Y", + TimeField::Month => "Mo", + TimeField::Day => "D", + TimeField::Hour => "H", + TimeField::Minute => "Mi", + TimeField::Second => "S", + TimeField::Micros => "U", + TimeField::Offset => "Z", + }; + format!("T({z}:{f})") + } + }) + .collect::>() + .join("|") + } + + #[test] + fn parse_plain_literal() { + let tokens = parse_format("hello world").unwrap(); + assert_eq!(render(&tokens), "L(hello world)"); + } + + #[test] + fn parse_empty_string() { + let tokens = parse_format("").unwrap(); + assert!(tokens.is_empty()); + } + + #[test] + fn parse_all_known_tokens() { + let tokens = parse_format( + "[{boottime}] {wallclock} {glog} {localglog} {pid}/{tid} <{thread}> {level} {levelchar} {location} -- {msg}", + ) + .unwrap(); + assert_eq!( + render(&tokens), + "L([)|B|L(] )|W|L( )|G|L( )|LG|L( )|P|L(/)|I|L( <)|T|L(> )|V|L( )|VC|L( )|O|L( -- )|M" + ); + } + + #[test] + fn parse_default_format_succeeds() { + let tokens = parse_format(DEFAULT_FORMAT).unwrap(); + // Default format has 5 tokens interleaved with literals. + assert!(tokens.iter().any(|t| matches!(t, Token::BootTime))); + assert!(tokens.iter().any(|t| matches!(t, Token::Thread))); + assert!(tokens.iter().any(|t| matches!(t, Token::Level))); + assert!(tokens.iter().any(|t| matches!(t, Token::Location))); + assert!(tokens.iter().any(|t| matches!(t, Token::Msg))); + } + + #[test] + fn parse_escaped_braces() { + let tokens = parse_format("{{not-a-token}}").unwrap(); + assert_eq!(render(&tokens), "L({not-a-token})"); + } + + #[test] + fn parse_escaped_braces_around_token() { + let tokens = parse_format("{{{level}}}").unwrap(); + assert_eq!(render(&tokens), "L({)|V|L(})"); + } + + #[test] + fn parse_unterminated_brace_errors() { + match parse_format("hello {level") { + Err(Error::UnterminatedBrace) => {} + Err(other) => panic!("unexpected error: {other:?}"), + Ok(_) => panic!("expected error"), + } + } + + #[test] + fn parse_unmatched_close_brace_errors() { + match parse_format("hello }") { + Err(Error::UnmatchedBrace) => {} + Err(other) => panic!("unexpected error: {other:?}"), + Ok(_) => panic!("expected error"), + } + } + + #[test] + fn parse_unknown_token_errors() { + match parse_format("{nope}") { + Err(Error::UnknownToken(name)) => assert_eq!(name, "nope"), + Err(other) => panic!("unexpected error: {other:?}"), + Ok(_) => panic!("expected error"), + } + } + + #[test] + fn logger_new_uses_default_format() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap(); + // The default format has all 5 dynamic tokens. + assert_eq!( + logger + .tokens + .iter() + .filter(|t| !matches!(t, Token::Literal(_))) + .count(), + 5 + ); + } + + #[test] + fn logger_enabled_always_true() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf), DEFAULT_FORMAT).unwrap(); + let metadata = log::Metadata::builder() + .level(log::Level::Trace) + .target("anything") + .build(); + assert!(logger.enabled(&metadata)); + } + + #[test] + fn logger_writes_expected_fields() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("hello {}", "world")) + .level(log::Level::Info) + .target("unit_test_target") + .file(Some("foo.rs")) + .line(Some(42)) + .build(), + ); + + let out = buf.contents(); + assert!(out.starts_with("cloud-hypervisor: "), "got: {out}"); + assert!(out.contains("INFO"), "got: {out}"); + assert!(out.contains("foo.rs:42"), "got: {out}"); + assert!(out.contains("hello world"), "got: {out}"); + assert!(out.ends_with("\r\n"), "got: {out}"); + } + + #[test] + fn logger_uses_target_when_no_file() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("no location")) + .level(log::Level::Warn) + .target("my_target") + .file(None) + .line(None) + .build(), + ); + + let out = buf.contents(); + assert!(out.contains("my_target"), "got: {out}"); + assert!(!out.contains("foo.rs"), "got: {out}"); + } + + #[test] + fn logger_wallclock_is_rfc3339() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{wallclock}").unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + + let out = buf.contents(); + let out = out.trim(); + assert_eq!(out.len(), 27, "got: {out}"); + assert_eq!(&out[4..5], "-", "got: {out}"); + assert_eq!(&out[7..8], "-", "got: {out}"); + assert_eq!(&out[10..11], "T", "got: {out}"); + assert_eq!(&out[13..14], ":", "got: {out}"); + assert_eq!(&out[16..17], ":", "got: {out}"); + assert_eq!(&out[19..20], ".", "got: {out}"); + assert!(out.ends_with('Z'), "got: {out}"); + } + + #[test] + fn logger_glog_style_output() { + // `{levelchar}{localglog}` => glog-style header like `I0521 08:02:15.542701`. + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{levelchar}{localglog}").unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + + let out = buf.contents(); + let out = out.trim(); + // `IMMDD HH:MM:SS.uuuuuu` => 21 chars. + assert_eq!(out.len(), 21, "got: {out}"); + assert_eq!(&out[0..1], "I", "got: {out}"); + assert_eq!(&out[5..6], " ", "got: {out}"); + assert_eq!(&out[8..9], ":", "got: {out}"); + assert_eq!(&out[11..12], ":", "got: {out}"); + assert_eq!(&out[14..15], ".", "got: {out}"); + // Every non-separator character is an ASCII digit. + for (i, ch) in out.chars().enumerate() { + if [0, 5, 8, 11, 14].contains(&i) { + continue; + } + assert!(ch.is_ascii_digit(), "non-digit at {i}: got {out}"); + } + } + + #[test] + fn logger_glog_utc_output_shape() { + // `{glog}` alone produces `MMDD HH:MM:SS.uuuuuu` (20 chars). + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{glog}").unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + + let out = buf.contents(); + let out = out.trim(); + assert_eq!(out.len(), 20, "got: {out}"); + assert_eq!(&out[4..5], " ", "got: {out}"); + assert_eq!(&out[7..8], ":", "got: {out}"); + assert_eq!(&out[10..11], ":", "got: {out}"); + assert_eq!(&out[13..14], ".", "got: {out}"); + } + + #[test] + fn parse_utc_time_fields() { + let tokens = + parse_format("{year}-{month}-{day}T{hour}:{minute}:{second}.{micros}{offset}").unwrap(); + assert_eq!( + render(&tokens), + "T(u:Y)|L(-)|T(u:Mo)|L(-)|T(u:D)|L(T)|T(u:H)|L(:)|T(u:Mi)|L(:)|T(u:S)|L(.)|T(u:U)|T(u:Z)" + ); + } + + #[test] + fn parse_local_time_fields() { + let tokens = parse_format( + "{localyear}-{localmonth}-{localday}T{localhour}:{localminute}:{localsecond}.{localmicros}{localoffset}", + ) + .unwrap(); + assert_eq!( + render(&tokens), + "T(l:Y)|L(-)|T(l:Mo)|L(-)|T(l:D)|L(T)|T(l:H)|L(:)|T(l:Mi)|L(:)|T(l:S)|L(.)|T(l:U)|T(l:Z)" + ); + } + + #[test] + fn logger_utc_offset_is_zero() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{offset}").unwrap(); + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + assert_eq!(buf.contents().trim(), "+00:00"); + } + + #[test] + fn logger_utc_year_matches_jiff() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{year}").unwrap(); + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + let year: i32 = buf.contents().trim().parse().expect("year is numeric"); + assert!(year >= 2024, "got: {year}"); + } + + #[test] + fn logger_levelchar_per_level() { + for (level, expected) in [ + (log::Level::Error, "E"), + (log::Level::Warn, "W"), + (log::Level::Info, "I"), + (log::Level::Debug, "D"), + (log::Level::Trace, "T"), + ] { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{levelchar}").unwrap(); + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(level) + .target("t") + .build(), + ); + assert_eq!(buf.contents().trim(), expected); + } + } + + #[test] + fn logger_pid_token() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{pid}").unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + + let out = buf.contents(); + let out = out.trim(); + assert_eq!(out, std::process::id().to_string(), "got: {out}"); + } + + #[test] + fn logger_tid_token() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), "{tid}").unwrap(); + + logger.log( + &log::Record::builder() + .args(format_args!("")) + .level(log::Level::Info) + .target("t") + .build(), + ); + + let out = buf.contents(); + let out = out.trim(); + let tid: i64 = out.parse().expect("tid should be numeric"); + assert!(tid > 0, "got: {tid}"); + } + + #[test] + fn logger_appends_each_record() { + let buf = SharedBuffer::default(); + let logger = Logger::new(Box::new(buf.clone()), DEFAULT_FORMAT).unwrap(); + + for i in 0..3 { + logger.log( + &log::Record::builder() + .args(format_args!("entry-{i}")) + .level(log::Level::Debug) + .target("t") + .build(), + ); + } + + let out = buf.contents(); + assert_eq!(out.matches("entry-").count(), 3, "got: {out}"); + assert_eq!(out.matches("\r\n").count(), 3, "got: {out}"); + } +} diff --git a/src/main.rs b/cloud-hypervisor/src/main.rs similarity index 89% rename from src/main.rs rename to cloud-hypervisor/src/main.rs index 4a0fbe91f9..a561f568d5 100644 --- a/src/main.rs +++ b/cloud-hypervisor/src/main.rs @@ -3,40 +3,45 @@ // SPDX-License-Identifier: Apache-2.0 // +mod logger; #[cfg(test)] mod test_util; use std::fs::File; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::sync::mpsc::channel; -use std::sync::Mutex; use std::{env, io}; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command}; use event_monitor::event; use libc::EFD_NONBLOCK; -use log::{warn, LevelFilter}; +use log::{LevelFilter, error, info, warn}; use option_parser::OptionParser; use seccompiler::SeccompAction; use signal_hook::consts::SIGSYS; use thiserror::Error; +use vmm::api::ApiAction; #[cfg(feature = "dbus_api")] -use vmm::api::dbus::{dbus_api_graceful_shutdown, DBusApiOptions}; +use vmm::api::dbus::{DBusApiOptions, dbus_api_graceful_shutdown}; use vmm::api::http::http_api_graceful_shutdown; -use vmm::api::ApiAction; use vmm::config::{RestoreConfig, VmParams}; use vmm::landlock::{Landlock, LandlockError}; use vmm::vm_config; -#[cfg(target_arch = "x86_64")] -use vmm::vm_config::SgxEpcConfig; +#[cfg(feature = "fw_cfg")] +use vmm::vm_config::FwCfgConfig; +#[cfg(feature = "ivshmem")] +use vmm::vm_config::IvshmemConfig; use vmm::vm_config::{ - BalloonConfig, DeviceConfig, DiskConfig, FsConfig, LandlockConfig, NetConfig, NumaConfig, - PciSegmentConfig, PmemConfig, RateLimiterGroupConfig, TpmConfig, UserDeviceConfig, VdpaConfig, + BalloonConfig, ConsoleConfig, DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, + LandlockConfig, NetConfig, NumaConfig, PciSegmentConfig, PlatformConfig, PmemConfig, + RateLimiterGroupConfig, RngConfig, SerialConfig, TpmConfig, UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, }; use vmm_sys_util::eventfd::EventFd; use vmm_sys_util::signal::block_signal; +use crate::logger::Logger; + #[cfg(feature = "dhat-heap")] #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; @@ -92,6 +97,8 @@ enum Error { BareGdb, #[error("Error creating log file")] LogFileCreation(#[source] std::io::Error), + #[error("Error parsing logger format")] + LoggerFormat(#[source] logger::Error), #[error("Error setting up logger")] LoggerSetup(#[source] log::SetLoggerError), #[error("Failed to gracefully shutdown http api")] @@ -114,51 +121,6 @@ enum FdTableError { Dup2(#[source] std::io::Error), } -struct Logger { - output: Mutex>, - start: std::time::Instant, -} - -impl log::Log for Logger { - fn enabled(&self, _metadata: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; - } - - let now = std::time::Instant::now(); - let duration = now.duration_since(self.start); - - if record.file().is_some() && record.line().is_some() { - write!( - *(*(self.output.lock().unwrap())), - "cloud-hypervisor: {:.6?}: <{}> {}:{}:{} -- {}\r\n", - duration, - std::thread::current().name().unwrap_or("anonymous"), - record.level(), - record.file().unwrap(), - record.line().unwrap(), - record.args() - ) - } else { - write!( - *(*(self.output.lock().unwrap())), - "cloud-hypervisor: {:.6?}: <{}> {}:{} -- {}\r\n", - duration, - std::thread::current().name().unwrap_or("anonymous"), - record.level(), - record.target(), - record.args() - ) - } - .ok(); - } - fn flush(&self) {} -} - fn prepare_default_values() -> (String, String, String) { (default_vcpus(), default_memory(), default_rng()) } @@ -176,7 +138,7 @@ fn default_memory() -> String { } fn default_rng() -> String { - format!("src={}", vm_config::DEFAULT_RNG_SOURCE) + format!("src={}", RngConfig::DEFAULT_RNG_SOURCE) } /// Returns all [`Arg`]s in alphabetical order. This is the order used in the @@ -189,7 +151,7 @@ fn get_cli_options_sorted( [ Arg::new("api-socket") .long("api-socket") - .help("HTTP API socket (UNIX domain socket): path= or fd=.") + .help("HTTP API socket (UNIX domain socket): path= or fd=.") .num_args(1) .group("vmm-config"), Arg::new("balloon") @@ -201,11 +163,10 @@ fn get_cli_options_sorted( .long("cmdline") .help("Kernel command line") .num_args(1) - .group("vm-config"), Arg::new("console") + .group("vm-config"), + Arg::new("console") .long("console") - .help( - "Control (virtio) console: \"off|null|pty|tty|file=,iommu=on|off\"", - ) + .help(ConsoleConfig::SYNTAX) .default_value("tty") .group("vm-config"), Arg::new("cpus") @@ -215,22 +176,11 @@ fn get_cli_options_sorted( topology=:::,\ kvm_hyperv=on|off,max_phys_bits=,\ affinity=,\ - features=", + features=,\ + nested=on|off,core_scheduling=vm|vcpu|off", ) .default_value(default_vcpus) .group("vm-config"), - #[cfg(target_arch = "x86_64")] - Arg::new("debug-console") - .long("debug-console") - .help("Debug console: off|pty|tty|file=,iobase=") - .default_value("off,iobase=0xe9") - .group("vm-config"), - #[cfg(feature = "dbus_api")] - Arg::new("dbus-service-name") - .long("dbus-service-name") - .help("Well known name of the device") - .num_args(1) - .group("vmm-config"), #[cfg(feature = "dbus_api")] Arg::new("dbus-object-path") .long("dbus-object-path") @@ -238,25 +188,39 @@ fn get_cli_options_sorted( .num_args(1) .group("vmm-config"), #[cfg(feature = "dbus_api")] + Arg::new("dbus-service-name") + .long("dbus-service-name") + .help("Well known name of the device") + .num_args(1) + .group("vmm-config"), + #[cfg(feature = "dbus_api")] Arg::new("dbus-system-bus") .long("dbus-system-bus") .action(ArgAction::SetTrue) .help("Use the system bus instead of a session bus") .num_args(0) .group("vmm-config"), + #[cfg(target_arch = "x86_64")] + Arg::new("debug-console") + .long("debug-console") + .help("Debug console: off|pty|tty|file=,iobase=") + .default_value("off,iobase=0xe9") + .group("vm-config"), Arg::new("device") .long("device") .help(DeviceConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("disk") .long("disk") .help(DiskConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("event-monitor") .long("event-monitor") - .help("File to report events on: path= or fd=") + .help("Path to report events on: path= or fd=") .num_args(1) .group("vmm-config"), Arg::new("firmware") @@ -268,13 +232,26 @@ fn get_cli_options_sorted( .long("fs") .help(FsConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), + #[cfg(feature = "fw_cfg")] + Arg::new("fw-cfg-config") + .long("fw-cfg-config") + .help(FwCfgConfig::SYNTAX) + .num_args(1) + .group("vm-payload"), #[cfg(feature = "guest_debug")] Arg::new("gdb") .long("gdb") - .help("GDB socket (UNIX domain socket): path=") + .help("GDB socket (UNIX domain socket): path=") .num_args(1) .group("vmm-config"), + Arg::new("generic-vhost-user") + .long("generic-vhost-user") + .help(GenericVhostUserConfig::SYNTAX) + .num_args(1..) + .action(ArgAction::Append) + .group("vm-config"), #[cfg(feature = "igvm")] Arg::new("igvm") .long("igvm") @@ -292,6 +269,12 @@ fn get_cli_options_sorted( .help("Path to initramfs image") .num_args(1) .group("vm-config"), + #[cfg(feature = "ivshmem")] + Arg::new("ivshmem") + .long("ivshmem") + .help(IvshmemConfig::SYNTAX) + .num_args(1) + .group("vm-config"), Arg::new("kernel") .long("kernel") .help( @@ -303,9 +286,7 @@ fn get_cli_options_sorted( Arg::new("landlock") .long("landlock") .num_args(0) - .help( - "enable/disable Landlock.", - ) + .help("enable/disable Landlock.") .action(ArgAction::SetTrue) .default_value("false") .group("vm-config"), @@ -313,12 +294,24 @@ fn get_cli_options_sorted( .long("landlock-rules") .help(LandlockConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("log-file") .long("log-file") .help("Log file. Standard error is used if not specified") .num_args(1) .group("logging"), + Arg::new("log-format") + .long("log-format") + .help( + "Log format. Common tokens: {boottime}, {wallclock}, {glog}, \ + {localglog}, {thread}, {level}, {location}, {msg}. See \ + docs/logging.md for the full list (per-field date/time tokens, \ + local-time variants, glog level letter).", + ) + .num_args(1) + .default_value(logger::DEFAULT_FORMAT) + .group("logging"), Arg::new("memory") .long("memory") .help( @@ -345,33 +338,42 @@ fn get_cli_options_sorted( prefault=on|off\"", ) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("net") .long("net") .help(NetConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), + Arg::new("no-shutdown") + .long("no-shutdown") + .help("Do not exit the VMM when the guest shuts down") + .num_args(0) + .action(ArgAction::SetTrue) + .group("vmm-config"), Arg::new("numa") .long("numa") .help(NumaConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("pci-segment") .long("pci-segment") .help(PciSegmentConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("platform") .long("platform") - .help( - "num_pci_segments=,iommu_segments=,iommu_address_width=,serial_number=,uuid=,oem_strings=" - ) + .help(PlatformConfig::syntax()) .num_args(1) .group("vm-config"), Arg::new("pmem") .long("pmem") .help(PmemConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), #[cfg(feature = "pvmemcontrol")] Arg::new("pvmemcontrol") @@ -390,6 +392,7 @@ fn get_cli_options_sorted( .long("rate-limit-group") .help(RateLimiterGroupConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("restore") .long("restore") @@ -398,9 +401,7 @@ fn get_cli_options_sorted( .group("vmm-config"), Arg::new("rng") .long("rng") - .help( - "Random number generator parameters \"src=,iommu=on|off\"", - ) + .help(RngConfig::SYNTAX) .default_value(default_rng) .group("vm-config"), Arg::new("seccomp") @@ -410,15 +411,9 @@ fn get_cli_options_sorted( .default_value("true"), Arg::new("serial") .long("serial") - .help("Control serial port: off|null|pty|tty|file=|socket=") + .help(SerialConfig::SYNTAX) .default_value("null") .group("vm-config"), - #[cfg(target_arch = "x86_64")] - Arg::new("sgx-epc") - .long("sgx-epc") - .help(SgxEpcConfig::SYNTAX) - .num_args(1..) - .group("vm-config"), Arg::new("tpm") .long("tpm") .num_args(1) @@ -428,6 +423,7 @@ fn get_cli_options_sorted( .long("user-device") .help(UserDeviceConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("v") .short('v') @@ -438,6 +434,7 @@ fn get_cli_options_sorted( .long("vdpa") .help(VdpaConfig::SYNTAX) .num_args(1..) + .action(ArgAction::Append) .group("vm-config"), Arg::new("version") .short('V') @@ -456,7 +453,9 @@ fn get_cli_options_sorted( .num_args(0) .action(ArgAction::SetTrue) .group("vm-config"), - ].to_vec().into_boxed_slice() + ] + .to_vec() + .into_boxed_slice() } /// Creates the CLI definition of Cloud Hypervisor. @@ -482,7 +481,37 @@ fn create_app(default_vcpus: String, default_memory: String, default_rng: String .args(args) } -fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { +fn parse_api_socket(cmd_arguments: &ArgMatches) -> Result<(Option, Option), Error> { + if let Some(socket_config) = cmd_arguments.get_one::("api-socket") { + let mut parser = OptionParser::new(); + parser.add("path").add("fd"); + parser.parse(socket_config).unwrap_or_default(); + + if let Some(fd) = parser.get("fd") { + Ok(( + None, + Some(fd.parse::().map_err(Error::ParsingApiSocket)?), + )) + } else if let Some(path) = parser.get("path") { + Ok((Some(path), None)) + } else { + Ok(( + cmd_arguments + .get_one::("api-socket") + .map(|s| s.to_string()), + None, + )) + } + } else { + Ok((None, None)) + } +} + +fn start_vmm( + cmd_arguments: &ArgMatches, + api_socket_path: &Option, + api_socket_fd: Option, +) -> Result<(), Error> { let log_level = match cmd_arguments.get_count("v") { 0 => LevelFilter::Warn, 1 => LevelFilter::Info, @@ -498,37 +527,11 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { Box::new(std::io::stderr()) }; - log::set_boxed_logger(Box::new(Logger { - output: Mutex::new(log_file), - start: std::time::Instant::now(), - })) - .map(|()| log::set_max_level(log_level)) - .map_err(Error::LoggerSetup)?; - - let (api_socket_path, api_socket_fd) = - if let Some(socket_config) = cmd_arguments.get_one::("api-socket") { - let mut parser = OptionParser::new(); - parser.add("path").add("fd"); - parser.parse(socket_config).unwrap_or_default(); - - if let Some(fd) = parser.get("fd") { - ( - None, - Some(fd.parse::().map_err(Error::ParsingApiSocket)?), - ) - } else if let Some(path) = parser.get("path") { - (Some(path), None) - } else { - ( - cmd_arguments - .get_one::("api-socket") - .map(|s| s.to_string()), - None, - ) - } - } else { - (None, None) - }; + let format = cmd_arguments.get_one::("log-format").unwrap(); + let logger = Logger::new(log_file, format).map_err(Error::LoggerFormat)?; + log::set_boxed_logger(Box::new(logger)) + .map(|()| log::set_max_level(log_level)) + .map_err(Error::LoggerSetup)?; let (api_request_sender, api_request_receiver) = channel(); let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?; @@ -553,15 +556,15 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { // handler safe functions (writing to stderr) and manipulating signals. unsafe { signal_hook::low_level::register(signal_hook::consts::SIGSYS, || { - eprint!( + eprintln!( "\n==== Possible seccomp violation ====\n\ Try running with `strace -ff` to identify the cause and open an issue: \ - https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new\n" + https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new" ); signal_hook::low_level::emulate_default_handler(SIGSYS).unwrap(); }) } - .map_err(|e| eprintln!("Error adding SIGSYS signal handler: {e}")) + .map_err(|e| error!("Error adding SIGSYS signal handler: {e}")) .ok(); } @@ -575,16 +578,18 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { // dedicated signal handling thread we'll start in a bit. for sig in &vmm::vm::Vm::HANDLED_SIGNALS { if let Err(e) = block_signal(*sig) { - eprintln!("Error blocking signals: {e}"); + error!("Error blocking signals: {e}"); } } for sig in &vmm::Vmm::HANDLED_SIGNALS { if let Err(e) = block_signal(*sig) { - eprintln!("Error blocking signals: {e}"); + error!("Error blocking signals: {e}"); } } + info!("{} starting", env!("BUILD_VERSION")); + let hypervisor = hypervisor::new().map_err(Error::CreateHypervisor)?; #[cfg(feature = "guest_debug")] @@ -608,6 +613,7 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { let exit_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateExitEventFd)?; let landlock_enable = cmd_arguments.get_flag("landlock"); + let no_shutdown = cmd_arguments.get_flag("no-shutdown"); #[allow(unused_mut)] let mut event_monitor = cmd_arguments @@ -684,11 +690,15 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { .map_err(Error::EventMonitorThread)?; } + info!( + "Cloud Hypervisor starting: build version: {}", + env!("BUILD_VERSION"), + ); event!("vmm", "starting"); let vmm_thread_handle = vmm::start_vmm_thread( vmm::VmmVersionInfo::new(env!("BUILD_VERSION"), env!("CARGO_PKG_VERSION")), - &api_socket_path, + api_socket_path, api_socket_fd, #[cfg(feature = "dbus_api")] dbus_options, @@ -704,6 +714,7 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { exit_evt.try_clone().unwrap(), &seccomp_action, hypervisor, + no_shutdown, landlock_enable, ) .map_err(Error::StartVmmThread)?; @@ -718,7 +729,7 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { cmd_arguments.contains_id("kernel") || cmd_arguments.contains_id("firmware"); if payload_present { - let vm_params = VmParams::from_arg_matches(&cmd_arguments); + let vm_params = VmParams::from_arg_matches(cmd_arguments); let vm_config = VmConfig::parse(vm_params).map_err(Error::ParsingConfig)?; // Create and boot the VM based off the VM config we just built. @@ -746,10 +757,10 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { Ok(()) })(); - if r.is_err() { - if let Err(e) = exit_evt.write(1) { - warn!("writing to exit EventFd: {e}"); - } + if r.is_err() + && let Err(e) = exit_evt.write(1) + { + warn!("writing to exit EventFd: {e}"); } if landlock_enable { @@ -766,7 +777,7 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { .map_err(Error::VmmThread)?; if let Some(api_handle) = vmm_thread_handle.http_api_handle { - http_api_graceful_shutdown(api_handle).map_err(Error::HttpApiShutdown)? + http_api_graceful_shutdown(api_handle).map_err(Error::HttpApiShutdown)?; } #[cfg(feature = "dbus_api")] @@ -774,7 +785,7 @@ fn start_vmm(cmd_arguments: ArgMatches) -> Result, Error> { dbus_api_graceful_shutdown(chs); } - r.map(|_| api_socket_path) + r } // This is a best-effort solution to the latency induced by the RCU @@ -853,6 +864,8 @@ fn main() { compile_error!("Feature 'tdx' and 'sev_snp' are mutually exclusive."); #[cfg(all(feature = "sev_snp", not(target_arch = "x86_64")))] compile_error!("Feature 'sev_snp' needs target 'x86_64'"); + #[cfg(all(feature = "fw_cfg", target_arch = "riscv64"))] + compile_error!("Feature 'fw_cfg' needs targets 'x86_64' or 'aarch64'"); #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); @@ -878,9 +891,23 @@ fn main() { warn!("Error expanding FD table: {e}"); } - let exit_code = match start_vmm(cmd_arguments) { - Ok(path) => { - path.map(|s| std::fs::remove_file(s).ok()); + let (api_socket_path, api_socket_fd) = match parse_api_socket(&cmd_arguments) { + Ok(p) => p, + Err(top_error) => { + cloud_hypervisor::cli_print_error_chain(&top_error, "Cloud Hypervisor", |_, _, _| None); + std::process::exit(1); + } + }; + + let vmm_result = start_vmm(&cmd_arguments, &api_socket_path, api_socket_fd); + + if let Some(ref p) = api_socket_path { + let _ = std::fs::remove_file(p); + } + + let exit_code = match vmm_result { + Ok(()) => { + info!("Cloud Hypervisor exited successfully"); 0 } Err(top_error) => { @@ -903,8 +930,9 @@ mod unit_tests { #[cfg(target_arch = "x86_64")] use vmm::vm_config::DebugConsoleConfig; use vmm::vm_config::{ - ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig, - PayloadConfig, RngConfig, VmConfig, + CommonConsoleConfig, ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures, + CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, PciDeviceCommonConfig, RngConfig, + SerialConfig, VmConfig, }; use crate::test_util::assert_args_sorted; @@ -954,6 +982,9 @@ mod unit_tests { max_phys_bits: 46, affinity: None, features: CpuFeatures::default(), + nested: true, + core_scheduling: CoreScheduling::Vm, + profile: Default::default(), }, memory: MemoryConfig { size: 536_870_912, @@ -977,28 +1008,36 @@ mod unit_tests { igvm: None, #[cfg(feature = "sev_snp")] host_data: None, + #[cfg(feature = "fw_cfg")] + fw_cfg_config: None, }), rate_limit_groups: None, disks: None, net: None, rng: RngConfig { src: PathBuf::from("/dev/urandom"), - iommu: false, + pci_common: PciDeviceCommonConfig::default(), }, balloon: None, fs: None, + generic_vhost_user: None, pmem: None, - serial: ConsoleConfig { - file: None, - mode: ConsoleOutputMode::Null, - iommu: false, - socket: None, + serial: SerialConfig { + common: CommonConsoleConfig { + file: None, + mode: ConsoleOutputMode::Null, + socket: None, + url: None, + }, }, console: ConsoleConfig { - file: None, - mode: ConsoleOutputMode::Tty, - iommu: false, - socket: None, + common: CommonConsoleConfig { + file: None, + mode: ConsoleOutputMode::Tty, + socket: None, + url: None, + }, + pci_common: PciDeviceCommonConfig::default(), }, #[cfg(target_arch = "x86_64")] debug_console: DebugConsoleConfig::default(), @@ -1010,8 +1049,6 @@ mod unit_tests { #[cfg(feature = "pvmemcontrol")] pvmemcontrol: None, iommu: false, - #[cfg(target_arch = "x86_64")] - sgx_epc: None, numa: None, watchdog: false, #[cfg(feature = "guest_debug")] @@ -1022,6 +1059,8 @@ mod unit_tests { preserved_fds: None, landlock_enable: false, landlock_rules: None, + #[cfg(feature = "ivshmem")] + ivshmem: None, }; assert_eq!(expected_vm_config, result_vm_config); @@ -1081,8 +1120,7 @@ mod unit_tests { #[test] fn test_valid_vm_config_memory() { - vec![ - ( + [( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1073741824"], r#"{ "payload": {"kernel": "/path/to/kernel"}, @@ -1137,8 +1175,7 @@ mod unit_tests { "memory": {"size": 1073741824, "hotplug_method": "VirtioMem", "hotplug_size": 1073741824} }"#, true, - ), - ] + )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); @@ -1190,14 +1227,14 @@ mod unit_tests { "--kernel", "/path/to/kernel", "--disk", - "path=/path/to/disk/1", + "path=/path/to/disk/1,image_type=raw", "path=/path/to/disk/2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "disks": [ - {"path": "/path/to/disk/1"}, - {"path": "/path/to/disk/2"} + {"path": "/path/to/disk/1", "image_type": "Raw"}, + {"path": "/path/to/disk/2", "image_type": "Unknown"} ] }"#, true, @@ -1208,8 +1245,8 @@ mod unit_tests { "--kernel", "/path/to/kernel", "--disk", - "path=/path/to/disk/1", - "path=/path/to/disk/2", + "path=/path/to/disk/1,image_type=raw", + "path=/path/to/disk/2,image_type=qcow2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, @@ -1271,8 +1308,8 @@ mod unit_tests { r#"{ "payload": {"kernel": "/path/to/kernel"}, "disks": [ - {"path": "/path/to/disk/1", "rate_limit_group": "group0"}, - {"path": "/path/to/disk/2", "rate_limit_group": "group0"} + {"path": "/path/to/disk/1", "rate_limit_group": "group0", "image_type": "Unknown"}, + {"path": "/path/to/disk/2", "rate_limit_group": "group0", "image_type": "Unknown"} ], "rate_limit_groups": [ {"id": "group0", "rate_limiter_config": {"bandwidth": {"size": 1000, "one_time_burst": 0, "refill_time": 100}}} @@ -1289,7 +1326,7 @@ mod unit_tests { #[test] fn test_valid_vm_config_net() { - vec![ + [ // This test is expected to fail because the default MAC address is // randomly generated. There's no way we can have twice the same // default value. @@ -1325,20 +1362,6 @@ mod unit_tests { }"#, true, ), - ( - vec![ - "cloud-hypervisor", "--kernel", "/path/to/kernel", - "--net", - "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4", - ], - r#"{ - "payload": {"kernel": "/path/to/kernel"}, - "net": [ - {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4"} - ] - }"#, - true, - ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", @@ -1682,10 +1705,12 @@ mod unit_tests { "--serial", "null", "--console", - "tty", + "tty,pci_segment=1,pci_device_id=7", ], r#"{ - "payload": {"kernel": "/path/to/kernel"} + "payload": {"kernel": "/path/to/kernel"}, + "serial": {"mode": "Null"}, + "console": {"mode": "Tty", "iommu": false, "pci_segment": 1, "pci_device_id": 7} }"#, true, ), @@ -1767,7 +1792,7 @@ mod unit_tests { #[test] #[cfg(target_arch = "x86_64")] fn test_valid_vm_config_devices() { - vec![ + [ ( vec![ "cloud-hypervisor", @@ -2017,6 +2042,6 @@ mod unit_tests { let (default_vcpus, default_memory, default_rng) = prepare_default_values(); let args = get_cli_options_sorted(default_vcpus, default_memory, default_rng); - assert_args_sorted(|| args.iter()) + assert_args_sorted(|| args.iter()); } } diff --git a/src/test_util.rs b/cloud-hypervisor/src/test_util.rs similarity index 100% rename from src/test_util.rs rename to cloud-hypervisor/src/test_util.rs diff --git a/cloud-hypervisor/tests/common/mod.rs b/cloud-hypervisor/tests/common/mod.rs new file mode 100644 index 0000000000..da58f907e8 --- /dev/null +++ b/cloud-hypervisor/tests/common/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod tests_wrappers; +pub(crate) mod utils; diff --git a/cloud-hypervisor/tests/common/tests_wrappers.rs b/cloud-hypervisor/tests/common/tests_wrappers.rs new file mode 100644 index 0000000000..a236db3d5a --- /dev/null +++ b/cloud-hypervisor/tests/common/tests_wrappers.rs @@ -0,0 +1,3562 @@ +// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +use std::ffi::{CStr, CString}; +use std::fs::{self, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::os::unix::io::AsRawFd; +use std::path::{Path, PathBuf}; +use std::string::String; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +use block::ImageType; +use net_util::MacAddr; +use test_infra::*; +use vmm_sys_util::tempdir::TempDir; +use vmm_sys_util::tempfile::TempFile; +use wait_timeout::ChildExt; + +use crate::common::utils::{TargetApi, *}; + +// Start cloud-hypervisor with no VM parameters, only the API server running. +// From the API: Create a VM, boot it and check that it looks as expected. +pub(crate) fn _test_api_create_boot(target_api: &TargetApi, guest: &Guest) { + let mut child = GuestCommand::new(guest) + .args(target_api.guest_args()) + .capture_output() + .spawn() + .unwrap(); + + // Wait for API server to be ready + assert!(wait_until(Duration::from_secs(5), || target_api + .remote_command("ping", None))); + + // Create the VM first + let request_body = guest.api_create_body(); + + let temp_config_path = guest.tmp_dir.as_path().join("config"); + std::fs::write(&temp_config_path, request_body).unwrap(); + let create_config = temp_config_path.as_os_str().to_str().unwrap(); + + assert!(target_api.remote_command("create", Some(create_config),)); + + // Then boot it + assert!(target_api.remote_command("boot", None)); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +// Start cloud-hypervisor with no VM parameters, only the API server running. +// From the API: Create a VM, boot it and check it can be shutdown and then +// booted again +pub(crate) fn _test_api_shutdown(target_api: &TargetApi, guest: &Guest) { + let mut child = GuestCommand::new(guest) + .args(target_api.guest_args()) + .capture_output() + .spawn() + .unwrap(); + + // Wait for API server to be ready + assert!(wait_until(Duration::from_secs(5), || target_api + .remote_command("ping", None))); + + // Create the VM first + let request_body = guest.api_create_body(); + + let temp_config_path = guest.tmp_dir.as_path().join("config"); + std::fs::write(&temp_config_path, request_body).unwrap(); + let create_config = temp_config_path.as_os_str().to_str().unwrap(); + + let r = std::panic::catch_unwind(|| { + assert!(target_api.remote_command("create", Some(create_config))); + + // Then boot it + assert!(target_api.remote_command("boot", None)); + + guest.wait_vm_boot().unwrap(); + + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + + // Sync and shutdown without powering off to prevent filesystem + // corruption. + guest.ssh_command("sync").unwrap(); + guest.ssh_command("sudo shutdown -H now").unwrap(); + + // Wait for the guest to be fully shutdown + assert!(guest.wait_for_ssh_unresponsive(Duration::from_secs(20))); + + // Then shut it down + assert!(target_api.remote_command("shutdown", None)); + + // Then boot it again + assert!(target_api.remote_command("boot", None)); + + guest.wait_vm_boot().unwrap(); + + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +// Start cloud-hypervisor with no VM parameters, only the API server running. +// From the API: Create a VM, boot it and check it can be deleted and then recreated +// booted again. +pub(crate) fn _test_api_delete(target_api: &TargetApi, guest: &Guest) { + let mut child = GuestCommand::new(guest) + .args(target_api.guest_args()) + .capture_output() + .spawn() + .unwrap(); + + // Wait for API server to be ready + assert!(wait_until(Duration::from_secs(5), || target_api + .remote_command("ping", None))); + + // Create the VM first + let request_body = guest.api_create_body(); + + let temp_config_path = guest.tmp_dir.as_path().join("config"); + std::fs::write(&temp_config_path, request_body).unwrap(); + let create_config = temp_config_path.as_os_str().to_str().unwrap(); + + let r = std::panic::catch_unwind(|| { + assert!(target_api.remote_command("create", Some(create_config))); + + // Then boot it + assert!(target_api.remote_command("boot", None)); + + guest.wait_vm_boot().unwrap(); + + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + + // Sync and shutdown without powering off to prevent filesystem + // corruption. + guest.ssh_command("sync").unwrap(); + guest.ssh_command("sudo shutdown -H now").unwrap(); + + // Wait for the guest to be fully shutdown + assert!(guest.wait_for_ssh_unresponsive(Duration::from_secs(20))); + + // Then delete it + assert!(target_api.remote_command("delete", None)); + + assert!(target_api.remote_command("create", Some(create_config))); + + // Then boot it again + assert!(target_api.remote_command("boot", None)); + + guest.wait_vm_boot().unwrap(); + + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +// Start cloud-hypervisor with no VM parameters, only the API server running. +// From the API: Create a VM, boot it and check that it looks as expected. +// Then we pause the VM, check that it's no longer available. +// Finally we resume the VM and check that it's available. +pub(crate) fn _test_api_pause_resume(target_api: &TargetApi, guest: &Guest) { + let mut child = GuestCommand::new(guest) + .args(target_api.guest_args()) + .capture_output() + .spawn() + .unwrap(); + + // Wait for API server to be ready + assert!(wait_until(Duration::from_secs(5), || target_api + .remote_command("ping", None))); + + // Create the VM first + let request_body = guest.api_create_body(); + + let temp_config_path = guest.tmp_dir.as_path().join("config"); + std::fs::write(&temp_config_path, request_body).unwrap(); + let create_config = temp_config_path.as_os_str().to_str().unwrap(); + + assert!(target_api.remote_command("create", Some(create_config))); + + // Then boot it + assert!(target_api.remote_command("boot", None)); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + + // We now pause the VM + assert!(target_api.remote_command("pause", None)); + + // Check pausing again fails + assert!(!target_api.remote_command("pause", None)); + + thread::sleep(std::time::Duration::new(2, 0)); + + // SSH into the VM should fail + ssh_command_ip( + "grep -c processor /proc/cpuinfo", + &guest.network.guest_ip0, + 2, + 5, + ) + .unwrap_err(); + + // Resume the VM + assert!(target_api.remote_command("resume", None)); + + // Check resuming again fails + assert!(!target_api.remote_command("resume", None)); + + thread::sleep(std::time::Duration::new(2, 0)); + + // Now we should be able to SSH back in and get the right number of CPUs + guest.validate_cpu_count(None); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_pty_interaction(pty_path: PathBuf) { + let mut cf = std::fs::OpenOptions::new() + .write(true) + .read(true) + .open(pty_path) + .unwrap(); + + // Some dumb sleeps but we don't want to write + // before the console is up and we don't want + // to try and write the next line before the + // login process is ready. + thread::sleep(std::time::Duration::new(5, 0)); + assert_eq!(cf.write(b"cloud\n").unwrap(), 6); + thread::sleep(std::time::Duration::new(2, 0)); + assert_eq!(cf.write(b"cloud123\n").unwrap(), 9); + thread::sleep(std::time::Duration::new(2, 0)); + assert_eq!(cf.write(b"echo test_pty_console\n").unwrap(), 22); + thread::sleep(std::time::Duration::new(2, 0)); + + // read pty and ensure they have a login shell + // some fairly hacky workarounds to avoid looping + // forever in case the channel is blocked getting output + let ptyc = pty_read(cf); + let mut empty = 0; + let mut prev = String::new(); + loop { + thread::sleep(std::time::Duration::new(2, 0)); + match ptyc.try_recv() { + Ok(line) => { + empty = 0; + prev = prev + &line; + if prev.contains("test_pty_console") { + break; + } + } + Err(mpsc::TryRecvError::Empty) => { + empty += 1; + assert!(empty <= 5, "No login on pty"); + } + _ => { + panic!("No login on pty") + } + } + } +} + +pub(crate) fn test_cpu_topology( + threads_per_core: u8, + cores_per_package: u8, + packages: u8, + use_fw: bool, +) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let total_vcpus = threads_per_core * cores_per_package * packages; + let direct_kernel_boot_path = direct_kernel_boot_path(); + let mut kernel_path = direct_kernel_boot_path.to_str().unwrap(); + let fw_path = fw_path(FwType::RustHypervisorFirmware); + if use_fw { + kernel_path = fw_path.as_str(); + } + + let mut child = GuestCommand::new(&guest) + .args([ + "--cpus", + &format!( + "boot={total_vcpus},topology={threads_per_core}:{cores_per_package}:1:{packages}" + ), + ]) + .default_memory() + .args(["--kernel", kernel_path]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + assert_eq!( + guest.get_cpu_count().unwrap_or_default(), + u32::from(total_vcpus) + ); + assert_eq!( + guest + .ssh_command("lscpu | grep \"per core\" | cut -f 2 -d \":\" | sed \"s# *##\"") + .unwrap() + .trim() + .parse::() + .unwrap_or(0), + threads_per_core + ); + + #[cfg(target_arch = "x86_64")] + let cores_per_package_grep = "per socket"; + #[cfg(target_arch = "aarch64")] + let cores_per_package_grep = if use_fw { "per socket" } else { "per cluster" }; + + assert_eq!( + guest + .ssh_command(&format!( + "lscpu | grep \"{cores_per_package_grep}\" | cut -f 2 -d \":\" | sed \"s# *##\"" + )) + .unwrap() + .trim() + .parse::() + .unwrap_or(0), + cores_per_package + ); + + #[cfg(target_arch = "x86_64")] + let packages_grep = "Socket"; + #[cfg(target_arch = "aarch64")] + let packages_grep = if use_fw { "Socket" } else { "Cluster" }; + + assert_eq!( + guest + .ssh_command(&format!( + "lscpu | grep \"{packages_grep}\" | cut -f 2 -d \":\" | sed \"s# *##\"" + )) + .unwrap() + .trim() + .parse::() + .unwrap_or(0), + packages + ); + + #[cfg(target_arch = "x86_64")] + { + let mut cpu_id = 0; + for package_id in 0..packages { + for core_id in 0..cores_per_package { + for _ in 0..threads_per_core { + assert_eq!( + guest + .ssh_command(&format!("cat /sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id")) + .unwrap() + .trim() + .parse::() + .unwrap_or(0), + package_id + ); + + assert_eq!( + guest + .ssh_command(&format!( + "cat /sys/devices/system/cpu/cpu{cpu_id}/topology/core_id" + )) + .unwrap() + .trim() + .parse::() + .unwrap_or(0), + core_id + ); + + cpu_id += 1; + } + } + } + } + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +#[allow(unused_variables)] +pub(crate) fn _test_guest_numa_nodes(acpi: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = if acpi { + edk2_path() + } else { + direct_kernel_boot_path() + }; + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=6,max=12"]) + .args(["--memory", "size=0,hotplug_method=virtio-mem"]) + .args([ + "--memory-zone", + "id=mem0,size=1G,hotplug_size=3G", + "id=mem1,size=2G,hotplug_size=3G", + "id=mem2,size=3G,hotplug_size=3G", + ]) + .args([ + "--numa", + "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", + "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", + "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", + ]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--api-socket", &api_socket]) + .capture_output() + .default_disks() + .default_net() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + guest.check_numa_common( + Some(&[960_000, 1_920_000, 2_880_000]), + Some(&[&[0, 1, 2], &[3, 4], &[5]]), + Some(&["10 15 20", "20 10 25", "25 30 10"]), + ); + + // AArch64 currently does not support hotplug, and therefore we only + // test hotplug-related function on x86_64 here. + #[cfg(target_arch = "x86_64")] + { + guest.enable_memory_hotplug(); + + // Resize every memory zone and check each associated NUMA node + // has been assigned the right amount of memory. + resize_zone_command(&api_socket, "mem0", "4G"); + resize_zone_command(&api_socket, "mem1", "4G"); + resize_zone_command(&api_socket, "mem2", "4G"); + // Resize to the maximum amount of CPUs and check each NUMA + // node has been assigned the right CPUs set. + resize_command(&api_socket, Some(12), None, None, None); + thread::sleep(std::time::Duration::new(5, 0)); + + guest.check_numa_common( + Some(&[3_840_000, 3_840_000, 3_840_000]), + Some(&[&[0, 1, 2, 9], &[3, 4, 6, 7, 8], &[5, 10, 11]]), + None, + ); + } + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +#[allow(unused_variables)] +pub(crate) fn _test_power_button(guest: &Guest) { + let mut cmd = GuestCommand::new(guest); + let api_socket = temp_api_path(&guest.tmp_dir); + + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline() + .capture_output() + .default_disks() + .default_net() + .args(["--api-socket", &api_socket]); + + let child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + assert!(remote_command(&api_socket, "power-button", None)); + }); + + let output = child.wait_with_output().unwrap(); + assert!(output.status.success()); + handle_child_output(r, &output); +} + +pub(crate) fn test_vhost_user_net( + tap: Option<&str>, + num_queues: usize, + prepare_daemon: &PrepareNetDaemon, + generate_host_mac: bool, + client_mode_daemon: bool, +) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let kernel_path = direct_kernel_boot_path(); + + let host_mac = if generate_host_mac { + Some(MacAddr::local_random()) + } else { + None + }; + + let mtu = Some(3000); + + let (mut daemon_command, vunet_socket_path) = prepare_daemon( + &guest.tmp_dir, + &guest.network.host_ip0, + tap, + mtu, + num_queues, + client_mode_daemon, + ); + + let net_params = format!( + "vhost_user=true,mac={},socket={},num_queues={},queue_size=1024{},vhost_mode={},mtu=3000", + guest.network.guest_mac0, + vunet_socket_path, + num_queues, + if let Some(host_mac) = host_mac { + format!(",host_mac={host_mac}") + } else { + String::new() + }, + if client_mode_daemon { + "server" + } else { + "client" + }, + ); + + let mut ch_command = GuestCommand::new(&guest); + ch_command + .args(["--cpus", format!("boot={}", num_queues / 2).as_str()]) + .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &api_socket]) + .capture_output(); + + let mut daemon_child: std::process::Child; + let mut child: std::process::Child; + + if client_mode_daemon { + child = ch_command.spawn().unwrap(); + // Wait for the VMM to create the socket before starting the daemon + assert!(wait_until(Duration::from_secs(10), || Path::new( + &vunet_socket_path + ) + .exists())); + daemon_child = daemon_command.spawn().unwrap(); + } else { + daemon_child = daemon_command.spawn().unwrap(); + // Wait for the daemon to create the socket before starting the VMM + assert!(wait_until(Duration::from_secs(10), || Path::new( + &vunet_socket_path + ) + .exists())); + child = ch_command.spawn().unwrap(); + } + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + if let Some(tap_name) = tap { + let tap_count = exec_host_command_output(&format!("ip link | grep -c {tap_name}")); + assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); + } + + if let Some(host_mac) = tap { + let mac_count = exec_host_command_output(&format!("ip link | grep -c {host_mac}")); + assert_eq!(String::from_utf8_lossy(&mac_count.stdout).trim(), "1"); + } + + #[cfg(target_arch = "aarch64")] + let iface = "enp0s4"; + #[cfg(target_arch = "x86_64")] + let iface = "ens4"; + + assert_eq!( + guest + .ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str()) + .unwrap() + .trim(), + "3000" + ); + + // 1 network interface + default localhost ==> 2 interfaces + // It's important to note that this test is fully exercising the + // vhost-user-net implementation and the associated backend since + // it does not define any --net network interface. That means all + // the ssh communication in that test happens through the network + // interface backed by vhost-user-net. + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 2 + ); + + // The following pci devices will appear on guest with PCI-MSI + // interrupt vectors assigned. + // 1 virtio-console with 3 vectors: config, Rx, Tx + // 1 virtio-blk with 2 vectors: config, Request + // 1 virtio-blk with 2 vectors: config, Request + // 1 virtio-rng with 2 vectors: config, Request + // Since virtio-net has 2 queue pairs, its vectors is as follows: + // 1 virtio-net with 5 vectors: config, Rx (2), Tx (2) + // Based on the above, the total vectors should 14. + let grep_cmd = format!("grep -c {} /proc/interrupts", get_msi_interrupt_pattern()); + + assert_eq!( + guest + .ssh_command(&grep_cmd) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 10 + (num_queues as u32) + ); + + // ACPI feature is needed. + #[cfg(target_arch = "x86_64")] + { + guest.enable_memory_hotplug(); + + // Add RAM to the VM + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); + + // Here by simply checking the size (through ssh), we validate + // the connection is still working, which means vhost-user-net + // keeps working after the resize. + assert!(wait_until(Duration::from_secs(10), || guest + .get_total_memory() + .unwrap_or_default() + > 960_000)); + } + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + + handle_child_output(r, &output); +} + +type PrepareBlkDaemon = dyn Fn(&TempDir, &str, usize, bool, bool) -> (std::process::Child, String); + +pub(crate) fn test_vhost_user_blk( + num_queues: usize, + readonly: bool, + direct: bool, + prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>, +) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let kernel_path = direct_kernel_boot_path(); + + let (blk_params, daemon_child) = { + let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap(); + // Start the daemon + let (daemon_child, vubd_socket_path) = + prepare_daemon(&guest.tmp_dir, "blk.img", num_queues, readonly, direct); + + ( + format!( + "vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128", + ), + Some(daemon_child), + ) + }; + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", format!("boot={num_queues}").as_str()]) + .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + blk_params.as_str(), + ]) + .default_net() + .args(["--api-socket", &api_socket]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check both if /dev/vdc exists and if the block size is 16M. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | grep -c 16M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Check if this block is RO or RW. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | awk '{print $5}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + readonly as u32 + ); + + // Check if the number of queues in /sys/block/vdc/mq matches the + // expected num_queues. + assert_eq!( + guest + .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + num_queues as u32 + ); + + // Mount the device + let mount_ro_rw_flag = if readonly { "ro,noload" } else { "rw" }; + guest.ssh_command("mkdir mount_image").unwrap(); + guest + .ssh_command( + format!("sudo mount -o {mount_ro_rw_flag} -t ext4 /dev/vdc mount_image/").as_str(), + ) + .unwrap(); + + // Check the content of the block device. The file "foo" should + // contain "bar". + assert_eq!( + guest.ssh_command("cat mount_image/foo").unwrap().trim(), + "bar" + ); + + // ACPI feature is needed. + #[cfg(target_arch = "x86_64")] + { + guest.enable_memory_hotplug(); + + // Add RAM to the VM + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); + + assert!(wait_until(Duration::from_secs(10), || guest + .get_total_memory() + .unwrap_or_default() + > 960_000)); + + // Check again the content of the block device after the resize + // has been performed. + assert_eq!( + guest.ssh_command("cat mount_image/foo").unwrap().trim(), + "bar" + ); + } + + // Unmount the device + guest.ssh_command("sudo umount /dev/vdc").unwrap(); + guest.ssh_command("rm -r mount_image").unwrap(); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + if let Some(mut daemon_child) = daemon_child { + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + } + + handle_child_output(r, &output); +} + +pub(crate) fn test_boot_from_vhost_user_blk( + num_queues: usize, + readonly: bool, + direct: bool, + prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>, +) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + + let kernel_path = direct_kernel_boot_path(); + + let disk_path = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); + + let (blk_boot_params, daemon_child) = { + let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap(); + // Start the daemon + let (daemon_child, vubd_socket_path) = prepare_daemon( + &guest.tmp_dir, + disk_path.as_str(), + num_queues, + readonly, + direct, + ); + + ( + format!( + "vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128", + ), + Some(daemon_child), + ) + }; + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", format!("boot={num_queues}").as_str()]) + .args(["--memory", "size=512M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + blk_boot_params.as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Just check the VM booted correctly. + assert_eq!(guest.get_cpu_count().unwrap_or_default(), num_queues as u32); + assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + }); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + if let Some(mut daemon_child) = daemon_child { + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + } + + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_fs( + prepare_daemon: &dyn Fn(&TempDir, &str) -> (std::process::Child, String), + hotplug: bool, + use_generic_vhost_user: bool, + pci_segment: Option, +) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + let event_path = temp_event_monitor_path(&guest.tmp_dir); + + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + + let mut shared_dir = workload_path; + shared_dir.push("shared_dir"); + + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = if hotplug { + edk2_path() + } else { + direct_kernel_boot_path() + }; + + let (mut daemon_child, virtiofsd_socket_path) = + prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); + + let mut guest_command = GuestCommand::new(&guest); + guest_command + .default_cpus() + .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .args(["--api-socket", &api_socket]) + .args(["--event-monitor", format!("path={event_path}").as_str()]); + if pci_segment.is_some() { + guest_command.args([ + "--platform", + &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), + ]); + } + + let fs_params = format!( + "socket={},id=myfs0,{}{}", + virtiofsd_socket_path, + if use_generic_vhost_user { + "queue_sizes=[1024,1024],virtio_id=26" + } else { + "tag=myfs,num_queues=1,queue_size=1024" + }, + if let Some(pci_segment) = pci_segment { + format!(",pci_segment={pci_segment}") + } else { + String::new() + } + ); + + if !hotplug { + guest_command.args([ + if use_generic_vhost_user { + "--generic-vhost-user" + } else { + "--fs" + }, + fs_params.as_str(), + ]); + } + + let mut child = guest_command.capture_output().spawn().unwrap(); + let add_arg = if use_generic_vhost_user { + "add-generic-vhost-user" + } else { + "add-fs" + }; + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + if hotplug { + // Add fs to the VM + let (cmd_success, cmd_output, _) = + remote_command_w_output(&api_socket, add_arg, Some(&fs_params)); + assert!(cmd_success); + + if let Some(pci_segment) = pci_segment { + assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( + "{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" + ))); + } else { + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}") + ); + } + } + + // Mount shared directory through virtio_fs filesystem + guest + .wait_for_ssh_command( + "mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/", + Duration::from_secs(10), + ) + .unwrap(); + + // Check file1 exists and its content is "foo" + assert_eq!( + guest.ssh_command("cat mount_dir/file1").unwrap().trim(), + "foo" + ); + // Check file2 does not exist + guest + .ssh_command("[ ! -f 'mount_dir/file2' ] || true") + .unwrap(); + + // Check file3 exists and its content is "bar" + assert_eq!( + guest.ssh_command("cat mount_dir/file3").unwrap().trim(), + "bar" + ); + + // ACPI feature is needed. + #[cfg(target_arch = "x86_64")] + { + guest.enable_memory_hotplug(); + + // Add RAM to the VM + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); + + assert!(wait_until(Duration::from_secs(30), || guest + .get_total_memory() + .unwrap_or_default() + > 960_000)); + + // After the resize, check again that file1 exists and its + // content is "foo". + assert_eq!( + guest.ssh_command("cat mount_dir/file1").unwrap().trim(), + "foo" + ); + } + + if hotplug { + // Remove from VM + guest.ssh_command("sudo umount mount_dir").unwrap(); + assert!(remote_command(&api_socket, "remove-device", Some("myfs0"))); + + // Wait for the device to be fully removed before re-adding + let removed_event = MetaEvent { + event: "device-removed".to_string(), + device_id: Some("myfs0".to_string()), + }; + assert!(wait_for_sequential_events( + Duration::from_secs(10), + &[&removed_event], + &event_path + )); + } + }); + + let (r, hotplug_daemon_child) = if r.is_ok() && hotplug { + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + // Remove the stale socket so wait_for_virtiofsd_socket actually waits + let _ = std::fs::remove_file(&virtiofsd_socket_path); + + let (daemon_child, virtiofsd_socket_path) = + prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); + + let r = std::panic::catch_unwind(|| { + // Wait for the daemon socket to be ready + assert!(wait_until(Duration::from_secs(10), || Path::new( + &virtiofsd_socket_path + ) + .exists())); + let fs_params = format!( + "id=myfs0,socket={},{}{}", + virtiofsd_socket_path, + if use_generic_vhost_user { + "queue_sizes=[1024,1024],virtio_id=26" + } else { + "tag=myfs,num_queues=1,queue_size=1024" + }, + if let Some(pci_segment) = pci_segment { + format!(",pci_segment={pci_segment}") + } else { + String::new() + } + ); + + // Add back and check it works + let (cmd_success, cmd_output, _) = + remote_command_w_output(&api_socket, add_arg, Some(&fs_params)); + assert!(cmd_success); + if let Some(pci_segment) = pci_segment { + assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( + "{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" + ))); + } else { + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}") + ); + } + + // Mount shared directory through virtio_fs filesystem, retrying + // until the hotplugged device is recognized by the guest + guest + .wait_for_ssh_command( + "mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/", + Duration::from_secs(10), + ) + .unwrap(); + + // Check file1 exists and its content is "foo" + assert_eq!( + guest.ssh_command("cat mount_dir/file1").unwrap().trim(), + "foo" + ); + }); + + (r, Some(daemon_child)) + } else { + (r, None) + }; + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + + if let Some(mut daemon_child) = hotplug_daemon_child { + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + } + + handle_child_output(r, &output); +} + +pub(crate) fn test_virtio_pmem(discard_writes: bool, specify_size: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + + let kernel_path = direct_kernel_boot_path(); + + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .args([ + "--pmem", + format!( + "file={}{}{}", + pmem_temp_file.as_path().to_str().unwrap(), + if specify_size { ",size=128M" } else { "" }, + if discard_writes { + ",discard_writes=on" + } else { + "" + } + ) + .as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check for the presence of /dev/pmem0 + assert_eq!( + guest.ssh_command("ls /dev/pmem0").unwrap().trim(), + "/dev/pmem0" + ); + + // Check changes persist after reboot + assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), ""); + assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n"); + guest + .ssh_command("echo test123 | sudo tee /mnt/test") + .unwrap(); + assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), ""); + assert_eq!(guest.ssh_command("ls /mnt").unwrap(), ""); + + guest.reboot_linux(0); + assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), ""); + assert_eq!( + guest + .ssh_command("sudo cat /mnt/test || true") + .unwrap() + .trim(), + if discard_writes { "" } else { "test123" } + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_vsock(guest: &Guest, hotplug: bool) { + let socket = temp_vsock_path(&guest.tmp_dir); + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut cmd = GuestCommand::new(guest); + cmd.args(["--api-socket", &api_socket]); + cmd.default_cpus(); + cmd.default_memory(); + cmd.default_kernel_cmdline(); + cmd.default_disks(); + cmd.default_net(); + + if !hotplug { + cmd.args(["--vsock", format!("cid=3,socket={socket}").as_str()]); + } + + let mut child = cmd.capture_output().spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + if hotplug { + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-vsock", + Some(format!("cid=3,socket={socket},id=test0").as_str()), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}") + ); + thread::sleep(std::time::Duration::new(10, 0)); + // Check adding a second one fails + assert!(!remote_command( + &api_socket, + "add-vsock", + Some("cid=1234,socket=/tmp/fail") + )); + } + + // Validate vsock works as expected. + guest.check_vsock(socket.as_str()); + guest.reboot_linux(0); + // Validate vsock still works after a reboot. + guest.check_vsock(socket.as_str()); + + if hotplug { + assert!(remote_command(&api_socket, "remove-device", Some("test0"))); + } + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn test_memory_mergeable(mergeable: bool) { + let memory_param = if mergeable { + "mergeable=on" + } else { + "mergeable=off" + }; + + // We assume the number of shared pages in the rest of the system to be constant + let ksm_ps_init = get_ksm_pages_shared(); + + let disk_config1 = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest1 = Guest::new(Box::new(disk_config1)); + let mut child1 = GuestCommand::new(&guest1) + .default_cpus() + .args(["--memory", format!("size=512M,{memory_param}").as_str()]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", guest1.default_net_string().as_str()]) + .args(["--serial", "tty", "--console", "off"]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest1.wait_vm_boot().unwrap(); + }); + if r.is_err() { + kill_child(&mut child1); + let output = child1.wait_with_output().unwrap(); + handle_child_output(r, &output); + panic!("Test should already be failed/panicked"); // To explicitly mark this block never return + } + + let ksm_ps_guest1 = get_ksm_pages_shared(); + + let disk_config2 = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest2 = Guest::new(Box::new(disk_config2)); + let mut child2 = GuestCommand::new(&guest2) + .default_cpus() + .args(["--memory", format!("size=512M,{memory_param}").as_str()]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", guest2.default_net_string().as_str()]) + .args(["--serial", "tty", "--console", "off"]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest2.wait_vm_boot().unwrap(); + let ksm_ps_guest2 = get_ksm_pages_shared(); + + if mergeable { + println!( + "ksm pages_shared after vm1 booted '{ksm_ps_guest1}', ksm pages_shared after vm2 booted '{ksm_ps_guest2}'" + ); + // We are expecting the number of shared pages to increase as the number of VM increases + assert!(ksm_ps_guest1 < ksm_ps_guest2); + } else { + assert!(ksm_ps_guest1 == ksm_ps_init); + assert!(ksm_ps_guest2 == ksm_ps_init); + } + }); + + kill_child(&mut child1); + kill_child(&mut child2); + + let output = child1.wait_with_output().unwrap(); + child2.wait().unwrap(); + + handle_child_output(r, &output); +} + +// This test validates that it can find the virtio-iommu device at first. +// It also verifies that both disks and the network card are attached to +// the virtual IOMMU by looking at /sys/kernel/iommu_groups directory. +// The last interesting part of this test is that it exercises the network +// interface attached to the virtual IOMMU since this is the one used to +// send all commands through SSH. +pub(crate) fn _test_virtio_iommu(_acpi: bool /* not needed on x86_64 */) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = if _acpi { + edk2_path() + } else { + direct_kernel_boot_path() + }; + + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + format!( + "path={},iommu=on", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={},iommu=on", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + ]) + .args(["--net", guest.default_net_string_w_iommu().as_str()]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Verify the virtio-iommu device is present. + assert!( + guest + .does_device_vendor_pair_match("0x1057", "0x1af4") + .unwrap_or_default() + ); + + // On AArch64, if the guest system boots from FDT, the behavior of IOMMU is a bit + // different with ACPI. + // All devices on the PCI bus will be attached to the virtual IOMMU, except the + // virtio-iommu device itself. So these devices will all be added to IOMMU groups, + // and appear under folder '/sys/kernel/iommu_groups/'. + // + // Verify the first disk is in an iommu group. + assert!( + guest + .ssh_command("ls /sys/kernel/iommu_groups/*/devices") + .unwrap() + .contains("0000:00:02.0") + ); + + // Verify the second disk is in an iommu group. + assert!( + guest + .ssh_command("ls /sys/kernel/iommu_groups/*/devices") + .unwrap() + .contains("0000:00:03.0") + ); + + // Verify the network card is in an iommu group. + assert!( + guest + .ssh_command("ls /sys/kernel/iommu_groups/*/devices") + .unwrap() + .contains("0000:00:04.0") + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +// ivshmem test +// This case validates that read data from host(host write data to ivshmem backend file, +// guest read data from ivshmem pci bar2 memory) +// and write data to host(guest write data to ivshmem pci bar2 memory, host read it from +// ivshmem backend file). +// It also checks the size of the shared memory region. +pub(crate) fn _test_ivshmem(guest: &Guest, ivshmem_file_path: impl AsRef, file_size: &str) { + let ivshmem_file_path = ivshmem_file_path.as_ref(); + let test_message_read = String::from("ivshmem device test data read"); + // Modify backend file data before function test + let mut file = OpenOptions::new() + .read(true) + .write(true) + .open(ivshmem_file_path) + .unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + file.write_all(test_message_read.as_bytes()).unwrap(); + file.write_all(b"\0").unwrap(); + file.flush().unwrap(); + + let output = fs::read_to_string(ivshmem_file_path).unwrap(); + let nul_pos = output.as_bytes().iter().position(|&b| b == 0).unwrap(); + let c_str = CStr::from_bytes_until_nul(&output.as_bytes()[..=nul_pos]).unwrap(); + let file_message = c_str.to_string_lossy().to_string(); + // Check if the backend file data is correct + assert_eq!(test_message_read, file_message); + + let device_id_line = String::from( + guest + .ssh_command("lspci -D | grep \"Inter-VM shared memory\"") + .unwrap() + .trim(), + ); + // Check if ivshmem exists + assert!(!device_id_line.is_empty()); + let device_id = device_id_line.split(" ").next().unwrap(); + // Check shard memory size + assert_eq!( + guest + .ssh_command( + format!("lspci -vv -s {device_id} | grep -c \"Region 2.*size={file_size}\"") + .as_str(), + ) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // guest don't have gcc or g++, try to use python to test :( + // This python program try to mmap the ivshmem pci bar2 memory and read the data from it. + let ivshmem_test_read = format!( + r#" +import os +import mmap +from ctypes import create_string_buffer, c_char, memmove + +if __name__ == "__main__": + device_path = f"/sys/bus/pci/devices/{device_id}/resource2" + fd = os.open(device_path, os.O_RDWR | os.O_SYNC) + + PAGE_SIZE = os.sysconf('SC_PAGESIZE') + + with mmap.mmap(fd, PAGE_SIZE, flags=mmap.MAP_SHARED, + prot=mmap.PROT_READ | mmap.PROT_WRITE, offset=0) as shmem: + c_buf = (c_char * PAGE_SIZE).from_buffer(shmem) + null_pos = c_buf.raw.find(b'\x00') + valid_data = c_buf.raw[:null_pos] if null_pos != -1 else c_buf.raw + print(valid_data.decode('utf-8', errors='replace'), end="") + shmem.flush() + del c_buf + + os.close(fd) + "# + ); + guest + .ssh_command( + format!( + r#"cat << EOF > test_read.py +{ivshmem_test_read} +EOF +"# + ) + .as_str(), + ) + .unwrap(); + let guest_message = guest.ssh_command("sudo python3 test_read.py").unwrap(); + + // Check the probe message in host and guest + assert_eq!(test_message_read, guest_message); + + let test_message_write = "ivshmem device test data write"; + // Then the program writes a test message to the memory and flush it. + let ivshmem_test_write = format!( + r#" +import os +import mmap +from ctypes import create_string_buffer, c_char, memmove + +if __name__ == "__main__": + device_path = f"/sys/bus/pci/devices/{device_id}/resource2" + test_message = "{test_message_write}" + fd = os.open(device_path, os.O_RDWR | os.O_SYNC) + + PAGE_SIZE = os.sysconf('SC_PAGESIZE') + + with mmap.mmap(fd, PAGE_SIZE, flags=mmap.MAP_SHARED, + prot=mmap.PROT_READ | mmap.PROT_WRITE, offset=0) as shmem: + shmem.flush() + c_buf = (c_char * PAGE_SIZE).from_buffer(shmem) + encoded_msg = test_message.encode('utf-8').ljust(1000, b'\x00') + memmove(c_buf, encoded_msg, len(encoded_msg)) + shmem.flush() + del c_buf + + os.close(fd) + "# + ); + + guest + .ssh_command( + format!( + r#"cat << EOF > test_write.py +{ivshmem_test_write} +EOF +"# + ) + .as_str(), + ) + .unwrap(); + + let _ = guest.ssh_command("sudo python3 test_write.py").unwrap(); + + let output = fs::read_to_string(ivshmem_file_path).unwrap(); + let nul_pos = output.as_bytes().iter().position(|&b| b == 0).unwrap(); + let c_str = CStr::from_bytes_until_nul(&output.as_bytes()[..=nul_pos]).unwrap(); + let file_message = c_str.to_string_lossy().to_string(); + // Check to send data from guest to host + assert_eq!(test_message_write, file_message); +} + +pub(crate) fn _test_simple_launch(guest: &Guest) { + let event_path = temp_event_monitor_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .args(["--serial", "tty", "--console", "off"]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + guest.validate_cpu_count(None); + guest.validate_memory(None); + assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000"); + assert!(check_sequential_events( + &guest + .get_expected_seq_events_for_simple_launch() + .iter() + .collect::>(), + &event_path + )); + + // It's been observed on the Bionic image that udev and snapd + // services can cause some delay in the VM's shutdown. Disabling + // them improves the reliability of this test. + let _ = guest.ssh_command("sudo systemctl disable udev"); + let _ = guest.ssh_command("sudo systemctl stop udev"); + let _ = guest.ssh_command("sudo systemctl disable snapd"); + let _ = guest.ssh_command("sudo systemctl stop snapd"); + + guest.ssh_command("sudo poweroff").unwrap(); + let latest_events = [ + &MetaEvent { + event: "shutdown".to_string(), + device_id: None, + }, + &MetaEvent { + event: "deleted".to_string(), + device_id: None, + }, + &MetaEvent { + event: "shutdown".to_string(), + device_id: None, + }, + ]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(20), + &latest_events, + &event_path + )); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_multi_cpu(guest: &Guest) { + let mut cmd = GuestCommand::new(guest); + cmd.args(["--cpus", "boot=2,max=4"]) + .default_memory() + .default_kernel_cmdline() + .capture_output() + .default_disks() + .default_net(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); + + assert_eq!( + guest + .ssh_command(r#"sudo dmesg | grep "smp: Brought up" | sed "s/\[\ *[0-9.]*\] //""#) + .unwrap() + .trim(), + "smp: Brought up 1 node, 2 CPUs" + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_cpu_affinity(guest: &Guest) { + // We need the host to have at least 4 CPUs if we want to be able + // to run this test. + let host_cpus_count = exec_host_command_output("nproc"); + assert!( + String::from_utf8_lossy(&host_cpus_count.stdout) + .trim() + .parse::() + .unwrap_or(0) + >= 4 + ); + + let mut child = GuestCommand::new(guest) + .default_cpus_with_affinity() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + let pid = child.id(); + let taskset_vcpu0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); + assert_eq!(String::from_utf8_lossy(&taskset_vcpu0.stdout).trim(), "0,2"); + let taskset_vcpu1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); + assert_eq!(String::from_utf8_lossy(&taskset_vcpu1.stdout).trim(), "1,3"); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_queue_affinity(guest: &Guest) { + // We need the host to have at least 4 CPUs if we want to be able + // to run this test. + let host_cpus_count = exec_host_command_output("nproc"); + assert!( + String::from_utf8_lossy(&host_cpus_count.stdout) + .trim() + .parse::() + .unwrap_or(0) + >= 4 + ); + + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={},num_queues=4,queue_affinity=[0@[0,2],1@[1,3],2@[1],3@[3]]", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + let pid = child.id(); + let taskset_q0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); + assert_eq!(String::from_utf8_lossy(&taskset_q0.stdout).trim(), "0,2"); + let taskset_q1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); + assert_eq!(String::from_utf8_lossy(&taskset_q1.stdout).trim(), "1,3"); + let taskset_q2 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q2 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); + assert_eq!(String::from_utf8_lossy(&taskset_q2.stdout).trim(), "1"); + let taskset_q3 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q3 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); + assert_eq!(String::from_utf8_lossy(&taskset_q3.stdout).trim(), "3"); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); +} + +pub(crate) fn _test_pci_msi(guest: &Guest) { + let mut cmd = GuestCommand::new(guest); + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline() + .capture_output() + .default_disks() + .default_net(); + + let mut child = cmd.spawn().unwrap(); + + guest.wait_vm_boot().unwrap(); + + let grep_cmd = format!("grep -c {} /proc/interrupts", get_msi_interrupt_pattern()); + + let r = std::panic::catch_unwind(|| { + assert_eq!( + guest + .ssh_command(&grep_cmd) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 12 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_net_ctrl_queue(guest: &Guest) { + let mut cmd = GuestCommand::new(guest); + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline() + .args(["--net", guest.default_net_string_w_mtu(3000).as_str()]) + .capture_output() + .default_disks(); + + let mut child = cmd.spawn().unwrap(); + + guest.wait_vm_boot().unwrap(); + + #[cfg(target_arch = "aarch64")] + let iface = "enp0s4"; + #[cfg(target_arch = "x86_64")] + let iface = "ens4"; + + let r = std::panic::catch_unwind(|| { + assert_eq!( + guest + .ssh_command( + format!("sudo ethtool -K {iface} rx-gro-hw off && echo success").as_str() + ) + .unwrap() + .trim(), + "success" + ); + assert_eq!( + guest + .ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str()) + .unwrap() + .trim(), + "3000" + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_pci_multiple_segments( + guest: &Guest, + max_num_pci_segments: u16, + pci_segments_for_disk: u16, +) { + // Prepare another disk file for the virtio-disk device + let test_disk_path = String::from( + guest + .tmp_dir + .as_path() + .join("test-disk.raw") + .to_str() + .unwrap(), + ); + assert!( + exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() + ); + assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); + + let mut cmd = GuestCommand::new(guest); + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline_with_platform(Some(&format!( + "num_pci_segments={max_num_pci_segments}" + ))) + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={test_disk_path},pci_segment={pci_segments_for_disk},image_type=raw") + .as_str(), + ]) + .capture_output() + .default_net(); + + let mut child = cmd.spawn().unwrap(); + + guest.wait_vm_boot().unwrap(); + + let grep_cmd = "lspci | grep \"Host bridge\" | wc -l"; + + let r = std::panic::catch_unwind(|| { + // There should be MAX_NUM_PCI_SEGMENTS PCI host bridges in the guest. + assert_eq!( + guest + .ssh_command(grep_cmd) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + max_num_pci_segments + ); + + // Check both if /dev/vdc exists and if the block size is 4M. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | grep -c 4M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Mount the device. + guest.ssh_command("mkdir mount_image").unwrap(); + guest + .ssh_command("sudo mount -o rw -t ext4 /dev/vdc mount_image/") + .unwrap(); + // Grant all users with write permission. + guest.ssh_command("sudo chmod a+w mount_image/").unwrap(); + + // Write something to the device. + guest + .ssh_command("sudo echo \"bar\" >> mount_image/foo") + .unwrap(); + + // Check the content of the block device. The file "foo" should + // contain "bar". + assert_eq!( + guest + .ssh_command("sudo cat mount_image/foo") + .unwrap() + .trim(), + "bar" + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_direct_kernel_boot(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + guest.validate_cpu_count(None); + guest.validate_memory(None); + + let grep_cmd = format!("grep -c {} /proc/interrupts", get_msi_interrupt_pattern()); + assert_eq!( + guest + .ssh_command(&grep_cmd) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 12 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_block( + guest: &Guest, + disable_io_uring: bool, + disable_aio: bool, + verify_os_disk: bool, + backing_files: bool, + image_type: ImageType, +) { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + + let mut blk_file_path = workload_path; + blk_file_path.push("blk.img"); + + let initial_backing_checksum = if verify_os_disk { + compute_backing_checksum(guest.disk_config.disk(DiskType::OperatingSystem).unwrap()) + } else { + None + }; + assert!( + guest.num_cpu >= 4, + "_test_virtio_block requires at least 4 CPUs to match num_queues=4" + ); + let mut cloud_child = GuestCommand::new(guest) + .default_cpus() + .args(["--memory", "size=512M,shared=on"]) + .default_kernel_cmdline() + .args([ + "--disk", + format!( + "path={},backing_files={},image_type={image_type}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), + if backing_files { "on" } else { "off" }, + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!( + "path={},readonly=on,direct=on,num_queues=4,_disable_io_uring={},_disable_aio={}", + blk_file_path.to_str().unwrap(), + disable_io_uring, + disable_aio, + ) + .as_str(), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check both if /dev/vdc exists and if the block size is 16M. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | grep -c 16M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Check both if /dev/vdc exists and if this block is RO. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | awk '{print $5}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Check if the number of queues is 4. + assert_eq!( + guest + .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4 + ); + }); + + if verify_os_disk { + // Use clean shutdown to allow cloud-hypervisor to clear + // the dirty bit in the QCOW2 v3 image. + kill_child(&mut cloud_child); + } else { + let _ = cloud_child.kill(); + } + let output = cloud_child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + + if verify_os_disk { + disk_check_consistency( + guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), + initial_backing_checksum, + ); + } +} + +pub fn _test_virtio_block_dynamic_vhdx_expand(guest: &Guest) { + const VIRTUAL_DISK_SIZE: u64 = 100 << 20; + const EMPTY_VHDX_FILE_SIZE: u64 = 8 << 20; + const FULL_VHDX_FILE_SIZE: u64 = 112 << 20; + const DYNAMIC_VHDX_NAME: &str = "dynamic.vhdx"; + + let vhdx_pathbuf = guest.tmp_dir.as_path().join(DYNAMIC_VHDX_NAME); + let vhdx_path = vhdx_pathbuf.to_str().unwrap(); + + // Generate a 100 MiB dynamic VHDX file + std::process::Command::new("qemu-img") + .arg("create") + .args(["-f", "vhdx"]) + .arg(vhdx_path) + .arg(VIRTUAL_DISK_SIZE.to_string()) + .output() + .expect("Expect generating dynamic VHDX image"); + + // Check if the size matches with empty VHDx file size + assert_eq!(vhdx_image_size(vhdx_path), EMPTY_VHDX_FILE_SIZE); + + let mut cloud_child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={vhdx_path}").as_str(), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check both if /dev/vdc exists and if the block size is 100 MiB. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | grep -c 100M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Write 100 MB of data to the VHDx disk + guest + .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=100") + .unwrap(); + }); + + // Check if the size matches with expected expanded VHDx file size + assert_eq!(vhdx_image_size(vhdx_path), FULL_VHDX_FILE_SIZE); + + kill_child(&mut cloud_child); + let output = cloud_child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + + disk_check_consistency(vhdx_path, None); +} + +fn vhdx_image_size(disk_name: &str) -> u64 { + std::fs::File::open(disk_name) + .unwrap() + .seek(SeekFrom::End(0)) + .unwrap() +} + +#[cfg(target_arch = "x86_64")] +pub fn _test_split_irqchip(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("grep -c IO-APIC.*timer /proc/interrupts || true") + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); + assert_eq!( + guest + .ssh_command("grep -c IO-APIC.*cascade /proc/interrupts || true") + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +#[cfg(target_arch = "x86_64")] +pub(crate) fn _test_dmi_serial_number(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline_with_platform(Some("system_serial_number=a=b;c=d")) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("sudo cat /sys/class/dmi/id/product_serial") + .unwrap() + .trim(), + "a=b;c=d" + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_dmi_uuid(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline_with_platform(Some( + "system_uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a", + )) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("sudo cat /sys/class/dmi/id/product_uuid") + .unwrap() + .trim(), + "1e8aa28a-435d-4027-87f4-40dceff1fa0a" + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_dmi_oem_strings(guest: &Guest) { + let s1 = "io.systemd.credential:xx=yy"; + let s2 = "This is a test string"; + + let oem_strings = format!("oem_strings=[{s1},{s2}]"); + + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline_with_platform(Some(&oem_strings)) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("sudo dmidecode --oem-string count") + .unwrap() + .trim(), + "2" + ); + + assert_eq!( + guest + .ssh_command("sudo dmidecode --oem-string 1") + .unwrap() + .trim(), + s1 + ); + + assert_eq!( + guest + .ssh_command("sudo dmidecode --oem-string 2") + .unwrap() + .trim(), + s2 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +#[cfg(target_arch = "x86_64")] +pub(crate) fn _test_dmi_system_and_chassis(guest: &Guest) { + let fields = [ + ("system_manufacturer", "system-manufacturer", "Manufacturer"), + ("system_product_name", "system-product-name", "ProductName"), + ("system_version", "system-version", "Version"), + ("system_family", "system-family", "Family"), + ("system_sku_number", "system-sku-number", "SkuNumber"), + ("chassis_asset_tag", "chassis-asset-tag", "AssetTag"), + ]; + + let platform = fields + .iter() + .map(|(key, _, value)| format!("{key}={value}")) + .collect::>() + .join(","); + + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline_with_platform(Some(&platform)) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + for (_, dmidecode_field, expected) in fields { + assert_eq!( + guest + .ssh_command(&format!("sudo dmidecode -s {dmidecode_field}")) + .unwrap() + .trim(), + expected, + "DMI field {dmidecode_field} mismatch" + ); + } + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_serial_off(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .args(["--serial", "off"]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Test that there is no ttyS0 + assert_eq!( + guest + .ssh_command(GREP_SERIAL_IRQ_CMD) + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_multiple_network_interfaces(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args([ + "--net", + guest.default_net_string().as_str(), + "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.128", + "tap=mytap1,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.128", + ]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + let tap_count = exec_host_command_output("ip link | grep -c mytap1"); + assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); + + // 3 network interfaces + default localhost ==> 4 interfaces + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_console(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .args(["--console", "tty"]) + .args(["--serial", "null"]) + .capture_output() + .spawn() + .unwrap(); + + let text = String::from("On a branch floating down river a cricket, singing."); + let cmd = format!("echo {text} | sudo tee /dev/hvc0"); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert!( + guest + .does_device_vendor_pair_match("0x1043", "0x1af4") + .unwrap_or_default() + ); + + guest.ssh_command(&cmd).unwrap(); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&output.stdout).contains(&text)); + }); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_console_file(guest: &Guest) { + let console_path = guest.tmp_dir.as_path().join("console-output"); + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .args([ + "--console", + format!("file={}", console_path.to_str().unwrap()).as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); + + guest.wait_vm_boot().unwrap(); + + guest.ssh_command("sudo shutdown -h now").unwrap(); + + let _ = child.wait_timeout(std::time::Duration::from_secs(20)); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + let r = std::panic::catch_unwind(|| { + // Check that the cloud-hypervisor binary actually terminated + assert!(output.status.success()); + + // Do this check after shutdown of the VM as an easy way to ensure + // all writes are flushed to disk + let mut f = std::fs::File::open(console_path).unwrap(); + let mut buf = String::new(); + f.read_to_string(&mut buf).unwrap(); + + if !buf.contains(CONSOLE_TEST_STRING) { + eprintln!( + "\n\n==== Console file output ====\n\n{buf}\n\n==== End console file output ====" + ); + } + assert!(buf.contains(CONSOLE_TEST_STRING)); + }); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_direct_kernel_boot_noacpi(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); + assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_pci_bar_reprogramming(guest: &Guest) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args([ + "--net", + guest.default_net_string().as_str(), + "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.128", + ]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // 2 network interfaces + default localhost ==> 3 interfaces + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 3 + ); + + let init_bar_addr = guest + .ssh_command("sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource") + .unwrap(); + + // Remove the PCI device + guest + .ssh_command("echo 1 | sudo tee /sys/bus/pci/devices/0000:00:05.0/remove") + .unwrap(); + + // Only 1 network interface left + default localhost ==> 2 interfaces + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 2 + ); + + // Remove the PCI device + guest + .ssh_command("echo 1 | sudo tee /sys/bus/pci/rescan") + .unwrap(); + + // Back to 2 network interface + default localhost ==> 3 interfaces + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 3 + ); + + let new_bar_addr = guest + .ssh_command("sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource") + .unwrap(); + + // Let's compare the BAR addresses for our virtio-net device. + // They should be different as we expect the BAR reprogramming + // to have happened. + assert_ne!(init_bar_addr, new_bar_addr); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_memory_overhead(guest: &Guest, guest_memory_size_kb: u32) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_net() + .default_disks() + .capture_output() + .spawn() + .unwrap(); + + guest.wait_vm_boot().unwrap(); + + let r = std::panic::catch_unwind(|| { + let overhead = get_vmm_overhead(child.id(), guest_memory_size_kb); + eprintln!("Guest memory overhead: {overhead} vs {MAXIMUM_VMM_OVERHEAD_KB}"); + assert!(overhead <= MAXIMUM_VMM_OVERHEAD_KB); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_landlock(guest: &Guest) { + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(guest) + .args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .args(["--landlock"]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check /dev/vdc is not there + assert_eq!( + guest + .ssh_command("lsblk | grep -c vdc.*16M || true") + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); + + // Now let's add the extra disk. + let mut blk_file_path = dirs::home_dir().unwrap(); + blk_file_path.push("workloads"); + blk_file_path.push("blk.img"); + // As the path to the hotplug disk is not pre-added, this remote + // command will fail. + assert!(!remote_command( + &api_socket, + "add-disk", + Some( + format!( + "path={},id=test0,readonly=true", + blk_file_path.to_str().unwrap() + ) + .as_str() + ), + )); + }); + + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_disk_hotplug(guest: &Guest, landlock_enabled: bool) { + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut blk_file_path = dirs::home_dir().unwrap(); + blk_file_path.push("workloads"); + blk_file_path.push("blk.img"); + + let mut cmd = GuestCommand::new(guest); + if landlock_enabled { + cmd.args(["--landlock"]).args([ + "--landlock-rules", + format!("path={blk_file_path:?},access=rw").as_str(), + ]); + } + + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .default_net() + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check /dev/vdc is not there + assert_eq!( + guest + .ssh_command("lsblk | grep -c vdc.*16M || true") + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); + + // Now let's add the extra disk. + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some( + format!( + "path={},id=test0,readonly=true", + blk_file_path.to_str().unwrap() + ) + .as_str(), + ), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}") + ); + + // Wait for the hotplugged disk to appear in the guest + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command("lsblk | grep vdc | grep -c 16M") + .is_ok_and(|s| s.trim().parse::().unwrap_or_default() == 1) + })); + // And check the block device can be read. + guest + .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16") + .unwrap(); + + // Let's remove it the extra disk. + assert!(remote_command(&api_socket, "remove-device", Some("test0"))); + // Wait for the disk to disappear + assert!(wait_until(Duration::from_secs(10), || guest + .ssh_command("lsblk | grep -c vdc.*16M || true") + .is_ok_and(|s| s.trim().parse::().unwrap_or(1) == 0))); + + // And add it back to validate unplug did work correctly. + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some( + format!( + "path={},id=test0,readonly=true", + blk_file_path.to_str().unwrap() + ) + .as_str(), + ), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}") + ); + + // Wait for the hotplugged disk to appear in the guest + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command("lsblk | grep vdc | grep -c 16M") + .is_ok_and(|s| s.trim().parse::().unwrap_or_default() == 1) + })); + // And check the block device can be read. + guest + .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16") + .unwrap(); + + // Reboot the VM. + guest.reboot_linux(0); + + // Check still there after reboot + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | grep -c 16M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + assert!(remote_command(&api_socket, "remove-device", Some("test0"))); + + // Wait for the disk to disappear + assert!(wait_until(Duration::from_secs(20), || guest + .ssh_command("lsblk | grep -c vdc.*16M || true") + .is_ok_and(|s| s.trim().parse::().unwrap_or(1) == 0))); + + guest.reboot_linux(1); + + // Check device still absent + assert_eq!( + guest + .ssh_command("lsblk | grep -c vdc.*16M || true") + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_virtio_block_topology(guest: &Guest, loop_dev: &str) { + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={loop_dev}").as_str(), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // MIN-IO column + assert_eq!( + guest + .ssh_command("lsblk -t| grep vdc | awk '{print $3}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4096 + ); + // PHY-SEC column + assert_eq!( + guest + .ssh_command("lsblk -t| grep vdc | awk '{print $5}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4096 + ); + // LOG-SEC column + assert_eq!( + guest + .ssh_command("lsblk -t| grep vdc | awk '{print $6}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4096 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_net_hotplug( + guest: &Guest, + max_num_pci_segments: u16, + pci_segment: Option, +) { + let api_socket = temp_api_path(&guest.tmp_dir); + + // Boot without network + let mut cmd = GuestCommand::new(guest); + + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() + .default_net() + .default_disks() + .capture_output(); + + if pci_segment.is_some() { + cmd.default_kernel_cmdline_with_platform(Some(&format!( + "num_pci_segments={max_num_pci_segments}" + ))); + } else { + cmd.default_kernel_cmdline(); + } + + let mut child = cmd.spawn().unwrap(); + + guest.wait_vm_boot().unwrap(); + + let r = std::panic::catch_unwind(|| { + // Add network + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test0,tap=,mac={},ip={},mask=255.255.255.128{}", + guest.network.guest_mac1, + guest.network.host_ip1, + if let Some(pci_segment) = pci_segment { + format!(",pci_segment={pci_segment}") + } else { + String::new() + } + ) + .as_str(), + ), + ); + assert!(cmd_success); + + if let Some(pci_segment) = pci_segment { + assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( + "{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" + ))); + } else { + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}") + ); + } + + // Wait for the hotplugged network interface to appear + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command("ip -o link | wc -l") + .is_ok_and(|s| s.trim().parse::().unwrap_or_default() == 3) + })); + + // Test the same using the added network interface's IP + assert_eq!( + ssh_command_ip( + "ip -o link | wc -l", + &guest.network.guest_ip1, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT + ) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 3 + ); + + // Remove network and wait for it to disappear + assert!(remote_command(&api_socket, "remove-device", Some("test0"),)); + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command("ip -o link | wc -l") + .is_ok_and(|s| s.trim().parse::().unwrap_or_default() == 2) + })); + + // Add network + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test1,tap=,mac={},ip={},mask=255.255.255.128{}", + guest.network.guest_mac1, + guest.network.host_ip1, + if let Some(pci_segment) = pci_segment { + format!(",pci_segment={pci_segment}") + } else { + String::new() + } + ) + .as_str(), + ), + ); + assert!(cmd_success); + + if let Some(pci_segment) = pci_segment { + assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( + "{{\"id\":\"test1\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" + ))); + } else { + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test1\",\"bdf\":\"0000:00:06.0\"}") + ); + } + + // Wait for the hotplugged network interface to appear + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command("ip -o link | wc -l") + .is_ok_and(|s| s.trim().parse::().unwrap_or_default() == 3) + })); + + guest.reboot_linux(0); + + // 2 network interfaces + default localhost ==> 3 interfaces + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 3 + ); + + // Test the same using the added network interface's IP + assert_eq!( + ssh_command_ip( + "ip -o link | wc -l", + &guest.network.guest_ip1, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT + ) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 3 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_counters(guest: &Guest) { + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut cmd = GuestCommand::new(guest); + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args(["--net", guest.default_net_string().as_str()]) + .args(["--api-socket", &api_socket]) + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + let orig_counters = get_counters(&api_socket); + guest + .ssh_command("dd if=/dev/zero of=test count=8 bs=1M") + .unwrap(); + + let new_counters = get_counters(&api_socket); + + // Check that all the counters have increased + assert!(new_counters > orig_counters); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_watchdog(guest: &Guest) { + let api_socket = temp_api_path(&guest.tmp_dir); + let event_path = temp_event_monitor_path(&guest.tmp_dir); + + let mut cmd = GuestCommand::new(guest); + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args(["--net", guest.default_net_string().as_str()]) + .args(["--watchdog"]) + .args(["--api-socket", &api_socket]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + let mut expected_reboot_count = 1; + + // Enable the watchdog with a 15s timeout + enable_guest_watchdog(guest, 15); + + assert_eq!(get_reboot_count(guest), expected_reboot_count); + assert_eq!( + guest + .ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Allow some normal time to elapse to check we don't get spurious reboots + thread::sleep(std::time::Duration::new(40, 0)); + // Check no reboot + assert_eq!(get_reboot_count(guest), expected_reboot_count); + + // Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns. + guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); + // Allow some time for the watchdog to trigger (max 30s) and reboot to happen + guest.wait_vm_boot_custom_timeout(120).unwrap(); + // Check a reboot is triggered by the watchdog + expected_reboot_count += 1; + assert_eq!(get_reboot_count(guest), expected_reboot_count); + + #[cfg(target_arch = "x86_64")] + { + // Now pause the VM and remain offline for 30s + assert!(remote_command(&api_socket, "pause", None)); + let latest_events = [ + &MetaEvent { + event: "pausing".to_string(), + device_id: None, + }, + &MetaEvent { + event: "paused".to_string(), + device_id: None, + }, + ]; + assert!(check_latest_events_exact(&latest_events, &event_path)); + assert!(remote_command(&api_socket, "resume", None)); + + // Check no reboot + assert_eq!(get_reboot_count(guest), expected_reboot_count); + } + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_pvpanic(guest: &Guest) { + let api_socket = temp_api_path(&guest.tmp_dir); + let event_path = temp_event_monitor_path(&guest.tmp_dir); + + let mut cmd = GuestCommand::new(guest); + cmd.default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args(["--net", guest.default_net_string().as_str()]) + .args(["--pvpanic"]) + .args(["--api-socket", &api_socket]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Trigger guest a panic + make_guest_panic(guest); + + // Wait for the panic event to be recorded + let expected_sequential_events = [&MetaEvent { + event: "panic".to_string(), + device_id: None, + }]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(10), + &expected_sequential_events, + &event_path + )); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_tap_from_fd(guest: &Guest) { + // Create a TAP interface with multi-queue enabled + let num_queue_pairs: usize = 2; + + use std::str::FromStr; + let taps = net_util::open_tap( + Some("chtap0"), + Some(std::net::IpAddr::V4( + std::net::Ipv4Addr::from_str(&guest.network.host_ip0).unwrap(), + )), + None, + &mut None, + None, + num_queue_pairs, + Some(libc::O_RDWR | libc::O_NONBLOCK), + ) + .unwrap(); + + let mut child = GuestCommand::new(guest) + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args([ + "--net", + &format!( + "fd=[{},{}],mac={},num_queues={}", + taps[0].as_raw_fd(), + taps[1].as_raw_fd(), + guest.network.guest_mac0, + num_queue_pairs * 2 + ), + ]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 2 + ); + + guest.reboot_linux(0); + + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 2 + ); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +// test creates two macvtap interfaces in 'bridge' mode on the +// same physical net interface, one for the guest and one for +// the host. With additional setup on the IP address and the +// routing table, it enables the communications between the +// guest VM and the host machine. +// Details: https://wiki.libvirt.org/page/TroubleshootMacvtapHostFail +pub(crate) fn _test_macvtap( + guest: &Guest, + hotplug: bool, + guest_macvtap_name: &str, + host_macvtap_name: &str, +) { + let api_socket = temp_api_path(&guest.tmp_dir); + + let phy_net = "eth0"; + + // Clean up any stale macvtap interfaces from previous test runs + exec_host_command_status(&format!( + "sudo ip link del {guest_macvtap_name} 2>/dev/null" + )); + exec_host_command_status(&format!("sudo ip link del {host_macvtap_name} 2>/dev/null")); + + // Create a macvtap interface for the guest VM to use + assert!( + exec_host_command_status(&format!( + "sudo ip link add link {phy_net} name {guest_macvtap_name} type macvtap mod bridge" + )) + .success() + ); + assert!( + exec_host_command_status(&format!( + "sudo ip link set {} address {} up", + guest_macvtap_name, guest.network.guest_mac0 + )) + .success() + ); + assert!(exec_host_command_status(&format!("sudo ip link show {guest_macvtap_name}")).success()); + + let tap_index = + fs::read_to_string(format!("/sys/class/net/{guest_macvtap_name}/ifindex")).unwrap(); + let tap_device = format!("/dev/tap{}", tap_index.trim()); + + assert!(exec_host_command_status(&format!("sudo chown $UID.$UID {tap_device}")).success()); + + let cstr_tap_device = CString::new(tap_device).unwrap(); + let tap_fd1 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) }; + assert!(tap_fd1 > 0); + let tap_fd2 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) }; + assert!(tap_fd2 > 0); + + // Create a macvtap on the same physical net interface for + // the host machine to use + assert!( + exec_host_command_status(&format!( + "sudo ip link add link {phy_net} name {host_macvtap_name} type macvtap mod bridge" + )) + .success() + ); + // Use default mask "255.255.255.0" + assert!( + exec_host_command_status(&format!( + "sudo ip address add {}/24 dev {}", + guest.network.host_ip0, host_macvtap_name + )) + .success() + ); + assert!( + exec_host_command_status(&format!("sudo ip link set dev {host_macvtap_name} up")).success() + ); + + let mut guest_command = GuestCommand::new(guest); + guest_command + .default_cpus() + .default_memory() + .default_kernel_cmdline() + .default_disks() + .args(["--api-socket", &api_socket]); + + let net_params = format!( + "fd=[{},{}],mac={},num_queues=4", + tap_fd1, tap_fd2, guest.network.guest_mac0 + ); + + if !hotplug { + guest_command.args(["--net", &net_params]); + } + + let mut child = guest_command.capture_output().spawn().unwrap(); + + if hotplug { + // Wait for the VMM process to listen to the API socket + assert!(wait_until(Duration::from_secs(10), || remote_command( + &api_socket, + "ping", + None + ))); + // Hotplug the virtio-net device + let (cmd_success, cmd_output, _) = + remote_command_w_output(&api_socket, "add-net", Some(&net_params)); + assert!(cmd_success); + #[cfg(target_arch = "x86_64")] + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"_net2\",\"bdf\":\"0000:00:05.0\"}") + ); + #[cfg(target_arch = "aarch64")] + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"_net0\",\"bdf\":\"0000:00:05.0\"}") + ); + } + + // The functional connectivity provided by the virtio-net device + // gets tested through wait_vm_boot() as it expects to receive a + // HTTP request, and through the SSH command as well. + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 2 + ); + + guest.reboot_linux(0); + + assert_eq!( + guest + .ssh_command("ip -o link | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 2 + ); + }); + + kill_child(&mut child); + + exec_host_command_status(&format!("sudo ip link del {guest_macvtap_name}")); + exec_host_command_status(&format!("sudo ip link del {host_macvtap_name}")); + + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} + +pub(crate) fn _test_vdpa_block(guest: &Guest) { + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(guest) + .default_cpus() + .args(["--memory", "size=512M,hugepages=on"]) + .default_kernel_cmdline_with_platform(Some("num_pci_segments=2,iommu_segments=1")) + .default_disks() + .default_net() + .args(["--vdpa", "path=/dev/vhost-vdpa-0,num_queues=1"]) + .args(["--api-socket", &api_socket]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Check both if /dev/vdc exists and if the block size is 128M. + assert_eq!( + guest + .ssh_command("lsblk | grep vdc | grep -c 128M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Check the content of the block device after we wrote to it. + // The vpda-sim-blk should let us read what we previously wrote. + guest + .ssh_command("sudo bash -c 'echo foobar > /dev/vdc'") + .unwrap(); + assert_eq!( + guest.ssh_command("sudo head -1 /dev/vdc").unwrap().trim(), + "foobar" + ); + + // Hotplug an extra vDPA block device behind the vIOMMU + // Add a new vDPA device to the VM + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-vdpa", + Some("id=myvdpa0,path=/dev/vhost-vdpa-1,num_queues=1,pci_segment=1,iommu=on"), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"myvdpa0\",\"bdf\":\"0001:00:01.0\"}") + ); + + // Wait for the hotplugged device to appear + assert!(wait_until(Duration::from_secs(10), || guest + .does_device_vendor_pair_match("0x1057", "0x1af4") + .unwrap_or_default())); + assert!( + guest + .ssh_command("ls /sys/kernel/iommu_groups/*/devices") + .unwrap() + .contains("0001:00:01.0") + ); + + // Check both if /dev/vdd exists and if the block size is 128M. + assert_eq!( + guest + .ssh_command("lsblk | grep vdd | grep -c 128M") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + // Write some content to the block device we've just plugged. + guest + .ssh_command("sudo bash -c 'echo foobar > /dev/vdd'") + .unwrap(); + + // Check we can read the content back. + assert_eq!( + guest.ssh_command("sudo head -1 /dev/vdd").unwrap().trim(), + "foobar" + ); + + // Unplug the device + let cmd_success = remote_command(&api_socket, "remove-device", Some("myvdpa0")); + assert!(cmd_success); + + // Wait for the device to disappear + assert!(wait_until(Duration::from_secs(10), || guest + .ssh_command("lsblk | grep -c vdd || true") + .is_ok_and(|s| s.trim().parse::().unwrap_or(1) == 0))); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); +} diff --git a/cloud-hypervisor/tests/common/utils.rs b/cloud-hypervisor/tests/common/utils.rs new file mode 100644 index 0000000000..5e2967c7a4 --- /dev/null +++ b/cloud-hypervisor/tests/common/utils.rs @@ -0,0 +1,1290 @@ +// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; +use std::fs::{File, OpenOptions}; +use std::io::{BufRead, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +#[cfg(not(feature = "mshv"))] +use std::process::Stdio; +use std::process::{Child, Command}; +use std::string::String; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::time::{Duration, Instant}; +use std::{cmp, fs, io, thread}; + +use test_infra::*; +use vmm_sys_util::tempdir::TempDir; +#[cfg(not(feature = "mshv"))] +use wait_timeout::ChildExt; + +const QCOW2_INCOMPATIBLE_FEATURES_OFFSET: u64 = 72; +// 10MB is our maximum accepted overhead. +pub(crate) const MAXIMUM_VMM_OVERHEAD_KB: u32 = 10 * 1024; + +// This enum exists to make it more convenient to +// implement test for both D-Bus and REST APIs. +pub(crate) enum TargetApi { + // API socket + HttpApi(String), + // well known service name, object path + DBusApi(String, String), +} + +impl TargetApi { + pub(crate) fn new_http_api(tmp_dir: &TempDir) -> Self { + Self::HttpApi(temp_api_path(tmp_dir)) + } + + pub(crate) fn new_dbus_api(tmp_dir: &TempDir) -> Self { + // `tmp_dir` is in the form of "/tmp/chXXXXXX" + // and we take the `chXXXXXX` part as a unique identifier for the guest + let id = tmp_dir.as_path().file_name().unwrap().to_str().unwrap(); + + Self::DBusApi( + format!("org.cloudhypervisor.{id}"), + format!("/org/cloudhypervisor/{id}"), + ) + } + + pub(crate) fn guest_args(&self) -> Vec { + match self { + TargetApi::HttpApi(api_socket) => { + vec![format!("--api-socket={}", api_socket.as_str())] + } + TargetApi::DBusApi(service_name, object_path) => { + vec![ + format!("--dbus-service-name={}", service_name.as_str()), + format!("--dbus-object-path={}", object_path.as_str()), + ] + } + } + } + + pub(crate) fn remote_args(&self) -> Vec { + // `guest_args` and `remote_args` are consistent with each other + self.guest_args() + } + + pub(crate) fn remote_command(&self, command: &str, arg: Option<&str>) -> bool { + let mut cmd = Command::new(clh_command("ch-remote")); + cmd.args(self.remote_args()); + cmd.arg(command); + + if let Some(arg) = arg { + cmd.arg(arg); + } + + let output = cmd.output().unwrap(); + if output.status.success() { + true + } else { + eprintln!("Error running ch-remote command: {cmd:?}"); + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("stderr: {stderr}"); + false + } + } +} + +pub(crate) fn temp_api_path(tmp_dir: &TempDir) -> String { + String::from( + tmp_dir + .as_path() + .join("cloud-hypervisor.sock") + .to_str() + .unwrap(), + ) +} + +pub(crate) fn wait_for_virtiofsd_socket(socket: &str) { + // Wait for virtiofds to start + let deadline = Instant::now() + Duration::from_secs(10); + while !Path::new(socket).exists() { + if Instant::now() > deadline { + panic!("virtiofsd socket did not appear within 10s"); + } + thread::sleep(Duration::from_millis(50)); + } +} + +pub(crate) fn prepare_virtiofsd( + tmp_dir: &TempDir, + shared_dir: &str, +) -> (std::process::Child, String) { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + + let mut virtiofsd_path = workload_path; + virtiofsd_path.push("virtiofsd"); + let virtiofsd_path = String::from(virtiofsd_path.to_str().unwrap()); + + let virtiofsd_socket_path = + String::from(tmp_dir.as_path().join("virtiofs.sock").to_str().unwrap()); + + // Start the daemon + let child = Command::new(virtiofsd_path.as_str()) + .args(["--shared-dir", shared_dir]) + .args(["--socket-path", virtiofsd_socket_path.as_str()]) + .args(["--cache", "never"]) + .args(["--tag", "myfs"]) + .spawn() + .unwrap(); + + wait_for_virtiofsd_socket(virtiofsd_socket_path.as_str()); + + (child, virtiofsd_socket_path) +} + +pub(crate) fn prepare_vubd( + tmp_dir: &TempDir, + blk_img: &str, + num_queues: usize, + rdonly: bool, + direct: bool, +) -> (std::process::Child, String) { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + + let mut blk_file_path = workload_path; + blk_file_path.push(blk_img); + let blk_file_path = String::from(blk_file_path.to_str().unwrap()); + + let vubd_socket_path = String::from(tmp_dir.as_path().join("vub.sock").to_str().unwrap()); + + // Start the daemon + let child = Command::new(clh_command("vhost_user_block")) + .args([ + "--block-backend", + format!( + "path={blk_file_path},socket={vubd_socket_path},num_queues={num_queues},readonly={rdonly},direct={direct}" + ) + .as_str(), + ]) + .spawn() + .unwrap(); + + thread::sleep(std::time::Duration::new(10, 0)); + + (child, vubd_socket_path) +} + +pub(crate) fn temp_vsock_path(tmp_dir: &TempDir) -> String { + String::from(tmp_dir.as_path().join("vsock").to_str().unwrap()) +} + +pub(crate) fn temp_event_monitor_path(tmp_dir: &TempDir) -> String { + String::from(tmp_dir.as_path().join("event.json").to_str().unwrap()) +} + +// Creates the directory and returns the path. +pub(crate) fn temp_snapshot_dir_path(tmp_dir: &TempDir) -> String { + let snapshot_dir = String::from(tmp_dir.as_path().join("snapshot").to_str().unwrap()); + std::fs::create_dir(&snapshot_dir).unwrap(); + snapshot_dir +} + +pub(crate) fn temp_vmcore_file_path(tmp_dir: &TempDir) -> String { + String::from(tmp_dir.as_path().join("vmcore").to_str().unwrap()) +} + +pub(crate) fn cloud_hypervisor_release_path() -> String { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + + let mut ch_release_path = workload_path; + #[cfg(target_arch = "x86_64")] + ch_release_path.push("cloud-hypervisor-static"); + #[cfg(target_arch = "aarch64")] + ch_release_path.push("cloud-hypervisor-static-aarch64"); + + ch_release_path.into_os_string().into_string().unwrap() +} + +pub(crate) fn prepare_vhost_user_net_daemon( + tmp_dir: &TempDir, + ip: &str, + tap: Option<&str>, + mtu: Option, + num_queues: usize, + client_mode: bool, +) -> (std::process::Command, String) { + let vunet_socket_path = String::from(tmp_dir.as_path().join("vunet.sock").to_str().unwrap()); + + // Start the daemon + let mut net_params = format!( + "ip={ip},mask=255.255.255.128,socket={vunet_socket_path},num_queues={num_queues},queue_size=1024,client={client_mode}" + ); + + if let Some(tap) = tap { + net_params.push_str(format!(",tap={tap}").as_str()); + } + + if let Some(mtu) = mtu { + net_params.push_str(format!(",mtu={mtu}").as_str()); + } + + let mut command = Command::new(clh_command("vhost_user_net")); + command.args(["--net-backend", net_params.as_str()]); + + (command, vunet_socket_path) +} + +pub(crate) fn prepare_swtpm_daemon(tmp_dir: &TempDir) -> (std::process::Command, String) { + let swtpm_tpm_dir = String::from(tmp_dir.as_path().join("swtpm").to_str().unwrap()); + let swtpm_socket_path = String::from( + tmp_dir + .as_path() + .join("swtpm") + .join("swtpm.sock") + .to_str() + .unwrap(), + ); + std::fs::create_dir(&swtpm_tpm_dir).unwrap(); + + let mut swtpm_command = Command::new("swtpm"); + let swtpm_args = [ + "socket", + "--tpmstate", + &format!("dir={swtpm_tpm_dir}"), + "--ctrl", + &format!("type=unixio,path={swtpm_socket_path}"), + "--flags", + "startup-clear", + "--tpm2", + ]; + swtpm_command.args(swtpm_args); + + (swtpm_command, swtpm_socket_path) +} + +pub(crate) fn resize_command( + api_socket: &str, + desired_vcpus: Option, + desired_ram: Option, + desired_balloon: Option, + event_file: Option<&str>, +) -> bool { + let mut cmd = Command::new(clh_command("ch-remote")); + cmd.args([&format!("--api-socket={api_socket}"), "resize"]); + + if let Some(desired_vcpus) = desired_vcpus { + cmd.arg(format!("--cpus={desired_vcpus}")); + } + + if let Some(desired_ram) = desired_ram { + cmd.arg(format!("--memory={desired_ram}")); + } + + if let Some(desired_balloon) = desired_balloon { + cmd.arg(format!("--balloon={desired_balloon}")); + } + + let ret = cmd.status().expect("Failed to launch ch-remote").success(); + + if let Some(event_path) = event_file { + let latest_events = [ + &MetaEvent { + event: "resizing".to_string(), + device_id: None, + }, + &MetaEvent { + event: "resized".to_string(), + device_id: None, + }, + ]; + // See: #5938 + thread::sleep(std::time::Duration::new(1, 0)); + assert!(check_latest_events_exact(&latest_events, event_path)); + } + + ret +} + +pub(crate) fn resize_zone_command(api_socket: &str, id: &str, desired_size: &str) -> bool { + let mut cmd = Command::new(clh_command("ch-remote")); + cmd.args([ + &format!("--api-socket={api_socket}"), + "resize-zone", + &format!("--id={id}"), + &format!("--size={desired_size}"), + ]); + + cmd.status().expect("Failed to launch ch-remote").success() +} + +pub(crate) fn resize_disk_command(api_socket: &str, id: &str, desired_size: &str) -> bool { + let mut cmd = Command::new(clh_command("ch-remote")); + cmd.args([ + &format!("--api-socket={api_socket}"), + "resize-disk", + &format!("--disk={id}"), + &format!("--size={desired_size}"), + ]); + + cmd.status().expect("Failed to launch ch-remote").success() +} + +// setup OVS-DPDK bridge and ports +pub(crate) fn setup_ovs_dpdk() { + // setup OVS-DPDK + assert!(exec_host_command_status("service openvswitch-switch start").success()); + assert!(exec_host_command_status("ovs-vsctl init").success()); + assert!( + exec_host_command_status("ovs-vsctl set Open_vSwitch . other_config:dpdk-init=true") + .success() + ); + assert!(exec_host_command_status("service openvswitch-switch restart").success()); + + // Clean up any stale bridge from a previous failed run + exec_host_command_status("ovs-vsctl --if-exists del-br ovsbr0"); + + // Create OVS-DPDK bridge and ports + assert!( + exec_host_command_status( + "ovs-vsctl add-br ovsbr0 -- set bridge ovsbr0 datapath_type=netdev", + ) + .success() + ); + assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success()); + assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user2 -- set Interface vhost-user2 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient2").success()); + assert!(exec_host_command_status("ip link set up dev ovsbr0").success()); + assert!(exec_host_command_status("service openvswitch-switch restart").success()); +} + +pub(crate) fn cleanup_ovs_dpdk() { + assert!(exec_host_command_status("ovs-vsctl del-br ovsbr0").success()); + exec_host_command_status("rm -f ovs-vsctl /tmp/dpdkvhostclient1 /tmp/dpdkvhostclient2"); +} + +// Setup two guests and ensure they are connected through ovs-dpdk +pub(crate) fn setup_ovs_dpdk_guests( + guest1: &Guest, + guest2: &Guest, + api_socket: &str, + release_binary: bool, +) -> (Child, Child) { + setup_ovs_dpdk(); + + let clh_path = if release_binary { + cloud_hypervisor_release_path() + } else { + clh_command("cloud-hypervisor") + }; + + let mut child1 = GuestCommand::new_with_binary_path(guest1, &clh_path) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=0,shared=on"]) + .args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", guest1.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient1,num_queues=2,queue_size=256,vhost_mode=server"]) + .capture_output() + .spawn() + .unwrap(); + + #[cfg(target_arch = "x86_64")] + let guest_net_iface = "ens5"; + #[cfg(target_arch = "aarch64")] + let guest_net_iface = "enp0s5"; + + let r = std::panic::catch_unwind(|| { + guest1.wait_vm_boot().unwrap(); + + guest1 + .ssh_command(&format!( + "sudo ip addr add 172.100.0.1/24 dev {guest_net_iface}" + )) + .unwrap(); + guest1 + .ssh_command(&format!("sudo ip link set up dev {guest_net_iface}")) + .unwrap(); + + let guest_ip = guest1.network.guest_ip0.clone(); + thread::spawn(move || { + ssh_command_ip( + "nc -l 12345", + &guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + }); + }); + if r.is_err() { + cleanup_ovs_dpdk(); + + let _ = child1.kill(); + let output = child1.wait_with_output().unwrap(); + handle_child_output(r, &output); + panic!("Test should already be failed/panicked"); // To explicitly mark this block never return + } + + let mut child2 = GuestCommand::new_with_binary_path(guest2, &clh_path) + .args(["--api-socket", api_socket]) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=0,shared=on"]) + .args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", guest2.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient2,num_queues=2,queue_size=256,vhost_mode=server"]) + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest2.wait_vm_boot().unwrap(); + + guest2 + .ssh_command(&format!( + "sudo ip addr add 172.100.0.2/24 dev {guest_net_iface}" + )) + .unwrap(); + guest2 + .ssh_command(&format!("sudo ip link set up dev {guest_net_iface}")) + .unwrap(); + + // Check the connection works properly between the two VMs + guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); + }); + if r.is_err() { + cleanup_ovs_dpdk(); + + let _ = child1.kill(); + let _ = child2.kill(); + let output = child2.wait_with_output().unwrap(); + handle_child_output(r, &output); + panic!("Test should already be failed/panicked"); // To explicitly mark this block never return + } + + (child1, child2) +} + +pub enum FwType { + Ovmf, + RustHypervisorFirmware, +} + +pub(crate) fn fw_path(_fw_type: FwType) -> String { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + + let mut fw_path = workload_path; + #[cfg(target_arch = "aarch64")] + fw_path.push("CLOUDHV_EFI.fd"); + #[cfg(target_arch = "x86_64")] + { + match _fw_type { + FwType::Ovmf => fw_path.push(OVMF_NAME), + FwType::RustHypervisorFirmware => fw_path.push("hypervisor-fw"), + } + } + + fw_path.to_str().unwrap().to_string() +} + +/// Parse the event_monitor file based on the format that each event +/// is followed by a double newline +fn parse_event_file(event_file: &str) -> Vec { + let content = fs::read(event_file).unwrap(); + let mut ret = Vec::new(); + for entry in String::from_utf8_lossy(&content) + .trim() + .split("\n\n") + .collect::>() + { + ret.push(serde_json::from_str(entry).unwrap()); + } + + ret +} + +/// Return true if all events from the input 'expected_events' are matched sequentially +/// with events from the 'event_file' +pub(crate) fn check_sequential_events(expected_events: &[&MetaEvent], event_file: &str) -> bool { + check_sequential_events_with_options(expected_events, event_file, true) +} + +/// Wait for a sequential event match and print diagnostics only after timeout. +pub(crate) fn wait_for_sequential_events( + timeout: Duration, + expected_events: &[&MetaEvent], + event_file: &str, +) -> bool { + if wait_until(timeout, || { + check_sequential_events_with_options(expected_events, event_file, false) + }) { + return true; + } + + check_sequential_events(expected_events, event_file); + false +} + +/// Check sequential events with optional mismatch diagnostics. +fn check_sequential_events_with_options( + expected_events: &[&MetaEvent], + event_file: &str, + print_diagnostics: bool, +) -> bool { + if !Path::new(event_file).exists() { + return false; + } + let json_events = parse_event_file(event_file); + let len = expected_events.len(); + let mut idx = 0; + for e in &json_events { + if idx == len { + break; + } + if expected_events[idx].match_with_json_event(e) { + idx += 1; + } + } + + let ret = idx == len; + + if !ret && print_diagnostics { + eprintln!( + "\n\n==== Start 'check_sequential_events' failed ==== \ + \n\nexpected_events={expected_events:?}\nactual_events={json_events:?} \ + \n\n==== End 'check_sequential_events' failed ====", + ); + } + + ret +} + +// Return true if all events from the input 'expected_events' are matched exactly +// with events from the 'event_file' +pub(crate) fn check_sequential_events_exact( + expected_events: &[&MetaEvent], + event_file: &str, +) -> bool { + if !Path::new(event_file).exists() { + return false; + } + let json_events = parse_event_file(event_file); + if expected_events.len() > json_events.len() { + return false; + } + let json_events = &json_events[..expected_events.len()]; + + for (idx, e) in json_events.iter().enumerate() { + if !expected_events[idx].match_with_json_event(e) { + eprintln!( + "\n\n==== Start 'check_sequential_events_exact' failed ==== \ + \n\nexpected_events={expected_events:?}\nactual_events={json_events:?} \ + \n\n==== End 'check_sequential_events_exact' failed ====", + ); + + return false; + } + } + + true +} + +/// Return true if events from the input 'latest_events' are matched exactly +/// with the most recent events from the 'event_file' +pub(crate) fn check_latest_events_exact(latest_events: &[&MetaEvent], event_file: &str) -> bool { + check_latest_events_exact_with_options(latest_events, event_file, true) +} + +/// Wait for an exact latest-event match and print diagnostics only after timeout. +pub(crate) fn wait_for_latest_events_exact( + timeout: Duration, + latest_events: &[&MetaEvent], + event_file: &str, +) -> bool { + if wait_until(timeout, || { + check_latest_events_exact_with_options(latest_events, event_file, false) + }) { + return true; + } + + check_latest_events_exact(latest_events, event_file); + false +} + +/// Check latest events with optional mismatch diagnostics. +fn check_latest_events_exact_with_options( + latest_events: &[&MetaEvent], + event_file: &str, + print_diagnostics: bool, +) -> bool { + if !Path::new(event_file).exists() { + return false; + } + let json_events = parse_event_file(event_file); + if latest_events.len() > json_events.len() { + return false; + } + let json_events = &json_events[(json_events.len() - latest_events.len())..]; + + for (idx, e) in json_events.iter().enumerate() { + if !latest_events[idx].match_with_json_event(e) { + if print_diagnostics { + eprintln!( + "\n\n==== Start 'check_latest_events_exact' failed ==== \ + \n\nexpected_events={latest_events:?}\nactual_events={json_events:?} \ + \n\n==== End 'check_latest_events_exact' failed ====", + ); + } + + return false; + } + } + + true +} + +pub(super) fn get_msi_interrupt_pattern() -> String { + #[cfg(target_arch = "x86_64")] + { + "PCI-MSI".to_string() + } + #[cfg(target_arch = "aarch64")] + { + if cfg!(feature = "mshv") { + "GICv2m-PCI-MSIX".to_string() + } else { + "ITS-PCI-MSIX".to_string() + } + } +} + +pub(super) type PrepareNetDaemon = dyn Fn( + &TempDir, + &str, + Option<&str>, + Option, + usize, + bool, +) -> (std::process::Command, String); + +pub(super) fn get_ksm_pages_shared() -> u32 { + fs::read_to_string("/sys/kernel/mm/ksm/pages_shared") + .unwrap() + .trim() + .parse::() + .unwrap() +} + +fn _get_vmm_overhead(pid: u32, guest_memory_size: u32) -> HashMap { + let smaps = fs::File::open(format!("/proc/{pid}/smaps")).unwrap(); + let reader = io::BufReader::new(smaps); + + let mut skip_map: bool = false; + let mut region_name: String = String::new(); + let mut region_maps = HashMap::new(); + for line in reader.lines() { + let l = line.unwrap(); + + if l.contains('-') { + let values: Vec<&str> = l.split_whitespace().collect(); + region_name = values.last().unwrap().trim().to_string(); + if region_name == "0" { + region_name = "anonymous".to_string(); + } + } + + // Each section begins with something that looks like: + // Size: 2184 kB + if l.starts_with("Size:") { + let values: Vec<&str> = l.split_whitespace().collect(); + let map_size = values[1].parse::().unwrap(); + // We skip the assigned guest RAM map, its RSS is only + // dependent on the guest actual memory usage. + // Everything else can be added to the VMM overhead. + skip_map = map_size >= guest_memory_size; + continue; + } + + // If this is a map we're taking into account, then we only + // count the RSS. The sum of all counted RSS is the VMM overhead. + if !skip_map && l.starts_with("Rss:") { + let values: Vec<&str> = l.split_whitespace().collect(); + let value = values[1].trim().parse::().unwrap(); + *region_maps.entry(region_name.clone()).or_insert(0) += value; + } + } + + region_maps +} + +pub(crate) fn get_vmm_overhead(pid: u32, guest_memory_size: u32) -> u32 { + let mut total = 0; + + for (region_name, value) in &_get_vmm_overhead(pid, guest_memory_size) { + eprintln!("{region_name}: {value}"); + total += value; + } + + total +} + +pub(crate) fn process_rss_kib(pid: u32) -> usize { + let command = format!("ps -q {pid} -o rss="); + let rss = exec_host_command_output(&command); + String::from_utf8_lossy(&rss.stdout).trim().parse().unwrap() +} + +#[derive(PartialEq, Eq, PartialOrd)] +pub struct Counters { + rx_bytes: u64, + rx_frames: u64, + tx_bytes: u64, + tx_frames: u64, + read_bytes: u64, + write_bytes: u64, + read_ops: u64, + write_ops: u64, +} + +pub(crate) fn get_counters(api_socket: &str) -> Counters { + // Get counters + let (cmd_success, cmd_output, _) = remote_command_w_output(api_socket, "counters", None); + assert!(cmd_success); + + let counters: HashMap<&str, HashMap<&str, u64>> = + serde_json::from_slice(&cmd_output).unwrap_or_default(); + + let rx_bytes = *counters.get("_net2").unwrap().get("rx_bytes").unwrap(); + let rx_frames = *counters.get("_net2").unwrap().get("rx_frames").unwrap(); + let tx_bytes = *counters.get("_net2").unwrap().get("tx_bytes").unwrap(); + let tx_frames = *counters.get("_net2").unwrap().get("tx_frames").unwrap(); + + let read_bytes = *counters.get("_disk0").unwrap().get("read_bytes").unwrap(); + let write_bytes = *counters.get("_disk0").unwrap().get("write_bytes").unwrap(); + let read_ops = *counters.get("_disk0").unwrap().get("read_ops").unwrap(); + let write_ops = *counters.get("_disk0").unwrap().get("write_ops").unwrap(); + + Counters { + rx_bytes, + rx_frames, + tx_bytes, + tx_frames, + read_bytes, + write_bytes, + read_ops, + write_ops, + } +} + +pub(super) fn pty_read(mut pty: std::fs::File) -> Receiver { + let (tx, rx) = mpsc::channel::(); + thread::spawn(move || { + loop { + thread::sleep(std::time::Duration::new(1, 0)); + let mut buf = [0; 512]; + match pty.read(&mut buf) { + Ok(_bytes) => { + let output = std::str::from_utf8(&buf).unwrap().to_string(); + match tx.send(output) { + Ok(_) => (), + Err(_) => break, + } + } + Err(_) => break, + } + } + }); + rx +} + +pub(crate) fn get_pty_path(api_socket: &str, pty_type: &str) -> PathBuf { + let (cmd_success, cmd_output, _) = remote_command_w_output(api_socket, "info", None); + assert!(cmd_success); + let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); + assert_eq!("Pty", info["config"][pty_type]["mode"]); + PathBuf::from( + info["config"][pty_type]["file"] + .as_str() + .expect("Missing pty path"), + ) +} + +// VFIO test network setup. +// We reserve a different IP class for it: 172.18.0.0/24. +#[cfg(target_arch = "x86_64")] +pub(crate) fn setup_vfio_network_interfaces() { + // Clean up any leftover interfaces from previous runs + cleanup_vfio_network_interfaces(); + + // 'vfio-br0' + assert!(exec_host_command_status("sudo ip link add name vfio-br0 type bridge").success()); + assert!(exec_host_command_status("sudo ip link set vfio-br0 up").success()); + assert!(exec_host_command_status("sudo ip addr add 172.18.0.1/24 dev vfio-br0").success()); + // 'vfio-tap0' + assert!(exec_host_command_status("sudo ip tuntap add vfio-tap0 mode tap").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap0 master vfio-br0").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap0 up").success()); + // 'vfio-tap1' + assert!(exec_host_command_status("sudo ip tuntap add vfio-tap1 mode tap").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap1 master vfio-br0").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap1 up").success()); + // 'vfio-tap2' + assert!(exec_host_command_status("sudo ip tuntap add vfio-tap2 mode tap").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap2 master vfio-br0").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap2 up").success()); + // 'vfio-tap3' + assert!(exec_host_command_status("sudo ip tuntap add vfio-tap3 mode tap").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap3 master vfio-br0").success()); + assert!(exec_host_command_status("sudo ip link set vfio-tap3 up").success()); +} + +// Tear VFIO test network down +#[cfg(target_arch = "x86_64")] +pub(crate) fn cleanup_vfio_network_interfaces() { + let _ = exec_host_command_status("sudo ip link del vfio-br0"); + let _ = exec_host_command_status("sudo ip link del vfio-tap0"); + let _ = exec_host_command_status("sudo ip link del vfio-tap1"); + let _ = exec_host_command_status("sudo ip link del vfio-tap2"); + let _ = exec_host_command_status("sudo ip link del vfio-tap3"); +} + +pub(crate) fn balloon_size(api_socket: &str) -> u64 { + let (cmd_success, cmd_output, _) = remote_command_w_output(api_socket, "info", None); + assert!(cmd_success); + + let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); + let total_mem = &info["config"]["memory"]["size"] + .to_string() + .parse::() + .unwrap(); + let actual_mem = &info["memory_actual_size"] + .to_string() + .parse::() + .unwrap(); + total_mem - actual_mem +} + +pub(crate) fn vm_state(api_socket: &str) -> String { + let (cmd_success, cmd_output, _) = remote_command_w_output(api_socket, "info", None); + assert!(cmd_success); + + let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); + let state = &info["state"].as_str().unwrap(); + + state.to_string() +} + +pub(crate) fn make_virtio_block_guest(factory: &GuestFactory, image_name: &str) -> Guest { + let disk_config = UbuntuDiskConfig::new(image_name.to_string()); + factory.create_guest(Box::new(disk_config)).with_cpu(4) +} + +pub(crate) fn compute_backing_checksum( + path_or_image_name: impl AsRef, +) -> Option<(std::path::PathBuf, String, u32)> { + let path = resolve_disk_path(path_or_image_name); + + let mut file = File::open(&path).ok()?; + if !matches!( + block::detect_image_type(&mut file).ok()?, + block::ImageType::Qcow2 + ) { + return None; + } + + let info = get_image_info(&path)?; + + let backing_file = info["backing-filename"].as_str()?; + let backing_path = if std::path::Path::new(backing_file).is_absolute() { + std::path::PathBuf::from(backing_file) + } else { + path.parent() + .unwrap_or_else(|| std::path::Path::new(".")) + .join(backing_file) + }; + + let backing_info = get_image_info(&backing_path)?; + let backing_format = backing_info["format"].as_str()?.to_string(); + let mut file = File::open(&backing_path).ok()?; + let file_size = file.metadata().ok()?.len(); + let checksum = compute_file_checksum(&mut file, file_size); + + Some((backing_path, backing_format, checksum)) +} + +/// Uses `qemu-img check` to verify disk image consistency. +/// +/// Supported formats are `qcow2` (compressed and uncompressed), +/// `vhdx`, `qed`, `parallels`, `vmdk`, and `vdi`. See man page +/// for more details. +/// +/// It takes either a full path to the image or just the name of +/// the image located in the `workloads` directory. +/// +/// For QCOW2 images with backing files, also verifies the backing file +/// integrity and checks that the backing file hasn't been modified +/// during the test. +/// +/// For QCOW2 v3 images, also verifies the dirty bit is cleared. +pub(crate) fn disk_check_consistency( + path_or_image_name: impl AsRef, + initial_backing_checksum: Option<(std::path::PathBuf, String, u32)>, +) { + let path = resolve_disk_path(path_or_image_name); + let output = run_qemu_img(&path, &["check"], None); + + assert!( + output.status.success(), + "qemu-img check failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + match check_dirty_flag(&path) { + Ok(Some(dirty)) => { + assert!(!dirty, "QCOW2 image shutdown unclean"); + } + Ok(None) => {} // Not a QCOW2 v3 image, skip dirty flag check + Err(e) => panic!("Failed to check dirty flag: {e}"), + } + + if let Some((backing_path, format, initial_checksum)) = initial_backing_checksum { + if format.parse::().ok() != Some(block::qcow::ImageType::Raw) { + let output = run_qemu_img(&backing_path, &["check"], None); + + assert!( + output.status.success(), + "qemu-img check of backing file failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let mut file = File::open(&backing_path).unwrap(); + let file_size = file.metadata().unwrap().len(); + assert_eq!( + initial_checksum, + compute_file_checksum(&mut file, file_size) + ); + } +} + +pub(crate) fn run_qemu_img( + path: &std::path::Path, + args: &[&str], + trailing_args: Option<&[&str]>, +) -> std::process::Output { + let mut cmd = std::process::Command::new("qemu-img"); + cmd.arg(args[0]) + .args(&args[1..]) + .arg(path.to_str().unwrap()); + if let Some(extra) = trailing_args { + cmd.args(extra); + } + cmd.output().unwrap() +} + +fn get_image_info(path: &std::path::Path) -> Option { + let output = run_qemu_img(path, &["info", "-U", "--output=json"], None); + + output.status.success().then_some(())?; + serde_json::from_slice(&output.stdout).ok() +} + +fn get_qcow2_v3_info(path: &Path) -> Result, String> { + let info = get_image_info(path) + .ok_or_else(|| format!("qemu-img info failed for {}", path.display()))?; + if info["format"].as_str() != Some("qcow2") { + return Ok(None); + } + // QCOW2 v3 has compat "1.1", v2 has "0.10" + if info["format-specific"]["data"]["compat"].as_str() != Some("1.1") { + return Ok(None); + } + Ok(Some(info)) +} + +pub(crate) fn check_dirty_flag(path: &Path) -> Result, String> { + Ok(get_qcow2_v3_info(path)?.and_then(|info| info["dirty-flag"].as_bool())) +} + +pub(crate) fn check_corrupt_flag(path: &Path) -> Result, String> { + Ok(get_qcow2_v3_info(path)? + .and_then(|info| info["format-specific"]["data"]["corrupt"].as_bool())) +} + +pub(crate) fn set_corrupt_flag(path: &Path, corrupt: bool) -> io::Result<()> { + let mut file = OpenOptions::new().read(true).write(true).open(path)?; + + file.seek(SeekFrom::Start(QCOW2_INCOMPATIBLE_FEATURES_OFFSET))?; + let mut buf = [0u8; 8]; + file.read_exact(&mut buf)?; + let mut features = u64::from_be_bytes(buf); + + if corrupt { + features |= 0x02; + } else { + features &= !0x02; + } + + file.seek(SeekFrom::Start(QCOW2_INCOMPATIBLE_FEATURES_OFFSET))?; + file.write_all(&features.to_be_bytes())?; + file.sync_all()?; + Ok(()) +} + +fn resolve_disk_path(path_or_image_name: impl AsRef) -> std::path::PathBuf { + if path_or_image_name.as_ref().exists() { + // A full path is provided + path_or_image_name.as_ref().to_path_buf() + } else { + // An image name is provided + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + workload_path.as_path().join(path_or_image_name.as_ref()) + } +} + +pub(crate) fn compute_file_checksum(reader: &mut dyn std::io::Read, size: u64) -> u32 { + // Read first 16MB or entire data if smaller + let read_size = cmp::min(size, 16 * 1024 * 1024) as usize; + + let mut buffer = vec![0u8; read_size]; + reader.read_exact(&mut buffer).unwrap(); + + // DJB2 hash + let mut hash: u32 = 5381; + for byte in buffer.iter() { + hash = hash.wrapping_mul(33).wrapping_add(*byte as u32); + } + hash +} + +pub(crate) fn get_reboot_count(guest: &Guest) -> u32 { + guest + .ssh_command("sudo last | grep -c reboot") + .unwrap() + .trim() + .parse::() + .unwrap_or_default() +} + +pub(crate) fn enable_guest_watchdog(guest: &Guest, watchdog_sec: u32) { + // Check for PCI device + assert!( + guest + .does_device_vendor_pair_match("0x1063", "0x1af4") + .unwrap_or_default() + ); + + guest + .ssh_command(&format!( + "echo RuntimeWatchdogSec={watchdog_sec}s | sudo tee -a /etc/systemd/system.conf" + )) + .unwrap(); + + guest.ssh_command("sudo systemctl daemon-reexec").unwrap(); +} + +pub(crate) fn make_guest_panic(guest: &Guest) { + // Check for pvpanic device + assert!( + guest + .does_device_vendor_pair_match("0x0011", "0x1b36") + .unwrap_or_default() + ); + + // Trigger guest a panic + guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); +} + +/// Extracts a BDF from a CHV returned response +pub(crate) fn bdf_from_hotplug_response( + s: &str, +) -> ( + u16, /* Segment ID */ + u8, /* Bus ID */ + u8, /* Device ID */ + u8, /* Function ID */ +) { + let json: serde_json::Value = serde_json::from_str(s).expect("should be valid JSON"); + let bdf_str = json["bdf"] + .as_str() + .expect("should contain string key `bdf`"); + + // BDF format: "SSSS:BB:DD.F" + let parts: Vec<&str> = bdf_str.split(&[':', '.'][..]).collect(); + assert_eq!(parts.len(), 4, "unexpected BDF format: {bdf_str}"); + + let segment_id = u16::from_str_radix(parts[0], 16).unwrap(); + let bus_id = u8::from_str_radix(parts[1], 16).unwrap(); + let device_id = u8::from_str_radix(parts[2], 16).unwrap(); + let function_id = u8::from_str_radix(parts[3], 16).unwrap(); + + (segment_id, bus_id, device_id, function_id) +} + +#[cfg(not(feature = "mshv"))] +pub(crate) fn start_live_migration( + migration_socket: &str, + src_api_socket: &str, + dest_api_socket: &str, + local: bool, + paused: bool, +) -> bool { + // Start to receive migration from the destination VM + let mut receive_migration = Command::new(clh_command("ch-remote")) + .args([ + &format!("--api-socket={dest_api_socket}"), + "receive-migration", + &format! {"unix:{migration_socket}"}, + ]) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + // Give it '1s' to make sure the 'migration_socket' file is properly created + thread::sleep(std::time::Duration::new(1, 0)); + + if paused { + // Test the migration of a paused VM. + let cmd_success = remote_command(src_api_socket, "pause", None); + if !cmd_success { + let _ = receive_migration.kill(); + eprintln!("Failed to pause the source VM before live migration"); + } + } + + // Start to send migration from the source VM + let args = [ + format!("--api-socket={src_api_socket}"), + "send-migration".to_string(), + format!( + "destination_url=unix:{migration_socket},local={}", + if local { "on" } else { "off" } + ), + ] + .to_vec(); + + let mut send_migration = Command::new(clh_command("ch-remote")) + .args(&args) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + // The 'send-migration' command should be executed successfully within the given timeout + let send_success = if let Some(status) = send_migration + .wait_timeout(std::time::Duration::from_secs(30)) + .unwrap() + { + status.success() + } else { + false + }; + + if !send_success { + let _ = send_migration.kill(); + let output = send_migration.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'send_migration' output ==== \ + \n\n---stdout---\n{}\n\n---stderr---\n{} \ + \n\n==== End 'send_migration' output ====\n\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + // The 'receive-migration' command should be executed successfully within the given timeout + let receive_success = if let Some(status) = receive_migration + .wait_timeout(std::time::Duration::from_secs(30)) + .unwrap() + { + status.success() + } else { + false + }; + + if !receive_success { + let _ = receive_migration.kill(); + let output = receive_migration.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'receive_migration' output ==== \ + \n\n---stdout---\n{}\n\n---stderr---\n{} \ + \n\n==== End 'receive_migration' output ====\n\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } else if paused { + // for a paused VM, we should make sure the destinations VM state is still 'Paused' after + // migration. + let dest_state = vm_state(dest_api_socket); + if dest_state != "Paused" { + eprintln!( + "\n\n==== Start 'destination VM state' output ==== \ + \n\nExpected destination VM state: Paused\nActual destination VM state: {dest_state} \ + \n\n==== End 'destination VM state' output ====\n\n" + ); + return false; + } + // Resume the paused VM to make sure it still works after migration + let cmd_success = remote_command(dest_api_socket, "resume", None); + if !cmd_success { + eprintln!( + "\n\n==== Start 'destination VM state' output ==== \ + \n\nFailed to resume the destination VM after live migration \ + \n\n==== End 'destination VM state' output ====\n\n" + ); + return false; + } + } + + send_success && receive_success +} + +#[cfg(not(feature = "mshv"))] +pub(crate) fn print_and_panic( + src_vm: Child, + dest_vm: Child, + ovs_vm: Option, + message: &str, +) -> ! { + let mut src_vm = src_vm; + let mut dest_vm = dest_vm; + + let _ = src_vm.kill(); + let src_output = src_vm.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'source_vm' stdout ====\n\n{}\n\n==== End 'source_vm' stdout ====", + String::from_utf8_lossy(&src_output.stdout) + ); + eprintln!( + "\n\n==== Start 'source_vm' stderr ====\n\n{}\n\n==== End 'source_vm' stderr ====", + String::from_utf8_lossy(&src_output.stderr) + ); + let _ = dest_vm.kill(); + let dest_output = dest_vm.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'destination_vm' stdout ====\n\n{}\n\n==== End 'destination_vm' stdout ====", + String::from_utf8_lossy(&dest_output.stdout) + ); + eprintln!( + "\n\n==== Start 'destination_vm' stderr ====\n\n{}\n\n==== End 'destination_vm' stderr ====", + String::from_utf8_lossy(&dest_output.stderr) + ); + + if let Some(ovs_vm) = ovs_vm { + let mut ovs_vm = ovs_vm; + let _ = ovs_vm.kill(); + let ovs_output = ovs_vm.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'ovs_vm' stdout ====\n\n{}\n\n==== End 'ovs_vm' stdout ====", + String::from_utf8_lossy(&ovs_output.stdout) + ); + eprintln!( + "\n\n==== Start 'ovs_vm' stderr ====\n\n{}\n\n==== End 'ovs_vm' stderr ====", + String::from_utf8_lossy(&ovs_output.stderr) + ); + + cleanup_ovs_dpdk(); + } + + panic!("Test failed: {message}") +} diff --git a/tests/integration.rs b/cloud-hypervisor/tests/integration.rs similarity index 54% rename from tests/integration.rs rename to cloud-hypervisor/tests/integration.rs index 0536864fd2..087418b843 100644 --- a/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -2,2440 +2,1974 @@ // // SPDX-License-Identifier: Apache-2.0 // +#![cfg(any(devcli_testenv, clippy))] #![allow(clippy::undocumented_unsafe_blocks)] // When enabling the `mshv` feature, we skip quite some tests and // hence have known dead-code. This annotation silences dead-code // related warnings for our quality workflow to pass. #![allow(dead_code)] - -extern crate test_infra; - -use std::collections::HashMap; -use std::io::{BufRead, Read, Seek, Write}; +use std::fs::{File, OpenOptions, copy}; +use std::io::{Read, Seek, Write}; +#[cfg(not(feature = "mshv"))] use std::net::TcpListener; use std::os::unix::io::AsRawFd; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::string::String; -use std::sync::mpsc::Receiver; -use std::sync::{mpsc, Mutex}; -use std::time::Duration; -use std::{fs, io, thread}; +use std::sync::Mutex; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::{fs, thread}; -use net_util::MacAddr; +use block::ImageType; use test_infra::*; use vmm_sys_util::tempdir::TempDir; use vmm_sys_util::tempfile::TempFile; use wait_timeout::ChildExt; -// Constant taken from the VMM crate. -const MAX_NUM_PCI_SEGMENTS: u16 = 96; - -#[cfg(target_arch = "x86_64")] -mod x86_64 { - pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-amd64-custom-20210609-0.raw"; - pub const JAMMY_VFIO_IMAGE_NAME: &str = - "jammy-server-cloudimg-amd64-custom-vfio-20241012-0.raw"; - pub const FOCAL_IMAGE_NAME_QCOW2: &str = "focal-server-cloudimg-amd64-custom-20210609-0.qcow2"; - pub const FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE: &str = - "focal-server-cloudimg-amd64-custom-20210609-0-backing.qcow2"; - pub const FOCAL_IMAGE_NAME_VHD: &str = "focal-server-cloudimg-amd64-custom-20210609-0.vhd"; - pub const FOCAL_IMAGE_NAME_VHDX: &str = "focal-server-cloudimg-amd64-custom-20210609-0.vhdx"; - pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-amd64-custom-20241017-0.raw"; - pub const WINDOWS_IMAGE_NAME: &str = "windows-server-2022-amd64-2.raw"; - pub const OVMF_NAME: &str = "CLOUDHV.fd"; - pub const GREP_SERIAL_IRQ_CMD: &str = "grep -c 'IO-APIC.*ttyS0' /proc/interrupts || true"; -} - -#[cfg(target_arch = "x86_64")] -use x86_64::*; +mod common; +use common::tests_wrappers::*; +use common::utils::*; -#[cfg(target_arch = "aarch64")] -mod aarch64 { - pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-arm64-custom-20210929-0.raw"; - pub const FOCAL_IMAGE_UPDATE_KERNEL_NAME: &str = - "focal-server-cloudimg-arm64-custom-20210929-0-update-kernel.raw"; - pub const FOCAL_IMAGE_NAME_QCOW2: &str = "focal-server-cloudimg-arm64-custom-20210929-0.qcow2"; - pub const FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE: &str = - "focal-server-cloudimg-arm64-custom-20210929-0-backing.qcow2"; - pub const FOCAL_IMAGE_NAME_VHD: &str = "focal-server-cloudimg-arm64-custom-20210929-0.vhd"; - pub const FOCAL_IMAGE_NAME_VHDX: &str = "focal-server-cloudimg-arm64-custom-20210929-0.vhdx"; - pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.raw"; - pub const WINDOWS_IMAGE_NAME: &str = "windows-11-iot-enterprise-aarch64.raw"; - pub const OVMF_NAME: &str = "CLOUDHV_EFI.fd"; - pub const GREP_SERIAL_IRQ_CMD: &str = "grep -c 'GICv3.*uart-pl011' /proc/interrupts || true"; - pub const GREP_PMU_IRQ_CMD: &str = "grep -c 'GICv3.*arm-pmu' /proc/interrupts || true"; +macro_rules! basic_regular_guest { + ($image_name:expr) => {{ + let disk_config = UbuntuDiskConfig::new($image_name.to_string()); + GuestFactory::new_regular_guest_factory().create_guest(Box::new(disk_config)) + }}; } -#[cfg(target_arch = "aarch64")] -use aarch64::*; - -const DIRECT_KERNEL_BOOT_CMDLINE: &str = - "root=/dev/vda1 console=hvc0 rw systemd.journald.forward_to_console=1"; +mod common_parallel { + use std::io::{self, SeekFrom}; + #[cfg(not(feature = "mshv"))] + use std::num::NonZeroU32; + use std::process::Command; -const CONSOLE_TEST_STRING: &str = "Started OpenBSD Secure Shell server"; + use test_infra::GuestFactory; + #[cfg(not(feature = "mshv"))] + use vmm::api::TimeoutStrategy; -// This enum exists to make it more convenient to -// implement test for both D-Bus and REST APIs. -enum TargetApi { - // API socket - HttpApi(String), - // well known service name, object path - DBusApi(String, String), -} + use crate::*; -impl TargetApi { - fn new_http_api(tmp_dir: &TempDir) -> Self { - Self::HttpApi(temp_api_path(tmp_dir)) + #[test] + #[cfg(target_arch = "x86_64")] + fn test_jammy_hypervisor_fw() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME) + .with_kernel(fw_path(FwType::RustHypervisorFirmware)); + _test_simple_launch(&guest); } - fn new_dbus_api(tmp_dir: &TempDir) -> Self { - // `tmp_dir` is in the form of "/tmp/chXXXXXX" - // and we take the `chXXXXXX` part as a unique identifier for the guest - let id = tmp_dir.as_path().file_name().unwrap().to_str().unwrap(); - - Self::DBusApi( - format!("org.cloudhypervisor.{id}"), - format!("/org/cloudhypervisor/{id}"), - ) + #[test] + #[cfg(target_arch = "x86_64")] + fn test_jammy_ovmf() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel(fw_path(FwType::Ovmf)); + _test_simple_launch(&guest); } - fn guest_args(&self) -> Vec { - match self { - TargetApi::HttpApi(api_socket) => { - vec![format!("--api-socket={}", api_socket.as_str())] - } - TargetApi::DBusApi(service_name, object_path) => { - vec![ - format!("--dbus-service-name={}", service_name.as_str()), - format!("--dbus-object-path={}", object_path.as_str()), - ] - } - } + #[test] + fn test_multi_cpu() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_multi_cpu(&guest); } - fn remote_args(&self) -> Vec { - // `guest_args` and `remote_args` are consistent with each other - self.guest_args() + #[test] + #[cfg_attr(target_arch = "x86_64", should_panic)] + fn test_cpu_topology_421() { + test_cpu_topology(4, 2, 1, false); } - fn remote_command(&self, command: &str, arg: Option<&str>) -> bool { - let mut cmd = Command::new(clh_command("ch-remote")); - cmd.args(self.remote_args()); - cmd.arg(command); - - if let Some(arg) = arg { - cmd.arg(arg); - } - - let output = cmd.output().unwrap(); - if output.status.success() { - true - } else { - eprintln!("Error running ch-remote command: {:?}", &cmd); - let stderr = String::from_utf8_lossy(&output.stderr); - eprintln!("stderr: {stderr}"); - false - } + #[test] + fn test_cpu_topology_142() { + test_cpu_topology(1, 4, 2, false); } -} -// Start cloud-hypervisor with no VM parameters, only the API server running. -// From the API: Create a VM, boot it and check that it looks as expected. -fn _test_api_create_boot(target_api: TargetApi, guest: Guest) { - let mut child = GuestCommand::new(&guest) - .args(target_api.guest_args()) - .capture_output() - .spawn() - .unwrap(); + #[test] + fn test_cpu_topology_262() { + test_cpu_topology(2, 6, 2, false); + } - thread::sleep(std::time::Duration::new(1, 0)); + #[test] + #[cfg(target_arch = "x86_64")] + #[cfg(not(feature = "mshv"))] + fn test_cpu_physical_bits() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let max_phys_bits: u8 = 36; + let mut child = GuestCommand::new(&guest) + .args(["--cpus", &format!("max_phys_bits={max_phys_bits}")]) + .default_memory() + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - // Verify API server is running - assert!(target_api.remote_command("ping", None)); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - // Create the VM first - let cpu_count: u8 = 4; - let request_body = guest.api_create_body( - cpu_count, - direct_kernel_boot_path().to_str().unwrap(), - DIRECT_KERNEL_BOOT_CMDLINE, - ); + assert!( + guest + .ssh_command("lscpu | grep \"Address sizes:\" | cut -f 2 -d \":\" | sed \"s# *##\" | cut -f 1 -d \" \"") + .unwrap() + .trim() + .parse::() + .unwrap_or(max_phys_bits + 1) <= max_phys_bits, + ); + }); - let temp_config_path = guest.tmp_dir.as_path().join("config"); - std::fs::write(&temp_config_path, request_body).unwrap(); - let create_config = temp_config_path.as_os_str().to_str().unwrap(); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - assert!(target_api.remote_command("create", Some(create_config),)); + handle_child_output(r, &output); + } - // Then boot it - assert!(target_api.remote_command("boot", None)); - thread::sleep(std::time::Duration::new(20, 0)); + fn _test_nested_virtualization(nested: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)).with_nested(nested); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - let r = std::panic::catch_unwind(|| { - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - }); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let expected = if nested { "yes" } else { "no" }; + assert_eq!( + guest + .ssh_command("test -c /dev/kvm && echo yes || echo no") + .unwrap() + .trim(), + expected + ); + }); - handle_child_output(r, &output); -} + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); -// Start cloud-hypervisor with no VM parameters, only the API server running. -// From the API: Create a VM, boot it and check it can be shutdown and then -// booted again -fn _test_api_shutdown(target_api: TargetApi, guest: Guest) { - let mut child = GuestCommand::new(&guest) - .args(target_api.guest_args()) - .capture_output() - .spawn() - .unwrap(); + handle_child_output(r, &output); + } - thread::sleep(std::time::Duration::new(1, 0)); + #[test] + #[cfg(target_arch = "x86_64")] + fn test_nested_virtualization_on() { + _test_nested_virtualization(true); + } - // Verify API server is running - assert!(target_api.remote_command("ping", None)); + #[test] + #[cfg(target_arch = "x86_64")] + fn test_nested_virtualization_off() { + _test_nested_virtualization(false); + } - // Create the VM first - let cpu_count: u8 = 4; - let request_body = guest.api_create_body( - cpu_count, - direct_kernel_boot_path().to_str().unwrap(), - DIRECT_KERNEL_BOOT_CMDLINE, - ); + #[test] + fn test_cpu_affinity() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_cpu_affinity(&guest); + } - let temp_config_path = guest.tmp_dir.as_path().join("config"); - std::fs::write(&temp_config_path, request_body).unwrap(); - let create_config = temp_config_path.as_os_str().to_str().unwrap(); + #[test] + fn test_virtio_queue_affinity() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); + _test_virtio_queue_affinity(&guest); + } - let r = std::panic::catch_unwind(|| { - assert!(target_api.remote_command("create", Some(create_config))); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_large_vm() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut cmd = GuestCommand::new(&guest); + cmd.args(["--cpus", "boot=48"]) + .args(["--memory", "size=5120M"]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) + .capture_output() + .default_disks() + .default_net(); - // Then boot it - assert!(target_api.remote_command("boot", None)); + let mut child = cmd.spawn().unwrap(); - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + let r = std::panic::catch_unwind(|| { + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 48); + assert_eq!( + guest + .ssh_command("lscpu | grep \"On-line\" | cut -f 2 -d \":\" | sed \"s# *##\"") + .unwrap() + .trim(), + "0-47" + ); - // Sync and shutdown without powering off to prevent filesystem - // corruption. - guest.ssh_command("sync").unwrap(); - guest.ssh_command("sudo shutdown -H now").unwrap(); + assert!(guest.get_total_memory().unwrap_or_default() > 5_000_000); + }); - // Wait for the guest to be fully shutdown - thread::sleep(std::time::Duration::new(20, 0)); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - // Then shut it down - assert!(target_api.remote_command("shutdown", None)); + handle_child_output(r, &output); + } - // Then boot it again - assert!(target_api.remote_command("boot", None)); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_huge_memory() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut cmd = GuestCommand::new(&guest); + cmd.default_cpus() + .args(["--memory", "size=128G"]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .capture_output() + .default_disks() + .default_net(); - guest.wait_vm_boot(None).unwrap(); + let mut child = cmd.spawn().unwrap(); - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - }); + guest.wait_vm_boot().unwrap(); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let r = std::panic::catch_unwind(|| { + assert!(guest.get_total_memory().unwrap_or_default() > 128_000_000); + }); - handle_child_output(r, &output); -} + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); -// Start cloud-hypervisor with no VM parameters, only the API server running. -// From the API: Create a VM, boot it and check it can be deleted and then recreated -// booted again. -fn _test_api_delete(target_api: TargetApi, guest: Guest) { - let mut child = GuestCommand::new(&guest) - .args(target_api.guest_args()) - .capture_output() - .spawn() - .unwrap(); + handle_child_output(r, &output); + } - thread::sleep(std::time::Duration::new(1, 0)); + #[test] + fn test_power_button() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_power_button(&guest); + } - // Verify API server is running - assert!(target_api.remote_command("ping", None)); + #[test] + #[cfg(not(feature = "mshv"))] // See #7456 + fn test_user_defined_memory_regions() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); - // Create the VM first - let cpu_count: u8 = 4; - let request_body = guest.api_create_body( - cpu_count, - direct_kernel_boot_path().to_str().unwrap(), - DIRECT_KERNEL_BOOT_CMDLINE, - ); - let temp_config_path = guest.tmp_dir.as_path().join("config"); - std::fs::write(&temp_config_path, request_body).unwrap(); - let create_config = temp_config_path.as_os_str().to_str().unwrap(); + let kernel_path = direct_kernel_boot_path(); - let r = std::panic::catch_unwind(|| { - assert!(target_api.remote_command("create", Some(create_config))); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .args(["--memory", "size=0,hotplug_method=virtio-mem"]) + .args([ + "--memory-zone", + "id=mem0,size=1G,hotplug_size=2G", + "id=mem1,size=1G,shared=on", + "id=mem2,size=1G,host_numa_node=0,hotplug_size=2G", + ]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--api-socket", &api_socket]) + .capture_output() + .default_disks() + .default_net() + .spawn() + .unwrap(); - // Then boot it - assert!(target_api.remote_command("boot", None)); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - guest.wait_vm_boot(None).unwrap(); + assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + guest.enable_memory_hotplug(); - // Sync and shutdown without powering off to prevent filesystem - // corruption. - guest.ssh_command("sync").unwrap(); - guest.ssh_command("sudo shutdown -H now").unwrap(); + resize_zone_command(&api_socket, "mem0", "3G"); + assert!(wait_until(Duration::from_secs(5), || guest + .get_total_memory() + .unwrap_or_default() + > 4_800_000)); + resize_zone_command(&api_socket, "mem2", "3G"); + assert!(wait_until(Duration::from_secs(5), || guest + .get_total_memory() + .unwrap_or_default() + > 6_720_000)); + resize_zone_command(&api_socket, "mem0", "2G"); + assert!(wait_until(Duration::from_secs(5), || guest + .get_total_memory() + .unwrap_or_default() + > 5_760_000)); + resize_zone_command(&api_socket, "mem2", "2G"); + assert!(wait_until(Duration::from_secs(5), || guest + .get_total_memory() + .unwrap_or_default() + > 4_800_000)); - // Wait for the guest to be fully shutdown - thread::sleep(std::time::Duration::new(20, 0)); + guest.reboot_linux(0); - // Then delete it - assert!(target_api.remote_command("delete", None)); + // Check the amount of RAM after reboot + assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); + assert!(guest.get_total_memory().unwrap_or_default() < 5_760_000); - assert!(target_api.remote_command("create", Some(create_config))); + // Check if we can still resize down to the initial 'boot'size + resize_zone_command(&api_socket, "mem0", "1G"); + assert!(wait_until(Duration::from_secs(5), || guest + .get_total_memory() + .unwrap_or_default() + < 4_800_000)); + resize_zone_command(&api_socket, "mem2", "1G"); + assert!(wait_until(Duration::from_secs(5), || guest + .get_total_memory() + .unwrap_or_default() + < 3_840_000)); + }); - // Then boot it again - assert!(target_api.remote_command("boot", None)); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - guest.wait_vm_boot(None).unwrap(); + handle_child_output(r, &output); + } - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - }); + #[test] + #[cfg(not(feature = "mshv"))] // See #7456 + fn test_guest_numa_nodes() { + _test_guest_numa_nodes(false); + } - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + #[test] + #[cfg(target_arch = "x86_64")] + fn test_iommu_segments() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - handle_child_output(r, &output); -} + // Prepare another disk file for the virtio-disk device + let test_disk_path = String::from( + guest + .tmp_dir + .as_path() + .join("test-disk.raw") + .to_str() + .unwrap(), + ); + assert!( + exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() + ); + assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); -// Start cloud-hypervisor with no VM parameters, only the API server running. -// From the API: Create a VM, boot it and check that it looks as expected. -// Then we pause the VM, check that it's no longer available. -// Finally we resume the VM and check that it's available. -fn _test_api_pause_resume(target_api: TargetApi, guest: Guest) { - let mut child = GuestCommand::new(&guest) - .args(target_api.guest_args()) - .capture_output() - .spawn() - .unwrap(); + let api_socket = temp_api_path(&guest.tmp_dir); + let mut cmd = GuestCommand::new(&guest); - thread::sleep(std::time::Duration::new(1, 0)); + cmd.default_cpus() + .args(["--api-socket", &api_socket]) + .default_memory() + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--platform", + &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS},iommu_segments=[1]"), + ]) + .default_disks() + .capture_output() + .default_net(); - // Verify API server is running - assert!(target_api.remote_command("ping", None)); + let mut child = cmd.spawn().unwrap(); - // Create the VM first - let cpu_count: u8 = 4; - let request_body = guest.api_create_body( - cpu_count, - direct_kernel_boot_path().to_str().unwrap(), - DIRECT_KERNEL_BOOT_CMDLINE, - ); + guest.wait_vm_boot().unwrap(); - let temp_config_path = guest.tmp_dir.as_path().join("config"); - std::fs::write(&temp_config_path, request_body).unwrap(); - let create_config = temp_config_path.as_os_str().to_str().unwrap(); + let r = std::panic::catch_unwind(|| { + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some( + format!( + "path={},id=test0,pci_segment=1,iommu=on", + test_disk_path.as_str() + ) + .as_str(), + ), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0001:00:01.0\"}") + ); - assert!(target_api.remote_command("create", Some(create_config))); + // Check IOMMU setup + assert!( + guest + .does_device_vendor_pair_match("0x1057", "0x1af4") + .unwrap_or_default() + ); + assert!( + guest + .ssh_command("ls /sys/kernel/iommu_groups/*/devices") + .unwrap() + .contains("0001:00:01.0") + ); + }); - // Then boot it - assert!(target_api.remote_command("boot", None)); - thread::sleep(std::time::Duration::new(20, 0)); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - let r = std::panic::catch_unwind(|| { - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - - // We now pause the VM - assert!(target_api.remote_command("pause", None)); - - // Check pausing again fails - assert!(!target_api.remote_command("pause", None)); - - thread::sleep(std::time::Duration::new(2, 0)); - - // SSH into the VM should fail - ssh_command_ip( - "grep -c processor /proc/cpuinfo", - &guest.network.guest_ip, - 2, - 5, - ) - .unwrap_err(); - - // Resume the VM - assert!(target_api.remote_command("resume", None)); - - // Check resuming again fails - assert!(!target_api.remote_command("resume", None)); - - thread::sleep(std::time::Duration::new(2, 0)); - - // Now we should be able to SSH back in and get the right number of CPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); -} - -fn _test_pty_interaction(pty_path: PathBuf) { - let mut cf = std::fs::OpenOptions::new() - .write(true) - .read(true) - .open(pty_path) - .unwrap(); - - // Some dumb sleeps but we don't want to write - // before the console is up and we don't want - // to try and write the next line before the - // login process is ready. - thread::sleep(std::time::Duration::new(5, 0)); - assert_eq!(cf.write(b"cloud\n").unwrap(), 6); - thread::sleep(std::time::Duration::new(2, 0)); - assert_eq!(cf.write(b"cloud123\n").unwrap(), 9); - thread::sleep(std::time::Duration::new(2, 0)); - assert_eq!(cf.write(b"echo test_pty_console\n").unwrap(), 22); - thread::sleep(std::time::Duration::new(2, 0)); - - // read pty and ensure they have a login shell - // some fairly hacky workarounds to avoid looping - // forever in case the channel is blocked getting output - let ptyc = pty_read(cf); - let mut empty = 0; - let mut prev = String::new(); - loop { - thread::sleep(std::time::Duration::new(2, 0)); - match ptyc.try_recv() { - Ok(line) => { - empty = 0; - prev = prev + &line; - if prev.contains("test_pty_console") { - break; - } - } - Err(mpsc::TryRecvError::Empty) => { - empty += 1; - assert!(empty <= 5, "No login on pty"); - } - _ => { - panic!("No login on pty") - } - } + handle_child_output(r, &output); } -} - -fn prepare_virtiofsd(tmp_dir: &TempDir, shared_dir: &str) -> (std::process::Child, String) { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); - - let mut virtiofsd_path = workload_path; - virtiofsd_path.push("virtiofsd"); - let virtiofsd_path = String::from(virtiofsd_path.to_str().unwrap()); - let virtiofsd_socket_path = - String::from(tmp_dir.as_path().join("virtiofs.sock").to_str().unwrap()); + #[test] + fn test_pci_msi() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_pci_msi(&guest); + } - // Start the daemon - let child = Command::new(virtiofsd_path.as_str()) - .args(["--shared-dir", shared_dir]) - .args(["--socket-path", virtiofsd_socket_path.as_str()]) - .args(["--cache", "never"]) - .spawn() - .unwrap(); + #[test] + fn test_virtio_net_ctrl_queue() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_virtio_net_ctrl_queue(&guest); + } - thread::sleep(std::time::Duration::new(10, 0)); + #[test] + fn test_pci_multiple_segments() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_pci_multiple_segments(&guest, MAX_NUM_PCI_SEGMENTS, 15u16); + } - (child, virtiofsd_socket_path) -} + #[test] + fn test_pci_multiple_segments_numa_node() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); -fn prepare_vubd( - tmp_dir: &TempDir, - blk_img: &str, - num_queues: usize, - rdonly: bool, - direct: bool, -) -> (std::process::Child, String) { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); - - let mut blk_file_path = workload_path; - blk_file_path.push(blk_img); - let blk_file_path = String::from(blk_file_path.to_str().unwrap()); - - let vubd_socket_path = String::from(tmp_dir.as_path().join("vub.sock").to_str().unwrap()); - - // Start the daemon - let child = Command::new(clh_command("vhost_user_block")) - .args([ - "--block-backend", - format!( - "path={blk_file_path},socket={vubd_socket_path},num_queues={num_queues},readonly={rdonly},direct={direct}" - ) - .as_str(), - ]) - .spawn() - .unwrap(); + // Prepare another disk file for the virtio-disk device + let test_disk_path = String::from( + guest + .tmp_dir + .as_path() + .join("test-disk.raw") + .to_str() + .unwrap(), + ); + assert!( + exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() + ); + assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); + const TEST_DISK_NODE: u16 = 1; - thread::sleep(std::time::Duration::new(10, 0)); + let mut child = GuestCommand::new(&guest) + .args(["--platform", "num_pci_segments=2"]) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=0"]) + .args(["--memory-zone", "id=mem0,size=256M", "id=mem1,size=256M"]) + .args([ + "--numa", + "guest_numa_id=0,cpus=[0],distances=[1@20],memory_zones=mem0,pci_segments=[0]", + "guest_numa_id=1,cpus=[1],distances=[0@20],memory_zones=mem1,pci_segments=[1]", + ]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--api-socket", &api_socket]) + .capture_output() + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={test_disk_path},pci_segment={TEST_DISK_NODE}").as_str(), + ]) + .default_net() + .spawn() + .unwrap(); - (child, vubd_socket_path) -} + let cmd = "cat /sys/block/vdc/device/../numa_node"; -fn temp_vsock_path(tmp_dir: &TempDir) -> String { - String::from(tmp_dir.as_path().join("vsock").to_str().unwrap()) -} + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); -fn temp_api_path(tmp_dir: &TempDir) -> String { - String::from( - tmp_dir - .as_path() - .join("cloud-hypervisor.sock") - .to_str() - .unwrap(), - ) -} + assert_eq!( + guest + .ssh_command(cmd) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + TEST_DISK_NODE + ); -fn temp_event_monitor_path(tmp_dir: &TempDir) -> String { - String::from(tmp_dir.as_path().join("event.json").to_str().unwrap()) -} + // Each PNP0A08 host bridge in the DSDT must expose a unique + // _UID matching its PCI segment id. Linux surfaces the + // evaluated _UID via /sys/bus/acpi/devices/PNP0A08:*/uid. + // This test uses firmware boot on aarch64, so ACPI is + // available on both supported architectures. + let mut uids: Vec = guest + .ssh_command("cat /sys/bus/acpi/devices/PNP0A08:*/uid") + .unwrap() + .lines() + .filter_map(|l| l.trim().parse::().ok()) + .collect(); + uids.sort(); + assert_eq!(uids, vec![0u16, 1u16]); + }); -// Creates the directory and returns the path. -fn temp_snapshot_dir_path(tmp_dir: &TempDir) -> String { - let snapshot_dir = String::from(tmp_dir.as_path().join("snapshot").to_str().unwrap()); - std::fs::create_dir(&snapshot_dir).unwrap(); - snapshot_dir -} + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); -fn temp_vmcore_file_path(tmp_dir: &TempDir) -> String { - let vmcore_file = String::from(tmp_dir.as_path().join("vmcore").to_str().unwrap()); - vmcore_file -} + handle_child_output(r, &output); + } -// Creates the path for direct kernel boot and return the path. -// For x86_64, this function returns the vmlinux kernel path. -// For AArch64, this function returns the PE kernel path. -fn direct_kernel_boot_path() -> PathBuf { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + #[test] + fn test_direct_kernel_boot() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_direct_kernel_boot(&guest); + } - let mut kernel_path = workload_path; + #[test] #[cfg(target_arch = "x86_64")] - kernel_path.push("vmlinux-x86_64"); - #[cfg(target_arch = "aarch64")] - kernel_path.push("Image-arm64"); - - kernel_path -} - -fn edk2_path() -> PathBuf { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); - let mut edk2_path = workload_path; - edk2_path.push(OVMF_NAME); + fn test_direct_kernel_boot_bzimage() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - edk2_path -} + let mut kernel_path = direct_kernel_boot_path(); + // Replace the default kernel with the bzImage. + kernel_path.pop(); + kernel_path.push("bzImage-x86_64"); -fn cloud_hypervisor_release_path() -> String { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - let mut ch_release_path = workload_path; - #[cfg(target_arch = "x86_64")] - ch_release_path.push("cloud-hypervisor-static"); - #[cfg(target_arch = "aarch64")] - ch_release_path.push("cloud-hypervisor-static-aarch64"); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - ch_release_path.into_os_string().into_string().unwrap() -} + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); + assert!(guest.get_total_memory().unwrap_or_default() > 480_000); -fn prepare_vhost_user_net_daemon( - tmp_dir: &TempDir, - ip: &str, - tap: Option<&str>, - mtu: Option, - num_queues: usize, - client_mode: bool, -) -> (std::process::Command, String) { - let vunet_socket_path = String::from(tmp_dir.as_path().join("vunet.sock").to_str().unwrap()); + let grep_cmd = "grep -c PCI-MSI /proc/interrupts"; + assert_eq!( + guest + .ssh_command(grep_cmd) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 12 + ); + }); - // Start the daemon - let mut net_params = format!( - "ip={ip},mask=255.255.255.0,socket={vunet_socket_path},num_queues={num_queues},queue_size=1024,client={client_mode}" - ); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - if let Some(tap) = tap { - net_params.push_str(format!(",tap={tap}").as_str()); + handle_child_output(r, &output); } - if let Some(mtu) = mtu { - net_params.push_str(format!(",mtu={mtu}").as_str()); + #[test] + fn test_virtio_block_io_uring() { + let guest = + make_virtio_block_guest(&GuestFactory::new_regular_guest_factory(), JAMMY_IMAGE_NAME); + _test_virtio_block(&guest, false, true, false, false, ImageType::Raw); } - let mut command = Command::new(clh_command("vhost_user_net")); - command.args(["--net-backend", net_params.as_str()]); + #[test] + fn test_virtio_block_aio() { + let guest = + make_virtio_block_guest(&GuestFactory::new_regular_guest_factory(), JAMMY_IMAGE_NAME) + .with_cpu(4); + _test_virtio_block(&guest, true, false, false, false, ImageType::Raw); + } - (command, vunet_socket_path) -} + #[test] + fn test_virtio_block_sync() { + let guest = + make_virtio_block_guest(&GuestFactory::new_regular_guest_factory(), JAMMY_IMAGE_NAME) + .with_cpu(4); + _test_virtio_block(&guest, true, true, false, false, ImageType::Raw); + } -fn prepare_swtpm_daemon(tmp_dir: &TempDir) -> (std::process::Command, String) { - let swtpm_tpm_dir = String::from(tmp_dir.as_path().join("swtpm").to_str().unwrap()); - let swtpm_socket_path = String::from( - tmp_dir - .as_path() - .join("swtpm") - .join("swtpm.sock") - .to_str() - .unwrap(), - ); - std::fs::create_dir(&swtpm_tpm_dir).unwrap(); - - let mut swtpm_command = Command::new("swtpm"); - let swtpm_args = [ - "socket", - "--tpmstate", - &format!("dir={swtpm_tpm_dir}"), - "--ctrl", - &format!("type=unixio,path={swtpm_socket_path}"), - "--flags", - "startup-clear", - "--tpm2", - ]; - swtpm_command.args(swtpm_args); - - (swtpm_command, swtpm_socket_path) -} + #[test] + fn test_compute_file_checksum_empty() { + let mut reader = io::Cursor::new(vec![]); + let checksum = compute_file_checksum(&mut reader, 0); + assert_eq!(checksum, 5381); + } -fn remote_command(api_socket: &str, command: &str, arg: Option<&str>) -> bool { - let mut cmd = Command::new(clh_command("ch-remote")); - cmd.args([&format!("--api-socket={api_socket}"), command]); + #[test] + fn test_compute_file_checksum_small() { + let data = b"hello world"; + let mut reader = io::Cursor::new(data); + let checksum = compute_file_checksum(&mut reader, data.len() as u64); + assert_eq!(checksum, 894552257); + } - if let Some(arg) = arg { - cmd.arg(arg); + #[test] + fn test_compute_file_checksum_same_data() { + let data = b"test data 123"; + let mut reader1 = io::Cursor::new(data); + let mut reader2 = io::Cursor::new(data); + let checksum1 = compute_file_checksum(&mut reader1, data.len() as u64); + let checksum2 = compute_file_checksum(&mut reader2, data.len() as u64); + assert_eq!(checksum1, checksum2); } - let output = cmd.output().unwrap(); - if output.status.success() { - true - } else { - eprintln!("Error running ch-remote command: {:?}", &cmd); - let stderr = String::from_utf8_lossy(&output.stderr); - eprintln!("stderr: {stderr}"); - false + + #[test] + fn test_compute_file_checksum_different_data() { + let data1 = b"data1"; + let data2 = b"data2"; + let mut reader1 = io::Cursor::new(data1); + let mut reader2 = io::Cursor::new(data2); + let checksum1 = compute_file_checksum(&mut reader1, data1.len() as u64); + let checksum2 = compute_file_checksum(&mut reader2, data2.len() as u64); + assert_ne!(checksum1, checksum2); } -} -fn remote_command_w_output(api_socket: &str, command: &str, arg: Option<&str>) -> (bool, Vec) { - let mut cmd = Command::new(clh_command("ch-remote")); - cmd.args([&format!("--api-socket={api_socket}"), command]); + #[test] + fn test_compute_file_checksum_large_data() { + let size = 20 * 1024 * 1024; + let data = vec![0xABu8; size]; + let mut reader = io::Cursor::new(data); + let checksum = compute_file_checksum(&mut reader, size as u64); + // Should only read first 16MB + assert!(checksum != 5381); - if let Some(arg) = arg { - cmd.arg(arg); + // Verify only 16MB was read + let position = reader.position(); + assert_eq!(position, 16 * 1024 * 1024); } - let output = cmd.output().expect("Failed to launch ch-remote"); - - (output.status.success(), output.stdout) -} + #[test] + fn test_virtio_block_qcow2() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_cpu(4); + _test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2); + } -fn resize_command( - api_socket: &str, - desired_vcpus: Option, - desired_ram: Option, - desired_balloon: Option, - event_file: Option<&str>, -) -> bool { - let mut cmd = Command::new(clh_command("ch-remote")); - cmd.args([&format!("--api-socket={api_socket}"), "resize"]); + #[test] + fn test_virtio_block_qcow2_zlib() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2_ZLIB.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_cpu(4); + _test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2); + } - if let Some(desired_vcpus) = desired_vcpus { - cmd.arg(format!("--cpus={desired_vcpus}")); + #[test] + fn test_virtio_block_qcow2_zstd() { + let guest = make_virtio_block_guest( + &GuestFactory::new_regular_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_ZSTD, + ); + _test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2); } - if let Some(desired_ram) = desired_ram { - cmd.arg(format!("--memory={desired_ram}")); + #[test] + fn test_virtio_block_qcow2_backing_zstd_file() { + let guest = make_virtio_block_guest( + &GuestFactory::new_regular_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE, + ); + _test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2); } - if let Some(desired_balloon) = desired_balloon { - cmd.arg(format!("--balloon={desired_balloon}")); + #[test] + fn test_virtio_block_qcow2_backing_uncompressed_file() { + let guest = make_virtio_block_guest( + &GuestFactory::new_regular_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_BACKING_UNCOMPRESSED_FILE, + ); + _test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2); } - let ret = cmd.status().expect("Failed to launch ch-remote").success(); + #[test] + fn test_virtio_block_qcow2_backing_raw_file() { + let guest = make_virtio_block_guest( + &GuestFactory::new_regular_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE, + ); + _test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2); + } - if let Some(event_path) = event_file { - let latest_events = [ - &MetaEvent { - event: "resizing".to_string(), - device_id: None, - }, - &MetaEvent { - event: "resized".to_string(), - device_id: None, - }, - ]; - // See: #5938 - thread::sleep(std::time::Duration::new(1, 0)); - assert!(check_latest_events_exact(&latest_events, event_path)); + /// Configuration for QCOW2 multiqueue test image setup + enum QcowTestImageConfig { + /// Simple QCOW2 image with given size (e.g., "256M") + Simple(&'static str), + /// QCOW2 overlay with backing file + WithBacking, } - ret -} + /// Helper to run QCOW2 multiqueue stress tests with shared setup/teardown. + /// + /// Creates a VM with multiple virtio queues on the test disk, then runs the + /// provided test closure. Handles VM lifecycle and consistency checks. + fn run_multiqueue_qcow2_test(image_config: &QcowTestImageConfig, test_fn: F) + where + F: FnOnce(&Guest) + std::panic::UnwindSafe, + { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); -fn resize_zone_command(api_socket: &str, id: &str, desired_size: &str) -> bool { - let mut cmd = Command::new(clh_command("ch-remote")); - cmd.args([ - &format!("--api-socket={api_socket}"), - "resize-zone", - &format!("--id={id}"), - &format!("--size={desired_size}"), - ]); + let test_image_path = guest.tmp_dir.as_path().join("test.qcow2"); + + // Create test image based on configuration and capture backing checksum if applicable + let initial_backing_checksum = match *image_config { + QcowTestImageConfig::Simple(size) => { + Command::new("qemu-img") + .arg("create") + .args(["-f", "qcow2"]) + .arg(test_image_path.to_str().unwrap()) + .arg(size) + .output() + .expect("Failed to create QCOW2 test image"); + None + } + QcowTestImageConfig::WithBacking => { + let backing_path = guest.tmp_dir.as_path().join("backing.qcow2"); + Command::new("qemu-img") + .arg("create") + .args(["-f", "qcow2"]) + .arg(backing_path.to_str().unwrap()) + .arg("256M") + .output() + .expect("Failed to create backing QCOW2"); + + Command::new("qemu-img") + .arg("create") + .args(["-f", "qcow2"]) + .args(["-b", backing_path.to_str().unwrap()]) + .args(["-F", "qcow2"]) + .arg(test_image_path.to_str().unwrap()) + .output() + .expect("Failed to create overlay QCOW2"); + + compute_backing_checksum(&test_image_path) + } + }; - cmd.status().expect("Failed to launch ch-remote").success() -} + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=8"]) + .args(["--memory", "size=1024M"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!( + "path={},num_queues=8", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + &format!( + "path={},num_queues=8,backing_files={},image_type=qcow2", + test_image_path.to_str().unwrap(), + if initial_backing_checksum.is_some() { + "on" + } else { + "off" + }, + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); -// setup OVS-DPDK bridge and ports -fn setup_ovs_dpdk() { - // setup OVS-DPDK - assert!(exec_host_command_status("service openvswitch-switch start").success()); - assert!(exec_host_command_status("ovs-vsctl init").success()); - assert!( - exec_host_command_status("ovs-vsctl set Open_vSwitch . other_config:dpdk-init=true") - .success() - ); - assert!(exec_host_command_status("service openvswitch-switch restart").success()); - - // Create OVS-DPDK bridge and ports - assert!(exec_host_command_status( - "ovs-vsctl add-br ovsbr0 -- set bridge ovsbr0 datapath_type=netdev", - ) - .success()); - assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success()); - assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user2 -- set Interface vhost-user2 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient2").success()); - assert!(exec_host_command_status("ip link set up dev ovsbr0").success()); - assert!(exec_host_command_status("service openvswitch-switch restart").success()); -} -fn cleanup_ovs_dpdk() { - assert!(exec_host_command_status("ovs-vsctl del-br ovsbr0").success()); - exec_host_command_status("rm -f ovs-vsctl /tmp/dpdkvhostclient1 /tmp/dpdkvhostclient2"); -} -// Setup two guests and ensure they are connected through ovs-dpdk -fn setup_ovs_dpdk_guests( - guest1: &Guest, - guest2: &Guest, - api_socket: &str, - release_binary: bool, -) -> (Child, Child) { - setup_ovs_dpdk(); - - let clh_path = if !release_binary { - clh_command("cloud-hypervisor") - } else { - cloud_hypervisor_release_path() - }; - - let mut child1 = GuestCommand::new_with_binary_path(guest1, &clh_path) - .args(["--cpus", "boot=2"]) - .args(["--memory", "size=0,shared=on"]) - .args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", guest1.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient1,num_queues=2,queue_size=256,vhost_mode=server"]) - .capture_output() - .spawn() - .unwrap(); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + test_fn(&guest); + }); - #[cfg(target_arch = "x86_64")] - let guest_net_iface = "ens5"; - #[cfg(target_arch = "aarch64")] - let guest_net_iface = "enp0s5"; + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - let r = std::panic::catch_unwind(|| { - guest1.wait_vm_boot(None).unwrap(); + handle_child_output(r, &output); - guest1 - .ssh_command(&format!( - "sudo ip addr add 172.100.0.1/24 dev {guest_net_iface}" - )) - .unwrap(); - guest1 - .ssh_command(&format!("sudo ip link set up dev {guest_net_iface}")) - .unwrap(); + disk_check_consistency( + guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), + None, + ); + disk_check_consistency(&test_image_path, initial_backing_checksum); + } - let guest_ip = guest1.network.guest_ip.clone(); - thread::spawn(move || { - ssh_command_ip( - "nc -l 12345", - &guest_ip, - DEFAULT_SSH_RETRIES, - DEFAULT_SSH_TIMEOUT, - ) - .unwrap(); - }); - }); - if r.is_err() { - cleanup_ovs_dpdk(); - - let _ = child1.kill(); - let output = child1.wait_with_output().unwrap(); - handle_child_output(r, &output); - panic!("Test should already be failed/panicked"); // To explicitly mark this block never return - } - - let mut child2 = GuestCommand::new_with_binary_path(guest2, &clh_path) - .args(["--api-socket", api_socket]) - .args(["--cpus", "boot=2"]) - .args(["--memory", "size=0,shared=on"]) - .args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", guest2.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient2,num_queues=2,queue_size=256,vhost_mode=server"]) - .capture_output() - .spawn() - .unwrap(); - - let r = std::panic::catch_unwind(|| { - guest2.wait_vm_boot(None).unwrap(); + #[test] + fn test_virtio_block_qcow2_multiqueue_writes() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| { + assert_eq!( + guest + .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 8, + "Expected 8 queues on vdc" + ); - guest2 - .ssh_command(&format!( - "sudo ip addr add 172.100.0.2/24 dev {guest_net_iface}" - )) - .unwrap(); - guest2 - .ssh_command(&format!("sudo ip link set up dev {guest_net_iface}")) - .unwrap(); + guest + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test") + .expect("Failed to mount disk"); - // Check the connection works properly between the two VMs - guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); - }); - if r.is_err() { - cleanup_ovs_dpdk(); + guest + .ssh_command( + "for i in $(seq 1 8); do \ + sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=32 conv=fsync & \ + done; wait", + ) + .expect("Failed to write files in parallel"); - let _ = child1.kill(); - let _ = child2.kill(); - let output = child2.wait_with_output().unwrap(); - handle_child_output(r, &output); - panic!("Test should already be failed/panicked"); // To explicitly mark this block never return - } + assert_eq!( + guest + .ssh_command("ls /mnt/test/file* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 8, + "Expected 8 files to be created" + ); - (child1, child2) -} + guest + .ssh_command("sudo rm -f /mnt/test/file*") + .expect("Failed to remove files"); -enum FwType { - Ovmf, - RustHypervisorFirmware, -} + // Do another round of heavy parallel I/O + guest + .ssh_command( + "for i in $(seq 1 16); do \ + sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=16 conv=fsync & \ + done; wait", + ) + .expect("Failed to write files in second round"); -fn fw_path(_fw_type: FwType) -> String { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + assert_eq!( + guest + .ssh_command("ls /mnt/test/file* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 16, + "Expected 16 files after second round" + ); - let mut fw_path = workload_path; - #[cfg(target_arch = "aarch64")] - fw_path.push("CLOUDHV_EFI.fd"); - #[cfg(target_arch = "x86_64")] - { - match _fw_type { - FwType::Ovmf => fw_path.push(OVMF_NAME), - FwType::RustHypervisorFirmware => fw_path.push("hypervisor-fw"), - } + guest + .ssh_command("sudo umount /mnt/test") + .expect("Failed to unmount"); + }); } - fw_path.to_str().unwrap().to_string() -} + #[test] + fn test_virtio_block_qcow2_multiqueue_mixed_rw() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("512M"), |guest| { + guest + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test") + .expect("Failed to mount disk"); -#[derive(Debug)] -struct MetaEvent { - event: String, - device_id: Option, -} + guest + .ssh_command( + "sudo dd if=/dev/urandom of=/mnt/test/readfile bs=1M count=64 conv=fsync", + ) + .expect("Failed to create initial file"); -impl MetaEvent { - pub fn match_with_json_event(&self, v: &serde_json::Value) -> bool { - let mut matched = false; - if v["event"].as_str().unwrap() == self.event { - if let Some(device_id) = &self.device_id { - if v["properties"]["id"].as_str().unwrap() == device_id { - matched = true - } - } else { - matched = true; - } - } - matched - } -} + guest + .ssh_command( + "for i in $(seq 1 4); do \ + sudo dd if=/mnt/test/readfile of=/dev/null bs=64K & \ + sudo dd if=/dev/urandom of=/mnt/test/writefile$i bs=1M count=32 conv=fsync & \ + done; wait", + ) + .expect("Failed mixed read/write workload"); -// Parse the event_monitor file based on the format that each event -// is followed by a double newline -fn parse_event_file(event_file: &str) -> Vec { - let content = fs::read(event_file).unwrap(); - let mut ret = Vec::new(); - for entry in String::from_utf8_lossy(&content) - .trim() - .split("\n\n") - .collect::>() - { - ret.push(serde_json::from_str(entry).unwrap()); - } + assert_eq!( + guest + .ssh_command("ls /mnt/test/writefile* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4, + "Expected 4 write files" + ); - ret -} + guest + .ssh_command( + "for i in $(seq 1 4); do \ + sudo dd if=/mnt/test/writefile$i of=/dev/null bs=64K & \ + sudo dd if=/dev/urandom of=/mnt/test/newfile$i bs=1M count=16 conv=fsync & \ + done; wait", + ) + .expect("Failed second mixed workload"); -// Return true if all events from the input 'expected_events' are matched sequentially -// with events from the 'event_file' -fn check_sequential_events(expected_events: &[&MetaEvent], event_file: &str) -> bool { - let json_events = parse_event_file(event_file); - let len = expected_events.len(); - let mut idx = 0; - for e in &json_events { - if idx == len { - break; - } - if expected_events[idx].match_with_json_event(e) { - idx += 1; - } + guest + .ssh_command("sudo umount /mnt/test") + .expect("Failed to unmount"); + }); } - let ret = idx == len; - - if !ret { - eprintln!( - "\n\n==== Start 'check_sequential_events' failed ==== \ - \n\nexpected_events={expected_events:?}\nactual_events={json_events:?} \ - \n\n==== End 'check_sequential_events' failed ====", - ); - } + #[test] + fn test_virtio_block_qcow2_multiqueue_backing() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::WithBacking, |guest| { + guest + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test") + .expect("Failed to mount disk"); - ret -} + guest + .ssh_command( + "for i in $(seq 1 8); do \ + sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=16 conv=fsync & \ + done; wait", + ) + .expect("Failed to write files"); -// Return true if all events from the input 'expected_events' are matched exactly -// with events from the 'event_file' -fn check_sequential_events_exact(expected_events: &[&MetaEvent], event_file: &str) -> bool { - let json_events = parse_event_file(event_file); - assert!(expected_events.len() <= json_events.len()); - let json_events = &json_events[..expected_events.len()]; + guest + .ssh_command( + "for i in $(seq 1 8); do \ + sudo dd if=/mnt/test/file$i of=/dev/null bs=64K & \ + sudo dd if=/dev/urandom of=/mnt/test/new$i bs=1M count=8 conv=fsync & \ + done; wait", + ) + .expect("Failed mixed backing/overlay workload"); - for (idx, e) in json_events.iter().enumerate() { - if !expected_events[idx].match_with_json_event(e) { - eprintln!( - "\n\n==== Start 'check_sequential_events_exact' failed ==== \ - \n\nexpected_events={expected_events:?}\nactual_events={json_events:?} \ - \n\n==== End 'check_sequential_events_exact' failed ====", + assert_eq!( + guest + .ssh_command("ls /mnt/test/new* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 8, + "Expected 8 new files" ); - return false; - } + guest + .ssh_command("sudo umount /mnt/test") + .expect("Failed to unmount"); + }); } - true -} - -// Return true if events from the input 'latest_events' are matched exactly -// with the most recent events from the 'event_file' -fn check_latest_events_exact(latest_events: &[&MetaEvent], event_file: &str) -> bool { - let json_events = parse_event_file(event_file); - assert!(latest_events.len() <= json_events.len()); - let json_events = &json_events[(json_events.len() - latest_events.len())..]; + #[test] + fn test_virtio_block_qcow2_multiqueue_random_4k() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| { + guest + .ssh_command( + "for i in $(seq 1 8); do \ + sudo dd if=/dev/urandom of=/dev/vdc bs=4K count=1000 seek=$((RANDOM % 60000)) conv=notrunc & \ + done; wait", + ) + .expect("Failed random 4K writes round 1"); - for (idx, e) in json_events.iter().enumerate() { - if !latest_events[idx].match_with_json_event(e) { - eprintln!( - "\n\n==== Start 'check_latest_events_exact' failed ==== \ - \n\nexpected_events={latest_events:?}\nactual_events={json_events:?} \ - \n\n==== End 'check_latest_events_exact' failed ====", - ); + guest + .ssh_command( + "for i in $(seq 1 8); do \ + sudo dd if=/dev/urandom of=/dev/vdc bs=4K count=1000 seek=$((RANDOM % 60000)) conv=notrunc & \ + done; wait", + ) + .expect("Failed random 4K writes round 2"); - return false; - } + guest + .ssh_command( + "for i in $(seq 1 4); do \ + sudo dd if=/dev/vdc of=/dev/null bs=4K count=500 skip=$((RANDOM % 60000)) & \ + sudo dd if=/dev/urandom of=/dev/vdc bs=4K count=500 seek=$((RANDOM % 60000)) conv=notrunc & \ + done; wait", + ) + .expect("Failed mixed random I/O"); + }); } - true -} - -fn test_cpu_topology(threads_per_core: u8, cores_per_package: u8, packages: u8, use_fw: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let total_vcpus = threads_per_core * cores_per_package * packages; - let direct_kernel_boot_path = direct_kernel_boot_path(); - let mut kernel_path = direct_kernel_boot_path.to_str().unwrap(); - let fw_path = fw_path(FwType::RustHypervisorFirmware); - if use_fw { - kernel_path = fw_path.as_str(); - } - - let mut child = GuestCommand::new(&guest) - .args([ - "--cpus", - &format!( - "boot={total_vcpus},topology={threads_per_core}:{cores_per_package}:1:{packages}" - ), - ]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); - - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - assert_eq!( - guest.get_cpu_count().unwrap_or_default(), - u32::from(total_vcpus) - ); - assert_eq!( + #[test] + fn test_virtio_block_qcow2_multiqueue_fsync() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| { guest - .ssh_command("lscpu | grep \"per core\" | cut -f 2 -d \":\" | sed \"s# *##\"") - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - threads_per_core - ); - - assert_eq!( + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); guest - .ssh_command("lscpu | grep \"per socket\" | cut -f 2 -d \":\" | sed \"s# *##\"") - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - cores_per_package - ); + .ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test") + .expect("Failed to mount disk"); - assert_eq!( guest - .ssh_command("lscpu | grep \"Socket\" | cut -f 2 -d \":\" | sed \"s# *##\"") - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - packages - ); - - #[cfg(target_arch = "x86_64")] - { - let mut cpu_id = 0; - for package_id in 0..packages { - for core_id in 0..cores_per_package { - for _ in 0..threads_per_core { - assert_eq!( - guest - .ssh_command(&format!("cat /sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id")) - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - package_id - ); - - assert_eq!( - guest - .ssh_command(&format!( - "cat /sys/devices/system/cpu/cpu{cpu_id}/topology/core_id" - )) - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - core_id - ); - - cpu_id += 1; - } - } - } - } - }); + .ssh_command( + "for i in $(seq 1 8); do \ + (for j in $(seq 1 100); do \ + echo \"data$j\" | sudo tee /mnt/test/file${i}_$j > /dev/null && sudo sync; \ + done) & \ + done; wait", + ) + .expect("Failed fsync storm round 1"); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + assert_eq!( + guest + .ssh_command("ls /mnt/test/file* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 800, + "Expected 800 files (8 processes x 100 files)" + ); - handle_child_output(r, &output); -} + guest + .ssh_command( + "for i in $(seq 1 8); do \ + (for j in $(seq 1 50); do \ + sudo dd if=/dev/urandom of=/mnt/test/dd${i}_$j bs=4K count=1 conv=fsync 2>/dev/null; \ + done) & \ + done; wait", + ) + .expect("Failed fsync storm round 2"); -#[allow(unused_variables)] -fn _test_guest_numa_nodes(acpi: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = if acpi { - edk2_path() - } else { - direct_kernel_boot_path() - }; - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=6,max=12"]) - .args(["--memory", "size=0,hotplug_method=virtio-mem"]) - .args([ - "--memory-zone", - "id=mem0,size=1G,hotplug_size=3G", - "id=mem1,size=2G,hotplug_size=3G", - "id=mem2,size=3G,hotplug_size=3G", - ]) - .args([ - "--numa", - "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", - "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", - "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", - ]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--api-socket", &api_socket]) - .capture_output() - .default_disks() - .default_net() - .spawn() - .unwrap(); + guest + .ssh_command("sudo umount /mnt/test") + .expect("Failed to unmount"); + }); + } - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + #[test] + fn test_virtio_block_qcow2_multiqueue_metadata() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| { + guest + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test") + .expect("Failed to mount disk"); - guest.check_numa_common( - Some(&[960_000, 1_920_000, 2_880_000]), - Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), - Some(&["10 15 20", "20 10 25", "25 30 10"]), - ); + guest + .ssh_command( + "for i in $(seq 1 8); do \ + (for j in $(seq 1 50); do \ + sudo mkdir -p /mnt/test/dir$i/subdir$j; \ + done) & \ + done; wait", + ) + .expect("Failed parallel mkdir"); - // AArch64 currently does not support hotplug, and therefore we only - // test hotplug-related function on x86_64 here. - #[cfg(target_arch = "x86_64")] - { - guest.enable_memory_hotplug(); + let dir_count: u32 = guest + .ssh_command("find /mnt/test -type d | wc -l") + .expect("Failed to count directories") + .trim() + .parse() + .unwrap_or(0); + assert!( + dir_count >= 400, + "Expected at least 400 directories, got {dir_count}" + ); - // Resize every memory zone and check each associated NUMA node - // has been assigned the right amount of memory. - resize_zone_command(&api_socket, "mem0", "4G"); - resize_zone_command(&api_socket, "mem1", "4G"); - resize_zone_command(&api_socket, "mem2", "4G"); - // Resize to the maximum amount of CPUs and check each NUMA - // node has been assigned the right CPUs set. - resize_command(&api_socket, Some(12), None, None, None); - thread::sleep(std::time::Duration::new(5, 0)); + guest + .ssh_command( + "for i in $(seq 1 8); do \ + (for j in $(seq 1 100); do \ + sudo touch /mnt/test/dir$i/file$j; \ + done) & \ + done; wait", + ) + .expect("Failed parallel touch"); - guest.check_numa_common( - Some(&[3_840_000, 3_840_000, 3_840_000]), - Some(&[vec![0, 1, 2, 9], vec![3, 4, 6, 7, 8], vec![5, 10, 11]]), - None, + let file_count: u32 = guest + .ssh_command("find /mnt/test -type f | wc -l") + .expect("Failed to count files") + .trim() + .parse() + .unwrap_or(0); + assert!( + file_count >= 400, + "Expected at least 400 files, got {file_count}" ); - } - }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + guest + .ssh_command( + "for i in $(seq 1 4); do \ + sudo rm -rf /mnt/test/dir$i & \ + (for j in $(seq 1 50); do \ + sudo touch /mnt/test/newfile${i}_$j; \ + done) & \ + done; wait", + ) + .expect("Failed parallel rm + touch"); - handle_child_output(r, &output); -} + guest + .ssh_command( + "for i in $(seq 5 8); do \ + (for j in $(seq 1 25); do \ + sudo mv /mnt/test/dir$i/file$j /mnt/test/dir$i/renamed$j 2>/dev/null || true; \ + done) & \ + done; wait", + ) + .expect("Failed parallel rename"); -#[allow(unused_variables)] -fn _test_power_button(acpi: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); - let api_socket = temp_api_path(&guest.tmp_dir); + guest + .ssh_command("sync && sudo umount /mnt/test") + .expect("Failed to unmount"); + }); + } - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = if acpi { - edk2_path() - } else { - direct_kernel_boot_path() - }; - - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .capture_output() - .default_disks() - .default_net() - .args(["--api-socket", &api_socket]); - - let child = cmd.spawn().unwrap(); - - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - assert!(remote_command(&api_socket, "power-button", None)); - }); - - let output = child.wait_with_output().unwrap(); - assert!(output.status.success()); - handle_child_output(r, &output); -} + #[test] + fn test_virtio_block_qcow2_multiqueue_discard_mount() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("256M"), |guest| { + guest + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); -type PrepareNetDaemon = dyn Fn( - &TempDir, - &str, - Option<&str>, - Option, - usize, - bool, -) -> (std::process::Command, String); - -fn test_vhost_user_net( - tap: Option<&str>, - num_queues: usize, - prepare_daemon: &PrepareNetDaemon, - generate_host_mac: bool, - client_mode_daemon: bool, -) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - - let kernel_path = direct_kernel_boot_path(); - - let host_mac = if generate_host_mac { - Some(MacAddr::local_random()) - } else { - None - }; - - let mtu = Some(3000); - - let (mut daemon_command, vunet_socket_path) = prepare_daemon( - &guest.tmp_dir, - &guest.network.host_ip, - tap, - mtu, - num_queues, - client_mode_daemon, - ); - - let net_params = format!( - "vhost_user=true,mac={},socket={},num_queues={},queue_size=1024{},vhost_mode={},mtu=3000", - guest.network.guest_mac, - vunet_socket_path, - num_queues, - if let Some(host_mac) = host_mac { - format!(",host_mac={host_mac}") - } else { - "".to_owned() - }, - if client_mode_daemon { - "server" - } else { - "client" - }, - ); - - let mut ch_command = GuestCommand::new(&guest); - ch_command - .args(["--cpus", format!("boot={}", num_queues / 2).as_str()]) - .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", net_params.as_str()]) - .args(["--api-socket", &api_socket]) - .capture_output(); - - let mut daemon_child: std::process::Child; - let mut child: std::process::Child; - - if client_mode_daemon { - child = ch_command.spawn().unwrap(); - // Make sure the VMM is waiting for the backend to connect - thread::sleep(std::time::Duration::new(10, 0)); - daemon_child = daemon_command.spawn().unwrap(); - } else { - daemon_child = daemon_command.spawn().unwrap(); - // Make sure the backend is waiting for the VMM to connect - thread::sleep(std::time::Duration::new(10, 0)); - child = ch_command.spawn().unwrap(); - } - - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - if let Some(tap_name) = tap { - let tap_count = exec_host_command_output(&format!("ip link | grep -c {tap_name}")); - assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); - } + // Mount with discard option to enable automatic TRIM/DISCARD + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount -o discard /dev/vdc /mnt/test") + .expect("Failed to mount disk with discard option"); - if let Some(host_mac) = tap { - let mac_count = exec_host_command_output(&format!("ip link | grep -c {host_mac}")); - assert_eq!(String::from_utf8_lossy(&mac_count.stdout).trim(), "1"); - } + guest + .ssh_command( + "for i in $(seq 1 4); do \n\ + sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=32 conv=fsync & \n\ + done; wait", + ) + .expect("Failed to write files in parallel"); - #[cfg(target_arch = "aarch64")] - let iface = "enp0s4"; - #[cfg(target_arch = "x86_64")] - let iface = "ens4"; + assert_eq!( + guest + .ssh_command("ls /mnt/test/file* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4, + "Expected 4 files to be created" + ); - assert_eq!( guest - .ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str()) - .unwrap() - .trim(), - "3000" - ); + .ssh_command("sudo rm -f /mnt/test/file*") + .expect("Failed to remove files"); - // 1 network interface + default localhost ==> 2 interfaces - // It's important to note that this test is fully exercising the - // vhost-user-net implementation and the associated backend since - // it does not define any --net network interface. That means all - // the ssh communication in that test happens through the network - // interface backed by vhost-user-net. - assert_eq!( guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); + .ssh_command("sudo fstrim -v /mnt/test") + .expect("fstrim failed - DISCARD not working"); - // The following pci devices will appear on guest with PCI-MSI - // interrupt vectors assigned. - // 1 virtio-console with 3 vectors: config, Rx, Tx - // 1 virtio-blk with 2 vectors: config, Request - // 1 virtio-blk with 2 vectors: config, Request - // 1 virtio-rng with 2 vectors: config, Request - // Since virtio-net has 2 queue pairs, its vectors is as follows: - // 1 virtio-net with 5 vectors: config, Rx (2), Tx (2) - // Based on the above, the total vectors should 14. - #[cfg(target_arch = "x86_64")] - let grep_cmd = "grep -c PCI-MSI /proc/interrupts"; - #[cfg(target_arch = "aarch64")] - let grep_cmd = "grep -c ITS-PCI-MSIX /proc/interrupts"; - assert_eq!( guest - .ssh_command(grep_cmd) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 10 + (num_queues as u32) - ); - - // ACPI feature is needed. - #[cfg(target_arch = "x86_64")] - { - guest.enable_memory_hotplug(); - - // Add RAM to the VM - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); - - thread::sleep(std::time::Duration::new(10, 0)); - - // Here by simply checking the size (through ssh), we validate - // the connection is still working, which means vhost-user-net - // keeps working after the resize. - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); - } - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - thread::sleep(std::time::Duration::new(5, 0)); - let _ = daemon_child.kill(); - let _ = daemon_child.wait(); - - handle_child_output(r, &output); -} - -type PrepareBlkDaemon = dyn Fn(&TempDir, &str, usize, bool, bool) -> (std::process::Child, String); - -fn test_vhost_user_blk( - num_queues: usize, - readonly: bool, - direct: bool, - prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>, -) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - - let kernel_path = direct_kernel_boot_path(); - - let (blk_params, daemon_child) = { - let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap(); - // Start the daemon - let (daemon_child, vubd_socket_path) = - prepare_daemon(&guest.tmp_dir, "blk.img", num_queues, readonly, direct); - - ( - format!( - "vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128", - ), - Some(daemon_child), - ) - }; - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", format!("boot={num_queues}").as_str()]) - .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - blk_params.as_str(), - ]) - .default_net() - .args(["--api-socket", &api_socket]) - .capture_output() - .spawn() - .unwrap(); + .ssh_command( + "for i in $(seq 1 8); do \n\ + sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=16 conv=fsync & \n\ + done; wait", + ) + .expect("Failed to write files in second round"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + assert_eq!( + guest + .ssh_command("ls /mnt/test/file* | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 8, + "Expected 8 files after second round" + ); - // Check both if /dev/vdc exists and if the block size is 16M. - assert_eq!( guest - .ssh_command("lsblk | grep vdc | grep -c 16M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); - - // Check if this block is RO or RW. - assert_eq!( + .ssh_command("sudo umount /mnt/test") + .expect("Failed to unmount"); + }); + } + #[test] + fn test_virtio_block_qcow2_multiqueue_wide_writes() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("1G"), |guest| { + // Scattered write pattern - write to widely separated offsets in parallel. + // This should initiate many L2 table allocations simultaneously across different queues. guest - .ssh_command("lsblk | grep vdc | awk '{print $5}'") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - readonly as u32 - ); + .ssh_command( + "for i in $(seq 0 7); do \n\ + offset=$((i * 128)) \n\ + sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=16 seek=$offset conv=notrunc,fsync & \n\ + done; wait", + ) + .expect("Failed to write sparse pattern in parallel"); - // Check if the number of queues in /sys/block/vdc/mq matches the - // expected num_queues. - assert_eq!( + // Write known patterns to the same sparse locations guest - .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - num_queues as u32 - ); + .ssh_command( + "for i in $(seq 0 7); do \n\ + offset=$((i * 128)) \n\ + sudo dd if=/dev/zero of=/dev/vdc bs=1M count=8 seek=$offset conv=notrunc,fsync & \n\ + done; wait", + ) + .expect("Failed second sparse write pattern"); - // Mount the device - let mount_ro_rw_flag = if readonly { "ro,noload" } else { "rw" }; - guest.ssh_command("mkdir mount_image").unwrap(); - guest + // Even more aggressive sparse writes with smaller chunks but more of them + guest .ssh_command( - format!("sudo mount -o {mount_ro_rw_flag} -t ext4 /dev/vdc mount_image/").as_str(), + "for i in $(seq 0 15); do \n\ + offset=$((i * 64)) \n\ + sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=2 seek=$offset conv=notrunc,fsync & \n\ + done; wait", ) - .unwrap(); + .expect("Failed third sparse write pattern"); - // Check the content of the block device. The file "foo" should - // contain "bar". - assert_eq!( - guest.ssh_command("cat mount_image/foo").unwrap().trim(), - "bar" - ); + guest + .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M count=64") + .expect("Failed to read back data after sparse writes"); + }); + } - // ACPI feature is needed. - #[cfg(target_arch = "x86_64")] - { - guest.enable_memory_hotplug(); + #[test] + fn test_virtio_block_qcow2_multiqueue_discard_stress() { + run_multiqueue_qcow2_test(&QcowTestImageConfig::Simple("512M"), |guest| { + guest + .ssh_command("sudo mkfs.ext4 -F /dev/vdc") + .expect("Failed to format disk"); + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount -o discard /dev/vdc /mnt/test") + .expect("Failed to mount disk with discard option"); - // Add RAM to the VM - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + // Round 1: Start background writes while simultaneously doing DISCARD operations + // This stresses refcount table locking - writes increment refs, discard decrements + guest + .ssh_command( + "for i in $(seq 1 4); do \n\ + sudo dd if=/dev/urandom of=/mnt/test/file$i bs=1M count=32 & \n\ + done", + ) + .expect("Failed to start background writes"); - thread::sleep(std::time::Duration::new(10, 0)); + guest + .ssh_command( + "for i in $(seq 5 8); do \n\ + sudo dd if=/dev/urandom of=/mnt/test/temp$i bs=1M count=16 conv=fsync \n\ + sudo rm -f /mnt/test/temp$i & \n\ + done; \n\ + wait; \n\ + sudo fstrim -v /mnt/test", + ) + .expect("Failed to do parallel write-delete-discard"); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + guest + .ssh_command("wait") + .expect("Failed to wait for background writes"); - // Check again the content of the block device after the resize - // has been performed. assert_eq!( - guest.ssh_command("cat mount_image/foo").unwrap().trim(), - "bar" + guest + .ssh_command("ls /mnt/test/file* 2>/dev/null | wc -l") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4, + "Expected 4 files after round 1" ); - } - // Unmount the device - guest.ssh_command("sudo umount /dev/vdc").unwrap(); - guest.ssh_command("rm -r mount_image").unwrap(); - }); + // Round 2: More aggressive - 8 parallel writes with simultaneous blkdiscard on raw device + guest + .ssh_command("sudo umount /mnt/test") + .expect("Failed to unmount"); + + guest + .ssh_command( + "for i in $(seq 0 7); do \n\ + offset=$((i * 64)) \n\ + sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=4 seek=$offset conv=notrunc,fsync & \n\ + done; wait", + ) + .expect("Failed sparse writes"); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // Now discard half the regions while writing to the other half + guest + .ssh_command( + "for i in $(seq 0 3); do \n\ + offset=$((i * 64 * 1024 * 1024)) \n\ + sudo blkdiscard -o $offset -l $((4 * 1024 * 1024)) /dev/vdc & \n\ + done; \n\ + for i in $(seq 4 7); do \n\ + offset=$((i * 64)) \n\ + sudo dd if=/dev/zero of=/dev/vdc bs=1M count=4 seek=$offset conv=notrunc,fsync & \n\ + done; wait", + ) + .expect("Failed parallel discard and write stress test"); - if let Some(mut daemon_child) = daemon_child { - thread::sleep(std::time::Duration::new(5, 0)); - let _ = daemon_child.kill(); - let _ = daemon_child.wait(); + guest + .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M count=128") + .expect("Failed to read back data after discard stress"); + }); } - handle_child_output(r, &output); -} + #[test] + fn test_virtio_block_qcow2_uefi_direct_io() { + // Regression test for #8007. + // Place the QCOW2 OS image on a 4096 byte sector filesystem so + // O_DIRECT forces 4096 byte alignment on all I/O buffers. + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = edk2_path(); -fn test_boot_from_vhost_user_blk( - num_queues: usize, - readonly: bool, - direct: bool, - prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>, -) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - - let kernel_path = direct_kernel_boot_path(); - - let disk_path = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); - - let (blk_boot_params, daemon_child) = { - let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap(); - // Start the daemon - let (daemon_child, vubd_socket_path) = prepare_daemon( - &guest.tmp_dir, - disk_path.as_str(), - num_queues, - readonly, - direct, - ); + let mut workloads_path = dirs::home_dir().unwrap(); + workloads_path.push("workloads"); + let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap(); + let fs_img_path = img_dir.as_path().join("fs_4ksec.img"); - ( - format!( - "vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128", - ), - Some(daemon_child), - ) - }; - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", format!("boot={num_queues}").as_str()]) - .args(["--memory", "size=512M,shared=on"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--disk", - blk_boot_params.as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - ]) - .default_net() - .capture_output() - .spawn() - .unwrap(); + assert!( + exec_host_command_output(&format!("truncate -s 4G {}", fs_img_path.to_str().unwrap())) + .status + .success(), + "truncate failed" + ); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let loop_dev_path = create_loop_device(fs_img_path.to_str().unwrap(), 4096, 5); - // Just check the VM booted correctly. - assert_eq!(guest.get_cpu_count().unwrap_or_default(), num_queues as u32); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + assert!( + exec_host_command_output(&format!("mkfs.ext4 -q {loop_dev_path}")) + .status + .success(), + "mkfs.ext4 failed" + ); - if let Some(mut daemon_child) = daemon_child { - thread::sleep(std::time::Duration::new(5, 0)); - let _ = daemon_child.kill(); - let _ = daemon_child.wait(); - } + let mnt_dir = img_dir.as_path().join("mnt"); + fs::create_dir_all(&mnt_dir).unwrap(); + assert!( + exec_host_command_output(&format!( + "mount {} {}", + loop_dev_path, + mnt_dir.to_str().unwrap() + )) + .status + .success(), + "mount failed" + ); - handle_child_output(r, &output); -} + let src_qcow2 = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); + let dest_qcow2 = mnt_dir.join("os.qcow2"); + assert!( + exec_host_command_output(&format!( + "cp {} {}", + src_qcow2, + dest_qcow2.to_str().unwrap() + )) + .status + .success(), + "cp failed" + ); -fn _test_virtio_fs( - prepare_daemon: &dyn Fn(&TempDir, &str) -> (std::process::Child, String), - hotplug: bool, - pci_segment: Option, -) { - #[cfg(target_arch = "aarch64")] - let focal_image = if hotplug { - FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string() - } else { - FOCAL_IMAGE_NAME.to_string() - }; - #[cfg(target_arch = "x86_64")] - let focal_image = FOCAL_IMAGE_NAME.to_string(); - let focal = UbuntuDiskConfig::new(focal_image); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args([ + "--disk", + &format!( + "path={},direct=on,image_type=qcow2", + dest_qcow2.to_str().unwrap() + ), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot_custom_timeout(180).unwrap(); + }); - let mut shared_dir = workload_path; - shared_dir.push("shared_dir"); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = if hotplug { - edk2_path() - } else { - direct_kernel_boot_path() - }; - - let (mut daemon_child, virtiofsd_socket_path) = - prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); - - let mut guest_command = GuestCommand::new(&guest); - guest_command - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .args(["--api-socket", &api_socket]); - if pci_segment.is_some() { - guest_command.args([ - "--platform", - &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), - ]); - } - - let fs_params = format!( - "id=myfs0,tag=myfs,socket={},num_queues=1,queue_size=1024{}", - virtiofsd_socket_path, - if let Some(pci_segment) = pci_segment { - format!(",pci_segment={pci_segment}") - } else { - "".to_owned() - } - ); + let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap())); + let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}")); - if !hotplug { - guest_command.args(["--fs", fs_params.as_str()]); + handle_child_output(r, &output); } - let mut child = guest_command.capture_output().spawn().unwrap(); + #[test] + fn test_virtio_block_qcow2_dirty_bit_unclean_shutdown() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let test_image_path = guest.tmp_dir.as_path().join("test-dirty.qcow2"); + let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); - if hotplug { - // Add fs to the VM - let (cmd_success, cmd_output) = - remote_command_w_output(&api_socket, "add-fs", Some(&fs_params)); - assert!(cmd_success); + copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); - if let Some(pci_segment) = pci_segment { - assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( - "{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" - ))); - } else { - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}")); - } + assert_eq!( + check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), + Some(false), + "Image should start with dirty bit cleared" + ); - thread::sleep(std::time::Duration::new(10, 0)); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!("path={}", test_image_path.to_str().unwrap()), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), + Some(true), + "Dirty bit should be set while VM is running" + ); + }); + + if r.is_err() { + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + return; } - // Mount shared directory through virtio_fs filesystem - guest - .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") - .unwrap(); + // Simulate unclean shutdown with SIGKILL + let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) }; + let _ = child.wait(); - // Check file1 exists and its content is "foo" assert_eq!( - guest.ssh_command("cat mount_dir/file1").unwrap().trim(), - "foo" + check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), + Some(true), + "Dirty bit should remain set after unclean shutdown" ); - // Check file2 does not exist - guest - .ssh_command("[ ! -f 'mount_dir/file2' ] || true") - .unwrap(); + } + + #[test] + fn test_virtio_block_qcow2_dirty_bit_clean_shutdown() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + + let test_image_path = guest.tmp_dir.as_path().join("test-dirty.qcow2"); + let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); + + copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); - // Check file3 exists and its content is "bar" assert_eq!( - guest.ssh_command("cat mount_dir/file3").unwrap().trim(), - "bar" + check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), + Some(false), + "Image should start with dirty bit cleared" ); - // ACPI feature is needed. - #[cfg(target_arch = "x86_64")] - { - guest.enable_memory_hotplug(); - - // Add RAM to the VM - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!("path={}", test_image_path.to_str().unwrap()), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); - thread::sleep(std::time::Duration::new(30, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - // After the resize, check again that file1 exists and its - // content is "foo". assert_eq!( - guest.ssh_command("cat mount_dir/file1").unwrap().trim(), - "foo" + check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), + Some(true), + "Dirty bit should be set while VM is running" ); - } + }); + + // Clean shutdown using SIGTERM + kill_child(&mut child); - if hotplug { - // Remove from VM - guest.ssh_command("sudo umount mount_dir").unwrap(); - assert!(remote_command(&api_socket, "remove-device", Some("myfs0"))); + if r.is_err() { + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + return; } - }); - let (r, hotplug_daemon_child) = if r.is_ok() && hotplug { - thread::sleep(std::time::Duration::new(10, 0)); - let (daemon_child, virtiofsd_socket_path) = - prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); + let _ = child.wait(); - let r = std::panic::catch_unwind(|| { - thread::sleep(std::time::Duration::new(10, 0)); - let fs_params = format!( - "id=myfs0,tag=myfs,socket={},num_queues=1,queue_size=1024{}", - virtiofsd_socket_path, - if let Some(pci_segment) = pci_segment { - format!(",pci_segment={pci_segment}") - } else { - "".to_owned() - } - ); + disk_check_consistency(&test_image_path, None); + } - // Add back and check it works - let (cmd_success, cmd_output) = - remote_command_w_output(&api_socket, "add-fs", Some(&fs_params)); - assert!(cmd_success); - if let Some(pci_segment) = pci_segment { - assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( - "{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" - ))); - } else { - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}")); - } + #[test] + fn test_virtio_block_qcow2_corrupt_bit_rejected_for_write() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - thread::sleep(std::time::Duration::new(10, 0)); - // Mount shared directory through virtio_fs filesystem - guest - .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") - .unwrap(); + let test_image_path = guest.tmp_dir.as_path().join("test-corrupt.qcow2"); + let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); - // Check file1 exists and its content is "foo" - assert_eq!( - guest.ssh_command("cat mount_dir/file1").unwrap().trim(), - "foo" - ); - }); + copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); - (r, Some(daemon_child)) - } else { - (r, None) - }; + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(false), + "Image should start with corrupt bit cleared" + ); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag"); - let _ = daemon_child.kill(); - let _ = daemon_child.wait(); + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(true), + "Corrupt bit should be set" + ); - if let Some(mut daemon_child) = hotplug_daemon_child { - let _ = daemon_child.kill(); - let _ = daemon_child.wait(); + let child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!("path={}", test_image_path.to_str().unwrap()), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let output = child.wait_with_output().unwrap(); + assert!( + !output.status.success(), + "VM should fail to start with corrupt disk image" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("corrupt") || stderr.contains("Corrupt"), + "Error message should mention corruption: {stderr}" + ); } - handle_child_output(r, &output); -} + #[test] + fn test_virtio_block_qcow2_corrupt_bit_allowed_readonly() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME_QCOW2.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); -fn test_virtio_pmem(discard_writes: bool, specify_size: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - - let kernel_path = direct_kernel_boot_path(); - - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - - std::process::Command::new("mkfs.ext4") - .arg(pmem_temp_file.as_path()) - .output() - .expect("Expect creating disk image to succeed"); - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .args([ - "--pmem", - format!( - "file={}{}{}", - pmem_temp_file.as_path().to_str().unwrap(), - if specify_size { ",size=128M" } else { "" }, - if discard_writes { - ",discard_writes=on" - } else { - "" - } - ) - .as_str(), - ]) - .capture_output() - .spawn() - .unwrap(); + let test_image_path = guest.tmp_dir.as_path().join("test-corrupt-ro.qcow2"); + let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); + + copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag"); - // Check for the presence of /dev/pmem0 assert_eq!( - guest.ssh_command("ls /dev/pmem0").unwrap().trim(), - "/dev/pmem0" + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(true), + "Corrupt bit should be set" ); - // Check changes persist after reboot - assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), ""); - assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n"); - guest - .ssh_command("echo test123 | sudo tee /mnt/test") + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--disk", + &format!("path={},readonly=on", test_image_path.to_str().unwrap()), + &format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ), + ]) + .default_net() + .capture_output() + .spawn() .unwrap(); - assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), ""); - assert_eq!(guest.ssh_command("ls /mnt").unwrap(), ""); - - guest.reboot_linux(0, None); - assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), ""); - assert_eq!( - guest - .ssh_command("sudo cat /mnt/test || true") - .unwrap() - .trim(), - if discard_writes { "" } else { "test123" } - ); - }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + thread::sleep(Duration::from_secs(5)); - handle_child_output(r, &output); -} + match child.try_wait() { + Ok(Some(status)) => { + let output = child.wait_with_output().unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + panic!( + "VM should not have exited when opening corrupt image as readonly. Exit status: {status}, stderr: {stderr}" + ); + } + Ok(None) => { + // VM is still running as expected + } + Err(e) => { + panic!("Error checking process status: {e}"); + } + } -fn get_fd_count(pid: u32) -> usize { - fs::read_dir(format!("/proc/{pid}/fd")).unwrap().count() -} + let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) }; + let output = child.wait_with_output().unwrap(); -fn _test_virtio_vsock(hotplug: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("QCOW2 image is marked corrupt, opening read-only"), + "Expected warning about corrupt image being opened read-only. stderr: {stderr}" + ); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = if hotplug { - edk2_path() - } else { - direct_kernel_boot_path() - }; + assert_eq!( + check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), + Some(true), + "Corrupt bit should remain set for read-only access" + ); + } - let socket = temp_vsock_path(&guest.tmp_dir); - let api_socket = temp_api_path(&guest.tmp_dir); + #[test] + fn test_virtio_block_vhd() { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--api-socket", &api_socket]); - cmd.args(["--cpus", "boot=1"]); - cmd.args(["--memory", "size=512M"]); - cmd.args(["--kernel", kernel_path.to_str().unwrap()]); - cmd.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]); - cmd.default_disks(); - cmd.default_net(); + let mut raw_file_path = workload_path.clone(); + let mut vhd_file_path = workload_path; + raw_file_path.push(JAMMY_IMAGE_NAME); + vhd_file_path.push(JAMMY_IMAGE_NAME_VHD); - if !hotplug { - cmd.args(["--vsock", format!("cid=3,socket={socket}").as_str()]); + // Generate VHD file from RAW file + std::process::Command::new("qemu-img") + .arg("convert") + .arg("-p") + .args(["-f", "raw"]) + .args(["-O", "vpc"]) + .args(["-o", "subformat=fixed"]) + .arg(raw_file_path.to_str().unwrap()) + .arg(vhd_file_path.to_str().unwrap()) + .output() + .expect("Expect generating VHD image from RAW image"); + let guest = make_virtio_block_guest( + &GuestFactory::new_regular_guest_factory(), + JAMMY_IMAGE_NAME_VHD, + ); + _test_virtio_block(&guest, false, false, false, false, ImageType::FixedVhd); } - let mut child = cmd.capture_output().spawn().unwrap(); + #[test] + fn test_virtio_block_vhdx() { + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let mut raw_file_path = workload_path.clone(); + let mut vhdx_file_path = workload_path; + raw_file_path.push(JAMMY_IMAGE_NAME); + vhdx_file_path.push(JAMMY_IMAGE_NAME_VHDX); - if hotplug { - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-vsock", - Some(format!("cid=3,socket={socket},id=test0").as_str()), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); - thread::sleep(std::time::Duration::new(10, 0)); - // Check adding a second one fails - assert!(!remote_command( - &api_socket, - "add-vsock", - Some("cid=1234,socket=/tmp/fail") - )); - } + // Generate dynamic VHDX file from RAW file + std::process::Command::new("qemu-img") + .arg("convert") + .arg("-p") + .args(["-f", "raw"]) + .args(["-O", "vhdx"]) + .arg(raw_file_path.to_str().unwrap()) + .arg(vhdx_file_path.to_str().unwrap()) + .output() + .expect("Expect generating dynamic VHDx image from RAW image"); + let guest = make_virtio_block_guest( + &GuestFactory::new_regular_guest_factory(), + JAMMY_IMAGE_NAME_VHDX, + ); + _test_virtio_block(&guest, false, false, true, false, ImageType::Vhdx); + } - // Validate vsock works as expected. - guest.check_vsock(socket.as_str()); - guest.reboot_linux(0, None); - // Validate vsock still works after a reboot. - guest.check_vsock(socket.as_str()); + #[test] + fn test_virtio_block_dynamic_vhdx_expand() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_virtio_block_dynamic_vhdx_expand(&guest); + } - if hotplug { - assert!(remote_command(&api_socket, "remove-device", Some("test0"))); - } - }); + #[test] + fn test_virtio_block_direct_and_firmware() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // The OS disk must be copied to a location that is not backed by + // tmpfs, otherwise the syscall openat(2) with O_DIRECT simply fails + // with EINVAL because tmpfs doesn't support this flag. + let mut workloads_path = dirs::home_dir().unwrap(); + workloads_path.push("workloads"); + let os_dir = TempDir::new_in(workloads_path.as_path()).unwrap(); + let mut os_path = os_dir.as_path().to_path_buf(); + os_path.push("osdisk.img"); + rate_limited_copy( + guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), + os_path.as_path(), + ) + .expect("copying of OS disk failed"); - handle_child_output(r, &output); -} + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args([ + "--disk", + format!("path={},direct=on", os_path.as_path().to_str().unwrap()).as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); -fn get_ksm_pages_shared() -> u32 { - fs::read_to_string("/sys/kernel/mm/ksm/pages_shared") - .unwrap() - .trim() - .parse::() - .unwrap() -} + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot_custom_timeout(180).unwrap(); + }); -fn test_memory_mergeable(mergeable: bool) { - let memory_param = if mergeable { - "mergeable=on" - } else { - "mergeable=off" - }; - - // We are assuming the rest of the system in our CI is not using mergeable memory - let ksm_ps_init = get_ksm_pages_shared(); - assert!(ksm_ps_init == 0); - - let focal1 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest1 = Guest::new(Box::new(focal1)); - let mut child1 = GuestCommand::new(&guest1) - .args(["--cpus", "boot=1"]) - .args(["--memory", format!("size=512M,{memory_param}").as_str()]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", guest1.default_net_string().as_str()]) - .args(["--serial", "tty", "--console", "off"]) - .capture_output() - .spawn() - .unwrap(); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - let r = std::panic::catch_unwind(|| { - guest1.wait_vm_boot(None).unwrap(); - }); - if r.is_err() { - kill_child(&mut child1); - let output = child1.wait_with_output().unwrap(); handle_child_output(r, &output); - panic!("Test should already be failed/panicked"); // To explicitly mark this block never return - } - - let ksm_ps_guest1 = get_ksm_pages_shared(); - - let focal2 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest2 = Guest::new(Box::new(focal2)); - let mut child2 = GuestCommand::new(&guest2) - .args(["--cpus", "boot=1"]) - .args(["--memory", format!("size=512M,{memory_param}").as_str()]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", guest2.default_net_string().as_str()]) - .args(["--serial", "tty", "--console", "off"]) - .capture_output() - .spawn() - .unwrap(); - - let r = std::panic::catch_unwind(|| { - guest2.wait_vm_boot(None).unwrap(); - let ksm_ps_guest2 = get_ksm_pages_shared(); + } - if mergeable { - println!( - "ksm pages_shared after vm1 booted '{ksm_ps_guest1}', ksm pages_shared after vm2 booted '{ksm_ps_guest2}'" - ); - // We are expecting the number of shared pages to increase as the number of VM increases - assert!(ksm_ps_guest1 < ksm_ps_guest2); - } else { - assert!(ksm_ps_guest1 == 0); - assert!(ksm_ps_guest2 == 0); - } - }); + #[test] + fn test_vhost_user_net_default() { + test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, false); + } - kill_child(&mut child1); - kill_child(&mut child2); + #[test] + fn test_vhost_user_net_named_tap() { + test_vhost_user_net( + Some("mytap0"), + 2, + &prepare_vhost_user_net_daemon, + false, + false, + ); + } - let output = child1.wait_with_output().unwrap(); - child2.wait().unwrap(); + #[test] + fn test_vhost_user_net_existing_tap() { + test_vhost_user_net( + Some("vunet-tap0"), + 2, + &prepare_vhost_user_net_daemon, + false, + false, + ); + } - handle_child_output(r, &output); -} + #[test] + fn test_vhost_user_net_multiple_queues() { + test_vhost_user_net(None, 4, &prepare_vhost_user_net_daemon, false, false); + } -fn _get_vmm_overhead(pid: u32, guest_memory_size: u32) -> HashMap { - let smaps = fs::File::open(format!("/proc/{pid}/smaps")).unwrap(); - let reader = io::BufReader::new(smaps); - - let mut skip_map: bool = false; - let mut region_name: String = "".to_string(); - let mut region_maps = HashMap::new(); - for line in reader.lines() { - let l = line.unwrap(); - - if l.contains('-') { - let values: Vec<&str> = l.split_whitespace().collect(); - region_name = values.last().unwrap().trim().to_string(); - if region_name == "0" { - region_name = "anonymous".to_string() - } - } + #[test] + fn test_vhost_user_net_tap_multiple_queues() { + test_vhost_user_net( + Some("vunet-tap1"), + 4, + &prepare_vhost_user_net_daemon, + false, + false, + ); + } - // Each section begins with something that looks like: - // Size: 2184 kB - if l.starts_with("Size:") { - let values: Vec<&str> = l.split_whitespace().collect(); - let map_size = values[1].parse::().unwrap(); - // We skip the assigned guest RAM map, its RSS is only - // dependent on the guest actual memory usage. - // Everything else can be added to the VMM overhead. - skip_map = map_size >= guest_memory_size; - continue; - } + #[test] + fn test_vhost_user_net_host_mac() { + test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, true, false); + } - // If this is a map we're taking into account, then we only - // count the RSS. The sum of all counted RSS is the VMM overhead. - if !skip_map && l.starts_with("Rss:") { - let values: Vec<&str> = l.split_whitespace().collect(); - let value = values[1].trim().parse::().unwrap(); - *region_maps.entry(region_name.clone()).or_insert(0) += value; - } + #[test] + fn test_vhost_user_net_client_mode() { + test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, true); } - region_maps -} + #[test] + #[cfg(not(target_arch = "aarch64"))] + fn test_vhost_user_blk_default() { + test_vhost_user_blk(2, false, false, Some(&prepare_vubd)); + } -fn get_vmm_overhead(pid: u32, guest_memory_size: u32) -> u32 { - let mut total = 0; + #[test] + #[cfg(not(target_arch = "aarch64"))] + fn test_vhost_user_blk_readonly() { + test_vhost_user_blk(1, true, false, Some(&prepare_vubd)); + } - for (region_name, value) in &_get_vmm_overhead(pid, guest_memory_size) { - eprintln!("{region_name}: {value}"); - total += value; + #[test] + #[cfg(not(target_arch = "aarch64"))] + fn test_vhost_user_blk_direct() { + test_vhost_user_blk(1, false, true, Some(&prepare_vubd)); } - total -} + #[test] + fn test_boot_from_vhost_user_blk_default() { + test_boot_from_vhost_user_blk(1, false, false, Some(&prepare_vubd)); + } -fn process_rss_kib(pid: u32) -> usize { - let command = format!("ps -q {pid} -o rss="); - let rss = exec_host_command_output(&command); - String::from_utf8_lossy(&rss.stdout).trim().parse().unwrap() -} + #[test] + #[cfg(target_arch = "x86_64")] + fn test_split_irqchip() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_split_irqchip(&guest); + } -// 10MB is our maximum accepted overhead. -const MAXIMUM_VMM_OVERHEAD_KB: u32 = 10 * 1024; - -#[derive(PartialEq, Eq, PartialOrd)] -struct Counters { - rx_bytes: u64, - rx_frames: u64, - tx_bytes: u64, - tx_frames: u64, - read_bytes: u64, - write_bytes: u64, - read_ops: u64, - write_ops: u64, -} + #[test] + #[cfg(target_arch = "x86_64")] + fn test_dmi_serial_number() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); -fn get_counters(api_socket: &str) -> Counters { - // Get counters - let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "counters", None); - assert!(cmd_success); - - let counters: HashMap<&str, HashMap<&str, u64>> = - serde_json::from_slice(&cmd_output).unwrap_or_default(); - - let rx_bytes = *counters.get("_net2").unwrap().get("rx_bytes").unwrap(); - let rx_frames = *counters.get("_net2").unwrap().get("rx_frames").unwrap(); - let tx_bytes = *counters.get("_net2").unwrap().get("tx_bytes").unwrap(); - let tx_frames = *counters.get("_net2").unwrap().get("tx_frames").unwrap(); - - let read_bytes = *counters.get("_disk0").unwrap().get("read_bytes").unwrap(); - let write_bytes = *counters.get("_disk0").unwrap().get("write_bytes").unwrap(); - let read_ops = *counters.get("_disk0").unwrap().get("read_ops").unwrap(); - let write_ops = *counters.get("_disk0").unwrap().get("write_ops").unwrap(); - - Counters { - rx_bytes, - rx_frames, - tx_bytes, - tx_frames, - read_bytes, - write_bytes, - read_ops, - write_ops, + _test_dmi_serial_number(&guest); } -} -fn pty_read(mut pty: std::fs::File) -> Receiver { - let (tx, rx) = mpsc::channel::(); - thread::spawn(move || loop { - thread::sleep(std::time::Duration::new(1, 0)); - let mut buf = [0; 512]; - match pty.read(&mut buf) { - Ok(_bytes) => { - let output = std::str::from_utf8(&buf).unwrap().to_string(); - match tx.send(output) { - Ok(_) => (), - Err(_) => break, - } - } - Err(_) => break, - } - }); - rx -} - -fn get_pty_path(api_socket: &str, pty_type: &str) -> PathBuf { - let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None); - assert!(cmd_success); - let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); - assert_eq!("Pty", info["config"][pty_type]["mode"]); - PathBuf::from( - info["config"][pty_type]["file"] - .as_str() - .expect("Missing pty path"), - ) -} - -// VFIO test network setup. -// We reserve a different IP class for it: 172.18.0.0/24. -#[cfg(target_arch = "x86_64")] -fn setup_vfio_network_interfaces() { - // 'vfio-br0' - assert!(exec_host_command_status("sudo ip link add name vfio-br0 type bridge").success()); - assert!(exec_host_command_status("sudo ip link set vfio-br0 up").success()); - assert!(exec_host_command_status("sudo ip addr add 172.18.0.1/24 dev vfio-br0").success()); - // 'vfio-tap0' - assert!(exec_host_command_status("sudo ip tuntap add vfio-tap0 mode tap").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap0 master vfio-br0").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap0 up").success()); - // 'vfio-tap1' - assert!(exec_host_command_status("sudo ip tuntap add vfio-tap1 mode tap").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap1 master vfio-br0").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap1 up").success()); - // 'vfio-tap2' - assert!(exec_host_command_status("sudo ip tuntap add vfio-tap2 mode tap").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap2 master vfio-br0").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap2 up").success()); - // 'vfio-tap3' - assert!(exec_host_command_status("sudo ip tuntap add vfio-tap3 mode tap").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap3 master vfio-br0").success()); - assert!(exec_host_command_status("sudo ip link set vfio-tap3 up").success()); -} - -// Tear VFIO test network down -#[cfg(target_arch = "x86_64")] -fn cleanup_vfio_network_interfaces() { - assert!(exec_host_command_status("sudo ip link del vfio-br0").success()); - assert!(exec_host_command_status("sudo ip link del vfio-tap0").success()); - assert!(exec_host_command_status("sudo ip link del vfio-tap1").success()); - assert!(exec_host_command_status("sudo ip link del vfio-tap2").success()); - assert!(exec_host_command_status("sudo ip link del vfio-tap3").success()); -} - -fn balloon_size(api_socket: &str) -> u64 { - let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None); - assert!(cmd_success); - - let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); - let total_mem = &info["config"]["memory"]["size"] - .to_string() - .parse::() - .unwrap(); - let actual_mem = &info["memory_actual_size"] - .to_string() - .parse::() - .unwrap(); - total_mem - actual_mem -} - -fn vm_state(api_socket: &str) -> String { - let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None); - assert!(cmd_success); - - let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); - let state = &info["state"].as_str().unwrap(); - - state.to_string() -} - -// This test validates that it can find the virtio-iommu device at first. -// It also verifies that both disks and the network card are attached to -// the virtual IOMMU by looking at /sys/kernel/iommu_groups directory. -// The last interesting part of this test is that it exercises the network -// interface attached to the virtual IOMMU since this is the one used to -// send all commands through SSH. -fn _test_virtio_iommu(acpi: bool) { - // Virtio-iommu support is ready in recent kernel (v5.14). But the kernel in - // Focal image is still old. - // So if ACPI is enabled on AArch64, we use a modified Focal image in which - // the kernel binary has been updated. - #[cfg(target_arch = "aarch64")] - let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string(); + #[test] #[cfg(target_arch = "x86_64")] - let focal_image = FOCAL_IMAGE_NAME.to_string(); - let focal = UbuntuDiskConfig::new(focal_image); - let guest = Guest::new(Box::new(focal)); + fn test_dmi_uuid() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_dmi_uuid(&guest); + } + #[test] #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = if acpi { - edk2_path() - } else { - direct_kernel_boot_path() - }; - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--disk", - format!( - "path={},iommu=on", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - format!( - "path={},iommu=on", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - ]) - .args(["--net", guest.default_net_string_w_iommu().as_str()]) - .capture_output() - .spawn() - .unwrap(); - - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Verify the virtio-iommu device is present. - assert!(guest - .does_device_vendor_pair_match("0x1057", "0x1af4") - .unwrap_or_default()); - - // On AArch64, if the guest system boots from FDT, the behavior of IOMMU is a bit - // different with ACPI. - // All devices on the PCI bus will be attached to the virtual IOMMU, except the - // virtio-iommu device itself. So these devices will all be added to IOMMU groups, - // and appear under folder '/sys/kernel/iommu_groups/'. - // The result is, in the case of FDT, IOMMU group '0' contains "0000:00:01.0" - // which is the console. The first disk "0000:00:02.0" is in group '1'. - // While on ACPI, console device is not attached to IOMMU. So the IOMMU group '0' - // contains "0000:00:02.0" which is the first disk. - // - // Verify the iommu group of the first disk. - let iommu_group = !acpi as i32; - assert_eq!( - guest - .ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str()) - .unwrap() - .trim(), - "0000:00:02.0" - ); - - // Verify the iommu group of the second disk. - let iommu_group = if acpi { 1 } else { 2 }; - assert_eq!( - guest - .ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str()) - .unwrap() - .trim(), - "0000:00:03.0" - ); - - // Verify the iommu group of the network card. - let iommu_group = if acpi { 2 } else { 3 }; - assert_eq!( - guest - .ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str()) - .unwrap() - .trim(), - "0000:00:04.0" - ); - }); + fn test_dmi_oem_strings() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_dmi_oem_strings(&guest); + } - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + #[test] + #[cfg(target_arch = "x86_64")] + fn test_dmi_system_and_chassis() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_dmi_system_and_chassis(&guest); + } - handle_child_output(r, &output); -} + #[test] + fn test_virtio_fs() { + _test_virtio_fs(&prepare_virtiofsd, false, false, None); + } -fn get_reboot_count(guest: &Guest) -> u32 { - guest - .ssh_command("sudo last | grep -c reboot") - .unwrap() - .trim() - .parse::() - .unwrap_or_default() -} + #[test] + fn test_virtio_fs_hotplug() { + _test_virtio_fs(&prepare_virtiofsd, true, false, None); + } -fn enable_guest_watchdog(guest: &Guest, watchdog_sec: u32) { - // Check for PCI device - assert!(guest - .does_device_vendor_pair_match("0x1063", "0x1af4") - .unwrap_or_default()); - - // Enable systemd watchdog - guest - .ssh_command(&format!( - "echo RuntimeWatchdogSec={watchdog_sec}s | sudo tee -a /etc/systemd/system.conf" - )) - .unwrap(); + #[test] + fn test_virtio_fs_multi_segment_hotplug() { + _test_virtio_fs(&prepare_virtiofsd, true, false, Some(15)); + } - guest.ssh_command("sudo systemctl daemon-reexec").unwrap(); -} + #[test] + fn test_virtio_fs_multi_segment() { + _test_virtio_fs(&prepare_virtiofsd, false, false, Some(15)); + } -fn make_guest_panic(guest: &Guest) { - // Check for pvpanic device - assert!(guest - .does_device_vendor_pair_match("0x0011", "0x1b36") - .unwrap_or_default()); + #[test] + fn test_generic_vhost_user() { + _test_virtio_fs(&prepare_virtiofsd, false, true, None); + } - // Trigger guest a panic - guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); -} + #[test] + fn test_generic_vhost_user_hotplug() { + _test_virtio_fs(&prepare_virtiofsd, true, true, None); + } -mod common_parallel { - use std::fs::OpenOptions; - use std::io::SeekFrom; + #[test] + fn test_generic_vhost_user_multi_segment_hotplug() { + _test_virtio_fs(&prepare_virtiofsd, true, true, Some(15)); + } - use crate::*; + #[test] + fn test_generic_vhost_user_multi_segment() { + _test_virtio_fs(&prepare_virtiofsd, false, true, Some(15)); + } #[test] - #[cfg(target_arch = "x86_64")] - fn test_focal_hypervisor_fw() { - test_simple_launch(fw_path(FwType::RustHypervisorFirmware), FOCAL_IMAGE_NAME) + fn test_virtio_pmem_discard_writes() { + test_virtio_pmem(true, false); } #[test] - #[cfg(target_arch = "x86_64")] - fn test_focal_ovmf() { - test_simple_launch(fw_path(FwType::Ovmf), FOCAL_IMAGE_NAME) + fn test_virtio_pmem_with_size() { + test_virtio_pmem(true, true); } - #[cfg(target_arch = "x86_64")] - fn test_simple_launch(fw_path: String, disk_path: &str) { - let disk_config = Box::new(UbuntuDiskConfig::new(disk_path.to_string())); - let guest = Guest::new(disk_config); - let event_path = temp_event_monitor_path(&guest.tmp_dir); + #[test] + fn test_boot_from_virtio_pmem() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + + let kernel_path = direct_kernel_boot_path(); let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", fw_path.as_str()]) - .default_disks() + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + ]) .default_net() - .args(["--serial", "tty", "--console", "off"]) - .args(["--event-monitor", format!("path={event_path}").as_str()]) + .args([ + "--pmem", + format!( + "file={},size={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), + fs::metadata(guest.disk_config.disk(DiskType::OperatingSystem).unwrap()) + .unwrap() + .len() + ) + .as_str(), + ]) + .args([ + "--cmdline", + DIRECT_KERNEL_BOOT_CMDLINE + .replace("vda1", "pmem0p1") + .as_str(), + ]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(Some(120)).unwrap(); + guest.wait_vm_boot().unwrap(); + // Simple checks to validate the VM booted properly assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000"); - - let expected_sequential_events = [ - &MetaEvent { - event: "starting".to_string(), - device_id: None, - }, - &MetaEvent { - event: "booting".to_string(), - device_id: None, - }, - &MetaEvent { - event: "booted".to_string(), - device_id: None, - }, - &MetaEvent { - event: "activated".to_string(), - device_id: Some("_disk0".to_string()), - }, - &MetaEvent { - event: "reset".to_string(), - device_id: Some("_disk0".to_string()), - }, - ]; - assert!(check_sequential_events( - &expected_sequential_events, - &event_path - )); - - // It's been observed on the Bionic image that udev and snapd - // services can cause some delay in the VM's shutdown. Disabling - // them improves the reliability of this test. - let _ = guest.ssh_command("sudo systemctl disable udev"); - let _ = guest.ssh_command("sudo systemctl stop udev"); - let _ = guest.ssh_command("sudo systemctl disable snapd"); - let _ = guest.ssh_command("sudo systemctl stop snapd"); - - guest.ssh_command("sudo poweroff").unwrap(); - thread::sleep(std::time::Duration::new(20, 0)); - let latest_events = [ - &MetaEvent { - event: "shutdown".to_string(), - device_id: None, - }, - &MetaEvent { - event: "deleted".to_string(), - device_id: None, - }, - &MetaEvent { - event: "shutdown".to_string(), - device_id: None, - }, - ]; - assert!(check_latest_events_exact(&latest_events, &event_path)); }); kill_child(&mut child); @@ -2445,35 +1979,39 @@ mod common_parallel { } #[test] - fn test_multi_cpu() { - let jammy_image = JAMMY_IMAGE_NAME.to_string(); - let jammy = UbuntuDiskConfig::new(jammy_image); - let guest = Guest::new(Box::new(jammy)); + fn test_multiple_network_interfaces() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_multiple_network_interfaces(&guest); + } - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=2,max=4"]) - .args(["--memory", "size=512M"]) + #[test] + #[cfg(target_arch = "aarch64")] + fn test_pmu_on() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .capture_output() .default_disks() - .default_net(); - - let mut child = cmd.spawn().unwrap(); + .default_net() + .capture_output() + .spawn() + .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(Some(120)).unwrap(); - - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); + guest.wait_vm_boot().unwrap(); + // Test that PMU exists. assert_eq!( guest - .ssh_command( - r#"sudo dmesg | grep "smp: Brought up" | sed "s/\[\ *[0-9.]*\] //""# - ) + .ssh_command(GREP_PMU_IRQ_CMD) .unwrap() - .trim(), - "smp: Brought up 1 node, 2 CPUs" + .trim() + .parse::() + .unwrap_or_default(), + 1 ); }); @@ -2484,492 +2022,383 @@ mod common_parallel { } #[test] - fn test_cpu_topology_421() { - test_cpu_topology(4, 2, 1, false); - } - - #[test] - fn test_cpu_topology_142() { - test_cpu_topology(1, 4, 2, false); + fn test_serial_off() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_serial_off(&guest); } #[test] - fn test_cpu_topology_262() { - test_cpu_topology(2, 6, 2, false); - } + fn test_serial_null() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut cmd = GuestCommand::new(&guest); + #[cfg(target_arch = "x86_64")] + let console_str: &str = "console=ttyS0"; + #[cfg(target_arch = "aarch64")] + let console_str: &str = "console=ttyAMA0"; - #[test] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - fn test_cpu_physical_bits() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let max_phys_bits: u8 = 36; - let mut child = GuestCommand::new(&guest) - .args(["--cpus", &format!("max_phys_bits={max_phys_bits}")]) - .args(["--memory", "size=512M"]) + cmd.default_cpus() + .default_memory() .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--cmdline", + DIRECT_KERNEL_BOOT_CMDLINE + .replace("console=hvc0", console_str) + .as_str(), + ]) .default_disks() .default_net() - .capture_output() - .spawn() - .unwrap(); + .args(["--serial", "null"]) + .args(["--console", "off"]) + .capture_output(); + + let mut child = cmd.spawn().unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - assert!( - guest - .ssh_command("lscpu | grep \"Address sizes:\" | cut -f 2 -d \":\" | sed \"s# *##\" | cut -f 1 -d \" \"") - .unwrap() - .trim() - .parse::() - .unwrap_or(max_phys_bits + 1) <= max_phys_bits, - ); + // Test that there is a ttyS0 + assert_eq!( + guest + .ssh_command(GREP_SERIAL_IRQ_CMD) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + + let r = std::panic::catch_unwind(|| { + assert!(!String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); + }); handle_child_output(r, &output); } #[test] - fn test_cpu_affinity() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_serial_tty() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - // We need the host to have at least 4 CPUs if we want to be able - // to run this test. - let host_cpus_count = exec_host_command_output("nproc"); - assert!( - String::from_utf8_lossy(&host_cpus_count.stdout) - .trim() - .parse::() - .unwrap_or(0) - >= 4 - ); + let kernel_path = direct_kernel_boot_path(); + + #[cfg(target_arch = "x86_64")] + let console_str: &str = "console=ttyS0"; + #[cfg(target_arch = "aarch64")] + let console_str: &str = "console=ttyAMA0"; let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2,affinity=[0@[0,2],1@[1,3]]"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args([ + "--cmdline", + DIRECT_KERNEL_BOOT_CMDLINE + .replace("console=hvc0", console_str) + .as_str(), + ]) .default_disks() .default_net() + .args(["--serial", "tty"]) + .args(["--console", "off"]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - let pid = child.id(); - let taskset_vcpu0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); - assert_eq!(String::from_utf8_lossy(&taskset_vcpu0.stdout).trim(), "0,2"); - let taskset_vcpu1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); - assert_eq!(String::from_utf8_lossy(&taskset_vcpu1.stdout).trim(), "1,3"); + guest.wait_vm_boot().unwrap(); + + // Test that there is a ttyS0 + assert_eq!( + guest + .ssh_command(GREP_SERIAL_IRQ_CMD) + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); }); + // This sleep is needed to wait for the login prompt + thread::sleep(std::time::Duration::new(2, 0)); + kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); + + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); + }); + + handle_child_output(r, &output); } #[test] - fn test_virtio_queue_affinity() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_serial_file() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - // We need the host to have at least 4 CPUs if we want to be able - // to run this test. - let host_cpus_count = exec_host_command_output("nproc"); - assert!( - String::from_utf8_lossy(&host_cpus_count.stdout) - .trim() - .parse::() - .unwrap_or(0) - >= 4 - ); + let serial_path = guest.tmp_dir.as_path().join("serial-output"); + #[cfg(target_arch = "x86_64")] + let console_str: &str = "console=ttyS0"; + #[cfg(target_arch = "aarch64")] + let console_str: &str = "console=ttyAMA0"; let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) - .args(["--memory", "size=512M"]) + .default_cpus() + .default_memory() .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - format!( - "path={},num_queues=4,queue_affinity=[0@[0,2],1@[1,3],2@[1],3@[3]]", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), + "--cmdline", + DIRECT_KERNEL_BOOT_CMDLINE + .replace("console=hvc0", console_str) + .as_str(), ]) + .default_disks() .default_net() + .args([ + "--serial", + format!("file={}", serial_path.to_str().unwrap()).as_str(), + ]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - let pid = child.id(); - let taskset_q0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); - assert_eq!(String::from_utf8_lossy(&taskset_q0.stdout).trim(), "0,2"); - let taskset_q1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); - assert_eq!(String::from_utf8_lossy(&taskset_q1.stdout).trim(), "1,3"); - let taskset_q2 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q2 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); - assert_eq!(String::from_utf8_lossy(&taskset_q2.stdout).trim(), "1"); - let taskset_q3 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q3 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); - assert_eq!(String::from_utf8_lossy(&taskset_q3.stdout).trim(), "3"); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - handle_child_output(r, &output); - } - - #[test] - #[cfg(not(feature = "mshv"))] - fn test_large_vm() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=48"]) - .args(["--memory", "size=5120M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) - .capture_output() - .default_disks() - .default_net(); - - let mut child = cmd.spawn().unwrap(); - - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - let r = std::panic::catch_unwind(|| { - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 48); + // Test that there is a ttyS0 assert_eq!( guest - .ssh_command("lscpu | grep \"On-line\" | cut -f 2 -d \":\" | sed \"s# *##\"") + .ssh_command(GREP_SERIAL_IRQ_CMD) .unwrap() - .trim(), - "0-47" + .trim() + .parse::() + .unwrap_or_default(), + 1 ); - assert!(guest.get_total_memory().unwrap_or_default() > 5_000_000); + guest.ssh_command("sudo shutdown -h now").unwrap(); }); + let _ = child.wait_timeout(std::time::Duration::from_secs(20)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + + let r = std::panic::catch_unwind(|| { + // Check that the cloud-hypervisor binary actually terminated + assert!(output.status.success()); + + // Do this check after shutdown of the VM as an easy way to ensure + // all writes are flushed to disk + let mut f = std::fs::File::open(serial_path).unwrap(); + let mut buf = String::new(); + f.read_to_string(&mut buf).unwrap(); + assert!(buf.contains(CONSOLE_TEST_STRING)); + }); handle_child_output(r, &output); } #[test] - #[cfg(not(feature = "mshv"))] - fn test_huge_memory() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=128G"]) + fn test_pty_interaction() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + let serial_option = if cfg!(target_arch = "x86_64") { + " console=ttyS0" + } else { + " console=ttyAMA0" + }; + let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option; + + let mut child = GuestCommand::new(&guest) + .default_cpus() + .default_memory() .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .capture_output() + .args(["--cmdline", &cmdline]) .default_disks() - .default_net(); - - let mut child = cmd.spawn().unwrap(); - - guest.wait_vm_boot(Some(120)).unwrap(); + .default_net() + .args(["--serial", "null"]) + .args(["--console", "pty"]) + .args(["--api-socket", &api_socket]) + .spawn() + .unwrap(); let r = std::panic::catch_unwind(|| { - assert!(guest.get_total_memory().unwrap_or_default() > 128_000_000); + guest.wait_vm_boot().unwrap(); + // Get pty fd for console + let console_path = get_pty_path(&api_socket, "console"); + _test_pty_interaction(console_path); + + guest.ssh_command("sudo shutdown -h now").unwrap(); }); - kill_child(&mut child); + let _ = child.wait_timeout(std::time::Duration::from_secs(20)); + let _ = child.kill(); let output = child.wait_with_output().unwrap(); - handle_child_output(r, &output); - } - #[test] - fn test_power_button() { - _test_power_button(false); + let r = std::panic::catch_unwind(|| { + // Check that the cloud-hypervisor binary actually terminated + assert!(output.status.success()); + }); + handle_child_output(r, &output); } #[test] - #[cfg(not(feature = "mshv"))] - fn test_user_defined_memory_regions() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - - let kernel_path = direct_kernel_boot_path(); + fn test_serial_socket_interaction() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let serial_socket = guest.tmp_dir.as_path().join("serial.socket"); + let serial_socket_pty = guest.tmp_dir.as_path().join("serial.pty"); + let serial_option = if cfg!(target_arch = "x86_64") { + " console=ttyS0" + } else { + " console=ttyAMA0" + }; + let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option; let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=0,hotplug_method=virtio-mem"]) - .args([ - "--memory-zone", - "id=mem0,size=1G,hotplug_size=2G", - "id=mem1,size=1G,shared=on", - "id=mem2,size=1G,host_numa_node=0,hotplug_size=2G", - ]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--api-socket", &api_socket]) - .capture_output() + .default_cpus() + .default_memory() + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", &cmdline]) .default_disks() .default_net() + .args(["--console", "null"]) + .args([ + "--serial", + format!("socket={}", serial_socket.to_str().unwrap()).as_str(), + ]) .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); + let _ = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + }); - guest.enable_memory_hotplug(); + let mut socat_command = Command::new("socat"); + let socat_args = [ + &format!("pty,link={},raw", serial_socket_pty.display()), + &format!("UNIX-CONNECT:{}", serial_socket.display()), + ]; + socat_command.args(socat_args); - resize_zone_command(&api_socket, "mem0", "3G"); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); - resize_zone_command(&api_socket, "mem2", "3G"); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000); - resize_zone_command(&api_socket, "mem0", "2G"); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); - resize_zone_command(&api_socket, "mem2", "2G"); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); + let mut socat_child = socat_command.spawn().unwrap(); + thread::sleep(std::time::Duration::new(1, 0)); - guest.reboot_linux(0, None); + let _ = std::panic::catch_unwind(|| { + _test_pty_interaction(serial_socket_pty); + }); - // Check the amount of RAM after reboot - assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); - assert!(guest.get_total_memory().unwrap_or_default() < 5_760_000); + let _ = socat_child.kill(); + let _ = socat_child.wait(); - // Check if we can still resize down to the initial 'boot'size - resize_zone_command(&api_socket, "mem0", "1G"); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() < 4_800_000); - resize_zone_command(&api_socket, "mem2", "1G"); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() < 3_840_000); + let r = std::panic::catch_unwind(|| { + guest.ssh_command("sudo shutdown -h now").unwrap(); }); + let _ = child.wait_timeout(std::time::Duration::from_secs(20)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + let r = std::panic::catch_unwind(|| { + // Check that the cloud-hypervisor binary actually terminated + if !output.status.success() { + panic!( + "Cloud Hypervisor process failed to terminate gracefully: {:?}", + output.status + ); + } + }); handle_child_output(r, &output); } #[test] - #[cfg(not(feature = "mshv"))] - fn test_guest_numa_nodes() { - _test_guest_numa_nodes(false); + fn test_virtio_console() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_virtio_console(&guest); } #[test] - #[cfg(target_arch = "x86_64")] - fn test_iommu_segments() { - let focal_image = FOCAL_IMAGE_NAME.to_string(); - let focal = UbuntuDiskConfig::new(focal_image); - let guest = Guest::new(Box::new(focal)); - - // Prepare another disk file for the virtio-disk device - let test_disk_path = String::from( - guest - .tmp_dir - .as_path() - .join("test-disk.raw") - .to_str() - .unwrap(), - ); - assert!( - exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() - ); - assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); - - let api_socket = temp_api_path(&guest.tmp_dir); - let mut cmd = GuestCommand::new(&guest); - - cmd.args(["--cpus", "boot=1"]) - .args(["--api-socket", &api_socket]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--platform", - &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS},iommu_segments=[1]"), - ]) - .default_disks() - .capture_output() - .default_net(); - - let mut child = cmd.spawn().unwrap(); - - guest.wait_vm_boot(None).unwrap(); - - let r = std::panic::catch_unwind(|| { - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some( - format!( - "path={},id=test0,pci_segment=1,iommu=on", - test_disk_path.as_str() - ) - .as_str(), - ), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test0\",\"bdf\":\"0001:00:01.0\"}")); - - // Check IOMMU setup - assert!(guest - .does_device_vendor_pair_match("0x1057", "0x1af4") - .unwrap_or_default()); - assert_eq!( - guest - .ssh_command("ls /sys/kernel/iommu_groups/0/devices") - .unwrap() - .trim(), - "0001:00:01.0" - ); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); + fn test_console_file() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_console_file(&guest); } #[test] - fn test_pci_msi() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .capture_output() - .default_disks() - .default_net(); - - let mut child = cmd.spawn().unwrap(); - - guest.wait_vm_boot(None).unwrap(); - - #[cfg(target_arch = "x86_64")] - let grep_cmd = "grep -c PCI-MSI /proc/interrupts"; - #[cfg(target_arch = "aarch64")] - let grep_cmd = "grep -c ITS-PCI-MSIX /proc/interrupts"; - - let r = std::panic::catch_unwind(|| { - assert_eq!( - guest - .ssh_command(grep_cmd) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 12 - ); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + #[cfg(target_arch = "x86_64")] + #[cfg(not(feature = "mshv"))] + // The VFIO integration test starts cloud-hypervisor guest with 3 TAP + // backed networking interfaces, bound through a simple bridge on the host. + // So if the nested cloud-hypervisor succeeds in getting a directly + // assigned interface from its cloud-hypervisor host, we should be able to + // ssh into it, and verify that it's running with the right kernel command + // line (We tag the command line from cloud-hypervisor for that purpose). + // The third device is added to validate that hotplug works correctly since + // it is being added to the L2 VM through hotplugging mechanism. + // Also, we pass-through a virtio-blk device to the L2 VM to test the 32-bit + // vfio device support + fn test_vfio() { + setup_vfio_network_interfaces(); - handle_child_output(r, &output); - } + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new_from_ip_range(Box::new(disk_config), "172.18", 0); - #[test] - fn test_virtio_net_ctrl_queue() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--net", guest.default_net_string_w_mtu(3000).as_str()]) - .capture_output() - .default_disks(); + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); - let mut child = cmd.spawn().unwrap(); + let kernel_path = direct_kernel_boot_path(); - guest.wait_vm_boot(None).unwrap(); + let mut vfio_path = workload_path.clone(); + vfio_path.push("vfio"); - #[cfg(target_arch = "aarch64")] - let iface = "enp0s4"; - #[cfg(target_arch = "x86_64")] - let iface = "ens4"; + let mut cloud_init_vfio_base_path = vfio_path.clone(); + cloud_init_vfio_base_path.push("cloudinit.img"); - let r = std::panic::catch_unwind(|| { - assert_eq!( - guest - .ssh_command( - format!("sudo ethtool -K {iface} rx-gro-hw off && echo success").as_str() - ) - .unwrap() - .trim(), - "success" - ); - assert_eq!( - guest - .ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str()) - .unwrap() - .trim(), - "3000" - ); - }); + // Prepare a separate cloud-init for the L2 guest with its own + // boot notification port. + let (_l2_ci_dir, l2_ci_path) = guest.prepare_l2_cloudinit(); + rate_limited_copy(l2_ci_path, &cloud_init_vfio_base_path) + .expect("copying of L2 cloud-init disk failed"); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let mut vfio_disk_path = workload_path.clone(); + vfio_disk_path.push("vfio.img"); - handle_child_output(r, &output); - } + // Create the vfio disk image + let output = Command::new("mkfs.ext4") + .arg("-d") + .arg(vfio_path.to_str().unwrap()) + .arg(vfio_disk_path.to_str().unwrap()) + .arg("2g") + .output() + .unwrap(); + if !output.status.success() { + eprintln!("{}", String::from_utf8_lossy(&output.stderr)); + panic!("mkfs.ext4 command generated an error"); + } - #[test] - fn test_pci_multiple_segments() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let mut blk_file_path = workload_path; + blk_file_path.push("blk.img"); - // Prepare another disk file for the virtio-disk device - let test_disk_path = String::from( - guest - .tmp_dir - .as_path() - .join("test-disk.raw") - .to_str() - .unwrap(), - ); - assert!( - exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() - ); - assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); + let vfio_tap0 = "vfio-tap0"; + let vfio_tap1 = "vfio-tap1"; + let vfio_tap2 = "vfio-tap2"; + let vfio_tap3 = "vfio-tap3"; - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--platform", - &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), - ]) + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args(["--memory", "size=2G,hugepages=on,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) .args([ "--disk", format!( @@ -2982,237 +2411,360 @@ mod common_parallel { guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), - format!("path={test_disk_path},pci_segment=15").as_str(), + format!("path={},image_type=raw", vfio_disk_path.to_str().unwrap()).as_str(), + format!("path={},iommu=on,readonly=true", blk_file_path.to_str().unwrap()).as_str(), + ]) + .args([ + "--cmdline", + format!( + "{DIRECT_KERNEL_BOOT_CMDLINE} kvm-intel.nested=1 vfio_iommu_type1.allow_unsafe_interrupts" + ) + .as_str(), + ]) + .args([ + "--net", + format!("tap={},mac={}", vfio_tap0, guest.network.guest_mac0).as_str(), + format!( + "tap={},mac={},iommu=on", + vfio_tap1, guest.network.l2_guest_mac1 + ) + .as_str(), + format!( + "tap={},mac={},iommu=on", + vfio_tap2, guest.network.l2_guest_mac2 + ) + .as_str(), + format!( + "tap={},mac={},iommu=on", + vfio_tap3, guest.network.l2_guest_mac3 + ) + .as_str(), ]) .capture_output() - .default_net(); - - let mut child = cmd.spawn().unwrap(); - - guest.wait_vm_boot(None).unwrap(); + .spawn() + .unwrap(); - let grep_cmd = "lspci | grep \"Host bridge\" | wc -l"; + guest.wait_for_ssh(Duration::from_secs(30)).unwrap(); let r = std::panic::catch_unwind(|| { - // There should be MAX_NUM_PCI_SEGMENTS PCI host bridges in the guest. - assert_eq!( - guest - .ssh_command(grep_cmd) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - MAX_NUM_PCI_SEGMENTS - ); + guest.ssh_command_l1("sudo systemctl start vfio").unwrap(); + GuestNetworkConfig::wait_vm_boot_from( + guest.network.l2_tcp_listener_port, + &guest.network.l2_guest_ip2, + DEFAULT_TCP_LISTENER_TIMEOUT, + ) + .unwrap(); - // Check both if /dev/vdc exists and if the block size is 4M. - assert_eq!( - guest - .ssh_command("lsblk | grep vdc | grep -c 4M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + let auth = PasswordAuth { + username: String::from("cloud"), + password: String::from("cloud123"), + }; - // Mount the device. - guest.ssh_command("mkdir mount_image").unwrap(); - guest - .ssh_command("sudo mount -o rw -t ext4 /dev/vdc mount_image/") - .unwrap(); - // Grant all users with write permission. - guest.ssh_command("sudo chmod a+w mount_image/").unwrap(); + // We booted our cloud hypervisor L2 guest with a "VFIOTAG" tag + // added to its kernel command line. + // Let's ssh into it and verify that it's there. If it is it means + // we're in the right guest (The L2 one) because the QEMU L1 guest + // does not have this command line tag. + assert!(check_matched_lines_count( + guest.ssh_command_l2_1("cat /proc/cmdline").unwrap().trim(), + &["VFIOTAG"], + 1 + )); - // Write something to the device. - guest - .ssh_command("sudo echo \"bar\" >> mount_image/foo") - .unwrap(); + // Let's also verify from the second virtio-net device passed to + // the L2 VM. + assert!(check_matched_lines_count( + guest.ssh_command_l2_2("cat /proc/cmdline").unwrap().trim(), + &["VFIOTAG"], + 1 + )); - // Check the content of the block device. The file "foo" should - // contain "bar". - assert_eq!( + // Check the amount of PCI devices appearing in L2 VM. + assert!(check_lines_count( guest - .ssh_command("sudo cat mount_image/foo") + .ssh_command_l2_1("ls /sys/bus/pci/devices") .unwrap() .trim(), - "bar" - ); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } + 8 + )); - #[test] - fn test_pci_multiple_segments_numa_node() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = edk2_path(); + // Check both if /dev/vdc exists and if the block size is 16M in L2 VM + assert!(check_matched_lines_count( + guest.ssh_command_l2_1("lsblk").unwrap().trim(), + &["vdc", "16M"], + 1 + )); - // Prepare another disk file for the virtio-disk device - let test_disk_path = String::from( + // Hotplug an extra virtio-net device through L2 VM. guest - .tmp_dir - .as_path() - .join("test-disk.raw") - .to_str() - .unwrap(), - ); - assert!( - exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() - ); - assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); - const TEST_DISK_NODE: u16 = 1; - - let mut child = GuestCommand::new(&guest) - .args(["--platform", "num_pci_segments=2"]) - .args(["--cpus", "boot=2"]) - .args(["--memory", "size=0"]) - .args(["--memory-zone", "id=mem0,size=256M", "id=mem1,size=256M"]) - .args([ - "--numa", - "guest_numa_id=0,cpus=[0],distances=[1@20],memory_zones=mem0,pci_segments=[0]", - "guest_numa_id=1,cpus=[1],distances=[0@20],memory_zones=mem1,pci_segments=[1]", - ]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--api-socket", &api_socket]) - .capture_output() - .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + .ssh_command_l1( + "echo 0000:00:09.0 | sudo tee /sys/bus/pci/devices/0000:00:09.0/driver/unbind", ) - .as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() + .unwrap(); + guest + .ssh_command_l1("echo 0000:00:09.0 | sudo tee /sys/bus/pci/drivers/vfio-pci/bind") + .unwrap(); + let vfio_hotplug_output = guest + .ssh_command_l1( + "sudo /mnt/ch-remote \ + --api-socket=/tmp/ch_api.sock \ + add-device path=/sys/bus/pci/devices/0000:00:09.0,id=vfio123", ) - .as_str(), - format!("path={test_disk_path},pci_segment={TEST_DISK_NODE}").as_str(), - ]) - .default_net() - .spawn() + .unwrap(); + assert!(check_matched_lines_count( + vfio_hotplug_output.trim(), + &["{\"id\":\"vfio123\",\"bdf\":\"0000:00:08.0\"}"], + 1 + )); + + wait_for_ssh( + "true", + &auth, + &guest.network.l2_guest_ip3, + Duration::from_secs(10), + ) .unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command_l2_1("ls /sys/bus/pci/devices") + .is_ok_and(|output| check_lines_count(output.trim(), 9)) + })); - let cmd = "cat /sys/block/vdc/device/../numa_node"; + // Let's also verify from the third virtio-net device passed to + // the L2 VM. This third device has been hotplugged through the L2 + // VM, so this is our way to validate hotplug works for VFIO PCI. + assert!(check_matched_lines_count( + guest.ssh_command_l2_3("cat /proc/cmdline").unwrap().trim(), + &["VFIOTAG"], + 1 + )); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Check the amount of PCI devices appearing in L2 VM. + // There should be one more device than before, raising the count + // up to 9 PCI devices. + assert!(check_lines_count( + guest + .ssh_command_l2_1("ls /sys/bus/pci/devices") + .unwrap() + .trim(), + 9 + )); - assert_eq!( + // Let's now verify that we can correctly remove the virtio-net + // device through the "remove-device" command responsible for + // unplugging VFIO devices. + guest + .ssh_command_l1( + "sudo /mnt/ch-remote \ + --api-socket=/tmp/ch_api.sock \ + remove-device vfio123", + ) + .unwrap(); + assert!(wait_until(Duration::from_secs(10), || { guest - .ssh_command(cmd) + .ssh_command_l2_1("ls /sys/bus/pci/devices") + .is_ok_and(|output| check_lines_count(output.trim(), 8)) + })); + + // Check the amount of PCI devices appearing in L2 VM is back down + // to 8 devices. + assert!(check_lines_count( + guest + .ssh_command_l2_1("ls /sys/bus/pci/devices") .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - TEST_DISK_NODE - ); + .trim(), + 8 + )); + + // Perform memory hotplug in L2 and validate the memory is showing + // up as expected. In order to check, we will use the virtio-net + // device already passed through L2 as a VFIO device, this will + // verify that VFIO devices are functional with memory hotplug. + assert!(guest.get_total_memory_l2().unwrap_or_default() > 480_000); + guest + .ssh_command_l2_1( + "sudo bash -c 'echo online > /sys/devices/system/memory/auto_online_blocks'", + ) + .unwrap(); + guest + .ssh_command_l1( + "sudo /mnt/ch-remote \ + --api-socket=/tmp/ch_api.sock \ + resize --memory=1073741824", + ) + .unwrap(); + assert!(guest.get_total_memory_l2().unwrap_or_default() > 960_000); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); + cleanup_vfio_network_interfaces(); + handle_child_output(r, &output); } #[test] - fn test_direct_kernel_boot() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_direct_kernel_boot_noacpi() { + let mut guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + guest.kernel_cmdline = Some(format!("{DIRECT_KERNEL_BOOT_CMDLINE} acpi=off")); + _test_direct_kernel_boot_noacpi(&guest); + } - let kernel_path = direct_kernel_boot_path(); + #[test] + fn test_virtio_vsock() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_virtio_vsock(&guest, false); + } - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + #[test] + fn test_virtio_vsock_hotplug() { + #[cfg(target_arch = "x86_64")] + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + #[cfg(target_arch = "aarch64")] + let guest = + basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(edk2_path().to_str().unwrap()); + _test_virtio_vsock(&guest, true); + } - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + #[test] + fn test_api_http_shutdown() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_shutdown(&target_api, &guest); + } - let grep_cmd = if cfg!(target_arch = "x86_64") { - "grep -c PCI-MSI /proc/interrupts" - } else { - "grep -c ITS-PCI-MSIX /proc/interrupts" - }; - assert_eq!( - guest - .ssh_command(grep_cmd) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 12 - ); - }); + #[test] + fn test_api_http_delete() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_delete(&target_api, &guest); + } - handle_child_output(r, &output); + #[test] + fn test_api_http_pause_resume() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); + + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_pause_resume(&target_api, &guest); + } + + #[test] + fn test_api_http_create_boot() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); + + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_create_boot(&target_api, &guest); + } + + #[test] + fn test_virtio_iommu() { + _test_virtio_iommu(cfg!(target_arch = "x86_64")); + } + + #[test] + // We cannot force the software running in the guest to reprogram the BAR + // with some different addresses, but we have a reliable way of testing it + // with a standard Linux kernel. + // By removing a device from the PCI tree, and then rescanning the tree, + // Linux consistently chooses to reorganize the PCI device BARs to other + // locations in the guest address space. + // This test creates a dedicated PCI network device to be checked as being + // properly probed first, then removing it, and adding it again by doing a + // rescan. + fn test_pci_bar_reprogramming() { + #[cfg(target_arch = "aarch64")] + let guest = + basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(edk2_path().to_str().unwrap()); + #[cfg(target_arch = "x86_64")] + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_pci_bar_reprogramming(&guest); + } + + #[test] + fn test_memory_mergeable_off() { + test_memory_mergeable(false); } #[test] + #[cfg(not(feature = "mshv"))] // See issue #7435 #[cfg(target_arch = "x86_64")] - fn test_direct_kernel_boot_bzimage() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_cpu_hotplug() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + let console_str = "console=ttyS0"; - let mut kernel_path = direct_kernel_boot_path(); - // Replace the default kernel with the bzImage. - kernel_path.pop(); - kernel_path.push("bzImage-x86_64"); + let kernel_path = direct_kernel_boot_path(); let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + .args(["--cpus", "boot=2,max=4"]) + .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--cmdline", + DIRECT_KERNEL_BOOT_CMDLINE + .replace("console=hvc0", console_str) + .as_str(), + ]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) .default_disks() .default_net() + .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); + + // Resize the VM + let desired_vcpus = 4; + resize_command(&api_socket, Some(desired_vcpus), None, None, None); + + guest + .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") + .unwrap(); + guest + .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") + .unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus) + })); + + guest.reboot_linux(0); - let grep_cmd = if cfg!(target_arch = "x86_64") { - "grep -c PCI-MSI /proc/interrupts" - } else { - "grep -c ITS-PCI-MSIX /proc/interrupts" - }; assert_eq!( - guest - .ssh_command(grep_cmd) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 12 + guest.get_cpu_count().unwrap_or_default(), + u32::from(desired_vcpus) ); + + // Resize the VM + let desired_vcpus = 2; + resize_command(&api_socket, Some(desired_vcpus), None, None, None); + + assert!(wait_until(Duration::from_secs(10), || { + guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus) + })); + + // Resize the VM back up to 4 + let desired_vcpus = 4; + resize_command(&api_socket, Some(desired_vcpus), None, None, None); + + guest + .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") + .unwrap(); + guest + .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") + .unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus) + })); }); kill_child(&mut child); @@ -3221,294 +2773,222 @@ mod common_parallel { handle_child_output(r, &output); } - fn _test_virtio_block(image_name: &str, disable_io_uring: bool, disable_aio: bool) { - let focal = UbuntuDiskConfig::new(image_name.to_string()); - let guest = Guest::new(Box::new(focal)); - - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); - - let mut blk_file_path = workload_path; - blk_file_path.push("blk.img"); + #[test] + #[cfg_attr(target_arch = "aarch64", ignore = "See #8187")] + fn test_memory_hotplug() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); + #[cfg(target_arch = "x86_64")] let kernel_path = direct_kernel_boot_path(); - let mut cloud_child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) - .args(["--memory", "size=512M,shared=on"]) + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=2,max=4"]) + .args(["--memory", "size=512M,hotplug_size=8192M"]) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - format!( - "path={},readonly=on,direct=on,num_queues=4,_disable_io_uring={},_disable_aio={}", - blk_file_path.to_str().unwrap(), - disable_io_uring, - disable_aio, - ) - .as_str(), - ]) + .default_disks() .default_net() + .args(["--balloon", "size=0"]) + .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Check both if /dev/vdc exists and if the block size is 16M. - assert_eq!( - guest - .ssh_command("lsblk | grep vdc | grep -c 16M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - // Check both if /dev/vdc exists and if this block is RO. - assert_eq!( - guest - .ssh_command("lsblk | grep vdc | awk '{print $5}'") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + guest.enable_memory_hotplug(); - // Check if the number of queues is 4. - assert_eq!( - guest - .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 4 - ); - }); + // Add RAM to the VM + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); - let _ = cloud_child.kill(); - let output = cloud_child.wait_with_output().unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + guest.get_total_memory().unwrap_or_default() > 960_000 + })); - handle_child_output(r, &output); - } + // Use balloon to remove RAM from the VM + let desired_balloon = 512 << 20; + resize_command(&api_socket, None, None, Some(desired_balloon), None); - #[test] - fn test_virtio_block_io_uring() { - _test_virtio_block(FOCAL_IMAGE_NAME, false, true) - } + assert!(wait_until(Duration::from_secs(10), || { + let total_memory = guest.get_total_memory().unwrap_or_default(); + total_memory > 480_000 && total_memory < 960_000 + })); - #[test] - fn test_virtio_block_aio() { - _test_virtio_block(FOCAL_IMAGE_NAME, true, false) - } + guest.reboot_linux(0); - #[test] - fn test_virtio_block_sync() { - _test_virtio_block(FOCAL_IMAGE_NAME, true, true) - } + assert!(guest.get_total_memory().unwrap_or_default() < 960_000); - #[test] - fn test_virtio_block_qcow2() { - _test_virtio_block(FOCAL_IMAGE_NAME_QCOW2, false, false) - } + // Use balloon add RAM to the VM + let desired_balloon = 0; + resize_command(&api_socket, None, None, Some(desired_balloon), None); - #[test] - fn test_virtio_block_qcow2_backing_file() { - _test_virtio_block(FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE, false, false) - } + assert!(wait_until(Duration::from_secs(10), || { + guest.get_total_memory().unwrap_or_default() > 960_000 + })); - #[test] - fn test_virtio_block_vhd() { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + guest.enable_memory_hotplug(); - let mut raw_file_path = workload_path.clone(); - let mut vhd_file_path = workload_path; - raw_file_path.push(FOCAL_IMAGE_NAME); - vhd_file_path.push(FOCAL_IMAGE_NAME_VHD); + // Add RAM to the VM + let desired_ram = 2048 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); - // Generate VHD file from RAW file - std::process::Command::new("qemu-img") - .arg("convert") - .arg("-p") - .args(["-f", "raw"]) - .args(["-O", "vpc"]) - .args(["-o", "subformat=fixed"]) - .arg(raw_file_path.to_str().unwrap()) - .arg(vhd_file_path.to_str().unwrap()) - .output() - .expect("Expect generating VHD image from RAW image"); + assert!(wait_until(Duration::from_secs(10), || { + guest.get_total_memory().unwrap_or_default() > 1_920_000 + })); - _test_virtio_block(FOCAL_IMAGE_NAME_VHD, false, false) - } + // Remove RAM to the VM (only applies after reboot) + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); - #[test] - fn test_virtio_block_vhdx() { - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + guest.reboot_linux(1); - let mut raw_file_path = workload_path.clone(); - let mut vhdx_file_path = workload_path; - raw_file_path.push(FOCAL_IMAGE_NAME); - vhdx_file_path.push(FOCAL_IMAGE_NAME_VHDX); + assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); + }); - // Generate dynamic VHDX file from RAW file - std::process::Command::new("qemu-img") - .arg("convert") - .arg("-p") - .args(["-f", "raw"]) - .args(["-O", "vhdx"]) - .arg(raw_file_path.to_str().unwrap()) - .arg(vhdx_file_path.to_str().unwrap()) - .output() - .expect("Expect generating dynamic VHDx image from RAW image"); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - _test_virtio_block(FOCAL_IMAGE_NAME_VHDX, false, false) + handle_child_output(r, &output); } #[test] - fn test_virtio_block_dynamic_vhdx_expand() { - const VIRTUAL_DISK_SIZE: u64 = 100 << 20; - const EMPTY_VHDX_FILE_SIZE: u64 = 8 << 20; - const FULL_VHDX_FILE_SIZE: u64 = 112 << 20; - const DYNAMIC_VHDX_NAME: &str = "dynamic.vhdx"; - - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); - - let mut vhdx_file_path = workload_path; - vhdx_file_path.push(DYNAMIC_VHDX_NAME); - let vhdx_path = vhdx_file_path.to_str().unwrap(); - - // Generate a 100 MiB dynamic VHDX file - std::process::Command::new("qemu-img") - .arg("create") - .args(["-f", "vhdx"]) - .arg(vhdx_path) - .arg(VIRTUAL_DISK_SIZE.to_string()) - .output() - .expect("Expect generating dynamic VHDx image from RAW image"); - - // Check if the size matches with empty VHDx file size - assert_eq!(vhdx_image_size(vhdx_path), EMPTY_VHDX_FILE_SIZE); + #[cfg(not(feature = "mshv"))] // See #7456 + fn test_virtio_mem() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); let kernel_path = direct_kernel_boot_path(); - let mut cloud_child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=2,max=4"]) .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - format!("path={vhdx_path}").as_str(), + "--memory", + "size=512M,hotplug_method=virtio-mem,hotplug_size=8192M", ]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() .default_net() + .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Check both if /dev/vdc exists and if the block size is 100 MiB. - assert_eq!( - guest - .ssh_command("lsblk | grep vdc | grep -c 100M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - // Write 100 MB of data to the VHDx disk - guest - .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=100") - .unwrap(); - }); + guest.enable_memory_hotplug(); - // Check if the size matches with expected expanded VHDx file size - assert_eq!(vhdx_image_size(vhdx_path), FULL_VHDX_FILE_SIZE); + // Add RAM to the VM + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); - kill_child(&mut cloud_child); - let output = cloud_child.wait_with_output().unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + guest.get_total_memory().unwrap_or_default() > 960_000 + })); - handle_child_output(r, &output); - } + // Add RAM to the VM + let desired_ram = 2048 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); - fn vhdx_image_size(disk_name: &str) -> u64 { - std::fs::File::open(disk_name) - .unwrap() - .seek(SeekFrom::End(0)) - .unwrap() - } + assert!(wait_until(Duration::from_secs(10), || { + guest.get_total_memory().unwrap_or_default() > 1_920_000 + })); - #[test] - fn test_virtio_block_direct_and_firmware() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Remove RAM from the VM + let desired_ram = 1024 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); - // The OS disk must be copied to a location that is not backed by - // tmpfs, otherwise the syscall openat(2) with O_DIRECT simply fails - // with EINVAL because tmpfs doesn't support this flag. - let mut workloads_path = dirs::home_dir().unwrap(); - workloads_path.push("workloads"); - let os_dir = TempDir::new_in(workloads_path.as_path()).unwrap(); - let mut os_path = os_dir.as_path().to_path_buf(); - os_path.push("osdisk.img"); - rate_limited_copy( - guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), - os_path.as_path(), - ) - .expect("copying of OS disk failed"); + assert!(wait_until(Duration::from_secs(10), || { + let total_memory = guest.get_total_memory().unwrap_or_default(); + total_memory > 960_000 && total_memory < 1_920_000 + })); + + guest.reboot_linux(0); + + // Check the amount of memory after reboot is 1GiB + assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); + + // Check we can still resize to 512MiB + let desired_ram = 512 << 20; + resize_command(&api_socket, None, Some(desired_ram), None, None); + assert!(wait_until(Duration::from_secs(10), || { + let total_memory = guest.get_total_memory().unwrap_or_default(); + total_memory > 480_000 && total_memory < 960_000 + })); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + } + + #[test] + #[cfg(target_arch = "x86_64")] + // Test both vCPU and memory resizing together + fn test_resize() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let kernel_path = direct_kernel_boot_path(); let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .args([ - "--disk", - format!("path={},direct=on", os_path.as_path().to_str().unwrap()).as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - ]) + .args(["--cpus", "boot=2,max=4"]) + .args(["--memory", "size=512M,hotplug_size=8192M"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() .default_net() + .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(Some(120)).unwrap(); + guest.wait_vm_boot().unwrap(); + + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); + assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + + guest.enable_memory_hotplug(); + + // Resize the VM + let desired_vcpus = 4; + let desired_ram = 1024 << 20; + resize_command( + &api_socket, + Some(desired_vcpus), + Some(desired_ram), + None, + None, + ); + + guest + .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") + .unwrap(); + guest + .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") + .unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + guest.get_cpu_count().unwrap_or_default() == u32::from(desired_vcpus) + })); + + assert!(guest.get_total_memory().unwrap_or_default() > 960_000); }); kill_child(&mut child); @@ -3518,155 +2998,143 @@ mod common_parallel { } #[test] - fn test_vhost_user_net_default() { - test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, false) - } - - #[test] - fn test_vhost_user_net_named_tap() { - test_vhost_user_net( - Some("mytap0"), - 2, - &prepare_vhost_user_net_daemon, - false, - false, - ) - } - - #[test] - fn test_vhost_user_net_existing_tap() { - test_vhost_user_net( - Some("vunet-tap0"), - 2, - &prepare_vhost_user_net_daemon, - false, - false, - ) - } - - #[test] - fn test_vhost_user_net_multiple_queues() { - test_vhost_user_net(None, 4, &prepare_vhost_user_net_daemon, false, false) + fn test_memory_overhead() { + let guest_memory_size_kb: u32 = 512 * 1024; + let guest = + basic_regular_guest!(JAMMY_IMAGE_NAME).with_memory(&format!("{guest_memory_size_kb}K")); + _test_memory_overhead(&guest, guest_memory_size_kb); } #[test] - fn test_vhost_user_net_tap_multiple_queues() { - test_vhost_user_net( - Some("vunet-tap1"), - 4, - &prepare_vhost_user_net_daemon, - false, - false, - ) + #[cfg(target_arch = "x86_64")] + // This test runs a guest with Landlock enabled and hotplugs a new disk. As + // the path for the hotplug disk is not pre-added to Landlock rules, this + // the test will result in a failure. + fn test_landlock() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_landlock(&guest); } #[test] - fn test_vhost_user_net_host_mac() { - test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, true, false) + fn test_disk_hotplug() { + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); + let guest = + basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(kernel_path.to_str().unwrap()); + _test_disk_hotplug(&guest, false); } #[test] - fn test_vhost_user_net_client_mode() { - test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, true) + #[cfg(target_arch = "x86_64")] + fn test_disk_hotplug_with_landlock() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_disk_hotplug(&guest, true); } #[test] - #[cfg(not(target_arch = "aarch64"))] - fn test_vhost_user_blk_default() { - test_vhost_user_blk(2, false, false, Some(&prepare_vubd)) - } + fn test_disk_resize() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - #[test] - #[cfg(not(target_arch = "aarch64"))] - fn test_vhost_user_blk_readonly() { - test_vhost_user_blk(1, true, false, Some(&prepare_vubd)) - } + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); - #[test] - #[cfg(not(target_arch = "aarch64"))] - fn test_vhost_user_blk_direct() { - test_vhost_user_blk(1, false, true, Some(&prepare_vubd)) - } + let api_socket = temp_api_path(&guest.tmp_dir); - #[test] - fn test_boot_from_vhost_user_blk_default() { - test_boot_from_vhost_user_blk(1, false, false, Some(&prepare_vubd)) - } + // Create a disk image that we can write to + assert!( + exec_host_command_output("sudo dd if=/dev/zero of=/tmp/resize.img bs=1M count=16") + .status + .success() + ); - #[test] - #[cfg(target_arch = "x86_64")] - fn test_split_irqchip() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let mut cmd = GuestCommand::new(&guest); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() .default_net() - .capture_output() - .spawn() - .unwrap(); + .capture_output(); + + let mut child = cmd.spawn().unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); + + // Add the disk to the VM + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some("path=/tmp/resize.img,id=test0"), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}") + ); + + // Check that /dev/vdc exists and the block size is 16M. assert_eq!( guest - .ssh_command("grep -c IO-APIC.*timer /proc/interrupts || true") + .ssh_command("lsblk | grep vdc | grep -c 16M") .unwrap() .trim() .parse::() - .unwrap_or(1), - 0 + .unwrap_or_default(), + 1 ); + // And check the block device can be written to. + guest + .ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=16") + .unwrap(); + + // Resize disk to 32M + let resize_up_success = + resize_disk_command(&api_socket, "test0", "33554432" /* 32M */); + assert!(resize_up_success); + assert_eq!( guest - .ssh_command("grep -c IO-APIC.*cascade /proc/interrupts || true") + .ssh_command("lsblk | grep vdc | grep -c 32M") .unwrap() .trim() .parse::() - .unwrap_or(1), - 0 + .unwrap_or_default(), + 1 ); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } - - #[test] - #[cfg(target_arch = "x86_64")] - fn test_dmi_serial_number() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--platform", "serial_number=a=b;c=d"]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + // And check all blocks can be written to + guest + .ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=32") + .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Resize down to original size + let resize_down_success = + resize_disk_command(&api_socket, "test0", "16777216" /* 16M */); + assert!(resize_down_success); assert_eq!( guest - .ssh_command("sudo cat /sys/class/dmi/id/product_serial") + .ssh_command("lsblk | grep vdc | grep -c 16M") .unwrap() - .trim(), - "a=b;c=d" + .trim() + .parse::() + .unwrap_or_default(), + 1 ); + + // And check all blocks can be written to, again + guest + .ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=16") + .unwrap(); }); kill_child(&mut child); @@ -3676,456 +3144,983 @@ mod common_parallel { } #[test] - #[cfg(target_arch = "x86_64")] - fn test_dmi_uuid() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_disk_resize_qcow2() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); + + let api_socket = temp_api_path(&guest.tmp_dir); + + let test_disk_path = guest.tmp_dir.as_path().join("resize-test.qcow2"); + + // Create a 16MB QCOW2 disk image + assert!( + exec_host_command_output(&format!( + "qemu-img create -f qcow2 {} 16M", + test_disk_path.to_str().unwrap() + )) + .status + .success() + ); + + let mut cmd = GuestCommand::new(&guest); + + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--platform", "uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a"]) .default_disks() .default_net() - .capture_output() - .spawn() - .unwrap(); + .capture_output(); + + let mut child = cmd.spawn().unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); + + // Add the QCOW2 disk to the VM + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some(&format!( + "path={},id=test0", + test_disk_path.to_str().unwrap() + )), + ); + assert!(cmd_success); + assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"test0\"")); + + // Check that /dev/vdc exists and the block size is 16M assert_eq!( guest - .ssh_command("sudo cat /sys/class/dmi/id/product_uuid") + .ssh_command("lsblk | grep vdc | grep -c 16M") .unwrap() - .trim(), - "1e8aa28a-435d-4027-87f4-40dceff1fa0a" + .trim() + .parse::() + .unwrap_or_default(), + 1 ); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } - - #[test] - #[cfg(target_arch = "x86_64")] - fn test_dmi_oem_strings() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - - let s1 = "io.systemd.credential:xx=yy"; - let s2 = "This is a test string"; - let oem_strings = format!("oem_strings=[{s1},{s2}]"); - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--platform", &oem_strings]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + // Write some data to verify it persists after resize + guest + .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=8") + .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Resize disk up to 32M + let resize_up_success = + resize_disk_command(&api_socket, "test0", "33554432" /* 32M */); + assert!(resize_up_success); + // Check new size is visible assert_eq!( guest - .ssh_command("sudo dmidecode --oem-string count") + .ssh_command("lsblk | grep vdc | grep -c 32M") .unwrap() - .trim(), - "2" + .trim() + .parse::() + .unwrap_or_default(), + 1 ); - assert_eq!( - guest - .ssh_command("sudo dmidecode --oem-string 1") - .unwrap() - .trim(), - s1 - ); + // Write to the expanded area to verify it works + guest + .ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=32") + .unwrap(); + + // Resize to 64M to exercise L1 table growth + let resize_up_again_success = + resize_disk_command(&api_socket, "test0", "67108864" /* 64M */); + assert!(resize_up_again_success); assert_eq!( guest - .ssh_command("sudo dmidecode --oem-string 2") + .ssh_command("lsblk | grep vdc | grep -c 64M") .unwrap() - .trim(), - s2 + .trim() + .parse::() + .unwrap_or_default(), + 1 ); + + // Write to the full disk + guest + .ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=64") + .unwrap(); + + // QCOW2 does not support shrinking, no resize down test here. }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); + disk_check_consistency(&test_disk_path, None); + handle_child_output(r, &output); } - #[test] - fn test_virtio_fs() { - _test_virtio_fs(&prepare_virtiofsd, false, None) - } + fn create_loop_device(backing_file_path: &str, block_size: u32, num_retries: usize) -> String { + const LOOP_CONFIGURE: u64 = 0x4c0a; + const LOOP_CTL_GET_FREE: u64 = 0x4c82; + const LOOP_CTL_PATH: &str = "/dev/loop-control"; + const LOOP_DEVICE_PREFIX: &str = "/dev/loop"; - #[test] - fn test_virtio_fs_hotplug() { - _test_virtio_fs(&prepare_virtiofsd, true, None) - } + #[repr(C)] + struct LoopInfo64 { + lo_device: u64, + lo_inode: u64, + lo_rdevice: u64, + lo_offset: u64, + lo_sizelimit: u64, + lo_number: u32, + lo_encrypt_type: u32, + lo_encrypt_key_size: u32, + lo_flags: u32, + lo_file_name: [u8; 64], + lo_crypt_name: [u8; 64], + lo_encrypt_key: [u8; 32], + lo_init: [u64; 2], + } - #[test] - #[cfg(not(feature = "mshv"))] - fn test_virtio_fs_multi_segment_hotplug() { - _test_virtio_fs(&prepare_virtiofsd, true, Some(15)) - } + impl Default for LoopInfo64 { + fn default() -> Self { + LoopInfo64 { + lo_device: 0, + lo_inode: 0, + lo_rdevice: 0, + lo_offset: 0, + lo_sizelimit: 0, + lo_number: 0, + lo_encrypt_type: 0, + lo_encrypt_key_size: 0, + lo_flags: 0, + lo_file_name: [0; 64], + lo_crypt_name: [0; 64], + lo_encrypt_key: [0; 32], + lo_init: [0; 2], + } + } + } - #[test] - #[cfg(not(feature = "mshv"))] - fn test_virtio_fs_multi_segment() { - _test_virtio_fs(&prepare_virtiofsd, false, Some(15)) - } + #[derive(Default)] + #[repr(C)] + struct LoopConfig { + fd: u32, + block_size: u32, + info: LoopInfo64, + _reserved: [u64; 8], + } - #[test] - fn test_virtio_pmem_discard_writes() { - test_virtio_pmem(true, false) + // Open loop-control device + let loop_ctl_file = OpenOptions::new() + .read(true) + .write(true) + .open(LOOP_CTL_PATH) + .unwrap(); + + // Open backing file + let backing_file = OpenOptions::new() + .read(true) + .write(true) + .open(backing_file_path) + .unwrap(); + + // Retry the whole get free -> open -> configure sequence so that a + // race with another parallel test claiming the same loop device + // is resolved by requesting a new free device on each attempt. + let mut loop_device_path = String::new(); + for i in 0..num_retries { + // Request a free loop device + let loop_device_number = + unsafe { libc::ioctl(loop_ctl_file.as_raw_fd(), LOOP_CTL_GET_FREE as _) }; + + if loop_device_number < 0 { + panic!("Couldn't find a free loop device"); + } + + loop_device_path = format!("{LOOP_DEVICE_PREFIX}{loop_device_number}"); + + // Open loop device + let loop_device_file = OpenOptions::new() + .read(true) + .write(true) + .open(&loop_device_path) + .unwrap(); + + let loop_config = LoopConfig { + fd: backing_file.as_raw_fd() as u32, + block_size, + ..Default::default() + }; + + let ret = unsafe { + libc::ioctl( + loop_device_file.as_raw_fd(), + LOOP_CONFIGURE as _, + &loop_config, + ) + }; + if ret == 0 { + break; + } + + if i < num_retries - 1 { + println!( + "Iteration {}: Failed to configure loop device {}: {}", + i, + loop_device_path, + io::Error::last_os_error() + ); + let jitter_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .subsec_nanos() + % 500 + + 100; + thread::sleep(Duration::from_millis(jitter_ms as u64)); + } else { + panic!( + "Failed {} times trying to configure the loop device {}: {}", + num_retries, + loop_device_path, + io::Error::last_os_error() + ); + } + } + + loop_device_path } #[test] - fn test_virtio_pmem_with_size() { - test_virtio_pmem(true, true) + fn test_virtio_block_topology() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + let test_disk_path = guest.tmp_dir.as_path().join("test.img"); + + let output = exec_host_command_output( + format!( + "qemu-img create -f raw {} 16M", + test_disk_path.to_str().unwrap() + ) + .as_str(), + ); + if !output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("qemu-img command failed\nstdout\n{stdout}\nstderr\n{stderr}"); + } + + let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5); + _test_virtio_block_topology(&guest, &loop_dev); + Command::new("losetup") + .args(["-d", &loop_dev]) + .output() + .expect("loop device not found"); } #[test] - fn test_boot_from_virtio_pmem() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - + fn test_virtio_block_direct_io_block_device_alignment_4k() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); + // The backing file for the loop device must live on a filesystem that + // supports O_DIRECT (e.g. ext4). guest.tmp_dir is on tmpfs inside + // Docker, and the loop driver forwards I/O to the backing file. + let mut workloads_path = dirs::home_dir().unwrap(); + workloads_path.push("workloads"); + let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap(); + let test_disk_path = img_dir.as_path().join("directio_test.img"); + // Preallocate the backing file -- a sparse file can deadlock when + // O_DIRECT writes through a loop device trigger block allocation + // in the backing filesystem. + assert!( + exec_host_command_output(&format!( + "fallocate -l 64M {}", + test_disk_path.to_str().unwrap() + )) + .status + .success(), + "fallocate failed" + ); + + let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5); + let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=1"]) .args(["--memory", "size=512M"]) .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args([ "--disk", format!( "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() ) .as_str(), - ]) - .default_net() - .args([ - "--pmem", format!( - "file={},size={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), - fs::metadata(guest.disk_config.disk(DiskType::OperatingSystem).unwrap()) - .unwrap() - .len() + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), + format!("path={loop_dev},direct=on,image_type=raw").as_str(), ]) - .args([ - "--cmdline", - DIRECT_KERNEL_BOOT_CMDLINE - .replace("vda1", "pmem0p1") - .as_str(), - ]) + .default_net() .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Simple checks to validate the VM booted properly - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + assert_eq!( + guest + .ssh_command("lsblk -t | grep vdc | awk '{print $6}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4096 + ); + + guest + .ssh_command( + "sudo dd if=/dev/urandom of=/tmp/pattern bs=4096 count=1 && \ + sudo dd if=/tmp/pattern of=/dev/vdc bs=4096 count=1 seek=1 oflag=direct && \ + sudo dd if=/dev/vdc of=/tmp/readback bs=4096 count=1 skip=1 iflag=direct && \ + cmp /tmp/pattern /tmp/readback", + ) + .unwrap(); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); + + Command::new("losetup") + .args(["-d", &loop_dev]) + .output() + .expect("loop device cleanup failed"); } #[test] - fn test_multiple_network_interfaces() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - + fn test_virtio_block_direct_io_file_backed_alignment_4k() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); + let mut workloads_path = dirs::home_dir().unwrap(); + workloads_path.push("workloads"); + let img_dir = TempDir::new_in(workloads_path.as_path()).unwrap(); + let fs_img_path = img_dir.as_path().join("fs_4ksec.img"); + + assert!( + exec_host_command_output(&format!( + "truncate -s 512M {}", + fs_img_path.to_str().unwrap() + )) + .status + .success(), + "truncate failed" + ); + + let loop_dev_path = create_loop_device(fs_img_path.to_str().unwrap(), 4096, 5); + + assert!( + exec_host_command_output(&format!("mkfs.ext4 -q {loop_dev_path}")) + .status + .success(), + "mkfs.ext4 failed" + ); + + let mnt_dir = img_dir.as_path().join("mnt"); + fs::create_dir_all(&mnt_dir).unwrap(); + assert!( + exec_host_command_output(&format!( + "mount {} {}", + loop_dev_path, + mnt_dir.to_str().unwrap() + )) + .status + .success(), + "mount failed" + ); + + let test_disk_path = mnt_dir.join("dio_file_test.raw"); + assert!( + exec_host_command_output(&format!( + "truncate -s 64M {}", + test_disk_path.to_str().unwrap() + )) + .status + .success(), + "truncate test disk failed" + ); + let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=1"]) .args(["--memory", "size=512M"]) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() .args([ - "--net", - guest.default_net_string().as_str(), - "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", - "tap=mytap1,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0", + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!( + "path={},direct=on,image_type=raw", + test_disk_path.to_str().unwrap() + ) + .as_str(), ]) + .default_net() .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - let tap_count = exec_host_command_output("ip link | grep -c mytap1"); - assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); + guest.wait_vm_boot().unwrap(); - // 3 network interfaces + default localhost ==> 4 interfaces + let log_sec: u32 = guest + .ssh_command("lsblk -t | grep vdc | awk '{print $6}'") + .unwrap() + .trim() + .parse() + .unwrap_or_default(); assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 4 + log_sec, 4096, + "expected 4096-byte logical sector for file on 4k-sector fs, got {log_sec}" ); + + guest + .ssh_command( + "sudo dd if=/dev/urandom of=/tmp/pattern bs=4096 count=8 && \ + sudo dd if=/tmp/pattern of=/dev/vdc bs=4096 count=8 seek=1 oflag=direct && \ + sudo dd if=/dev/vdc of=/tmp/readback bs=4096 count=8 skip=1 iflag=direct && \ + cmp /tmp/pattern /tmp/readback", + ) + .unwrap(); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); + + let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap())); + let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}")); } - #[test] - #[cfg(target_arch = "aarch64")] - fn test_pmu_on() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + // Helper function to verify sparse file + fn verify_sparse_file(test_disk_path: &str, expected_ratio: f64) { + let res = exec_host_command_output(&format!("ls -s --block-size=1 {test_disk_path}")); + assert!(res.status.success(), "ls -s command failed"); + let out = String::from_utf8_lossy(&res.stdout); + let actual_bytes: u64 = out + .split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .expect("Failed to parse ls -s output"); + + let res = exec_host_command_output(&format!("ls -l {test_disk_path}")); + assert!(res.status.success(), "ls -l command failed"); + let out = String::from_utf8_lossy(&res.stdout); + let apparent_size: u64 = out + .split_whitespace() + .nth(4) + .and_then(|s| s.parse().ok()) + .expect("Failed to parse ls -l output"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let threshold = (apparent_size as f64 * expected_ratio) as u64; + assert!( + actual_bytes < threshold, + "Expected file to be sparse: apparent_size={apparent_size} bytes, actual_disk_usage={actual_bytes} bytes (threshold={threshold})" + ); + } - // Test that PMU exists. - assert_eq!( - guest - .ssh_command(GREP_PMU_IRQ_CMD) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 + // Helper function to count zero flagged regions in QCOW2 image + fn count_qcow2_zero_regions(test_disk_path: &str) -> Option { + let res = + exec_host_command_output(&format!("qemu-img map --output=json -U {test_disk_path}")); + if !res.status.success() { + return None; + } + + let out = String::from_utf8_lossy(&res.stdout); + let map_json = serde_json::from_str::(&out).ok()?; + let regions = map_json.as_array()?; + + Some( + regions + .iter() + .filter(|r| { + let data = r["data"].as_bool().unwrap_or(true); + let zero = r["zero"].as_bool().unwrap_or(false); + // holes - data: false + // zero flagged regions - data: true, zero: true + !data || zero + }) + .count(), + ) + } + + // Helper function to verify file extents using FIEMAP after DISCARD + // TODO: Make verification more format-specific: + // - QCOW2: Check for fragmentation patterns showing deallocated clusters + // - RAW: Verify actual holes (unallocated extents) exist in sparse regions + // - Could parse extent output to count holes vs allocated regions + fn verify_fiemap_extents(test_disk_path: &str, format_type: &str) { + let blocksize_output = exec_host_command_output(&format!("stat -f -c %S {test_disk_path}")); + let blocksize = if blocksize_output.status.success() { + String::from_utf8_lossy(&blocksize_output.stdout) + .trim() + .parse::() + .unwrap_or(4096) + } else { + 4096 + }; + + let fiemap_output = + exec_host_command_output(&format!("filefrag -b {blocksize} -v {test_disk_path}")); + if fiemap_output.status.success() { + let fiemap_str = String::from_utf8_lossy(&fiemap_output.stdout); + + // Verify we have extent information indicating sparse regions + let has_extents = fiemap_str.contains("extent") || fiemap_str.contains("extents"); + let has_holes = fiemap_str.contains("hole"); + + assert!( + has_extents || has_holes, + "FIEMAP should show extent information or holes for {format_type} file" ); - }); + } + } - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + /// Helper function to verify a disk region reads as all zeros from within the guest + fn assert_guest_disk_region_is_zero(guest: &Guest, device: &str, offset: u64, length: u64) { + let result = guest + .ssh_command(&format!( + "sudo hexdump -v -s {offset} -n {length} -e '1/1 \"%02x\"' {device} | grep -qv '^00*$' && echo 'NONZERO' || echo 'ZEROS'" + )) + .unwrap(); - handle_child_output(r, &output); + assert!( + result.trim() == "ZEROS", + "Expected {} region at offset {} length {} to read as zeros, but got: {}", + device, + offset, + length, + result.trim() + ); } - #[test] - fn test_serial_off() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Common test sizes for discard/fstrim tests (all formats): 9 small (≤256KB), then one 4MB + const BLOCK_DISCARD_TEST_SIZES_KB: &[u64] = &[64, 128, 256, 64, 128, 256, 64, 128, 256, 4096]; + + fn _test_virtio_block_discard( + format_name: &str, + qemu_img_format: &str, + extra_create_args: &[&str], + expect_discard_success: bool, + verify_disk: bool, + ) { + _test_virtio_block_discard_with_backend( + format_name, + qemu_img_format, + extra_create_args, + expect_discard_success, + verify_disk, + false, + ); + } + + fn _test_virtio_block_discard_with_backend( + format_name: &str, + qemu_img_format: &str, + extra_create_args: &[&str], + expect_discard_success: bool, + verify_disk: bool, + disable_io_uring: bool, + ) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + + let test_disk_path = guest + .tmp_dir + .as_path() + .join(format!("discard_test.{}", format_name.to_lowercase())); + + let mut cmd = format!("qemu-img create -f {qemu_img_format} "); + if !extra_create_args.is_empty() { + cmd.push_str(&extra_create_args.join(" ")); + cmd.push(' '); + } + cmd.push_str(&format!("{} 2G", test_disk_path.to_str().unwrap())); + + let res = exec_host_command_output(&cmd); + assert!( + res.status.success(), + "Failed to create {format_name} test image" + ); + let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cpus", "boot=4"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!( + "path={},num_queues=4,image_type={}{}", + test_disk_path.to_str().unwrap(), + format_name.to_lowercase(), + if disable_io_uring { + ",_disable_io_uring=on" + } else { + "" + } + ) + .as_str(), + ]) .default_net() - .args(["--serial", "off"]) .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + const CLUSTER_SIZE_BYTES: u64 = 64 * 1024; // One QCOW2 cluster + const WRITE_SIZE_MB: u64 = 4; + const WRITE_OFFSET_MB: u64 = 1; + + // Build discard operations within the written region + let write_start = WRITE_OFFSET_MB * 1024 * 1024; + let mut discard_operations: Vec<(u64, u64)> = Vec::new(); + let mut current_offset = write_start; + + for &size_kb in BLOCK_DISCARD_TEST_SIZES_KB { + let size = size_kb * 1024; + discard_operations.push((current_offset, size)); + current_offset += size + CLUSTER_SIZE_BYTES; // Add gap between operations + } + + let size_after_write = std::cell::Cell::new(0u64); + + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + guest.wait_vm_boot().unwrap(); - // Test that there is no ttyS0 assert_eq!( guest - .ssh_command(GREP_SERIAL_IRQ_CMD) + .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() - .unwrap_or(1), - 0 + .unwrap_or_default(), + 1 ); - }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // Write one 4MB block at offset 1MB + guest + .ssh_command(&format!( + "sudo dd if=/dev/zero of=/dev/vdc bs=1M count={WRITE_SIZE_MB} seek={WRITE_OFFSET_MB} oflag=direct" + )) + .unwrap(); + guest.ssh_command("sync").unwrap(); - handle_child_output(r, &output); - } + // For QCOW2, measure file size after write to verify deallocation later + let write_size = if qemu_img_format == "qcow2" { + let res = exec_host_command_output(&format!( + "ls -s --block-size=1 {}", + test_disk_path.to_str().unwrap() + )); + assert!(res.status.success()); + String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .next() + .and_then(|s| s.parse::().ok()) + .expect("Failed to parse file size after write") + } else { + 0 + }; + size_after_write.set(write_size); + + if expect_discard_success { + for (i, (offset, length)) in discard_operations.iter().enumerate() { + let result = guest + .ssh_command(&format!( + "sudo blkdiscard -v -o {offset} -l {length} /dev/vdc 2>&1 || true" + )) + .unwrap(); + + assert!( + !result.contains("Operation not supported") + && !result.contains("BLKDISCARD"), + "blkdiscard #{i} at offset {offset} length {length} failed: {result}" + ); + } - #[test] - fn test_serial_null() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); - #[cfg(target_arch = "x86_64")] - let console_str: &str = "console=ttyS0"; - #[cfg(target_arch = "aarch64")] - let console_str: &str = "console=ttyAMA0"; + // Force sync to ensure async DISCARD operations complete + guest.ssh_command("sync").unwrap(); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args([ - "--cmdline", - DIRECT_KERNEL_BOOT_CMDLINE - .replace("console=hvc0 ", console_str) - .as_str(), - ]) - .default_disks() - .default_net() - .args(["--serial", "null"]) - .args(["--console", "off"]) - .capture_output(); + // Verify VM sees zeros in discarded regions + for (offset, length) in discard_operations.iter() { + assert_guest_disk_region_is_zero(&guest, "/dev/vdc", *offset, *length); + } - let mut child = cmd.spawn().unwrap(); + guest.ssh_command("echo test").unwrap(); + } else { + // For unsupported formats, blkdiscard should fail with "not supported" + use test_infra::ssh_command_ip; + let result = ssh_command_ip( + "sudo blkdiscard -o 0 -l 4096 /dev/vdc 2>&1", + &guest.network.guest_ip0, + 0, + 5, + ); + assert!( + result.is_err(), + "blkdiscard should fail on unsupported format" + ); + guest.ssh_command("echo test").unwrap(); + } - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + if expect_discard_success { + if qemu_img_format == "qcow2" { + let res = exec_host_command_output(&format!( + "ls -s --block-size=1 {}", + test_disk_path.to_str().unwrap() + )); + assert!(res.status.success()); + let size_after_discard: u64 = String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .expect("Failed to parse file size after discard"); + + assert!( + size_after_discard < size_after_write.get(), + "QCOW2 file should shrink after DISCARD with sparse=true: after_write={} bytes, after_discard={} bytes", + size_after_write.get(), + size_after_discard + ); - // Test that there is a ttyS0 - assert_eq!( - guest - .ssh_command(GREP_SERIAL_IRQ_CMD) - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); - }); + verify_fiemap_extents(test_disk_path.to_str().unwrap(), "QCOW2"); + } else if qemu_img_format == "raw" { + let mut file = File::open(&test_disk_path) + .expect("Failed to open test disk for verification"); + + // Verify each discarded region contains all zeros + for (offset, length) in &discard_operations { + file.seek(SeekFrom::Start(*offset)) + .expect("Failed to seek to discarded region"); + + let mut buffer = vec![0u8; *length as usize]; + file.read_exact(&mut buffer) + .expect("Failed to read discarded region"); + + let all_zeros = buffer.iter().all(|&b| b == 0); + assert!( + all_zeros, + "Expected discarded region at offset {offset} length {length} to contain all zeros" + ); + } + + verify_sparse_file(test_disk_path.to_str().unwrap(), 1.0); + + verify_fiemap_extents(test_disk_path.to_str().unwrap(), "RAW"); + } + } + })); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); - let r = std::panic::catch_unwind(|| { - assert!(!String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); - }); + if verify_disk { + disk_check_consistency(&test_disk_path, None); + } + } - handle_child_output(r, &output); + #[test] + fn test_virtio_block_discard_qcow2() { + _test_virtio_block_discard("qcow2", "qcow2", &[], true, true); } #[test] - fn test_serial_tty() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_virtio_block_discard_raw() { + _test_virtio_block_discard("raw", "raw", &[], true, false); + } - let kernel_path = direct_kernel_boot_path(); + #[test] + fn test_virtio_block_discard_raw_aio() { + _test_virtio_block_discard_with_backend("raw", "raw", &[], true, false, true); + } - #[cfg(target_arch = "x86_64")] - let console_str: &str = "console=ttyS0"; - #[cfg(target_arch = "aarch64")] - let console_str: &str = "console=ttyAMA0"; + #[test] + fn test_virtio_block_write_zeroes_unmap_raw() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + + let test_disk_path = guest.tmp_dir.as_path().join("write_zeroes_unmap_test.raw"); + + let res = exec_host_command_output(&format!( + "dd if=/dev/zero of={} bs=1M count=128", + test_disk_path.to_str().unwrap() + )); + assert!(res.status.success(), "Failed to create raw test image"); let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) + .default_cpus() + .default_memory() + .default_kernel_cmdline() .args([ - "--cmdline", - DIRECT_KERNEL_BOOT_CMDLINE - .replace("console=hvc0 ", console_str) - .as_str(), + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={},image_type=raw", test_disk_path.to_str().unwrap()).as_str(), ]) - .default_disks() .default_net() - .args(["--serial", "tty"]) - .args(["--console", "off"]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Test that there is a ttyS0 assert_eq!( guest - .ssh_command(GREP_SERIAL_IRQ_CMD) + .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); - }); - // This sleep is needed to wait for the login prompt - thread::sleep(std::time::Duration::new(2, 0)); + let wz_max = guest + .ssh_command("cat /sys/block/vdc/queue/write_zeroes_max_bytes") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(); + assert!( + wz_max > 0, + "write_zeroes_max_bytes={wz_max}, VIRTIO_BLK_F_WRITE_ZEROES not negotiated" + ); + + guest + .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=64 oflag=direct") + .unwrap(); + guest.ssh_command("sync").unwrap(); + + // fallocate --punch-hole on a block device sends + // WRITE_ZEROES with VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP set. + let result = guest + .ssh_command("sudo fallocate -p -o 0 -l 67108864 /dev/vdc 2>&1 || true") + .unwrap(); + assert!( + !result.contains("Operation not supported") && !result.contains("not supported"), + "fallocate --punch-hole failed: {result}" + ); + guest.ssh_command("sync").unwrap(); + + assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 0, 4096 * 256); + + let test_disk_str = test_disk_path.to_str().unwrap(); + verify_sparse_file(test_disk_str, 1.0); + verify_fiemap_extents(test_disk_str, "raw"); + }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); + } - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); - }); + #[test] + fn test_virtio_block_discard_unsupported_vhd() { + _test_virtio_block_discard("vhd", "vpc", &["-o", "subformat=fixed"], false, false); + } - handle_child_output(r, &output); + #[test] + fn test_virtio_block_discard_unsupported_vhdx() { + _test_virtio_block_discard("vhdx", "vhdx", &[], false, false); } #[test] - fn test_serial_file() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_virtio_block_discard_loop_device() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - let serial_path = guest.tmp_dir.as_path().join("serial-output"); - #[cfg(target_arch = "x86_64")] - let console_str: &str = "console=ttyS0"; - #[cfg(target_arch = "aarch64")] - let console_str: &str = "console=ttyAMA0"; + let test_disk_path = guest.tmp_dir.as_path().join("loop_discard_test.raw"); + let res = run_qemu_img(&test_disk_path, &["create", "-f", "raw"], Some(&["128M"])); + assert!( + res.status.success(), + "Failed to create raw backing image: {}", + String::from_utf8_lossy(&res.stderr) + ); + + let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5); let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=1"]) .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args([ - "--cmdline", - DIRECT_KERNEL_BOOT_CMDLINE - .replace("console=hvc0 ", console_str) - .as_str(), + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={loop_dev},image_type=raw").as_str(), ]) - .default_disks() .default_net() - .args([ - "--serial", - format!("file={}", serial_path.to_str().unwrap()).as_str(), - ]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Test that there is a ttyS0 assert_eq!( guest - .ssh_command(GREP_SERIAL_IRQ_CMD) + .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() @@ -4133,305 +4128,498 @@ mod common_parallel { 1 ); - guest.ssh_command("sudo shutdown -h now").unwrap(); - }); - - let _ = child.wait_timeout(std::time::Duration::from_secs(20)); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - handle_child_output(r, &output); - - let r = std::panic::catch_unwind(|| { - // Check that the cloud-hypervisor binary actually terminated - assert!(output.status.success()); - - // Do this check after shutdown of the VM as an easy way to ensure - // all writes are flushed to disk - let mut f = std::fs::File::open(serial_path).unwrap(); - let mut buf = String::new(); - f.read_to_string(&mut buf).unwrap(); - assert!(buf.contains(CONSOLE_TEST_STRING)); - }); + assert_eq!( + guest + .ssh_command("lsblk -t | grep vdc | awk '{print $6}'") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 4096 + ); - handle_child_output(r, &output); - } + let discard_max = guest + .ssh_command("cat /sys/block/vdc/queue/discard_max_bytes") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(); + assert!( + discard_max > 0, + "discard_max_bytes={discard_max}, VIRTIO_BLK_F_DISCARD not negotiated" + ); - #[test] - fn test_pty_interaction() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - let serial_option = if cfg!(target_arch = "x86_64") { - " console=ttyS0" - } else { - " console=ttyAMA0" - }; - let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option; + guest + .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=4096 count=1024 oflag=direct") + .unwrap(); + guest.ssh_command("sync").unwrap(); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", &cmdline]) - .default_disks() - .default_net() - .args(["--serial", "null"]) - .args(["--console", "pty"]) - .args(["--api-socket", &api_socket]) - .spawn() - .unwrap(); + let result = guest + .ssh_command("sudo blkdiscard -v -o 0 -l 4194304 /dev/vdc 2>&1 || true") + .unwrap(); + assert!( + !result.contains("Operation not supported") + && !result.contains("BLKDISCARD ioctl failed"), + "blkdiscard failed on loop device: {result}" + ); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - // Get pty fd for console - let console_path = get_pty_path(&api_socket, "console"); - _test_pty_interaction(console_path); + guest.ssh_command("sync").unwrap(); - guest.ssh_command("sudo shutdown -h now").unwrap(); + assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 0, 4194304); }); - let _ = child.wait_timeout(std::time::Duration::from_secs(20)); - let _ = child.kill(); + kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); - let r = std::panic::catch_unwind(|| { - // Check that the cloud-hypervisor binary actually terminated - assert!(output.status.success()) - }); - handle_child_output(r, &output); + Command::new("losetup") + .args(["-d", &loop_dev]) + .output() + .expect("loop device not found"); } #[test] - fn test_serial_socket_interaction() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let serial_socket = guest.tmp_dir.as_path().join("serial.socket"); - let serial_socket_pty = guest.tmp_dir.as_path().join("serial.pty"); - let serial_option = if cfg!(target_arch = "x86_64") { - " console=ttyS0" - } else { - " console=ttyAMA0" - }; - let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option; - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", &cmdline]) - .default_disks() - .default_net() - .args(["--console", "null"]) - .args([ - "--serial", - format!("socket={}", serial_socket.to_str().unwrap()).as_str(), - ]) - .spawn() - .unwrap(); + fn test_virtio_block_discard_dm_snapshot() { + // Verify that the guest remains stable when BLKDISCARD fails on the + // host backend. DM snapshot targets do not support discard, so the + // VMM returns VIRTIO_BLK_S_IOERR. The guest must handle this + // gracefully even under repeated attempts. + // + // DM topology follows the same pattern used by WindowsDiskConfig. + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - let _ = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - }); + let origin_path = guest.tmp_dir.as_path().join("dm_origin.raw"); + let cow_path = guest.tmp_dir.as_path().join("dm_cow.raw"); - let mut socat_command = Command::new("socat"); - let socat_args = [ - &format!("pty,link={},raw", serial_socket_pty.display()), - &format!("UNIX-CONNECT:{}", serial_socket.display()), - ]; - socat_command.args(socat_args); + let res = run_qemu_img(&origin_path, &["create", "-f", "raw"], Some(&["128M"])); + assert!( + res.status.success(), + "Failed to create origin image: {}", + String::from_utf8_lossy(&res.stderr) + ); - let mut socat_child = socat_command.spawn().unwrap(); - thread::sleep(std::time::Duration::new(1, 0)); + let cow_size: u64 = 128 << 20; + let cow_sectors = cow_size / 512; + let cow_file = File::create(&cow_path).expect("Expect creating COW image to succeed"); + cow_file + .set_len(cow_size) + .expect("Expect truncating COW image to succeed"); - let _ = std::panic::catch_unwind(|| { - _test_pty_interaction(serial_socket_pty); - }); + let origin_sectors: u64 = 128 * 1024 * 1024 / 512; + let origin_loop = create_loop_device(origin_path.to_str().unwrap(), 4096, 5); + let cow_loop = create_loop_device(cow_path.to_str().unwrap(), 512, 5); - let _ = socat_child.kill(); - let _ = socat_child.wait(); + let unique = format!( + "ch-test-{}", + guest + .tmp_dir + .as_path() + .file_name() + .unwrap() + .to_str() + .unwrap() + ); + let cow_dm_name = format!("{unique}-cow"); + let snap_dm_name = format!("{unique}-snap"); - let r = std::panic::catch_unwind(|| { - guest.ssh_command("sudo shutdown -h now").unwrap(); - }); + let output = Command::new("dmsetup") + .args([ + "create", + &cow_dm_name, + "--table", + &format!("0 {cow_sectors} linear {cow_loop} 0"), + ]) + .output() + .expect("Failed to run dmsetup"); + assert!( + output.status.success(), + "dmsetup create (cow linear) failed: {}", + String::from_utf8_lossy(&output.stderr) + ); - let _ = child.wait_timeout(std::time::Duration::from_secs(20)); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - handle_child_output(r, &output); + Command::new("dmsetup") + .arg("mknodes") + .output() + .expect("dmsetup mknodes failed"); - let r = std::panic::catch_unwind(|| { - // Check that the cloud-hypervisor binary actually terminated - if !output.status.success() { - panic!( - "Cloud Hypervisor process failed to terminate gracefully: {:?}", - output.status - ); - } - }); - handle_child_output(r, &output); - } + // dm-snapshot: origin + COW, non-persistent, chunk size 8 sectors. + let output = Command::new("dmsetup") + .args([ + "create", + &snap_dm_name, + "--table", + &format!("0 {origin_sectors} snapshot {origin_loop} /dev/mapper/{cow_dm_name} N 8"), + ]) + .output() + .expect("Failed to run dmsetup"); + assert!( + output.status.success(), + "dmsetup create (snapshot) failed: {}", + String::from_utf8_lossy(&output.stderr) + ); - #[test] - fn test_virtio_console() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + Command::new("dmsetup") + .arg("mknodes") + .output() + .expect("dmsetup mknodes failed"); - let kernel_path = direct_kernel_boot_path(); + let dm_dev = format!("/dev/mapper/{snap_dm_name}"); let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=1"]) .args(["--memory", "size=512M"]) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!("path={dm_dev},image_type=raw").as_str(), + ]) .default_net() - .args(["--console", "tty"]) - .args(["--serial", "null"]) .capture_output() .spawn() .unwrap(); - let text = String::from("On a branch floating down river a cricket, singing."); - let cmd = format!("echo {text} | sudo tee /dev/hvc0"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("lsblk | grep -c vdc") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + + let discard_max = guest + .ssh_command("cat /sys/block/vdc/queue/discard_max_bytes") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(); + assert!( + discard_max > 0, + "discard_max_bytes={discard_max}, VIRTIO_BLK_F_DISCARD not negotiated" + ); + + guest + .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=4096 count=1024 oflag=direct") + .unwrap(); + guest.ssh_command("sync").unwrap(); + + // Discard is expected to fail on DM snapshot because the + // snapshot target does not support BLKDISCARD. + for attempt in 1..=3 { + let result = guest + .ssh_command("sudo blkdiscard -o 0 -l 4194304 /dev/vdc 2>&1; echo rc=$?") + .unwrap(); + println!("blkdiscard attempt {attempt}: {result}"); - assert!(guest - .does_device_vendor_pair_match("0x1043", "0x1af4") - .unwrap_or_default()); + let uptime = guest.ssh_command("uptime").unwrap(); + assert!( + !uptime.is_empty(), + "Guest unresponsive after blkdiscard attempt {attempt}" + ); + } - guest.ssh_command(&cmd).unwrap(); + guest + .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=4096 count=256 oflag=direct") + .unwrap(); + let readback = guest + .ssh_command("sudo dd if=/dev/vdc bs=4096 count=1 iflag=direct 2>/dev/null | od -A n -t x1 | head -1") + .unwrap(); + assert!( + !readback.trim().is_empty(), + "Failed to read back from device after discard errors" + ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&output.stdout).contains(&text)); - }); - - handle_child_output(r, &output); + let _ = Command::new("dmsetup") + .args(["remove", &snap_dm_name]) + .output(); + let _ = Command::new("dmsetup") + .args(["remove", &cow_dm_name]) + .output(); + let _ = Command::new("losetup").args(["-d", &origin_loop]).output(); + let _ = Command::new("losetup").args(["-d", &cow_loop]).output(); + } + + fn _test_virtio_block_fstrim( + format_name: &str, + qemu_img_format: &str, + extra_create_args: &[&str], + expect_fstrim_success: bool, + verify_disk: bool, + ) { + _test_virtio_block_fstrim_with_backend( + format_name, + qemu_img_format, + extra_create_args, + expect_fstrim_success, + verify_disk, + false, + ); } - #[test] - fn test_console_file() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn _test_virtio_block_fstrim_with_backend( + format_name: &str, + qemu_img_format: &str, + extra_create_args: &[&str], + expect_fstrim_success: bool, + verify_disk: bool, + disable_io_uring: bool, + ) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - let console_path = guest.tmp_dir.as_path().join("console-output"); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + let test_disk_path = guest + .tmp_dir + .as_path() + .join(format!("fstrim_test.{}", format_name.to_lowercase())); + + let mut cmd = format!("qemu-img create -f {qemu_img_format} "); + if !extra_create_args.is_empty() { + cmd.push_str(&extra_create_args.join(" ")); + cmd.push(' '); + } + cmd.push_str(&format!("{} 2G", test_disk_path.to_str().unwrap())); + + let res = exec_host_command_output(&cmd); + assert!( + res.status.success(), + "Failed to create {format_name} test image" + ); + + const WRITE_SIZE_MB: u64 = 4; + const CLUSTER_SIZE_BYTES: u64 = 64 * 1024; + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() .args([ - "--console", - format!("file={}", console_path.to_str().unwrap()).as_str(), + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + format!( + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ) + .as_str(), + format!( + "path={},num_queues=4,image_type={}{}", + test_disk_path.to_str().unwrap(), + format_name.to_lowercase(), + if disable_io_uring { + ",_disable_io_uring=on" + } else { + "" + } + ) + .as_str(), ]) + .default_net() .capture_output() .spawn() .unwrap(); - guest.wait_vm_boot(None).unwrap(); + let max_size_during_writes = std::cell::Cell::new(0u64); - guest.ssh_command("sudo shutdown -h now").unwrap(); + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + guest.wait_vm_boot().unwrap(); - let _ = child.wait_timeout(std::time::Duration::from_secs(20)); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + assert_eq!( + guest + .ssh_command("lsblk | grep -c vdc") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); - let r = std::panic::catch_unwind(|| { - // Check that the cloud-hypervisor binary actually terminated - assert!(output.status.success()); + guest.ssh_command("sudo mkfs.ext4 -F /dev/vdc").unwrap(); - // Do this check after shutdown of the VM as an easy way to ensure - // all writes are flushed to disk - let mut f = std::fs::File::open(console_path).unwrap(); - let mut buf = String::new(); - f.read_to_string(&mut buf).unwrap(); + guest + .ssh_command("sudo mkdir -p /mnt/test && sudo mount /dev/vdc /mnt/test") + .unwrap(); + + for (iteration, &write_size_kb) in BLOCK_DISCARD_TEST_SIZES_KB.iter().enumerate() { + guest + .ssh_command(&format!( + "sudo dd if=/dev/zero of=/mnt/test/testfile{iteration} bs=1K count={write_size_kb}" + )) + .unwrap(); + + guest.ssh_command("sync").unwrap(); + + // Measure QCOW2 file size after writing + if qemu_img_format == "qcow2" { + let res = exec_host_command_output(&format!( + "ls -s --block-size=1 {}", + test_disk_path.to_str().unwrap() + )); + if res.status.success() + && let Some(size) = String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .next() + .and_then(|s| s.parse::().ok()) + { + max_size_during_writes.set(max_size_during_writes.get().max(size)); + } + } + + // Make blocks available for discard + guest + .ssh_command(&format!("sudo rm /mnt/test/testfile{iteration}")) + .unwrap(); + + guest.ssh_command("sync").unwrap(); + + if expect_fstrim_success { + let fstrim_result = guest.ssh_command("sudo fstrim -v /mnt/test 2>&1").unwrap(); - if !buf.contains(CONSOLE_TEST_STRING) { - eprintln!( - "\n\n==== Console file output ====\n\n{buf}\n\n==== End console file output ====" + // Would output like "/mnt/test: X bytes (Y MB) trimmed" + assert!( + fstrim_result.contains("trimmed") || fstrim_result.contains("bytes"), + "fstrim iteration {iteration} ({write_size_kb}KB) should report trimmed bytes: {fstrim_result}" + ); + } else { + // For unsupported formats, expect fstrim to fail + use test_infra::ssh_command_ip; + let result = ssh_command_ip( + "sudo fstrim -v /mnt/test 2>&1", + &guest.network.guest_ip0, + 0, + 5, + ); + assert!(result.is_err(), "fstrim should fail on unsupported format"); + guest.ssh_command("echo 'VM responsive'").unwrap(); + } + } + + guest.ssh_command("sudo umount /mnt/test").unwrap(); + + guest.ssh_command("echo test").unwrap(); + })); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + if expect_fstrim_success { + if qemu_img_format == "qcow2" { + // Verify QCOW2 file shrank after fstrim (sparse=true deallocates clusters) + let res = exec_host_command_output(&format!( + "ls -s --block-size=1 {}", + test_disk_path.to_str().unwrap() + )); + assert!(res.status.success()); + let size_after_fstrim: u64 = String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .expect("Failed to parse file size after fstrim"); + + assert!( + size_after_fstrim < max_size_during_writes.get(), + "QCOW2 file should shrink after fstrim with sparse=true: max_during_writes={} bytes, after_fstrim={} bytes", + max_size_during_writes.get(), + size_after_fstrim ); + } else if qemu_img_format == "raw" { + verify_sparse_file(test_disk_path.to_str().unwrap(), 0.5); } - assert!(buf.contains(CONSOLE_TEST_STRING)); - }); + } handle_child_output(r, &output); + + if verify_disk { + disk_check_consistency(&test_disk_path, None); + } } #[test] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - // The VFIO integration test starts cloud-hypervisor guest with 3 TAP - // backed networking interfaces, bound through a simple bridge on the host. - // So if the nested cloud-hypervisor succeeds in getting a directly - // assigned interface from its cloud-hypervisor host, we should be able to - // ssh into it, and verify that it's running with the right kernel command - // line (We tag the command line from cloud-hypervisor for that purpose). - // The third device is added to validate that hotplug works correctly since - // it is being added to the L2 VM through hotplugging mechanism. - // Also, we pass-through a virtio-blk device to the L2 VM to test the 32-bit - // vfio device support - fn test_vfio() { - setup_vfio_network_interfaces(); - - let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); - let guest = Guest::new_from_ip_range(Box::new(jammy), "172.18", 0); + fn test_virtio_block_fstrim_qcow2() { + _test_virtio_block_fstrim("qcow2", "qcow2", &[], true, true); + } - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + #[test] + fn test_virtio_block_fstrim_raw() { + _test_virtio_block_fstrim("raw", "raw", &[], true, false); + } - let kernel_path = direct_kernel_boot_path(); + #[test] + fn test_virtio_block_fstrim_raw_aio() { + _test_virtio_block_fstrim_with_backend("raw", "raw", &[], true, false, true); + } - let mut vfio_path = workload_path.clone(); - vfio_path.push("vfio"); + #[test] + fn test_virtio_block_fstrim_unsupported_vhd() { + _test_virtio_block_fstrim("vhd", "vpc", &["-o", "subformat=fixed"], false, false); + } - let mut cloud_init_vfio_base_path = vfio_path.clone(); - cloud_init_vfio_base_path.push("cloudinit.img"); + #[test] + fn test_virtio_block_fstrim_unsupported_vhdx() { + _test_virtio_block_fstrim("vhdx", "vhdx", &[], false, false); + } - // We copy our cloudinit into the vfio mount point, for the nested - // cloud-hypervisor guest to use. - rate_limited_copy( - guest.disk_config.disk(DiskType::CloudInit).unwrap(), - &cloud_init_vfio_base_path, - ) - .expect("copying of cloud-init disk failed"); + #[test] + #[ignore = "fallocate() preallocation requires native filesystem support (fails on overlay/tmpfs in CI)"] + fn test_virtio_block_sparse_off_raw() { + const TEST_DISK_SIZE: &str = "2G"; + const TEST_DISK_SIZE_BYTES: u64 = 2 * 1024 * 1024 * 1024; + const INITIAL_ALLOCATION_THRESHOLD: u64 = 1024 * 1024; - let mut vfio_disk_path = workload_path.clone(); - vfio_disk_path.push("vfio.img"); + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - // Create the vfio disk image - let output = Command::new("mkfs.ext4") - .arg("-d") - .arg(vfio_path.to_str().unwrap()) - .arg(vfio_disk_path.to_str().unwrap()) - .arg("2g") - .output() - .unwrap(); - if !output.status.success() { - eprintln!("{}", String::from_utf8_lossy(&output.stderr)); - panic!("mkfs.ext4 command generated an error"); - } + let test_disk_path = guest.tmp_dir.as_path().join("sparse_off_test.raw"); + let test_disk_path = test_disk_path.to_str().unwrap(); - let mut blk_file_path = workload_path; - blk_file_path.push("blk.img"); + let res = + exec_host_command_output(&format!("truncate -s {TEST_DISK_SIZE} {test_disk_path}")); + assert!(res.status.success(), "Failed to create sparse test file"); - let vfio_tap0 = "vfio-tap0"; - let vfio_tap1 = "vfio-tap1"; - let vfio_tap2 = "vfio-tap2"; - let vfio_tap3 = "vfio-tap3"; + let res = exec_host_command_output(&format!("ls -s --block-size=1 {test_disk_path}")); + assert!(res.status.success()); + let initial_bytes: u64 = String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .expect("Failed to parse initial disk usage"); + assert!( + initial_bytes < INITIAL_ALLOCATION_THRESHOLD, + "File should be initially sparse: {initial_bytes} bytes allocated" + ); let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) - .args(["--memory", "size=2G,hugepages=on,shared=on"]) + .default_cpus() + .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args([ "--disk", format!( @@ -4444,189 +4632,199 @@ mod common_parallel { guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), - format!("path={}", vfio_disk_path.to_str().unwrap()).as_str(), - format!("path={},iommu=on,readonly=true", blk_file_path.to_str().unwrap()).as_str(), - ]) - .args([ - "--cmdline", - format!( - "{DIRECT_KERNEL_BOOT_CMDLINE} kvm-intel.nested=1 vfio_iommu_type1.allow_unsafe_interrupts" - ) - .as_str(), + format!("path={test_disk_path},sparse=off").as_str(), ]) + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + assert_eq!( + guest + .ssh_command("lsblk | grep -c vdc") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + }); + + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + + // After VM starts with sparse=off, verify file is fully allocated. + // Strategy is to compare compare physical vs logical bytes + // - physical >= logical is fully allocated, modulo block alignment + // - physical < logical is still sparse + + let res = exec_host_command_output(&format!("ls -l {test_disk_path}")); + assert!(res.status.success()); + let logical_size: u64 = String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .nth(4) + .and_then(|s| s.parse().ok()) + .expect("Failed to parse logical size"); + + let res = exec_host_command_output(&format!("ls -s --block-size=1 {test_disk_path}")); + assert!(res.status.success()); + let physical_size: u64 = String::from_utf8_lossy(&res.stdout) + .split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .expect("Failed to parse physical size"); + + assert_eq!( + logical_size, TEST_DISK_SIZE_BYTES, + "Logical size should be exactly {TEST_DISK_SIZE_BYTES} bytes, got {logical_size}" + ); + + let res = exec_host_command_output(&format!("stat -c '%o' {test_disk_path}")); + assert!(res.status.success()); + let block_size: u64 = String::from_utf8_lossy(&res.stdout) + .trim() + .parse() + .expect("Failed to parse block size from stat"); + + let expected_max = logical_size.div_ceil(block_size) * block_size; + + assert!( + physical_size >= logical_size, + "File should be fully allocated with sparse=off: logical={logical_size} bytes, physical={physical_size} bytes (physical < logical means still sparse)" + ); + + assert!( + physical_size <= expected_max, + "Physical size seems too large: logical={logical_size} bytes, physical={physical_size} bytes, expected_max={expected_max} bytes (block_size={block_size})" + ); + } + + #[test] + fn test_virtio_block_sparse_off_qcow2() { + const TEST_DISK_SIZE: &str = "2G"; + + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + + let test_disk_path = guest.tmp_dir.as_path().join("sparse_off_test.qcow2"); + let test_disk_path = test_disk_path.to_str().unwrap(); + + let res = exec_host_command_output(&format!( + "qemu-img create -f qcow2 {test_disk_path} {TEST_DISK_SIZE}" + )); + assert!(res.status.success(), "Failed to create QCOW2 test image"); + + let zero_regions_before = count_qcow2_zero_regions(test_disk_path) + .expect("Failed to get initial zero regions count"); + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args([ - "--net", - format!("tap={},mac={}", vfio_tap0, guest.network.guest_mac).as_str(), - format!( - "tap={},mac={},iommu=on", - vfio_tap1, guest.network.l2_guest_mac1 - ) - .as_str(), + "--disk", format!( - "tap={},mac={},iommu=on", - vfio_tap2, guest.network.l2_guest_mac2 + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() ) .as_str(), format!( - "tap={},mac={},iommu=on", - vfio_tap3, guest.network.l2_guest_mac3 + "path={}", + guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), + format!("path={test_disk_path},sparse=off,num_queues=4").as_str(), ]) + .default_net() .capture_output() .spawn() .unwrap(); - thread::sleep(std::time::Duration::new(30, 0)); - let r = std::panic::catch_unwind(|| { - guest.ssh_command_l1("sudo systemctl start vfio").unwrap(); - thread::sleep(std::time::Duration::new(120, 0)); - - // We booted our cloud hypervisor L2 guest with a "VFIOTAG" tag - // added to its kernel command line. - // Let's ssh into it and verify that it's there. If it is it means - // we're in the right guest (The L2 one) because the QEMU L1 guest - // does not have this command line tag. - assert!(check_matched_lines_count( - guest.ssh_command_l2_1("cat /proc/cmdline").unwrap().trim(), - vec!["VFIOTAG"], - 1 - )); - - // Let's also verify from the second virtio-net device passed to - // the L2 VM. - assert!(check_matched_lines_count( - guest.ssh_command_l2_2("cat /proc/cmdline").unwrap().trim(), - vec!["VFIOTAG"], - 1 - )); + guest.wait_vm_boot().unwrap(); - // Check the amount of PCI devices appearing in L2 VM. - assert!(check_lines_count( + assert_eq!( guest - .ssh_command_l2_1("ls /sys/bus/pci/devices") + .ssh_command("lsblk | grep -c vdc") .unwrap() - .trim(), - 8 - )); - - // Check both if /dev/vdc exists and if the block size is 16M in L2 VM - assert!(check_matched_lines_count( - guest.ssh_command_l2_1("lsblk").unwrap().trim(), - vec!["vdc", "16M"], + .trim() + .parse::() + .unwrap_or_default(), 1 - )); + ); - // Hotplug an extra virtio-net device through L2 VM. - guest - .ssh_command_l1( - "echo 0000:00:09.0 | sudo tee /sys/bus/pci/devices/0000:00:09.0/driver/unbind", - ) - .unwrap(); - guest - .ssh_command_l1("echo 0000:00:09.0 | sudo tee /sys/bus/pci/drivers/vfio-pci/bind") - .unwrap(); - let vfio_hotplug_output = guest - .ssh_command_l1( - "sudo /mnt/ch-remote \ - --api-socket=/tmp/ch_api.sock \ - add-device path=/sys/bus/pci/devices/0000:00:09.0,id=vfio123", - ) - .unwrap(); - assert!(check_matched_lines_count( - vfio_hotplug_output.trim(), - vec!["{\"id\":\"vfio123\",\"bdf\":\"0000:00:08.0\"}"], - 1 - )); - - thread::sleep(std::time::Duration::new(10, 0)); - - // Let's also verify from the third virtio-net device passed to - // the L2 VM. This third device has been hotplugged through the L2 - // VM, so this is our way to validate hotplug works for VFIO PCI. - assert!(check_matched_lines_count( - guest.ssh_command_l2_3("cat /proc/cmdline").unwrap().trim(), - vec!["VFIOTAG"], - 1 - )); - - // Check the amount of PCI devices appearing in L2 VM. - // There should be one more device than before, raising the count - // up to 9 PCI devices. - assert!(check_lines_count( - guest - .ssh_command_l2_1("ls /sys/bus/pci/devices") - .unwrap() - .trim(), - 9 - )); - - // Let's now verify that we can correctly remove the virtio-net - // device through the "remove-device" command responsible for - // unplugging VFIO devices. - guest - .ssh_command_l1( - "sudo /mnt/ch-remote \ - --api-socket=/tmp/ch_api.sock \ - remove-device vfio123", - ) - .unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - - // Check the amount of PCI devices appearing in L2 VM is back down - // to 8 devices. - assert!(check_lines_count( - guest - .ssh_command_l2_1("ls /sys/bus/pci/devices") - .unwrap() - .trim(), - 8 - )); + // With sparse=off, DISCARD should NOT be advertised. + // blkdiscard is expected to fail. + let discard_result = + guest.ssh_command("sudo blkdiscard -o 1048576 -l 1048576 /dev/vdc 2>&1; echo $?"); + let exit_code = discard_result + .unwrap() + .trim() + .lines() + .last() + .unwrap_or("1") + .parse::() + .unwrap_or(1); + assert_ne!( + exit_code, 0, + "blkdiscard should fail with sparse=off (DISCARD not advertised)" + ); - // Perform memory hotplug in L2 and validate the memory is showing - // up as expected. In order to check, we will use the virtio-net - // device already passed through L2 as a VFIO device, this will - // verify that VFIO devices are functional with memory hotplug. - assert!(guest.get_total_memory_l2().unwrap_or_default() > 480_000); + // WRITE_ZEROES should still work via blkdiscard --zeroout guest - .ssh_command_l2_1( - "sudo bash -c 'echo online > /sys/devices/system/memory/auto_online_blocks'", + .ssh_command( + "sudo dd if=/dev/urandom of=/dev/vdc bs=1K count=64 seek=1024 oflag=direct", ) .unwrap(); + guest.ssh_command("sync").unwrap(); guest - .ssh_command_l1( - "sudo /mnt/ch-remote \ - --api-socket=/tmp/ch_api.sock \ - resize --memory=1073741824", - ) + .ssh_command("sudo blkdiscard -z -o 1048576 -l 65536 /dev/vdc") .unwrap(); - assert!(guest.get_total_memory_l2().unwrap_or_default() > 960_000); + guest.ssh_command("sync").unwrap(); + + assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 1048576, 65536); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); - cleanup_vfio_network_interfaces(); + let zero_regions_after = count_qcow2_zero_regions(test_disk_path) + .expect("Failed to get final zero regions count"); handle_child_output(r, &output); + + // WRITE_ZEROES should still produce zero-flagged regions + assert!( + zero_regions_after > zero_regions_before, + "Expected zero-flagged regions to increase via WRITE_ZEROES: before={zero_regions_before}, after={zero_regions_after}" + ); + + disk_check_consistency(test_disk_path, None); } #[test] - fn test_direct_kernel_boot_noacpi() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_virtio_balloon_deflate_on_oom() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); + let api_socket = temp_api_path(&guest.tmp_dir); + + //Let's start a 4G guest with balloon occupied 2G memory let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + .args(["--api-socket", &api_socket]) + .default_cpus() + .args(["--memory", "size=4G"]) .args(["--kernel", kernel_path.to_str().unwrap()]) - .args([ - "--cmdline", - format!("{DIRECT_KERNEL_BOOT_CMDLINE} acpi=off").as_str(), - ]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--balloon", "size=2G,deflate_on_oom=on"]) .default_disks() .default_net() .capture_output() @@ -4634,10 +4832,35 @@ mod common_parallel { .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + // Wait for balloon memory's initialization and check its size. + // The virtio-balloon driver might take a few seconds to report the + // balloon effective size back to the VMM. + assert!(wait_until(Duration::from_secs(20), || { + balloon_size(&api_socket) == 2147483648 + })); + let orig_balloon = balloon_size(&api_socket); + println!("The original balloon memory size is {orig_balloon} bytes"); + assert!(orig_balloon == 2147483648); + + // Two steps to verify if the 'deflate_on_oom' parameter works. + // 1st: run a command to trigger an OOM in the guest. + guest + .ssh_command("echo f | sudo tee /proc/sysrq-trigger") + .unwrap(); + + // Give some time for the OOM to happen in the guest and be reported + // back to the host. + assert!(wait_until(Duration::from_secs(20), || { + balloon_size(&api_socket) < 2147483648 + })); + + // 2nd: check balloon_mem's value to verify balloon has been automatically deflated + let deflated_balloon = balloon_size(&api_socket); + println!("After deflating, balloon memory size is {deflated_balloon} bytes"); + // Verify the balloon size deflated + assert!(deflated_balloon < 2147483648); }); kill_child(&mut child); @@ -4647,148 +4870,196 @@ mod common_parallel { } #[test] - fn test_virtio_vsock() { - _test_virtio_vsock(false) - } + #[cfg(not(feature = "mshv"))] // See #7456 + fn test_virtio_balloon_free_page_reporting() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - #[test] - fn test_virtio_vsock_hotplug() { - _test_virtio_vsock(true); - } + //Let's start a 4G guest with balloon occupied 2G memory + let mut child = GuestCommand::new(&guest) + .default_cpus() + .args(["--memory", "size=4G"]) + .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--balloon", "size=0,free_page_reporting=on"]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - #[test] - fn test_api_http_shutdown() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let pid = child.id(); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - _test_api_shutdown(TargetApi::new_http_api(&guest.tmp_dir), guest) - } + // Check the initial RSS is less than 1GiB + let rss = process_rss_kib(pid); + println!("RSS {rss} < 1048576"); + assert!(rss < 1048576); - #[test] - fn test_api_http_delete() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Spawn a command inside the guest to consume 2GiB of RAM for 60 + // seconds + let guest_ip = guest.network.guest_ip0.clone(); + thread::spawn(move || { + ssh_command_ip( + "stress --vm 1 --vm-bytes 2G --vm-keep --timeout 60", + &guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + }); - _test_api_delete(TargetApi::new_http_api(&guest.tmp_dir), guest); - } + // Wait for guest memory consumption to reach the expected level. + assert!(wait_until(Duration::from_secs(60), || process_rss_kib(pid) >= 2097152)); + let rss = process_rss_kib(pid); + println!("RSS {rss} >= 2097152"); + assert!(rss >= 2097152); - #[test] - fn test_api_http_pause_resume() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Wait for stress to complete and free-page reporting to shrink RSS again. + assert!(wait_until(Duration::from_secs(120), || process_rss_kib( + pid + ) < 2097152)); + let rss = process_rss_kib(pid); + println!("RSS {rss} < 2097152"); + assert!(rss < 2097152); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - _test_api_pause_resume(TargetApi::new_http_api(&guest.tmp_dir), guest) + handle_child_output(r, &output); } #[test] - fn test_api_http_create_boot() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - - _test_api_create_boot(TargetApi::new_http_api(&guest.tmp_dir), guest) + #[cfg_attr(target_arch = "aarch64", ignore = "See #8187")] + fn test_pmem_hotplug() { + _test_pmem_hotplug(None); } #[test] - fn test_virtio_iommu() { - _test_virtio_iommu(cfg!(target_arch = "x86_64")) + #[cfg_attr(target_arch = "aarch64", ignore = "See #8187")] + fn test_pmem_multi_segment_hotplug() { + _test_pmem_hotplug(Some(15)); } - #[test] - // We cannot force the software running in the guest to reprogram the BAR - // with some different addresses, but we have a reliable way of testing it - // with a standard Linux kernel. - // By removing a device from the PCI tree, and then rescanning the tree, - // Linux consistently chooses to reorganize the PCI device BARs to other - // locations in the guest address space. - // This test creates a dedicated PCI network device to be checked as being - // properly probed first, then removing it, and adding it again by doing a - // rescan. - fn test_pci_bar_reprogramming() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn _test_pmem_hotplug(pci_segment: Option) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); #[cfg(target_arch = "x86_64")] let kernel_path = direct_kernel_boot_path(); #[cfg(target_arch = "aarch64")] let kernel_path = edk2_path(); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut cmd = GuestCommand::new(&guest); + + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .args([ - "--net", - guest.default_net_string().as_str(), - "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", - ]) - .capture_output() - .spawn() - .unwrap(); + .default_net() + .capture_output(); + + if pci_segment.is_some() { + cmd.args([ + "--platform", + &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), + ]); + } + + let mut child = cmd.spawn().unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // 2 network interfaces + default localhost ==> 3 interfaces + // Check /dev/pmem0 is not there assert_eq!( guest - .ssh_command("ip -o link | wc -l") + .ssh_command("lsblk | grep -c pmem0 || true") .unwrap() .trim() .parse::() - .unwrap_or_default(), - 3 + .unwrap_or(1), + 0 ); - let init_bar_addr = guest - .ssh_command( - "sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource", - ) - .unwrap(); - - // Remove the PCI device - guest - .ssh_command("echo 1 | sudo tee /sys/bus/pci/devices/0000:00:05.0/remove") - .unwrap(); + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-pmem", + Some(&format!( + "file={},id=test0{}", + pmem_temp_file.as_path().to_str().unwrap(), + if let Some(pci_segment) = pci_segment { + format!(",pci_segment={pci_segment}") + } else { + String::new() + } + )), + ); + assert!(cmd_success); + if let Some(pci_segment) = pci_segment { + assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( + "{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" + ))); + } else { + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}") + ); + } - // Only 1 network interface left + default localhost ==> 2 interfaces + // Check that /dev/pmem0 exists and the block size is 128M assert_eq!( guest - .ssh_command("ip -o link | wc -l") + .ssh_command("lsblk | grep pmem0 | grep -c 128M") .unwrap() .trim() .parse::() .unwrap_or_default(), - 2 + 1 ); - // Remove the PCI device - guest - .ssh_command("echo 1 | sudo tee /sys/bus/pci/rescan") - .unwrap(); + guest.reboot_linux(0); - // Back to 2 network interface + default localhost ==> 3 interfaces + // Check still there after reboot assert_eq!( guest - .ssh_command("ip -o link | wc -l") + .ssh_command("lsblk | grep pmem0 | grep -c 128M") .unwrap() .trim() .parse::() .unwrap_or_default(), - 3 + 1 ); - let new_bar_addr = guest - .ssh_command( - "sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource", - ) - .unwrap(); + assert!(remote_command(&api_socket, "remove-device", Some("test0"))); + + // Wait for the pmem device to disappear from lsblk. + assert!(wait_until(Duration::from_secs(20), || { + guest + .ssh_command("lsblk | grep -c pmem0.*128M || true") + .is_ok_and(|output| output.trim().parse::().unwrap_or(1) == 0) + })); - // Let's compare the BAR addresses for our virtio-net device. - // They should be different as we expect the BAR reprogramming - // to have happened. - assert_ne!(init_bar_addr, new_bar_addr); + guest.reboot_linux(1); + + // Check still absent after reboot + assert_eq!( + guest + .ssh_command("lsblk | grep -c pmem0.*128M || true") + .unwrap() + .trim() + .parse::() + .unwrap_or(1), + 0 + ); }); kill_child(&mut child); @@ -4798,171 +5069,160 @@ mod common_parallel { } #[test] - fn test_memory_mergeable_off() { - test_memory_mergeable(false) + fn test_net_hotplug() { + #[cfg(target_arch = "x86_64")] + let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); + let guest = + basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(kernel_path.to_str().unwrap()); + + _test_net_hotplug(&guest, MAX_NUM_PCI_SEGMENTS, None); } #[test] - #[cfg(target_arch = "x86_64")] - fn test_cpu_hotplug() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - + fn test_net_multi_segment_hotplug() { + #[cfg(target_arch = "x86_64")] let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); + let guest = + basic_regular_guest!(JAMMY_IMAGE_NAME).with_kernel_path(kernel_path.to_str().unwrap()); + _test_net_hotplug(&guest, MAX_NUM_PCI_SEGMENTS, Some(15)); + } - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2,max=4"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .args(["--api-socket", &api_socket]) - .capture_output() - .spawn() - .unwrap(); + #[test] + fn test_initramfs() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + #[cfg(target_arch = "x86_64")] + let mut kernels = vec![direct_kernel_boot_path()]; + #[cfg(target_arch = "aarch64")] + let kernels = [direct_kernel_boot_path()]; - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); + #[cfg(target_arch = "x86_64")] + { + let mut pvh_kernel_path = workload_path.clone(); + pvh_kernel_path.push("vmlinux-x86_64"); + kernels.push(pvh_kernel_path); + } - // Resize the VM - let desired_vcpus = 4; - resize_command(&api_socket, Some(desired_vcpus), None, None, None); + let mut initramfs_path = workload_path; + initramfs_path.push("alpine_initramfs.img"); - guest - .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") - .unwrap(); - guest - .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") - .unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - assert_eq!( - guest.get_cpu_count().unwrap_or_default(), - u32::from(desired_vcpus) - ); + let test_string = String::from("axz34i9rylotd8n50wbv6kcj7f2qushme1pg"); + let cmdline = format!("console=hvc0 quiet TEST_STRING={test_string}"); - guest.reboot_linux(0, None); + kernels.iter().for_each(|k_path| { + let mut child = GuestCommand::new(&guest) + .args(["--kernel", k_path.to_str().unwrap()]) + .args(["--initramfs", initramfs_path.to_str().unwrap()]) + .args(["--cmdline", &cmdline]) + .capture_output() + .spawn() + .unwrap(); - assert_eq!( - guest.get_cpu_count().unwrap_or_default(), - u32::from(desired_vcpus) - ); + thread::sleep(std::time::Duration::new(20, 0)); - // Resize the VM - let desired_vcpus = 2; - resize_command(&api_socket, Some(desired_vcpus), None, None, None); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - assert_eq!( - guest.get_cpu_count().unwrap_or_default(), - u32::from(desired_vcpus) - ); + let r = std::panic::catch_unwind(|| { + let s = String::from_utf8_lossy(&output.stdout); - // Resize the VM back up to 4 - let desired_vcpus = 4; - resize_command(&api_socket, Some(desired_vcpus), None, None, None); + assert_ne!(s.lines().position(|line| line == test_string), None); + }); - guest - .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") - .unwrap(); - guest - .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") - .unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - assert_eq!( - guest.get_cpu_count().unwrap_or_default(), - u32::from(desired_vcpus) - ); + handle_child_output(r, &output); }); + } - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); + #[test] + fn test_counters() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_counters(&guest); } #[test] - fn test_memory_hotplug() { - #[cfg(target_arch = "aarch64")] - let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string(); - #[cfg(target_arch = "x86_64")] - let focal_image = FOCAL_IMAGE_NAME.to_string(); - let focal = UbuntuDiskConfig::new(focal_image); - let guest = Guest::new(Box::new(focal)); + #[cfg(feature = "guest_debug")] + fn test_coredump() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let api_socket = temp_api_path(&guest.tmp_dir); - #[cfg(target_arch = "aarch64")] - let kernel_path = edk2_path(); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2,max=4"]) - .args(["--memory", "size=512M,hotplug_size=8192M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + let mut cmd = GuestCommand::new(&guest); + cmd.args(["--cpus", "boot=4"]) + .args(["--memory", "size=1G"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) .default_disks() - .default_net() - .args(["--balloon", "size=0"]) + .args(["--net", guest.default_net_string().as_str()]) .args(["--api-socket", &api_socket]) - .capture_output() - .spawn() - .unwrap(); - - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - - guest.enable_memory_hotplug(); - - // Add RAM to the VM - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + .capture_output(); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + let mut child = cmd.spawn().unwrap(); + let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); - // Use balloon to remove RAM from the VM - let desired_balloon = 512 << 20; - resize_command(&api_socket, None, None, Some(desired_balloon), None); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - assert!(guest.get_total_memory().unwrap_or_default() < 960_000); + assert!(remote_command(&api_socket, "pause", None)); - guest.reboot_linux(0, None); + assert!(remote_command( + &api_socket, + "coredump", + Some(format!("file://{vmcore_file}").as_str()), + )); - assert!(guest.get_total_memory().unwrap_or_default() < 960_000); + // the num of CORE notes should equals to vcpu + let readelf_core_num_cmd = + format!("readelf --all {vmcore_file} |grep CORE |grep -v Type |wc -l"); + let core_num_in_elf = exec_host_command_output(&readelf_core_num_cmd); + assert_eq!(String::from_utf8_lossy(&core_num_in_elf.stdout).trim(), "4"); - // Use balloon add RAM to the VM - let desired_balloon = 0; - resize_command(&api_socket, None, None, Some(desired_balloon), None); + // the num of QEMU notes should equals to vcpu + let readelf_vmm_num_cmd = format!("readelf --all {vmcore_file} |grep QEMU |wc -l"); + let vmm_num_in_elf = exec_host_command_output(&readelf_vmm_num_cmd); + assert_eq!(String::from_utf8_lossy(&vmm_num_in_elf.stdout).trim(), "4"); + }); - thread::sleep(std::time::Duration::new(10, 0)); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + handle_child_output(r, &output); + } - guest.enable_memory_hotplug(); + #[test] + #[cfg(feature = "guest_debug")] + fn test_coredump_no_pause() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); - // Add RAM to the VM - let desired_ram = 2048 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + let mut cmd = GuestCommand::new(&guest); + cmd.args(["--cpus", "boot=4"]) + .args(["--memory", "size=1G"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .default_disks() + .args(["--net", guest.default_net_string().as_str()]) + .args(["--api-socket", &api_socket]) + .capture_output(); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 1_920_000); + let mut child = cmd.spawn().unwrap(); + let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); - // Remove RAM to the VM (only applies after reboot) - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - guest.reboot_linux(1, None); + assert!(remote_command( + &api_socket, + "coredump", + Some(format!("file://{vmcore_file}").as_str()), + )); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); - assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); + assert_eq!(vm_state(&api_socket), "Running"); }); kill_child(&mut child); @@ -4972,198 +5232,257 @@ mod common_parallel { } #[test] - #[cfg(not(feature = "mshv"))] - fn test_virtio_mem() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + fn test_pvpanic() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_pvpanic(&guest); + } - let kernel_path = direct_kernel_boot_path(); + #[test] + fn test_tap_from_fd() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_tap_from_fd(&guest); + } - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2,max=4"]) - .args([ - "--memory", - "size=512M,hotplug_method=virtio-mem,hotplug_size=8192M", - ]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .args(["--api-socket", &api_socket]) - .capture_output() - .spawn() - .unwrap(); + #[test] + #[cfg_attr(target_arch = "aarch64", ignore = "See #5443")] + fn test_macvtap() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_macvtap(&guest, false, "guestmacvtap0", "hostmacvtap0"); + } - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + #[test] + #[cfg_attr(target_arch = "aarch64", ignore = "See #5443")] + fn test_macvtap_hotplug() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_macvtap(&guest, true, "guestmacvtap1", "hostmacvtap1"); + } - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_ovs_dpdk() { + let disk_config1 = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest1 = Guest::new(Box::new(disk_config1)); - guest.enable_memory_hotplug(); + let disk_config2 = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest2 = Guest::new(Box::new(disk_config2)); + let api_socket_source = format!("{}.1", temp_api_path(&guest2.tmp_dir)); - // Add RAM to the VM - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + let (mut child1, mut child2) = + setup_ovs_dpdk_guests(&guest1, &guest2, &api_socket_source, false); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + // Create the snapshot directory + let snapshot_dir = temp_snapshot_dir_path(&guest2.tmp_dir); - // Add RAM to the VM - let desired_ram = 2048 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + let r = std::panic::catch_unwind(|| { + // Remove one of the two ports from the OVS bridge + assert!(exec_host_command_status("ovs-vsctl del-port vhost-user1").success()); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 1_920_000); + // Spawn a new netcat listener in the first VM + let guest_ip = guest1.network.guest_ip0.clone(); + thread::spawn(move || { + ssh_command_ip( + "nc -l 12345", + &guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + }); - // Remove RAM from the VM - let desired_ram = 1024 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); + guest1 + .wait_for_ssh_command( + "ss -ltnH | awk '{print $4}' | grep -q ':12345$'", + Duration::from_secs(20), + ) + .unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); - assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); + // Check the connection fails this time + guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap_err(); - guest.reboot_linux(0, None); + // Add the OVS port back + assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success()); - // Check the amount of memory after reboot is 1GiB - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); - assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); + // And finally check the connection is functional again + guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); - // Check we can still resize to 512MiB - let desired_ram = 512 << 20; - resize_command(&api_socket, None, Some(desired_ram), None, None); - thread::sleep(std::time::Duration::new(10, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - assert!(guest.get_total_memory().unwrap_or_default() < 960_000); - }); + // Pause the VM + assert!(remote_command(&api_socket_source, "pause", None)); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // Take a snapshot from the VM + assert!(remote_command( + &api_socket_source, + "snapshot", + Some(format!("file://{snapshot_dir}").as_str()), + )); - handle_child_output(r, &output); - } + // Wait for the source VM snapshot artifacts to be ready. + assert!(wait_until(Duration::from_secs(10), || { + std::path::Path::new(&snapshot_dir).exists() + })); + }); - #[test] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - // Test both vCPU and memory resizing together - fn test_resize() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + // Shutdown the source VM + kill_child(&mut child2); + let output = child2.wait_with_output().unwrap(); + handle_child_output(r, &output); - let kernel_path = direct_kernel_boot_path(); + // Remove the vhost-user socket file. + Command::new("rm") + .arg("-f") + .arg("/tmp/dpdkvhostclient2") + .output() + .unwrap(); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2,max=4"]) - .args(["--memory", "size=512M,hotplug_size=8192M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .args(["--api-socket", &api_socket]) + let api_socket_restored = format!("{}.2", temp_api_path(&guest2.tmp_dir)); + // Restore the VM from the snapshot + let mut child2 = GuestCommand::new(&guest2) + .args(["--api-socket", &api_socket_restored]) + .args([ + "--restore", + format!("source_url=file://{snapshot_dir}").as_str(), + ]) .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - - guest.enable_memory_hotplug(); + // Wait for the restored VM to accept SSH again after resume. - // Resize the VM - let desired_vcpus = 4; - let desired_ram = 1024 << 20; - resize_command( - &api_socket, - Some(desired_vcpus), - Some(desired_ram), - None, - None, - ); + let r = std::panic::catch_unwind(|| { + // Resume the VM + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); + assert!(remote_command(&api_socket_restored, "resume", None)); + guest2.wait_for_ssh(Duration::from_secs(30)).unwrap(); - guest - .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") + // Spawn a new netcat listener in the first VM + let guest_ip = guest1.network.guest_ip0.clone(); + thread::spawn(move || { + ssh_command_ip( + "nc -l 12345", + &guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) .unwrap(); - guest - .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") + }); + + guest1 + .wait_for_ssh_command( + "ss -ltnH | awk '{print $4}' | grep -q ':12345$'", + Duration::from_secs(20), + ) .unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - assert_eq!( - guest.get_cpu_count().unwrap_or_default(), - u32::from(desired_vcpus) - ); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + // And check the connection is still functional after restore + guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + kill_child(&mut child1); + kill_child(&mut child2); + + let output = child1.wait_with_output().unwrap(); + let output2 = child2.wait_with_output().unwrap(); + + cleanup_ovs_dpdk(); + + if r.is_err() { + eprintln!( + "\n\n==== Start restored VM stdout ====\n\n{}\n\n==== End restored VM stdout ====", + String::from_utf8_lossy(&output2.stdout) + ); + eprintln!( + "\n\n==== Start restored VM stderr ====\n\n{}\n\n==== End restored VM stderr ====", + String::from_utf8_lossy(&output2.stderr) + ); + } handle_child_output(r, &output); } - #[test] - fn test_memory_overhead() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - - let kernel_path = direct_kernel_boot_path(); + fn setup_spdk_nvme(nvme_dir: &std::path::Path) -> Child { + cleanup_spdk_nvme(); - let guest_memory_size_kb = 512 * 1024; + assert!( + exec_host_command_status(&format!( + "mkdir -p {}", + nvme_dir.join("nvme-vfio-user").to_str().unwrap() + )) + .success() + ); + assert!( + exec_host_command_status(&format!( + "truncate {} -s 128M", + nvme_dir.join("test-disk.raw").to_str().unwrap() + )) + .success() + ); + assert!( + exec_host_command_status(&format!( + "mkfs.ext4 {}", + nvme_dir.join("test-disk.raw").to_str().unwrap() + )) + .success() + ); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", format!("size={guest_memory_size_kb}K").as_str()]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_net() - .default_disks() - .capture_output() + // Start the SPDK nvmf_tgt daemon to present NVMe device as a VFIO user device + let child = Command::new("/usr/local/bin/spdk-nvme/nvmf_tgt") + .args(["-i", "0", "-m", "0x1"]) .spawn() .unwrap(); + thread::sleep(std::time::Duration::new(2, 0)); - guest.wait_vm_boot(None).unwrap(); - - let r = std::panic::catch_unwind(|| { - let overhead = get_vmm_overhead(child.id(), guest_memory_size_kb); - eprintln!("Guest memory overhead: {overhead} vs {MAXIMUM_VMM_OVERHEAD_KB}"); - assert!(overhead <= MAXIMUM_VMM_OVERHEAD_KB); - }); + assert!(exec_host_command_with_retries( + "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_transport -t VFIOUSER", + 3, + std::time::Duration::new(5, 0), + )); + assert!( + exec_host_command_status(&format!( + "/usr/local/bin/spdk-nvme/rpc.py bdev_aio_create {} test 512", + nvme_dir.join("test-disk.raw").to_str().unwrap() + )) + .success() + ); + assert!(exec_host_command_status( + "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_subsystem nqn.2019-07.io.spdk:cnode -a -s test" + ) + .success()); + assert!(exec_host_command_status( + "/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_ns nqn.2019-07.io.spdk:cnode test" + ) + .success()); + assert!(exec_host_command_status(&format!( + "/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_listener nqn.2019-07.io.spdk:cnode -t VFIOUSER -a {} -s 0", + nvme_dir.join("nvme-vfio-user").to_str().unwrap() + )) + .success()); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + child + } - handle_child_output(r, &output); + fn cleanup_spdk_nvme() { + exec_host_command_status("pkill -f nvmf_tgt"); } #[test] - #[cfg(target_arch = "x86_64")] - // This test runs a guest with Landlock enabled and hotplugs a new disk. As - // the path for the hotplug disk is not pre-added to Landlock rules, this - // the test will result in a failure. - fn test_landlock() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_vfio_user() { + let jammy_image = JAMMY_IMAGE_NAME.to_string(); + let disk_config = UbuntuDiskConfig::new(jammy_image); + let guest = Guest::new(Box::new(disk_config)); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = edk2_path(); + let spdk_nvme_dir = guest.tmp_dir.as_path().join("test-vfio-user"); + let mut spdk_child = setup_spdk_nvme(spdk_nvme_dir.as_path()); let api_socket = temp_api_path(&guest.tmp_dir); - let mut child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--landlock"]) + .default_cpus() + .args(["--memory", "size=1G,shared=on,hugepages=on"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--serial", "tty", "--console", "off"]) .default_disks() .default_net() .capture_output() @@ -5171,706 +5490,552 @@ mod common_parallel { .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Check /dev/vdc is not there - assert_eq!( - guest - .ssh_command("lsblk | grep -c vdc.*16M || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 + // Hotplug the SPDK-NVMe device to the VM + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-user-device", + Some(&format!( + "socket={},id=vfio_user0", + spdk_nvme_dir + .as_path() + .join("nvme-vfio-user/cntrl") + .to_str() + .unwrap(), + )), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"vfio_user0\",\"bdf\":\"0000:00:05.0\"}") ); - // Now let's add the extra disk. - let mut blk_file_path = dirs::home_dir().unwrap(); - blk_file_path.push("workloads"); - blk_file_path.push("blk.img"); - // As the path to the hotplug disk is not pre-added, this remote - // command will fail. - assert!(!remote_command( - &api_socket, - "add-disk", - Some( - format!( - "path={},id=test0,readonly=true", - blk_file_path.to_str().unwrap() - ) - .as_str() - ), - )); + // Check both if /dev/nvme exists and if the block size is 128M. + assert!(wait_until(Duration::from_secs(10), || { + guest + .ssh_command("lsblk | grep nvme0n1 | grep -c 128M") + .ok() + .and_then(|output| output.trim().parse::().ok()) + == Some(1) + })); + + // Check changes persist after reboot + assert_eq!( + guest.ssh_command("sudo mount /dev/nvme0n1 /mnt").unwrap(), + "" + ); + assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n"); + guest + .ssh_command("echo test123 | sudo tee /mnt/test") + .unwrap(); + assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), ""); + assert_eq!(guest.ssh_command("ls /mnt").unwrap(), ""); + + guest.reboot_linux(0); + assert_eq!( + guest.ssh_command("sudo mount /dev/nvme0n1 /mnt").unwrap(), + "" + ); + assert_eq!( + guest.ssh_command("sudo cat /mnt/test").unwrap().trim(), + "test123" + ); }); - let _ = child.kill(); + let _ = spdk_child.kill(); + let _ = spdk_child.wait(); + + kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } - fn _test_disk_hotplug(landlock_enabled: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + #[test] + #[cfg(target_arch = "x86_64")] + fn test_vdpa_block() { + // Before trying to run the test, verify the vdpa_sim_blk module is correctly loaded. + assert!(exec_host_command_status("lsmod | grep vdpa_sim_blk").success()); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = edk2_path(); + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_vdpa_block(&guest); + } - let api_socket = temp_api_path(&guest.tmp_dir); + #[test] + #[cfg(target_arch = "x86_64")] + fn test_vdpa_net() { + // Before trying to run the test, verify the vdpa_sim_net module is correctly loaded. + if !exec_host_command_status("lsmod | grep vdpa_sim_net").success() { + return; + } - let mut blk_file_path = dirs::home_dir().unwrap(); - blk_file_path.push("workloads"); - blk_file_path.push("blk.img"); + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - let mut cmd = GuestCommand::new(&guest); - if landlock_enabled { - cmd.args(["--landlock"]).args([ - "--landlock-rules", - format!("path={blk_file_path:?},access=rw").as_str(), - ]); - } + let kernel_path = direct_kernel_boot_path(); - cmd.args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=512M,hugepages=on"]) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() .default_net() - .capture_output(); - - let mut child = cmd.spawn().unwrap(); + .args(["--vdpa", "path=/dev/vhost-vdpa-2,num_queues=3"]) + .capture_output() + .spawn() + .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Check /dev/vdc is not there - assert_eq!( - guest - .ssh_command("lsblk | grep -c vdc.*16M || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 - ); - - // Now let's add the extra disk. - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some( - format!( - "path={},id=test0,readonly=true", - blk_file_path.to_str().unwrap() - ) - .as_str(), - ), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); - - thread::sleep(std::time::Duration::new(10, 0)); + guest.wait_vm_boot().unwrap(); - // Check that /dev/vdc exists and the block size is 16M. + // Check we can find network interface related to vDPA device assert_eq!( guest - .ssh_command("lsblk | grep vdc | grep -c 16M") + .ssh_command("ip -o link | grep -c ens6") .unwrap() .trim() .parse::() - .unwrap_or_default(), + .unwrap_or(0), 1 ); - // And check the block device can be read. + + guest + .ssh_command("sudo ip link set dev ens6 address 00:e8:ca:33:ba:06") + .unwrap(); guest - .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16") + .ssh_command("sudo ip addr add 172.16.1.2/24 dev ens6") + .unwrap(); + // Disable IPv6 on the interface before bringing it up to avoid + // IPv6 link-local autoconfiguration emitting NDP/RS packets which + // would invalidate the "zero packets" precondition checked below + // (some guest kernels emit these before our stats query races in). + // Use `sysctl -e` so the command is a no-op (rather than an error) + // on kernels built without IPv6, where these keys do not exist. + guest + .ssh_command( + "sudo sysctl -e -w net.ipv6.conf.ens6.disable_ipv6=1 \ + net.ipv6.conf.ens6.accept_ra=0 \ + net.ipv6.conf.ens6.autoconf=0", + ) .unwrap(); + guest.ssh_command("sudo ip link set up dev ens6").unwrap(); - // Let's remove it the extra disk. - assert!(remote_command(&api_socket, "remove-device", Some("test0"))); - thread::sleep(std::time::Duration::new(5, 0)); - // And check /dev/vdc is not there + // Check there is no packet yet on both TX/RX of the network interface assert_eq!( guest - .ssh_command("lsblk | grep -c vdc.*16M || true") + .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 0'") .unwrap() .trim() .parse::() - .unwrap_or(1), - 0 - ); - - // And add it back to validate unplug did work correctly. - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some( - format!( - "path={},id=test0,readonly=true", - blk_file_path.to_str().unwrap() - ) - .as_str(), - ), + .unwrap_or(0), + 2 ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); - thread::sleep(std::time::Duration::new(10, 0)); + // Send 6 packets with ping command + guest.ssh_command("ping 172.16.1.10 -c 6 || true").unwrap(); - // Check that /dev/vdc exists and the block size is 16M. + // Check we can find 6 packets on both TX/RX of the network interface assert_eq!( guest - .ssh_command("lsblk | grep vdc | grep -c 16M") + .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 6'") .unwrap() .trim() .parse::() - .unwrap_or_default(), - 1 + .unwrap_or(0), + 2 ); - // And check the block device can be read. - guest - .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16") - .unwrap(); - // Reboot the VM. - guest.reboot_linux(0, None); + // No need to check for hotplug as we already tested it through + // test_vdpa_block() + }); - // Check still there after reboot - assert_eq!( - guest - .ssh_command("lsblk | grep vdc | grep -c 16M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - assert!(remote_command(&api_socket, "remove-device", Some("test0"))); + handle_child_output(r, &output); + } - thread::sleep(std::time::Duration::new(20, 0)); + #[test] + #[cfg(not(feature = "mshv"))] // See issue #7439 + #[cfg(target_arch = "x86_64")] + fn test_tpm() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - // Check device has gone away - assert_eq!( - guest - .ssh_command("lsblk | grep -c vdc.*16M || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 - ); + let (mut swtpm_command, swtpm_socket_path) = prepare_swtpm_daemon(&guest.tmp_dir); - guest.reboot_linux(1, None); + let mut guest_cmd = GuestCommand::new(&guest); + guest_cmd + .default_cpus() + .args(["--memory", "size=1G"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--tpm", &format!("socket={swtpm_socket_path}")]) + .capture_output() + .default_disks() + .default_net(); - // Check device still absent + // Start swtpm daemon + let mut swtpm_child = swtpm_command.spawn().unwrap(); + assert!(wait_until(Duration::from_secs(10), || { + std::path::Path::new(&swtpm_socket_path).exists() + })); + let mut child = guest_cmd.spawn().unwrap(); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); assert_eq!( - guest - .ssh_command("lsblk | grep -c vdc.*16M || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 + guest.ssh_command("ls /dev/tpm0").unwrap().trim(), + "/dev/tpm0" ); + guest.ssh_command("sudo tpm2_selftest -f").unwrap(); + guest + .ssh_command("echo 'hello' > /tmp/checksum_test; ") + .unwrap(); + guest.ssh_command("cmp <(sudo tpm2_pcrevent /tmp/checksum_test | grep sha256 | awk '{print $2}') <(sha256sum /tmp/checksum_test| awk '{print $1}')").unwrap(); }); + let _ = swtpm_child.kill(); + let _d_out = swtpm_child.wait_with_output().unwrap(); + kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } - #[test] - fn test_disk_hotplug() { - _test_disk_hotplug(false) - } - #[test] #[cfg(target_arch = "x86_64")] - fn test_disk_hotplug_with_landlock() { - _test_disk_hotplug(true) - } + fn test_double_tty() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut cmd = GuestCommand::new(&guest); + let api_socket = temp_api_path(&guest.tmp_dir); + let tty_str: &str = "console=hvc0 earlyprintk=ttyS0 "; + // linux printk module enable console log. + let con_dis_str: &str = "console [hvc0] enabled"; + // linux printk module disable console log. + let con_enb_str: &str = "bootconsole [earlyser0] disabled"; - fn create_loop_device(backing_file_path: &str, block_size: u32, num_retries: usize) -> String { - const LOOP_CONFIGURE: u64 = 0x4c0a; - const LOOP_CTL_GET_FREE: u64 = 0x4c82; - const LOOP_CTL_PATH: &str = "/dev/loop-control"; - const LOOP_DEVICE_PREFIX: &str = "/dev/loop"; + let kernel_path = direct_kernel_boot_path(); - #[repr(C)] - struct LoopInfo64 { - lo_device: u64, - lo_inode: u64, - lo_rdevice: u64, - lo_offset: u64, - lo_sizelimit: u64, - lo_number: u32, - lo_encrypt_type: u32, - lo_encrypt_key_size: u32, - lo_flags: u32, - lo_file_name: [u8; 64], - lo_crypt_name: [u8; 64], - lo_encrypt_key: [u8; 32], - lo_init: [u64; 2], - } + cmd.default_cpus() + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args([ + "--cmdline", + DIRECT_KERNEL_BOOT_CMDLINE + .replace("console=hvc0", tty_str) + .as_str(), + ]) + .capture_output() + .default_disks() + .default_net() + .args(["--serial", "tty"]) + .args(["--console", "tty"]) + .args(["--api-socket", &api_socket]); - impl Default for LoopInfo64 { - fn default() -> Self { - LoopInfo64 { - lo_device: 0, - lo_inode: 0, - lo_rdevice: 0, - lo_offset: 0, - lo_sizelimit: 0, - lo_number: 0, - lo_encrypt_type: 0, - lo_encrypt_key_size: 0, - lo_flags: 0, - lo_file_name: [0; 64], - lo_crypt_name: [0; 64], - lo_encrypt_key: [0; 32], - lo_init: [0; 2], - } - } - } + let mut child = cmd.spawn().unwrap(); - #[derive(Default)] - #[repr(C)] - struct LoopConfig { - fd: u32, - block_size: u32, - info: LoopInfo64, - _reserved: [u64; 8], + let mut r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + if r.is_ok() { + r = std::panic::catch_unwind(|| { + let s = String::from_utf8_lossy(&output.stdout); + assert!(s.contains(tty_str)); + assert!(s.contains(con_dis_str)); + assert!(s.contains(con_enb_str)); + }); } - // Open loop-control device - let loop_ctl_file = OpenOptions::new() - .read(true) - .write(true) - .open(LOOP_CTL_PATH) - .unwrap(); + handle_child_output(r, &output); + } - // Request a free loop device - let loop_device_number = - unsafe { libc::ioctl(loop_ctl_file.as_raw_fd(), LOOP_CTL_GET_FREE as _) }; + #[test] + #[cfg(target_arch = "x86_64")] + fn test_nmi() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + let event_path = temp_event_monitor_path(&guest.tmp_dir); - if loop_device_number < 0 { - panic!("Couldn't find a free loop device"); - } + let kernel_path = direct_kernel_boot_path(); + let cmd_line = format!("{} {}", DIRECT_KERNEL_BOOT_CMDLINE, "unknown_nmi_panic=1"); - // Create loop device path - let loop_device_path = format!("{LOOP_DEVICE_PREFIX}{loop_device_number}"); + let mut cmd = GuestCommand::new(&guest); + cmd.args(["--cpus", "boot=4"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", cmd_line.as_str()]) + .default_disks() + .args(["--net", guest.default_net_string().as_str()]) + .args(["--pvpanic"]) + .args(["--api-socket", &api_socket]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .capture_output(); - // Open loop device - let loop_device_file = OpenOptions::new() - .read(true) - .write(true) - .open(&loop_device_path) - .unwrap(); + let mut child = cmd.spawn().unwrap(); - // Open backing file - let backing_file = OpenOptions::new() - .read(true) - .write(true) - .open(backing_file_path) - .unwrap(); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - let loop_config = LoopConfig { - fd: backing_file.as_raw_fd() as u32, - block_size, - ..Default::default() - }; + assert!(remote_command(&api_socket, "nmi", None)); - for i in 0..num_retries { - let ret = unsafe { - libc::ioctl( - loop_device_file.as_raw_fd(), - LOOP_CONFIGURE as _, - &loop_config, - ) - }; - if ret != 0 { - if i < num_retries - 1 { - println!( - "Iteration {}: Failed to configure the loop device {}: {}", - i, - loop_device_path, - std::io::Error::last_os_error() - ); - } else { - panic!( - "Failed {} times trying to configure the loop device {}: {}", - num_retries, - loop_device_path, - std::io::Error::last_os_error() - ); - } - } else { - break; - } + let expected_sequential_events = [&MetaEvent { + event: "panic".to_string(), + device_id: None, + }]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(3), + &expected_sequential_events, + &event_path + )); + }); - // Wait for a bit before retrying - thread::sleep(std::time::Duration::new(5, 0)); - } + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - loop_device_path + handle_child_output(r, &output); } + // Checks that explicit PCI device IDs are honored for boot-time and hotplugged devices. + // It also verifies dynamic hotplug allocation reuses freed PCI device ID holes. #[test] - fn test_virtio_block_topology() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + fn test_pci_device_id() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + #[cfg(target_arch = "x86_64")] let kernel_path = direct_kernel_boot_path(); - let test_disk_path = guest.tmp_dir.as_path().join("test.img"); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); - let output = exec_host_command_output( - format!( - "qemu-img create -f raw {} 16M", - test_disk_path.to_str().unwrap() - ) - .as_str(), - ); - if !output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - panic!("qemu-img command failed\nstdout\n{stdout}\nstderr\n{stderr}"); - } + let api_socket = temp_api_path(&guest.tmp_dir); - let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5); + // Boot without network + let mut cmd = GuestCommand::new(&guest); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - format!( - "path={}", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ) - .as_str(), - format!("path={}", &loop_dev).as_str(), - ]) + .args(["--console", "tty,pci_device_id=7"]) .default_net() - .capture_output() - .spawn() - .unwrap(); + .default_disks() + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + guest.wait_vm_boot().unwrap(); + // Add a network device with non-static device id request let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Make sure an explicit BDF for virtio-console is set. + assert!(wait_until(Duration::from_secs(10), || { + ssh_command_ip_with_auth( + "lspci | grep \"00:07.0\" | grep Virtio | grep console", + &default_guest_auth(), + &guest.network.guest_ip0, + Some(Duration::from_secs(1)), + ) + .is_ok() + })); - // MIN-IO column - assert_eq!( - guest - .ssh_command("lsblk -t| grep vdc | awk '{print $3}'") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 4096 + let (cmd_success, cmd_stdout, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test0,tap=,mac={},ip={},mask=255.255.255.128", + guest.network.guest_mac1, guest.network.host_ip1, + ) + .as_str(), + ), ); - // PHY-SEC column - assert_eq!( - guest - .ssh_command("lsblk -t| grep vdc | awk '{print $5}'") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 4096 + assert!(cmd_success); + // We now know the first free device ID on the bus + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, first_free_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_ne!(first_free_device_id, 0); + + // Wait for the hotplugged device to appear in the guest + assert!(wait_until(Duration::from_secs(10), || { + ssh_command_ip_with_auth( + &format!("lspci -n | grep \"00:{first_free_device_id:02x}.0\""), + &default_guest_auth(), + &guest.network.guest_ip0, + Some(Duration::from_secs(1)), + ) + .is_ok() + })); + // Calculate the succeeding device ID + let device_id_to_allocate = first_free_device_id + 1; + // We expect the succeeding device ID to be free. + assert!(wait_until(Duration::from_secs(10), || { + matches!( + ssh_command_ip_with_auth( + &format!("lspci -n | grep \"00:{device_id_to_allocate:02x}.0\""), + &default_guest_auth(), + &guest.network.guest_ip0, + Some(Duration::from_secs(5)), + ), + Err(SshCommandError::NonZeroExitStatus(1)) + ) + })); + + // Add a device to the next device slot explicitly + let (cmd_success, cmd_stdout, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test1337,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id={}", + guest.network.guest_mac1, guest.network.host_ip1, device_id_to_allocate, + ) + .as_str(), + ), ); - // LOG-SEC column - assert_eq!( - guest - .ssh_command("lsblk -t| grep vdc | awk '{print $6}'") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 4096 + assert!(cmd_success); + // Retrieve what BDF we actually reserved and assert it's equal to that we wanted to reserve + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, allocated_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_eq!(device_id_to_allocate, allocated_device_id); + // Wait for the hotplugged device to appear in the guest + assert!(wait_until(Duration::from_secs(10), || { + ssh_command_ip_with_auth( + &format!("lspci -n | grep \"00:{allocated_device_id:02x}.0\""), + &default_guest_auth(), + &guest.network.guest_ip0, + Some(Duration::from_secs(1)), + ) + .is_ok() + })); + // Remove the first device to create a hole + let cmd_success = remote_command(&api_socket, "remove-device", Some("test0")); + assert!(cmd_success); + // Wait for the device to disappear from the guest + assert!(wait_until(Duration::from_secs(10), || { + matches!( + ssh_command_ip_with_auth( + &format!("lspci -n | grep \"00:{first_free_device_id:02x}.0\""), + &default_guest_auth(), + &guest.network.guest_ip0, + Some(Duration::from_secs(1)), + ), + Err(SshCommandError::NonZeroExitStatus(1)) + ) + })); + // Reuse the device ID hole by dynamically coalescing with the first free ID + let (cmd_success, cmd_stdout, _) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test0,tap=,mac={},ip={},mask=255.255.255.128", + guest.network.guest_mac1, guest.network.host_ip1, + ) + .as_str(), + ), ); + assert!(cmd_success); + // Check that CHV reports that we added the same device to the same ID + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, allocated_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_eq!(first_free_device_id, allocated_device_id); + + // Wait for the re-added device to appear in the guest + assert!(wait_until(Duration::from_secs(10), || { + ssh_command_ip_with_auth( + &format!("lspci -n | grep \"00:{allocated_device_id:02x}.0\""), + &default_guest_auth(), + &guest.network.guest_ip0, + Some(Duration::from_secs(1)), + ) + .is_ok() + })); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); - - Command::new("losetup") - .args(["-d", &loop_dev]) - .output() - .expect("loop device not found"); } #[test] - fn test_virtio_balloon_deflate_on_oom() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Test that adding a duplicate PCI device ID fails + fn test_duplicate_pci_device_id() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + #[cfg(target_arch = "x86_64")] let kernel_path = direct_kernel_boot_path(); + #[cfg(target_arch = "aarch64")] + let kernel_path = edk2_path(); let api_socket = temp_api_path(&guest.tmp_dir); - //Let's start a 4G guest with balloon occupied 2G memory - let mut child = GuestCommand::new(&guest) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=4G"]) + // Boot without network + let mut cmd = GuestCommand::new(&guest); + + cmd.args(["--api-socket", &api_socket]) + .default_cpus() + .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--balloon", "size=2G,deflate_on_oom=on"]) - .default_disks() .default_net() - .capture_output() - .spawn() - .unwrap(); - - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + .default_disks() + .capture_output(); - // Wait for balloon memory's initialization and check its size. - // The virtio-balloon driver might take a few seconds to report the - // balloon effective size back to the VMM. - thread::sleep(std::time::Duration::new(20, 0)); + let mut child = cmd.spawn().unwrap(); - let orig_balloon = balloon_size(&api_socket); - println!("The original balloon memory size is {orig_balloon} bytes"); - assert!(orig_balloon == 2147483648); - - // Two steps to verify if the 'deflate_on_oom' parameter works. - // 1st: run a command to trigger an OOM in the guest. - guest - .ssh_command("echo f | sudo tee /proc/sysrq-trigger") - .unwrap(); - - // Give some time for the OOM to happen in the guest and be reported - // back to the host. - thread::sleep(std::time::Duration::new(20, 0)); - - // 2nd: check balloon_mem's value to verify balloon has been automatically deflated - let deflated_balloon = balloon_size(&api_socket); - println!("After deflating, balloon memory size is {deflated_balloon} bytes"); - // Verify the balloon size deflated - assert!(deflated_balloon < 2147483648); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } - - #[test] - #[cfg(not(feature = "mshv"))] - fn test_virtio_balloon_free_page_reporting() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - - //Let's start a 4G guest with balloon occupied 2G memory - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .args(["--balloon", "size=0,free_page_reporting=on"]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); - - let pid = child.id(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Check the initial RSS is less than 1GiB - let rss = process_rss_kib(pid); - println!("RSS {rss} < 1048576"); - assert!(rss < 1048576); - - // Spawn a command inside the guest to consume 2GiB of RAM for 60 - // seconds - let guest_ip = guest.network.guest_ip.clone(); - thread::spawn(move || { - ssh_command_ip( - "stress --vm 1 --vm-bytes 2G --vm-keep --timeout 60", - &guest_ip, - DEFAULT_SSH_RETRIES, - DEFAULT_SSH_TIMEOUT, - ) - .unwrap(); - }); - - // Wait for 50 seconds to make sure the stress command is consuming - // the expected amount of memory. - thread::sleep(std::time::Duration::new(50, 0)); - let rss = process_rss_kib(pid); - println!("RSS {rss} >= 2097152"); - assert!(rss >= 2097152); - - // Wait for an extra minute to make sure the stress command has - // completed and that the guest reported the free pages to the VMM - // through the virtio-balloon device. We expect the RSS to be under - // 2GiB. - thread::sleep(std::time::Duration::new(60, 0)); - let rss = process_rss_kib(pid); - println!("RSS {rss} < 2097152"); - assert!(rss < 2097152); - }); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } - - #[test] - fn test_pmem_hotplug() { - _test_pmem_hotplug(None) - } - - #[test] - fn test_pmem_multi_segment_hotplug() { - _test_pmem_hotplug(Some(15)) - } - - fn _test_pmem_hotplug(pci_segment: Option) { - #[cfg(target_arch = "aarch64")] - let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string(); - #[cfg(target_arch = "x86_64")] - let focal_image = FOCAL_IMAGE_NAME.to_string(); - let focal = UbuntuDiskConfig::new(focal_image); - let guest = Guest::new(Box::new(focal)); - - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = edk2_path(); - - let api_socket = temp_api_path(&guest.tmp_dir); - - let mut cmd = GuestCommand::new(&guest); - - cmd.args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .capture_output(); - - if pci_segment.is_some() { - cmd.args([ - "--platform", - &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), - ]); - } - - let mut child = cmd.spawn().unwrap(); + guest.wait_vm_boot().unwrap(); + // Add a network device with non-static device ID request let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Check /dev/pmem0 is not there - assert_eq!( - guest - .ssh_command("lsblk | grep -c pmem0 || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 - ); - - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - let (cmd_success, cmd_output) = remote_command_w_output( + let (cmd_success, cmd_stdout, _) = remote_command_w_output( &api_socket, - "add-pmem", - Some(&format!( - "file={},id=test0{}", - pmem_temp_file.as_path().to_str().unwrap(), - if let Some(pci_segment) = pci_segment { - format!(",pci_segment={pci_segment}") - } else { - "".to_owned() - } - )), + "add-net", + Some( + format!( + "id=test0,tap=,mac={},ip={},mask=255.255.255.128", + guest.network.guest_mac1, guest.network.host_ip1, + ) + .as_str(), + ), ); assert!(cmd_success); - if let Some(pci_segment) = pci_segment { - assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( - "{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" - ))); - } else { - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); - } - - // Check that /dev/pmem0 exists and the block size is 128M - assert_eq!( - guest - .ssh_command("lsblk | grep pmem0 | grep -c 128M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); - - guest.reboot_linux(0, None); - - // Check still there after reboot - assert_eq!( - guest - .ssh_command("lsblk | grep pmem0 | grep -c 128M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); - - assert!(remote_command(&api_socket, "remove-device", Some("test0"))); - thread::sleep(std::time::Duration::new(20, 0)); + // We now know the first free device ID on the bus + let output = String::from_utf8(cmd_stdout).expect("should work"); + let (_, _, first_free_device_id, _) = bdf_from_hotplug_response(output.as_str()); + assert_ne!(first_free_device_id, 0); - // Check device has gone away - assert_eq!( - guest - .ssh_command("lsblk | grep -c pmem0.*128M || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 + let (cmd_success, _, cmd_stderr) = remote_command_w_output( + &api_socket, + "add-net", + Some( + format!( + "id=test1337,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id={first_free_device_id}", + guest.network.guest_mac1, guest.network.host_ip1, + ) + .as_str(), + ), ); - - guest.reboot_linux(1, None); - - // Check still absent after reboot - assert_eq!( - guest - .ssh_command("lsblk | grep -c pmem0.*128M || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 + // Check for fail; Allocating the same device ID for two devices is disallowed + assert!(!cmd_success); + // Check that the error message contains the expected error + let std_err_str = String::from_utf8(cmd_stderr).unwrap(); + assert!( + std_err_str.contains(&format!( + "Valid PCI device identifier but already used: {first_free_device_id}" + )), + "Command return was: {std_err_str}" ); }); @@ -5881,18 +6046,10 @@ mod common_parallel { } #[test] - fn test_net_hotplug() { - _test_net_hotplug(None) - } - - #[test] - fn test_net_multi_segment_hotplug() { - _test_net_hotplug(Some(15)) - } - - fn _test_net_hotplug(pci_segment: Option) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Test that requesting an invalid device ID fails. + fn test_invalid_pci_device_id() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); #[cfg(target_arch = "x86_64")] let kernel_path = direct_kernel_boot_path(); @@ -5905,917 +6062,1084 @@ mod common_parallel { let mut cmd = GuestCommand::new(&guest); cmd.args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + .default_cpus() + .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_net() .default_disks() .capture_output(); - if pci_segment.is_some() { - cmd.args([ - "--platform", - &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), - ]); - } - let mut child = cmd.spawn().unwrap(); - thread::sleep(std::time::Duration::new(20, 0)); + guest.wait_vm_boot().unwrap(); let r = std::panic::catch_unwind(|| { - // Add network - let (cmd_success, cmd_output) = remote_command_w_output( + // Invalid API call because the PCI device ID is out of range + let (cmd_success, _, cmd_stderr) = remote_command_w_output( &api_socket, "add-net", Some( format!( - "{}{},id=test0", - guest.default_net_string(), - if let Some(pci_segment) = pci_segment { - format!(",pci_segment={pci_segment}") - } else { - "".to_owned() - } + "id=test0,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id=188", + guest.network.guest_mac1, guest.network.host_ip1, ) .as_str(), ), ); - assert!(cmd_success); - - if let Some(pci_segment) = pci_segment { - assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( - "{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" - ))); - } else { - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:05.0\"}")); - } - - thread::sleep(std::time::Duration::new(5, 0)); - - // 1 network interfaces + default localhost ==> 2 interfaces - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 + // Check for fail + assert!(!cmd_success); + // Check that the error message contains the expected error + let std_err_str = String::from_utf8(cmd_stderr).unwrap(); + assert!( + std_err_str + .contains("Given PCI device ID (188) is out of the supported range of 0..32"), + "Command return was: {std_err_str}", ); - // Remove network - assert!(remote_command(&api_socket, "remove-device", Some("test0"),)); - thread::sleep(std::time::Duration::new(5, 0)); - - let (cmd_success, cmd_output) = remote_command_w_output( + // Use the reserved device ID 0 (root device) + let (cmd_success, _, cmd_stderr) = remote_command_w_output( &api_socket, "add-net", Some( format!( - "{}{},id=test1", - guest.default_net_string(), - if let Some(pci_segment) = pci_segment { - format!(",pci_segment={pci_segment}") - } else { - "".to_owned() - } + "id=test0,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id=0", + guest.network.guest_mac1, guest.network.host_ip1, ) .as_str(), ), ); - assert!(cmd_success); + // Check for fail + assert!(!cmd_success); + // Check that the error message contains the expected error + let std_err_str = String::from_utf8(cmd_stderr).unwrap(); + assert!( + std_err_str.contains("Given PCI device ID (0) is reserved"), + "Command return was: {std_err_str}" + ); + }); - if let Some(pci_segment) = pci_segment { - assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( - "{{\"id\":\"test1\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" - ))); - } else { - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"test1\",\"bdf\":\"0000:00:05.0\"}")); - } + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); - thread::sleep(std::time::Duration::new(5, 0)); + handle_child_output(r, &output); + } - // 1 network interfaces + default localhost ==> 2 interfaces - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); - - guest.reboot_linux(0, None); - - // Check still there after reboot - // 1 network interfaces + default localhost ==> 2 interfaces - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); - }); + // This test exercises the local live-migration between two Cloud Hypervisor VMs on the + // same host. It ensures the following behaviors: + // 1. The source VM is up and functional (including various virtio-devices are working properly); + // 2. The 'send-migration' and 'receive-migration' command finished successfully; + // 3. The source VM terminated gracefully after live migration; + // 4. The destination VM is functional (including various virtio-devices are working properly) after + // live migration; + // Note: This test does not use vsock as we can't create two identical vsock on the same host. + #[cfg(not(feature = "mshv"))] + fn _test_live_migration(upgrade_test: bool, local: bool, paused: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + let console_text = String::from("On a branch floating down river a cricket, singing."); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let memory_param: &[&str] = if local { + &["--memory", "size=1500M,shared=on"] + } else { + &["--memory", "size=1500M"] + }; - handle_child_output(r, &output); - } + let boot_vcpus = 2; + let max_vcpus = 4; - #[test] - fn test_initramfs() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut workload_path = dirs::home_dir().unwrap(); - workload_path.push("workloads"); + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + let pmem_path = String::from("/dev/pmem0"); - #[cfg(target_arch = "x86_64")] - let mut kernels = vec![direct_kernel_boot_path()]; - #[cfg(target_arch = "aarch64")] - let kernels = [direct_kernel_boot_path()]; + // Start the source VM + let src_vm_path = if upgrade_test { + cloud_hypervisor_release_path() + } else { + clh_command("cloud-hypervisor") + }; + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd + .args([ + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + ]) + .args(memory_param) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) + .args([ + "--pmem", + format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), + ]); + let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); - #[cfg(target_arch = "x86_64")] - { - let mut pvh_kernel_path = workload_path.clone(); - pvh_kernel_path.push("vmlinux-x86_64"); - kernels.push(pvh_kernel_path); - } + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) + .capture_output() + .spawn() + .unwrap(); - let mut initramfs_path = workload_path; - initramfs_path.push("alpine_initramfs.img"); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - let test_string = String::from("axz34i9rylotd8n50wbv6kcj7f2qushme1pg"); - let cmdline = format!("console=hvc0 quiet TEST_STRING={test_string}"); + // Make sure the source VM is functional + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - kernels.iter().for_each(|k_path| { - let mut child = GuestCommand::new(&guest) - .args(["--kernel", k_path.to_str().unwrap()]) - .args(["--initramfs", initramfs_path.to_str().unwrap()]) - .args(["--cmdline", &cmdline]) - .capture_output() - .spawn() - .unwrap(); + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); - thread::sleep(std::time::Duration::new(20, 0)); + // Check the guest virtio-devices, e.g. block, rng, console, and net + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // x86_64: Following what's done in the `test_snapshot_restore`, we need + // to make sure that removing and adding back the virtio-net device does + // not break the live-migration support for virtio-pci. + #[cfg(target_arch = "x86_64")] + { + assert!(remote_command( + &src_api_socket, + "remove-device", + Some(net_id), + )); + assert!(wait_until(Duration::from_secs(10), || { + guest.wait_for_ssh(Duration::from_secs(1)).is_err() + })); - let r = std::panic::catch_unwind(|| { - let s = String::from_utf8_lossy(&output.stdout); + // Plug the virtio-net device again + assert!(remote_command( + &src_api_socket, + "add-net", + Some(net_params.as_str()), + )); + guest.wait_for_ssh(Duration::from_secs(10)).unwrap(); + } - assert_ne!(s.lines().position(|line| line == test_string), None); - }); + // Start the live-migration + let migration_socket = String::from( + guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - handle_child_output(r, &output); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + paused + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); }); - } - - #[test] - fn test_counters() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", guest.default_net_string().as_str()]) - .args(["--api-socket", &api_socket]) - .capture_output(); + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } - let mut child = cmd.spawn().unwrap(); + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } + // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Perform same checks to validate VM has been properly migrated + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); - let orig_counters = get_counters(&api_socket); - guest - .ssh_command("dd if=/dev/zero of=test count=8 bs=1M") - .unwrap(); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + }); - let new_counters = get_counters(&api_socket); + // Clean-up the destination VM and make sure it terminated correctly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); - // Check that all the counters have increased - assert!(new_counters > orig_counters); + // Check the destination VM has the expected 'console_text' from its output + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); + handle_child_output(r, &dest_output); + } - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // This test exercises the local live-migration between two Cloud Hypervisor VMs on the + // same host with Landlock enabled on both VMs. The test validates the following: + // 1. The source VM is up and functional + // 2. Ensure Landlock is enabled on source VM by hotplugging a disk. As the path for this + // disk is not known to the source VM this step will fail. + // 3. The 'send-migration' and 'receive-migration' command finished successfully; + // 4. The source VM terminated gracefully after live migration; + // 5. The destination VM is functional after live migration; + // 6. Ensure Landlock is enabled on destination VM by hotplugging a disk. As the path for + // this disk is not known to the destination VM this step will fail. + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_with_landlock() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); - handle_child_output(r, &output); - } + let boot_vcpus = 2; + let max_vcpus = 4; - #[test] - #[cfg(feature = "guest_debug")] - fn test_coredump() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + let mut blk_file_path = dirs::home_dir().unwrap(); + blk_file_path.push("workloads"); + blk_file_path.push("blk.img"); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=4"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_child = GuestCommand::new(&guest) + .args([ + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + ]) + .args(["--memory", "size=1500M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .args(["--net", guest.default_net_string().as_str()]) - .args(["--api-socket", &api_socket]) - .capture_output(); + .args(["--api-socket", &src_api_socket]) + .args(["--landlock"]) + .args(["--net", net_params.as_str()]) + .args([ + "--landlock-rules", + format!("path={:?},access=rw", guest.tmp_dir.as_path()).as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); - let mut child = cmd.spawn().unwrap(); - let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) + .capture_output() + .spawn() + .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - assert!(remote_command(&api_socket, "pause", None)); + // Make sure the source VM is functaionl + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - assert!(remote_command( - &api_socket, - "coredump", - Some(format!("file://{vmcore_file}").as_str()), + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); + + // Check Landlock is enabled by hot-plugging a disk. + assert!(!remote_command( + &src_api_socket, + "add-disk", + Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), )); - // the num of CORE notes should equals to vcpu - let readelf_core_num_cmd = - format!("readelf --all {vmcore_file} |grep CORE |grep -v Type |wc -l"); - let core_num_in_elf = exec_host_command_output(&readelf_core_num_cmd); - assert_eq!(String::from_utf8_lossy(&core_num_in_elf.stdout).trim(), "4"); + // Start the live-migration + let migration_socket = String::from( + guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - // the num of QEMU notes should equals to vcpu - let readelf_vmm_num_cmd = format!("readelf --all {vmcore_file} |grep QEMU |wc -l"); - let vmm_num_in_elf = exec_host_command_output(&readelf_vmm_num_cmd); - assert_eq!(String::from_utf8_lossy(&vmm_num_in_elf.stdout).trim(), "4"); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + true, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } - handle_child_output(r, &output); + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } + + // Post live-migration check to make sure the destination VM is functioning + let r = std::panic::catch_unwind(|| { + // Perform same checks to validate VM has been properly migrated + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); + }); + + // Check Landlock is enabled on destination VM by hot-plugging a disk. + assert!(!remote_command( + &dest_api_socket, + "add-disk", + Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), + )); + + // Clean-up the destination VM and make sure it terminated correctly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); } - #[test] - #[cfg(feature = "guest_debug")] - fn test_coredump_no_pause() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + // Function to get an available port + #[cfg(not(feature = "mshv"))] + fn get_available_port() -> u16 { + TcpListener::bind("127.0.0.1:0") + .expect("Failed to bind to address") + .local_addr() + .unwrap() + .port() + } - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=4"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .default_disks() - .args(["--net", guest.default_net_string().as_str()]) - .args(["--api-socket", &api_socket]) - .capture_output(); + #[cfg(not(feature = "mshv"))] + fn start_live_migration_tcp( + src_api_socket: &str, + dest_api_socket: &str, + connections: NonZeroU32, + ) -> bool { + // Get an available TCP port + let migration_port = get_available_port(); + let host_ip = "127.0.0.1"; - let mut child = cmd.spawn().unwrap(); - let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); + // Start the 'receive-migration' command on the destination + let mut receive_migration = Command::new(clh_command("ch-remote")) + .args([ + &format!("--api-socket={dest_api_socket}"), + "receive-migration", + &format!("tcp:0.0.0.0:{migration_port}"), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Give the destination some time to start listening + thread::sleep(Duration::from_secs(1)); - assert!(remote_command( - &api_socket, - "coredump", - Some(format!("file://{vmcore_file}").as_str()), - )); + // Start the 'send-migration' command on the source + let connections = connections.get(); + let mut send_migration = Command::new(clh_command("ch-remote")) + .args([ + &format!("--api-socket={src_api_socket}"), + "send-migration", + &format!( + "destination_url=tcp:{host_ip}:{migration_port},connections={connections}" + ), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); - assert_eq!(vm_state(&api_socket), "Running"); - }); + // Check if the 'send-migration' command executed successfully + let send_success = if let Some(status) = send_migration + .wait_timeout(Duration::from_secs(60)) + .unwrap() + { + status.success() + } else { + false + }; - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + if !send_success { + let _ = send_migration.kill(); + let output = send_migration.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'send_migration' output ====\n\n---stdout---\n{}\n\n---stderr---\n{}\n\n==== End 'send_migration' output ====\n\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } - handle_child_output(r, &output); - } + // Check if the 'receive-migration' command executed successfully + let receive_success = if let Some(status) = receive_migration + .wait_timeout(Duration::from_secs(60)) + .unwrap() + { + status.success() + } else { + false + }; - #[test] - fn test_watchdog() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + if !receive_success { + let _ = receive_migration.kill(); + let output = receive_migration.wait_with_output().unwrap(); + eprintln!( + "\n\n==== Start 'receive_migration' output ====\n\n---stdout---\n{}\n\n---stderr---\n{}\n\n==== End 'receive_migration' output ====\n\n", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + send_success && receive_success + } + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_tcp(connections: NonZeroU32) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); - let event_path = temp_event_monitor_path(&guest.tmp_dir); + let console_text = String::from("On a branch floating down river a cricket, singing."); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); + let memory_param: &[&str] = &["--memory", "size=1500M,shared=on"]; + let boot_vcpus = 2; + let max_vcpus = 4; + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + let pmem_path = String::from("/dev/pmem0"); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + // Start the source VM + let src_vm_path = clh_command("cloud-hypervisor"); + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd + .args([ + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + ]) + .args(memory_param) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .args(["--net", guest.default_net_string().as_str()]) - .args(["--watchdog"]) - .args(["--api-socket", &api_socket]) - .args(["--event-monitor", format!("path={event_path}").as_str()]) + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) + .args([ + "--pmem", + format!( + "file={},discard_writes=on", + pmem_temp_file.as_path().to_str().unwrap(), + ) + .as_str(), + ]) .capture_output(); + let mut src_child = src_vm_cmd.spawn().unwrap(); - let mut child = cmd.spawn().unwrap(); + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) + .capture_output() + .spawn() + .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - let mut expected_reboot_count = 1; - - // Enable the watchdog with a 15s timeout - enable_guest_watchdog(&guest, 15); - - assert_eq!(get_reboot_count(&guest), expected_reboot_count); - assert_eq!( - guest - .ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); - - // Allow some normal time to elapse to check we don't get spurious reboots - thread::sleep(std::time::Duration::new(40, 0)); - // Check no reboot - assert_eq!(get_reboot_count(&guest), expected_reboot_count); - - // Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns. - guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); - // Allow some time for the watchdog to trigger (max 30s) and reboot to happen - guest.wait_vm_boot(Some(50)).unwrap(); - // Check a reboot is triggered by the watchdog - expected_reboot_count += 1; - assert_eq!(get_reboot_count(&guest), expected_reboot_count); + guest.wait_vm_boot().unwrap(); + // Ensure the source VM is running normally + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + // On x86_64 architecture, remove and re-add the virtio-net device #[cfg(target_arch = "x86_64")] { - // Now pause the VM and remain offline for 30s - assert!(remote_command(&api_socket, "pause", None)); - let latest_events = [ - &MetaEvent { - event: "pausing".to_string(), - device_id: None, - }, - &MetaEvent { - event: "paused".to_string(), - device_id: None, - }, - ]; - assert!(check_latest_events_exact(&latest_events, &event_path)); - assert!(remote_command(&api_socket, "resume", None)); - - // Check no reboot - assert_eq!(get_reboot_count(&guest), expected_reboot_count); + assert!(remote_command( + &src_api_socket, + "remove-device", + Some(net_id), + )); + assert!(wait_until(Duration::from_secs(10), || { + guest.wait_for_ssh(Duration::from_secs(1)).is_err() + })); + // Re-add the virtio-net device + assert!(remote_command( + &src_api_socket, + "add-net", + Some(net_params.as_str()), + )); + guest.wait_for_ssh(Duration::from_secs(10)).unwrap(); } + // Start TCP live migration + assert!( + start_live_migration_tcp(&src_api_socket, &dest_api_socket, connections), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } - - #[test] - fn test_pvpanic() { - let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(jammy)); - let api_socket = temp_api_path(&guest.tmp_dir); - let event_path = temp_event_monitor_path(&guest.tmp_dir); - - let kernel_path = direct_kernel_boot_path(); - - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", guest.default_net_string().as_str()]) - .args(["--pvpanic"]) - .args(["--api-socket", &api_socket]) - .args(["--event-monitor", format!("path={event_path}").as_str()]) - .capture_output(); + // Check and report any errors that occurred during live migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } - let mut child = cmd.spawn().unwrap(); + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "Source VM was not terminated successfully.", + ); + } + // After live migration, ensure the destination VM is running normally let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Trigger guest a panic - make_guest_panic(&guest); - - // Wait a while for guest - thread::sleep(std::time::Duration::new(10, 0)); - - let expected_sequential_events = [&MetaEvent { - event: "panic".to_string(), - device_id: None, - }]; - assert!(check_latest_events_exact( - &expected_sequential_events, - &event_path - )); + // Perform the same checks to ensure the VM has migrated correctly + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // Clean up the destination VM and ensure it terminates properly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); - handle_child_output(r, &output); + // Check if the expected `console_text` is present in the destination VM's output + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + }); + handle_child_output(r, &dest_output); } - #[test] - fn test_tap_from_fd() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_tcp_timeout(timeout_strategy: TimeoutStrategy) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); + let net_id = "net1337"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); + let memory_param: &[&str] = &["--memory", "size=1500M,shared=on"]; + let boot_vcpus = 2; - // Create a TAP interface with multi-queue enabled - let num_queue_pairs: usize = 2; - - use std::str::FromStr; - let taps = net_util::open_tap( - Some("chtap0"), - Some(std::net::IpAddr::V4( - std::net::Ipv4Addr::from_str(&guest.network.host_ip).unwrap(), - )), - None, - &mut None, - None, - num_queue_pairs, - Some(libc::O_RDWR | libc::O_NONBLOCK), - ) - .unwrap(); - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", &format!("boot={num_queue_pairs}")]) - .args(["--memory", "size=512M"]) + let src_vm_path = clh_command("cloud-hypervisor"); + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd + .args(["--cpus", format!("boot={boot_vcpus}").as_str()]) + .args(memory_param) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .args([ - "--net", - &format!( - "fd=[{},{}],mac={},num_queues={}", - taps[0].as_raw_fd(), - taps[1].as_raw_fd(), - guest.network.guest_mac, - num_queue_pairs * 2 - ), - ]) + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) + .capture_output(); + let mut src_child = src_vm_cmd.spawn().unwrap(); + + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + guest.wait_vm_boot().unwrap(); - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - guest.reboot_linux(0, None); + // Start a memory stressor in the background to keep pages dirty, + // ensuring the precopy loop cannot converge within the 1s timeout. + guest + .ssh_command("nohup stress --vm 2 --vm-bytes 220M --vm-keep &>/dev/null &") + .unwrap(); + // Give stress a moment to actually start dirtying memory + thread::sleep(Duration::from_secs(3)); - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); - }); + let migration_port = get_available_port(); + let host_ip = "127.0.0.1"; - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let mut receive_migration = Command::new(clh_command("ch-remote")) + .args([ + &format!("--api-socket={dest_api_socket}"), + "receive-migration", + &format!("tcp:0.0.0.0:{migration_port}"), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); - handle_child_output(r, &output); - } + thread::sleep(Duration::from_secs(1)); - // By design, a guest VM won't be able to connect to the host - // machine when using a macvtap network interface (while it can - // communicate externally). As a workaround, this integration - // test creates two macvtap interfaces in 'bridge' mode on the - // same physical net interface, one for the guest and one for - // the host. With additional setup on the IP address and the - // routing table, it enables the communications between the - // guest VM and the host machine. - // Details: https://wiki.libvirt.org/page/TroubleshootMacvtapHostFail - fn _test_macvtap(hotplug: bool, guest_macvtap_name: &str, host_macvtap_name: &str) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); + // Use a tight downtime budget (1ms) combined with a 1s timeout so the + // migration practically cannot converge regardless of strategy. + let mut send_migration = Command::new(clh_command("ch-remote")) + .args([ + &format!("--api-socket={src_api_socket}"), + "send-migration", + &format!( + "destination_url=tcp:{host_ip}:{migration_port},downtime_ms=1,timeout_s=1,timeout_strategy={timeout_strategy:?}" + ), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); - #[cfg(target_arch = "x86_64")] - let kernel_path = direct_kernel_boot_path(); - #[cfg(target_arch = "aarch64")] - let kernel_path = edk2_path(); + let send_status = send_migration + .wait_timeout(Duration::from_secs(60)) + .unwrap(); + let receive_status = receive_migration + .wait_timeout(Duration::from_secs(60)) + .unwrap(); - let phy_net = "eth0"; + // Clean up receive-migration regardless of its outcome + if receive_status.is_none() { + let _ = receive_migration.kill(); + } - // Create a macvtap interface for the guest VM to use - assert!(exec_host_command_status(&format!( - "sudo ip link add link {phy_net} name {guest_macvtap_name} type macvtap mod bridge" - )) - .success()); - assert!(exec_host_command_status(&format!( - "sudo ip link set {} address {} up", - guest_macvtap_name, guest.network.guest_mac - )) - .success()); - assert!( - exec_host_command_status(&format!("sudo ip link show {guest_macvtap_name}")).success() - ); + // Kill the stressor now that migration has completed or aborted, + // to reduce system load during post-migration checks. + let _ = guest.ssh_command("pkill -f 'stress --vm'"); + + match timeout_strategy { + TimeoutStrategy::Cancel => { + // With cancel strategy the send must fail and the source VM + // must keep running. + let send_failed = match send_status { + Some(status) => !status.success(), + None => { + let _ = send_migration.kill(); + false + } + }; + assert!( + send_failed, + "send-migration should have failed due to 1s timeout with cancel strategy" + ); - let tap_index = - fs::read_to_string(format!("/sys/class/net/{guest_macvtap_name}/ifindex")).unwrap(); - let tap_device = format!("/dev/tap{}", tap_index.trim()); + thread::sleep(Duration::from_secs(2)); + assert!( + src_child.try_wait().unwrap().is_none(), + "Source VM should still be running after a cancelled migration" + ); - assert!(exec_host_command_status(&format!("sudo chown $UID.$UID {tap_device}")).success()); + // Confirm the source VM is still responsive over SSH + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + } + TimeoutStrategy::Ignore => { + // With Ignore strategy the send must succeed despite the timeout + // being reached, and the source VM must have terminated. + let send_succeeded = match send_status { + Some(status) => status.success(), + None => { + let _ = send_migration.kill(); + false + } + }; + assert!( + send_succeeded, + "send-migration should have succeeded with timeout_strategy=ignore" + ); - let cstr_tap_device = std::ffi::CString::new(tap_device).unwrap(); - let tap_fd1 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) }; - assert!(tap_fd1 > 0); - let tap_fd2 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) }; - assert!(tap_fd2 > 0); + thread::sleep(Duration::from_secs(3)); + assert!( + src_child.try_wait().unwrap().is_some(), + "Source VM should have terminated after a forced migration" + ); - // Create a macvtap on the same physical net interface for - // the host machine to use - assert!(exec_host_command_status(&format!( - "sudo ip link add link {phy_net} name {host_macvtap_name} type macvtap mod bridge" - )) - .success()); - // Use default mask "255.255.255.0" - assert!(exec_host_command_status(&format!( - "sudo ip address add {}/24 dev {}", - guest.network.host_ip, host_macvtap_name - )) - .success()); - assert!( - exec_host_command_status(&format!("sudo ip link set dev {host_macvtap_name} up")) - .success() - ); + // Confirm the VM is still responsive over SSH on the new host + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + } + } + })); - let mut guest_command = GuestCommand::new(&guest); - guest_command - .args(["--cpus", "boot=2"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--api-socket", &api_socket]); + let _ = src_child.kill(); + let src_output = src_child.wait_with_output().unwrap(); + let _ = dest_child.kill(); + let _dest_output = dest_child.wait_with_output().unwrap(); - let net_params = format!( - "fd=[{},{}],mac={},num_queues=4", - tap_fd1, tap_fd2, guest.network.guest_mac - ); + handle_child_output(r, &src_output); + } - if !hotplug { - guest_command.args(["--net", &net_params]); - } + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_basic() { + _test_live_migration(false, false, false); + } - let mut child = guest_command.capture_output().spawn().unwrap(); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_local() { + _test_live_migration(false, true, false); + } - if hotplug { - // Give some time to the VMM process to listen to the API - // socket. This is the only requirement to avoid the following - // call to ch-remote from failing. - thread::sleep(std::time::Duration::new(10, 0)); - // Hotplug the virtio-net device - let (cmd_success, cmd_output) = - remote_command_w_output(&api_socket, "add-net", Some(&net_params)); - assert!(cmd_success); - #[cfg(target_arch = "x86_64")] - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"_net2\",\"bdf\":\"0000:00:05.0\"}")); - #[cfg(target_arch = "aarch64")] - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"_net0\",\"bdf\":\"0000:00:05.0\"}")); - } + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_basic_paused() { + _test_live_migration(false, false, true); + } - // The functional connectivity provided by the virtio-net device - // gets tested through wait_vm_boot() as it expects to receive a - // HTTP request, and through the SSH command as well. - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_local_paused() { + _test_live_migration(false, true, true); + } - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); - - guest.reboot_linux(0, None); - - assert_eq!( - guest - .ssh_command("ip -o link | wc -l") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 2 - ); - }); - - kill_child(&mut child); - - exec_host_command_status(&format!("sudo ip link del {guest_macvtap_name}")); - exec_host_command_status(&format!("sudo ip link del {host_macvtap_name}")); - - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_tcp() { + _test_live_migration_tcp(NonZeroU32::new(1).unwrap()); } #[test] - #[cfg_attr(target_arch = "aarch64", ignore = "See #5443")] - fn test_macvtap() { - _test_macvtap(false, "guestmacvtap0", "hostmacvtap0") + #[cfg(not(feature = "mshv"))] + fn test_live_migration_tcp_parallel_connections() { + _test_live_migration_tcp(NonZeroU32::new(8).unwrap()); } #[test] - #[cfg_attr(target_arch = "aarch64", ignore = "See #5443")] - fn test_macvtap_hotplug() { - _test_macvtap(true, "guestmacvtap1", "hostmacvtap1") + #[cfg(not(feature = "mshv"))] + fn test_live_migration_tcp_timeout_cancel() { + _test_live_migration_tcp_timeout(TimeoutStrategy::Cancel); } #[test] #[cfg(not(feature = "mshv"))] - fn test_ovs_dpdk() { - let focal1 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest1 = Guest::new(Box::new(focal1)); - - let focal2 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest2 = Guest::new(Box::new(focal2)); - let api_socket_source = format!("{}.1", temp_api_path(&guest2.tmp_dir)); - - let (mut child1, mut child2) = - setup_ovs_dpdk_guests(&guest1, &guest2, &api_socket_source, false); - - // Create the snapshot directory - let snapshot_dir = temp_snapshot_dir_path(&guest2.tmp_dir); - - let r = std::panic::catch_unwind(|| { - // Remove one of the two ports from the OVS bridge - assert!(exec_host_command_status("ovs-vsctl del-port vhost-user1").success()); - - // Spawn a new netcat listener in the first VM - let guest_ip = guest1.network.guest_ip.clone(); - thread::spawn(move || { - ssh_command_ip( - "nc -l 12345", - &guest_ip, - DEFAULT_SSH_RETRIES, - DEFAULT_SSH_TIMEOUT, - ) - .unwrap(); - }); - - // Wait for the server to be listening - thread::sleep(std::time::Duration::new(5, 0)); - - // Check the connection fails this time - guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap_err(); + fn test_live_migration_tcp_timeout_ignore() { + _test_live_migration_tcp_timeout(TimeoutStrategy::Ignore); + } - // Add the OVS port back - assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success()); + // TODO: Add test of live upgrade paused vm after cloud-hypervisor-static + // version is updated. + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_basic() { + _test_live_migration(true, false, false); + } - // And finally check the connection is functional again - guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_local() { + _test_live_migration(true, true, false); + } - // Pause the VM - assert!(remote_command(&api_socket_source, "pause", None)); + #[test] + #[cfg(not(feature = "mshv"))] + #[cfg(target_arch = "x86_64")] + fn test_live_migration_with_landlock() { + _test_live_migration_with_landlock(); + } - // Take a snapshot from the VM - assert!(remote_command( - &api_socket_source, - "snapshot", - Some(format!("file://{snapshot_dir}").as_str()), - )); + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_virtio_fs(local: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - // Wait to make sure the snapshot is completed - thread::sleep(std::time::Duration::new(10, 0)); - }); + let shared_dir = guest.tmp_dir.as_path().join("virtiofs_shared"); + std::fs::create_dir(&shared_dir).unwrap(); - // Shutdown the source VM - kill_child(&mut child2); - let output = child2.wait_with_output().unwrap(); - handle_child_output(r, &output); + let (daemon_child, virtiofsd_socket_path) = + prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap()); - // Remove the vhost-user socket file. - Command::new("rm") - .arg("-f") - .arg("/tmp/dpdkvhostclient2") - .output() - .unwrap(); + let src_api_socket = temp_api_path(&guest.tmp_dir); - let api_socket_restored = format!("{}.2", temp_api_path(&guest2.tmp_dir)); - // Restore the VM from the snapshot - let mut child2 = GuestCommand::new(&guest2) - .args(["--api-socket", &api_socket_restored]) + // Start the source VM + let mut src_child = GuestCommand::new(&guest) + .args(["--api-socket", &src_api_socket]) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=512M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() .args([ - "--restore", - format!("source_url=file://{snapshot_dir}").as_str(), + "--fs", + format!("socket={virtiofsd_socket_path},tag=myfs,num_queues=1,queue_size=1024") + .as_str(), ]) .capture_output() .spawn() .unwrap(); - // Wait for the VM to be restored - thread::sleep(std::time::Duration::new(10, 0)); + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) + .capture_output() + .spawn() + .unwrap(); + + // Spawn a thread that waits for the old virtiofsd to exit then + // starts a replacement. During migration the source saves + // DEVICE_STATE then disconnects, causing virtiofsd to exit. + // The destination needs a fresh virtiofsd to load DEVICE_STATE. + // We remove the socket file first so the destination cannot + // accidentally connect to the old instance. + let virtiofsd_socket_clone = virtiofsd_socket_path.clone(); + let shared_dir_str = shared_dir.to_str().unwrap().to_string(); + let (restart_tx, restart_rx) = std::sync::mpsc::channel(); + let _monitor = thread::spawn(move || { + let mut child = daemon_child; + let _ = child.wait(); + let mut path = dirs::home_dir().unwrap(); + path.push("workloads"); + path.push("virtiofsd"); + let new_child = Command::new(path) + .args(["--shared-dir", &shared_dir_str]) + .args(["--socket-path", &virtiofsd_socket_clone]) + .args(["--cache", "never"]) + .args(["--tag", "myfs"]) + .spawn() + .unwrap(); + wait_for_virtiofsd_socket(&virtiofsd_socket_clone); + let _ = restart_tx.send(new_child); + }); let r = std::panic::catch_unwind(|| { - // Resume the VM - assert!(remote_command(&api_socket_restored, "resume", None)); + guest.wait_vm_boot().unwrap(); - // Spawn a new netcat listener in the first VM - let guest_ip = guest1.network.guest_ip.clone(); - thread::spawn(move || { - ssh_command_ip( - "nc -l 12345", - &guest_ip, - DEFAULT_SSH_RETRIES, - DEFAULT_SSH_TIMEOUT, + // Mount virtiofs and verify it works + guest + .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") + .unwrap(); + + // Write a test file through virtiofs before migration + guest + .ssh_command( + "sudo bash -c 'echo pre_migration_data > mount_dir/migration_test_file'", ) .unwrap(); - }); - // Wait for the server to be listening - thread::sleep(std::time::Duration::new(5, 0)); + // Verify the file is accessible + assert_eq!( + guest + .ssh_command("cat mount_dir/migration_test_file") + .unwrap() + .trim(), + "pre_migration_data" + ); - // And check the connection is still functional after restore - guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); - }); + let migration_socket = String::from( + guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - kill_child(&mut child1); - kill_child(&mut child2); + // Remove the socket so the destination cannot connect to + // the old virtiofsd (which is still running). The source's + // existing connection uses an already-accepted fd. + let _ = std::fs::remove_file(&virtiofsd_socket_path); - let output = child1.wait_with_output().unwrap(); - child2.wait().unwrap(); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); + }); - cleanup_ovs_dpdk(); + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration with virtio-fs", + ); + } - handle_child_output(r, &output); - } + // Check the source vm has been terminated successfully (give it '3s' to settle) + thread::sleep(Duration::from_secs(3)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } - fn setup_spdk_nvme(nvme_dir: &std::path::Path) -> Child { - cleanup_spdk_nvme(); + // Post live-migration checks + let r = std::panic::catch_unwind(|| { + // Verify virtiofs still works after migration + // Read the file written before migration + assert_eq!( + guest + .ssh_command("cat mount_dir/migration_test_file") + .unwrap() + .trim(), + "pre_migration_data" + ); - assert!(exec_host_command_status(&format!( - "mkdir -p {}", - nvme_dir.join("nvme-vfio-user").to_str().unwrap() - )) - .success()); - assert!(exec_host_command_status(&format!( - "truncate {} -s 128M", - nvme_dir.join("test-disk.raw").to_str().unwrap() - )) - .success()); - assert!(exec_host_command_status(&format!( - "mkfs.ext4 {}", - nvme_dir.join("test-disk.raw").to_str().unwrap() - )) - .success()); + // Write a new file after migration + guest + .ssh_command( + "sudo bash -c 'echo post_migration_data > mount_dir/post_migration_file'", + ) + .unwrap(); - // Start the SPDK nvmf_tgt daemon to present NVMe device as a VFIO user device - let child = Command::new("/usr/local/bin/spdk-nvme/nvmf_tgt") - .args(["-i", "0", "-m", "0x1"]) - .spawn() - .unwrap(); - thread::sleep(std::time::Duration::new(2, 0)); + // Verify the new file exists on the host + let post_content = + std::fs::read_to_string(shared_dir.join("post_migration_file")).unwrap(); + assert_eq!(post_content.trim(), "post_migration_data"); + }); - assert!(exec_host_command_with_retries( - "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_transport -t VFIOUSER", - 3, - std::time::Duration::new(5, 0), - )); - assert!(exec_host_command_status(&format!( - "/usr/local/bin/spdk-nvme/rpc.py bdev_aio_create {} test 512", - nvme_dir.join("test-disk.raw").to_str().unwrap() - )) - .success()); - assert!(exec_host_command_status( - "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_subsystem nqn.2019-07.io.spdk:cnode -a -s test" - ) - .success()); - assert!(exec_host_command_status( - "/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_ns nqn.2019-07.io.spdk:cnode test" - ) - .success()); - assert!(exec_host_command_status(&format!( - "/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_listener nqn.2019-07.io.spdk:cnode -t VFIOUSER -a {} -s 0", - nvme_dir.join("nvme-vfio-user").to_str().unwrap() - )) - .success()); + // Clean up + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + if let Ok(mut new_daemon) = restart_rx.try_recv() { + let _ = new_daemon.kill(); + let _ = new_daemon.wait(); + } + let _ = std::fs::remove_file(shared_dir.join("migration_test_file")); + let _ = std::fs::remove_file(shared_dir.join("post_migration_file")); - child + handle_child_output(r, &dest_output); } - fn cleanup_spdk_nvme() { - exec_host_command_status("pkill -f nvmf_tgt"); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_virtio_fs() { + _test_live_migration_virtio_fs(false); } #[test] - fn test_vfio_user() { - let jammy_image = JAMMY_IMAGE_NAME.to_string(); - let jammy = UbuntuDiskConfig::new(jammy_image); - let guest = Guest::new(Box::new(jammy)); + #[cfg(not(feature = "mshv"))] + fn test_live_migration_virtio_fs_local() { + _test_live_migration_virtio_fs(true); + } +} - let spdk_nvme_dir = guest.tmp_dir.as_path().join("test-vfio-user"); - let mut spdk_child = setup_spdk_nvme(spdk_nvme_dir.as_path()); +mod dbus_api { + use crate::*; + + // Start cloud-hypervisor with no VM parameters, running both the HTTP + // and DBus APIs. Alternate calls to the external APIs (HTTP and DBus) + // to create a VM, boot it, and verify that it can be shut down and then + // booted again. + #[test] + fn test_api_dbus_and_http_interleaved() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let dbus_api = TargetApi::new_dbus_api(&guest.tmp_dir); + let http_api = TargetApi::new_http_api(&guest.tmp_dir); - let api_socket = temp_api_path(&guest.tmp_dir); let mut child = GuestCommand::new(&guest) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=1G,shared=on,hugepages=on"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .args(["--serial", "tty", "--console", "off"]) - .default_disks() - .default_net() + .args(dbus_api.guest_args()) + .args(http_api.guest_args()) .capture_output() .spawn() .unwrap(); + thread::sleep(std::time::Duration::new(1, 0)); + + // Verify API servers are running + assert!(dbus_api.remote_command("ping", None)); + assert!(http_api.remote_command("ping", None)); + + // Create the VM first + let request_body = guest.api_create_body(); + + let temp_config_path = guest.tmp_dir.as_path().join("config"); + std::fs::write(&temp_config_path, request_body).unwrap(); + let create_config = temp_config_path.as_os_str().to_str().unwrap(); + let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Create the VM + assert!(dbus_api.remote_command("create", Some(create_config),)); - // Hotplug the SPDK-NVMe device to the VM - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-user-device", - Some(&format!( - "socket={},id=vfio_user0", - spdk_nvme_dir - .as_path() - .join("nvme-vfio-user/cntrl") - .to_str() - .unwrap(), - )), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"vfio_user0\",\"bdf\":\"0000:00:05.0\"}")); + // Then boot it + assert!(http_api.remote_command("boot", None)); + guest.wait_vm_boot().unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); - // Check both if /dev/nvme exists and if the block size is 128M. - assert_eq!( - guest - .ssh_command("lsblk | grep nvme0n1 | grep -c 128M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + // Sync and shutdown without powering off to prevent filesystem + // corruption. + guest.ssh_command("sync").unwrap(); + guest.ssh_command("sudo shutdown -H now").unwrap(); - // Check changes persist after reboot - assert_eq!( - guest.ssh_command("sudo mount /dev/nvme0n1 /mnt").unwrap(), - "" - ); - assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n"); - guest - .ssh_command("echo test123 | sudo tee /mnt/test") - .unwrap(); - assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), ""); - assert_eq!(guest.ssh_command("ls /mnt").unwrap(), ""); + // Wait for the guest to be fully shutdown + assert!(guest.wait_for_ssh_unresponsive(Duration::from_secs(20))); - guest.reboot_linux(0, None); - assert_eq!( - guest.ssh_command("sudo mount /dev/nvme0n1 /mnt").unwrap(), - "" - ); - assert_eq!( - guest.ssh_command("sudo cat /mnt/test").unwrap().trim(), - "test123" - ); - }); + // Then shutdown the VM + assert!(dbus_api.remote_command("shutdown", None)); - let _ = spdk_child.kill(); - let _ = spdk_child.wait(); + // Then boot it again + assert!(http_api.remote_command("boot", None)); + guest.wait_vm_boot().unwrap(); + + // Check that the VM booted as expected + guest.validate_cpu_count(None); + guest.validate_memory(None); + }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); @@ -6824,473 +7148,521 @@ mod common_parallel { } #[test] - #[cfg(target_arch = "x86_64")] - fn test_vdpa_block() { - // Before trying to run the test, verify the vdpa_sim_blk module is correctly loaded. - assert!(exec_host_command_status("lsmod | grep vdpa_sim_blk").success()); - - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let api_socket = temp_api_path(&guest.tmp_dir); - - let kernel_path = direct_kernel_boot_path(); - - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2"]) - .args(["--memory", "size=512M,hugepages=on"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .default_net() - .args(["--vdpa", "path=/dev/vhost-vdpa-0,num_queues=1"]) - .args(["--platform", "num_pci_segments=2,iommu_segments=1"]) - .args(["--api-socket", &api_socket]) - .capture_output() - .spawn() - .unwrap(); + fn test_api_dbus_create_boot() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_cpu(4); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); + _test_api_create_boot(&target_api, &guest); + } - // Check both if /dev/vdc exists and if the block size is 128M. - assert_eq!( - guest - .ssh_command("lsblk | grep vdc | grep -c 128M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + #[test] + fn test_api_dbus_shutdown() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_cpu(4); - // Check the content of the block device after we wrote to it. - // The vpda-sim-blk should let us read what we previously wrote. - guest - .ssh_command("sudo bash -c 'echo foobar > /dev/vdc'") - .unwrap(); - assert_eq!( - guest.ssh_command("sudo head -1 /dev/vdc").unwrap().trim(), - "foobar" - ); + let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); + _test_api_shutdown(&target_api, &guest); + } - // Hotplug an extra vDPA block device behind the vIOMMU - // Add a new vDPA device to the VM - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-vdpa", - Some("id=myvdpa0,path=/dev/vhost-vdpa-1,num_queues=1,pci_segment=1,iommu=on"), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"myvdpa0\",\"bdf\":\"0001:00:01.0\"}")); + #[test] + fn test_api_dbus_delete() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_cpu(4); - thread::sleep(std::time::Duration::new(10, 0)); + let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); + _test_api_delete(&target_api, &guest); + } - // Check IOMMU setup - assert!(guest - .does_device_vendor_pair_match("0x1057", "0x1af4") - .unwrap_or_default()); - assert_eq!( - guest - .ssh_command("ls /sys/kernel/iommu_groups/0/devices") - .unwrap() - .trim(), - "0001:00:01.0" - ); + #[test] + fn test_api_dbus_pause_resume() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_cpu(4); - // Check both if /dev/vdd exists and if the block size is 128M. - assert_eq!( - guest - .ssh_command("lsblk | grep vdd | grep -c 128M") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); + let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); + _test_api_pause_resume(&target_api, &guest); + } +} - // Write some content to the block device we've just plugged. - guest - .ssh_command("sudo bash -c 'echo foobar > /dev/vdd'") - .unwrap(); +mod ivshmem { + #[cfg(not(feature = "mshv"))] + use std::fs::remove_dir_all; + use std::process::Command; - // Check we can read the content back. - assert_eq!( - guest.ssh_command("sudo head -1 /dev/vdd").unwrap().trim(), - "foobar" - ); + use test_infra::{Guest, GuestCommand, UbuntuDiskConfig, handle_child_output, kill_child}; - // Unplug the device - let cmd_success = remote_command(&api_socket, "remove-device", Some("myvdpa0")); - assert!(cmd_success); - thread::sleep(std::time::Duration::new(10, 0)); + use crate::*; - // Check /dev/vdd doesn't exist anymore - assert_eq!( - guest - .ssh_command("lsblk | grep -c vdd || true") - .unwrap() - .trim() - .parse::() - .unwrap_or(1), - 0 - ); - }); + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_ivshmem(local: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + let console_text = String::from("On a branch floating down river a cricket, singing."); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + let memory_param: &[&str] = if local { + &["--memory", "size=4G,shared=on"] + } else { + &["--memory", "size=4G"] + }; - handle_child_output(r, &output); - } + let boot_vcpus = 2; + let max_vcpus = 4; - #[test] - #[cfg(target_arch = "x86_64")] - #[ignore = "See #5756"] - fn test_vdpa_net() { - // Before trying to run the test, verify the vdpa_sim_net module is correctly loaded. - if !exec_host_command_status("lsmod | grep vdpa_sim_net").success() { - return; - } + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + let pmem_path = String::from("/dev/pmem0"); - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let ivshmem_file_path = String::from( + guest + .tmp_dir + .as_path() + .join("ivshmem.data") + .to_str() + .unwrap(), + ); + let file_size = "1M"; - let kernel_path = direct_kernel_boot_path(); + // Create a file to be used as the shared memory + Command::new("dd") + .args([ + "if=/dev/zero", + format!("of={ivshmem_file_path}").as_str(), + format!("bs={file_size}").as_str(), + "count=1", + ]) + .status() + .unwrap(); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=2"]) - .args(["--memory", "size=512M,hugepages=on"]) + // Start the source VM + let src_vm_path = clh_command("cloud-hypervisor"); + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd + .args([ + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + ]) + .args(memory_param) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .default_net() - .args(["--vdpa", "path=/dev/vhost-vdpa-2,num_queues=2"]) + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) + .args([ + "--pmem", + format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), + ]) + .args([ + "--ivshmem", + format!("path={ivshmem_file_path},size={file_size}").as_str(), + ]); + let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); + + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Check we can find network interface related to vDPA device - assert_eq!( - guest - .ssh_command("ip -o link | grep -c ens6") - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - 1 - ); + guest.wait_vm_boot().unwrap(); - guest - .ssh_command("sudo ip addr add 172.16.1.2/24 dev ens6") - .unwrap(); - guest.ssh_command("sudo ip link set up dev ens6").unwrap(); + // Make sure the source VM is functional + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + // Check the guest virtio-devices, e.g. block, rng, console, and net + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + // x86_64: Following what's done in the `test_snapshot_restore`, we need + // to make sure that removing and adding back the virtio-net device does + // not break the live-migration support for virtio-pci. + #[cfg(target_arch = "x86_64")] + { + assert!(remote_command( + &src_api_socket, + "remove-device", + Some(net_id), + )); + thread::sleep(Duration::new(10, 0)); - // Check there is no packet yet on both TX/RX of the network interface - assert_eq!( - guest - .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 0'") - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - 2 - ); + // Plug the virtio-net device again + assert!(remote_command( + &src_api_socket, + "add-net", + Some(net_params.as_str()), + )); + thread::sleep(Duration::new(10, 0)); + } - // Send 6 packets with ping command - guest.ssh_command("ping 172.16.1.10 -c 6 || true").unwrap(); + // Check ivshmem device in src guest. + _test_ivshmem(&guest, &ivshmem_file_path, file_size); + // Allow some normal time to elapse to check we don't get spurious reboots + thread::sleep(std::time::Duration::new(40, 0)); - // Check we can find 6 packets on both TX/RX of the network interface - assert_eq!( + // Start the live-migration + let migration_socket = String::from( guest - .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 6'") - .unwrap() - .trim() - .parse::() - .unwrap_or(0), - 2 + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), ); - // No need to check for hotplug as we already tested it through - // test_vdpa_block() + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); }); - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } - #[test] - #[cfg(target_arch = "x86_64")] - fn test_tpm() { - let focal = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } - let (mut swtpm_command, swtpm_socket_path) = prepare_swtpm_daemon(&guest.tmp_dir); + // Post live-migration check to make sure the destination VM is functional + let r = std::panic::catch_unwind(|| { + // Perform same checks to validate VM has been properly migrated + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - let mut guest_cmd = GuestCommand::new(&guest); - guest_cmd - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=1G"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .args(["--tpm", &format!("socket={swtpm_socket_path}")]) - .capture_output() - .default_disks() - .default_net(); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - // Start swtpm daemon - let mut swtpm_child = swtpm_command.spawn().unwrap(); - thread::sleep(std::time::Duration::new(10, 0)); - let mut child = guest_cmd.spawn().unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - assert_eq!( - guest.ssh_command("ls /dev/tpm0").unwrap().trim(), - "/dev/tpm0" - ); - guest.ssh_command("sudo tpm2_selftest -f").unwrap(); - guest - .ssh_command("echo 'hello' > /tmp/checksum_test; ") - .unwrap(); - guest.ssh_command("cmp <(sudo tpm2_pcrevent /tmp/checksum_test | grep sha256 | awk '{print $2}') <(sha256sum /tmp/checksum_test| awk '{print $1}')").unwrap(); + // Check ivshmem device + _test_ivshmem(&guest, &ivshmem_file_path, file_size); }); - let _ = swtpm_child.kill(); - let _d_out = swtpm_child.wait_with_output().unwrap(); - - kill_child(&mut child); - let output = child.wait_with_output().unwrap(); + // Clean-up the destination VM and make sure it terminated correctly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); - handle_child_output(r, &output); + // Check the destination VM has the expected 'console_text' from its output + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + }); + handle_child_output(r, &dest_output); } #[test] - #[cfg(target_arch = "x86_64")] - fn test_double_tty() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let mut cmd = GuestCommand::new(&guest); + fn test_ivshmem() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let api_socket = temp_api_path(&guest.tmp_dir); - let tty_str: &str = "console=hvc0 earlyprintk=ttyS0 "; - // linux printk module enable console log. - let con_dis_str: &str = "console [hvc0] enabled"; - // linux printk module disable console log. - let con_enb_str: &str = "bootconsole [earlyser0] disabled"; let kernel_path = direct_kernel_boot_path(); - cmd.args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) + let ivshmem_file_path = String::from( + guest + .tmp_dir + .as_path() + .join("ivshmem.data") + .to_str() + .unwrap(), + ); + let file_size = "1M"; + + // Create a file to be used as the shared memory + Command::new("dd") .args([ - "--cmdline", - DIRECT_KERNEL_BOOT_CMDLINE - .replace("console=hvc0 ", tty_str) - .as_str(), + "if=/dev/zero", + format!("of={ivshmem_file_path}").as_str(), + format!("bs={file_size}").as_str(), + "count=1", ]) - .capture_output() + .status() + .unwrap(); + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=2"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() .default_net() - .args(["--serial", "tty"]) - .args(["--console", "tty"]) - .args(["--api-socket", &api_socket]); - - let mut child = cmd.spawn().unwrap(); + .args([ + "--ivshmem", + format!("path={ivshmem_file_path},size={file_size}").as_str(), + ]) + .args(["--api-socket", &api_socket]) + .capture_output() + .spawn() + .unwrap(); - let mut r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + _test_ivshmem(&guest, &ivshmem_file_path, file_size); }); - kill_child(&mut child); let output = child.wait_with_output().unwrap(); - if r.is_ok() { - r = std::panic::catch_unwind(|| { - let s = String::from_utf8_lossy(&output.stdout); - assert!(s.contains(tty_str)); - assert!(s.contains(con_dis_str)); - assert!(s.contains(con_enb_str)); - }); - } - handle_child_output(r, &output); } #[test] - #[cfg(target_arch = "x86_64")] - fn test_nmi() { - let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(jammy)); - let api_socket = temp_api_path(&guest.tmp_dir); - let event_path = temp_event_monitor_path(&guest.tmp_dir); - + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_ivshmem() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); - let cmd_line = format!("{} {}", DIRECT_KERNEL_BOOT_CMDLINE, "unknown_nmi_panic=1"); - let mut cmd = GuestCommand::new(&guest); - cmd.args(["--cpus", "boot=4"]) - .args(["--memory", "size=512M"]) + let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); + + let ivshmem_file_path = String::from( + guest + .tmp_dir + .as_path() + .join("ivshmem.data") + .to_str() + .unwrap(), + ); + let file_size = "1M"; + + // Create a file to be used as the shared memory + Command::new("dd") + .args([ + "if=/dev/zero", + format!("of={ivshmem_file_path}").as_str(), + format!("bs={file_size}").as_str(), + "count=1", + ]) + .status() + .unwrap(); + + let socket = temp_vsock_path(&guest.tmp_dir); + let event_path = temp_event_monitor_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(&guest) + .args(["--api-socket", &api_socket_source]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=1G"]) .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", cmd_line.as_str()]) .default_disks() - .args(["--net", guest.default_net_string().as_str()]) - .args(["--pvpanic"]) - .args(["--api-socket", &api_socket]) - .args(["--event-monitor", format!("path={event_path}").as_str()]) - .capture_output(); + .default_net() + .args(["--vsock", format!("cid=3,socket={socket}").as_str()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args([ + "--ivshmem", + format!("path={ivshmem_file_path},size={file_size}").as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); - let mut child = cmd.spawn().unwrap(); + let console_text = String::from("On a branch floating down river a cricket, singing."); + // Create the snapshot directory + let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - assert!(remote_command(&api_socket, "nmi", None)); + guest.wait_vm_boot().unwrap(); - // Wait a while for guest - thread::sleep(std::time::Duration::new(3, 0)); + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); - let expected_sequential_events = [&MetaEvent { - event: "panic".to_string(), - device_id: None, - }]; - assert!(check_latest_events_exact( - &expected_sequential_events, - &event_path - )); + snapshot_restore_common::snapshot_and_check_events( + &api_socket_source, + &snapshot_dir, + &event_path, + ); }); + // Shutdown the source VM and check console output kill_child(&mut child); let output = child.wait_with_output().unwrap(); - handle_child_output(r, &output); - } -} -mod dbus_api { - use crate::*; + // Remove the vsock socket file. + Command::new("rm") + .arg("-f") + .arg(socket.as_str()) + .output() + .unwrap(); - // Start cloud-hypervisor with no VM parameters, running both the HTTP - // and DBus APIs. Alternate calls to the external APIs (HTTP and DBus) - // to create a VM, boot it, and verify that it can be shut down and then - // booted again. - #[test] - fn test_api_dbus_and_http_interleaved() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let dbus_api = TargetApi::new_dbus_api(&guest.tmp_dir); - let http_api = TargetApi::new_http_api(&guest.tmp_dir); + let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); + let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); + // Restore the VM from the snapshot let mut child = GuestCommand::new(&guest) - .args(dbus_api.guest_args()) - .args(http_api.guest_args()) + .args(["--api-socket", &api_socket_restored]) + .args([ + "--event-monitor", + format!("path={event_path_restored}").as_str(), + ]) + .args([ + "--restore", + format!("source_url=file://{snapshot_dir}").as_str(), + ]) .capture_output() .spawn() .unwrap(); - thread::sleep(std::time::Duration::new(1, 0)); - - // Verify API servers are running - assert!(dbus_api.remote_command("ping", None)); - assert!(http_api.remote_command("ping", None)); - - // Create the VM first - let cpu_count: u8 = 4; - let request_body = guest.api_create_body( - cpu_count, - direct_kernel_boot_path().to_str().unwrap(), - DIRECT_KERNEL_BOOT_CMDLINE, - ); + let latest_events = [&MetaEvent { + event: "restored".to_string(), + device_id: None, + }]; + // Wait for the restored event to show up in the monitor file. + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path_restored + )); - let temp_config_path = guest.tmp_dir.as_path().join("config"); - std::fs::write(&temp_config_path, request_body).unwrap(); - let create_config = temp_config_path.as_os_str().to_str().unwrap(); + // Remove the snapshot dir + let _ = remove_dir_all(snapshot_dir.as_str()); let r = std::panic::catch_unwind(|| { - // Create the VM - assert!(dbus_api.remote_command("create", Some(create_config),)); - - // Then boot it - assert!(http_api.remote_command("boot", None)); - guest.wait_vm_boot(None).unwrap(); - - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); - - // Sync and shutdown without powering off to prevent filesystem - // corruption. - guest.ssh_command("sync").unwrap(); - guest.ssh_command("sudo shutdown -H now").unwrap(); - - // Wait for the guest to be fully shutdown - thread::sleep(std::time::Duration::new(20, 0)); - - // Then shutdown the VM - assert!(dbus_api.remote_command("shutdown", None)); - - // Then boot it again - assert!(http_api.remote_command("boot", None)); - guest.wait_vm_boot(None).unwrap(); + // Resume the VM + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); + assert!(remote_command(&api_socket_restored, "resume", None)); + let latest_events = [ + &MetaEvent { + event: "resuming".to_string(), + device_id: None, + }, + &MetaEvent { + event: "resumed".to_string(), + device_id: None, + }, + ]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path_restored + )); - // Check that the VM booted as expected - assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); - assert!(guest.get_total_memory().unwrap_or_default() > 480_000); + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); + guest.check_devices_common(Some(&socket), Some(&console_text), None); + _test_ivshmem(&guest, &ivshmem_file_path, file_size); }); - + // Shutdown the target VM and check console output kill_child(&mut child); let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); + + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); + }); handle_child_output(r, &output); } #[test] - fn test_api_dbus_create_boot() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + #[cfg(not(feature = "mshv"))] + fn test_live_migration_ivshmem() { + _test_live_migration_ivshmem(false); + } - _test_api_create_boot(TargetApi::new_dbus_api(&guest.tmp_dir), guest) + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_ivshmem_local() { + _test_live_migration_ivshmem(true); } #[test] - fn test_api_dbus_shutdown() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_hotplug_virtiomem() { + snapshot_restore_common::_test_snapshot_restore(true, false); + } - _test_api_shutdown(TargetApi::new_dbus_api(&guest.tmp_dir), guest) + #[test] + #[cfg(not(feature = "mshv"))] // See issue #7437 + fn test_snapshot_restore_basic() { + snapshot_restore_common::_test_snapshot_restore(false, false); } #[test] - fn test_api_dbus_delete() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_with_resume() { + snapshot_restore_common::_test_snapshot_restore(false, true); + } - _test_api_delete(TargetApi::new_dbus_api(&guest.tmp_dir), guest); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_uffd() { + snapshot_restore_common::_test_snapshot_restore_uffd("size=2G", &[], 1_920_000); } #[test] - fn test_api_dbus_pause_resume() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_uffd_shared_memory() { + snapshot_restore_common::_test_snapshot_restore_uffd("size=512M,shared=on", &[], 480_000); + } + + #[test] + #[cfg(not(feature = "mshv"))] // See issue #7437 + #[cfg(target_arch = "x86_64")] + fn test_snapshot_restore_pvpanic() { + snapshot_restore_common::_test_snapshot_restore_devices(true); + } - _test_api_pause_resume(TargetApi::new_dbus_api(&guest.tmp_dir), guest) + #[test] + fn test_virtio_pmem_persist_writes() { + test_virtio_pmem(false, false); } } -mod common_sequential { +#[cfg(not(feature = "mshv"))] +mod snapshot_restore_common { use std::fs::remove_dir_all; + use std::process::Command; use crate::*; - #[test] - #[cfg(not(feature = "mshv"))] - fn test_memory_mergeable_on() { - test_memory_mergeable(true) - } - - fn snapshot_and_check_events(api_socket: &str, snapshot_dir: &str, event_path: &str) { + pub(crate) fn snapshot_and_check_events( + api_socket: &str, + snapshot_dir: &str, + event_path: &str, + ) { // Pause the VM assert!(remote_command(api_socket, "pause", None)); let latest_events: [&MetaEvent; 2] = [ @@ -7303,9 +7675,12 @@ mod common_sequential { device_id: None, }, ]; - // See: #5938 - thread::sleep(std::time::Duration::new(1, 0)); - assert!(check_latest_events_exact(&latest_events, event_path)); + + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + event_path + )); // Take a snapshot from the VM assert!(remote_command( @@ -7314,9 +7689,6 @@ mod common_sequential { Some(format!("file://{snapshot_dir}").as_str()), )); - // Wait to make sure the snapshot is completed - thread::sleep(std::time::Duration::new(10, 0)); - let latest_events = [ &MetaEvent { event: "snapshotting".to_string(), @@ -7327,41 +7699,30 @@ mod common_sequential { device_id: None, }, ]; - // See: #5938 - thread::sleep(std::time::Duration::new(1, 0)); - assert!(check_latest_events_exact(&latest_events, event_path)); - } - - // One thing to note about this test. The virtio-net device is heavily used - // through each ssh command. There's no need to perform a dedicated test to - // verify the migration went well for virtio-net. - #[test] - #[cfg(not(feature = "mshv"))] - fn test_snapshot_restore_hotplug_virtiomem() { - _test_snapshot_restore(true); - } - #[test] - fn test_snapshot_restore_basic() { - _test_snapshot_restore(false); + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + event_path + )); } - fn _test_snapshot_restore(use_hotplug: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + pub(crate) fn _test_snapshot_restore(use_hotplug: bool, use_resume_option: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); let net_id = "net123"; let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 ); - let mut mem_params = "size=2G"; + let mut mem_params = "size=1G"; if use_hotplug { - mem_params = "size=2G,hotplug_method=virtio-mem,hotplug_size=32G" + mem_params = "size=2G,hotplug_method=virtio-mem,hotplug_size=32G"; } let cloudinit_params = format!( @@ -7400,12 +7761,17 @@ mod common_sequential { let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); // Check the number of vCPUs assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 1_920_000); + let total_memory = guest.get_total_memory().unwrap_or_default(); + if use_hotplug { + assert!(total_memory > 1_900_000, "total memory: {total_memory}"); + } else { + assert!(total_memory > 900_000, "total memory: {total_memory}"); + } if use_hotplug { // Increase guest RAM with virtio-mem resize_command( @@ -7427,8 +7793,8 @@ mod common_sequential { ); thread::sleep(std::time::Duration::new(5, 0)); let total_memory = guest.get_total_memory().unwrap_or_default(); - assert!(total_memory > 4_800_000); - assert!(total_memory < 5_760_000); + assert!(total_memory > 4_800_000, "total_memory is {total_memory}"); + assert!(total_memory < 5_760_000, "total_memory is {total_memory}"); } // Check the guest virtio-devices, e.g. block, rng, vsock, console, and net guest.check_devices_common(Some(&socket), Some(&console_text), None); @@ -7447,14 +7813,15 @@ mod common_sequential { "remove-device", Some(net_id), )); - thread::sleep(std::time::Duration::new(10, 0)); let latest_events = [&MetaEvent { event: "device-removed".to_string(), device_id: Some(net_id.to_string()), }]; - // See: #5938 - thread::sleep(std::time::Duration::new(1, 0)); - assert!(check_latest_events_exact(&latest_events, &event_path)); + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path + )); // Plug the virtio-net device again assert!(remote_command( @@ -7465,7 +7832,11 @@ mod common_sequential { thread::sleep(std::time::Duration::new(10, 0)); } - snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); + snapshot_restore_common::snapshot_and_check_events( + &api_socket_source, + &snapshot_dir, + &event_path, + ); }); // Shutdown the source VM and check console output @@ -7498,14 +7869,12 @@ mod common_sequential { ]) .args([ "--restore", - format!("source_url=file://{snapshot_dir}").as_str(), + format!("source_url=file://{snapshot_dir},resume={use_resume_option}").as_str(), ]) .capture_output() .spawn() .unwrap(); - // Wait for the VM to be restored - thread::sleep(std::time::Duration::new(20, 0)); let expected_events = [ &MetaEvent { event: "starting".to_string(), @@ -7524,32 +7893,17 @@ mod common_sequential { device_id: None, }, ]; - assert!(check_sequential_events( + assert!(wait_for_sequential_events( + Duration::from_secs(30), &expected_events, &event_path_restored )); - let latest_events = [&MetaEvent { - event: "restored".to_string(), - device_id: None, - }]; - assert!(check_latest_events_exact( - &latest_events, - &event_path_restored - )); - - // Remove the snapshot dir - let _ = remove_dir_all(snapshot_dir.as_str()); - - let r = std::panic::catch_unwind(|| { - // Resume the VM - assert!(remote_command(&api_socket_restored, "resume", None)); - // There is no way that we can ensure the 'write()' to the - // event file is completed when the 'resume' request is - // returned successfully, because the 'write()' was done - // asynchronously from a different thread of Cloud - // Hypervisor (e.g. the event-monitor thread). - thread::sleep(std::time::Duration::new(1, 0)); + if use_resume_option { let latest_events = [ + &MetaEvent { + event: "restored".to_string(), + device_id: None, + }, &MetaEvent { event: "resuming".to_string(), device_id: None, @@ -7559,19 +7913,69 @@ mod common_sequential { device_id: None, }, ]; - assert!(check_latest_events_exact( + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path_restored + )); + } else { + let latest_events = [&MetaEvent { + event: "restored".to_string(), + device_id: None, + }]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), &latest_events, &event_path_restored )); + } + + // Wait until the restored VM API is ready before issuing follow-up requests. + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); + + // Remove the snapshot dir + let _ = remove_dir_all(snapshot_dir.as_str()); + + let r = std::panic::catch_unwind(|| { + if use_resume_option { + // VM was automatically resumed via restore option, just wait for events + thread::sleep(std::time::Duration::new(1, 0)); + } else { + // Resume the VM manually + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); + assert!(remote_command(&api_socket_restored, "resume", None)); + + let latest_events = [ + &MetaEvent { + event: "resuming".to_string(), + device_id: None, + }, + &MetaEvent { + event: "resumed".to_string(), + device_id: None, + }, + ]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path_restored + )); + } // Perform same checks to validate VM has been properly restored assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); let total_memory = guest.get_total_memory().unwrap_or_default(); - if !use_hotplug { - assert!(total_memory > 1_920_000); - } else { - assert!(total_memory > 4_800_000); - assert!(total_memory < 5_760_000); + if use_hotplug { + assert!(total_memory > 4_800_000, "total_memory is {total_memory}"); + assert!(total_memory < 5_760_000, "total_memory is {total_memory}"); // Deflate balloon to restore entire RAM to the VM resize_command(&api_socket_restored, None, None, Some(0), None); thread::sleep(std::time::Duration::new(5, 0)); @@ -7580,8 +7984,10 @@ mod common_sequential { resize_command(&api_socket_restored, None, Some(5 << 30), None, None); thread::sleep(std::time::Duration::new(5, 0)); let total_memory = guest.get_total_memory().unwrap_or_default(); - assert!(total_memory > 4_800_000); - assert!(total_memory < 5_760_000); + assert!(total_memory > 4_800_000, "total_memory is {total_memory}"); + assert!(total_memory < 5_760_000, "total_memory is {total_memory}"); + } else { + assert!(total_memory > 900_000, "total memory: {total_memory}"); } guest.check_devices_common(Some(&socket), Some(&console_text), None); @@ -7598,95 +8004,54 @@ mod common_sequential { handle_child_output(r, &output); } - #[test] - #[cfg_attr(target_arch = "aarch64", ignore = "See #6970")] - fn test_snapshot_restore_with_fd() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + pub(crate) fn _test_snapshot_restore_uffd( + memory_config: &str, + memory_zone_config: &[&str], + min_total_memory_kib: u32, + ) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); - let net_id = "net123"; - let num_queue_pairs: usize = 2; - // use a name that does not conflict with tap dev created from other tests - let tap_name = "chtap999"; - use std::str::FromStr; - let taps = net_util::open_tap( - Some(tap_name), - Some(std::net::IpAddr::V4( - std::net::Ipv4Addr::from_str(&guest.network.host_ip).unwrap(), - )), - None, - &mut None, - None, - num_queue_pairs, - Some(libc::O_RDWR | libc::O_NONBLOCK), - ) - .unwrap(); - let net_params = format!( - "id={},fd=[{},{}],mac={},ip={},mask=255.255.255.0,num_queues={}", - net_id, - taps[0].as_raw_fd(), - taps[1].as_raw_fd(), - guest.network.guest_mac, - guest.network.host_ip, - num_queue_pairs * 2 - ); - - let cloudinit_params = format!( - "path={},iommu=on", - guest.disk_config.disk(DiskType::CloudInit).unwrap() - ); - - let n_cpu = 2; + let console_text = String::from("On a branch floating down river a cricket, singing."); + let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); + let socket = temp_vsock_path(&guest.tmp_dir); let event_path = temp_event_monitor_path(&guest.tmp_dir); - let mut child = GuestCommand::new(&guest) + let mut source_cmd = GuestCommand::new(&guest); + source_cmd .args(["--api-socket", &api_socket_source]) .args(["--event-monitor", format!("path={event_path}").as_str()]) - .args(["--cpus", format!("boot={n_cpu}").as_str()]) - .args(["--memory", "size=1G"]) + .args(["--cpus", "boot=4"]) + .args(["--memory", memory_config]); + + if !memory_zone_config.is_empty() { + source_cmd.args(["--memory-zone"]).args(memory_zone_config); + } + + let mut child = source_cmd .args(["--kernel", kernel_path.to_str().unwrap()]) - .args([ - "--disk", - format!( - "path={}", - guest.disk_config.disk(DiskType::OperatingSystem).unwrap() - ) - .as_str(), - cloudinit_params.as_str(), - ]) - .args(["--net", net_params.as_str()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .default_net() + .args(["--vsock", format!("cid=3,socket={socket}").as_str()]) .capture_output() .spawn() .unwrap(); - let console_text = String::from("On a branch floating down river a cricket, singing."); - // Create the snapshot directory - let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // close the fds after VM boots, as CH duplicates them before using - for tap in taps.iter() { - unsafe { libc::close(tap.as_raw_fd()) }; - } + guest.wait_vm_boot().unwrap(); - // Check the number of vCPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu); - // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); + assert!(guest.get_total_memory().unwrap_or_default() > min_total_memory_kib); - // Check the guest virtio-devices, e.g. block, rng, vsock, console, and net - guest.check_devices_common(None, Some(&console_text), None); + guest.check_devices_common(Some(&socket), Some(&console_text), None); snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); }); - // Shutdown the source VM and check console output kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); @@ -7694,100 +8059,50 @@ mod common_sequential { let r = std::panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); - handle_child_output(r, &output); + Command::new("rm") + .arg("-f") + .arg(socket.as_str()) + .output() + .unwrap(); + let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); - // Restore the VM from the snapshot let mut child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket_restored]) .args([ "--event-monitor", format!("path={event_path_restored}").as_str(), ]) + .args([ + "--restore", + format!("source_url=file://{snapshot_dir},memory_restore_mode=ondemand").as_str(), + ]) .capture_output() .spawn() .unwrap(); - thread::sleep(std::time::Duration::new(2, 0)); - - let taps = net_util::open_tap( - Some(tap_name), - Some(std::net::IpAddr::V4( - std::net::Ipv4Addr::from_str(&guest.network.host_ip).unwrap(), - )), - None, - &mut None, - None, - num_queue_pairs, - Some(libc::O_RDWR | libc::O_NONBLOCK), - ) - .unwrap(); - let restore_params = format!( - "source_url=file://{},net_fds=[{}@[{},{}]]", - snapshot_dir, - net_id, - taps[0].as_raw_fd(), - taps[1].as_raw_fd() - ); - assert!(remote_command( - &api_socket_restored, - "restore", - Some(restore_params.as_str()) - )); - - // Wait for the VM to be restored - thread::sleep(std::time::Duration::new(20, 0)); - // close the fds as CH duplicates them before using - for tap in taps.iter() { - unsafe { libc::close(tap.as_raw_fd()) }; - } - - let expected_events = [ - &MetaEvent { - event: "starting".to_string(), - device_id: None, - }, - &MetaEvent { - event: "activated".to_string(), - device_id: Some("__console".to_string()), - }, - &MetaEvent { - event: "activated".to_string(), - device_id: Some("__rng".to_string()), - }, - &MetaEvent { - event: "restoring".to_string(), - device_id: None, - }, - ]; - assert!(check_sequential_events( - &expected_events, - &event_path_restored - )); let latest_events = [&MetaEvent { event: "restored".to_string(), device_id: None, }]; - assert!(check_latest_events_exact( + + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), &latest_events, &event_path_restored )); - // Remove the snapshot dir - let _ = remove_dir_all(snapshot_dir.as_str()); - let r = std::panic::catch_unwind(|| { - // Resume the VM + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); assert!(remote_command(&api_socket_restored, "resume", None)); - // There is no way that we can ensure the 'write()' to the - // event file is completed when the 'resume' request is - // returned successfully, because the 'write()' was done - // asynchronously from a different thread of Cloud - // Hypervisor (e.g. the event-monitor thread). - thread::sleep(std::time::Duration::new(1, 0)); + let latest_events = [ &MetaEvent { event: "resuming".to_string(), @@ -7798,38 +8113,43 @@ mod common_sequential { device_id: None, }, ]; - assert!(check_latest_events_exact( + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), &latest_events, &event_path_restored )); - // Perform same checks to validate VM has been properly restored - assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu); - assert!(guest.get_total_memory().unwrap_or_default() > 960_000); + assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); + assert!(guest.get_total_memory().unwrap_or_default() > min_total_memory_kib); - guest.check_devices_common(None, Some(&console_text), None); + guest.check_devices_common(Some(&socket), Some(&console_text), None); }); - // Shutdown the target VM and check console output + kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = std::panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); - }); + let logs = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + logs.contains("UFFD restore: demand-paged restore enabled"), + "Expected UFFD restore path to be enabled. output: {logs}" + ); + }); handle_child_output(r, &output); - } - #[test] - #[cfg(target_arch = "x86_64")] - fn test_snapshot_restore_pvpanic() { - _test_snapshot_restore_devices(true); + let _ = remove_dir_all(snapshot_dir.as_str()); } - fn _test_snapshot_restore_devices(pvpanic: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + pub(crate) fn _test_snapshot_restore_devices(pvpanic: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); @@ -7837,7 +8157,7 @@ mod common_sequential { let device_params = { let mut data = vec![]; if pvpanic { - data.push("--pvpanic"); + data.push(String::from("--pvpanic")); } data }; @@ -7861,24 +8181,20 @@ mod common_sequential { .unwrap(); let console_text = String::from("On a branch floating down river a cricket, singing."); - // Create the snapshot directory let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); - // Check the number of vCPUs assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); }); - // Shutdown the source VM and check console output kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); - // Remove the vsock socket file. Command::new("rm") .arg("-f") .arg(socket.as_str()) @@ -7888,7 +8204,6 @@ mod common_sequential { let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); - // Restore the VM from the snapshot let mut child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket_restored]) .args([ @@ -7903,30 +8218,25 @@ mod common_sequential { .spawn() .unwrap(); - // Wait for the VM to be restored - thread::sleep(std::time::Duration::new(20, 0)); - let latest_events = [&MetaEvent { event: "restored".to_string(), device_id: None, }]; - assert!(check_latest_events_exact( + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), &latest_events, &event_path_restored )); - // Remove the snapshot dir let _ = remove_dir_all(snapshot_dir.as_str()); let r = std::panic::catch_unwind(|| { - // Resume the VM + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); assert!(remote_command(&api_socket_restored, "resume", None)); - // There is no way that we can ensure the 'write()' to the - // event file is completed when the 'resume' request is - // returned successfully, because the 'write()' was done - // asynchronously from a different thread of Cloud - // Hypervisor (e.g. the event-monitor thread). - thread::sleep(std::time::Duration::new(1, 0)); let latest_events = [ &MetaEvent { event: "resuming".to_string(), @@ -7937,19 +8247,17 @@ mod common_sequential { device_id: None, }, ]; - assert!(check_latest_events_exact( + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), &latest_events, &event_path_restored )); - // Check the number of vCPUs assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); guest.check_devices_common(Some(&socket), Some(&console_text), None); if pvpanic { - // Trigger guest a panic make_guest_panic(&guest); - // Wait a while for guest thread::sleep(std::time::Duration::new(10, 0)); let expected_sequential_events = [&MetaEvent { @@ -7962,7 +8270,6 @@ mod common_sequential { )); } }); - // Shutdown the target VM and check console output kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); @@ -7973,2739 +8280,2888 @@ mod common_sequential { handle_child_output(r, &output); } - - #[test] - fn test_virtio_pmem_persist_writes() { - test_virtio_pmem(false, false) - } } -mod windows { - use std::sync::LazyLock; +mod common_sequential { + #[cfg(not(feature = "mshv"))] + use std::fs::remove_dir_all; use crate::*; - static NEXT_DISK_ID: LazyLock> = LazyLock::new(|| Mutex::new(1)); - - struct WindowsGuest { - guest: Guest, - auth: PasswordAuth, - } - - trait FsType { - const FS_FAT: u8; - const FS_NTFS: u8; - } - impl FsType for WindowsGuest { - const FS_FAT: u8 = 0; - const FS_NTFS: u8 = 1; + #[test] + #[cfg(not(feature = "mshv"))] + fn test_memory_mergeable_on() { + test_memory_mergeable(true); } - impl WindowsGuest { - fn new() -> Self { - let disk = WindowsDiskConfig::new(WINDOWS_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(disk)); - let auth = PasswordAuth { - username: String::from("administrator"), - password: String::from("Admin123"), - }; - - WindowsGuest { guest, auth } - } - - fn guest(&self) -> &Guest { - &self.guest - } - - fn ssh_cmd(&self, cmd: &str) -> String { - ssh_command_ip_with_auth( - cmd, - &self.auth, - &self.guest.network.guest_ip, - DEFAULT_SSH_RETRIES, - DEFAULT_SSH_TIMEOUT, - ) - .unwrap() + #[test] + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_uffd_hugepage_zone() { + if !exec_host_command_status( + "grep -q '^Hugepagesize:[[:space:]]*2048 kB' /proc/meminfo && test $(awk '/HugePages_Free/ {print $2}' /proc/meminfo) -ge 256", + ) + .success() + { + println!("SKIPPED: not enough free 2MiB hugepages for UFFD restore test"); + return; } - fn cpu_count(&self) -> u8 { - self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).NumberOfLogicalProcessors\"") - .trim() - .parse::() - .unwrap_or(0) - } + snapshot_restore_common::_test_snapshot_restore_uffd( + "size=0", + &["id=mem0,size=512M,hugepages=on,hugepage_size=2M"], + 480_000, + ); + } - fn ram_size(&self) -> usize { - self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).TotalPhysicalMemory\"") - .trim() - .parse::() - .unwrap_or(0) - } + #[test] + #[cfg(not(feature = "mshv"))] // See issue #7437 + #[ignore = "See #6970"] + fn test_snapshot_restore_with_fd() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - fn netdev_count(&self) -> u8 { - self.ssh_cmd("powershell -Command \"netsh int ipv4 show interfaces | Select-String ethernet | Measure-Object -Line | Format-Table -HideTableHeaders\"") - .trim() - .parse::() - .unwrap_or(0) - } + let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); - fn disk_count(&self) -> u8 { - self.ssh_cmd("powershell -Command \"Get-Disk | Measure-Object -Line | Format-Table -HideTableHeaders\"") - .trim() - .parse::() - .unwrap_or(0) - } + let net_id = "net123"; + let num_queue_pairs: usize = 2; + // use a name that does not conflict with tap dev created from other tests + let tap_name = "chtap999"; + use std::str::FromStr; + let taps = net_util::open_tap( + Some(tap_name), + Some(std::net::IpAddr::V4( + std::net::Ipv4Addr::from_str(&guest.network.host_ip0).unwrap(), + )), + None, + &mut None, + None, + num_queue_pairs, + Some(libc::O_RDWR | libc::O_NONBLOCK), + ) + .unwrap(); + let net_params = format!( + "id={},fd=[{},{}],mac={},ip={},mask=255.255.255.128,num_queues={}", + net_id, + taps[0].as_raw_fd(), + taps[1].as_raw_fd(), + guest.network.guest_mac0, + guest.network.host_ip0, + num_queue_pairs * 2 + ); - fn reboot(&self) { - let _ = self.ssh_cmd("shutdown /r /t 0"); - } + let cloudinit_params = format!( + "path={},iommu=on", + guest.disk_config.disk(DiskType::CloudInit).unwrap() + ); - fn shutdown(&self) { - let _ = self.ssh_cmd("shutdown /s /t 0"); - } + let n_cpu = 2; + let event_path = temp_event_monitor_path(&guest.tmp_dir); - fn run_dnsmasq(&self) -> std::process::Child { - let listen_address = format!("--listen-address={}", self.guest.network.host_ip); - let dhcp_host = format!( - "--dhcp-host={},{}", - self.guest.network.guest_mac, self.guest.network.guest_ip - ); - let dhcp_range = format!( - "--dhcp-range=eth,{},{}", - self.guest.network.guest_ip, self.guest.network.guest_ip - ); + let mut child = GuestCommand::new(&guest) + .args(["--api-socket", &api_socket_source]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .args(["--cpus", format!("boot={n_cpu}").as_str()]) + .args(["--memory", "size=1G"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args([ + "--disk", + format!( + "path={}", + guest.disk_config.disk(DiskType::OperatingSystem).unwrap() + ) + .as_str(), + cloudinit_params.as_str(), + ]) + .args(["--net", net_params.as_str()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .capture_output() + .spawn() + .unwrap(); - Command::new("dnsmasq") - .arg("--no-daemon") - .arg("--log-queries") - .arg(listen_address.as_str()) - .arg("--except-interface=lo") - .arg("--bind-dynamic") // Allow listening to host_ip while the interface is not ready yet. - .arg("--conf-file=/dev/null") - .arg(dhcp_host.as_str()) - .arg(dhcp_range.as_str()) - .spawn() - .unwrap() - } + let console_text = String::from("On a branch floating down river a cricket, singing."); + // Create the snapshot directory + let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); - // TODO Cleanup image file explicitly after test, if there's some space issues. - fn disk_new(&self, fs: u8, sz: usize) -> String { - let mut guard = NEXT_DISK_ID.lock().unwrap(); - let id = *guard; - *guard = id + 1; + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - let img = PathBuf::from(format!("/tmp/test-hotplug-{id}.raw")); - let _ = fs::remove_file(&img); + // close the fds after VM boots, as CH duplicates them before using + for tap in taps.iter() { + unsafe { libc::close(tap.as_raw_fd()) }; + } - // Create an image file - let out = Command::new("qemu-img") - .args([ - "create", - "-f", - "raw", - img.to_str().unwrap(), - format!("{sz}m").as_str(), - ]) - .output() - .expect("qemu-img command failed") - .stdout; - println!("{out:?}"); + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu); + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 960_000); - // Associate image to a loop device - let out = Command::new("losetup") - .args(["--show", "-f", img.to_str().unwrap()]) - .output() - .expect("failed to create loop device") - .stdout; - let _tmp = String::from_utf8_lossy(&out); - let loop_dev = _tmp.trim(); - println!("{out:?}"); + // Check the guest virtio-devices, e.g. block, rng, vsock, console, and net + guest.check_devices_common(None, Some(&console_text), None); - // Create a partition table - // echo 'type=7' | sudo sfdisk "${LOOP}" - let mut child = Command::new("sfdisk") - .args([loop_dev]) - .stdin(Stdio::piped()) - .spawn() - .unwrap(); - let stdin = child.stdin.as_mut().expect("failed to open stdin"); - stdin - .write_all("type=7".as_bytes()) - .expect("failed to write stdin"); - let out = child.wait_with_output().expect("sfdisk failed").stdout; - println!("{out:?}"); + snapshot_restore_common::snapshot_and_check_events( + &api_socket_source, + &snapshot_dir, + &event_path, + ); + }); - // Disengage the loop device - let out = Command::new("losetup") - .args(["-d", loop_dev]) - .output() - .expect("loop device not found") - .stdout; - println!("{out:?}"); + // Shutdown the source VM and check console output + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); - // Re-associate loop device pointing to the partition only - let out = Command::new("losetup") - .args([ - "--show", - "--offset", - (512 * 2048).to_string().as_str(), - "-f", - img.to_str().unwrap(), - ]) - .output() - .expect("failed to create loop device") - .stdout; - let _tmp = String::from_utf8_lossy(&out); - let loop_dev = _tmp.trim(); - println!("{out:?}"); + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); + }); - // Create filesystem. - let fs_cmd = match fs { - WindowsGuest::FS_FAT => "mkfs.msdos", - WindowsGuest::FS_NTFS => "mkfs.ntfs", - _ => panic!("Unknown filesystem type '{fs}'"), - }; - let out = Command::new(fs_cmd) - .args([&loop_dev]) - .output() - .unwrap_or_else(|_| panic!("{fs_cmd} failed")) - .stdout; - println!("{out:?}"); + handle_child_output(r, &output); - // Disengage the loop device - let out = Command::new("losetup") - .args(["-d", loop_dev]) - .output() - .unwrap_or_else(|_| panic!("loop device '{loop_dev}' not found")) - .stdout; - println!("{out:?}"); + let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); + let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); - img.to_str().unwrap().to_string() - } + // Restore the VM from the snapshot + let mut child = GuestCommand::new(&guest) + .args(["--api-socket", &api_socket_restored]) + .args([ + "--event-monitor", + format!("path={event_path_restored}").as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); + thread::sleep(std::time::Duration::new(2, 0)); - fn disks_set_rw(&self) { - let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsReadOnly $False\""); - } + let taps = net_util::open_tap( + Some(tap_name), + Some(std::net::IpAddr::V4( + std::net::Ipv4Addr::from_str(&guest.network.host_ip0).unwrap(), + )), + None, + &mut None, + None, + num_queue_pairs, + Some(libc::O_RDWR | libc::O_NONBLOCK), + ) + .unwrap(); + let restore_params = format!( + "source_url=file://{},net_fds=[{}@[{},{}]]", + snapshot_dir, + net_id, + taps[0].as_raw_fd(), + taps[1].as_raw_fd() + ); + assert!(remote_command( + &api_socket_restored, + "restore", + Some(restore_params.as_str()) + )); - fn disks_online(&self) { - let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsOffline $False\""); - } + // Wait for the VM to be restored + assert!(wait_until(Duration::from_secs(20), || { + remote_command(&api_socket_restored, "info", None) + })); - fn disk_file_put(&self, fname: &str, data: &str) { - let _ = self.ssh_cmd(&format!( - "powershell -Command \"'{data}' | Set-Content -Path {fname}\"" - )); + // close the fds as CH duplicates them before using + for tap in taps.iter() { + unsafe { libc::close(tap.as_raw_fd()) }; } - fn disk_file_read(&self, fname: &str) -> String { - self.ssh_cmd(&format!( - "powershell -Command \"Get-Content -Path {fname}\"" - )) - } + let expected_events = [ + &MetaEvent { + event: "starting".to_string(), + device_id: None, + }, + &MetaEvent { + event: "activated".to_string(), + device_id: Some("__console".to_string()), + }, + &MetaEvent { + event: "activated".to_string(), + device_id: Some("__rng".to_string()), + }, + &MetaEvent { + event: "restoring".to_string(), + device_id: None, + }, + ]; + // Wait for the restore event sequence to be recorded. + assert!(wait_for_sequential_events( + Duration::from_secs(30), + &expected_events, + &event_path_restored + )); + let latest_events = [&MetaEvent { + event: "restored".to_string(), + device_id: None, + }]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path_restored + )); - fn wait_for_boot(&self) -> bool { - let cmd = "dir /b c:\\ | find \"Windows\""; - let tmo_max = 180; - // The timeout increase by n*1+n*2+n*3+..., therefore the initial - // interval must be small. - let tmo_int = 2; - let out = ssh_command_ip_with_auth( - cmd, - &self.auth, - &self.guest.network.guest_ip, - { - let mut ret = 1; - let mut tmo_acc = 0; - loop { - tmo_acc += tmo_int * ret; - if tmo_acc >= tmo_max { - break; - } - ret += 1; - } - ret - }, - tmo_int, - ) - .unwrap(); + // Remove the snapshot dir + let _ = remove_dir_all(snapshot_dir.as_str()); - if "Windows" == out.trim() { - return true; - } + let r = std::panic::catch_unwind(|| { + // Resume the VM + assert!(wait_until(Duration::from_secs(20), || remote_command( + &api_socket_restored, + "info", + None + ))); + assert!(remote_command(&api_socket_restored, "resume", None)); - false - } - } + let latest_events = [ + &MetaEvent { + event: "resuming".to_string(), + device_id: None, + }, + &MetaEvent { + event: "resumed".to_string(), + device_id: None, + }, + ]; + assert!(wait_for_latest_events_exact( + Duration::from_secs(30), + &latest_events, + &event_path_restored + )); - fn vcpu_threads_count(pid: u32) -> u8 { - // ps -T -p 12345 | grep vcpu | wc -l - let out = Command::new("ps") - .args(["-T", "-p", format!("{pid}").as_str()]) - .output() - .expect("ps command failed") - .stdout; - String::from_utf8_lossy(&out).matches("vcpu").count() as u8 - } + // Perform same checks to validate VM has been properly restored + assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu); + assert!(guest.get_total_memory().unwrap_or_default() > 960_000); - fn netdev_ctrl_threads_count(pid: u32) -> u8 { - // ps -T -p 12345 | grep "_net[0-9]*_ctrl" | wc -l - let out = Command::new("ps") - .args(["-T", "-p", format!("{pid}").as_str()]) - .output() - .expect("ps command failed") - .stdout; - let mut n = 0; - String::from_utf8_lossy(&out) - .split_whitespace() - .for_each(|s| n += (s.starts_with("_net") && s.ends_with("_ctrl")) as u8); // _net1_ctrl - n - } + guest.check_devices_common(None, Some(&console_text), None); + }); + // Shutdown the target VM and check console output + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); - fn disk_ctrl_threads_count(pid: u32) -> u8 { - // ps -T -p 15782 | grep "_disk[0-9]*_q0" | wc -l - let out = Command::new("ps") - .args(["-T", "-p", format!("{pid}").as_str()]) - .output() - .expect("ps command failed") - .stdout; - let mut n = 0; - String::from_utf8_lossy(&out) - .split_whitespace() - .for_each(|s| n += (s.starts_with("_disk") && s.ends_with("_q0")) as u8); // _disk0_q0, don't care about multiple queues as they're related to the same hdd - n + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); + }); + + handle_child_output(r, &output); } #[test] - fn test_windows_guest() { - let windows_guest = WindowsGuest::new(); + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_virtio_fs() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", edk2_path().to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) + let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); + + let mut workload_path = dirs::home_dir().unwrap(); + workload_path.push("workloads"); + let mut shared_dir = workload_path; + shared_dir.push("shared_dir"); + + let (mut daemon_child, virtiofsd_socket_path) = + prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap()); + + let event_path = temp_event_monitor_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(&guest) + .args(["--api-socket", &api_socket_source]) + .args(["--event-monitor", format!("path={event_path}").as_str()]) + .args(["--cpus", "boot=2"]) + .args(["--memory", "size=512M,shared=on"]) + .args(["--kernel", kernel_path.to_str().unwrap()]) .default_disks() .default_net() + .args([ + "--fs", + format!("socket={virtiofsd_socket_path},tag=myfs,num_queues=1,queue_size=1024") + .as_str(), + ]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .capture_output() .spawn() .unwrap(); - let fd = child.stdout.as_ref().unwrap().as_raw_fd(); - let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - let fd = child.stderr.as_ref().unwrap().as_raw_fd(); - let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; + let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); - assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - let mut child_dnsmasq = windows_guest.run_dnsmasq(); + // Mount virtiofs and write a test file + guest + .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") + .unwrap(); - let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); + // Verify the shared directory is accessible + assert_eq!( + guest.ssh_command("cat mount_dir/file1").unwrap().trim(), + "foo" + ); - windows_guest.shutdown(); + // Write a file from the guest + guest + .ssh_command( + "sudo bash -c 'echo snapshot_test_data > mount_dir/snapshot_test_file'", + ) + .unwrap(); + snapshot_restore_common::snapshot_and_check_events( + &api_socket_source, + &snapshot_dir, + &event_path, + ); }); - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); + // Shutdown the source VM + kill_child(&mut child); let output = child.wait_with_output().unwrap(); + handle_child_output(r, &output); - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + // Kill the old virtiofsd + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + + // Start a fresh virtiofsd (reusing the same socket path) + let (mut daemon_child, _) = prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap()); + + let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); + let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); + + // Restore the VM from the snapshot + let mut child = GuestCommand::new(&guest) + .args(["--api-socket", &api_socket_restored]) + .args([ + "--event-monitor", + format!("path={event_path_restored}").as_str(), + ]) + .args([ + "--restore", + format!("source_url=file://{snapshot_dir}").as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); + + // Wait for the VM to be restored + assert!(wait_until(Duration::from_secs(30), || { + remote_command(&api_socket_restored, "info", None) + })); + + let latest_events = [&MetaEvent { + event: "restored".to_string(), + device_id: None, + }]; + assert!(check_latest_events_exact( + &latest_events, + &event_path_restored + )); + + // Remove the snapshot dir + let _ = remove_dir_all(snapshot_dir.as_str()); + + let r = std::panic::catch_unwind(|| { + // Resume the VM + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); + assert!(remote_command(&api_socket_restored, "resume", None)); + thread::sleep(std::time::Duration::new(5, 0)); + + // Verify virtiofs still works after restore + // Read the file written before snapshot + assert_eq!( + guest + .ssh_command("cat mount_dir/snapshot_test_file") + .unwrap() + .trim(), + "snapshot_test_data" + ); + + // Read the pre-existing shared file + assert_eq!( + guest.ssh_command("cat mount_dir/file1").unwrap().trim(), + "foo" + ); + // Write a new file after restore + guest + .ssh_command("sudo bash -c 'echo post_restore_data > mount_dir/post_restore_file'") + .unwrap(); + + // Verify the new file exists on the host + let post_restore_content = + std::fs::read_to_string(shared_dir.join("post_restore_file")).unwrap(); + assert_eq!(post_restore_content.trim(), "post_restore_data"); + }); + + // Shutdown the target VM + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); + + // Clean up virtiofsd and test files + let _ = daemon_child.kill(); + let _ = daemon_child.wait(); + let _ = std::fs::remove_file(shared_dir.join("snapshot_test_file")); + let _ = std::fs::remove_file(shared_dir.join("post_restore_file")); } - #[test] - fn test_windows_guest_multiple_queues() { - let windows_guest = WindowsGuest::new(); + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_balloon(upgrade_test: bool, local: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + let console_text = String::from("On a branch floating down river a cricket, singing."); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); + let memory_param: &[&str] = if local { + &[ + "--memory", + "size=4G,hotplug_method=virtio-mem,hotplug_size=8G,shared=on", + "--balloon", + "size=0", + ] + } else { + &[ + "--memory", + "size=4G,hotplug_method=virtio-mem,hotplug_size=8G", + "--balloon", + "size=0", + ] + }; - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--cpus", "boot=4,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) + let boot_vcpus = 2; + let max_vcpus = 4; + + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + let pmem_path = String::from("/dev/pmem0"); + + // Start the source VM + let src_vm_path = if upgrade_test { + cloud_hypervisor_release_path() + } else { + clh_command("cloud-hypervisor") + }; + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd .args([ - "--disk", - format!( - "path={},num_queues=4", - windows_guest - .guest() - .disk_config - .disk(DiskType::OperatingSystem) - .unwrap() - ) - .as_str(), + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), ]) + .args(memory_param) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .default_disks() + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) .args([ - "--net", - format!( - "tap=,mac={},ip={},mask=255.255.255.0,num_queues=8", - windows_guest.guest().network.guest_mac, - windows_guest.guest().network.host_ip - ) - .as_str(), - ]) + "--pmem", + format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), + ]); + let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); + + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) .capture_output() .spawn() .unwrap(); - let fd = child.stdout.as_ref().unwrap().as_raw_fd(); - let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - let fd = child.stderr.as_ref().unwrap().as_raw_fd(); - let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + + // Make sure the source VM is functional + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + // Increase the guest RAM + resize_command(&src_api_socket, None, Some(6 << 30), None, None); + assert!(wait_until(Duration::from_secs(30), || { + guest.get_total_memory().unwrap_or_default() > 5_760_000 + })); + assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); + // Use balloon to remove RAM from the VM + resize_command(&src_api_socket, None, None, Some(1 << 30), None); + assert!(wait_until(Duration::from_secs(5), || { + let total_memory = guest.get_total_memory().unwrap_or_default(); + total_memory > 4_800_000 && total_memory < 5_760_000 + })); + let total_memory = guest.get_total_memory().unwrap_or_default(); + assert!(total_memory > 4_800_000); + assert!(total_memory < 5_760_000); + + // Check the guest virtio-devices, e.g. block, rng, console, and net + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + + // x86_64: Following what's done in the `test_snapshot_restore`, we need + // to make sure that removing and adding back the virtio-net device does + // not break the live-migration support for virtio-pci. + #[cfg(target_arch = "x86_64")] + { + assert!(remote_command( + &src_api_socket, + "remove-device", + Some(net_id), + )); + assert!(wait_until(Duration::from_secs(10), || { + guest.wait_for_ssh(Duration::from_secs(1)).is_err() + })); + + // Plug the virtio-net device again + assert!(remote_command( + &src_api_socket, + "add-net", + Some(net_params.as_str()), + )); + guest.wait_for_ssh(Duration::from_secs(10)).unwrap(); + } - assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); + // Start the live-migration + let migration_socket = String::from( + guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - let mut child_dnsmasq = windows_guest.run_dnsmasq(); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); + }); + + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } + + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } + // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); + // Perform same checks to validate VM has been properly migrated + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - windows_guest.shutdown(); - }); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + // Perform checks on guest RAM using balloon + let total_memory = guest.get_total_memory().unwrap_or_default(); + assert!(total_memory > 4_800_000); + assert!(total_memory < 5_760_000); + // Deflate balloon to restore entire RAM to the VM + resize_command(&dest_api_socket, None, None, Some(0), None); + thread::sleep(std::time::Duration::new(5, 0)); + assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); + // Decrease guest RAM with virtio-mem + resize_command(&dest_api_socket, None, Some(5 << 30), None, None); + thread::sleep(std::time::Duration::new(5, 0)); + let total_memory = guest.get_total_memory().unwrap_or_default(); + assert!(total_memory > 4_800_000); + assert!(total_memory < 5_760_000); + }); - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + // Clean-up the destination VM and make sure it terminated correctly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); - handle_child_output(r, &output); + // Check the destination VM has the expected 'console_text' from its output + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + }); + handle_child_output(r, &dest_output); } - #[test] #[cfg(not(feature = "mshv"))] - #[ignore = "See #4327"] - fn test_windows_guest_snapshot_restore() { - let windows_guest = WindowsGuest::new(); + fn _test_live_migration_numa(upgrade_test: bool, local: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + let console_text = String::from("On a branch floating down river a cricket, singing."); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); + let memory_param: &[&str] = if local { + &[ + "--memory", + "size=0,hotplug_method=virtio-mem,shared=on", + "--memory-zone", + "id=mem0,size=1G,hotplug_size=4G,shared=on", + "id=mem1,size=1G,hotplug_size=4G,shared=on", + "id=mem2,size=2G,hotplug_size=4G,shared=on", + "--numa", + "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", + "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", + "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", + ] + } else { + &[ + "--memory", + "size=0,hotplug_method=virtio-mem", + "--memory-zone", + "id=mem0,size=1G,hotplug_size=4G", + "id=mem1,size=1G,hotplug_size=4G", + "id=mem2,size=2G,hotplug_size=4G", + "--numa", + "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", + "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", + "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", + ] + }; - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket_source = format!("{}.1", temp_api_path(&tmp_dir)); + let boot_vcpus = 6; + let max_vcpus = 12; - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket_source]) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + let pmem_path = String::from("/dev/pmem0"); + + // Start the source VM + let src_vm_path = if upgrade_test { + cloud_hypervisor_release_path() + } else { + clh_command("cloud-hypervisor") + }; + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd + .args([ + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + ]) + .args(memory_param) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .default_net() + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) + .args([ + "--pmem", + format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), + ]); + let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); + + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) .capture_output() .spawn() .unwrap(); - let fd = child.stdout.as_ref().unwrap().as_raw_fd(); - let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - let fd = child.stderr.as_ref().unwrap().as_raw_fd(); - let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - - assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); - - let mut child_dnsmasq = windows_guest.run_dnsmasq(); - - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); - - let snapshot_dir = temp_snapshot_dir_path(&tmp_dir); - - // Pause the VM - assert!(remote_command(&api_socket_source, "pause", None)); - - // Take a snapshot from the VM - assert!(remote_command( - &api_socket_source, - "snapshot", - Some(format!("file://{snapshot_dir}").as_str()), - )); + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - // Wait to make sure the snapshot is completed - thread::sleep(std::time::Duration::new(30, 0)); + // Make sure the source VM is functional + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - let _ = child.kill(); - child.wait().unwrap(); + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); - let api_socket_restored = format!("{}.2", temp_api_path(&tmp_dir)); + // Check the guest virtio-devices, e.g. block, rng, console, and net + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - // Restore the VM from the snapshot - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket_restored]) - .args([ - "--restore", - format!("source_url=file://{snapshot_dir}").as_str(), - ]) - .capture_output() - .spawn() - .unwrap(); + // Check the NUMA parameters are applied correctly and resize + // each zone to test the case where we migrate a VM with the + // virtio-mem regions being used. + { + guest.check_numa_common( + Some(&[960_000, 960_000, 1_920_000]), + Some(&[&[0, 1, 2], &[3, 4], &[5]]), + Some(&["10 15 20", "20 10 25", "25 30 10"]), + ); - // Wait for the VM to be restored - thread::sleep(std::time::Duration::new(20, 0)); + // AArch64 currently does not support hotplug, and therefore we only + // test hotplug-related function on x86_64 here. + #[cfg(target_arch = "x86_64")] + { + guest.enable_memory_hotplug(); - let r = std::panic::catch_unwind(|| { - // Resume the VM - assert!(remote_command(&api_socket_restored, "resume", None)); + // Resize every memory zone and check each associated NUMA node + // has been assigned the right amount of memory. + resize_zone_command(&src_api_socket, "mem0", "2G"); + resize_zone_command(&src_api_socket, "mem1", "2G"); + resize_zone_command(&src_api_socket, "mem2", "3G"); + thread::sleep(std::time::Duration::new(5, 0)); - windows_guest.shutdown(); - }); + guest.check_numa_common(Some(&[1_920_000, 1_920_000, 1_920_000]), None, None); + } + } - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + // x86_64: Following what's done in the `test_snapshot_restore`, we need + // to make sure that removing and adding back the virtio-net device does + // not break the live-migration support for virtio-pci. + #[cfg(target_arch = "x86_64")] + { + assert!(remote_command( + &src_api_socket, + "remove-device", + Some(net_id), + )); + assert!(wait_until(Duration::from_secs(10), || { + guest.wait_for_ssh(Duration::from_secs(1)).is_err() + })); - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + // Plug the virtio-net device again + assert!(remote_command( + &src_api_socket, + "add-net", + Some(net_params.as_str()), + )); + guest.wait_for_ssh(Duration::from_secs(10)).unwrap(); + } - handle_child_output(r, &output); - } + // Start the live-migration + let migration_socket = String::from( + guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - #[test] - #[cfg(not(feature = "mshv"))] - #[cfg(not(target_arch = "aarch64"))] - fn test_windows_guest_cpu_hotplug() { - let windows_guest = WindowsGuest::new(); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); + }); - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket = temp_api_path(&tmp_dir); + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=2,max=8,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + // Post live-migration check to make sure the destination VM is functional + let r = std::panic::catch_unwind(|| { + // Perform same checks to validate VM has been properly migrated + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + #[cfg(target_arch = "x86_64")] + assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000); + #[cfg(target_arch = "aarch64")] + assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - let mut child_dnsmasq = windows_guest.run_dnsmasq(); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); + // Perform NUMA related checks + { + #[cfg(target_arch = "aarch64")] + { + guest.check_numa_common( + Some(&[960_000, 960_000, 1_920_000]), + Some(&[&[0, 1, 2], &[3, 4], &[5]]), + Some(&["10 15 20", "20 10 25", "25 30 10"]), + ); + } - let vcpu_num = 2; - // Check the initial number of CPUs the guest sees - assert_eq!(windows_guest.cpu_count(), vcpu_num); - // Check the initial number of vcpu threads in the CH process - assert_eq!(vcpu_threads_count(child.id()), vcpu_num); + // AArch64 currently does not support hotplug, and therefore we only + // test hotplug-related function on x86_64 here. + #[cfg(target_arch = "x86_64")] + { + guest.check_numa_common( + Some(&[1_920_000, 1_920_000, 2_880_000]), + Some(&[&[0, 1, 2], &[3, 4], &[5]]), + Some(&["10 15 20", "20 10 25", "25 30 10"]), + ); - let vcpu_num = 6; - // Hotplug some CPUs - resize_command(&api_socket, Some(vcpu_num), None, None, None); - // Wait to make sure CPUs are added - thread::sleep(std::time::Duration::new(10, 0)); - // Check the guest sees the correct number - assert_eq!(windows_guest.cpu_count(), vcpu_num); - // Check the CH process has the correct number of vcpu threads - assert_eq!(vcpu_threads_count(child.id()), vcpu_num); + guest.enable_memory_hotplug(); - let vcpu_num = 4; - // Remove some CPUs. Note that Windows doesn't support hot-remove. - resize_command(&api_socket, Some(vcpu_num), None, None, None); - // Wait to make sure CPUs are removed - thread::sleep(std::time::Duration::new(10, 0)); - // Reboot to let Windows catch up - windows_guest.reboot(); - // Wait to make sure Windows completely rebooted - thread::sleep(std::time::Duration::new(60, 0)); - // Check the guest sees the correct number - assert_eq!(windows_guest.cpu_count(), vcpu_num); - // Check the CH process has the correct number of vcpu threads - assert_eq!(vcpu_threads_count(child.id()), vcpu_num); + // Resize every memory zone and check each associated NUMA node + // has been assigned the right amount of memory. + resize_zone_command(&dest_api_socket, "mem0", "4G"); + resize_zone_command(&dest_api_socket, "mem1", "4G"); + resize_zone_command(&dest_api_socket, "mem2", "4G"); + // Resize to the maximum amount of CPUs and check each NUMA + // node has been assigned the right CPUs set. + resize_command(&dest_api_socket, Some(max_vcpus), None, None, None); + thread::sleep(std::time::Duration::new(5, 0)); - windows_guest.shutdown(); + guest.check_numa_common( + Some(&[3_840_000, 3_840_000, 3_840_000]), + Some(&[&[0, 1, 2, 9], &[3, 4, 6, 7, 8], &[5, 10, 11]]), + None, + ); + } + } }); - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); - - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + // Clean-up the destination VM and make sure it terminated correctly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); - handle_child_output(r, &output); + // Check the destination VM has the expected 'console_text' from its output + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + }); + handle_child_output(r, &dest_output); } - #[test] #[cfg(not(feature = "mshv"))] - #[cfg(not(target_arch = "aarch64"))] - fn test_windows_guest_ram_hotplug() { - let windows_guest = WindowsGuest::new(); + fn _test_live_migration_ovs_dpdk(upgrade_test: bool, local: bool) { + let ovs_disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let ovs_guest = Guest::new(Box::new(ovs_disk_config)); - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); + let migration_disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let migration_guest = Guest::new(Box::new(migration_disk_config)); + let src_api_socket = temp_api_path(&migration_guest.tmp_dir); - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket = temp_api_path(&tmp_dir); + // Start two VMs that are connected through ovs-dpdk and one of the VMs is the source VM for live-migration + let (mut ovs_child, mut src_child) = + setup_ovs_dpdk_guests(&ovs_guest, &migration_guest, &src_api_socket, upgrade_test); - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=2G,hotplug_size=5G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) - .default_disks() - .default_net() + // Start the destination VM + let mut dest_api_socket = temp_api_path(&migration_guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&migration_guest) + .args(["--api-socket", &dest_api_socket]) .capture_output() .spawn() .unwrap(); - let mut child_dnsmasq = windows_guest.run_dnsmasq(); - let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); + // Give it '1s' to make sure the 'dest_api_socket' file is properly created + thread::sleep(std::time::Duration::new(1, 0)); - let ram_size = 2 * 1024 * 1024 * 1024; - // Check the initial number of RAM the guest sees - let current_ram_size = windows_guest.ram_size(); - // This size seems to be reserved by the system and thus the - // reported amount differs by this constant value. - let reserved_ram_size = ram_size - current_ram_size; - // Verify that there's not more than 4mb constant diff wasted - // by the reserved ram. - assert!(reserved_ram_size < 4 * 1024 * 1024); + // Start the live-migration + let migration_socket = String::from( + migration_guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - let ram_size = 4 * 1024 * 1024 * 1024; - // Hotplug some RAM - resize_command(&api_socket, None, Some(ram_size), None, None); - // Wait to make sure RAM has been added - thread::sleep(std::time::Duration::new(10, 0)); - // Check the guest sees the correct number - assert_eq!(windows_guest.ram_size(), ram_size - reserved_ram_size); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); + }); - let ram_size = 3 * 1024 * 1024 * 1024; - // Unplug some RAM. Note that hot-remove most likely won't work. - resize_command(&api_socket, None, Some(ram_size), None, None); - // Wait to make sure RAM has been added - thread::sleep(std::time::Duration::new(10, 0)); - // Reboot to let Windows catch up - windows_guest.reboot(); - // Wait to make sure guest completely rebooted - thread::sleep(std::time::Duration::new(60, 0)); - // Check the guest sees the correct number - assert_eq!(windows_guest.ram_size(), ram_size - reserved_ram_size); + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + Some(ovs_child), + "Error occurred during live-migration", + ); + } + + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + Some(ovs_child), + "source VM was not terminated successfully.", + ); + } + + // Post live-migration check to make sure the destination VM is functional + let r = std::panic::catch_unwind(|| { + // Perform same checks to validate VM has been properly migrated + // Spawn a new netcat listener in the OVS VM + let guest_ip = ovs_guest.network.guest_ip0.clone(); + thread::spawn(move || { + ssh_command_ip( + "nc -l 12345", + &guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + }); - windows_guest.shutdown(); + // Wait for the server to be listening + thread::sleep(std::time::Duration::new(5, 0)); + + // And check the connection is still functional after live-migration + migration_guest + .ssh_command("nc -vz 172.100.0.1 12345") + .unwrap(); }); - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + // Clean-up the destination VM and OVS VM, and make sure they terminated correctly + let _ = dest_child.kill(); + let _ = ovs_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + let ovs_output = ovs_child.wait_with_output().unwrap(); - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + cleanup_ovs_dpdk(); - handle_child_output(r, &output); + handle_child_output(r, &dest_output); + handle_child_output(Ok(()), &ovs_output); } + // NUMA and balloon live migration tests run sequentially + #[test] #[cfg(not(feature = "mshv"))] - fn test_windows_guest_netdev_hotplug() { - let windows_guest = WindowsGuest::new(); - - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); - - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket = temp_api_path(&tmp_dir); + fn test_live_migration_balloon() { + _test_live_migration_balloon(false, false); + } - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_balloon_local() { + _test_live_migration_balloon(false, true); + } - let mut child_dnsmasq = windows_guest.run_dnsmasq(); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_balloon() { + _test_live_migration_balloon(true, false); + } - let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_balloon_local() { + _test_live_migration_balloon(true, true); + } - // Initially present network device - let netdev_num = 1; - assert_eq!(windows_guest.netdev_count(), netdev_num); - assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_numa() { + _test_live_migration_numa(false, false); + } - // Hotplug network device - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-net", - Some(windows_guest.guest().default_net_string().as_str()), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_net2\"")); - thread::sleep(std::time::Duration::new(5, 0)); - // Verify the device is on the system - let netdev_num = 2; - assert_eq!(windows_guest.netdev_count(), netdev_num); - assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_numa_local() { + _test_live_migration_numa(false, true); + } - // Remove network device - let cmd_success = remote_command(&api_socket, "remove-device", Some("_net2")); - assert!(cmd_success); - thread::sleep(std::time::Duration::new(5, 0)); - // Verify the device has been removed - let netdev_num = 1; - assert_eq!(windows_guest.netdev_count(), netdev_num); - assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_numa() { + _test_live_migration_numa(true, false); + } - windows_guest.shutdown(); - }); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_numa_local() { + _test_live_migration_numa(true, true); + } - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + // Require to run ovs-dpdk tests sequentially because they rely on the same ovs-dpdk setup + #[test] + #[ignore = "See #5532"] + #[cfg(target_arch = "x86_64")] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_ovs_dpdk() { + _test_live_migration_ovs_dpdk(false, false); + } - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + #[test] + #[ignore = "See #5532 and #7689"] + #[cfg(target_arch = "x86_64")] + #[cfg(not(feature = "mshv"))] + fn test_live_migration_ovs_dpdk_local() { + _test_live_migration_ovs_dpdk(false, true); + } - handle_child_output(r, &output); + #[test] + #[ignore = "See #5532"] + #[cfg(target_arch = "x86_64")] + #[cfg(not(feature = "mshv"))] + fn test_live_upgrade_ovs_dpdk() { + _test_live_migration_ovs_dpdk(true, false); } #[test] - #[ignore = "See #6037"] + #[ignore = "See #5532"] + #[cfg(target_arch = "x86_64")] #[cfg(not(feature = "mshv"))] - #[cfg(not(target_arch = "aarch64"))] - fn test_windows_guest_disk_hotplug() { - let windows_guest = WindowsGuest::new(); + fn test_live_upgrade_ovs_dpdk_local() { + _test_live_migration_ovs_dpdk(true, true); + } - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); + #[cfg(not(feature = "mshv"))] + fn _test_live_migration_watchdog(upgrade_test: bool, local: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let kernel_path = direct_kernel_boot_path(); + let console_text = String::from("On a branch floating down river a cricket, singing."); + let net_id = "net123"; + let net_params = format!( + "id={},tap=,mac={},ip={},mask=255.255.255.128", + net_id, guest.network.guest_mac0, guest.network.host_ip0 + ); - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket = temp_api_path(&tmp_dir); + let memory_param: &[&str] = if local { + &["--memory", "size=1500M,shared=on"] + } else { + &["--memory", "size=1500M"] + }; - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) + let boot_vcpus = 2; + let max_vcpus = 4; + + let pmem_temp_file = TempFile::new().unwrap(); + pmem_temp_file.as_file().set_len(128 << 20).unwrap(); + std::process::Command::new("mkfs.ext4") + .arg(pmem_temp_file.as_path()) + .output() + .expect("Expect creating disk image to succeed"); + let pmem_path = String::from("/dev/pmem0"); + + // Start the source VM + let src_vm_path = if upgrade_test { + cloud_hypervisor_release_path() + } else { + clh_command("cloud-hypervisor") + }; + let src_api_socket = temp_api_path(&guest.tmp_dir); + let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); + src_vm_cmd + .args([ + "--cpus", + format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + ]) + .args(memory_param) + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() - .default_net() + .args(["--net", net_params.as_str()]) + .args(["--api-socket", &src_api_socket]) + .args([ + "--pmem", + format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), + ]) + .args(["--watchdog"]); + let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); + + // Start the destination VM + let mut dest_api_socket = temp_api_path(&guest.tmp_dir); + dest_api_socket.push_str(".dest"); + let mut dest_child = GuestCommand::new(&guest) + .args(["--api-socket", &dest_api_socket]) .capture_output() .spawn() .unwrap(); - let mut child_dnsmasq = windows_guest.run_dnsmasq(); - - let disk = windows_guest.disk_new(WindowsGuest::FS_FAT, 100); - let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); - - // Initially present disk device - let disk_num = 1; - assert_eq!(windows_guest.disk_count(), disk_num); - assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - - // Hotplug disk device - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some(format!("path={disk},readonly=off").as_str()), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_disk2\"")); - thread::sleep(std::time::Duration::new(5, 0)); - // Online disk device - windows_guest.disks_set_rw(); - windows_guest.disks_online(); - // Verify the device is on the system - let disk_num = 2; - assert_eq!(windows_guest.disk_count(), disk_num); - assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - - let data = "hello"; - let fname = "d:\\world"; - windows_guest.disk_file_put(fname, data); - - // Unmount disk device - let cmd_success = remote_command(&api_socket, "remove-device", Some("_disk2")); - assert!(cmd_success); - thread::sleep(std::time::Duration::new(5, 0)); - // Verify the device has been removed - let disk_num = 1; - assert_eq!(windows_guest.disk_count(), disk_num); - assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - - // Remount and check the file exists with the expected contents - let (cmd_success, _cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some(format!("path={disk},readonly=off").as_str()), - ); - assert!(cmd_success); - thread::sleep(std::time::Duration::new(5, 0)); - let out = windows_guest.disk_file_read(fname); - assert_eq!(data, out.trim()); - - // Intentionally no unmount, it'll happen at shutdown. - - windows_guest.shutdown(); - }); - - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + guest.wait_vm_boot().unwrap(); - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + // Make sure the source VM is functional + // Check the number of vCPUs + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + // Check the guest RAM + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); + // Check the guest virtio-devices, e.g. block, rng, console, and net + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + // x86_64: Following what's done in the `test_snapshot_restore`, we need + // to make sure that removing and adding back the virtio-net device does + // not break the live-migration support for virtio-pci. + #[cfg(target_arch = "x86_64")] + { + assert!(remote_command( + &src_api_socket, + "remove-device", + Some(net_id), + )); + assert!(wait_until(Duration::from_secs(10), || { + guest.wait_for_ssh(Duration::from_secs(1)).is_err() + })); - handle_child_output(r, &output); - } + // Plug the virtio-net device again + assert!(remote_command( + &src_api_socket, + "add-net", + Some(net_params.as_str()), + )); + guest.wait_for_ssh(Duration::from_secs(10)).unwrap(); + } - #[test] - #[ignore = "See #6037"] - #[cfg(not(feature = "mshv"))] - #[cfg(not(target_arch = "aarch64"))] - fn test_windows_guest_disk_hotplug_multi() { - let windows_guest = WindowsGuest::new(); + // Enable watchdog and ensure its functional + let expected_reboot_count = 1; + // Enable the watchdog with a 15s timeout + enable_guest_watchdog(&guest, 15); - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); + assert_eq!(get_reboot_count(&guest), expected_reboot_count); + assert_eq!( + guest + .ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"") + .unwrap() + .trim() + .parse::() + .unwrap_or_default(), + 1 + ); + // Allow some normal time to elapse to check we don't get spurious reboots + thread::sleep(std::time::Duration::new(40, 0)); + // Check no reboot + assert_eq!(get_reboot_count(&guest), expected_reboot_count); - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket = temp_api_path(&tmp_dir); + // Start the live-migration + let migration_socket = String::from( + guest + .tmp_dir + .as_path() + .join("live-migration.sock") + .to_str() + .unwrap(), + ); - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=2G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + assert!( + start_live_migration( + &migration_socket, + &src_api_socket, + &dest_api_socket, + local, + false + ), + "Unsuccessful command: 'send-migration' or 'receive-migration'." + ); + }); - let mut child_dnsmasq = windows_guest.run_dnsmasq(); + // Check and report any errors occurred during the live-migration + if r.is_err() { + print_and_panic( + src_child, + dest_child, + None, + "Error occurred during live-migration", + ); + } - // Predefined data to used at various test stages - let disk_test_data: [[String; 4]; 2] = [ - [ - "_disk2".to_string(), - windows_guest.disk_new(WindowsGuest::FS_FAT, 123), - "d:\\world".to_string(), - "hello".to_string(), - ], - [ - "_disk3".to_string(), - windows_guest.disk_new(WindowsGuest::FS_NTFS, 333), - "e:\\hello".to_string(), - "world".to_string(), - ], - ]; + // Check the source vm has been terminated successful (give it '3s' to settle) + thread::sleep(std::time::Duration::new(3, 0)); + if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { + print_and_panic( + src_child, + dest_child, + None, + "source VM was not terminated successfully.", + ); + } + // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); - - // Initially present disk device - let disk_num = 1; - assert_eq!(windows_guest.disk_count(), disk_num); - assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); + // Perform same checks to validate VM has been properly migrated + assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(guest.get_total_memory().unwrap_or_default() > 1_400_000); - for it in &disk_test_data { - let disk_id = it[0].as_str(); - let disk = it[1].as_str(); - // Hotplug disk device - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some(format!("path={disk},readonly=off").as_str()), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains(format!("\"id\":\"{disk_id}\"").as_str())); - thread::sleep(std::time::Duration::new(5, 0)); - // Online disk devices - windows_guest.disks_set_rw(); - windows_guest.disks_online(); - } - // Verify the devices are on the system - let disk_num = (disk_test_data.len() + 1) as u8; - assert_eq!(windows_guest.disk_count(), disk_num); - assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); + guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - // Put test data - for it in &disk_test_data { - let fname = it[2].as_str(); - let data = it[3].as_str(); - windows_guest.disk_file_put(fname, data); - } + // Perform checks on watchdog + let mut expected_reboot_count = 1; - // Unmount disk devices - for it in &disk_test_data { - let disk_id = it[0].as_str(); - let cmd_success = remote_command(&api_socket, "remove-device", Some(disk_id)); - assert!(cmd_success); - thread::sleep(std::time::Duration::new(5, 0)); - } + // Allow some normal time to elapse to check we don't get spurious reboots + thread::sleep(std::time::Duration::new(40, 0)); + // Check no reboot + assert_eq!(get_reboot_count(&guest), expected_reboot_count); - // Verify the devices have been removed - let disk_num = 1; - assert_eq!(windows_guest.disk_count(), disk_num); - assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); + // Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns. + guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); + // Allow some time for the watchdog to trigger (max 30s) and reboot to happen + guest.wait_vm_boot_custom_timeout(120).unwrap(); + // Check a reboot is triggered by the watchdog + expected_reboot_count += 1; + assert_eq!(get_reboot_count(&guest), expected_reboot_count); - // Remount - for it in &disk_test_data { - let disk = it[1].as_str(); - let (cmd_success, _cmd_output) = remote_command_w_output( - &api_socket, - "add-disk", - Some(format!("path={disk},readonly=off").as_str()), - ); - assert!(cmd_success); - thread::sleep(std::time::Duration::new(5, 0)); - } + #[cfg(target_arch = "x86_64")] + { + // Now pause the VM and remain offline for 30s + assert!(remote_command(&dest_api_socket, "pause", None)); + thread::sleep(std::time::Duration::new(30, 0)); + assert!(remote_command(&dest_api_socket, "resume", None)); - // Check the files exists with the expected contents - for it in &disk_test_data { - let fname = it[2].as_str(); - let data = it[3].as_str(); - let out = windows_guest.disk_file_read(fname); - assert_eq!(data, out.trim()); + // Check no reboot + assert_eq!(get_reboot_count(&guest), expected_reboot_count); } - - // Intentionally no unmount, it'll happen at shutdown. - - windows_guest.shutdown(); }); - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + // Clean-up the destination VM and make sure it terminated correctly + let _ = dest_child.kill(); + let dest_output = dest_child.wait_with_output().unwrap(); + handle_child_output(r, &dest_output); - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + // Check the destination VM has the expected 'console_text' from its output + let r = std::panic::catch_unwind(|| { + assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + }); + handle_child_output(r, &dest_output); + } - handle_child_output(r, &output); + #[test] + fn test_watchdog() { + let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); + _test_watchdog(&guest); } #[test] #[cfg(not(feature = "mshv"))] - #[cfg(not(target_arch = "aarch64"))] - fn test_windows_guest_netdev_multi() { - let windows_guest = WindowsGuest::new(); + fn test_live_migration_watchdog() { + _test_live_migration_watchdog(false, false); + } +} - let mut ovmf_path = dirs::home_dir().unwrap(); - ovmf_path.push("workloads"); - ovmf_path.push(OVMF_NAME); +mod windows { + use std::sync::LazyLock; - let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); - let api_socket = temp_api_path(&tmp_dir); + use crate::*; - let mut child = GuestCommand::new(windows_guest.guest()) - .args(["--api-socket", &api_socket]) - .args(["--cpus", "boot=2,kvm_hyperv=on"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", ovmf_path.to_str().unwrap()]) - .args(["--serial", "tty"]) - .args(["--console", "off"]) - .default_disks() - // The multi net dev config is borrowed from test_multiple_network_interfaces - .args([ - "--net", - windows_guest.guest().default_net_string().as_str(), - "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", - "tap=mytap42,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0", - ]) - .capture_output() - .spawn() - .unwrap(); + static NEXT_DISK_ID: LazyLock> = LazyLock::new(|| Mutex::new(1)); + + struct WindowsGuest { + guest: Guest, + auth: PasswordAuth, + } + + trait FsType { + const FS_FAT: u8; + const FS_NTFS: u8; + } + impl FsType for WindowsGuest { + const FS_FAT: u8 = 0; + const FS_NTFS: u8 = 1; + } + + impl WindowsGuest { + fn new() -> Self { + let disk = WindowsDiskConfig::new(WINDOWS_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk)); + let auth = PasswordAuth { + username: String::from("administrator"), + password: String::from("Admin123"), + }; - let mut child_dnsmasq = windows_guest.run_dnsmasq(); + WindowsGuest { guest, auth } + } - let r = std::panic::catch_unwind(|| { - // Wait to make sure Windows boots up - assert!(windows_guest.wait_for_boot()); + fn guest(&self) -> &Guest { + &self.guest + } - let netdev_num = 3; - assert_eq!(windows_guest.netdev_count(), netdev_num); - assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); + fn ssh_cmd(&self, cmd: &str) -> String { + ssh_command_ip_with_auth_retry( + cmd, + &self.auth, + &self.guest.network.guest_ip0, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap() + } - let tap_count = exec_host_command_output("ip link | grep -c mytap42"); - assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); + fn cpu_count(&self) -> u8 { + self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).NumberOfLogicalProcessors\"") + .trim() + .parse::() + .unwrap_or(0) + } - windows_guest.shutdown(); - }); + fn ram_size(&self) -> usize { + self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).TotalPhysicalMemory\"") + .trim() + .parse::() + .unwrap_or(0) + } - let _ = child.wait_timeout(std::time::Duration::from_secs(60)); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + fn netdev_count(&self) -> u8 { + self.ssh_cmd("powershell -Command \"netsh int ipv4 show interfaces | Select-String ethernet | Measure-Object -Line | Format-Table -HideTableHeaders\"") + .trim() + .parse::() + .unwrap_or(0) + } - let _ = child_dnsmasq.kill(); - let _ = child_dnsmasq.wait(); + fn disk_count(&self) -> u8 { + self.ssh_cmd("powershell -Command \"Get-Disk | Measure-Object -Line | Format-Table -HideTableHeaders\"") + .trim() + .parse::() + .unwrap_or(0) + } - handle_child_output(r, &output); - } -} + fn reboot(&self) { + let _ = self.ssh_cmd("shutdown /r /t 0"); + } -#[cfg(target_arch = "x86_64")] -mod sgx { - use crate::*; + fn shutdown(&self) { + let _ = self.ssh_cmd("shutdown /s /t 0"); + } - #[test] - fn test_sgx() { - let jammy_image = JAMMY_IMAGE_NAME.to_string(); - let jammy = UbuntuDiskConfig::new(jammy_image); - let guest = Guest::new(Box::new(jammy)); + fn run_dnsmasq(&self) -> std::process::Child { + let listen_address = format!("--listen-address={}", self.guest.network.host_ip0); + let dhcp_host = format!( + "--dhcp-host={},{}", + self.guest.network.guest_mac0, self.guest.network.guest_ip0 + ); + let dhcp_range = format!( + "--dhcp-range=eth,{},{}", + self.guest.network.guest_ip0, self.guest.network.guest_ip0 + ); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .default_disks() - .default_net() - .args(["--sgx-epc", "id=epc0,size=64M"]) - .capture_output() - .spawn() - .unwrap(); + Command::new("dnsmasq") + .arg("--no-daemon") + .arg("--log-queries") + .arg(listen_address.as_str()) + .arg("--except-interface=lo") + .arg("--bind-dynamic") // Allow listening to host_ip while the interface is not ready yet. + .arg("--conf-file=/dev/null") + .arg(dhcp_host.as_str()) + .arg(dhcp_range.as_str()) + .spawn() + .unwrap() + } - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // TODO Cleanup image file explicitly after test, if there's some space issues. + fn disk_new(&self, fs: u8, sz: usize) -> String { + let mut guard = NEXT_DISK_ID.lock().unwrap(); + let id = *guard; + *guard = id + 1; - // Check if SGX is correctly detected in the guest. - guest.check_sgx_support().unwrap(); + let img = PathBuf::from(format!("/tmp/test-hotplug-{id}.raw")); + let _ = fs::remove_file(&img); - // Validate the SGX EPC section is 64MiB. - assert_eq!( - guest - .ssh_command("cpuid -l 0x12 -s 2 | grep 'section size' | cut -d '=' -f 2") - .unwrap() - .trim(), - "0x0000000004000000" - ); - }); + // Create an image file + let out = Command::new("qemu-img") + .args([ + "create", + "-f", + "raw", + img.to_str().unwrap(), + format!("{sz}m").as_str(), + ]) + .output() + .expect("qemu-img command failed") + .stdout; + println!("{out:?}"); - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + // Associate image to a loop device + let out = Command::new("losetup") + .args(["--show", "-f", img.to_str().unwrap()]) + .output() + .expect("failed to create loop device") + .stdout; + let _tmp = String::from_utf8_lossy(&out); + let loop_dev = _tmp.trim(); + println!("{out:?}"); - handle_child_output(r, &output); - } -} + // Create a partition table + // echo 'type=7' | sudo sfdisk "${LOOP}" + let mut child = Command::new("sfdisk") + .args([loop_dev]) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + let stdin = child.stdin.as_mut().expect("failed to open stdin"); + stdin + .write_all("type=7".as_bytes()) + .expect("failed to write stdin"); + let out = child.wait_with_output().expect("sfdisk failed").stdout; + println!("{out:?}"); -#[cfg(target_arch = "x86_64")] -mod vfio { - use crate::*; - const NVIDIA_VFIO_DEVICE: &str = "/sys/bus/pci/devices/0002:00:01.0"; + // Disengage the loop device + let out = Command::new("losetup") + .args(["-d", loop_dev]) + .output() + .expect("loop device not found") + .stdout; + println!("{out:?}"); - fn test_nvidia_card_memory_hotplug(hotplug_method: &str) { - let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(jammy)); - let api_socket = temp_api_path(&guest.tmp_dir); + // Re-associate loop device pointing to the partition only + let out = Command::new("losetup") + .args([ + "--show", + "--offset", + (512 * 2048).to_string().as_str(), + "-f", + img.to_str().unwrap(), + ]) + .output() + .expect("failed to create loop device") + .stdout; + let _tmp = String::from_utf8_lossy(&out); + let loop_dev = _tmp.trim(); + println!("{out:?}"); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) - .args([ - "--memory", - format!("size=4G,hotplug_size=4G,hotplug_method={hotplug_method}").as_str(), - ]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()]) - .args(["--api-socket", &api_socket]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + // Create filesystem. + let fs_cmd = match fs { + WindowsGuest::FS_FAT => "mkfs.msdos", + WindowsGuest::FS_NTFS => "mkfs.ntfs", + _ => panic!("Unknown filesystem type '{fs}'"), + }; + let out = Command::new(fs_cmd) + .args([&loop_dev]) + .output() + .unwrap_or_else(|_| panic!("{fs_cmd} failed")) + .stdout; + println!("{out:?}"); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Disengage the loop device + let out = Command::new("losetup") + .args(["-d", loop_dev]) + .output() + .unwrap_or_else(|_| panic!("loop device '{loop_dev}' not found")) + .stdout; + println!("{out:?}"); - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + img.to_str().unwrap().to_string() + } - guest.enable_memory_hotplug(); + fn disks_set_rw(&self) { + let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsReadOnly $False\""); + } - // Add RAM to the VM - let desired_ram = 6 << 30; - resize_command(&api_socket, None, Some(desired_ram), None, None); - thread::sleep(std::time::Duration::new(30, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); + fn disks_online(&self) { + let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsOffline $False\""); + } - // Check the VFIO device works when RAM is increased to 6GiB - guest.check_nvidia_gpu(); - }); + fn disk_file_put(&self, fname: &str, data: &str) { + let _ = self.ssh_cmd(&format!( + "powershell -Command \"'{data}' | Set-Content -Path {fname}\"" + )); + } - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); + fn disk_file_read(&self, fname: &str) -> String { + self.ssh_cmd(&format!( + "powershell -Command \"Get-Content -Path {fname}\"" + )) + } - handle_child_output(r, &output); + fn wait_for_boot(&self) -> Result<(), WaitForSshError> { + let out = wait_for_ssh( + "dir /b c:\\ | find \"Windows\"", + &self.auth, + &self.guest.network.guest_ip0, + Duration::from_secs(180), + )?; + + if out.trim() == "Windows" { + Ok(()) + } else { + panic!("Unexpected Windows boot probe output: {:?}", out.trim()); + } + } + } + + fn vcpu_threads_count(pid: u32) -> u8 { + // ps -T -p 12345 | grep vcpu | wc -l + let out = Command::new("ps") + .args(["-T", "-p", format!("{pid}").as_str()]) + .output() + .expect("ps command failed") + .stdout; + String::from_utf8_lossy(&out).matches("vcpu").count() as u8 } - #[test] - fn test_nvidia_card_memory_hotplug_acpi() { - test_nvidia_card_memory_hotplug("acpi") + fn netdev_ctrl_threads_count(pid: u32) -> u8 { + // ps -T -p 12345 | grep "_net[0-9]*_ctrl" | wc -l + let out = Command::new("ps") + .args(["-T", "-p", format!("{pid}").as_str()]) + .output() + .expect("ps command failed") + .stdout; + let mut n = 0; + String::from_utf8_lossy(&out) + .split_whitespace() + .for_each(|s| n += (s.starts_with("_net") && s.ends_with("_ctrl")) as u8); // _net1_ctrl + n } - #[test] - fn test_nvidia_card_memory_hotplug_virtio_mem() { - test_nvidia_card_memory_hotplug("virtio-mem") + fn disk_ctrl_threads_count(pid: u32) -> u8 { + // ps -T -p 15782 | grep "_disk[0-9]*_q0" | wc -l + let out = Command::new("ps") + .args(["-T", "-p", format!("{pid}").as_str()]) + .output() + .expect("ps command failed") + .stdout; + let mut n = 0; + String::from_utf8_lossy(&out) + .split_whitespace() + .for_each(|s| n += (s.starts_with("_disk") && s.ends_with("_q0")) as u8); // _disk0_q0, don't care about multiple queues as they're related to the same hdd + n } #[test] - fn test_nvidia_card_pci_hotplug() { - let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(jammy)); - let api_socket = temp_api_path(&guest.tmp_dir); + fn test_windows_guest() { + let windows_guest = WindowsGuest::new(); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) .args(["--memory", "size=4G"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .args(["--api-socket", &api_socket]) + .args(["--kernel", edk2_path().to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) .default_disks() .default_net() .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let fd = child.stdout.as_ref().unwrap().as_raw_fd(); + let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; + let fd = child.stderr.as_ref().unwrap().as_raw_fd(); + let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - // Hotplug the card to the VM - let (cmd_success, cmd_output) = remote_command_w_output( - &api_socket, - "add-device", - Some(format!("id=vfio0,path={NVIDIA_VFIO_DEVICE}").as_str()), - ); - assert!(cmd_success); - assert!(String::from_utf8_lossy(&cmd_output) - .contains("{\"id\":\"vfio0\",\"bdf\":\"0000:00:06.0\"}")); + assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); - thread::sleep(std::time::Duration::new(10, 0)); + let mut child_dnsmasq = windows_guest.run_dnsmasq(); - // Check the VFIO device works after hotplug - guest.check_nvidia_gpu(); + let r = std::panic::catch_unwind(|| { + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); + + windows_guest.shutdown(); }); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); let _ = child.kill(); let output = child.wait_with_output().unwrap(); + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); + handle_child_output(r, &output); } #[test] - fn test_nvidia_card_reboot() { - let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(jammy)); - let api_socket = temp_api_path(&guest.tmp_dir); + fn test_windows_guest_multiple_queues() { + let windows_guest = WindowsGuest::new(); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); + + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--cpus", "boot=4,kvm_hyperv=on"]) .args(["--memory", "size=4G"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) .args([ - "--device", - format!("path={NVIDIA_VFIO_DEVICE},iommu=on").as_str(), + "--disk", + format!( + "path={},num_queues=4", + windows_guest + .guest() + .disk_config + .disk(DiskType::OperatingSystem) + .unwrap() + ) + .as_str(), + ]) + .args([ + "--net", + format!( + "tap=,mac={},ip={},mask=255.255.255.128,num_queues=8", + windows_guest.guest().network.guest_mac0, + windows_guest.guest().network.host_ip0 + ) + .as_str(), ]) - .args(["--api-socket", &api_socket]) - .default_disks() - .default_net() .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - - // Check the VFIO device works after boot - guest.check_nvidia_gpu(); - - guest.reboot_linux(0, None); - - // Check the VFIO device works after reboot - guest.check_nvidia_gpu(); - }); - - let _ = child.kill(); - let output = child.wait_with_output().unwrap(); - - handle_child_output(r, &output); - } + let fd = child.stdout.as_ref().unwrap().as_raw_fd(); + let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; + let fd = child.stderr.as_ref().unwrap().as_raw_fd(); + let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - #[test] - fn test_nvidia_card_iommu_address_width() { - let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(jammy)); - let api_socket = temp_api_path(&guest.tmp_dir); + assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); - let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=4"]) - .args(["--memory", "size=4G"]) - .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) - .args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()]) - .args([ - "--platform", - "num_pci_segments=2,iommu_segments=1,iommu_address_width=42", - ]) - .args(["--api-socket", &api_socket]) - .default_disks() - .default_net() - .capture_output() - .spawn() - .unwrap(); + let mut child_dnsmasq = windows_guest.run_dnsmasq(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - assert!(guest - .ssh_command("sudo dmesg") - .unwrap() - .contains("input address: 42 bits")); + windows_guest.shutdown(); }); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); let _ = child.kill(); let output = child.wait_with_output().unwrap(); - handle_child_output(r, &output); - } -} - -mod live_migration { - use crate::*; - - fn start_live_migration( - migration_socket: &str, - src_api_socket: &str, - dest_api_socket: &str, - local: bool, - ) -> bool { - // Start to receive migration from the destination VM - let mut receive_migration = Command::new(clh_command("ch-remote")) - .args([ - &format!("--api-socket={dest_api_socket}"), - "receive-migration", - &format! {"unix:{migration_socket}"}, - ]) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap(); - // Give it '1s' to make sure the 'migration_socket' file is properly created - thread::sleep(std::time::Duration::new(1, 0)); - // Start to send migration from the source VM - - let mut args = [ - format!("--api-socket={}", &src_api_socket), - "send-migration".to_string(), - format! {"unix:{migration_socket}"}, - ] - .to_vec(); - - if local { - args.insert(2, "--local".to_string()); - } - - let mut send_migration = Command::new(clh_command("ch-remote")) - .args(&args) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap(); - - // The 'send-migration' command should be executed successfully within the given timeout - let send_success = if let Some(status) = send_migration - .wait_timeout(std::time::Duration::from_secs(30)) - .unwrap() - { - status.success() - } else { - false - }; - - if !send_success { - let _ = send_migration.kill(); - let output = send_migration.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'send_migration' output ==== \ - \n\n---stdout---\n{}\n\n---stderr---\n{} \ - \n\n==== End 'send_migration' output ====\n\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - // The 'receive-migration' command should be executed successfully within the given timeout - let receive_success = if let Some(status) = receive_migration - .wait_timeout(std::time::Duration::from_secs(30)) - .unwrap() - { - status.success() - } else { - false - }; - - if !receive_success { - let _ = receive_migration.kill(); - let output = receive_migration.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'receive_migration' output ==== \ - \n\n---stdout---\n{}\n\n---stderr---\n{} \ - \n\n==== End 'receive_migration' output ====\n\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - send_success && receive_success - } - - fn print_and_panic(src_vm: Child, dest_vm: Child, ovs_vm: Option, message: &str) -> ! { - let mut src_vm = src_vm; - let mut dest_vm = dest_vm; - - let _ = src_vm.kill(); - let src_output = src_vm.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'source_vm' stdout ====\n\n{}\n\n==== End 'source_vm' stdout ====", - String::from_utf8_lossy(&src_output.stdout) - ); - eprintln!( - "\n\n==== Start 'source_vm' stderr ====\n\n{}\n\n==== End 'source_vm' stderr ====", - String::from_utf8_lossy(&src_output.stderr) - ); - let _ = dest_vm.kill(); - let dest_output = dest_vm.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'destination_vm' stdout ====\n\n{}\n\n==== End 'destination_vm' stdout ====", - String::from_utf8_lossy(&dest_output.stdout) - ); - eprintln!( - "\n\n==== Start 'destination_vm' stderr ====\n\n{}\n\n==== End 'destination_vm' stderr ====", - String::from_utf8_lossy(&dest_output.stderr) - ); - - if let Some(ovs_vm) = ovs_vm { - let mut ovs_vm = ovs_vm; - let _ = ovs_vm.kill(); - let ovs_output = ovs_vm.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'ovs_vm' stdout ====\n\n{}\n\n==== End 'ovs_vm' stdout ====", - String::from_utf8_lossy(&ovs_output.stdout) - ); - eprintln!( - "\n\n==== Start 'ovs_vm' stderr ====\n\n{}\n\n==== End 'ovs_vm' stderr ====", - String::from_utf8_lossy(&ovs_output.stderr) - ); - - cleanup_ovs_dpdk(); - } + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - panic!("Test failed: {message}") + handle_child_output(r, &output); } - // This test exercises the local live-migration between two Cloud Hypervisor VMs on the - // same host. It ensures the following behaviors: - // 1. The source VM is up and functional (including various virtio-devices are working properly); - // 2. The 'send-migration' and 'receive-migration' command finished successfully; - // 3. The source VM terminated gracefully after live migration; - // 4. The destination VM is functional (including various virtio-devices are working properly) after - // live migration; - // Note: This test does not use vsock as we can't create two identical vsock on the same host. - fn _test_live_migration(upgrade_test: bool, local: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let kernel_path = direct_kernel_boot_path(); - let console_text = String::from("On a branch floating down river a cricket, singing."); - let net_id = "net123"; - let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip - ); - - let memory_param: &[&str] = if local { - &["--memory", "size=4G,shared=on"] - } else { - &["--memory", "size=4G"] - }; + #[test] + #[cfg(not(feature = "mshv"))] + #[cfg_attr(target_arch = "aarch64", ignore = "See #4327")] + fn test_windows_guest_snapshot_restore() { + let windows_guest = WindowsGuest::new(); - let boot_vcpus = 2; - let max_vcpus = 4; + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - std::process::Command::new("mkfs.ext4") - .arg(pmem_temp_file.as_path()) - .output() - .expect("Expect creating disk image to succeed"); - let pmem_path = String::from("/dev/pmem0"); + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket_source = format!("{}.1", temp_api_path(&tmp_dir)); - // Start the source VM - let src_vm_path = if !upgrade_test { - clh_command("cloud-hypervisor") - } else { - cloud_hypervisor_release_path() - }; - let src_api_socket = temp_api_path(&guest.tmp_dir); - let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); - src_vm_cmd - .args([ - "--cpus", - format!("boot={boot_vcpus},max={max_vcpus}").as_str(), - ]) - .args(memory_param) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket_source]) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=4G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) .default_disks() - .args(["--net", net_params.as_str()]) - .args(["--api-socket", &src_api_socket]) - .args([ - "--pmem", - format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), - ]); - let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); - - // Start the destination VM - let mut dest_api_socket = temp_api_path(&guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&guest) - .args(["--api-socket", &dest_api_socket]) + .default_net() .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let fd = child.stdout.as_ref().unwrap().as_raw_fd(); + let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; + let fd = child.stderr.as_ref().unwrap().as_raw_fd(); + let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - // Make sure the source VM is functional - // Check the number of vCPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); - // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + let mut child_dnsmasq = windows_guest.run_dnsmasq(); - // Check the guest virtio-devices, e.g. block, rng, console, and net - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - // x86_64: Following what's done in the `test_snapshot_restore`, we need - // to make sure that removing and adding back the virtio-net device does - // not break the live-migration support for virtio-pci. - #[cfg(target_arch = "x86_64")] - { - assert!(remote_command( - &src_api_socket, - "remove-device", - Some(net_id), - )); - thread::sleep(std::time::Duration::new(10, 0)); + let snapshot_dir = temp_snapshot_dir_path(&tmp_dir); - // Plug the virtio-net device again - assert!(remote_command( - &src_api_socket, - "add-net", - Some(net_params.as_str()), - )); - thread::sleep(std::time::Duration::new(10, 0)); - } + // Pause the VM + assert!(remote_command(&api_socket_source, "pause", None)); - // Start the live-migration - let migration_socket = String::from( - guest - .tmp_dir - .as_path() - .join("live-migration.sock") - .to_str() - .unwrap(), - ); + // Take a snapshot from the VM + assert!(remote_command( + &api_socket_source, + "snapshot", + Some(format!("file://{snapshot_dir}").as_str()), + )); - assert!( - start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); - }); + let snapshot_state_path = std::path::Path::new(&snapshot_dir).join("state.json"); + let snapshot_config_path = std::path::Path::new(&snapshot_dir).join("config.json"); + assert!(wait_until(Duration::from_secs(30), || { + snapshot_state_path.exists() && snapshot_config_path.exists() + })); - // Check and report any errors occurred during the live-migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - None, - "Error occurred during live-migration", - ); - } + let _ = child.kill(); + child.wait().unwrap(); - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - None, - "source VM was not terminated successfully.", - ); - }; + let api_socket_restored = format!("{}.2", temp_api_path(&tmp_dir)); + + // Restore the VM from the snapshot + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket_restored]) + .args([ + "--restore", + format!("source_url=file://{snapshot_dir}").as_str(), + ]) + .capture_output() + .spawn() + .unwrap(); + + // Wait for the VM to be restored + assert!(wait_until(Duration::from_secs(30), || { + remote_command(&api_socket_restored, "info", None) + })); - // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - // Perform same checks to validate VM has been properly migrated - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + // Resume the VM + assert!(wait_until(Duration::from_secs(30), || remote_command( + &api_socket_restored, + "info", + None + ))); + assert!(remote_command(&api_socket_restored, "resume", None)); - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + windows_guest.shutdown(); }); - // Clean-up the destination VM and make sure it terminated correctly - let _ = dest_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - handle_child_output(r, &dest_output); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // Check the destination VM has the expected 'console_text' from its output - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); - }); - handle_child_output(r, &dest_output); - } + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - fn _test_live_migration_balloon(upgrade_test: bool, local: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let kernel_path = direct_kernel_boot_path(); - let console_text = String::from("On a branch floating down river a cricket, singing."); - let net_id = "net123"; - let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip - ); + handle_child_output(r, &output); + } - let memory_param: &[&str] = if local { - &[ - "--memory", - "size=4G,hotplug_method=virtio-mem,hotplug_size=8G,shared=on", - "--balloon", - "size=0", - ] - } else { - &[ - "--memory", - "size=4G,hotplug_method=virtio-mem,hotplug_size=8G", - "--balloon", - "size=0", - ] - }; + #[test] + #[cfg(not(feature = "mshv"))] + #[cfg(not(target_arch = "aarch64"))] + fn test_windows_guest_cpu_hotplug() { + let windows_guest = WindowsGuest::new(); - let boot_vcpus = 2; - let max_vcpus = 4; + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - std::process::Command::new("mkfs.ext4") - .arg(pmem_temp_file.as_path()) - .output() - .expect("Expect creating disk image to succeed"); - let pmem_path = String::from("/dev/pmem0"); + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); - // Start the source VM - let src_vm_path = if !upgrade_test { - clh_command("cloud-hypervisor") - } else { - cloud_hypervisor_release_path() - }; - let src_api_socket = temp_api_path(&guest.tmp_dir); - let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); - src_vm_cmd - .args([ - "--cpus", - format!("boot={boot_vcpus},max={max_vcpus}").as_str(), - ]) - .args(memory_param) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket]) + .args(["--cpus", "boot=2,max=8,kvm_hyperv=on"]) + .args(["--memory", "size=4G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) .default_disks() - .args(["--net", net_params.as_str()]) - .args(["--api-socket", &src_api_socket]) - .args([ - "--pmem", - format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), - ]); - let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); - - // Start the destination VM - let mut dest_api_socket = temp_api_path(&guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&guest) - .args(["--api-socket", &dest_api_socket]) + .default_net() .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + let mut child_dnsmasq = windows_guest.run_dnsmasq(); + + let r = std::panic::catch_unwind(|| { + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); + + let vcpu_num = 2; + // Check the initial number of CPUs the guest sees + assert_eq!(windows_guest.cpu_count(), vcpu_num); + // Check the initial number of vcpu threads in the CH process + assert_eq!(vcpu_threads_count(child.id()), vcpu_num); + + let vcpu_num = 6; + // Hotplug some CPUs + resize_command(&api_socket, Some(vcpu_num), None, None, None); + // Wait for Windows to report the hotplugged CPUs. + assert!(wait_until(Duration::from_secs(10), || windows_guest + .cpu_count() + == vcpu_num)); + // Check the guest sees the correct number + assert_eq!(windows_guest.cpu_count(), vcpu_num); + // Check the CH process has the correct number of vcpu threads + assert_eq!(vcpu_threads_count(child.id()), vcpu_num); + + let vcpu_num = 4; + // Remove some CPUs. Note that Windows doesn't support hot-remove. + resize_command(&api_socket, Some(vcpu_num), None, None, None); + thread::sleep(std::time::Duration::new(10, 0)); - // Make sure the source VM is functional - // Check the number of vCPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + // Reboot to let Windows catch up + windows_guest.reboot(); + // Wait for Windows to come back after the reboot. + windows_guest.wait_for_boot().unwrap(); + // Wait for Windows to reflect the unplugged CPU count. + assert!(wait_until(Duration::from_secs(60), || windows_guest + .cpu_count() + == vcpu_num)); + // Check the guest sees the correct number + assert_eq!(windows_guest.cpu_count(), vcpu_num); + // Check the CH process has the correct number of vcpu threads + assert_eq!(vcpu_threads_count(child.id()), vcpu_num); - // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - // Increase the guest RAM - resize_command(&src_api_socket, None, Some(6 << 30), None, None); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); - // Use balloon to remove RAM from the VM - resize_command(&src_api_socket, None, None, Some(1 << 30), None); - thread::sleep(std::time::Duration::new(5, 0)); - let total_memory = guest.get_total_memory().unwrap_or_default(); - assert!(total_memory > 4_800_000); - assert!(total_memory < 5_760_000); + windows_guest.shutdown(); + }); - // Check the guest virtio-devices, e.g. block, rng, console, and net - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // x86_64: Following what's done in the `test_snapshot_restore`, we need - // to make sure that removing and adding back the virtio-net device does - // not break the live-migration support for virtio-pci. - #[cfg(target_arch = "x86_64")] - { - assert!(remote_command( - &src_api_socket, - "remove-device", - Some(net_id), - )); - thread::sleep(std::time::Duration::new(10, 0)); + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - // Plug the virtio-net device again - assert!(remote_command( - &src_api_socket, - "add-net", - Some(net_params.as_str()), - )); - thread::sleep(std::time::Duration::new(10, 0)); - } + handle_child_output(r, &output); + } - // Start the live-migration - let migration_socket = String::from( - guest - .tmp_dir - .as_path() - .join("live-migration.sock") - .to_str() - .unwrap(), - ); + #[test] + #[cfg(not(feature = "mshv"))] + #[cfg(not(target_arch = "aarch64"))] + fn test_windows_guest_ram_hotplug() { + let windows_guest = WindowsGuest::new(); - assert!( - start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); - }); + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); - // Check and report any errors occurred during the live-migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - None, - "Error occurred during live-migration", - ); - } + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - None, - "source VM was not terminated successfully.", - ); - }; + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket]) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=2G,hotplug_size=5G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + let mut child_dnsmasq = windows_guest.run_dnsmasq(); - // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - // Perform same checks to validate VM has been properly migrated - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + let ram_size = 2 * 1024 * 1024 * 1024; + // Check the initial number of RAM the guest sees + let current_ram_size = windows_guest.ram_size(); + // This size seems to be reserved by the system and thus the + // reported amount differs by this constant value. + let reserved_ram_size = ram_size - current_ram_size; + // Verify that there's not more than 4mb constant diff wasted + // by the reserved ram. + assert!(reserved_ram_size < 4 * 1024 * 1024); - // Perform checks on guest RAM using balloon - let total_memory = guest.get_total_memory().unwrap_or_default(); - assert!(total_memory > 4_800_000); - assert!(total_memory < 5_760_000); - // Deflate balloon to restore entire RAM to the VM - resize_command(&dest_api_socket, None, None, Some(0), None); - thread::sleep(std::time::Duration::new(5, 0)); - assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); - // Decrease guest RAM with virtio-mem - resize_command(&dest_api_socket, None, Some(5 << 30), None, None); - thread::sleep(std::time::Duration::new(5, 0)); - let total_memory = guest.get_total_memory().unwrap_or_default(); - assert!(total_memory > 4_800_000); - assert!(total_memory < 5_760_000); - }); + let ram_size = 4 * 1024 * 1024 * 1024; + // Hotplug some RAM + resize_command(&api_socket, None, Some(ram_size), None, None); + // Wait for Windows to report the hotplugged memory. + assert!(wait_until(Duration::from_secs(10), || windows_guest + .ram_size() + == ram_size - reserved_ram_size)); - // Clean-up the destination VM and make sure it terminated correctly - let _ = dest_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - handle_child_output(r, &dest_output); + let ram_size = 3 * 1024 * 1024 * 1024; + // Unplug some RAM. Note that hot-remove most likely won't work. + resize_command(&api_socket, None, Some(ram_size), None, None); + // Reboot to let Windows catch up + windows_guest.reboot(); + // Wait for Windows to come back after the reboot. + windows_guest.wait_for_boot().unwrap(); + // Wait for Windows to reflect the unplugged RAM amount. + assert!(wait_until(Duration::from_secs(60), || windows_guest + .ram_size() + == ram_size - reserved_ram_size)); + // Check the guest sees the correct number + assert_eq!(windows_guest.ram_size(), ram_size - reserved_ram_size); - // Check the destination VM has the expected 'console_text' from its output - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + windows_guest.shutdown(); }); - handle_child_output(r, &dest_output); - } - fn _test_live_migration_numa(upgrade_test: bool, local: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let kernel_path = direct_kernel_boot_path(); - let console_text = String::from("On a branch floating down river a cricket, singing."); - let net_id = "net123"; - let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip - ); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - let memory_param: &[&str] = if local { - &[ - "--memory", - "size=0,hotplug_method=virtio-mem,shared=on", - "--memory-zone", - "id=mem0,size=1G,hotplug_size=4G,shared=on", - "id=mem1,size=1G,hotplug_size=4G,shared=on", - "id=mem2,size=2G,hotplug_size=4G,shared=on", - "--numa", - "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", - "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", - "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", - ] - } else { - &[ - "--memory", - "size=0,hotplug_method=virtio-mem", - "--memory-zone", - "id=mem0,size=1G,hotplug_size=4G", - "id=mem1,size=1G,hotplug_size=4G", - "id=mem2,size=2G,hotplug_size=4G", - "--numa", - "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", - "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", - "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", - ] - }; + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - let boot_vcpus = 6; - let max_vcpus = 12; + handle_child_output(r, &output); + } - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - std::process::Command::new("mkfs.ext4") - .arg(pmem_temp_file.as_path()) - .output() - .expect("Expect creating disk image to succeed"); - let pmem_path = String::from("/dev/pmem0"); + #[test] + #[cfg(not(feature = "mshv"))] + fn test_windows_guest_netdev_hotplug() { + let windows_guest = WindowsGuest::new(); - // Start the source VM - let src_vm_path = if !upgrade_test { - clh_command("cloud-hypervisor") - } else { - cloud_hypervisor_release_path() - }; - let src_api_socket = temp_api_path(&guest.tmp_dir); - let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); - src_vm_cmd - .args([ - "--cpus", - format!("boot={boot_vcpus},max={max_vcpus}").as_str(), - ]) - .args(memory_param) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--net", net_params.as_str()]) - .args(["--api-socket", &src_api_socket]) - .args([ - "--pmem", - format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), - ]); - let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); - // Start the destination VM - let mut dest_api_socket = temp_api_path(&guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&guest) - .args(["--api-socket", &dest_api_socket]) + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); + + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket]) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=4G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) + .default_disks() + .default_net() .capture_output() .spawn() .unwrap(); + let mut child_dnsmasq = windows_guest.run_dnsmasq(); + let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - // Make sure the source VM is functional - // Check the number of vCPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + // Initially present network device + let netdev_num = 1; + assert_eq!(windows_guest.netdev_count(), netdev_num); + assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); - // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); + // Hotplug network device + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-net", + Some(windows_guest.guest().default_net_string().as_str()), + ); + assert!(cmd_success); + assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_net2\"")); + // Wait for Windows to enumerate the added network device. + assert!(wait_until(Duration::from_secs(5), || windows_guest + .netdev_count() + == 2 + && netdev_ctrl_threads_count(child.id()) == 2)); + // Verify the device is on the system + let netdev_num = 2; + assert_eq!(windows_guest.netdev_count(), netdev_num); + assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); - // Check the guest virtio-devices, e.g. block, rng, console, and net - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + // Remove network device + let cmd_success = remote_command(&api_socket, "remove-device", Some("_net2")); + assert!(cmd_success); + // Wait for Windows to drop the removed network device. + assert!(wait_until(Duration::from_secs(5), || windows_guest + .netdev_count() + == 1 + && netdev_ctrl_threads_count(child.id()) == 1)); + // Verify the device has been removed + let netdev_num = 1; + assert_eq!(windows_guest.netdev_count(), netdev_num); + assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); - // Check the NUMA parameters are applied correctly and resize - // each zone to test the case where we migrate a VM with the - // virtio-mem regions being used. - { - guest.check_numa_common( - Some(&[960_000, 960_000, 1_920_000]), - Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), - Some(&["10 15 20", "20 10 25", "25 30 10"]), - ); + windows_guest.shutdown(); + }); - // AArch64 currently does not support hotplug, and therefore we only - // test hotplug-related function on x86_64 here. - #[cfg(target_arch = "x86_64")] - { - guest.enable_memory_hotplug(); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // Resize every memory zone and check each associated NUMA node - // has been assigned the right amount of memory. - resize_zone_command(&src_api_socket, "mem0", "2G"); - resize_zone_command(&src_api_socket, "mem1", "2G"); - resize_zone_command(&src_api_socket, "mem2", "3G"); - thread::sleep(std::time::Duration::new(5, 0)); + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - guest.check_numa_common(Some(&[1_920_000, 1_920_000, 1_920_000]), None, None); - } - } + handle_child_output(r, &output); + } - // x86_64: Following what's done in the `test_snapshot_restore`, we need - // to make sure that removing and adding back the virtio-net device does - // not break the live-migration support for virtio-pci. - #[cfg(target_arch = "x86_64")] - { - assert!(remote_command( - &src_api_socket, - "remove-device", - Some(net_id), - )); - thread::sleep(std::time::Duration::new(10, 0)); + #[test] + #[ignore = "See #6037"] + #[cfg(not(feature = "mshv"))] + #[cfg(not(target_arch = "aarch64"))] + fn test_windows_guest_disk_hotplug() { + let windows_guest = WindowsGuest::new(); - // Plug the virtio-net device again - assert!(remote_command( - &src_api_socket, - "add-net", - Some(net_params.as_str()), - )); - thread::sleep(std::time::Duration::new(10, 0)); - } + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); - // Start the live-migration - let migration_socket = String::from( - guest - .tmp_dir - .as_path() - .join("live-migration.sock") - .to_str() - .unwrap(), - ); + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); - assert!( - start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); - }); + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket]) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=4G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - // Check and report any errors occurred during the live-migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - None, - "Error occurred during live-migration", - ); - } + let mut child_dnsmasq = windows_guest.run_dnsmasq(); - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - None, - "source VM was not terminated successfully.", - ); - }; + let disk = windows_guest.disk_new(WindowsGuest::FS_FAT, 100); - // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - // Perform same checks to validate VM has been properly migrated - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - #[cfg(target_arch = "x86_64")] - assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000); - #[cfg(target_arch = "aarch64")] - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + // Initially present disk device + let disk_num = 1; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - // Perform NUMA related checks - { - #[cfg(target_arch = "aarch64")] - { - guest.check_numa_common( - Some(&[960_000, 960_000, 1_920_000]), - Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), - Some(&["10 15 20", "20 10 25", "25 30 10"]), - ); - } + // Hotplug disk device + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some(format!("path={disk},readonly=off").as_str()), + ); + assert!(cmd_success); + assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_disk2\"")); + // Online disk device + windows_guest.disks_set_rw(); + windows_guest.disks_online(); + // Wait for Windows to enumerate the added disk. + assert!(wait_until(Duration::from_secs(5), || windows_guest + .disk_count() + == 2 + && disk_ctrl_threads_count(child.id()) == 2)); + // Verify the device is on the system + let disk_num = 2; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - // AArch64 currently does not support hotplug, and therefore we only - // test hotplug-related function on x86_64 here. - #[cfg(target_arch = "x86_64")] - { - guest.check_numa_common( - Some(&[1_920_000, 1_920_000, 2_880_000]), - Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), - Some(&["10 15 20", "20 10 25", "25 30 10"]), - ); + let data = "hello"; + let fname = "d:\\world"; + windows_guest.disk_file_put(fname, data); - guest.enable_memory_hotplug(); + // Unmount disk device + let cmd_success = remote_command(&api_socket, "remove-device", Some("_disk2")); + assert!(cmd_success); + // Wait for Windows to drop the removed disk. + assert!(wait_until(Duration::from_secs(5), || windows_guest + .disk_count() + == 1 + && disk_ctrl_threads_count(child.id()) == 1)); + // Verify the device has been removed + let disk_num = 1; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - // Resize every memory zone and check each associated NUMA node - // has been assigned the right amount of memory. - resize_zone_command(&dest_api_socket, "mem0", "4G"); - resize_zone_command(&dest_api_socket, "mem1", "4G"); - resize_zone_command(&dest_api_socket, "mem2", "4G"); - // Resize to the maximum amount of CPUs and check each NUMA - // node has been assigned the right CPUs set. - resize_command(&dest_api_socket, Some(max_vcpus), None, None, None); - thread::sleep(std::time::Duration::new(5, 0)); + // Remount and check the file exists with the expected contents + let (cmd_success, _cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some(format!("path={disk},readonly=off").as_str()), + ); + assert!(cmd_success); + // Wait for Windows to mount the re-added disk again. + assert!(wait_until(Duration::from_secs(5), || windows_guest + .disk_file_read(fname) + .trim() + == data)); + let out = windows_guest.disk_file_read(fname); + assert_eq!(data, out.trim()); - guest.check_numa_common( - Some(&[3_840_000, 3_840_000, 3_840_000]), - Some(&[vec![0, 1, 2, 9], vec![3, 4, 6, 7, 8], vec![5, 10, 11]]), - None, - ); - } - } + // Intentionally no unmount, it'll happen at shutdown. + + windows_guest.shutdown(); }); - // Clean-up the destination VM and make sure it terminated correctly - let _ = dest_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - handle_child_output(r, &dest_output); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // Check the destination VM has the expected 'console_text' from its output - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); - }); - handle_child_output(r, &dest_output); - } + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - fn _test_live_migration_watchdog(upgrade_test: bool, local: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let kernel_path = direct_kernel_boot_path(); - let console_text = String::from("On a branch floating down river a cricket, singing."); - let net_id = "net123"; - let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip - ); + handle_child_output(r, &output); + } - let memory_param: &[&str] = if local { - &["--memory", "size=4G,shared=on"] - } else { - &["--memory", "size=4G"] - }; + #[test] + #[ignore = "See #6037"] + #[cfg(not(feature = "mshv"))] + #[cfg(not(target_arch = "aarch64"))] + fn test_windows_guest_disk_hotplug_multi() { + let windows_guest = WindowsGuest::new(); - let boot_vcpus = 2; - let max_vcpus = 4; + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - std::process::Command::new("mkfs.ext4") - .arg(pmem_temp_file.as_path()) - .output() - .expect("Expect creating disk image to succeed"); - let pmem_path = String::from("/dev/pmem0"); + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); - // Start the source VM - let src_vm_path = if !upgrade_test { - clh_command("cloud-hypervisor") - } else { - cloud_hypervisor_release_path() - }; - let src_api_socket = temp_api_path(&guest.tmp_dir); - let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); - src_vm_cmd - .args([ - "--cpus", - format!("boot={boot_vcpus},max={max_vcpus}").as_str(), - ]) - .args(memory_param) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket]) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=2G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) .default_disks() - .args(["--net", net_params.as_str()]) - .args(["--api-socket", &src_api_socket]) - .args([ - "--pmem", - format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), - ]) - .args(["--watchdog"]); - let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); - - // Start the destination VM - let mut dest_api_socket = temp_api_path(&guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&guest) - .args(["--api-socket", &dest_api_socket]) + .default_net() .capture_output() .spawn() .unwrap(); + let mut child_dnsmasq = windows_guest.run_dnsmasq(); + + // Predefined data to used at various test stages + let disk_test_data: [[String; 4]; 2] = [ + [ + "_disk2".to_string(), + windows_guest.disk_new(WindowsGuest::FS_FAT, 123), + "d:\\world".to_string(), + "hello".to_string(), + ], + [ + "_disk3".to_string(), + windows_guest.disk_new(WindowsGuest::FS_NTFS, 333), + "e:\\hello".to_string(), + "world".to_string(), + ], + ]; + let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - // Make sure the source VM is functional - // Check the number of vCPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - // Check the guest virtio-devices, e.g. block, rng, console, and net - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - // x86_64: Following what's done in the `test_snapshot_restore`, we need - // to make sure that removing and adding back the virtio-net device does - // not break the live-migration support for virtio-pci. - #[cfg(target_arch = "x86_64")] - { - assert!(remote_command( - &src_api_socket, - "remove-device", - Some(net_id), - )); - thread::sleep(std::time::Duration::new(10, 0)); + // Initially present disk device + let disk_num = 1; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - // Plug the virtio-net device again - assert!(remote_command( - &src_api_socket, - "add-net", - Some(net_params.as_str()), - )); - thread::sleep(std::time::Duration::new(10, 0)); + for it in &disk_test_data { + let disk_id = it[0].as_str(); + let disk = it[1].as_str(); + + let expected_disk_num = windows_guest.disk_count() + 1; + let expected_ctrl_threads = disk_ctrl_threads_count(child.id()) + 1; + + // Hotplug disk device + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some(format!("path={disk},readonly=off").as_str()), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains(format!("\"id\":\"{disk_id}\"").as_str()) + ); + + // Wait for disk to appear + assert!(wait_until(Duration::from_secs(5), || { + windows_guest.disk_count() == expected_disk_num + && disk_ctrl_threads_count(child.id()) == expected_ctrl_threads + })); + + // Online disk devices + windows_guest.disks_set_rw(); + windows_guest.disks_online(); } + // Verify the devices are on the system + let disk_num = (disk_test_data.len() + 1) as u8; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - // Enable watchdog and ensure its functional - let expected_reboot_count = 1; - // Enable the watchdog with a 15s timeout - enable_guest_watchdog(&guest, 15); + // Put test data + for it in &disk_test_data { + let fname = it[2].as_str(); + let data = it[3].as_str(); + windows_guest.disk_file_put(fname, data); + } - assert_eq!(get_reboot_count(&guest), expected_reboot_count); - assert_eq!( - guest - .ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"") - .unwrap() - .trim() - .parse::() - .unwrap_or_default(), - 1 - ); - // Allow some normal time to elapse to check we don't get spurious reboots - thread::sleep(std::time::Duration::new(40, 0)); - // Check no reboot - assert_eq!(get_reboot_count(&guest), expected_reboot_count); + // Unmount disk devices + for it in &disk_test_data { + let disk_id = it[0].as_str(); + let cmd_success = remote_command(&api_socket, "remove-device", Some(disk_id)); + assert!(cmd_success); + } - // Start the live-migration - let migration_socket = String::from( - guest - .tmp_dir - .as_path() - .join("live-migration.sock") - .to_str() - .unwrap(), - ); + // Wait for Windows to drop all removed disks. + assert!(wait_until(Duration::from_secs(5), || windows_guest + .disk_count() + == 1 + && disk_ctrl_threads_count(child.id()) == 1)); + // Verify the devices have been removed + let disk_num = 1; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); - assert!( - start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); + // Remount + for it in &disk_test_data { + let disk = it[1].as_str(); + let (cmd_success, _cmd_output, _) = remote_command_w_output( + &api_socket, + "add-disk", + Some(format!("path={disk},readonly=off").as_str()), + ); + assert!(cmd_success); + } + + // Wait for Windows to enumerate the re-added disks. + assert!(wait_until(Duration::from_secs(5), || { + windows_guest.disk_count() == 4 && disk_ctrl_threads_count(child.id()) == 4 + })); + // Check the files exists with the expected contents + for it in &disk_test_data { + let fname = it[2].as_str(); + let data = it[3].as_str(); + let out = windows_guest.disk_file_read(fname); + assert_eq!(data, out.trim()); + } + + // Intentionally no unmount, it'll happen at shutdown. + + windows_guest.shutdown(); }); - // Check and report any errors occurred during the live-migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - None, - "Error occurred during live-migration", - ); - } + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - None, - "source VM was not terminated successfully.", - ); - }; + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - // Post live-migration check to make sure the destination VM is functional - let r = std::panic::catch_unwind(|| { - // Perform same checks to validate VM has been properly migrated - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + handle_child_output(r, &output); + } - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + #[test] + #[cfg(not(feature = "mshv"))] + #[cfg(not(target_arch = "aarch64"))] + fn test_windows_guest_netdev_multi() { + let windows_guest = WindowsGuest::new(); - // Perform checks on watchdog - let mut expected_reboot_count = 1; + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); + + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); + + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--api-socket", &api_socket]) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=4G"]) + .args(["--kernel", ovmf_path.to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) + .default_disks() + // The multi net dev config is borrowed from test_multiple_network_interfaces + .args([ + "--net", + windows_guest.guest().default_net_string().as_str(), + "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", + "tap=mytap42,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0", + ]) + .capture_output() + .spawn() + .unwrap(); + + let mut child_dnsmasq = windows_guest.run_dnsmasq(); - // Allow some normal time to elapse to check we don't get spurious reboots - thread::sleep(std::time::Duration::new(40, 0)); - // Check no reboot - assert_eq!(get_reboot_count(&guest), expected_reboot_count); + let r = std::panic::catch_unwind(|| { + // Wait to make sure Windows boots up + windows_guest.wait_for_boot().unwrap(); - // Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns. - guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); - // Allow some time for the watchdog to trigger (max 30s) and reboot to happen - guest.wait_vm_boot(Some(50)).unwrap(); - // Check a reboot is triggered by the watchdog - expected_reboot_count += 1; - assert_eq!(get_reboot_count(&guest), expected_reboot_count); + let netdev_num = 3; + assert_eq!(windows_guest.netdev_count(), netdev_num); + assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); - #[cfg(target_arch = "x86_64")] - { - // Now pause the VM and remain offline for 30s - assert!(remote_command(&dest_api_socket, "pause", None)); - thread::sleep(std::time::Duration::new(30, 0)); - assert!(remote_command(&dest_api_socket, "resume", None)); + let tap_count = exec_host_command_output("ip link | grep -c mytap42"); + assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); - // Check no reboot - assert_eq!(get_reboot_count(&guest), expected_reboot_count); - } + windows_guest.shutdown(); }); - // Clean-up the destination VM and make sure it terminated correctly - let _ = dest_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - handle_child_output(r, &dest_output); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // Check the destination VM has the expected 'console_text' from its output - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); - }); - handle_child_output(r, &dest_output); - } + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - fn _test_live_migration_ovs_dpdk(upgrade_test: bool, local: bool) { - let ovs_focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let ovs_guest = Guest::new(Box::new(ovs_focal)); + handle_child_output(r, &output); + } - let migration_focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let migration_guest = Guest::new(Box::new(migration_focal)); - let src_api_socket = temp_api_path(&migration_guest.tmp_dir); + #[test] + fn test_windows_guest_qcow2_backing_direct() { + let windows_guest = WindowsGuest::new(); - // Start two VMs that are connected through ovs-dpdk and one of the VMs is the source VM for live-migration - let (mut ovs_child, mut src_child) = - setup_ovs_dpdk_guests(&ovs_guest, &migration_guest, &src_api_socket, upgrade_test); + let qcow2_path = windows_guest.guest().disk_config.qcow2_disk().unwrap(); - // Start the destination VM - let mut dest_api_socket = temp_api_path(&migration_guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&migration_guest) - .args(["--api-socket", &dest_api_socket]) + let mut child = GuestCommand::new(windows_guest.guest()) + .args(["--cpus", "boot=2,kvm_hyperv=on"]) + .args(["--memory", "size=4G"]) + .args(["--kernel", edk2_path().to_str().unwrap()]) + .args(["--serial", "tty"]) + .args(["--console", "off"]) + .args([ + "--disk", + format!("path={qcow2_path},image_type=qcow2,backing_files=on,direct=on").as_str(), + ]) + .default_net() .capture_output() .spawn() .unwrap(); - let r = std::panic::catch_unwind(|| { - // Give it '1s' to make sure the 'dest_api_socket' file is properly created - thread::sleep(std::time::Duration::new(1, 0)); - - // Start the live-migration - let migration_socket = String::from( - migration_guest - .tmp_dir - .as_path() - .join("live-migration.sock") - .to_str() - .unwrap(), - ); - - assert!( - start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); - }); + let fd = child.stdout.as_ref().unwrap().as_raw_fd(); + let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; + let fd = child.stderr.as_ref().unwrap().as_raw_fd(); + let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; - // Check and report any errors occurred during the live-migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - Some(ovs_child), - "Error occurred during live-migration", - ); - } + assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - Some(ovs_child), - "source VM was not terminated successfully.", - ); - }; + let mut child_dnsmasq = windows_guest.run_dnsmasq(); - // Post live-migration check to make sure the destination VM is functional let r = std::panic::catch_unwind(|| { - // Perform same checks to validate VM has been properly migrated - // Spawn a new netcat listener in the OVS VM - let guest_ip = ovs_guest.network.guest_ip.clone(); - thread::spawn(move || { - ssh_command_ip( - "nc -l 12345", - &guest_ip, - DEFAULT_SSH_RETRIES, - DEFAULT_SSH_TIMEOUT, - ) - .unwrap(); - }); - - // Wait for the server to be listening - thread::sleep(std::time::Duration::new(5, 0)); + windows_guest.wait_for_boot().unwrap(); + + // Write and read back files through qcow2 + direct I/O. + for i in 0..5 { + let fname = format!("c:\\test-dio-{i}.bin"); + let fname2 = format!("c:\\test-dio-{i}-copy.bin"); + let size = (i + 1) * 4 * 1024 * 1024; + windows_guest.ssh_cmd(&format!( + "powershell -Command \"\ + $r = New-Object byte[] {size}; \ + (New-Object Random {i}).NextBytes($r); \ + [IO.File]::WriteAllBytes('{fname}', $r)\"" + )); + let hash_write = windows_guest.ssh_cmd(&format!( + "powershell -Command \"(Get-FileHash '{fname}' -Algorithm SHA256).Hash\"" + )); + windows_guest.ssh_cmd(&format!("copy {fname} {fname2}")); + let hash_read = windows_guest.ssh_cmd(&format!( + "powershell -Command \"(Get-FileHash '{fname2}' -Algorithm SHA256).Hash\"" + )); + assert_eq!(hash_write.trim(), hash_read.trim()); + } - // And check the connection is still functional after live-migration - migration_guest - .ssh_command("nc -vz 172.100.0.1 12345") - .unwrap(); + windows_guest.shutdown(); }); - // Clean-up the destination VM and OVS VM, and make sure they terminated correctly - let _ = dest_child.kill(); - let _ = ovs_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - let ovs_output = ovs_child.wait_with_output().unwrap(); + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - cleanup_ovs_dpdk(); + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); - handle_child_output(r, &dest_output); - handle_child_output(Ok(()), &ovs_output); + handle_child_output(r, &output); } +} - // This test exercises the local live-migration between two Cloud Hypervisor VMs on the - // same host with Landlock enabled on both VMs. The test validates the following: - // 1. The source VM is up and functional - // 2. Ensure Landlock is enabled on source VM by hotplugging a disk. As the path for this - // disk is not known to the source VM this step will fail. - // 3. The 'send-migration' and 'receive-migration' command finished successfully; - // 4. The source VM terminated gracefully after live migration; - // 5. The destination VM is functional after live migration; - // 6. Ensure Landlock is enabled on destination VM by hotplugging a disk. As the path for - // this disk is not known to the destination VM this step will fail. - fn _test_live_migration_with_landlock() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let kernel_path = direct_kernel_boot_path(); - let net_id = "net123"; - let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip - ); - - let boot_vcpus = 2; - let max_vcpus = 4; +#[cfg(target_arch = "x86_64")] +mod vfio { + use crate::*; - let mut blk_file_path = dirs::home_dir().unwrap(); - blk_file_path.push("workloads"); - blk_file_path.push("blk.img"); + const NVIDIA_VFIO_DEVICE: &str = "/sys/bus/pci/devices/0002:00:01.0"; + const IORESOURCE_MEM: u64 = 0x0000_0200; + const IORESOURCE_PREFETCH: u64 = 0x0000_2000; - let src_api_socket = temp_api_path(&guest.tmp_dir); - let mut src_child = GuestCommand::new(&guest) - .args([ - "--cpus", - format!("boot={boot_vcpus},max={max_vcpus}").as_str(), - ]) - .args(["--memory", "size=4G,shared=on"]) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) - .default_disks() - .args(["--api-socket", &src_api_socket]) - .args(["--landlock"]) - .args(["--net", net_params.as_str()]) - .args([ - "--landlock-rules", - format!("path={:?},access=rw", guest.tmp_dir.as_path()).as_str(), - ]) - .capture_output() - .spawn() - .unwrap(); + fn nvidia_vfio_device_ready() -> bool { + if !std::path::Path::new(NVIDIA_VFIO_DEVICE).exists() { + println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not found"); + return false; + } - // Start the destination VM - let mut dest_api_socket = temp_api_path(&guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&guest) - .args(["--api-socket", &dest_api_socket]) - .capture_output() - .spawn() - .unwrap(); + let driver_path = format!("{NVIDIA_VFIO_DEVICE}/driver"); + if let Ok(driver) = std::fs::read_link(&driver_path) { + let driver_name = driver.file_name().unwrap_or_default().to_string_lossy(); + if driver_name != "vfio-pci" { + println!( + "SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} bound to {driver_name}, not vfio-pci" + ); + return false; + } + } else { + println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not bound to any driver"); + return false; + } - let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + true + } - // Make sure the source VM is functaionl - // Check the number of vCPUs - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + fn largest_nvidia_prefetchable_memory_bar() -> Option { + let resource_path = format!("{NVIDIA_VFIO_DEVICE}/resource"); + let resource = match std::fs::read_to_string(&resource_path) { + Ok(resource) => resource, + Err(e) => { + println!("SKIPPED: failed to read {resource_path}: {e}"); + return None; + } + }; - // Check the guest RAM - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + let mut selected_bar = None; + let mut selected_size = 0; + for (index, line) in resource.lines().take(6).enumerate() { + let mut fields = line.split_whitespace(); + let Some(start) = fields.next() else { + continue; + }; + let Some(end) = fields.next() else { + continue; + }; + let Some(flags) = fields.next() else { + continue; + }; - // Check Landlock is enabled by hot-plugging a disk. - assert!(!remote_command( - &src_api_socket, - "add-disk", - Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), - )); + let parse_hex = |value: &str| u64::from_str_radix(value.trim_start_matches("0x"), 16); + let Ok(start) = parse_hex(start) else { + continue; + }; + let Ok(end) = parse_hex(end) else { + continue; + }; + let Ok(flags) = parse_hex(flags) else { + continue; + }; - // Start the live-migration - let migration_socket = String::from( - guest - .tmp_dir - .as_path() - .join("live-migration.sock") - .to_str() - .unwrap(), - ); + if flags & IORESOURCE_MEM == 0 || end < start || (start == 0 && end == 0) { + continue; + } + if flags & IORESOURCE_PREFETCH == 0 { + continue; + } - assert!( - start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, true), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); - }); + let size = end - start + 1; + if size > selected_size { + selected_bar = Some(index as u8); + selected_size = size; + } + } - // Check and report any errors occurred during the live-migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - None, - "Error occurred during live-migration", + if selected_bar.is_none() { + println!( + "SKIPPED: no non-empty prefetchable memory BAR found for {NVIDIA_VFIO_DEVICE}" ); } + selected_bar + } + + fn platform_cfg(iommufd: bool) -> String { + if iommufd { + "iommufd=on,vfio_p2p_dma=off".to_string() + } else { + "iommufd=off".to_string() + } + } - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - None, - "source VM was not terminated successfully.", - ); - }; + fn test_nvidia_card_memory_hotplug(hotplug_method: &str, iommufd: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args([ + "--memory", + format!("size=4G,hotplug_size=4G,hotplug_method={hotplug_method}").as_str(), + ]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--platform", &platform_cfg(iommufd)]) + .args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()]) + .args(["--api-socket", &api_socket]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - // Post live-migration check to make sure the destination VM is funcational let r = std::panic::catch_unwind(|| { - // Perform same checks to validate VM has been properly migrated - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); + guest.wait_vm_boot().unwrap(); + assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); + + // Verify the VFIO device works before memory hotplug + guest.check_nvidia_gpu(); + + guest.enable_memory_hotplug(); + + // Add RAM to the VM + let desired_ram = 6 << 30; + resize_command(&api_socket, None, Some(desired_ram), None, None); + assert!(wait_until(Duration::from_secs(15), || { + guest.get_total_memory().unwrap_or_default() > 5_760_000 + })); + + // Check the VFIO device works when RAM is increased to 6GiB. + // After guest memory hotplug, the VMM must refresh VFIO/iommufd DMA + // mappings for the passthrough GPU. + assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu())); }); - // Check Landlock is enabled on destination VM by hot-plugging a disk. - assert!(!remote_command( - &dest_api_socket, - "add-disk", - Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), - )); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - // Clean-up the destination VM and make sure it terminated correctly - let _ = dest_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - handle_child_output(r, &dest_output); + handle_child_output(r, &output); } - // Function to get an available port - fn get_available_port() -> u16 { - TcpListener::bind("127.0.0.1:0") - .expect("Failed to bind to address") - .local_addr() - .unwrap() - .port() + #[test] + fn test_nvidia_card_memory_hotplug_acpi() { + test_nvidia_card_memory_hotplug("acpi", false); } - fn start_live_migration_tcp(src_api_socket: &str, dest_api_socket: &str) -> bool { - // Get an available TCP port - let migration_port = get_available_port(); - let host_ip = "127.0.0.1"; + #[test] + fn test_nvidia_card_memory_hotplug_virtio_mem() { + test_nvidia_card_memory_hotplug("virtio-mem", false); + } - // Start the 'receive-migration' command on the destination - let mut receive_migration = Command::new(clh_command("ch-remote")) - .args([ - &format!("--api-socket={dest_api_socket}"), - "receive-migration", - &format!("tcp:0.0.0.0:{migration_port}"), - ]) - .stdin(Stdio::null()) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap(); + #[test] + fn test_iommufd_nvidia_card_memory_hotplug_acpi() { + test_nvidia_card_memory_hotplug("acpi", true); + } - // Give the destination some time to start listening - thread::sleep(Duration::from_secs(1)); + #[test] + fn test_iommufd_nvidia_card_memory_hotplug_virtio_mem() { + test_nvidia_card_memory_hotplug("virtio-mem", true); + } - // Start the 'send-migration' command on the source - let mut send_migration = Command::new(clh_command("ch-remote")) - .args([ - &format!("--api-socket={src_api_socket}"), - "send-migration", - &format!("tcp:{host_ip}:{migration_port}"), - ]) - .stdin(Stdio::null()) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) + fn test_nvidia_card_pci_hotplug_common(iommufd: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args(["--memory", "size=1G"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--platform", &platform_cfg(iommufd)]) + .args(["--api-socket", &api_socket]) + .default_disks() + .default_net() + .capture_output() .spawn() .unwrap(); - // Check if the 'send-migration' command executed successfully - let send_success = if let Some(status) = send_migration - .wait_timeout(Duration::from_secs(60)) - .unwrap() - { - status.success() - } else { - false - }; + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - if !send_success { - let _ = send_migration.kill(); - let output = send_migration.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'send_migration' output ====\n\n---stdout---\n{}\n\n---stderr---\n{}\n\n==== End 'send_migration' output ====\n\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) + // Hotplug the card to the VM + let (cmd_success, cmd_output, _) = remote_command_w_output( + &api_socket, + "add-device", + Some(format!("id=vfio0,path={NVIDIA_VFIO_DEVICE}").as_str()), + ); + assert!(cmd_success); + assert!( + String::from_utf8_lossy(&cmd_output) + .contains("{\"id\":\"vfio0\",\"bdf\":\"0000:00:06.0\"}") ); - } - // Check if the 'receive-migration' command executed successfully - let receive_success = if let Some(status) = receive_migration - .wait_timeout(Duration::from_secs(60)) - .unwrap() - { - status.success() - } else { - false - }; + // Check the VFIO device works after hotplug + assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu())); + }); - if !receive_success { - let _ = receive_migration.kill(); - let output = receive_migration.wait_with_output().unwrap(); - eprintln!( - "\n\n==== Start 'receive_migration' output ====\n\n---stdout---\n{}\n\n---stderr---\n{}\n\n==== End 'receive_migration' output ====\n\n", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); - send_success && receive_success + handle_child_output(r, &output); } - fn _test_live_migration_tcp() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); - let kernel_path = direct_kernel_boot_path(); - let console_text = String::from("On a branch floating down river a cricket, singing."); - let net_id = "net123"; - let net_params = format!( - "id={},tap=,mac={},ip={},mask=255.255.255.0", - net_id, guest.network.guest_mac, guest.network.host_ip - ); - let memory_param: &[&str] = &["--memory", "size=4G,shared=on"]; - let boot_vcpus = 2; - let max_vcpus = 4; - let pmem_temp_file = TempFile::new().unwrap(); - pmem_temp_file.as_file().set_len(128 << 20).unwrap(); - std::process::Command::new("mkfs.ext4") - .arg(pmem_temp_file.as_path()) - .output() - .expect("Expect creating disk image to succeed"); - let pmem_path = String::from("/dev/pmem0"); + #[test] + fn test_nvidia_card_pci_hotplug() { + test_nvidia_card_pci_hotplug_common(false); + } - // Start the source VM - let src_vm_path = clh_command("cloud-hypervisor"); - let src_api_socket = temp_api_path(&guest.tmp_dir); - let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); - src_vm_cmd + #[test] + fn test_iommufd_nvidia_card_pci_hotplug() { + test_nvidia_card_pci_hotplug_common(true); + } + + fn test_nvidia_card_reboot_common(iommufd: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args(["--memory", "size=1G"]) + .args(["--platform", &platform_cfg(iommufd)]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) .args([ - "--cpus", - format!("boot={boot_vcpus},max={max_vcpus}").as_str(), + "--device", + format!("path={NVIDIA_VFIO_DEVICE},iommu=on").as_str(), ]) - .args(memory_param) - .args(["--kernel", kernel_path.to_str().unwrap()]) - .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--api-socket", &api_socket]) .default_disks() - .args(["--net", net_params.as_str()]) - .args(["--api-socket", &src_api_socket]) - .args([ - "--pmem", - format!( - "file={},discard_writes=on", - pmem_temp_file.as_path().to_str().unwrap(), - ) - .as_str(), - ]) - .capture_output(); - let mut src_child = src_vm_cmd.spawn().unwrap(); - - // Start the destination VM - let mut dest_api_socket = temp_api_path(&guest.tmp_dir); - dest_api_socket.push_str(".dest"); - let mut dest_child = GuestCommand::new(&guest) - .args(["--api-socket", &dest_api_socket]) + .default_net() .capture_output() .spawn() .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - // Ensure the source VM is running normally - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); + guest.wait_vm_boot().unwrap(); - // On x86_64 architecture, remove and re-add the virtio-net device - #[cfg(target_arch = "x86_64")] - { - assert!(remote_command( - &src_api_socket, - "remove-device", - Some(net_id), - )); - thread::sleep(Duration::new(10, 0)); - // Re-add the virtio-net device - assert!(remote_command( - &src_api_socket, - "add-net", - Some(net_params.as_str()), - )); - thread::sleep(Duration::new(10, 0)); - } - // Start TCP live migration - assert!( - start_live_migration_tcp(&src_api_socket, &dest_api_socket), - "Unsuccessful command: 'send-migration' or 'receive-migration'." - ); + // Check the VFIO device works after boot + assert!(guest.check_nvidia_gpu()); + + guest.reboot_linux(0); + + // Check the VFIO device works after reboot + assert!(guest.check_nvidia_gpu()); }); - // Check and report any errors that occurred during live migration - if r.is_err() { - print_and_panic( - src_child, - dest_child, - None, - "Error occurred during live-migration", - ); - } + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + } + + #[test] + fn test_nvidia_card_reboot() { + test_nvidia_card_reboot_common(false); + } + + #[test] + fn test_iommufd_nvidia_card_reboot() { + test_nvidia_card_reboot_common(true); + } + + fn test_nvidia_card_iommu_address_width_common(iommufd: bool) { + let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); + + let platform = format!( + "num_pci_segments=2,iommu_segments=1,iommu_address_width=42,{}", + platform_cfg(iommufd) + ); - // Check the source vm has been terminated successful (give it '3s' to settle) - thread::sleep(std::time::Duration::new(3, 0)); - if !src_child.try_wait().unwrap().is_some_and(|s| s.success()) { - print_and_panic( - src_child, - dest_child, - None, - "Source VM was not terminated successfully.", - ); - }; + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args(["--memory", "size=1G"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()]) + .args(["--platform", &platform]) + .args(["--api-socket", &api_socket]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - // After live migration, ensure the destination VM is running normally let r = std::panic::catch_unwind(|| { - // Perform the same checks to ensure the VM has migrated correctly - assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); - assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); - guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); - }); + guest.wait_vm_boot().unwrap(); - // Clean up the destination VM and ensure it terminates properly - let _ = dest_child.kill(); - let dest_output = dest_child.wait_with_output().unwrap(); - handle_child_output(r, &dest_output); + assert!( + guest + .ssh_command("sudo dmesg") + .unwrap() + .contains("input address: 42 bits") + ); - // Check if the expected `console_text` is present in the destination VM's output - let r = std::panic::catch_unwind(|| { - assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); + // Check the VFIO device works after boot + guest.check_nvidia_gpu(); }); - handle_child_output(r, &dest_output); + + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); } - mod live_migration_parallel { - use super::*; - #[test] - fn test_live_migration_basic() { - _test_live_migration(false, false) - } + #[test] + fn test_nvidia_card_iommu_address_width() { + test_nvidia_card_iommu_address_width_common(false); + } - #[test] - fn test_live_migration_local() { - _test_live_migration(false, true) - } + #[test] + fn test_iommufd_nvidia_card_iommu_address_width() { + test_nvidia_card_iommu_address_width_common(true); + } - #[test] - fn test_live_migration_tcp() { - _test_live_migration_tcp(); + fn test_nvidia_card_x_exclude_mmap_bars_common(iommufd: bool) { + if !nvidia_vfio_device_ready() { + return; } - #[test] - fn test_live_migration_watchdog() { - _test_live_migration_watchdog(false, false) - } + let Some(bar) = largest_nvidia_prefetchable_memory_bar() else { + return; + }; - #[test] - fn test_live_migration_watchdog_local() { - _test_live_migration_watchdog(false, true) - } + let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - #[test] - fn test_live_upgrade_basic() { - _test_live_migration(true, false) - } + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args(["--memory", "size=1G"]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--platform", &platform_cfg(iommufd)]) + .args([ + "--device", + format!("path={NVIDIA_VFIO_DEVICE},x_exclude_mmap_bars=[{bar}]").as_str(), + ]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); - #[test] - fn test_live_upgrade_local() { - _test_live_migration(true, true) - } + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu())); + }); - #[test] - fn test_live_upgrade_watchdog() { - _test_live_migration_watchdog(true, false) - } + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); - #[test] - fn test_live_upgrade_watchdog_local() { - _test_live_migration_watchdog(true, true) - } - #[test] - #[cfg(target_arch = "x86_64")] - fn test_live_migration_with_landlock() { - _test_live_migration_with_landlock() - } + assert!( + stderr.contains("Skipping VFIO BAR mmap"), + "Expected x_exclude_mmap_bars log in stderr: {stderr}" + ); + assert!( + stderr.contains(format!("BAR {bar}").as_str()), + "Expected skipped BAR index in stderr: {stderr}" + ); + + handle_child_output(r, &output); } - mod live_migration_sequential { - use super::*; + #[test] + fn test_nvidia_card_x_exclude_mmap_bars() { + test_nvidia_card_x_exclude_mmap_bars_common(false); + } - // NUMA & balloon live migration tests are large so run sequentially + #[test] + fn test_iommufd_nvidia_card_x_exclude_mmap_bars() { + test_nvidia_card_x_exclude_mmap_bars_common(true); + } - #[test] - fn test_live_migration_balloon() { - _test_live_migration_balloon(false, false) + fn test_nvidia_guest_numa_generic_initiator_common(iommufd: bool) { + if !nvidia_vfio_device_ready() { + return; } - #[test] - fn test_live_migration_balloon_local() { - _test_live_migration_balloon(false, true) - } + let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let api_socket = temp_api_path(&guest.tmp_dir); - #[test] - fn test_live_upgrade_balloon() { - _test_live_migration_balloon(true, false) - } + // x86_64: Direct kernel boot + let mut child = GuestCommand::new(&guest) + .args(["--cpus", "boot=4"]) + .args(["--memory", "size=0"]) + .args(["--memory-zone", "id=mem0,size=1G", "id=mem1,size=1G"]) + .args([ + "--numa", + "guest_numa_id=0,cpus=[0-1],distances=[1@20,2@25],memory_zones=mem0", + "guest_numa_id=1,cpus=[2-3],distances=[0@20,2@30],memory_zones=mem1", + "guest_numa_id=2,device_id=vfio0,distances=[0@25,1@30]", + ]) + .args(["--platform", &platform_cfg(iommufd)]) + .args([ + "--device", + &format!("id=vfio0,path={NVIDIA_VFIO_DEVICE},iommu=on"), + ]) + .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) + .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) + .args(["--api-socket", &api_socket]) + .capture_output() + .default_disks() + .default_net() + .spawn() + .unwrap(); - #[test] - fn test_live_upgrade_balloon_local() { - _test_live_migration_balloon(true, true) - } + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); - #[test] - #[cfg(not(feature = "mshv"))] - fn test_live_migration_numa() { - _test_live_migration_numa(false, false) - } + // Verify NUMA topology is correct + guest.check_numa_common( + Some(&[960_000, 960_000]), + Some(&[&[0, 1], &[2, 3]]), + Some(&["10 20 25", "20 10 30", "25 30 10"]), + ); - #[test] - #[cfg(not(feature = "mshv"))] - fn test_live_migration_numa_local() { - _test_live_migration_numa(false, true) - } + // Verify Generic Initiator support is present + // Linux kernel sets has_generic_initiator when it parses Type 5 SRAT entries + let has_gi = guest + .ssh_command( + "cat /sys/devices/system/node/has_generic_initiator 2>/dev/null || echo 0", + ) + .unwrap() + .trim() + .to_string(); - #[test] - #[cfg(not(feature = "mshv"))] - fn test_live_upgrade_numa() { - _test_live_migration_numa(true, false) - } + assert_eq!( + has_gi, "2", + "Generic Initiator support should be detected by kernel" + ); - #[test] - #[cfg(not(feature = "mshv"))] - fn test_live_upgrade_numa_local() { - _test_live_migration_numa(true, true) - } + // Verify SRAT table contains Generic Initiator entry (Type 5) + // We'll check that /sys/firmware/acpi/tables/SRAT exists and contains our entry + let srat_check = guest + .ssh_command( + "[ -f /sys/firmware/acpi/tables/SRAT ] && echo 'exists' || echo 'missing'", + ) + .unwrap() + .trim() + .to_string(); - // Require to run ovs-dpdk tests sequentially because they rely on the same ovs-dpdk setup - #[test] - #[ignore = "See #5532"] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - fn test_live_migration_ovs_dpdk() { - _test_live_migration_ovs_dpdk(false, false); - } + assert_eq!( + srat_check, "exists", + "SRAT table should exist in guest firmware" + ); - #[test] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - fn test_live_migration_ovs_dpdk_local() { - _test_live_migration_ovs_dpdk(false, true); - } + // Use hexdump to verify Type 5 entry is present + // Type 5 (0x05) should appear in the SRAT table + let srat_has_type5 = guest + .ssh_command("sudo hexdump -C /sys/firmware/acpi/tables/SRAT | grep -q '05 20' && echo 'found' || echo 'not_found'") + .unwrap() + .trim() + .to_string(); - #[test] - #[ignore = "See #5532"] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - fn test_live_upgrade_ovs_dpdk() { - _test_live_migration_ovs_dpdk(true, false); - } + assert_eq!( + srat_has_type5, "found", + "SRAT table should contain Generic Initiator Affinity Structure (Type 5, Length 0x20/32)" + ); + }); - #[test] - #[ignore = "See #5532"] - #[cfg(target_arch = "x86_64")] - #[cfg(not(feature = "mshv"))] - fn test_live_upgrade_ovs_dpdk_local() { - _test_live_migration_ovs_dpdk(true, true); - } + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + } + + #[test] + fn test_nvidia_guest_numa_generic_initiator() { + test_nvidia_guest_numa_generic_initiator_common(false); + } + + #[test] + fn test_iommufd_nvidia_guest_numa_generic_initiator() { + test_nvidia_guest_numa_generic_initiator_common(true); } } @@ -10715,14 +11171,14 @@ mod aarch64_acpi { #[test] fn test_simple_launch_acpi() { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); + let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); - vec![Box::new(focal)].drain(..).for_each(|disk_config| { + vec![Box::new(jammy)].drain(..).for_each(|disk_config| { let guest = Guest::new(disk_config); let mut child = GuestCommand::new(&guest) - .args(["--cpus", "boot=1"]) - .args(["--memory", "size=512M"]) + .default_cpus() + .default_memory() .args(["--kernel", edk2_path().to_str().unwrap()]) .default_disks() .default_net() @@ -10732,7 +11188,7 @@ mod aarch64_acpi { .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(Some(120)).unwrap(); + guest.wait_vm_boot().unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); assert!(guest.get_total_memory().unwrap_or_default() > 400_000); @@ -10768,18 +11224,27 @@ mod aarch64_acpi { #[test] fn test_power_button_acpi() { - _test_power_button(true); + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = GuestFactory::new_regular_guest_factory() + .create_guest(Box::new(disk_config)) + .with_kernel_path(edk2_path().to_str().unwrap()); + _test_power_button(&guest); } #[test] + #[cfg_attr(target_arch = "aarch64", ignore = "See #8187")] fn test_virtio_iommu() { - _test_virtio_iommu(true) + _test_virtio_iommu(true); } } mod rate_limiter { use super::*; + const NET_RATE_LIMITER_RUNTIME: u32 = 20; + const BLOCK_RATE_LIMITER_RUNTIME: u32 = 20; + const BLOCK_RATE_LIMITER_RAMP_TIME: u32 = 5; + // Check if the 'measured' rate is within the expected 'difference' (in percentage) // compared to given 'limit' rate. fn check_rate_limit(measured: f64, limit: f64, difference: f64) -> bool { @@ -10800,20 +11265,19 @@ mod rate_limiter { } fn _test_rate_limiter_net(rx: bool) { - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); - let test_timeout = 10; let num_queues = 2; let queue_size = 256; - let bw_size = 10485760_u64; // bytes - let bw_refill_time = 100; // ms + let bw_size = 104857600_u64; // bytes + let bw_refill_time = 1000; // ms let limit_bps = (bw_size * 8 * 1000) as f64 / bw_refill_time as f64; let net_params = format!( - "tap=,mac={},ip={},mask=255.255.255.0,num_queues={},queue_size={},bw_size={},bw_refill_time={}", - guest.network.guest_mac, - guest.network.host_ip, + "tap=,mac={},ip={},mask=255.255.255.128,num_queues={},queue_size={},bw_size={},bw_one_time_burst=0,bw_refill_time={}", + guest.network.guest_mac0, + guest.network.host_ip0, num_queues, queue_size, bw_size, @@ -10822,7 +11286,7 @@ mod rate_limiter { let mut child = GuestCommand::new(&guest) .args(["--cpus", &format!("boot={}", num_queues / 2)]) - .args(["--memory", "size=4G"]) + .args(["--memory", "size=1G"]) .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() @@ -10832,10 +11296,15 @@ mod rate_limiter { .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); - let measured_bps = - measure_virtio_net_throughput(test_timeout, num_queues / 2, &guest, rx, true) - .unwrap(); + guest.wait_vm_boot().unwrap(); + let measured_bps = measure_virtio_net_throughput( + NET_RATE_LIMITER_RUNTIME, + num_queues / 2, + &guest, + rx, + true, + ) + .unwrap(); assert!(check_rate_limit(measured_bps, limit_bps, 0.1)); }); @@ -10855,44 +11324,45 @@ mod rate_limiter { } fn _test_rate_limiter_block(bandwidth: bool, num_queues: u32) { - let test_timeout = 10; let fio_ops = FioOps::RandRW; let bw_size = if bandwidth { - 10485760_u64 // bytes + 104857600_u64 // bytes } else { - 100_u64 // I/O + 1000_u64 // I/O }; - let bw_refill_time = 100; // ms + let bw_refill_time = 1000; // ms let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64; - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let api_socket = temp_api_path(&guest.tmp_dir); let test_img_dir = TempDir::new_with_prefix("/var/tmp/ch").unwrap(); let blk_rate_limiter_test_img = String::from(test_img_dir.as_path().join("blk.img").to_str().unwrap()); // Create the test block image - assert!(exec_host_command_output(&format!( - "dd if=/dev/zero of={blk_rate_limiter_test_img} bs=1M count=1024" - )) - .status - .success()); + assert!( + exec_host_command_output(&format!( + "dd if=/dev/zero of={blk_rate_limiter_test_img} bs=1M count=1024" + )) + .status + .success() + ); let test_blk_params = if bandwidth { format!( - "path={blk_rate_limiter_test_img},num_queues={num_queues},bw_size={bw_size},bw_refill_time={bw_refill_time}" + "path={blk_rate_limiter_test_img},num_queues={num_queues},bw_size={bw_size},bw_one_time_burst=0,bw_refill_time={bw_refill_time},image_type=raw" ) } else { format!( - "path={blk_rate_limiter_test_img},num_queues={num_queues},ops_size={bw_size},ops_refill_time={bw_refill_time}" + "path={blk_rate_limiter_test_img},num_queues={num_queues},ops_size={bw_size},ops_one_time_burst=0,ops_refill_time={bw_refill_time},image_type=raw" ) }; let mut child = GuestCommand::new(&guest) .args(["--cpus", &format!("boot={num_queues}")]) - .args(["--memory", "size=4G"]) + .args(["--memory", "size=1G"]) .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args([ @@ -10916,12 +11386,13 @@ mod rate_limiter { .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); let fio_command = format!( "sudo fio --filename=/dev/vdc --name=test --output-format=json \ --direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \ - --rw={fio_ops} --runtime={test_timeout} --numjobs={num_queues}" + --rw={fio_ops} --runtime={BLOCK_RATE_LIMITER_RUNTIME} \ + --ramp_time={BLOCK_RATE_LIMITER_RAMP_TIME} --numjobs={num_queues}", ); let output = guest.ssh_command(&fio_command).unwrap(); @@ -10940,26 +11411,29 @@ mod rate_limiter { } fn _test_rate_limiter_group_block(bandwidth: bool, num_queues: u32, num_disks: u32) { - let test_timeout = 10; let fio_ops = FioOps::RandRW; let bw_size = if bandwidth { - 10485760_u64 // bytes + 104857600_u64 // bytes } else { - 100_u64 // I/O + 1000_u64 // I/O }; - let bw_refill_time = 100; // ms + let bw_refill_time = 1000; // ms let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64; - let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); - let guest = Guest::new(Box::new(focal)); + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); let api_socket = temp_api_path(&guest.tmp_dir); let test_img_dir = TempDir::new_with_prefix("/var/tmp/ch").unwrap(); let rate_limit_group_arg = if bandwidth { - format!("id=group0,bw_size={bw_size},bw_refill_time={bw_refill_time}") + format!( + "id=group0,bw_size={bw_size},bw_one_time_burst=0,bw_refill_time={bw_refill_time}" + ) } else { - format!("id=group0,ops_size={bw_size},ops_refill_time={bw_refill_time}") + format!( + "id=group0,ops_size={bw_size},ops_one_time_burst=0,ops_refill_time={bw_refill_time}" + ) }; let mut disk_args = vec![ @@ -10983,20 +11457,22 @@ mod rate_limiter { .unwrap(), ); - assert!(exec_host_command_output(&format!( - "dd if=/dev/zero of={test_img_path} bs=1M count=1024" - )) - .status - .success()); + assert!( + exec_host_command_output(&format!( + "dd if=/dev/zero of={test_img_path} bs=1M count=1024" + )) + .status + .success() + ); disk_args.push(format!( - "path={test_img_path},num_queues={num_queues},rate_limit_group=group0" + "path={test_img_path},num_queues={num_queues},rate_limit_group=group0,image_type=raw" )); } let mut child = GuestCommand::new(&guest) .args(["--cpus", &format!("boot={}", num_queues * num_disks)]) - .args(["--memory", "size=4G"]) + .args(["--memory", "size=1G"]) .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args(["--rate-limit-group", &rate_limit_group_arg]) @@ -11008,12 +11484,13 @@ mod rate_limiter { .unwrap(); let r = std::panic::catch_unwind(|| { - guest.wait_vm_boot(None).unwrap(); + guest.wait_vm_boot().unwrap(); let mut fio_command = format!( "sudo fio --name=global --output-format=json \ --direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \ - --rw={fio_ops} --runtime={test_timeout} --numjobs={num_queues}" + --rw={fio_ops} --runtime={BLOCK_RATE_LIMITER_RUNTIME} \ + --ramp_time={BLOCK_RATE_LIMITER_RAMP_TIME} --numjobs={num_queues}", ); // Generate additional argument for each disk: @@ -11048,7 +11525,7 @@ mod rate_limiter { #[test] fn test_rate_limiter_block_bandwidth() { _test_rate_limiter_block(true, 1); - _test_rate_limiter_block(true, 2) + _test_rate_limiter_block(true, 2); } #[test] @@ -11073,3 +11550,97 @@ mod rate_limiter { _test_rate_limiter_group_block(false, 2, 2); } } + +#[cfg(not(target_arch = "riscv64"))] +mod fw_cfg { + use crate::*; + + #[test] + #[cfg_attr(feature = "mshv", ignore = "See #7434")] + fn test_fw_cfg() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut cmd = GuestCommand::new(&guest); + + let kernel_path = direct_kernel_boot_path(); + let cmd_line = DIRECT_KERNEL_BOOT_CMDLINE; + + let test_file = guest.tmp_dir.as_path().join("test-file"); + std::fs::write(&test_file, "test-file-content").unwrap(); + + cmd.args(["--cpus", "boot=4"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", cmd_line]) + .default_disks() + .default_net() + .args([ + "--fw-cfg-config", + &format!( + "initramfs=off,items=[name=opt/org.test/test-file,file={}]", + test_file.to_str().unwrap() + ), + ]) + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + // Wait a while for guest + thread::sleep(std::time::Duration::new(3, 0)); + let result = guest + .ssh_command( + "sudo cat /sys/firmware/qemu_fw_cfg/by_name/opt/org.test/test-file/raw", + ) + .unwrap(); + assert_eq!(result, "test-file-content"); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + } + + #[test] + #[cfg_attr(feature = "mshv", ignore = "See #7434")] + fn test_fw_cfg_string() { + let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); + let guest = Guest::new(Box::new(disk_config)); + let mut cmd = GuestCommand::new(&guest); + + let kernel_path = direct_kernel_boot_path(); + let cmd_line = DIRECT_KERNEL_BOOT_CMDLINE; + + cmd.args(["--cpus", "boot=4"]) + .default_memory() + .args(["--kernel", kernel_path.to_str().unwrap()]) + .args(["--cmdline", cmd_line]) + .default_disks() + .default_net() + .args([ + "--fw-cfg-config", + "initramfs=off,items=[name=opt/org.test/test-string,string=hello-from-vmm]", + ]) + .capture_output(); + + let mut child = cmd.spawn().unwrap(); + + let r = std::panic::catch_unwind(|| { + guest.wait_vm_boot().unwrap(); + thread::sleep(std::time::Duration::new(3, 0)); + let result = guest + .ssh_command( + "sudo cat /sys/firmware/qemu_fw_cfg/by_name/opt/org.test/test-string/raw", + ) + .unwrap(); + assert_eq!(result, "hello-from-vmm"); + }); + + kill_child(&mut child); + let output = child.wait_with_output().unwrap(); + + handle_child_output(r, &output); + } +} diff --git a/cloud-hypervisor/tests/integration_cvm.rs b/cloud-hypervisor/tests/integration_cvm.rs new file mode 100644 index 0000000000..1b6bd0c4bf --- /dev/null +++ b/cloud-hypervisor/tests/integration_cvm.rs @@ -0,0 +1,333 @@ +// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +#![cfg(any(devcli_testenv, clippy))] +#![allow(clippy::undocumented_unsafe_blocks)] +// When enabling the `mshv` feature, we skip quite some tests and +// hence have known dead-code. This annotation silences dead-code +// related warnings for our quality workflow to pass. +#![allow(dead_code)] +mod common; + +#[cfg(all(feature = "sev_snp", target_arch = "x86_64"))] +mod common_cvm { + use block::ImageType; + use common::tests_wrappers::*; + use common::utils::*; + use test_infra::*; + const NUM_PCI_SEGMENTS: u16 = 8; + + use super::*; + macro_rules! basic_cvm_guest { + ($image_name:expr) => {{ + let disk_config = UbuntuDiskConfig::new($image_name.to_string()); + GuestFactory::new_confidential_guest_factory().create_guest(Box::new(disk_config)) + }}; + } + + #[test] + fn test_jammy_simple_launch() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + + _test_simple_launch(&guest); + } + + #[test] + fn test_api_http_create_boot() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(4); + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_create_boot(&target_api, &guest); + } + + #[test] + fn test_api_http_shutdown() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(4); + + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_shutdown(&target_api, &guest); + } + + #[test] + fn test_api_http_delete() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + let target_api = TargetApi::new_http_api(&guest.tmp_dir); + _test_api_delete(&target_api, &guest); + } + + #[test] + fn test_power_button() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_power_button(&guest); + } + + #[test] + fn test_virtio_vsock() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_virtio_vsock(&guest, false); + } + + #[test] + fn test_multi_cpu() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_multi_cpu(&guest); + } + + #[test] + fn test_cpu_affinity() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_cpu_affinity(&guest); + } + + #[test] + fn test_virtio_queue_affinity() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(4); + _test_virtio_queue_affinity(&guest); + } + + #[test] + fn test_pci_msi() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_pci_msi(&guest); + } + + #[test] + fn test_virtio_net_ctrl_queue() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_virtio_net_ctrl_queue(&guest); + } + + #[test] + fn test_pci_multiple_segments() { + // Use 8 segments to test the multiple segment support since it's more than the default 6 + // supported by Linux + // IGVM file used by Sev-Snp Guest now support up to 8 segments, so we can use 8 segments for testing. + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_pci_multiple_segments(&guest, NUM_PCI_SEGMENTS, 5); + } + + #[test] + fn test_direct_kernel_boot() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_direct_kernel_boot(&guest); + } + + #[test] + fn test_virtio_block_io_uring() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME, + ); + _test_virtio_block(&guest, false, true, false, false, ImageType::Raw); + } + + #[test] + fn test_virtio_block_aio() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME, + ); + _test_virtio_block(&guest, true, false, false, false, ImageType::Raw); + } + + #[test] + fn test_virtio_block_sync() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME, + ); + _test_virtio_block(&guest, true, true, false, false, ImageType::Raw); + } + + #[test] + fn test_virtio_block_qcow2() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2, + ); + _test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2); + } + + #[test] + fn test_virtio_block_qcow2_zlib() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_ZLIB, + ); + _test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2); + } + + #[test] + fn test_virtio_block_qcow2_zstd() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_ZSTD, + ); + _test_virtio_block(&guest, false, false, true, false, ImageType::Qcow2); + } + + #[test] + fn test_virtio_block_qcow2_backing_zstd_file() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_BACKING_ZSTD_FILE, + ); + + _test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2); + } + + #[test] + fn test_virtio_block_qcow2_backing_uncompressed_file() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_BACKING_UNCOMPRESSED_FILE, + ); + + _test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2); + } + + #[test] + fn test_virtio_block_qcow2_backing_raw_file() { + let guest = make_virtio_block_guest( + &GuestFactory::new_confidential_guest_factory(), + JAMMY_IMAGE_NAME_QCOW2_BACKING_RAW_FILE, + ); + _test_virtio_block(&guest, false, false, true, true, ImageType::Qcow2); + } + + #[test] + fn test_virtio_block_dynamic_vhdx_expand() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_virtio_block_dynamic_vhdx_expand(&guest); + } + + #[test] + fn test_split_irqchip() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_split_irqchip(&guest); + } + + #[test] + fn test_dmi_uuid() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_dmi_uuid(&guest); + } + + #[test] + fn test_dmi_oem_strings() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_dmi_oem_strings(&guest); + } + + #[test] + fn test_dmi_system_and_chassis() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_dmi_system_and_chassis(&guest); + } + + #[test] + fn test_multiple_network_interfaces() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_multiple_network_interfaces(&guest); + } + + #[test] + fn test_serial_off() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_serial_off(&guest); + } + + #[test] + fn test_virtio_console() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_virtio_console(&guest); + } + + #[test] + fn test_console_file() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_console_file(&guest); + } + + #[test] + fn test_direct_kernel_boot_noacpi() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_direct_kernel_boot_noacpi(&guest); + } + + #[test] + fn test_pci_bar_reprogramming() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_pci_bar_reprogramming(&guest); + } + + #[test] + fn test_memory_overhead() { + let guest_memory_size_kb: u32 = 512 * 1024; + let guest = + basic_cvm_guest!(JAMMY_IMAGE_NAME).with_memory(&format!("{guest_memory_size_kb}K")); + _test_memory_overhead(&guest, guest_memory_size_kb); + } + + #[test] + fn test_landlock() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_landlock(&guest); + } + + #[test] + fn test_disk_hotplug() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_disk_hotplug(&guest, false); + } + + #[test] + fn test_net_hotplug() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_net_hotplug(&guest, NUM_PCI_SEGMENTS, None); + } + + #[test] + fn test_counters() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_counters(&guest); + } + + #[test] + fn test_watchdog() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_watchdog(&guest); + } + + #[test] + fn test_pvpanic() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); + _test_pvpanic(&guest); + } + + #[test] + fn test_tap_from_fd() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_tap_from_fd(&guest); + } + + #[test] + fn test_macvtap() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_macvtap(&guest, false, "guestmacvtap0", "hostmacvtap0"); + } + + #[test] + fn test_macvtap_hotplug() { + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_macvtap(&guest, true, "guestmacvtap1", "hostmacvtap1"); + } + + #[test] + fn test_vdpa_block() { + assert!(exec_host_command_status("lsmod | grep vdpa_sim_blk").success()); + + let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME).with_cpu(2); + _test_vdpa_block(&guest); + } +} diff --git a/devices/Cargo.toml b/devices/Cargo.toml index 334ec0e310..1045d961c9 100644 --- a/devices/Cargo.toml +++ b/devices/Cargo.toml @@ -1,22 +1,29 @@ [package] authors = ["The Chromium OS Authors"] -edition = "2021" +edition.workspace = true name = "devices" +rust-version.workspace = true version = "0.1.0" [dependencies] acpi_tables = { workspace = true } -anyhow = "1.0.94" +anyhow = { workspace = true } arch = { path = "../arch" } -bitflags = "2.9.0" -byteorder = "1.5.0" +bitfield-struct = { version = "0.13.0", optional = true } +bitflags = { workspace = true } +byteorder = { workspace = true } event_monitor = { path = "../event_monitor" } hypervisor = { path = "../hypervisor" } -libc = "0.2.167" -log = "0.4.22" -num_enum = "0.7.2" +libc = { workspace = true } +linux-loader = { workspace = true, features = [ + "bzimage", + "elf", + "pe", +], optional = true } +log = { workspace = true } +num_enum = "0.7.6" pci = { path = "../pci" } -serde = { version = "1.0.208", features = ["derive"] } +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tpm = { path = "../tpm" } vm-allocator = { path = "../vm-allocator" } @@ -28,11 +35,20 @@ vm-memory = { workspace = true, features = [ ] } vm-migration = { path = "../vm-migration" } vmm-sys-util = { workspace = true } +zerocopy = { version = "0.8.48", features = [ + "alloc", + "derive", +], optional = true } [target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies] arch = { path = "../arch" } [features] default = [] +fw_cfg = ["arch/fw_cfg", "bitfield-struct", "linux-loader", "zerocopy"] +ivshmem = [] kvm = ["arch/kvm"] pvmemcontrol = [] + +[lints] +workspace = true diff --git a/devices/src/acpi.rs b/devices/src/acpi.rs index 2a38f5974c..49f166655d 100644 --- a/devices/src/acpi.rs +++ b/devices/src/acpi.rs @@ -8,9 +8,10 @@ use std::sync::{Arc, Barrier}; use std::thread; use std::time::Instant; -use acpi_tables::{aml, Aml, AmlSink}; -use vm_device::interrupt::InterruptSourceGroup; +use acpi_tables::{Aml, AmlSink, aml}; +use log::{error, info, warn}; use vm_device::BusDevice; +use vm_device::interrupt::InterruptSourceGroup; use vm_memory::GuestAddress; use vmm_sys_util::eventfd::EventFd; @@ -20,22 +21,25 @@ pub const GED_DEVICE_ACPI_SIZE: usize = 0x1; /// A device for handling ACPI shutdown and reboot pub struct AcpiShutdownDevice { - exit_evt: EventFd, + guest_exit_evt: EventFd, reset_evt: EventFd, vcpus_kill_signalled: Arc, + vcpus_pause_signalled: Arc, } impl AcpiShutdownDevice { /// Constructs a device that will signal the given event when the guest requests it. pub fn new( - exit_evt: EventFd, + guest_exit_evt: EventFd, reset_evt: EventFd, vcpus_kill_signalled: Arc, + vcpus_pause_signalled: Arc, ) -> AcpiShutdownDevice { AcpiShutdownDevice { - exit_evt, + guest_exit_evt, reset_evt, vcpus_kill_signalled, + vcpus_pause_signalled, } } } @@ -44,18 +48,20 @@ impl AcpiShutdownDevice { impl BusDevice for AcpiShutdownDevice { // Spec has all fields as zero fn read(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { - data.fill(0) + data.fill(0); } fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { if data[0] == 1 { info!("ACPI Reboot signalled"); if let Err(e) = self.reset_evt.write(1) { - error!("Error triggering ACPI reset event: {}", e); + error!("Error triggering ACPI reset event: {e}"); } // Spin until we are sure the reset_evt has been handled and that when // we return from the KVM_RUN we will exit rather than re-enter the guest. - while !self.vcpus_kill_signalled.load(Ordering::SeqCst) { + while !self.vcpus_kill_signalled.load(Ordering::SeqCst) + && !self.vcpus_pause_signalled.load(Ordering::SeqCst) + { // This is more effective than thread::yield_now() at // avoiding a priority inversion with the VMM thread thread::sleep(std::time::Duration::from_millis(1)); @@ -67,12 +73,14 @@ impl BusDevice for AcpiShutdownDevice { const SLEEP_VALUE_BIT: u8 = 2; if data[0] == (S5_SLEEP_VALUE << SLEEP_VALUE_BIT) | (1 << SLEEP_STATUS_EN_BIT) { info!("ACPI Shutdown signalled"); - if let Err(e) = self.exit_evt.write(1) { - error!("Error triggering ACPI shutdown event: {}", e); + if let Err(e) = self.guest_exit_evt.write(1) { + error!("Error triggering ACPI shutdown event: {e}"); } // Spin until we are sure the reset_evt has been handled and that when // we return from the KVM_RUN we will exit rather than re-enter the guest. - while !self.vcpus_kill_signalled.load(Ordering::SeqCst) { + while !self.vcpus_kill_signalled.load(Ordering::SeqCst) + && !self.vcpus_pause_signalled.load(Ordering::SeqCst) + { // This is more effective than thread::yield_now() at // avoiding a priority inversion with the VMM thread thread::sleep(std::time::Duration::from_millis(1)); @@ -213,7 +221,7 @@ impl Aml for AcpiGedDevice { ), ], ) - .to_aml_bytes(sink) + .to_aml_bytes(sink); } } diff --git a/devices/src/aia.rs b/devices/src/aia.rs index 83ed1585f4..3471d1608c 100644 --- a/devices/src/aia.rs +++ b/devices/src/aia.rs @@ -3,8 +3,6 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use super::interrupt_controller::{Error, InterruptController}; -extern crate arch; use std::result; use std::sync::{Arc, Mutex}; @@ -19,6 +17,8 @@ use vm_memory::address::Address; use vm_migration::{Migratable, Pausable, Snapshottable, Transportable}; use vmm_sys_util::eventfd::EventFd; +use super::interrupt_controller::{Error, InterruptController}; + type Result = result::Result; // Reserve 32 IRQs for legacy devices. @@ -39,8 +39,9 @@ pub struct Aia { } impl Aia { + #[allow(clippy::needless_pass_by_value)] pub fn new( - vcpu_count: u8, + vcpu_count: u32, interrupt_manager: Arc>, vm: Arc, ) -> Result { @@ -51,9 +52,8 @@ impl Aia { }) .map_err(Error::CreateInterruptSourceGroup)?; - let vaia = vm - .create_vaia(Aia::create_default_config(vcpu_count as u64)) - .map_err(Error::CreateAia)?; + let config = Aia::create_default_config(vcpu_count as u64); + let vaia = vm.create_vaia(&config).map_err(Error::CreateAia)?; let aia = Aia { interrupt_source_group, diff --git a/devices/src/debug_console.rs b/devices/src/debug_console.rs index 8ec63573b7..7945edb000 100644 --- a/devices/src/debug_console.rs +++ b/devices/src/debug_console.rs @@ -9,6 +9,7 @@ use std::io; use std::io::Write; use std::sync::{Arc, Barrier}; +use log::error; use vm_device::BusDevice; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; diff --git a/devices/src/gic.rs b/devices/src/gic.rs index afa5814a16..65e8384023 100644 --- a/devices/src/gic.rs +++ b/devices/src/gic.rs @@ -2,15 +2,13 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use super::interrupt_controller::{Error, InterruptController}; -extern crate arch; use std::result; use std::sync::{Arc, Mutex}; use anyhow::anyhow; use arch::layout; -use hypervisor::arch::aarch64::gic::{GicState, Vgic, VgicConfig}; use hypervisor::CpuState; +use hypervisor::arch::aarch64::gic::{GicState, Vgic, VgicConfig}; use vm_device::interrupt::{ InterruptIndex, InterruptManager, InterruptSourceConfig, InterruptSourceGroup, LegacyIrqSourceConfig, MsiIrqGroupConfig, @@ -19,6 +17,8 @@ use vm_memory::address::Address; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::eventfd::EventFd; +use super::interrupt_controller::{Error, InterruptController}; + type Result = result::Result; // Reserve 32 IRQs for legacy devices. @@ -38,8 +38,9 @@ pub struct Gic { } impl Gic { + #[allow(clippy::needless_pass_by_value)] pub fn new( - vcpu_count: u8, + vcpu_count: u32, interrupt_manager: Arc>, vm: Arc, ) -> Result { @@ -50,9 +51,8 @@ impl Gic { }) .map_err(Error::CreateInterruptSourceGroup)?; - let vgic = vm - .create_vgic(Gic::create_default_config(vcpu_count as u64)) - .map_err(Error::CreateGic)?; + let config = Gic::create_default_config(vcpu_count as u64); + let vgic = vm.create_vgic(&config).map_err(Error::CreateGic)?; let gic = Gic { interrupt_source_group, @@ -167,10 +167,7 @@ impl Pausable for Gic { // Flush tables to guest RAM let vgic = self.vgic.as_ref().unwrap().clone(); vgic.lock().unwrap().save_data_tables().map_err(|e| { - MigratableError::Pause(anyhow!( - "Could not save GICv3ITS GIC pending tables {:?}", - e - )) + MigratableError::Pause(anyhow!("Could not save GICv3ITS GIC pending tables {e:?}",)) })?; Ok(()) } diff --git a/devices/src/ioapic.rs b/devices/src/ioapic.rs index 7adbe4f66c..9312ab1156 100644 --- a/devices/src/ioapic.rs +++ b/devices/src/ioapic.rs @@ -13,12 +13,13 @@ use std::result; use std::sync::{Arc, Barrier}; use byteorder::{ByteOrder, LittleEndian}; +use log::{debug, error, trace, warn}; use serde::{Deserialize, Serialize}; +use vm_device::BusDevice; use vm_device::interrupt::{ InterruptIndex, InterruptManager, InterruptSourceConfig, InterruptSourceGroup, MsiIrqGroupConfig, MsiIrqSourceConfig, }; -use vm_device::BusDevice; use vm_memory::GuestAddress; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::eventfd::EventFd; @@ -151,13 +152,13 @@ impl BusDevice for Ioapic { return; } - debug!("IOAPIC_R @ offset 0x{:x}", offset); + debug!("IOAPIC_R @ offset 0x{offset:x}"); let value: u32 = match offset as u8 { IOREGSEL_OFF => self.reg_sel, IOWIN_OFF => self.ioapic_read(), _ => { - error!("IOAPIC: failed reading at offset {}", offset); + error!("IOAPIC: failed reading at offset {offset}"); return; } }; @@ -171,7 +172,7 @@ impl BusDevice for Ioapic { return None; } - debug!("IOAPIC_W @ offset 0x{:x}", offset); + trace!("IOAPIC_W @ offset 0x{offset:x}"); let value = LittleEndian::read_u32(data); @@ -179,7 +180,7 @@ impl BusDevice for Ioapic { IOREGSEL_OFF => self.reg_sel = value, IOWIN_OFF => self.ioapic_write(value), _ => { - error!("IOAPIC: failed writing at offset {}", offset); + error!("IOAPIC: failed writing at offset {offset}"); } } None @@ -190,8 +191,8 @@ impl Ioapic { pub fn new( id: String, apic_address: GuestAddress, - interrupt_manager: Arc>, - state: Option, + interrupt_manager: &dyn InterruptManager, + state: Option<&IoapicState>, ) -> Result { let interrupt_source_group = interrupt_manager .create_group(MsiIrqGroupConfig { @@ -249,7 +250,7 @@ impl Ioapic { } fn ioapic_write(&mut self, val: u32) { - debug!("IOAPIC_W reg 0x{:x}, val 0x{:x}", self.reg_sel, val); + trace!("IOAPIC_W reg 0x{:x}, val 0x{:x}", self.reg_sel, val); match self.reg_sel as u8 { IOAPIC_REG_VERSION => { @@ -266,7 +267,7 @@ impl Ioapic { IOWIN_OFF..=REG_MAX_OFFSET => { let (index, is_high_bits) = decode_irq_from_selector(self.reg_sel as u8); if index > NUM_IOAPIC_PINS { - warn!("IOAPIC index out of range: {}", index); + warn!("IOAPIC index out of range: {index}"); return; } if is_high_bits { @@ -282,7 +283,7 @@ impl Ioapic { // The entry must be updated through the interrupt source // group. if let Err(e) = self.update_entry(index, true) { - error!("Failed updating IOAPIC entry: {:?}", e); + error!("Failed updating IOAPIC entry: {e:?}"); } // Store the information this IRQ is now being used. self.used_entries[index] = true; @@ -303,7 +304,7 @@ impl Ioapic { IOWIN_OFF..=REG_MAX_OFFSET => { let (index, is_high_bits) = decode_irq_from_selector(self.reg_sel as u8); if index > NUM_IOAPIC_PINS { - warn!("IOAPIC index out of range: {}", index); + warn!("IOAPIC index out of range: {index}"); return 0; } if is_high_bits { diff --git a/devices/src/ivshmem.rs b/devices/src/ivshmem.rs new file mode 100644 index 0000000000..932e0d9eba --- /dev/null +++ b/devices/src/ivshmem.rs @@ -0,0 +1,419 @@ +// Copyright © 2024 Tencent Corporation. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::any::Any; +use std::path::PathBuf; +use std::result; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, Barrier, Mutex}; + +use anyhow::anyhow; +use byteorder::{ByteOrder, LittleEndian}; +use log::{debug, error, warn}; +use pci::{ + BarReprogrammingParams, PCI_CONFIGURATION_ID, PciBarConfiguration, PciBarPrefetchable, + PciBarRegionType, PciClassCode, PciConfiguration, PciDevice, PciDeviceError, PciHeaderType, + PciSubclass, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use vm_allocator::{AddressAllocator, SystemAllocator}; +use vm_device::{BusDevice, Resource, UserspaceMapping}; +use vm_memory::bitmap::AtomicBitmap; +use vm_memory::{Address, GuestAddress}; +use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; + +const IVSHMEM_BAR0_IDX: usize = 0; +const IVSHMEM_BAR1_IDX: usize = 1; +const IVSHMEM_BAR2_IDX: usize = 2; + +const IVSHMEM_VENDOR_ID: u16 = 0x1af4; +const IVSHMEM_DEVICE_ID: u16 = 0x1110; + +const IVSHMEM_REG_BAR_SIZE: u64 = 0x100; + +type MmapRegion = vm_memory::MmapRegion; + +#[derive(Debug, Error)] +pub enum IvshmemError { + #[error("Failed to retrieve PciConfigurationState: {0}")] + RetrievePciConfigurationState(#[source] anyhow::Error), + #[error("Failed to retrieve IvshmemDeviceState: {0}")] + RetrieveIvshmemDeviceStateState(#[source] anyhow::Error), + #[error("Failed to remove user memory region")] + RemoveUserMemoryRegion, + #[error("Failed to create user memory region.")] + CreateUserMemoryRegion, + #[error("Failed to create userspace mapping.")] + CreateUserspaceMapping, + #[error("Failed to remove old userspace mapping.")] + RemoveUserspaceMapping, +} + +#[derive(Copy, Clone)] +pub enum IvshmemSubclass { + Other = 0x00, +} + +impl PciSubclass for IvshmemSubclass { + fn get_register_value(&self) -> u8 { + *self as u8 + } +} + +pub trait IvshmemOps: Send + Sync { + fn map_ram_region( + &mut self, + start_addr: u64, + size: usize, + backing_file: Option, + ) -> Result<(Arc, UserspaceMapping), IvshmemError>; + + fn unmap_ram_region(&mut self, mapping: UserspaceMapping) -> Result<(), IvshmemError>; +} + +/// Inner-Vm Shared Memory Device (Ivshmem device) +/// +/// This device can share memory between host and guest(ivshmem-plain) +/// and share memory between guests(ivshmem-doorbell). +/// But only ivshmem-plain support now, ivshmem-doorbell doesn't support yet. +pub struct IvshmemDevice { + id: String, + + // ivshmem device registers + // (only used for ivshmem-doorbell, ivshmem-doorbell don't support yet) + _interrupt_mask: u32, + _interrupt_status: Arc, + _iv_position: u32, + _doorbell: u32, + + // PCI configuration registers. + configuration: PciConfiguration, + bar_regions: Vec, + + region_size: u64, + ivshmem_ops: Arc>, + backend_file: Option, + region: Option>, + userspace_mapping: Option, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct IvshmemDeviceState { + interrupt_mask: u32, + interrupt_status: u32, + iv_position: u32, + doorbell: u32, +} + +impl IvshmemDevice { + pub fn new( + id: String, + region_size: u64, + backend_file: Option, + ivshmem_ops: Arc>, + snapshot: Option<&Snapshot>, + ) -> Result { + let pci_configuration_state = vm_migration::state_from_id(snapshot, PCI_CONFIGURATION_ID) + .map_err(|e| { + IvshmemError::RetrievePciConfigurationState(anyhow!( + "Failed to get PciConfigurationState from Snapshot: {e}", + )) + })?; + + let state: Option = snapshot + .as_ref() + .map(|s| s.to_state()) + .transpose() + .map_err(|e| { + IvshmemError::RetrieveIvshmemDeviceStateState(anyhow!( + "Failed to get IvshmemDeviceState from Snapshot: {e}", + )) + })?; + + let configuration = PciConfiguration::new( + IVSHMEM_VENDOR_ID, + IVSHMEM_DEVICE_ID, + 0x1, + PciClassCode::MemoryController, + &IvshmemSubclass::Other, + None, + PciHeaderType::Device, + 0, + 0, + None, + pci_configuration_state, + ); + + let device = if let Some(s) = state { + IvshmemDevice { + id, + configuration, + bar_regions: vec![], + _interrupt_mask: s.interrupt_mask, + _interrupt_status: Arc::new(AtomicU32::new(s.interrupt_status)), + _iv_position: s.iv_position, + _doorbell: s.doorbell, + region_size, + ivshmem_ops, + region: None, + userspace_mapping: None, + backend_file, + } + } else { + IvshmemDevice { + id, + configuration, + bar_regions: vec![], + _interrupt_mask: 0, + _interrupt_status: Arc::new(AtomicU32::new(0)), + _iv_position: 0, + _doorbell: 0, + region_size, + ivshmem_ops, + region: None, + userspace_mapping: None, + backend_file, + } + }; + Ok(device) + } + + pub fn set_region(&mut self, region: Arc, userspace_mapping: UserspaceMapping) { + self.region = Some(region); + self.userspace_mapping = Some(userspace_mapping); + } + + pub fn config_bar_addr(&self) -> u64 { + self.configuration.get_bar_addr(IVSHMEM_BAR0_IDX) + } + + pub fn data_bar_addr(&self) -> u64 { + self.configuration.get_bar_addr(IVSHMEM_BAR2_IDX) + } + + fn state(&self) -> IvshmemDeviceState { + IvshmemDeviceState { + interrupt_mask: self._interrupt_mask, + interrupt_status: self._interrupt_status.load(Ordering::SeqCst), + iv_position: self._iv_position, + doorbell: self._doorbell, + } + } +} + +impl BusDevice for IvshmemDevice { + fn read(&mut self, base: u64, offset: u64, data: &mut [u8]) { + self.read_bar(base, offset, data); + } + + fn write(&mut self, base: u64, offset: u64, data: &[u8]) -> Option> { + self.write_bar(base, offset, data) + } +} + +impl PciDevice for IvshmemDevice { + fn allocate_bars( + &mut self, + _allocator: &mut SystemAllocator, + mmio32_allocator: &mut AddressAllocator, + mmio64_allocator: &mut AddressAllocator, + resources: Option>, + ) -> std::result::Result, PciDeviceError> { + let mut bars = Vec::new(); + let mut bar0_addr = None; + let mut bar2_addr = None; + + let restoring = resources.is_some(); + if let Some(resources) = resources { + for resource in resources { + match resource { + Resource::PciBar { index, base, .. } => match index { + IVSHMEM_BAR0_IDX => { + bar0_addr = Some(GuestAddress(base)); + } + IVSHMEM_BAR1_IDX => {} + IVSHMEM_BAR2_IDX => { + bar2_addr = Some(GuestAddress(base)); + } + _ => { + error!("Unexpected pci bar index {index}"); + } + }, + _ => { + error!("Unexpected resource {resource:?}"); + } + } + } + if bar0_addr.is_none() || bar2_addr.is_none() { + return Err(PciDeviceError::MissingResource); + } + } + + // BAR0 holds device registers (256 Byte MMIO) + let bar0_addr = mmio32_allocator + .allocate(bar0_addr, IVSHMEM_REG_BAR_SIZE, None) + .ok_or(PciDeviceError::IoAllocationFailed(IVSHMEM_REG_BAR_SIZE))?; + debug!("ivshmem bar0 address 0x{:x}", bar0_addr.0); + + let bar0 = PciBarConfiguration::default() + .set_index(IVSHMEM_BAR0_IDX) + .set_address(bar0_addr.raw_value()) + .set_size(IVSHMEM_REG_BAR_SIZE) + .set_region_type(PciBarRegionType::Memory32BitRegion) + .set_prefetchable(PciBarPrefetchable::NotPrefetchable); + + // BAR1 holds MSI-X table and PBA (only ivshmem-doorbell). + + // BAR2 maps the shared memory object + let bar2_size = self.region_size; + let bar2_addr = mmio64_allocator + .allocate(bar2_addr, bar2_size, None) + .ok_or(PciDeviceError::IoAllocationFailed(bar2_size))?; + debug!("ivshmem bar2 address 0x{:x}", bar2_addr.0); + + let bar2 = PciBarConfiguration::default() + .set_index(IVSHMEM_BAR2_IDX) + .set_address(bar2_addr.raw_value()) + .set_size(bar2_size) + .set_region_type(PciBarRegionType::Memory64BitRegion) + .set_prefetchable(PciBarPrefetchable::Prefetchable); + + if !restoring { + self.configuration + .add_pci_bar(&bar0) + .map_err(|e| PciDeviceError::IoRegistrationFailed(bar0_addr.raw_value(), e))?; + self.configuration + .add_pci_bar(&bar2) + .map_err(|e| PciDeviceError::IoRegistrationFailed(bar2_addr.raw_value(), e))?; + } + + bars.push(bar0); + bars.push(bar2); + self.bar_regions = bars.clone(); + + Ok(bars) + } + + fn free_bars( + &mut self, + _allocator: &mut SystemAllocator, + _mmio32_allocator: &mut AddressAllocator, + _mmio64_allocator: &mut AddressAllocator, + ) -> std::result::Result<(), PciDeviceError> { + unimplemented!("Device hotplug and remove are not supported for ivshmem"); + } + + fn write_config_register( + &mut self, + reg_idx: usize, + offset: u64, + data: &[u8], + ) -> (Vec, Option>) { + ( + self.configuration + .write_config_register(reg_idx, offset, data), + None, + ) + } + + fn read_config_register(&mut self, reg_idx: usize) -> u32 { + self.configuration.read_reg(reg_idx) + } + + fn read_bar(&mut self, base: u64, offset: u64, data: &mut [u8]) { + debug!("read base {base:x} offset {offset}"); + + let mut bar_idx = 0; + for (idx, bar) in self.bar_regions.iter().enumerate() { + if bar.addr() == base { + bar_idx = idx; + } + } + match bar_idx { + // bar 0 + 0 => { + // ivshmem don't use interrupt, we return zero now. + LittleEndian::write_u32(data, 0); + } + // bar 2 + 1 => warn!("Unexpected read ivshmem memory idx: {offset}"), + _ => { + warn!("Invalid bar_idx: {bar_idx}"); + } + } + } + + fn write_bar(&mut self, base: u64, offset: u64, _data: &[u8]) -> Option> { + debug!("write base {base:x} offset {offset}"); + warn!("Unexpected write ivshmem memory idx: {offset}"); + None + } + + fn move_bar(&mut self, old_base: u64, new_base: u64) -> result::Result<(), std::io::Error> { + if new_base == self.data_bar_addr() { + if let Some(old_mapping) = self.userspace_mapping.take() { + self.ivshmem_ops + .lock() + .unwrap() + .unmap_ram_region(old_mapping) + .map_err(std::io::Error::other)?; + } + let (region, new_mapping) = self + .ivshmem_ops + .lock() + .unwrap() + .map_ram_region( + new_base, + self.region_size as usize, + self.backend_file.clone(), + ) + .map_err(std::io::Error::other)?; + self.set_region(region, new_mapping); + } + for bar in self.bar_regions.iter_mut() { + if bar.addr() == old_base { + *bar = bar.set_address(new_base); + } + } + + Ok(()) + } + + fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) { + self.configuration.restore_bar_addr(params); + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } +} + +impl Pausable for IvshmemDevice {} + +impl Snapshottable for IvshmemDevice { + fn id(&self) -> String { + self.id.clone() + } + + // The snapshot/restore (also live migration) support only work for ivshmem-plain mode. + // Additional work is needed for supporting ivshmem-doorbell. + fn snapshot(&mut self) -> std::result::Result { + let mut snapshot = Snapshot::new_from_state(&self.state())?; + + // Snapshot PciConfiguration + snapshot.add_snapshot(self.configuration.id(), self.configuration.snapshot()?); + + Ok(snapshot) + } +} + +impl Transportable for IvshmemDevice {} + +impl Migratable for IvshmemDevice {} diff --git a/devices/src/legacy/cmos.rs b/devices/src/legacy/cmos.rs index 386281c67f..8f4b44941b 100644 --- a/devices/src/legacy/cmos.rs +++ b/devices/src/legacy/cmos.rs @@ -12,7 +12,8 @@ use std::{mem, thread}; // https://github.com/rust-lang/libc/issues/1848 #[cfg_attr(target_env = "musl", allow(deprecated))] use libc::time_t; -use libc::{clock_gettime, gmtime_r, timespec, tm, CLOCK_REALTIME}; +use libc::{CLOCK_REALTIME, clock_gettime, gmtime_r, timespec, tm}; +use log::{info, warn}; use vm_device::BusDevice; use vmm_sys_util::eventfd::EventFd; @@ -26,7 +27,8 @@ pub struct Cmos { index: u8, data: [u8; DATA_LEN], reset_evt: EventFd, - vcpus_kill_signalled: Option>, + vcpus_kill_signalled: Arc, + vcpus_pause_signalled: Arc, } impl Cmos { @@ -37,7 +39,8 @@ impl Cmos { mem_below_4g: u64, mem_above_4g: u64, reset_evt: EventFd, - vcpus_kill_signalled: Option>, + vcpus_kill_signalled: Arc, + vcpus_pause_signalled: Arc, ) -> Cmos { let mut data = [0u8; DATA_LEN]; @@ -60,6 +63,7 @@ impl Cmos { data, reset_evt, vcpus_kill_signalled, + vcpus_pause_signalled, } } } @@ -77,21 +81,21 @@ impl BusDevice for Cmos { if self.index == 0x8f && data[0] == 0 { info!("CMOS reset"); self.reset_evt.write(1).unwrap(); - if let Some(vcpus_kill_signalled) = self.vcpus_kill_signalled.take() { - // Spin until we are sure the reset_evt has been handled and that when - // we return from the KVM_RUN we will exit rather than re-enter the guest. - while !vcpus_kill_signalled.load(Ordering::SeqCst) { - // This is more effective than thread::yield_now() at - // avoiding a priority inversion with the VMM thread - thread::sleep(std::time::Duration::from_millis(1)); - } + // Spin until we are sure the reset_evt has been handled and that when + // we return from the KVM_RUN we will exit rather than re-enter the guest. + while !self.vcpus_kill_signalled.load(Ordering::SeqCst) + && !self.vcpus_pause_signalled.load(Ordering::SeqCst) + { + // This is more effective than thread::yield_now() at + // avoiding a priority inversion with the VMM thread + thread::sleep(std::time::Duration::from_millis(1)); } } else { - self.data[(self.index & INDEX_MASK) as usize] = data[0] + self.data[(self.index & INDEX_MASK) as usize] = data[0]; } } - o => warn!("bad write offset on CMOS device: {}", o), - }; + o => warn!("bad write offset on CMOS device: {o}"), + } None } @@ -121,13 +125,13 @@ impl BusDevice for Cmos { // the tm and timespec struct because it contains only plain data. let update_in_progress = unsafe { let mut timespec: timespec = mem::zeroed(); - clock_gettime(CLOCK_REALTIME, &mut timespec as *mut _); + clock_gettime(CLOCK_REALTIME, &raw mut timespec); // https://github.com/rust-lang/libc/issues/1848 #[cfg_attr(target_env = "musl", allow(deprecated))] let now: time_t = timespec.tv_sec; let mut tm: tm = mem::zeroed(); - gmtime_r(&now, &mut tm as *mut _); + gmtime_r(&now, &raw mut tm); // The following lines of code are safe but depend on tm being in scope. seconds = tm.tm_sec; @@ -164,7 +168,7 @@ impl BusDevice for Cmos { } } o => { - warn!("bad read offset on CMOS device: {}", o); + warn!("bad read offset on CMOS device: {o}"); 0 } } diff --git a/devices/src/legacy/debug_port.rs b/devices/src/legacy/debug_port.rs index 3050e9e618..bd9a31d79b 100644 --- a/devices/src/legacy/debug_port.rs +++ b/devices/src/legacy/debug_port.rs @@ -6,6 +6,7 @@ use std::fmt; use std::time::Instant; +use log::{error, warn}; use vm_device::BusDevice; /// Debug I/O port, see: @@ -62,7 +63,7 @@ impl DebugPort { impl BusDevice for DebugPort { fn read(&mut self, _base: u64, _offset: u64, _data: &mut [u8]) { - error!("Invalid read to debug port") + error!("Invalid read to debug port"); } fn write( diff --git a/devices/src/legacy/fw_cfg.rs b/devices/src/legacy/fw_cfg.rs new file mode 100644 index 0000000000..c5200e5f50 --- /dev/null +++ b/devices/src/legacy/fw_cfg.rs @@ -0,0 +1,1011 @@ +// Copyright 2025 Google LLC. +// +// SPDX-License-Identifier: Apache-2.0 +// + +/// Cloud Hypervisor implementation of Qemu's fw_cfg spec +/// https://www.qemu.org/docs/master/specs/fw_cfg.html +/// Linux kernel fw_cfg driver header +/// https://github.com/torvalds/linux/blob/master/include/uapi/linux/qemu_fw_cfg.h +/// Uploading files to the guest via fw_cfg is supported for all kernels 4.6+ w/ CONFIG_FW_CFG_SYSFS enabled +/// https://cateee.net/lkddb/web-lkddb/FW_CFG_SYSFS.html +/// No kernel requirement if above functionality is not required, +/// only firmware must implement mechanism to interact with this fw_cfg device +use std::{ + fs::File, + io::{ErrorKind, Read, Result, Seek, SeekFrom}, + mem::offset_of, + os::unix::fs::FileExt, + sync::{Arc, Barrier}, +}; + +use acpi_tables::rsdp::Rsdp; +use arch::RegionType; +#[cfg(target_arch = "aarch64")] +use arch::aarch64::layout::{ + MEM_32BIT_DEVICES_START, MEM_32BIT_RESERVED_START, RAM_64BIT_START, RAM_START as HIGH_RAM_START, +}; +#[cfg(target_arch = "x86_64")] +use arch::layout::{ + EBDA_START, HIGH_RAM_START, MEM_32BIT_DEVICES_SIZE, MEM_32BIT_DEVICES_START, + MEM_32BIT_RESERVED_START, PCI_MMCONFIG_SIZE, PCI_MMCONFIG_START, RAM_64BIT_START, +}; +use bitfield_struct::bitfield; +#[cfg(target_arch = "x86_64")] +use linux_loader::bootparam::boot_params; +#[cfg(target_arch = "aarch64")] +use linux_loader::loader::pe::arm64_image_header as boot_params; +use log::{debug, error}; +use vm_device::BusDevice; +use vm_memory::bitmap::AtomicBitmap; +use vm_memory::{ + ByteValued, Bytes, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryMmap, +}; +use vmm_sys_util::sock_ctrl_msg::IntoIovec; +use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes}; + +#[cfg(target_arch = "x86_64")] +// https://github.com/project-oak/oak/tree/main/stage0_bin#memory-layout +const STAGE0_START_ADDRESS: GuestAddress = GuestAddress(0xfffe_0000); +#[cfg(target_arch = "x86_64")] +const STAGE0_SIZE: usize = 0x2_0000; +const E820_RAM: u32 = 1; +const E820_RESERVED: u32 = 2; + +#[cfg(target_arch = "x86_64")] +const PORT_FW_CFG_SELECTOR: u64 = 0x510; +#[cfg(target_arch = "x86_64")] +const PORT_FW_CFG_DATA: u64 = 0x511; +#[cfg(target_arch = "x86_64")] +const PORT_FW_CFG_DMA_HI: u64 = 0x514; +#[cfg(target_arch = "x86_64")] +const PORT_FW_CFG_DMA_LO: u64 = 0x518; +#[cfg(target_arch = "x86_64")] +pub const PORT_FW_CFG_BASE: u64 = 0x510; +#[cfg(target_arch = "x86_64")] +pub const PORT_FW_CFG_WIDTH: u64 = 0xc; +#[cfg(target_arch = "aarch64")] +const PORT_FW_CFG_SELECTOR: u64 = 0x9030008; +#[cfg(target_arch = "aarch64")] +const PORT_FW_CFG_DATA: u64 = 0x9030000; +#[cfg(target_arch = "aarch64")] +const PORT_FW_CFG_DMA_HI: u64 = 0x9030010; +#[cfg(target_arch = "aarch64")] +const PORT_FW_CFG_DMA_LO: u64 = 0x9030014; +#[cfg(target_arch = "aarch64")] +pub const PORT_FW_CFG_BASE: u64 = 0x9030000; +#[cfg(target_arch = "aarch64")] +pub const PORT_FW_CFG_WIDTH: u64 = 0x10; + +const FW_CFG_SIGNATURE: u16 = 0x00; +const FW_CFG_ID: u16 = 0x01; +const FW_CFG_KERNEL_SIZE: u16 = 0x08; +const FW_CFG_INITRD_SIZE: u16 = 0x0b; +const FW_CFG_KERNEL_DATA: u16 = 0x11; +const FW_CFG_INITRD_DATA: u16 = 0x12; +const FW_CFG_CMDLINE_SIZE: u16 = 0x14; +const FW_CFG_CMDLINE_DATA: u16 = 0x15; +const FW_CFG_SETUP_SIZE: u16 = 0x17; +const FW_CFG_SETUP_DATA: u16 = 0x18; +const FW_CFG_FILE_DIR: u16 = 0x19; +const FW_CFG_KNOWN_ITEMS: usize = 0x20; + +pub const FW_CFG_FILE_FIRST: u16 = 0x20; +pub const FW_CFG_DMA_SIGNATURE: [u8; 8] = *b"QEMU CFG"; +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/qemu_fw_cfg.h +pub const FW_CFG_ACPI_ID: &str = "QEMU0002"; +// Reserved (must be enabled) +const FW_CFG_F_RESERVED: u8 = 1 << 0; +// DMA Toggle Bit (enabled by default) +const FW_CFG_F_DMA: u8 = 1 << 1; +pub const FW_CFG_FEATURE: [u8; 4] = [FW_CFG_F_RESERVED | FW_CFG_F_DMA, 0, 0, 0]; + +const COMMAND_ALLOCATE: u32 = 0x1; +const COMMAND_ADD_POINTER: u32 = 0x2; +const COMMAND_ADD_CHECKSUM: u32 = 0x3; + +const ALLOC_ZONE_HIGH: u8 = 0x1; +const ALLOC_ZONE_FSEG: u8 = 0x2; + +const FW_CFG_FILENAME_TABLE_LOADER: &str = "etc/table-loader"; +const FW_CFG_FILENAME_RSDP: &str = "acpi/rsdp"; +const FW_CFG_FILENAME_ACPI_TABLES: &str = "acpi/tables"; + +#[derive(Debug)] +pub enum FwCfgContent { + Bytes(Vec), + Slice(&'static [u8]), + File(u64, File), + U32(u32), +} + +struct FwCfgContentAccess<'a> { + content: &'a FwCfgContent, + offset: u32, +} + +impl Read for FwCfgContentAccess<'_> { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self.content { + FwCfgContent::File(offset, f) => { + Seek::seek(&mut (&*f), SeekFrom::Start(offset + self.offset as u64))?; + Read::read(&mut (&*f), buf) + } + FwCfgContent::Bytes(b) => match b.get(self.offset as usize..) { + Some(mut s) => s.read(buf), + None => Err(ErrorKind::UnexpectedEof)?, + }, + FwCfgContent::Slice(b) => match b.get(self.offset as usize..) { + Some(mut s) => s.read(buf), + None => Err(ErrorKind::UnexpectedEof)?, + }, + FwCfgContent::U32(n) => match n.to_le_bytes().get(self.offset as usize..) { + Some(mut s) => s.read(buf), + None => Err(ErrorKind::UnexpectedEof)?, + }, + } + } +} + +impl Default for FwCfgContent { + fn default() -> Self { + FwCfgContent::Slice(&[]) + } +} + +impl FwCfgContent { + fn size(&self) -> Result { + let ret = match self { + FwCfgContent::Bytes(v) => v.len(), + FwCfgContent::File(offset, f) => (f.metadata()?.len() - offset) as usize, + FwCfgContent::Slice(s) => s.len(), + FwCfgContent::U32(n) => size_of_val(n), + }; + u32::try_from(ret).map_err(|_| std::io::ErrorKind::InvalidInput.into()) + } + fn access(&self, offset: u32) -> FwCfgContentAccess<'_> { + FwCfgContentAccess { + content: self, + offset, + } + } +} + +#[derive(Debug, Default)] +pub struct FwCfgItem { + pub name: String, + pub content: FwCfgContent, +} + +/// https://www.qemu.org/docs/master/specs/fw_cfg.html +#[derive(Debug)] +pub struct FwCfg { + selector: u16, + data_offset: u32, + dma_address: u64, + items: Vec, // 0x20 and above + known_items: [FwCfgContent; FW_CFG_KNOWN_ITEMS], // 0x0 to 0x19 + memory: GuestMemoryAtomic>, +} + +#[repr(C)] +#[derive(Debug, IntoBytes, FromBytes)] +struct FwCfgDmaAccess { + control_be: u32, + length_be: u32, + address_be: u64, +} + +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/qemu_fw_cfg.h#L67 +#[bitfield(u32)] +struct AccessControl { + // FW_CFG_DMA_CTL_ERROR = 0x01 + error: bool, + // FW_CFG_DMA_CTL_READ = 0x02 + read: bool, + #[bits(1)] + _unused2: u8, + // FW_CFG_DMA_CTL_SKIP = 0x04 + skip: bool, + #[bits(3)] + _unused3: u8, + // FW_CFG_DMA_CTL_ERROR = 0x08 + select: bool, + #[bits(7)] + _unused4: u8, + // FW_CFG_DMA_CTL_WRITE = 0x10 + write: bool, + #[bits(16)] + _unused: u32, +} + +#[repr(C)] +#[derive(Debug, IntoBytes, FromBytes)] +struct FwCfgFilesHeader { + count_be: u32, +} + +pub const FILE_NAME_SIZE: usize = 56; + +pub fn create_file_name(name: &str) -> [u8; FILE_NAME_SIZE] { + let mut c_name = [0u8; FILE_NAME_SIZE]; + let c_len = std::cmp::min(FILE_NAME_SIZE - 1, name.len()); + c_name[0..c_len].copy_from_slice(&name.as_bytes()[0..c_len]); + c_name +} + +#[allow(dead_code)] +#[repr(C, packed)] +#[derive(Debug, IntoBytes, FromBytes, Clone, Copy)] +struct BootE820Entry { + addr: u64, + size: u64, + type_: u32, +} + +#[repr(C)] +#[derive(Debug, IntoBytes, FromBytes)] +struct FwCfgFile { + size_be: u32, + select_be: u16, + _reserved: u16, + name: [u8; FILE_NAME_SIZE], +} + +#[repr(C, align(4))] +#[derive(Debug, IntoBytes, Immutable)] +struct Allocate { + command: u32, + file: [u8; FILE_NAME_SIZE], + align: u32, + zone: u8, + _pad: [u8; 63], +} + +#[repr(C, align(4))] +#[derive(Debug, IntoBytes, Immutable)] +struct AddPointer { + command: u32, + dst: [u8; FILE_NAME_SIZE], + src: [u8; FILE_NAME_SIZE], + offset: u32, + size: u8, + _pad: [u8; 7], +} + +#[repr(C, align(4))] +#[derive(Debug, IntoBytes, Immutable)] +struct AddChecksum { + command: u32, + file: [u8; FILE_NAME_SIZE], + offset: u32, + start: u32, + len: u32, + _pad: [u8; 56], +} + +fn create_intra_pointer(name: &str, offset: usize, size: u8) -> AddPointer { + AddPointer { + command: COMMAND_ADD_POINTER, + dst: create_file_name(name), + src: create_file_name(name), + offset: offset as u32, + size, + _pad: [0; 7], + } +} + +fn create_acpi_table_checksum(offset: usize, len: usize) -> AddChecksum { + AddChecksum { + command: COMMAND_ADD_CHECKSUM, + file: create_file_name(FW_CFG_FILENAME_ACPI_TABLES), + offset: (offset + offset_of!(AcpiTableHeader, checksum)) as u32, + start: offset as u32, + len: len as u32, + _pad: [0; 56], + } +} + +#[repr(C, align(4))] +#[derive(Debug, Clone, Default, FromBytes, IntoBytes)] +struct AcpiTableHeader { + signature: [u8; 4], + length: u32, + revision: u8, + checksum: u8, + oem_id: [u8; 6], + oem_table_id: [u8; 8], + oem_revision: u32, + asl_compiler_id: [u8; 4], + asl_compiler_revision: u32, +} + +struct AcpiTable { + rsdp: Rsdp, + tables: Vec, + table_pointers: Vec, + table_checksums: Vec<(usize, usize)>, +} + +impl AcpiTable { + fn pointers(&self) -> &[usize] { + &self.table_pointers + } + + fn checksums(&self) -> &[(usize, usize)] { + &self.table_checksums + } + + fn take(self) -> (Rsdp, Vec) { + (self.rsdp, self.tables) + } +} + +// Creates fw_cfg items used by firmware to load and verify Acpi tables +// https://github.com/qemu/qemu/blob/master/hw/acpi/bios-linker-loader.c +fn create_acpi_loader(acpi_table: AcpiTable) -> [FwCfgItem; 3] { + let mut table_loader_bytes: Vec = Vec::new(); + let allocate_rsdp = Allocate { + command: COMMAND_ALLOCATE, + file: create_file_name(FW_CFG_FILENAME_RSDP), + align: 4, + zone: ALLOC_ZONE_FSEG, + _pad: [0; 63], + }; + table_loader_bytes.extend(allocate_rsdp.as_bytes()); + + let allocate_tables = Allocate { + command: COMMAND_ALLOCATE, + file: create_file_name(FW_CFG_FILENAME_ACPI_TABLES), + align: 4, + zone: ALLOC_ZONE_HIGH, + _pad: [0; 63], + }; + table_loader_bytes.extend(allocate_tables.as_bytes()); + + for pointer_offset in acpi_table.pointers().iter() { + let pointer = create_intra_pointer(FW_CFG_FILENAME_ACPI_TABLES, *pointer_offset, 8); + table_loader_bytes.extend(pointer.as_bytes()); + } + for (offset, len) in acpi_table.checksums().iter() { + let checksum = create_acpi_table_checksum(*offset, *len); + table_loader_bytes.extend(checksum.as_bytes()); + } + let pointer_rsdp_to_xsdt = AddPointer { + command: COMMAND_ADD_POINTER, + dst: create_file_name(FW_CFG_FILENAME_RSDP), + src: create_file_name(FW_CFG_FILENAME_ACPI_TABLES), + offset: offset_of!(Rsdp, xsdt_addr) as u32, + size: 8, + _pad: [0; 7], + }; + table_loader_bytes.extend(pointer_rsdp_to_xsdt.as_bytes()); + let checksum_rsdp = AddChecksum { + command: COMMAND_ADD_CHECKSUM, + file: create_file_name(FW_CFG_FILENAME_RSDP), + offset: offset_of!(Rsdp, checksum) as u32, + start: 0, + len: offset_of!(Rsdp, length) as u32, + _pad: [0; 56], + }; + let checksum_rsdp_ext = AddChecksum { + command: COMMAND_ADD_CHECKSUM, + file: create_file_name(FW_CFG_FILENAME_RSDP), + offset: offset_of!(Rsdp, extended_checksum) as u32, + start: 0, + len: size_of::() as u32, + _pad: [0; 56], + }; + table_loader_bytes.extend(checksum_rsdp.as_bytes()); + table_loader_bytes.extend(checksum_rsdp_ext.as_bytes()); + + let table_loader = FwCfgItem { + name: FW_CFG_FILENAME_TABLE_LOADER.to_owned(), + content: FwCfgContent::Bytes(table_loader_bytes), + }; + let (rsdp, tables) = acpi_table.take(); + let acpi_rsdp = FwCfgItem { + name: FW_CFG_FILENAME_RSDP.to_owned(), + content: FwCfgContent::Bytes(rsdp.as_bytes().to_owned()), + }; + let apci_tables = FwCfgItem { + name: FW_CFG_FILENAME_ACPI_TABLES.to_owned(), + content: FwCfgContent::Bytes(tables), + }; + [table_loader, acpi_rsdp, apci_tables] +} + +impl FwCfg { + pub fn new(memory: GuestMemoryAtomic>) -> FwCfg { + const DEFAULT_ITEM: FwCfgContent = FwCfgContent::Slice(&[]); + let mut known_items = [DEFAULT_ITEM; FW_CFG_KNOWN_ITEMS]; + known_items[FW_CFG_SIGNATURE as usize] = FwCfgContent::Slice(&FW_CFG_DMA_SIGNATURE); + known_items[FW_CFG_ID as usize] = FwCfgContent::Slice(&FW_CFG_FEATURE); + let file_buf = Vec::from(FwCfgFilesHeader { count_be: 0 }.as_mut_bytes()); + known_items[FW_CFG_FILE_DIR as usize] = FwCfgContent::Bytes(file_buf); + + FwCfg { + selector: 0, + data_offset: 0, + dma_address: 0, + items: vec![], + known_items, + memory, + } + } + + pub fn populate_fw_cfg( + &mut self, + mem_size: Option, + kernel: Option, + initramfs: Option, + cmdline: Option, + fw_cfg_item_list: Option>, + #[cfg(target_arch = "x86_64")] kvm_sev_snp_enabled: bool, + ) -> Result<()> { + if let Some(mem_size) = mem_size { + self.add_e820(mem_size)?; + } + if let Some(kernel) = kernel { + self.add_kernel_data( + &kernel, + #[cfg(target_arch = "x86_64")] + kvm_sev_snp_enabled, + )?; + } + if let Some(cmdline) = cmdline { + self.add_kernel_cmdline(cmdline); + } + if let Some(initramfs) = initramfs { + self.add_initramfs_data(&initramfs)?; + } + if let Some(fw_cfg_item_list) = fw_cfg_item_list { + for item in fw_cfg_item_list { + self.add_item(item)?; + } + } + Ok(()) + } + + pub fn add_e820(&mut self, mem_size: usize) -> Result<()> { + #[cfg(target_arch = "x86_64")] + let mut mem_regions = vec![ + (GuestAddress(0), EBDA_START.0 as usize, RegionType::Ram), + ( + MEM_32BIT_DEVICES_START, + MEM_32BIT_DEVICES_SIZE as usize, + RegionType::Reserved, + ), + ( + PCI_MMCONFIG_START, + PCI_MMCONFIG_SIZE as usize, + RegionType::Reserved, + ), + (STAGE0_START_ADDRESS, STAGE0_SIZE, RegionType::Reserved), + ]; + #[cfg(target_arch = "aarch64")] + let mut mem_regions = arch::aarch64::arch_memory_regions(); + if mem_size < MEM_32BIT_DEVICES_START.0 as usize { + mem_regions.push(( + HIGH_RAM_START, + mem_size - HIGH_RAM_START.0 as usize, + RegionType::Ram, + )); + } else { + mem_regions.push(( + HIGH_RAM_START, + MEM_32BIT_RESERVED_START.0 as usize - HIGH_RAM_START.0 as usize, + RegionType::Ram, + )); + mem_regions.push(( + RAM_64BIT_START, + mem_size - (MEM_32BIT_DEVICES_START.0 as usize), + RegionType::Ram, + )); + } + let mut bytes = vec![]; + for (addr, size, region) in mem_regions.iter() { + let type_ = match region { + RegionType::Ram => E820_RAM, + RegionType::Reserved => E820_RESERVED, + RegionType::SubRegion => continue, + }; + let mut entry = BootE820Entry { + addr: addr.0, + size: *size as u64, + type_, + }; + bytes.extend_from_slice(entry.as_mut_bytes()); + } + let item = FwCfgItem { + name: "etc/e820".to_owned(), + content: FwCfgContent::Bytes(bytes), + }; + self.add_item(item) + } + + fn file_dir_mut(&mut self) -> &mut Vec { + let FwCfgContent::Bytes(file_buf) = &mut self.known_items[FW_CFG_FILE_DIR as usize] else { + unreachable!("fw_cfg: selector {FW_CFG_FILE_DIR:#x} should be FwCfgContent::Byte!") + }; + file_buf + } + + fn update_count(&mut self) { + let mut header = FwCfgFilesHeader { + count_be: (self.items.len() as u32).to_be(), + }; + self.file_dir_mut()[0..4].copy_from_slice(header.as_mut_bytes()); + } + + pub fn add_item(&mut self, item: FwCfgItem) -> Result<()> { + let index = self.items.len(); + let c_name = create_file_name(&item.name); + let size = item.content.size()?; + let mut cfg_file = FwCfgFile { + size_be: size.to_be(), + select_be: (FW_CFG_FILE_FIRST + index as u16).to_be(), + _reserved: 0, + name: c_name, + }; + self.file_dir_mut() + .extend_from_slice(cfg_file.as_mut_bytes()); + self.items.push(item); + self.update_count(); + Ok(()) + } + + fn dma_read_content( + &self, + content: &FwCfgContent, + offset: u32, + len: u32, + address: u64, + ) -> Result { + let content_size = content.size()?.saturating_sub(offset); + let op_size = std::cmp::min(content_size, len); + let mut access = content.access(offset); + let mut buf = vec![0u8; op_size as usize]; + access.read_exact(buf.as_mut_bytes())?; + let r = self + .memory + .memory() + .write(buf.as_bytes(), GuestAddress(address)); + match r { + Err(e) => { + error!("fw_cfg: dma read error: {e:x?}"); + Err(ErrorKind::InvalidInput.into()) + } + Ok(size) => Ok(size as u32), + } + } + + fn dma_read(&mut self, selector: u16, len: u32, address: u64) -> Result<()> { + let op_size = if let Some(content) = self.known_items.get(selector as usize) { + self.dma_read_content(content, self.data_offset, len, address) + } else if let Some(item) = self.items.get((selector - FW_CFG_FILE_FIRST) as usize) { + self.dma_read_content(&item.content, self.data_offset, len, address) + } else { + error!("fw_cfg: selector {selector:#x} does not exist."); + Err(ErrorKind::NotFound.into()) + }?; + self.data_offset += op_size; + Ok(()) + } + + fn do_dma(&mut self) { + let dma_address = self.dma_address; + let mut access = FwCfgDmaAccess::new_zeroed(); + let dma_access = match self + .memory + .memory() + .read(access.as_mut_bytes(), GuestAddress(dma_address)) + { + Ok(_) => access, + Err(e) => { + error!("fw_cfg: invalid address of dma access {dma_address:#x}: {e:?}"); + return; + } + }; + let control = AccessControl(u32::from_be(dma_access.control_be)); + if control.select() { + self.selector = control.select() as u16; + } + let len = u32::from_be(dma_access.length_be); + let addr = u64::from_be(dma_access.address_be); + let ret = if control.read() { + self.dma_read(self.selector, len, addr) + } else if control.write() { + Err(ErrorKind::InvalidInput.into()) + } else if control.skip() { + self.data_offset += len; + Ok(()) + } else { + Err(ErrorKind::InvalidData.into()) + }; + let mut access_resp = AccessControl(0); + if let Err(e) = ret { + error!("fw_cfg: dma operation {dma_access:x?}: {e:x?}"); + access_resp.set_error(true); + } + if let Err(e) = self.memory.memory().write( + &access_resp.0.to_be_bytes(), + GuestAddress(dma_address + core::mem::offset_of!(FwCfgDmaAccess, control_be) as u64), + ) { + error!("fw_cfg: finishing dma: {e:?}"); + } + } + + pub fn add_kernel_data( + &mut self, + file: &File, + #[cfg(target_arch = "x86_64")] kvm_sev_snp_enabled: bool, + ) -> Result<()> { + let mut buffer = vec![0u8; size_of::()]; + file.read_exact_at(&mut buffer, 0)?; + let bp = boot_params::from_mut_slice(&mut buffer).unwrap(); + #[cfg(target_arch = "x86_64")] + { + // For SEV-SNP guests on KVM, don't modify the kernel header so the + // bytes sent via fw_cfg match what the VMM hashes for the launch digest. + // The guest firmware handles these fields itself. + if !kvm_sev_snp_enabled { + if bp.hdr.setup_sects == 0 { + bp.hdr.setup_sects = 4; + } + bp.hdr.type_of_loader = 0xff; + } + } + #[cfg(target_arch = "aarch64")] + let kernel_start = bp.text_offset; + #[cfg(target_arch = "x86_64")] + let kernel_start = { + let sects = if bp.hdr.setup_sects == 0 { + 4 + } else { + bp.hdr.setup_sects + }; + (sects as usize + 1) * 512 + }; + + #[cfg(target_arch = "x86_64")] + if kernel_start <= buffer.len() { + buffer.truncate(kernel_start); + } else { + buffer.resize(kernel_start, 0); + file.read_exact_at( + &mut buffer[size_of::()..], + size_of::() as u64, + )?; + } + + self.known_items[FW_CFG_SETUP_SIZE as usize] = FwCfgContent::U32(buffer.len() as u32); + self.known_items[FW_CFG_SETUP_DATA as usize] = FwCfgContent::Bytes(buffer); + self.known_items[FW_CFG_KERNEL_SIZE as usize] = + FwCfgContent::U32(file.metadata()?.len() as u32 - kernel_start as u32); + self.known_items[FW_CFG_KERNEL_DATA as usize] = + FwCfgContent::File(kernel_start as u64, file.try_clone()?); + Ok(()) + } + + pub fn add_kernel_cmdline(&mut self, s: std::ffi::CString) { + let bytes = s.into_bytes_with_nul(); + self.known_items[FW_CFG_CMDLINE_SIZE as usize] = FwCfgContent::U32(bytes.len() as u32); + self.known_items[FW_CFG_CMDLINE_DATA as usize] = FwCfgContent::Bytes(bytes); + } + + pub fn add_acpi( + &mut self, + rsdp: Rsdp, + tables: Vec, + table_checksums: Vec<(usize, usize)>, + table_pointers: Vec, + ) -> Result<()> { + let acpi_table = AcpiTable { + rsdp, + tables, + table_checksums, + table_pointers, + }; + let [table_loader, acpi_rsdp, apci_tables] = create_acpi_loader(acpi_table); + self.add_item(table_loader)?; + self.add_item(acpi_rsdp)?; + self.add_item(apci_tables) + } + + pub fn add_initramfs_data(&mut self, file: &File) -> Result<()> { + let initramfs_size = file.metadata()?.len(); + self.known_items[FW_CFG_INITRD_SIZE as usize] = FwCfgContent::U32(initramfs_size as _); + self.known_items[FW_CFG_INITRD_DATA as usize] = FwCfgContent::File(0, file.try_clone()?); + Ok(()) + } + + fn read_content(content: &FwCfgContent, offset: u32, data: &mut [u8], size: u32) -> Option { + let start = offset as usize; + let end = start + size as usize; + match content { + FwCfgContent::Bytes(b) => { + if b.len() >= size as usize { + data.copy_from_slice(&b[start..end]); + } + } + FwCfgContent::Slice(s) => { + if s.len() >= size as usize { + data.copy_from_slice(&s[start..end]); + } + } + FwCfgContent::File(o, f) => { + f.read_exact_at(data, o + offset as u64).ok()?; + } + FwCfgContent::U32(n) => { + let bytes = n.to_le_bytes(); + data.copy_from_slice(&bytes[start..end]); + } + } + Some(size as u8) + } + + fn read_data(&mut self, data: &mut [u8], size: u32) -> u8 { + let ret = if let Some(content) = self.known_items.get(self.selector as usize) { + Self::read_content(content, self.data_offset, data, size) + } else if let Some(item) = self.items.get((self.selector - FW_CFG_FILE_FIRST) as usize) { + Self::read_content(&item.content, self.data_offset, data, size) + } else { + error!("fw_cfg: selector {:#x} does not exist.", self.selector); + None + }; + if let Some(val) = ret { + self.data_offset += size; + val + } else { + 0 + } + } +} + +impl BusDevice for FwCfg { + fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { + let port = offset + PORT_FW_CFG_BASE; + let size = data.len(); + match (port, size) { + (PORT_FW_CFG_SELECTOR, _) => { + error!("fw_cfg: selector register is write-only."); + } + (PORT_FW_CFG_DATA, _) => _ = self.read_data(data, size as u32), + (PORT_FW_CFG_DMA_HI, 4) => { + let addr = self.dma_address; + let addr_hi = (addr >> 32) as u32; + data.copy_from_slice(&addr_hi.to_be_bytes()); + } + (PORT_FW_CFG_DMA_LO, 4) => { + let addr = self.dma_address; + let addr_lo = (addr & 0xffff_ffff) as u32; + data.copy_from_slice(&addr_lo.to_be_bytes()); + } + _ => { + debug!( + "fw_cfg: read from unknown port {port:#x}: {size:#x} bytes and offset {offset:#x}." + ); + } + } + } + + fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { + let port = offset + PORT_FW_CFG_BASE; + let size = data.size(); + match (port, size) { + (PORT_FW_CFG_SELECTOR, 2) => { + let mut buf = [0u8; 2]; + buf[..size].copy_from_slice(&data[..size]); + #[cfg(target_arch = "x86_64")] + let val = u16::from_le_bytes(buf); + #[cfg(target_arch = "aarch64")] + let val = u16::from_be_bytes(buf); + self.selector = val; + self.data_offset = 0; + } + (PORT_FW_CFG_DATA, 1) => error!("fw_cfg: data register is read-only."), + (PORT_FW_CFG_DMA_HI, 4) => { + let mut buf = [0u8; 4]; + buf[..size].copy_from_slice(&data[..size]); + let val = u32::from_be_bytes(buf); + self.dma_address &= 0xffff_ffff; + self.dma_address |= (val as u64) << 32; + } + (PORT_FW_CFG_DMA_LO, 4) => { + let mut buf = [0u8; 4]; + buf[..size].copy_from_slice(&data[..size]); + let val = u32::from_be_bytes(buf); + self.dma_address &= !0xffff_ffff; + self.dma_address |= val as u64; + self.do_dma(); + } + _ => debug!( + "fw_cfg: write to unknown port {port:#x}: {size:#x} bytes and offset {offset:#x} ." + ), + } + None + } +} + +#[cfg(test)] +mod unit_tests { + use std::ffi::CString; + use std::io::Write; + + use vmm_sys_util::tempfile::TempFile; + + use super::*; + + #[cfg(target_arch = "x86_64")] + const SELECTOR_OFFSET: u64 = 0; + #[cfg(target_arch = "aarch64")] + const SELECTOR_OFFSET: u64 = 8; + #[cfg(target_arch = "x86_64")] + const DATA_OFFSET: u64 = 1; + #[cfg(target_arch = "aarch64")] + const DATA_OFFSET: u64 = 0; + #[cfg(target_arch = "x86_64")] + const DMA_OFFSET: u64 = 4; + #[cfg(target_arch = "aarch64")] + const DMA_OFFSET: u64 = 16; + + #[test] + fn test_signature() { + let gm = GuestMemoryAtomic::new( + GuestMemoryMmap::from_ranges(&[(GuestAddress(0), RAM_64BIT_START.0 as usize)]).unwrap(), + ); + + let mut fw_cfg = FwCfg::new(gm); + + let mut data = vec![0u8]; + + let mut sig_iter = FW_CFG_DMA_SIGNATURE.into_iter(); + fw_cfg.write(0, SELECTOR_OFFSET, &[FW_CFG_SIGNATURE as u8, 0]); + loop { + if let Some(char) = sig_iter.next() { + fw_cfg.read(0, DATA_OFFSET, &mut data); + assert_eq!(data[0], char); + } else { + return; + } + } + } + #[test] + fn test_kernel_cmdline() { + let gm = GuestMemoryAtomic::new( + GuestMemoryMmap::from_ranges(&[(GuestAddress(0), RAM_64BIT_START.0 as usize)]).unwrap(), + ); + + let mut fw_cfg = FwCfg::new(gm); + + let cmdline = *b"cmdline\0"; + + fw_cfg.add_kernel_cmdline(CString::from_vec_with_nul(cmdline.to_vec()).unwrap()); + + let mut data = vec![0u8]; + + let mut cmdline_iter = cmdline.into_iter(); + fw_cfg.write(0, SELECTOR_OFFSET, &[FW_CFG_CMDLINE_DATA as u8, 0]); + loop { + if let Some(char) = cmdline_iter.next() { + fw_cfg.read(0, DATA_OFFSET, &mut data); + assert_eq!(data[0], char); + } else { + return; + } + } + } + + #[test] + fn test_initram_fs() { + let gm = GuestMemoryAtomic::new( + GuestMemoryMmap::from_ranges(&[(GuestAddress(0), RAM_64BIT_START.0 as usize)]).unwrap(), + ); + + let mut fw_cfg = FwCfg::new(gm); + + let temp = TempFile::new().unwrap(); + let mut temp_file = temp.as_file(); + + let initram_content = b"this is the initramfs"; + let written = temp_file.write(initram_content); + assert_eq!(written.unwrap(), 21); + let _ = fw_cfg.add_initramfs_data(temp_file); + + let mut data = vec![0u8]; + + let mut initram_iter = (*initram_content).into_iter(); + fw_cfg.write(0, SELECTOR_OFFSET, &[FW_CFG_INITRD_DATA as u8, 0]); + loop { + if let Some(char) = initram_iter.next() { + fw_cfg.read(0, DATA_OFFSET, &mut data); + assert_eq!(data[0], char); + } else { + return; + } + } + } + + #[test] + fn test_string_item() { + let gm = GuestMemoryAtomic::new( + GuestMemoryMmap::from_ranges(&[(GuestAddress(0), RAM_64BIT_START.0 as usize)]).unwrap(), + ); + + let mut fw_cfg = FwCfg::new(gm); + + // Simulate OVMF X-PciMmio64Mb string item for GPU CC passthrough + let item = FwCfgItem { + name: "opt/ovmf/X-PciMmio64Mb".to_owned(), + content: FwCfgContent::Bytes("262144".as_bytes().to_vec()), + }; + fw_cfg.add_item(item).unwrap(); + + let expected = b"262144"; + let mut data = vec![0u8]; + + // Select the first file item (FW_CFG_FILE_FIRST = 0x20) + fw_cfg.write(0, SELECTOR_OFFSET, &[FW_CFG_FILE_FIRST as u8, 0]); + for &byte in expected.iter() { + fw_cfg.read(0, DATA_OFFSET, &mut data); + assert_eq!(data[0], byte); + } + } + + #[test] + fn test_dma() { + let code = [ + 0xba, 0xf8, 0x03, 0x00, 0xd8, 0x04, b'0', 0xee, 0xb0, b'\n', 0xee, 0xf4, + ]; + + let content = FwCfgContent::Bytes(code.to_vec()); + + let mem_size = 0x1000; + let load_addr = GuestAddress(0x1000); + let mem: GuestMemoryMmap = + GuestMemoryMmap::from_ranges(&[(load_addr, mem_size)]).unwrap(); + + // Note: In firmware we would just allocate FwCfgDmaAccess struct + // and use address of struct (&) as dma address + let mut access_control = AccessControl(0); + // bit 1 = read access + access_control.set_read(true); + // length of data to access + let length_be = (code.len() as u32).to_be(); + // guest address for data + let code_address = 0x1900_u64; + let address_be = code_address.to_be(); + let mut access = FwCfgDmaAccess { + control_be: access_control.0.to_be(), // bit(1) = read bit + length_be, + address_be, + }; + // access address is where to put the code + let access_address = GuestAddress(load_addr.0); + let address_bytes = access_address.0.to_be_bytes(); + let dma_lo: [u8; 4] = address_bytes[0..4].try_into().unwrap(); + let dma_hi: [u8; 4] = address_bytes[4..8].try_into().unwrap(); + + // writing the FwCfgDmaAccess to mem (this would just be self.dma_access.as_ref() in guest) + let _ = mem.write(access.as_mut_bytes(), access_address); + let mem_m = GuestMemoryAtomic::new(mem.clone()); + let mut fw_cfg = FwCfg::new(mem_m); + let cfg_item = FwCfgItem { + name: "code".to_string(), + content, + }; + let _ = fw_cfg.add_item(cfg_item); + + let mut data = [0u8; 12]; + + let _ = mem.read(&mut data, GuestAddress(code_address)); + assert_ne!(data, code); + + fw_cfg.write(0, SELECTOR_OFFSET, &[FW_CFG_FILE_FIRST as u8, 0]); + fw_cfg.write(0, DMA_OFFSET, &dma_lo); + fw_cfg.write(0, DMA_OFFSET + 4, &dma_hi); + let _ = mem.read(&mut data, GuestAddress(code_address)); + assert_eq!(data, code); + } +} diff --git a/devices/src/legacy/fwdebug.rs b/devices/src/legacy/fwdebug.rs index 0de5b6eea9..1024d262f5 100644 --- a/devices/src/legacy/fwdebug.rs +++ b/devices/src/legacy/fwdebug.rs @@ -9,6 +9,7 @@ use std::sync::{Arc, Barrier}; +use log::error; use vm_device::BusDevice; /// Provides firmware debug output via I/O port controls @@ -26,9 +27,9 @@ impl BusDevice for FwDebugDevice { /// Upon read return the magic value to indicate that there is a debug port fn read(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { if data.len() == 1 { - data[0] = 0xe9 + data[0] = 0xe9; } else { - error!("Invalid read size on debug port: {}", data.len()) + error!("Invalid read size on debug port: {}", data.len()); } } @@ -36,7 +37,7 @@ impl BusDevice for FwDebugDevice { if data.len() == 1 { print!("{}", data[0] as char); } else { - error!("Invalid write size on debug port: {}", data.len()) + error!("Invalid write size on debug port: {}", data.len()); } None diff --git a/devices/src/legacy/gpio_pl061.rs b/devices/src/legacy/gpio_pl061.rs index c7c66341a7..0f4ec5f90e 100644 --- a/devices/src/legacy/gpio_pl061.rs +++ b/devices/src/legacy/gpio_pl061.rs @@ -10,10 +10,11 @@ use std::sync::{Arc, Barrier}; use std::{io, result}; +use log::warn; use serde::{Deserialize, Serialize}; use thiserror::Error; -use vm_device::interrupt::InterruptSourceGroup; use vm_device::BusDevice; +use vm_device::interrupt::InterruptSourceGroup; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use crate::{read_le_u32, write_le_u32}; @@ -28,10 +29,10 @@ const GPIORIE: u64 = 0x414; // Raw Interrupt Status Register const GPIOMIS: u64 = 0x418; // Masked Interrupt Status Register const GPIOIC: u64 = 0x41c; // Interrupt Clear Register const GPIOAFSEL: u64 = 0x420; // Mode Control Select Register - // From 0x424 to 0xFDC => reserved space. - // From 0xFE0 to 0xFFC => Peripheral and PrimeCell Identification Registers which are Read Only registers. - // These registers can conceptually be treated as a 32-bit register, and PartNumber[11:0] is used to identify the peripheral. - // We are putting the expected values (look at 'Reset value' column from above mentioned document) in an array. +// From 0x424 to 0xFDC => reserved space. +// From 0xFE0 to 0xFFC => Peripheral and PrimeCell Identification Registers which are Read Only registers. +// These registers can conceptually be treated as a 32-bit register, and PartNumber[11:0] is used to identify the peripheral. +// We are putting the expected values (look at 'Reset value' column from above mentioned document) in an array. const GPIO_ID: [u8; 8] = [0x61, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; // ID Margins const GPIO_ID_LOW: u64 = 0xfe0; @@ -261,7 +262,7 @@ impl BusDevice for Gpio { let index = ((offset - GPIO_ID_LOW) >> 2) as usize; value = u32::from(GPIO_ID[index]); } else if offset < OFS_DATA { - value = self.data & ((offset >> 2) as u32) + value = self.data & ((offset >> 2) as u32); } else { value = match offset { GPIODIR => self.dir, @@ -294,7 +295,7 @@ impl BusDevice for Gpio { if data.len() <= 4 { let value = read_le_u32(data); if let Err(e) = self.handle_write(offset, value) { - warn!("Failed to write to GPIO PL061 device: {}", e); + warn!("Failed to write to GPIO PL061 device: {e}"); } } else { warn!( @@ -323,7 +324,7 @@ impl Transportable for Gpio {} impl Migratable for Gpio {} #[cfg(test)] -mod tests { +mod unit_tests { use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; use vmm_sys_util::eventfd::EventFd; diff --git a/devices/src/legacy/i8042.rs b/devices/src/legacy/i8042.rs index cc4bcd3e61..7639f819e0 100644 --- a/devices/src/legacy/i8042.rs +++ b/devices/src/legacy/i8042.rs @@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Barrier}; use std::thread; +use log::{error, info}; use vm_device::BusDevice; use vmm_sys_util::eventfd::EventFd; @@ -15,14 +16,20 @@ use vmm_sys_util::eventfd::EventFd; pub struct I8042Device { reset_evt: EventFd, vcpus_kill_signalled: Arc, + vcpus_pause_signalled: Arc, } impl I8042Device { /// Constructs a i8042 device that will signal the given event when the guest requests it. - pub fn new(reset_evt: EventFd, vcpus_kill_signalled: Arc) -> I8042Device { + pub fn new( + reset_evt: EventFd, + vcpus_kill_signalled: Arc, + vcpus_pause_signalled: Arc, + ) -> I8042Device { I8042Device { reset_evt, vcpus_kill_signalled, + vcpus_pause_signalled, } } } @@ -45,11 +52,13 @@ impl BusDevice for I8042Device { if data.len() == 1 && data[0] == 0xfe && offset == 3 { info!("i8042 reset signalled"); if let Err(e) = self.reset_evt.write(1) { - error!("Error triggering i8042 reset event: {}", e); + error!("Error triggering i8042 reset event: {e}"); } // Spin until we are sure the reset_evt has been handled and that when // we return from the KVM_RUN we will exit rather than re-enter the guest. - while !self.vcpus_kill_signalled.load(Ordering::SeqCst) { + while !self.vcpus_kill_signalled.load(Ordering::SeqCst) + && !self.vcpus_pause_signalled.load(Ordering::SeqCst) + { // This is more effective than thread::yield_now() at // avoiding a priority inversion with the VMM thread thread::sleep(std::time::Duration::from_millis(1)); diff --git a/devices/src/legacy/mod.rs b/devices/src/legacy/mod.rs index 3f58e5c842..1087d3d27d 100644 --- a/devices/src/legacy/mod.rs +++ b/devices/src/legacy/mod.rs @@ -8,6 +8,8 @@ mod cmos; #[cfg(target_arch = "x86_64")] mod debug_port; +#[cfg(feature = "fw_cfg")] +pub mod fw_cfg; #[cfg(target_arch = "x86_64")] mod fwdebug; #[cfg(target_arch = "aarch64")] @@ -22,6 +24,8 @@ mod uart_pl011; pub use self::cmos::Cmos; #[cfg(target_arch = "x86_64")] pub use self::debug_port::DebugPort; +#[cfg(feature = "fw_cfg")] +pub use self::fw_cfg::FwCfg; #[cfg(target_arch = "x86_64")] pub use self::fwdebug::FwDebugDevice; #[cfg(target_arch = "aarch64")] diff --git a/devices/src/legacy/rtc_pl031.rs b/devices/src/legacy/rtc_pl031.rs index 39c7911eed..ac4509113d 100644 --- a/devices/src/legacy/rtc_pl031.rs +++ b/devices/src/legacy/rtc_pl031.rs @@ -4,16 +4,19 @@ //! ARM PL031 Real Time Clock //! -//! This module implements a PL031 Real Time Clock (RTC) that provides to provides long time base counter. -//! This is achieved by generating an interrupt signal after counting for a programmed number of cycles of -//! a real-time clock input. +//! This module implements part of a PL031 Real Time Clock (RTC): +//! * provide a clock value via RTCDR +//! * no alarm is implemented through the match register +//! * no interrupt is generated +//! * RTC cannot be disabled via RTCCR +//! * no test registers //! +use std::result; use std::sync::{Arc, Barrier}; use std::time::Instant; -use std::{io, result}; +use log::warn; use thiserror::Error; -use vm_device::interrupt::InterruptSourceGroup; use vm_device::BusDevice; use crate::{read_le_u32, write_le_u32}; @@ -29,11 +32,11 @@ const RTCIMSC: u64 = 0x10; // Interrupt Mask Set or Clear Register. const RTCRIS: u64 = 0x14; // Raw Interrupt Status. const RTCMIS: u64 = 0x18; // Masked Interrupt Status. const RTCICR: u64 = 0x1c; // Interrupt Clear Register. - // From 0x020 to 0xFDC => reserved space. - // From 0xFE0 to 0x1000 => Peripheral and PrimeCell Identification Registers which are Read Only registers. - // AMBA standard devices have CIDs (Cell IDs) and PIDs (Peripheral IDs). The linux kernel will look for these in order to assert the identity - // of these devices (i.e look at the `amba_device_try_add` function). - // We are putting the expected values (look at 'Reset value' column from above mentioned document) in an array. +// From 0x020 to 0xFDC => reserved space. +// From 0xFE0 to 0x1000 => Peripheral and PrimeCell Identification Registers which are Read Only registers. +// AMBA standard devices have CIDs (Cell IDs) and PIDs (Peripheral IDs). The linux kernel will look for these in order to assert the identity +// of these devices (i.e look at the `amba_device_try_add` function). +// We are putting the expected values (look at 'Reset value' column from above mentioned document) in an array. const PL031_ID: [u8; 8] = [0x31, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; // We are only interested in the margins. const AMBA_ID_LOW: u64 = 0xFE0; @@ -45,8 +48,6 @@ pub const NANOS_PER_SECOND: u64 = 1_000_000_000; pub enum Error { #[error("Bad Write Offset: {0}")] BadWriteOffset(u64), - #[error("Failed to trigger interrupt")] - InterruptFailure(#[source] io::Error), } type Result = result::Result; @@ -107,31 +108,20 @@ pub struct Rtc { match_value: u32, // Writes to this register load an update value into the RTC. load: u32, - imsc: u32, - ris: u32, - interrupt: Arc, } impl Rtc { /// Constructs an AMBA PL031 RTC device. - pub fn new(interrupt: Arc) -> Self { + pub fn new() -> Self { Self { // This is used only for duration measuring purposes. previous_now: Instant::now(), tick_offset: get_time(ClockType::Real) as i64, match_value: 0, load: 0, - imsc: 0, - ris: 0, - interrupt, } } - fn trigger_interrupt(&mut self) -> Result<()> { - self.interrupt.trigger(0).map_err(Error::InterruptFailure)?; - Ok(()) - } - fn get_time(&self) -> u32 { let ts = (self.tick_offset as i128) + (Instant::now().duration_since(self.previous_now).as_nanos() as i128); @@ -155,16 +145,8 @@ impl Rtc { // we want to terminate the execution of the process. self.tick_offset = seconds_to_nanoseconds(i64::from(val)).unwrap(); } - RTCIMSC => { - self.imsc = val & 1; - self.trigger_interrupt()?; - } - RTCICR => { - // As per above mentioned doc, the interrupt is cleared by writing any data value to - // the Interrupt Clear Register. - self.ris = 0; - self.trigger_interrupt()?; - } + RTCIMSC => (), + RTCICR => (), RTCCR => (), // ignore attempts to turn off the timer. o => { return Err(Error::BadWriteOffset(o)); @@ -174,6 +156,12 @@ impl Rtc { } } +impl Default for Rtc { + fn default() -> Self { + Self::new() + } +} + impl BusDevice for Rtc { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { let mut read_ok = true; @@ -189,10 +177,10 @@ impl BusDevice for Rtc { self.match_value } RTCLR => self.load, - RTCCR => 1, // RTC is always enabled. - RTCIMSC => self.imsc, - RTCRIS => self.ris, - RTCMIS => self.ris & self.imsc, + RTCCR => 1, // RTC is always enabled. + RTCIMSC => 0, // Interrupt is always disabled. + RTCRIS => 0, + RTCMIS => 0, _ => { read_ok = false; 0 @@ -214,12 +202,11 @@ impl BusDevice for Rtc { if data.len() <= 4 { let v = read_le_u32(data); if let Err(e) = self.handle_write(offset, v) { - warn!("Failed to write to RTC PL031 device: {}", e); + warn!("Failed to write to RTC PL031 device: {e}"); } } else { warn!( - "Invalid RTC PL031 write: offset {}, data length {}", - offset, + "Invalid RTC PL031 write: offset {offset}, data length {}", data.len() ); } @@ -229,10 +216,7 @@ impl BusDevice for Rtc { } #[cfg(test)] -mod tests { - use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; - use vmm_sys_util::eventfd::EventFd; - +mod unit_tests { use super::*; use crate::{ read_be_u16, read_be_u32, read_le_i32, read_le_u16, read_le_u64, write_be_u16, @@ -366,45 +350,9 @@ mod tests { assert!(seconds_to_nanoseconds(9_223_372_037).is_none()); } - struct TestInterrupt { - event_fd: EventFd, - } - - impl InterruptSourceGroup for TestInterrupt { - fn trigger(&self, _index: InterruptIndex) -> result::Result<(), std::io::Error> { - self.event_fd.write(1) - } - - fn update( - &self, - _index: InterruptIndex, - _config: InterruptSourceConfig, - _masked: bool, - _set_gsi: bool, - ) -> result::Result<(), std::io::Error> { - Ok(()) - } - - fn set_gsi(&self) -> result::Result<(), std::io::Error> { - Ok(()) - } - - fn notifier(&self, _index: InterruptIndex) -> Option { - Some(self.event_fd.try_clone().unwrap()) - } - } - - impl TestInterrupt { - fn new(event_fd: EventFd) -> Self { - TestInterrupt { event_fd } - } - } - #[test] fn test_rtc_read_write_and_event() { - let intr_evt = EventFd::new(libc::EFD_NONBLOCK).unwrap(); - - let mut rtc = Rtc::new(Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap()))); + let mut rtc = Rtc::new(); let mut data = [0; 4]; // Read and write to the MR register. @@ -427,15 +375,13 @@ mod tests { assert_eq!((v / NANOS_PER_SECOND) as u32, v_read); // Read and write to IMSC register. - // Test with non zero value. + // Test with non zero value. Our device ignores the write. let non_zero = 1; write_le_u32(&mut data, non_zero); rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCIMSC, &data); - // The interrupt line should be on. - assert!(rtc.interrupt.notifier(0).unwrap().read().unwrap() == 1); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCIMSC, &mut data); let v = read_le_u32(&data); - assert_eq!(non_zero & 1, v); + assert_eq!(0, v); // Now test with 0. write_le_u32(&mut data, 0); @@ -447,8 +393,6 @@ mod tests { // Read and write to the ICR register. write_le_u32(&mut data, 1); rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCICR, &data); - // The interrupt line should be on. - assert!(rtc.interrupt.notifier(0).unwrap().read().unwrap() > 1); let v_before = read_le_u32(&data); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCICR, &mut data); diff --git a/devices/src/legacy/serial.rs b/devices/src/legacy/serial.rs index 973c96b0c5..be6fc126eb 100644 --- a/devices/src/legacy/serial.rs +++ b/devices/src/legacy/serial.rs @@ -10,8 +10,8 @@ use std::sync::{Arc, Barrier}; use std::{io, result}; use serde::{Deserialize, Serialize}; -use vm_device::interrupt::InterruptSourceGroup; use vm_device::BusDevice; +use vm_device::interrupt::InterruptSourceGroup; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::errno::Result; @@ -215,7 +215,7 @@ impl Serial { fn thr_empty(&mut self) -> Result<()> { if self.is_thr_intr_enabled() { self.add_intr_bit(IIR_THR_BIT); - self.trigger_interrupt()? + self.trigger_interrupt()?; } Ok(()) } @@ -223,7 +223,7 @@ impl Serial { fn recv_data(&mut self) -> Result<()> { if self.is_recv_intr_enabled() { self.add_intr_bit(IIR_RECV_BIT); - self.trigger_interrupt()? + self.trigger_interrupt()?; } self.line_status |= LSR_DATA_BIT; Ok(()) @@ -240,10 +240,10 @@ impl Serial { fn handle_write(&mut self, offset: u8, v: u8) -> Result<()> { match offset { DLAB_LOW if self.is_dlab_set() => { - self.baud_divisor = (self.baud_divisor & 0xff00) | u16::from(v) + self.baud_divisor = (self.baud_divisor & 0xff00) | u16::from(v); } DLAB_HIGH if self.is_dlab_set() => { - self.baud_divisor = (self.baud_divisor & 0x00ff) | ((u16::from(v)) << 8) + self.baud_divisor = (self.baud_divisor & 0x00ff) | ((u16::from(v)) << 8); } DATA => { if self.is_loop() { @@ -340,7 +340,7 @@ impl Transportable for Serial {} impl Migratable for Serial {} #[cfg(test)] -mod tests { +mod unit_tests { use std::sync::Mutex; use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; diff --git a/devices/src/legacy/uart_pl011.rs b/devices/src/legacy/uart_pl011.rs index b5603808bf..aac8f12ea8 100644 --- a/devices/src/legacy/uart_pl011.rs +++ b/devices/src/legacy/uart_pl011.rs @@ -11,10 +11,11 @@ use std::sync::{Arc, Barrier}; use std::time::Instant; use std::{io, result}; +use log::{debug, warn}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use vm_device::interrupt::InterruptSourceGroup; use vm_device::BusDevice; +use vm_device::interrupt::InterruptSourceGroup; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use crate::{read_le_u32, write_le_u32}; @@ -322,7 +323,7 @@ impl Pl011 { self.handle_debug(); } off => { - debug!("PL011: Bad write offset, offset: {}", off); + debug!("PL011: Bad write offset, offset: {off}"); return Err(Error::BadWriteOffset(off)); } } @@ -424,12 +425,11 @@ impl BusDevice for Pl011 { if data.len() <= 4 { let v = read_le_u32(data); if let Err(e) = self.handle_write(offset, v) { - warn!("Failed to write to PL011 device: {}", e); + warn!("Failed to write to PL011 device: {e}"); } } else { warn!( - "Invalid PL011 write: offset {}, data length {}", - offset, + "Invalid PL011 write: offset {offset}, data length {}", data.len() ); } @@ -453,7 +453,7 @@ impl Transportable for Pl011 {} impl Migratable for Pl011 {} #[cfg(test)] -mod tests { +mod unit_tests { use std::sync::Mutex; use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; diff --git a/devices/src/lib.rs b/devices/src/lib.rs index 6ea4bc70bb..cdec936e26 100644 --- a/devices/src/lib.rs +++ b/devices/src/lib.rs @@ -7,13 +7,6 @@ //! Emulates virtual and hardware devices. -#[macro_use] -extern crate bitflags; -#[macro_use] -extern crate event_monitor; -#[macro_use] -extern crate log; - pub mod acpi; #[cfg(target_arch = "riscv64")] pub mod aia; @@ -24,6 +17,8 @@ pub mod gic; pub mod interrupt_controller; #[cfg(target_arch = "x86_64")] pub mod ioapic; +#[cfg(feature = "ivshmem")] +pub mod ivshmem; pub mod legacy; #[cfg(feature = "pvmemcontrol")] pub mod pvmemcontrol; @@ -32,8 +27,12 @@ pub mod pvpanic; #[cfg(not(target_arch = "riscv64"))] pub mod tpm; +use bitflags::bitflags; + pub use self::acpi::{AcpiGedDevice, AcpiPmTimerDevice, AcpiShutdownDevice}; -pub use self::pvpanic::{PvPanicDevice, PVPANIC_DEVICE_MMIO_SIZE}; +#[cfg(feature = "ivshmem")] +pub use self::ivshmem::IvshmemDevice; +pub use self::pvpanic::{PVPANIC_DEVICE_MMIO_SIZE, PvPanicDevice}; bitflags! { pub struct AcpiNotificationFlags: u8 { diff --git a/devices/src/pvmemcontrol.rs b/devices/src/pvmemcontrol.rs index d119a21a1a..50e4cd16ba 100644 --- a/devices/src/pvmemcontrol.rs +++ b/devices/src/pvmemcontrol.rs @@ -5,9 +5,10 @@ use std::collections::HashMap; use std::ffi::CString; -use std::sync::{Arc, Barrier, Mutex, RwLock}; +use std::sync::{Arc, Barrier, RwLock}; use std::{io, result}; +use log::{debug, warn}; use num_enum::TryFromPrimitive; use pci::{ BarReprogrammingParams, PciBarConfiguration, PciBarPrefetchable, PciBarRegionType, @@ -137,7 +138,8 @@ impl PvmemcontrolTransport { } unsafe fn as_register(self) -> PvmemcontrolTransportRegister { - self.payload.register + // SAFETY: We access initialized data. + unsafe { self.payload.register } } } @@ -389,7 +391,7 @@ impl PvmemcontrolDevice { .iter() .skip(offset as usize) .zip(data.iter_mut()) - .for_each(|(src, dest)| *dest = *src) + .for_each(|(src, dest)| *dest = *src); } /// can only write to transport payload @@ -401,7 +403,7 @@ impl PvmemcontrolDevice { .iter_mut() .skip(offset as usize) .zip(data.iter()) - .for_each(|(dest, src)| *dest = *src) + .for_each(|(dest, src)| *dest = *src); } fn find_connection(&self, conn: GuestConnection) -> Option { @@ -428,22 +430,20 @@ impl PvmemcontrolBusDevice { /// [`range_base`, `range_base` + `range_len`) is present in the guest fn operate_on_memory_range(&self, addr: u64, length: u64, f: F) -> result::Result<(), Error> where - F: FnOnce(*mut libc::c_void, libc::size_t) -> libc::c_int, + F: FnOnce(*mut libc::c_void, usize) -> libc::c_int, { let memory = self.mem.memory(); let range_base = GuestAddress(addr); let range_len = usize::try_from(length).map_err(|_| Error::InvalidRequest)?; // assume guest memory is not interleaved with vmm memory on the host. - if !memory.check_range(range_base, range_len) { + let Ok(slice) = memory.get_slice(range_base, range_len) else { return Err(Error::GuestMemory(GuestMemoryError::InvalidGuestAddress( range_base, ))); - } - let hva = memory - .get_host_address(range_base) - .map_err(Error::GuestMemory)?; - let res = f(hva as *mut libc::c_void, range_len as libc::size_t); + }; + assert!(slice.len() >= range_len); + let res = f(slice.ptr_guard_mut().as_ptr().cast(), slice.len()); if res != 0 { return Err(Error::LibcFail(io::Error::last_os_error())); } @@ -490,7 +490,7 @@ impl PvmemcontrolBusDevice { } else { std::ptr::null() }; - debug!("addr {:X} length {} name {:?}", addr, length, name); + debug!("addr {addr:X} length {length} name {name:?}"); // SAFETY: [`base`, `base` + `len`) is guest memory self.operate_on_memory_range(addr, length, |base, len| unsafe { @@ -519,7 +519,7 @@ impl PvmemcontrolBusDevice { ret_value: get_page_size().into(), arg0: MAJOR_VERSION.into(), arg1: MINOR_VERSION.into(), - }) + }); } FunctionCode::Dontneed => self.madvise(addr, length, libc::MADV_DONTNEED), FunctionCode::Remove => self.madvise(addr, length, libc::MADV_REMOVE), @@ -580,7 +580,7 @@ impl PvmemcontrolBusDevice { ..Default::default() }, Error::GuestMemory(err) => { - warn!("{}", err); + warn!("{err}"); PvmemcontrolResp { ret_errno: (libc::EINVAL as u32).into(), ret_code: (func_code as u32).into(), @@ -605,7 +605,7 @@ impl PvmemcontrolBusDevice { let response: PvmemcontrolResp = match self.handle_request(request) { Ok(x) => x, Err(e) => { - warn!("cannot process request {:?} with error {}", request, e); + warn!("cannot process request {request:?} with error {e}"); return; } }; @@ -648,14 +648,16 @@ impl PvmemcontrolBusDevice { .find_connection(conn) .ok_or(Error::InvalidConnection(conn.command)) }) - .map(|gpa| self.handle_pvmemcontrol_request(gpa)) - .unwrap_or_else(|err| warn!("{:?}", err)); + .map_or_else( + |err| warn!("{err:?}"), + |gpa| self.handle_pvmemcontrol_request(gpa), + ); } } } fn handle_guest_read(&self, offset: u64, data: &mut [u8]) { - self.dev.read().unwrap().read_transport(offset, data) + self.dev.read().unwrap().read_transport(offset, data); } } @@ -710,6 +712,10 @@ impl PciDevice for PvmemcontrolPciDevice { self.configuration.read_config_register(reg_idx) } + fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) { + self.configuration.restore_bar_addr(params); + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } @@ -720,7 +726,7 @@ impl PciDevice for PvmemcontrolPciDevice { fn allocate_bars( &mut self, - _allocator: &Arc>, + _allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, _mmio64_allocator: &mut AddressAllocator, resources: Option>, @@ -759,7 +765,7 @@ impl PciDevice for PvmemcontrolPciDevice { _mmio64_allocator: &mut AddressAllocator, ) -> Result<(), PciDeviceError> { for bar in self.bar_regions.drain(..) { - mmio32_allocator.free(GuestAddress(bar.addr()), bar.size()) + mmio32_allocator.free(GuestAddress(bar.addr()), bar.size()); } Ok(()) } @@ -804,7 +810,7 @@ impl Migratable for PvmemcontrolPciDevice {} impl BusDeviceSync for PvmemcontrolBusDevice { fn read(&self, _base: u64, offset: u64, data: &mut [u8]) { - self.handle_guest_read(offset, data) + self.handle_guest_read(offset, data); } fn write(&self, _base: u64, offset: u64, data: &[u8]) -> Option> { diff --git a/devices/src/pvpanic.rs b/devices/src/pvpanic.rs index 98e7bfa9cd..3b9c9d5a80 100644 --- a/devices/src/pvpanic.rs +++ b/devices/src/pvpanic.rs @@ -5,13 +5,15 @@ use std::any::Any; use std::result; -use std::sync::{Arc, Barrier, Mutex}; +use std::sync::{Arc, Barrier}; use anyhow::anyhow; +use event_monitor::event; +use log::{debug, info}; use pci::{ - BarReprogrammingParams, PciBarConfiguration, PciBarPrefetchable, PciBarRegionType, - PciClassCode, PciConfiguration, PciDevice, PciDeviceError, PciHeaderType, PciSubclass, - PCI_CONFIGURATION_ID, + BarReprogrammingParams, PCI_CONFIGURATION_ID, PciBarConfiguration, PciBarPrefetchable, + PciBarRegionType, PciClassCode, PciConfiguration, PciDevice, PciDeviceError, PciHeaderType, + PciSubclass, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -64,14 +66,13 @@ pub struct PvPanicDeviceState { } impl PvPanicDevice { - pub fn new(id: String, snapshot: Option) -> Result { - let pci_configuration_state = - vm_migration::state_from_id(snapshot.as_ref(), PCI_CONFIGURATION_ID).map_err(|e| { - PvPanicError::RetrievePciConfigurationState(anyhow!( - "Failed to get PciConfigurationState from Snapshot: {}", - e - )) - })?; + pub fn new(id: String, snapshot: Option<&Snapshot>) -> Result { + let pci_configuration_state = vm_migration::state_from_id(snapshot, PCI_CONFIGURATION_ID) + .map_err(|e| { + PvPanicError::RetrievePciConfigurationState(anyhow!( + "Failed to get PciConfigurationState from Snapshot: {e}" + )) + })?; let mut configuration = PciConfiguration::new( PVPANIC_VENDOR_ID, @@ -100,8 +101,7 @@ impl PvPanicDevice { .transpose() .map_err(|e| { PvPanicError::CreatePvPanicDevice(anyhow!( - "Failed to get PvPanicDeviceState from Snapshot: {}", - e + "Failed to get PvPanicDeviceState from Snapshot: {e}" )) })?; let events = if let Some(state) = state { @@ -143,12 +143,12 @@ impl PvPanicDevice { impl BusDevice for PvPanicDevice { fn read(&mut self, base: u64, offset: u64, data: &mut [u8]) { - self.read_bar(base, offset, data) + self.read_bar(base, offset, data); } fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { let event = self.event_to_string(data[0]); - info!("pvpanic got guest event {}", event); + info!("pvpanic got guest event {event}"); event!("guest", "panic", "event", &event); None } @@ -174,7 +174,7 @@ impl PciDevice for PvPanicDevice { fn allocate_bars( &mut self, - _allocator: &Arc>, + _allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, _mmio64_allocator: &mut AddressAllocator, resources: Option>, @@ -231,6 +231,10 @@ impl PciDevice for PvPanicDevice { Ok(()) } + fn restore_bar_addr(&mut self, params: &BarReprogrammingParams) { + self.configuration.restore_bar_addr(params); + } + fn read_bar(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { data[0] = self.events; } diff --git a/devices/src/tpm.rs b/devices/src/tpm.rs index c6ed5ce0a9..75a0a9e429 100644 --- a/devices/src/tpm.rs +++ b/devices/src/tpm.rs @@ -4,6 +4,7 @@ // use std::cmp; +use std::path::Path; use std::sync::{Arc, Barrier}; use anyhow::anyhow; @@ -11,9 +12,10 @@ use anyhow::anyhow; use arch::aarch64::layout::{TPM_SIZE, TPM_START}; #[cfg(target_arch = "x86_64")] use arch::x86_64::layout::{TPM_SIZE, TPM_START}; +use log::{debug, error, warn}; use thiserror::Error; -use tpm::emulator::{BackendCmd, Emulator}; use tpm::TPM_CRB_BUFFER_MAX; +use tpm::emulator::{BackendCmd, Emulator}; use vm_device::BusDevice; #[derive(Error, Debug)] @@ -26,6 +28,7 @@ pub enum Error { type Result = anyhow::Result; #[allow(dead_code)] +#[derive(Copy, Clone)] enum LocStateFields { TpmEstablished, LocAssigned, @@ -34,12 +37,14 @@ enum LocStateFields { TpmRegValidSts, } +#[derive(Copy, Clone)] enum LocStsFields { Granted, BeenSeized, } #[allow(dead_code)] +#[derive(Copy, Clone)] enum IntfIdFields { InterfaceType, InterfaceVersion, @@ -57,16 +62,19 @@ enum IntfIdFields { } #[allow(dead_code)] +#[derive(Copy, Clone)] enum IntfId2Fields { Vid, Did, } +#[derive(Copy, Clone)] enum CtrlStsFields { TpmSts, TpmIdle, } +#[derive(Copy, Clone)] enum CrbRegister { LocState(LocStateFields), LocSts(LocStsFields), @@ -99,6 +107,7 @@ const CRB_LOC_CTRL_REQUEST_ACCESS: u32 = 1 << 0; const CRB_LOC_CTRL_RELINQUISH: u32 = 1 << 1; const CRB_LOC_CTRL_RESET_ESTABLISHMENT_BIT: u32 = 1 << 3; const CRB_LOC_STS: u32 = 0x0C; + const fn get_crb_loc_sts_field(f: LocStsFields) -> (u32, u32, u32) { let (offset, len) = match f { LocStsFields::Granted => (0, 1), @@ -219,9 +228,9 @@ pub struct Tpm { } impl Tpm { - pub fn new(path: String) -> Result { + pub fn new(path: impl AsRef) -> Result { let emulator = Emulator::new(path) - .map_err(|e| Error::Init(anyhow!("Failed while initializing tpm Emulator: {:?}", e)))?; + .map_err(|e| Error::Init(anyhow!("Failed while initializing tpm Emulator: {e:?}")))?; let mut tpm = Tpm { emulator, regs: [0; TPM_CRB_R_MAX], @@ -331,8 +340,7 @@ impl Tpm { if let Err(e) = self.emulator.startup_tpm(self.backend_buff_size) { return Err(Error::Init(anyhow!( - "Failed while running Startup TPM. Error: {:?}", - e + "Failed while running Startup TPM. Error: {e:?}" ))); } Ok(()) @@ -458,10 +466,9 @@ impl BusDevice for Tpm { CRB_CTRL_CANCEL => { if v == CRB_CANCEL_INVOKE && (self.regs[CRB_CTRL_START as usize] & CRB_START_INVOKE != 0) + && let Err(e) = self.emulator.cancel_cmd() { - if let Err(e) = self.emulator.cancel_cmd() { - error!("Failed to run cancel command. Error: {:?}", e); - } + error!("Failed to run cancel command. Error: {e:?}"); } } CRB_CTRL_START => { @@ -482,10 +489,7 @@ impl BusDevice for Tpm { } } CRB_LOC_CTRL => { - warn!( - "CRB_LOC_CTRL locality to write = {:?} val = {:?}", - locality, v - ); + warn!("CRB_LOC_CTRL locality to write = {locality:?} val = {v:?}"); match v { CRB_LOC_CTRL_RESET_ESTABLISHMENT_BIT => {} CRB_LOC_CTRL_RELINQUISH => { @@ -518,7 +522,7 @@ impl BusDevice for Tpm { ); } _ => { - error!("Invalid value to write in CRB_LOC_CTRL {:#X} ", v); + error!("Invalid value to write in CRB_LOC_CTRL {v:#X} "); } } } @@ -536,7 +540,7 @@ impl BusDevice for Tpm { } #[cfg(test)] -mod tests { +mod unit_tests { use super::*; #[test] diff --git a/docs/amd_sev_snp.md b/docs/amd_sev_snp.md index c3bcddf8d2..adf37f75a1 100644 --- a/docs/amd_sev_snp.md +++ b/docs/amd_sev_snp.md @@ -2,7 +2,7 @@ ### WARNING -This feature is only currently supported on MSHV. +This feature is currently only supported on MSHV. AMD Secure Encrypted Virtualization & Secure Nested Paging (SEV-SNP) is an AMD technology designed to add strong memory integrity protection to help prevent @@ -10,13 +10,12 @@ malicious hypervisor-based attacks like data replay, memory-remapping and more in order to create an isolated execution environment. Here are some useful links: -- [SNP Homepage](https://www.amd.com/content/dam/amd/en/documents/epyc-business-docs/solution-briefs/amd-secure-encrypted-virtualization-solution-brief.pdf): +- [SNP Homepage](https://docs.amd.com/v/u/en-US/amd-secure-encrypted-virtualization-solution-brief): more information about SEV-SNP technical aspects, design and specification. ## Cloud Hypervisor support -It is required to use a machine which has enabled support for AMD SEV-SNP in -the BIOS. +A machine with AMD SEV-SNP support which is enabled in the BIOS is required. On the Cloud Hypervisor side, all you need is to build the project with the `sev_snp` feature enabled: @@ -26,7 +25,7 @@ cargo build --no-default-features --features "sev_snp" ``` **Note** -Please note that `sev_snp` cannot be enabled in conjunction with `tdx` feature flag. +Please note that `sev_snp` cannot be enabled in conjunction with the `tdx` feature flag. You can run a SEV-SNP VM using the following command: @@ -38,4 +37,4 @@ You can run a SEV-SNP VM using the following command: --disk path=ubuntu.img ``` -For more information related to Microsoft Hypervisor please see [mshv.md](mshv.md) +For more information related to Microsoft Hypervisor, please see [mshv.md](mshv.md) diff --git a/docs/api.md b/docs/api.md index 94c465c0e3..d6f9be6e9e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,14 +8,14 @@ - [REST API Examples](#rest-api-examples) - [Create a Virtual Machine](#create-a-virtual-machine) - [Boot a Virtual Machine](#boot-a-virtual-machine) - - [Dump a Virtual Machine Information](#dump-a-virtual-machine-information) + - [Dump Virtual Machine Information](#dump-virtual-machine-information) - [Reboot a Virtual Machine](#reboot-a-virtual-machine) - [Shut a Virtual Machine Down](#shut-a-virtual-machine-down) - [D-Bus API](#d-bus-api) - [D-Bus API Location and availability](#d-bus-api-location-and-availability) - [D-Bus API Interface](#d-bus-api-interface) - [Command Line Interface](#command-line-interface) - - [REST API, D-Bus API and CLI Architectural Relationship](#rest-api-and-cli-architectural-relationship) + - [REST API, D-Bus API and CLI Architectural Relationship](#rest-api-d-bus-api-and-cli-architectural-relationship) - [Internal API](#internal-api) - [Goals and Design](#goals-and-design) - [End to End Example](#end-to-end-example) @@ -31,7 +31,7 @@ The Cloud Hypervisor API is made of 2 distinct interfaces: 1. **The internal API**, based on [rust's Multi-Producer, Single-Consumer (MPSC)](https://doc.rust-lang.org/std/sync/mpsc/) module. This API is used internally by the Cloud Hypervisor threads to - communicate between each others. + communicate with each other. The goal of this document is to describe the Cloud Hypervisor API as a whole, and to outline how the internal and external APIs are architecturally related. @@ -56,13 +56,6 @@ or a fd with `--api-socket fd=...`. ``` $ ./target/debug/cloud-hypervisor --api-socket path=/tmp/cloud-hypervisor.sock -Cloud Hypervisor Guest - API server: /tmp/cloud-hypervisor.sock - vCPUs: 1 - Memory: 512 MB - Kernel: None - Kernel cmdline: - Disk(s): None ``` #### REST API Endpoints @@ -78,36 +71,39 @@ The Cloud Hypervisor API exposes the following actions through its endpoints: ##### Virtual Machine (VM) Actions -| Action | Endpoint | Request Body | Response Body | Prerequisites | -| ---------------------------------- | ----------------------- | ------------------------------- | ------------------------ | ------------------------------------------------------ | -| Create the VM | `/vm.create` | `/schemas/VmConfig` | N/A | The VM is not created yet | -| Delete the VM | `/vm.delete` | N/A | N/A | N/A | -| Boot the VM | `/vm.boot` | N/A | N/A | The VM is created but not booted | -| Shut the VM down | `/vm.shutdown` | N/A | N/A | The VM is booted | -| Reboot the VM | `/vm.reboot` | N/A | N/A | The VM is booted | -| Trigger power button of the VM | `/vm.power-button` | N/A | N/A | The VM is booted | -| Pause the VM | `/vm.pause` | N/A | N/A | The VM is booted | -| Resume the VM | `/vm.resume` | N/A | N/A | The VM is paused | -| Task a snapshot of the VM | `/vm.snapshot` | `/schemas/VmSnapshotConfig` | N/A | The VM is paused | -| Perform a coredump of the VM* | `/vm.coredump` | `/schemas/VmCoredumpData` | N/A | The VM is paused | -| Restore the VM from a snapshot | `/vm.restore` | `/schemas/RestoreConfig` | N/A | The VM is created but not booted | -| Add/remove CPUs to/from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted | -| Add/remove memory from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted | -| Add/remove memory from a zone | `/vm.resize-zone` | `/schemas/VmResizeZone` | N/A | The VM is booted | -| Dump the VM information | `/vm.info` | N/A | `/schemas/VmInfo` | The VM is created | -| Add VFIO PCI device to the VM | `/vm.add-device` | `/schemas/VmAddDevice` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add disk device to the VM | `/vm.add-disk` | `/schemas/DiskConfig` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add fs device to the VM | `/vm.add-fs` | `/schemas/FsConfig` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add pmem device to the VM | `/vm.add-pmem` | `/schemas/PmemConfig` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add network device to the VM | `/vm.add-net` | `/schemas/NetConfig` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add userspace PCI device to the VM | `/vm.add-user-device` | `/schemas/VmAddUserDevice` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add vdpa device to the VM | `/vm.add-vdpa` | `/schemas/VdpaConfig` | `/schemas/PciDeviceInfo` | The VM is booted | -| Add vsock device to the VM | `/vm.add-vsock` | `/schemas/VsockConfig` | `/schemas/PciDeviceInfo` | The VM is booted | -| Remove device from the VM | `/vm.remove-device` | `/schemas/VmRemoveDevice` | N/A | The VM is booted | -| Dump the VM counters | `/vm.counters` | N/A | `/schemas/VmCounters` | The VM is booted | -| Inject an NMI | `/vm.nmi` | N/A | N/A | The VM is booted | -| Prepare to receive a migration | `/vm.receive-migration` | `/schemas/ReceiveMigrationData` | N/A | N/A | -| Start to send migration to target | `/vm.send-migration` | `/schemas/SendMigrationData` | N/A | The VM is booted and (shared mem or hugepages enabled) | +| Action | Endpoint | Request Body | Response Body | Prerequisites | +|-----------------------------------------| ---------------------------- | --------------------------------- | ------------------------ | ------------------------------------------------------ | +| Create the VM | `/vm.create` | `/schemas/VmConfig` | N/A | The VM is not created yet | +| Delete the VM | `/vm.delete` | N/A | N/A | N/A | +| Boot the VM | `/vm.boot` | N/A | N/A | The VM is created but not booted | +| Shut the VM down | `/vm.shutdown` | N/A | N/A | The VM is booted | +| Reboot the VM | `/vm.reboot` | N/A | N/A | The VM is booted | +| Trigger power button of the VM | `/vm.power-button` | N/A | N/A | The VM is booted | +| Pause the VM | `/vm.pause` | N/A | N/A | The VM is booted | +| Resume the VM | `/vm.resume` | N/A | N/A | The VM is paused | +| Trigger post-migration announce | `/vm.post-migration-announce` | N/A | N/A | The VM is booted and not paused | +| Take a snapshot of the VM | `/vm.snapshot` | `/schemas/VmSnapshotConfig` | N/A | The VM is paused | +| Perform a coredump of the VM* | `/vm.coredump` | `/schemas/VmCoredumpData` | N/A | The VM is paused | +| Restore the VM from a snapshot | `/vm.restore` | `/schemas/RestoreConfig` | N/A | The VM is created but not booted | +| Add/remove CPUs to/from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted | +| Add/remove memory from the VM | `/vm.resize` | `/schemas/VmResize` | N/A | The VM is booted | +| Resize a disk attached to the VM | `/vm.resize-disk` | `/schemas/VmResizeDisk` | N/A | The VM is created | +| Add/remove memory from a zone | `/vm.resize-zone` | `/schemas/VmResizeZone` | N/A | The VM is booted | +| Dump the VM information | `/vm.info` | N/A | `/schemas/VmInfo` | The VM is created | +| Add VFIO PCI device to the VM | `/vm.add-device` | `/schemas/VmAddDevice` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add disk device to the VM | `/vm.add-disk` | `/schemas/DiskConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add fs device to the VM | `/vm.add-fs` | `/schemas/FsConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add generic vhost-user device to the VM | `/vm.add-generic-vhost-user` | `/schemas/GenericVhostUserConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add pmem device to the VM | `/vm.add-pmem` | `/schemas/PmemConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add network device to the VM | `/vm.add-net` | `/schemas/NetConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add userspace PCI device to the VM | `/vm.add-user-device` | `/schemas/VmAddUserDevice` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add vdpa device to the VM | `/vm.add-vdpa` | `/schemas/VdpaConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Add vsock device to the VM | `/vm.add-vsock` | `/schemas/VsockConfig` | `/schemas/PciDeviceInfo` | The VM is booted | +| Remove device from the VM | `/vm.remove-device` | `/schemas/VmRemoveDevice` | N/A | The VM is booted | +| Dump the VM counters | `/vm.counters` | N/A | `/schemas/VmCounters` | The VM is booted | +| Inject an NMI | `/vm.nmi` | N/A | N/A | The VM is booted | +| Prepare to receive a migration | `/vm.receive-migration` | `/schemas/ReceiveMigrationData` | N/A | N/A | +| Start to send migration to target | `/vm.send-migration` | `/schemas/SendMigrationData` | N/A | The VM is booted and (shared mem or hugepages enabled) | * The `vmcoredump` action is available exclusively for the `x86_64` architecture and can be executed only when the `guest_debug` feature is @@ -121,13 +117,6 @@ the REST API available at `/tmp/cloud-hypervisor.sock`: ``` $ ./target/debug/cloud-hypervisor --api-socket /tmp/cloud-hypervisor.sock -Cloud Hypervisor Guest - API server: /tmp/cloud-hypervisor.sock - vCPUs: 1 - Memory: 512 MB - Kernel: None - Kernel cmdline: - Disk(s): None ``` ##### Create a Virtual Machine @@ -168,9 +157,9 @@ Once the VM is created, we can boot it: curl --unix-socket /tmp/cloud-hypervisor.sock -i -X PUT 'http://localhost/api/v1/vm.boot' ``` -##### Dump a Virtual Machine Information +##### Dump Virtual Machine Information -We can fetch information about any VM, as soon as it's created: +We can fetch information about any VM as soon as it's created: ```shell #!/usr/bin/env bash @@ -214,7 +203,7 @@ see [D-Bus API Interface](#d-bus-api-interface). #### D-Bus API Location and availability This feature is not compiled into Cloud Hypervisor by default. Users who -wish to use the D-Bus API, must explicitly enable it with the `dbus_api` +wish to use the D-Bus API must explicitly enable it with the `dbus_api` feature flag when compiling Cloud Hypervisor. ```sh @@ -291,7 +280,7 @@ From the CLI, one can: The REST API, D-Bus API and the CLI all rely on a common, [internal API](#internal-api). The CLI options are parsed by the -[clap crate](https://docs.rs/clap/4.3.11/clap/) and then translated into +[clap crate](https://docs.rs/clap/4.5.53/clap/) and then translated into [internal API](#internal-api) commands. The REST API is processed by an HTTP thread using the @@ -301,7 +290,7 @@ crate. As with the CLI, the HTTP requests eventually get translated into The D-Bus API is implemented using the [zbus](https://github.com/dbus2/zbus) crate and runs in its own thread. Whenever it needs to call the [internal API](#internal-api), -the [blocking](https://github.com/smol-rs/blocking) crate is used perform the call in zbus' async context. +the [blocking](https://github.com/smol-rs/blocking) crate is used to perform the call in zbus' async context. As a summary, the REST API, the D-Bus API and the CLI are essentially frontends for the [internal API](#internal-api): @@ -334,7 +323,7 @@ As a summary, the REST API, the D-Bus API and the CLI are essentially frontends The Cloud Hypervisor internal API, as its name suggests, is used internally by the different Cloud Hypervisor threads (VMM, HTTP, D-Bus, control loop, -etc) to send commands and responses to each others. +etc) to send commands and responses to each other. It is based on [rust's Multi-Producer, Single-Consumer (MPSC)](https://doc.rust-lang.org/std/sync/mpsc/), and the single consumer (a.k.a. the API receiver) is the Cloud Hypervisor @@ -378,9 +367,8 @@ APIs work together, let's look at a complete VM creation flow, from the [REST API](#rest-api) call, to the reply the external user will receive: 1. A user or operator sends an HTTP request to the Cloud Hypervisor - [REST API](#rest-api) in order to creates a virtual machine: - ``` - shell + [REST API](#rest-api) in order to create a virtual machine: + ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i \ @@ -427,7 +415,7 @@ APIs work together, let's look at a complete VM creation flow, from the the `VmCreate` payload, and extracts both the `VmConfig` structure and the [Sender](https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html) from the command payload. It stores the `VmConfig` structure and replies back to the - sender ((The HTTP thread): + sender (The HTTP thread): ```Rust match api_request { ApiRequest::VmCreate(config, sender) => { diff --git a/docs/balloon.md b/docs/balloon.md index 361c0fee34..ee6d5ac882 100644 --- a/docs/balloon.md +++ b/docs/balloon.md @@ -26,8 +26,8 @@ struct BalloonConfig { Size of the balloon device. It is subtracted from the VM's total size. For instance, if creating a VM with 4GiB of RAM, along with a balloon of 1GiB, the -guest will be able to use 3GiB of accessible memory. The guest sees all the RAM -and unless it is balloon enlightened is entitled to all of it. +guest will be able to use 3GiB of accessible memory. The guest sees all the RAM, +and unless it is balloon enlightened, it is entitled to all of it. This parameter is mandatory. @@ -42,7 +42,7 @@ _Example_ ### `deflate_on_oom` -Allow the guest to deflate the balloon if running Out Of Memory (OOM). Assuming +Allow the guest to deflate the balloon when running Out Of Memory (OOM). Assuming the balloon size is greater than 0, this means the guest is allowed to reduce the balloon size all the way down to 0 if this can help recover from the OOM event. diff --git a/docs/building.md b/docs/building.md index 39d9e1a056..b74941b390 100644 --- a/docs/building.md +++ b/docs/building.md @@ -24,8 +24,8 @@ Hypervisor. Here, all the steps are based on Ubuntu, for other Linux distributions please replace the package manager and package name. ```shell -# Install basic packages needed. For a package list targeting for more -# functionalities for example the test, please see resources/Dockerfile. +# Install basic dependencies. For a list of packages required for additional +# features (e.g., testing), please refer to resources/Dockerfile. $ sudo apt-get update $ sudo apt install git build-essential m4 bison flex uuid-dev qemu-utils musl-tools # Install rust tool chain diff --git a/docs/cpu.md b/docs/cpu.md index 5dd797315b..8ed247c909 100644 --- a/docs/cpu.md +++ b/docs/cpu.md @@ -11,30 +11,32 @@ to set vCPUs options for Cloud Hypervisor. ```rust struct CpusConfig { - boot_vcpus: u8, - max_vcpus: u8, + boot_vcpus: u32, + max_vcpus: u32, topology: Option, kvm_hyperv: bool, max_phys_bits: u8, affinity: Option>, features: CpuFeatures, + nested: bool, + core_scheduling: CoreScheduling, } ``` ``` ---cpus boot=,max=,topology=:::,kvm_hyperv=on|off,max_phys_bits=,affinity=,features= +--cpus boot=,max=,topology=:::,kvm_hyperv=on|off,max_phys_bits=,affinity=,features=,nested=on|off,core_scheduling=vm|vcpu|off ``` ### `boot` Number of vCPUs present at boot time. -This option allows to define a specific number of vCPUs to be present at the +This option allows defining a specific number of vCPUs to be present at the time the VM is started. This option is mandatory when using the `--cpus` parameter. If `--cpus` is not specified, this option takes the default value of `1`, starting the VM with a single vCPU. -Value is an unsigned integer of 8 bits. +Value is an unsigned integer of 32 bits. _Example_ @@ -47,14 +49,14 @@ _Example_ Maximum number of vCPUs. This option defines the maximum number of vCPUs that can be assigned to the VM. -In particular, this option is used when looking for CPU hotplug as it lets the -provide an indication about how many vCPUs might be needed later during the -runtime of the VM. +In particular, this option is used when looking for CPU hotplug as it provides +an indication about how many vCPUs might be needed later during the runtime of +the VM. For instance, if booting the VM with 2 vCPUs and a maximum of 6 vCPUs, it means up to 4 vCPUs can be added later at runtime by resizing the VM. The value must be greater than or equal to the number of boot vCPUs. -The value is an unsigned integer of 8 bits. +The value is an unsigned integer of 32 bits. By default this option takes the value of `boot`, meaning vCPU hotplug is not expected and can't be performed. @@ -72,16 +74,16 @@ Topology of the guest platform. This option gives the user a way to describe the exact topology that should be exposed to the guest. It can be useful to describe to the guest the same topology found on the host as it allows for proper usage of the resources and -is a way to achieve better performances. +is a way to achieve better performance. The topology is described through the following structure: ```rust struct CpuTopology { - threads_per_core: u8, - cores_per_die: u8, - dies_per_package: u8, - packages: u8, + threads_per_core: u16, + cores_per_die: u16, + dies_per_package: u16, + packages: u16, } ``` @@ -123,7 +125,7 @@ Maximum size for guest's addressable space. This option defines the maximum number of physical bits for all vCPUs, which sets a limit for the size of the guest's addressable space. This is mainly -useful for debug purpose. +useful for debugging purposes. The value is an unsigned integer of 8 bits. @@ -140,16 +142,16 @@ Affinity of each vCPU. This option gives the user a way to provide the host CPU set associated with each vCPU. It is useful for achieving CPU pinning, ensuring multiple VMs won't affect the performance of each other. It might also be used in the context of -NUMA as it is way of making sure the VM can run on a specific host NUMA node. -In general, this option is used to increase the performances of a VM depending +NUMA as it is a way of making sure the VM can run on a specific host NUMA node. +In general, this option is used to increase the performance of a VM depending on the host platform and the type of workload running in the guest. The affinity is described through the following structure: ```rust struct CpuAffinity { - vcpu: u8, - host_cpus: Vec, + vcpu: u32, + host_cpus: Vec, } ``` @@ -163,8 +165,8 @@ The outer brackets define the list of vCPUs. And for each vCPU, the inner brackets attached to `@` define the list of host CPUs the vCPU is allowed to run onto. -Multiple values can be provided to define each list. Each value is an unsigned -integer of 8 bits. +Multiple values can be provided to define each list. Each value is a +platform-native unsigned integer (`usize`). For instance, if one needs to run vCPU 0 on host CPUs from 0 to 4, the syntax using `-` will help define a contiguous range with `affinity=0@[0-4]`. The @@ -209,3 +211,45 @@ _Example_ ``` In this example the amx CPU feature will be enabled for the VMM. + + +### `nested` + +Enable nested virtualization (default on). Nested virtualization is needed to access hardware virtualization by this guest. This option can only be changed on x86-64. + +_Example_ + +``` +--cpus nested=on +``` + +### `core_scheduling` + +Core scheduling mode for vCPU threads. + +This option controls Linux core scheduling (`PR_SCHED_CORE`) for vCPU threads, +which prevents untrusted tasks from sharing SMT siblings. This mitigates +side-channel attacks (e.g. MDS, L1TF) between vCPU threads. + +Three modes are available: + +- `vm` (default): All vCPU threads share a single core scheduling cookie. + vCPUs may be co-scheduled on SMT siblings of the same core, providing + better performance while still isolating VM threads from host tasks. +- `vcpu`: Each vCPU thread gets its own unique cookie. No two vCPUs can + share SMT siblings, providing the strongest isolation between vCPUs at + the cost of performance. +- `off`: No core scheduling is applied. + +On kernels older than 5.14 (which lack `PR_SCHED_CORE` support), the +option silently has no effect. + +_Example_ + +``` +--cpus boot=2,core_scheduling=vm +``` + +In this example, both vCPUs will share the same core scheduling cookie, +allowing them to be co-scheduled on SMT siblings while preventing host +threads from sharing those siblings. diff --git a/docs/cpu_profile_generation.md b/docs/cpu_profile_generation.md new file mode 100644 index 0000000000..ccd7b47f70 --- /dev/null +++ b/docs/cpu_profile_generation.md @@ -0,0 +1,26 @@ +# CPU Profile Generation + +## Generating a CPU profile for a new target + +To generate a new CPU profile you execute the following command + +```shell +$ cargo run --release -p arch --bin generate-cpu-profile --features="cpu_profile_generation" "" +``` +on the machine you want to create a CPU profile for. This creates four new files in the `arch/src/x86_64/cpu_profiles` directory: +- `.cpuid.json` +- `.msr.json` +- one license file for each of the two files listed above + +check them in to git and then simply rebuild cloud-hypervisor `cargo build --release --bin cloud-hypervisor`. + +You can now use the new profile by adding `,profile=` to the list of `--cpus` configuration +options on the command line. + +## Can existing CPU profiles be updated? + +More recent KVM versions may introduce more support for already existing hardware features. When this happens it is of course +tempting to run the CPU profile generation tool again with the new KVM version as we then get a profile supporting more CPU +functionality. Doing this without giving the CPU profile a new name is however a breaking change and thus not permitted. +Such PRs will **not be accepted**. Instead we encourage you add a `V2` (or higher number if `V` already exists) suffix +when generating the profile. diff --git a/docs/debug-port.md b/docs/debug-port.md index 8983a8f0cb..490fae0924 100644 --- a/docs/debug-port.md +++ b/docs/debug-port.md @@ -13,7 +13,7 @@ be used simultaneously. ### `0x80` I/O port -Whenever the guest write one byte between `0x0` and `0xF` on this particular +Whenever the guest writes one byte between `0x0` and `0xF` on this particular I/O port, `cloud-hypervisor` will log and timestamp that event at the `debug` log level. @@ -52,7 +52,7 @@ to easily grep for the tracing logs (e.g. ``` ./target/debug/cloud-hypervisor \ - --kernel ~/rust-hypervisor-firmware/target/target/release/hypervisor-fw \ + --kernel ~/rust-hypervisor-firmware/target/release/hypervisor-fw \ --disk path=~/hypervisor/images/focal-server-cloudimg-amd64.raw \ --cpus 4 \ --memory size=1024M \ @@ -94,4 +94,4 @@ The `0x80` debug port and the port of the firmware debug device are always available. The debug console must be activated via the command line, but provides more configuration options. -You can use different ports for different aspect of your logging messages. +You can use different ports for different aspects of your logging messages. diff --git a/docs/device_model.md b/docs/device_model.md index 0233ad07a0..e915c47eec 100644 --- a/docs/device_model.md +++ b/docs/device_model.md @@ -31,7 +31,7 @@ Simple emulation of a serial port by reading and writing to specific port I/O addresses. The serial port can be very useful to gather early logs from the operating system booted inside the VM. -For x86_64, The default serial port is from an emulated 16550A device. It can +For x86_64, the default serial port is from an emulated 16550A device. It can be used as the default console for Linux when booting with the option `console=ttyS0`. For AArch64, the default serial port is from an emulated PL011 UART device. The related command line for AArch64 is `console=ttyAMA0`. @@ -48,7 +48,7 @@ This device is built-in by default, but it can be compiled out with Rust features. When compiled in, it is always enabled, and cannot be disabled from the command line. -For AArch64 machines, an ARM PrimeCell Real Time Clock(PL031) is implemented. +For AArch64 machines, an ARM PrimeCell Real Time Clock (PL031) is implemented. This device is built-in by default for the AArch64 platform, and it is always enabled, and cannot be disabled from the command line. @@ -90,8 +90,9 @@ feature is enabled by default. For all virtio devices listed below, only `virtio-pci` transport layer is supported. Cloud Hypervisor supports multiple PCI segments, and users can -append `,pci_segment=` to the device flag in the Cloud -Hypervisor command line to assign devices to a specific PCI segment. +append `,pci_segment=` or `,pci_device_id=` to +the device flag in the Cloud Hypervisor command line to assign devices to a specific +PCI segment or into a specific device slot. ### virtio-block @@ -136,7 +137,7 @@ flag `--net`. The `virtio-pmem` implementation emulates a virtual persistent memory device that `cloud-hypervisor` can e.g. boot from. Booting from a `virtio-pmem` device -allows to bypass the guest page cache and improve the guest memory footprint. +allows bypassing the guest page cache and improve the guest memory footprint. This device is always built-in, and it is enabled based on the presence of the flag `--pmem`. @@ -201,6 +202,24 @@ networking device (e.g. DPDK) into the VMM as their virtio network backend. This device is always built-in, and it is enabled when `vhost_user=true` and `socket` are provided to the `--net` parameter. +### vhost-user-generic + +This is a generic vhost-user device. The main use case is to provide a +vhost-user device that Cloud Hypervisor doesn't support natively. However, +there is nothing preventing its use for devices that Cloud Hypervisor does +support. For instance, the tag of a virtio-fs device can be set on the +virtiofsd command line, whereas the built-in virtio-fs support +requires the tag to be set in Cloud Hypervisor's command line. + +If the backend negotiates the `VHOST_USER_PROTOCOL_F_CONFIG` feature, +all configuration space access will be handled by it. Otherwise, +writes will be ignored and reads will return 0xFF. Cloud Hypervisor +warns if this happens. + +This device is always built-in, and it is enabled when the +`--generic-vhost-user` flag is passed. +See [the generic vhost-user documentation](generic-vhost-user.md) for more details. + ## VFIO VFIO (Virtual Function I/O) is a kernel framework that exposes direct device diff --git a/docs/disk_locking.md b/docs/disk_locking.md new file mode 100644 index 0000000000..e205031e3a --- /dev/null +++ b/docs/disk_locking.md @@ -0,0 +1,72 @@ +# Disk Image Locking + +Cloud Hypervisor places an advisory lock on each disk image opened via +`--disk` to prevent multiple instances from concurrently accessing the +same file. This avoids potential data corruption from overlapping writes. +Locks are advisory and require cooperating processes; a non-cooperating +process can still open and write to a locked file. Locking is host-local +and does not enforce coordination across multiple hosts. + +If the backing file resides on network storage, the storage system must +correctly translate or propagate OFD (Open File Description) locks across +the network to ensure that advisory locking semantics are preserved in a +multi-host environment. In the case of Linux, OFD locks are translated +into NFS locks by the NFS driver. + +The implementation uses Open File Description (OFD) locks (`F_OFD_SETLK`) +rather than traditional POSIX locks (`F_SETLK`). OFD locks are only +released when the last file descriptor referencing the open file +description is closed, preventing accidental early release. + +## Lock Granularity + +The `lock_granularity` parameter controls how the lock is placed on the +disk image: + +``` +--disk path=/bar.img,lock_granularity=qemu-compatible +--disk path=/foo.img,lock_granularity=byte-range +--disk path=/bar.img,lock_granularity=full +``` + +### `qemu-compatible` (default) + +Mimics QEMU's file locking behavior. Only locks marker-bytes to express +QEMU's file locking semantics. + +For read-only disks, this translates to QEMU's `BLK_PERM_CONSISTENT_READ` +with `BLK_PERM_WRITE` unshared. +For read-write disks, this translates to QEMU's `BLK_PERM_CONSISTENT_READ` +and `BLK_PERM_WRITE` with `BLK_PERM_WRITE` unshared. + +### `byte-range` + +Locks the byte range `[0, physical_file_size)`. The physical file size +is evaluated once at startup; if the file grows after the lock is +acquired, the newly appended region is not covered by the lock. + +The file is protected against concurrent access by other instances of +Cloud Hypervisor. That's the only thing we can guarantee. + +#### Fallback to full + +One caveat is that if the physical size of the disk image cannot be +determined at startup (e.g. with certain vhost-user backends), Cloud +Hypervisor falls back to a whole-file lock regardless of the +`lock_granularity` setting, as a byte-range lock cannot be safely +computed without knowing the physical file size. + +### `full` + +Locks the entire file using the OFD whole-file semantic (`l_start=0`, +`l_len=0`). This may be needed in environments that depend on whole-file +lock semantics. Note that on some network storage backends, whole-file +OFD locks may be treated as mandatory rather than advisory, which can +cause external tools to fail when accessing the disk image. Lock +behavior may also vary across network filesystem implementations. + +## Disk Resizing + +Cloud Hypervisor supports live disk resizing. Currently, byte-range +locks are not updated. However, as a part of the file is still locked, +no new Cloud Hypervisor instance can open the disk image. diff --git a/docs/fw_cfg.md b/docs/fw_cfg.md new file mode 100644 index 0000000000..76e6951f45 --- /dev/null +++ b/docs/fw_cfg.md @@ -0,0 +1,90 @@ +# Firmware Configuration (fw_cfg) Device + +The `fw_cfg` device is a QEMU-compatible device that allows the hypervisor to pass configuration and data to the guest operating system. This is particularly useful for firmware to access information like ACPI tables, kernel images, initramfs, kernel command lines, and other arbitrary data blobs. + +Cloud Hypervisor implements the `fw_cfg` device with DMA-enabled access. + +## Purpose + +The `fw_cfg` device serves as a generic information channel between the VMM and the guest. It can be used to: + +* Load the kernel, initramfs, and kernel command line for direct kernel boot with firmware. +* Provide ACPI tables to the guest firmware or OS. +* Pass custom configuration files or data blobs (e.g., attestation data, SEV-SNP launch secrets) to the guest. +* Supply an E820 memory map to the guest. + +## Enabling `fw_cfg` + +The `fw_cfg` device is enabled via the `fw_cfg` feature flag when building Cloud Hypervisor: + +```bash +cargo build --features fw_cfg +``` + +## Guest Kernel Configuration + +For the guest Linux kernel to recognize and use the `fw_cfg` device via sysfs, the following kernel configuration option must be enabled: + +* `CONFIG_FW_CFG_SYSFS=y` + +This option allows the kernel to expose `fw_cfg` entries under `/sys/firmware/qemu_fw_cfg/by_name/`. + +## Command Line Options + +The `fw_cfg` device is configured using the `--fw-cfg-config` command-line option. + +**Parameters:** +* `e820=on|off`: (Default: `on`) Whether to add an E820 memory map entry to `fw_cfg`. +* `kernel=on|off`: (Default: `on`) Whether to add the kernel image (specified by `--kernel`) to `fw_cfg`. +* `cmdline=on|off`: (Default: `on`) Whether to add the kernel command line (specified by `--cmdline`) to `fw_cfg`. +* `initramfs=on|off`: (Default: `on`) Whether to add the initramfs image (specified by `--initramfs`) to `fw_cfg`. +* `acpi_table=on|off`: (Default: `on`) Whether to add generated ACPI tables to `fw_cfg`. +* `items=[... : ...]`: A list of custom key-value pairs to be exposed via `fw_cfg`. Multiple items are separated by `:`. + * `name=`: The path under which the item will appear in the guest's sysfs (e.g., `opt/org.example/my-data`). + * `file=`: The path to a file on the host whose content will be provided to the guest for this item. + * `string=`: An inline string value to provide to the guest for this item. Each item must have exactly one of `file` or `string`, not both. + +**Example Usage:** + +1. **Direct kernel boot with custom `fw_cfg` entries:** + + ```bash + cloud-hypervisor \ + --kernel /path/to/vmlinux \ + --cmdline "console=hvc0 root=/dev/vda1" \ + --disk path=/path/to/rootfs.img \ + --fw-cfg-config initramfs=off,items=[name=opt/org.mycorp/setup_info,file=/tmp/guest_setup.txt] \ + ... + ``` + In the guest, `/tmp/guest_setup.txt` from the host will be accessible at `/sys/firmware/qemu_fw_cfg/by_name/opt/org.mycorp/setup_info/raw`. + +2. **Inline string items (e.g., OVMF MMIO64 configuration for GPU passthrough):** + + ```bash + cloud-hypervisor \ + --firmware /path/to/OVMF.fd \ + --disk path=/path/to/rootfs.img \ + --device path=/sys/bus/pci/devices/0000:41:00.0 \ + --fw-cfg-config items=[name=opt/ovmf/X-PciMmio64Mb,string=262144] \ + ... + ``` + The string `262144` is passed directly to the guest as the content of `opt/ovmf/X-PciMmio64Mb`. + +3. **Disabling `fw_cfg` explicitly:** + + ```bash + cloud-hypervisor \ + --fw-cfg-config disable \ + ... + ``` + +## Accessing `fw_cfg` Items in the Guest + +If `CONFIG_FW_CFG_SYSFS` is enabled in the guest kernel, items added to `fw_cfg` can be accessed via sysfs. + +For example, an item added with `name=opt/org.example/my-data` will be available at: +`/sys/firmware/qemu_fw_cfg/by_name/opt/org.example/my-data/raw` + +The `raw` file contains the binary content of the host file provided. + +Standard items like kernel, initramfs, cmdline, and ACPI tables also have predefined names (e.g., `etc/kernel`, `etc/cmdline`) if they are enabled to be passed via `fw_cfg`. diff --git a/docs/gdb.md b/docs/gdb.md index 10b75c9f6c..d9d90ec584 100644 --- a/docs/gdb.md +++ b/docs/gdb.md @@ -1,6 +1,6 @@ # GDB Support -This feature allows remote guest debugging using GDB. Note that this feature is only supported on x86_64/KVM. +This feature allows remote guest debugging using GDB. Note that this feature is supported on x86_64 and aarch64 with KVM. To enable debugging with GDB, build with the `guest_debug` feature enabled: @@ -8,7 +8,7 @@ To enable debugging with GDB, build with the `guest_debug` feature enabled: cargo build --features guest_debug ``` -To use the `--gdb` option, specify the Unix Domain Socket with `--path` that Cloud Hypervisor will use to communicate with the host's GDB: +To use the `--gdb` option, specify the Unix Domain Socket with `path` that Cloud Hypervisor will use to communicate with the host's GDB: ```bash ./cloud-hypervisor \ diff --git a/docs/generic-vhost-user.md b/docs/generic-vhost-user.md new file mode 100644 index 0000000000..6af813e28c --- /dev/null +++ b/docs/generic-vhost-user.md @@ -0,0 +1,76 @@ +# How to use generic vhost-user devices + +## What is a generic vhost-user device? + +Cloud Hypervisor deliberately does not have support for all types of virtio devices. +For instance, it does not natively support sound or media. + +However, the vhost-user protocol does not require the frontend to have separate +code for each type of vhost-user device. This allows writing a *generic* frontend +that supports almost all of them. + +Any vhost-user device that only uses supported protocol messages is +expected to work. It can (and often will) be of a type that Cloud +Hypervisor does not know about. It can even be of a type that is +not standardized. + +Virtio-GPU is known to *not* work. The version implemented in QEMU +requires `VHOST_USER_GPU_SET_SOCKET`, which is standard but will +never be implemented by Cloud Hypervisor. Other versions require +messages that have not been standardized. In the future, these +versions might be supported. + +## Examples + +virtiofsd meets these requirements if the `--tag` argument is passed. +Therefore, generic vhost-user can be used as an alternative to the built-in +virtio-fs support. See [fs.md](fs.md) for how to build the virtiofs daemon. + +To use generic vhost-user with virtiofsd, use a command line argument +similar to this: + +```bash +/path/to/virtiofsd \ + --tag=myfs \ + --log-level=debug \ + "--socket-path=$path_to_virtiofsd_socket" \ + "--shared-dir=$path_to_shared_directory" \ + "${other_virtiofsd_options[@]}" & + +/path/to/cloud-hypervisor \ + --cpus boot=1 \ + --memory size=1G,shared=on \ + --disk path=your-linux-image.iso \ + --kernel vmlinux \ + --cmdline "console=hvc0 root=/dev/vda1 rw" \ + --generic-vhost-user "socket=\"${path_to_virtiofsd_socket//\"/\"\"}\",virtio_id=26,queue_sizes=[512,512]" \ + "${other_cloud_hypervisor_options[@]}" +``` + +26 is the ID for a virtio-fs device. The IDs for other devices are defined +by the VIRTIO specification. The odd-looking variable expansion escapes +any double quotes in the socket path. It is also possible to provide +the name that is defined by the virtio specification, so `virtio_id=fs` +will also work. + +Inside the guest, you can mount the virtio-fs device with + +```bash +mkdir mount_dir +mount -t virtiofs -- myfs mount_dir/ +``` + +## Limitations + +Cloud Hypervisor does not save, restore, or migrate the PCI configuration +space of a generic vhost-user device. The backend can do it itself, but if +it does not these features will not work. + +Cloud Hypervisor cannot validate the number or size of the queues. Some +guest drivers do not validate these and will crash if they are wrong. +Notably, at least some versions of Linux will crash if one creates a +virtio-fs device (id 26) with only one queue. + +If any access to configuration space fails, Cloud Hypervisor will panic +instead of injecting an exception into the guest. It is unclear what +correct behavior is in this case. diff --git a/docs/hotplug.md b/docs/hotplug.md index 86ba9ad631..35e9053611 100644 --- a/docs/hotplug.md +++ b/docs/hotplug.md @@ -110,7 +110,7 @@ Mem: 3.0Gi 71Mi 2.8Gi 0.0Ki 47Mi 2.8Gi Swap: 32Mi 0B 32Mi ``` -Due to guest OS limitations is is necessary to ensure that amount of memory added (between currently assigned RAM and that which is desired) is a multiple of 128MiB. +Due to guest OS limitations it is necessary to ensure that amount of memory added (between currently assigned RAM and that which is desired) is a multiple of 128MiB. The same API can also be used to reduce the desired RAM for a VM but the change will not be applied until the VM is rebooted. @@ -179,7 +179,7 @@ Notice the addition of `--api-socket=/tmp/ch-socket`. ### Add VFIO Device -To ask the VMM to add additional VFIO device then use the `add-device` API. +To ask the VMM to add additional VFIO device, use the `add-device` API. ```shell ./ch-remote --api-socket=/tmp/ch-socket add-device path=/sys/bus/pci/devices/0000:01:00.0/ @@ -187,7 +187,7 @@ To ask the VMM to add additional VFIO device then use the `add-device` API. ### Add Disk Device -To ask the VMM to add additional disk device then use the `add-disk` API. +To ask the VMM to add additional disk device, use the `add-disk` API. ```shell ./ch-remote --api-socket=/tmp/ch-socket add-disk path=/foo/bar/cloud.img @@ -195,7 +195,7 @@ To ask the VMM to add additional disk device then use the `add-disk` API. ### Add Fs Device -To ask the VMM to add additional fs device then use the `add-fs` API. +To ask the VMM to add additional fs device, use the `add-fs` API. ```shell ./ch-remote --api-socket=/tmp/ch-socket add-fs tag=myfs,socket=/foo/bar/virtiofs.sock @@ -203,7 +203,7 @@ To ask the VMM to add additional fs device then use the `add-fs` API. ### Add Net Device -To ask the VMM to add additional network device then use the `add-net` API. +To ask the VMM to add additional network device, use the `add-net` API. ```shell ./ch-remote --api-socket=/tmp/ch-socket add-net tap=chtap0 @@ -211,7 +211,7 @@ To ask the VMM to add additional network device then use the `add-net` API. ### Add Pmem Device -To ask the VMM to add additional PMEM device then use the `add-pmem` API. +To ask the VMM to add additional PMEM device, use the `add-pmem` API. ```shell ./ch-remote --api-socket=/tmp/ch-socket add-pmem file=/foo/bar.cloud.img @@ -219,7 +219,7 @@ To ask the VMM to add additional PMEM device then use the `add-pmem` API. ### Add Vsock Device -To ask the VMM to add additional vsock device then use the `add-vsock` API. +To ask the VMM to add additional vsock device, use the `add-vsock` API. ```shell ./ch-remote --api-socket=/tmp/ch-socket add-vsock cid=3,socket=/foo/bar/vsock.sock @@ -241,7 +241,7 @@ After a reboot the added PCI device will remain. ### Remove PCI device -Removing a PCI device works the same way for all kind of PCI devices. The unique identifier related to the device must be provided. This identifier can be provided by the user when adding the new device, or by default Cloud Hypervisor will assign one. +Removing a PCI device works the same way for all kinds of PCI devices. The unique identifier related to the device must be provided. This identifier can be provided by the user when adding the new device, or by default Cloud Hypervisor will assign one. ```shell ./ch-remote --api-socket=/tmp/ch-socket remove-device _disk0 diff --git a/docs/intel_sgx.md b/docs/intel_sgx.md deleted file mode 100644 index 9f2ca76bdc..0000000000 --- a/docs/intel_sgx.md +++ /dev/null @@ -1,54 +0,0 @@ -# Intel SGX - -Intel® Software Guard Extensions (Intel® SGX) is an Intel technology designed -to increase the security of application code and data. Cloud Hypervisor supports -SGX virtualization through KVM. Because SGX is built on hardware features that -cannot be emulated in software, virtualizing SGX requires support in KVM and in -the host kernel. The required Linux and KVM changes can be found in Linux 5.13+. - -Utilizing SGX in the guest requires a kernel/OS with SGX support, e.g. a kernel -since release 5.11, see -[here](https://www.intel.com/content/www/us/en/developer/tools/software-guard-extensions/linux-overview.html). -Running Linux 5.13+ as the guest kernel allows nested virtualization of SGX. - -For more information about SGX, please refer to the [SGX Homepage](https://www.intel.com/content/www/us/en/developer/tools/software-guard-extensions/linux-overview.html). - -For more information about SGX SDK and how to test SGX, please refer to the -following [instructions](https://github.com/intel/linux-sgx). - -## Cloud Hypervisor support - -Assuming the host exposes `/dev/sgx_vepc`, we can pass SGX enclaves through -the guest. - -In order to use SGX enclaves within a Cloud Hypervisor VM, we must define one -or several Enclave Page Cache (EPC) sections. Here is an example of a VM being -created with 2 EPC sections, the first one being 64MiB with pre-allocated -memory, the second one being 32MiB with no pre-allocated memory. - -```bash -./cloud-hypervisor \ - --cpus boot=1 \ - --memory size=1G \ - --disk path=focal-server-cloudimg-amd64.raw \ - --kernel vmlinux \ - --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ - --sgx-epc id=epc0,size=64M,prefault=on id=epc1,size=32M,prefault=off -``` - -Once booted, and assuming your guest kernel contains the patches from the -[KVM SGX Tree](https://github.com/intel/kvm-sgx), you can validate SGX devices -have been correctly created under `/dev/sgx`: - -```bash -ls /dev/sgx* -/dev/sgx_enclave /dev/sgx_provision /dev/sgx_vepc -``` - -From this point, it is possible to run any SGX application from the guest, as -it will access `/dev/sgx_enclave` device to create dedicated SGX enclaves. - -Note: There is only one contiguous SGX EPC region, which contains all SGX EPC -sections. This region is exposed through ACPI and marked as reserved through -the e820 table. It is treated as yet another device, which means it should -appear at the end of the guest address space. diff --git a/docs/intel_tdx.md b/docs/intel_tdx.md index 95bb5a1b55..f2e7aa9b1f 100644 --- a/docs/intel_tdx.md +++ b/docs/intel_tdx.md @@ -75,7 +75,7 @@ meaning it will be printing guest kernel logs to the `virtio-console` device. ```bash ./cloud-hypervisor \ - --platform tdx=on + --platform tdx=on \ --firmware edk2/Build/IntelTdx/RELEASE_GCC5/FV/OVMF.fd \ --cpus boot=1 \ --memory size=1G \ @@ -87,7 +87,7 @@ firmware: ```bash ./cloud-hypervisor \ - --platform tdx=on + --platform tdx=on \ --firmware edk2/Build/IntelTdx/DEBUG_GCC5/FV/OVMF.fd \ --cpus boot=1 \ --memory size=1G \ @@ -105,7 +105,7 @@ This is a lightweight version of the TDVF, written in Rust and designed for direct kernel boot, which is useful for containers use cases. To build TDShim from source, it is required to install `Rust`, `NASM`, -and `LLVM` first. The TDshim can be build as follows: +and `LLVM` first. The TDshim can be built as follows: ```bash git clone https://github.com/confidential-containers/td-shim @@ -136,10 +136,10 @@ option as well. ```bash ./cloud-hypervisor \ - --platform tdx=on + --platform tdx=on \ --firmware td-shim/target/release/final.bin \ --kernel bzImage \ - --cmdline "root=/dev/vda3 console=hvc0 rw" + --cmdline "root=/dev/vda3 console=hvc0 rw" \ --cpus boot=1 \ --memory size=1G \ --disk path=tdx_guest_img @@ -150,10 +150,10 @@ TDShim: ```bash ./cloud-hypervisor \ - --platform tdx=on + --platform tdx=on \ --firmware td-shim/target/debug/final.bin \ --kernel bzImage \ - --cmdline "root=/dev/vda3 console=hvc0 rw" + --cmdline "root=/dev/vda3 console=hvc0 rw" \ --cpus boot=1 \ --memory size=1G \ --disk path=tdx_guest_img diff --git a/docs/io_throttling.md b/docs/io_throttling.md index 76b0219564..22c5dfc6f7 100644 --- a/docs/io_throttling.md +++ b/docs/io_throttling.md @@ -27,11 +27,11 @@ Hypervisor provides another three options for limiting I/O operations, i.e., `ops_size` (I/O operations), `ops_one_time_burst` (I/O operations), and `ops_refill_time` (ms). -One caveat in the I/O throttling is that every-time the bucket gets +One caveat in the I/O throttling is that every time the bucket gets empty, it will stop I/O operations for a fixed amount of time (`cool_down_time`). The `cool_down_time` now is fixed at `100 ms`, it -can have big implications to the actual rate limit (which can be a lot -different the expected "refill-rate" derived from user inputs). For +can have big implications for the actual rate limit (which can be quite +different from the expected "refill-rate" derived from user inputs). For example, to have a 1000 IOPS limit on a virtio-blk device, users should be able to provide either of the following two options: `ops_size=1000,ops_refill_time=1000` or @@ -53,5 +53,5 @@ demonstrates how to throttle the aggregate bandwidth of two disks to 10 MiB/s. ``` --disk path=disk0.raw,rate_limit_group=group0 \ path=disk1.raw,rate_limit_group=group0 \ ---rate-limit-group bw_size=1048576,bw_refill_time,bw_refill_time=100 +--rate-limit-group bw_size=1048576,bw_refill_time=100 ``` diff --git a/docs/iommu.md b/docs/iommu.md index 21d1d30c47..cf7a640ef5 100644 --- a/docs/iommu.md +++ b/docs/iommu.md @@ -15,7 +15,7 @@ to increase the security regarding the memory accesses performed by the virtual devices (VIRTIO devices), on behalf of the guest drivers. With a virtual IOMMU, the VMM stands between the guest driver and its device -counterpart, validating and translating every address before to try accessing +counterpart, validating and translating every address before trying accessing the guest memory. This is standard interposition that is performed here by the VMM. @@ -75,8 +75,8 @@ Not all devices support this extra option, and the default value will always be `off` since we want to avoid the performance impact for most users who don't need this. -Refer to the command line `--help` to find out which device support to be -attached to the virtual IOMMU. +Refer to the command line `--help` to find out which devices can be supported +to be attached to the virtual IOMMU. Below is a simple example exposing the `virtio-blk` device as attached to the virtual IOMMU: @@ -128,7 +128,7 @@ When ACPI is disabled, virtual IOMMU is supported through Flattened Device Tree IOMMU-attached and which should not. No matter how many devices you attached to the virtual IOMMU by setting `iommu=on` option, all the devices on the PCI bus will be attached to the virtual IOMMU (except the IOMMU itself). Each of the -devices will be added into a IOMMU group. +devices will be added into an IOMMU group. As a result, the directory content of `/sys/kernel/iommu_groups` would be: @@ -151,7 +151,7 @@ of requests need to be issued in order to create large mappings. One use case is even more impacted by the slowdown, the nested VFIO case. When passing a device through a L2 guest, the VFIO driver running in L1 will update the DMAR entries for the specific device. Because VFIO pins the entire guest -memory, this means the entire mapping of the L2 guest need to be stored into +memory, this means the entire mapping of the L2 guest needs to be stored into multiple 4k mappings. Obviously, the bigger the L2 guest RAM is, the longer the update of the mappings will last. There is an additional problem happening in this case, if the L2 guest RAM is quite large, it will require a large number @@ -194,7 +194,7 @@ be consumed. ### Nested usage Let's now look at the specific example of nested virtualization. In order to -reach optimized performances, the L2 guest also need to be mapped based on +reach optimized performances, the L2 guest also needs to be mapped based on huge pages. Here is how to achieve this, assuming the physical device you are passing through is `0000:00:01.0`. diff --git a/docs/ivshmem.md b/docs/ivshmem.md new file mode 100644 index 0000000000..0bc82cbb9a --- /dev/null +++ b/docs/ivshmem.md @@ -0,0 +1,52 @@ +# Inter-VM shared memory device + +The Inter-VM shared memory device (ivshmem) is designed to share a memory +region between a guest and the host. In order for all guests to be able to +pick up the shared memory area, it is modeled as a PCI device exposing said +memory to the guest as a PCI BAR. + +Device Specification is available +at https://www.qemu.org/docs/master/specs/ivshmem-spec.html. + +Now we support setting a backend file to share data between host and guest. +In other words, we only support ivshmem-plain and ivshmem-doorbell is not +supported yet. + +## Usage + +`--ivshmem`, an optional argument, can be passed to enable ivshmem device. +This argument takes a file as a `path` value and a file size as a `size` value. +The `size` value must be 2^n. + +``` +--ivshmem device backend file "path=,size=" +``` + +## Example + +Create a file with a size bigger than passed to `cloud-hypervisor`: + +``` +truncate -s 1M /tmp/ivshmem.data +``` + +Start application to mmap the file data to a memory region: + +``` +./cloud-hypervisor \ + --api-socket /tmp/cloud-hypervisor.sock \ + --kernel vmlinux \ + --disk path=focal-server-cloudimg-amd64.raw \ + --cpus boot=4 \ + --memory size=1024M \ + --ivshmem path=/tmp/ivshmem.data,size=1M +``` + +Insmod an ivshmem device driver to enable the device. The file data will be +mmapped to the PCI `bar2` of ivshmem device, +guest can r/w data by accessing this memory. + +A simple example of ivshmem driver can be obtained from: +https://github.com/lisongqian/clh-linux/commits/ch-6.12.8-ivshmem + +The host process can r/w this data by remapping the `/tmp/ivshmem.data`. diff --git a/docs/landlock.md b/docs/landlock.md index 571ddc7086..92ab0648d2 100644 --- a/docs/landlock.md +++ b/docs/landlock.md @@ -16,11 +16,11 @@ permissions. ## Host Setup -Landlock should be enabled in Host kernel to use it with cloud-hypervisor. -Please following [Kernel-Support](https://docs.kernel.org/userspace-api/landlock.html#kernel-support) link to enable Landlock on Host kernel. +Landlock should be enabled in host kernel to use it with cloud-hypervisor. +Please follow [Kernel-Support](https://docs.kernel.org/userspace-api/landlock.html#kernel-support) link to enable Landlock on Host kernel. -Landlock support can be checked with following command: +Landlock support can be checked with the following command: ``` $ sudo dmesg | grep -w landlock [ 0.000000] landlock: Up and running. @@ -30,8 +30,8 @@ Linux kernel confirms Landlock support with above message in dmesg. ## Enable Landlock At the time of enabling Landlock, Cloud-Hypervisor process needs the complete -list of files it accesses over its lifetime. So, Landlock is enabled `vm_create` -stage of guest boot. +list of files it accesses over its lifetime. So, Landlock is enabled at the +`vm_create` stage of guest boot. ### Command Line Append `--landlock` to Cloud-Hypervisor's command line to enable Landlock diff --git a/docs/live_migration.md b/docs/live_migration.md index 94c9afc236..36191dfc0c 100644 --- a/docs/live_migration.md +++ b/docs/live_migration.md @@ -3,10 +3,11 @@ This document gives examples of how to use the live migration support in Cloud Hypervisor: -1. local migration - migrating a VM from one Cloud Hypervisor instance to another on the same machine; -1. remote migration - migrating a VM between two machines; +1. **Local Migration**: Migrating a VM from one Cloud Hypervisor instance to another on the same machine; also called + UNIX socket migration. +1. **Remote Migration** (TCP Migration): migrating a VM between two TCP/IP hosts. -> :warning: These examples place sockets /tmp. This is done for +> :warning: These examples place sockets in /tmp. This is done for > simplicity and should not be done in production. ## Local Migration (Suitable for Live Upgrade of VMM) @@ -28,7 +29,8 @@ Launch the destination VM from the same directory (on the host machine): $ target/release/cloud-hypervisor --api-socket=/tmp/api2 ``` -Get ready for receiving migration for the destination VM (on the host machine): +Get ready for receiving migration for the destination VM (on the host +machine): ```console $ target/release/ch-remote --api-socket=/tmp/api2 receive-migration unix:/tmp/sock @@ -37,14 +39,16 @@ $ target/release/ch-remote --api-socket=/tmp/api2 receive-migration unix:/tmp/so Start to send migration for the source VM (on the host machine): ```console -$ target/release/ch-remote --api-socket=/tmp/api1 send-migration --local unix:/tmp/sock +$ target/release/ch-remote --api-socket=/tmp/api1 send-migration destination_url=unix:/tmp/sock,local=true ``` When the above commands completed, the source VM should be successfully migrated to the destination VM. Now the destination VM is running while the source VM is terminated gracefully. -## Remote Migration +## Remote Migration (TCP Migration) + +_Hint: For developing purposes, same-host TCP migrations are also supported._ In this example, we will migrate a VM from one machine (`src`) to another (`dst`) across the network. To keep it simple, we will use a @@ -130,6 +134,10 @@ src $ ch-remote --api-socket=/tmp/api send-migration unix:/tmp/sock When the above commands completed, the VM should be successfully migrated to the destination machine without interrupting the workload. +Cloud Hypervisor sends out RARP packages after the migration, to +announce the new location of the VM to the network. For `virtio-net` +devices, Cloud Hypervisor asks guests that negotiated +`VIRTIO_NET_F_GUEST_ANNOUNCE` to also re-announce themselves. ### TCP Socket Migration @@ -171,7 +179,13 @@ After a few seconds the VM should be up and you can interact with it. Initiate the Migration over TCP: ```console -src $ ch-remote --api-socket=/tmp/api send-migration tcp:{dst}:{port} +src $ ch-remote --api-socket=/tmp/api send-migration destination_url=tcp:{dst}:{port} +``` + +With migration parameters: + +```console +src $ ch-remote --api-socket=/tmp/api send-migration destination_url=tcp:{dst}:{port},downtime_ms=200,timeout_s=3600,timeout_strategy=cancel ``` > Replace {dst}:{port} with the actual IP address and port of your destination host. @@ -180,3 +194,58 @@ After completing the above commands, the source VM will be migrated to the destination host and continue running there. The source VM instance will terminate normally. All ongoing processes and connections within the VM should remain intact after the migration. +Cloud Hypervisor sends out RARP packages after the migration, to +announce the new location of the VM to the network. For `virtio-net` +devices, Cloud Hypervisor asks guests that negotiated +`VIRTIO_NET_F_GUEST_ANNOUNCE` to also re-announce themselves. + +#### Encryption + +TCP migration can be protected with TLS by passing `tls_dir=` to +both `receive-migration` and `send-migration`. + +The destination host needs a directory containing: + +- `server-cert.pem`: the certificate presented by the destination +- `server-key.pem`: the private key for `server-cert.pem` + +The source host needs a directory containing: + +- `ca-cert.pem`: the CA certificate used to verify the destination + certificate + +Example receiver command: + +```console +dst $ ch-remote --api-socket=/tmp/api receive-migration receiver_url=tcp:0.0.0.0:{port},tls_dir=/path/to/dst-tls +``` + +Example sender command: + +```console +src $ ch-remote --api-socket=/tmp/api send-migration destination_url=tcp:{dst}:{port},tls_dir=/path/to/src-tls +``` + +TLS encryption is only supported with `tcp::` migration +URLs, not with local UNIX-socket migration. + +#### Migration Parameters + +Cloud Hypervisor supports additional parameters to control the +migration process. Via the API or `ch-remote`, you may specify: + +- `downtime_ms `: \ + The maximum downtime the migration aims for, in milliseconds. + Defaults to `300ms`. +- `timeout_s `: \ + The timeout for the migration (maximum total duration), in seconds. + Defaults to `3600s` (one hour). +- `timeout_strategy ` (`[cancel, ignore]`): \ + The strategy to apply when the migration timeout is reached. + Cancel will abort the migration and keep the VM running on the source. + Ignore will proceed with the migration regardless of the downtime requirement. + Defaults to `cancel`. +- `connections `: \ + The number of parallel TCP connections to use for migration. + Must be between `1` and `128`. Defaults to `1`. + Multiple connections are not supported with local UNIX-socket migration. diff --git a/docs/logging.md b/docs/logging.md index ae9a5e8605..1e11140057 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -40,3 +40,61 @@ This level is for the benefit of developers. It should be used for sporadic and Use `-vv` to enable. For the most verbose of logging messages. It is acceptable to "spam" the log with repeated invocations of the same message. This level of logging would be combined with `--log-file`. + +## Format + +The `--log-format ` flag controls how each log record is rendered. +`` is a template string where tokens enclosed in `{...}` are substituted +at log time. Literal `{` and `}` can be escaped as `{{` and `}}`. + +The default format is: + +```text +cloud-hypervisor: {boottime}s: <{thread}> {level}:{location} -- {msg} +``` + +### Common tokens + +| Token | Substituted with | +|---------------|---------------------------------------------------------------| +| `{boottime}` | Seconds since process start (6 decimal places, right-aligned).| +| `{wallclock}` | UTC RFC 3339 (e.g. `2024-01-15T10:30:45.123456Z`). | +| `{glog}` | UTC glog timestamp `MMDD HH:MM:SS.uuuuuu`. | +| `{localglog}` | Local-time glog timestamp, same shape as `{glog}`. | +| `{thread}` | Thread name (`anonymous` if unnamed). | +| `{level}` | Log level word (`ERROR`/`WARN`/`INFO`/`DEBUG`/`TRACE`). | +| `{levelchar}` | Single-letter glog level: `E`/`W`/`I`/`D`/`T`. | +| `{location}` | `file:line`, or the `log` target if unavailable. | +| `{msg}` | Formatted log message. | +| `{pid}` | Process ID. | +| `{tid}` | Kernel thread ID (`gettid(2)`). | + +### Broken-down date/time fields + +Each UTC field has a `local`-prefixed variant that uses the system timezone. +All wallclock-derived tokens within a single record refer to the same instant. + +| UTC | Local | Output | +|-------------|------------------|---------------------------------------| +| `{year}` | `{localyear}` | 4-digit year. | +| `{month}` | `{localmonth}` | 2-digit month. | +| `{day}` | `{localday}` | 2-digit day of month. | +| `{hour}` | `{localhour}` | 2-digit hour (24h). | +| `{minute}` | `{localminute}` | 2-digit minute. | +| `{second}` | `{localsecond}` | 2-digit second. | +| `{micros}` | `{localmicros}` | 6-digit microseconds. | +| `{offset}` | `{localoffset}` | Timezone offset (`+00:00` for UTC). | + +### Examples + +Glog header `I0521 08:02:15.542701`: + +```text +--log-format '{levelchar}{localglog}' +``` + +Or built from individual fields: + +```text +--log-format '{levelchar}{localmonth}{localday} {localhour}:{localminute}:{localsecond}.{localmicros}' +``` diff --git a/docs/macvtap-bridge.md b/docs/macvtap-bridge.md index 5161eb7bb6..66c177d333 100644 --- a/docs/macvtap-bridge.md +++ b/docs/macvtap-bridge.md @@ -1,6 +1,6 @@ # Using MACVTAP to Bridge onto Host Network -Cloud Hypervisor supports using a MACVTAP device which is derived from a MACVLAN. Full details of configuring MACVLAN or MACVTAP is out of scope of this document. However the example below indicates how to bridge the guest directly onto the network the host is on. Due to the lack of hairpin mode it not usually possible to reach the guest directly from the host. +Cloud Hypervisor supports using a MACVTAP device which is derived from a MACVLAN. Full details of configuring MACVLAN or MACVTAP are out of scope of this document. However the example below indicates how to bridge the guest directly onto the network the host is on. Due to the lack of hairpin mode it is not usually possible to reach the guest directly from the host. ```bash # The MAC address must be attached to the macvtap and be used inside the guest @@ -26,7 +26,7 @@ target/debug/cloud-hypervisor \ --disk path=~/workloads/focal.raw \ --cpus boot=1 --memory size=512M \ --cmdline "root=/dev/vda1 console=hvc0" \ - --net fd=3,mac=$mac 3<>$"$tapdevice" + --net fd=3,mac=$mac 3<>"$tapdevice" ``` -As the guest is now connected to the same L2 network as the host you can obtain an IP address based on your host network (potentially including via DHCP) +As the guest is now connected to the same L2 network as the host, you can obtain an IP address based on your host network (potentially including via DHCP) diff --git a/docs/memory.md b/docs/memory.md index 46569449c8..fb42e89374 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -20,7 +20,7 @@ struct MemoryConfig { hugepages: bool, hugepage_size: Option, prefault: bool, - thp: bool + thp: bool, zones: Option>, } ``` @@ -119,7 +119,7 @@ By default this option is turned off, which results in performing `mmap(2)` with `MAP_PRIVATE` flag. If `hugepages=on` then the value of this field is ignored as huge pages always -requires `MAP_SHARED`. +require `MAP_SHARED`. _Example_ @@ -135,8 +135,7 @@ If no huge page size is supplied the system's default huge page size is used. By using hugepages, one can improve the overall performance of the VM, assuming the guest will allocate hugepages as well. Another interesting use case is VFIO -as it speeds up the VM's boot time since the amount of IOMMU mappings are -reduced. +as it speeds up the VM's boot time since the amount of IOMMU mappings is reduced. The user is responsible for ensuring there are sufficient huge pages of the specified size for the VMM to use. Failure to do so may result in strange VMM @@ -185,7 +184,7 @@ backing file) should be labelled `MADV_HUGEPAGE` with `madvise(2)` indicating to the kernel that this memory may be backed with huge pages transparently. The use of transparent huge pages can improve the performance of the guest as -there will fewer virtualisation related page faults. Unlike using +there will be fewer virtualisation related page faults. Unlike using `hugepages=on` a specific number of huge pages do not need to be allocated by the kernel. @@ -215,11 +214,12 @@ struct MemoryZoneConfig { hotplug_size: Option, hotplugged_size: Option, prefault: bool, + mergeable: bool, } ``` ``` ---memory-zone User defined memory zone parameters "size=,file=,shared=on|off,hugepages=on|off,hugepage_size=,host_numa_node=,id=,hotplug_size=,hotplugged_size=,prefault=on|off" +--memory-zone User defined memory zone parameters "size=,file=,shared=on|off,hugepages=on|off,hugepage_size=,host_numa_node=,id=,hotplug_size=,hotplugged_size=,prefault=on|off,mergeable=on|off" ``` This parameter expects one or more occurrences, allowing for a list of memory @@ -295,9 +295,9 @@ vhost-user devices as part of the VM device model, as they will be driven by standalone daemons needing access to the guest RAM content. If `hugepages=on` then the value of this field is ignored as huge pages always -requires `MAP_SHARED`. +require `MAP_SHARED`. -By default this option is turned off, which result in performing `mmap(2)` +By default this option is turned off, which results in performing `mmap(2)` with `MAP_PRIVATE` flag. _Example_ @@ -315,8 +315,7 @@ If no huge page size is supplied the system's default huge page size is used. By using hugepages, one can improve the overall performance of the VM, assuming the guest will allocate hugepages as well. Another interesting use case is VFIO -as it speeds up the VM's boot time since the amount of IOMMU mappings are -reduced. +as it speeds up the VM's boot time since the amount of IOMMU mappings is reduced. The user is responsible for ensuring there are sufficient huge pages of the specified size for the VMM to use. Failure to do so may result in strange VMM @@ -325,7 +324,7 @@ error with `hugepages` enabled, just disable it or check whether there are enoug huge pages. If `hugepages=on` then the value of `shared` is ignored as huge pages always -requires `MAP_SHARED`. +require `MAP_SHARED`. By default this option is turned off. @@ -424,6 +423,34 @@ _Example_ --memory-zone id=mem0,size=1G,prefault=on ``` +### `mergeable` + +Specifies if the pages from this memory zone must be marked as _mergeable_, +enabling Kernel Same-page Merging (KSM) for this zone. + +This is the per-zone equivalent of the top-level `--memory mergeable=on` option. +It allows KSM to be enabled selectively — for example, enabling it only on a +hotplug zone while leaving boot memory unaffected: + +``` +--memory size=2G,mergeable=off +--memory-zone id=hotplug,size=0,hotplug_size=8G,mergeable=on +``` + +For KSM to have any effect, the host kernel must have KSM enabled: +``` +echo 1 > /sys/kernel/mm/ksm/run +``` + +By default this option is turned off. + +_Example_ + +``` +--memory size=0 +--memory-zone id=mem0,size=1G,mergeable=on +``` + ## NUMA settings `NumaConfig` or what is known as `--numa` from the CLI perspective has been @@ -431,18 +458,20 @@ introduced to define a guest NUMA topology. It allows for a fine description about the CPUs and memory ranges associated with each NUMA node. Additionally it allows for specifying the distance between each NUMA node. +Furthermore, it supports ACPI Generic Initiator Affinity (SRAT Type 5), which allows VFIO-PCI devices (such as GPUs) to be associated with NUMA nodes that are {memory,cpu}-less. Detailed configuration for this feature can be found under the device_id parameter. + ```rust struct NumaConfig { guest_numa_id: u32, - cpus: Option>, + cpus: Option>, distances: Option>, memory_zones: Option>, - sgx_epc_sections: Option>, + device_id: Option, } ``` ``` ---numa Settings related to a given NUMA node "guest_numa_id=,cpus=,distances=,memory_zones=,sgx_epc_sections=" +--numa Settings related to a given NUMA node "guest_numa_id=,cpus=,distances=,memory_zones=,device_id=" ``` ### `guest_numa_id` @@ -457,7 +486,7 @@ Value is an unsigned integer of 32 bits. _Example_ ``` ---numa guest_numa_id=0 +--numa guest_numa_id=0,cpus=[0-1],memory_zones=mem0 ``` ### `cpus` @@ -471,7 +500,7 @@ regarding the CPUs associated with it, which might help the guest run more efficiently. Multiple values can be provided to define the list. Each value is an unsigned -integer of 8 bits. +integer of 32 bits. For instance, if one needs to attach all CPUs from 0 to 4 to a specific node, the syntax using `-` will help define a contiguous range with `cpus=0-4`. The @@ -484,6 +513,9 @@ simply be described with `cpus=[0-99,255]`. As soon as one tries to describe a list of values, `[` and `]` must be used to demarcate the list. +**Note:** When creating a Generic Initiator node via the `device_id` parameter, +the `cpus` option must not be specified. + _Example_ ``` @@ -494,7 +526,7 @@ _Example_ ### `distances` List of distances between the current NUMA node referred by `guest_numa_id` -and the destination NUMA nodes listed along with distances. This option let +and the destination NUMA nodes listed along with distances. This option lets the user choose the distances between guest NUMA nodes. This is important to provide an accurate description of the way non uniform memory accesses will perform in the guest. @@ -510,13 +542,34 @@ from the others with `,` separator. As soon as one tries to describe a list of values, `[` and `]` must be used to demarcate the list. +**Default distances:** +- If distances are not specified for a NUMA node, default values are applied: + - Distance to self: 10 + - Distance to all other nodes: 20 +- Partial distance specifications are allowed; unspecified distances use the defaults above + +**Distance symmetry:** +- Cloud Hypervisor automatically ensures distance symmetry in ACPI SLIT (System Locality Information Table) and FDT +- If node A specifies distance to node B, the reverse distance (B to A) is automatically set to the same value + For instance, if one wants to define 3 NUMA nodes, with each node located at different distances, it can be described with the following example. _Example_ ``` +# Explicit bidirectional distances --numa guest_numa_id=0,distances=[1@15,2@25] guest_numa_id=1,distances=[0@15,2@20] guest_numa_id=2,distances=[0@25,1@20] + +# Simplified with symmetry - only specify in one direction +--numa guest_numa_id=0,distances=[1@15,2@25] guest_numa_id=1,distances=[2@20] +# Results in the same topology: 0↔1=15, 0↔2=25, 1↔2=20 + +# Using defaults - only specify non-default distances +--numa guest_numa_id=0,cpus=[0-1],memory_zones=mem0,distances=[1@15] +--numa guest_numa_id=1,cpus=[2-3],memory_zones=mem1 +# Node 0: self=10, to node 1=15 +# Node 1: self=10, to node 0=15 (symmetric) ``` ### `memory_zones` @@ -542,6 +595,9 @@ Note that a memory zone must belong to a single NUMA node. The following configuration is incorrect, therefore not allowed: `--numa guest_numa_id=0,memory_zones=mem0 guest_numa_id=1,memory_zones=mem0` +**Note:** When creating a Generic Initiator node via the `device_id` parameter, +the `memory_zones` option must not be specified. + _Example_ ``` @@ -550,30 +606,48 @@ _Example_ --numa guest_numa_id=0,memory_zones=[mem0,mem2] guest_numa_id=1,memory_zones=mem1 ``` -### `sgx_epc_sections` +### `device_id` (Generic Initiator) -List of SGX EPC sections attached to the guest NUMA node identified by the -`guest_numa_id` option. This allows for describing a list of SGX EPC sections -which must be seen by the guest as belonging to the NUMA node `guest_numa_id`. +Device identifier for creating a Generic Initiator NUMA node that is +{CPU,memory}-less and associated with a specific VFIO-PCI device. -Multiple values can be provided to define the list. Each value is a string -referring to an existing SGX EPC section identifier. Values are separated from -each other with the `,` separator. +Generic Initiator nodes are defined by ACPI SRAT (System Resource Affinity +Table) Type 5 entries and allow the guest OS to understand device-to-memory +proximity relationships. Without Generic Initiator support, the guest OS has +no way to know which NUMA node a passthrough device is closest to. -As soon as one tries to describe a list of values, `[` and `]` must be used to -demarcate the list. +By exposing these proximity relationships, the guest OS can perform +NUMA-aware scheduling and optimize memory placement for workloads +utilizing those specific devices. + +When `device_id` is specified, `cpus` and `memory_zones` must NOT be provided. + +Value is a string referring to an existing device identifier defined via +`--device id=`. _Example_ +```bash +# Create two standard NUMA nodes with CPUs and memory, plus one Generic +# Initiator node for a VFIO GPU +--cpus boot=4 +--memory size=0 +--memory-zone id=mem0,size=2G id=mem1,size=2G +--numa guest_numa_id=0,cpus=[0-1],memory_zones=mem0,distances=[1@20,2@25] +--numa guest_numa_id=1,cpus=[2-3],memory_zones=mem1,distances=[0@20,2@30] +--numa guest_numa_id=2,device_id=gpu0,distances=[0@25,1@30] +--device id=gpu0,path=/sys/bus/pci/devices/0000:01:00.0,iommu=on ``` ---sgx-epc id=epc0,size=32M id=epc1,size=64M id=epc2,size=32M ---numa guest_numa_id=0,sgx_epc_sections=epc1 guest_numa_id=1,sgx_epc_sections=[epc0,epc2] -``` + +In this configuration: +- Node 0: CPUs 0-1, 2GB memory +- Node 1: CPUs 2-3, 2GB memory +- Node 2 (auto-assigned): GPU device, closer to node 0 (distance=25) than node 1 (distance=30) ### PCI bus Cloud Hypervisor supports guests with one or more PCI segments. The default PCI segment always -has affinity to NUMA node 0. Be default, all other PCI segments have affinity to NUMA node 0. +has affinity to NUMA node 0. By default, all other PCI segments have affinity to NUMA node 0. The user may configure the NUMA affinity for any additional PCI segments. _Example_ diff --git a/docs/snapshot_restore.md b/docs/snapshot_restore.md index 67f29ce6dc..567f77a9a6 100644 --- a/docs/snapshot_restore.md +++ b/docs/snapshot_restore.md @@ -90,9 +90,40 @@ start using it. ./ch-remote --api-socket=/tmp/cloud-hypervisor.sock resume ``` +Alternatively, the `resume` option can be used to automatically resume the VM +after restore completes: + +```bash +./cloud-hypervisor \ + --api-socket /tmp/cloud-hypervisor.sock \ + --restore source_url=file:///home/foo/snapshot,resume=true +``` + At this point, the VM is fully restored and is identical to the VM which was snapshot earlier. +Restore also supports selecting how guest memory is populated: + +```bash +./cloud-hypervisor \ + --api-socket /tmp/cloud-hypervisor.sock \ + --restore source_url=file:///home/foo/snapshot,memory_restore_mode=ondemand +``` + +If `memory_restore_mode` is omitted, Cloud Hypervisor uses the eager-copy +restore path (`copy`). + +With `memory_restore_mode=ondemand`, restore uses `userfaultfd` to fault snapshot +pages in on first access instead of copying the full `memory-ranges` file into +guest RAM before restore completes. This mode is strict: if Cloud Hypervisor +cannot enable the `userfaultfd` restore path, restore fails instead of falling +back to `copy`. + +Current constraints for `memory_restore_mode=ondemand`: + +- `prefault=on` is not supported +- the snapshot memory ranges must be page-aligned + ## Restore a VM with new Net FDs For a VM created with FDs explicitly passed to NetConfig, a set of valid FDs need to be provided along with the VM restore command in the following syntax: @@ -110,4 +141,4 @@ from the restored VM. ## Limitations -VFIO devices and Intel SGX are out of scope. +VFIO devices is out of scope. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000000..0029cf01df --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,382 @@ +# Testing + +- [Testing](#testing) + - [Overview](#overview) + - [Prerequisites](#prerequisites) + - [The dev\_cli.sh entry point](#the-dev_clish-entry-point) + - [Global flags](#global-flags) + - [Building](#building) + - [Running tests](#running-tests) + - [Argument passthrough](#argument-passthrough) + - [Custom kernel and firmware](#custom-kernel-and-firmware) + - [Unit tests](#unit-tests) + - [Integration tests](#integration-tests) + - [x86\_64](#x86_64) + - [ARM64](#arm64) + - [VFIO](#vfio) + - [Windows guests](#windows-guests) + - [Rate limiter](#rate-limiter) + - [Confidential VMs](#confidential-vms) + - [Performance metrics](#performance-metrics) + - [Code coverage](#code-coverage) + - [CI workflows](#ci-workflows) + +## Overview + +All Cloud Hypervisor builds and tests run inside a Docker container to +provide a reproducible environment. The main entry point is +`scripts/dev_cli.sh`, which manages the container lifecycle and +forwards arguments to the appropriate test scripts. + +The container image is published at +`ghcr.io/cloud-hypervisor/cloud-hypervisor` and is automatically +pulled on first use. A local build of the container can be triggered +with `scripts/dev_cli.sh build-container` or by passing the `--local` +flag. + +Test workloads (guest images, kernels, firmware) are stored on the host +under `$HOME/workloads` and bind-mounted into the container at +`/root/workloads`. Most test scripts download missing workloads +automatically on first run. + +## Prerequisites + +A working Docker (or Podman) installation and access to `/dev/kvm` +(or `/dev/mshv` for Microsoft Hypervisor tests) are required. The +host must be running Linux on x86_64 or aarch64. + +```shell +# Verify KVM is available +ls -l /dev/kvm +``` + +The container image bundles all build dependencies. No Rust toolchain +is needed on the host. + +## The dev_cli.sh entry point + +``` +scripts/dev_cli.sh [flags] [] +``` + +### Global flags + +| Flag | Description | +|-----------|--------------------------------------------------| +| `--local` | Build and use a local container image instead of pulling from the registry. | + +### Building + +```shell +scripts/dev_cli.sh build [--debug|--release] [--libc musl|gnu] \ + [--hypervisor kvm|mshv] [--features ] \ + [--volumes /host:/ctr#...] [-- ] +``` + +| Flag | Default | Description | +|----------------|---------|------------------------------------------| +| `--debug` | yes | Build debug binaries. | +| `--release` | | Build release binaries. | +| `--libc` | `gnu` | C library to link against (`musl`/`gnu`).| +| `--hypervisor` | `kvm` | Hypervisor backend (`kvm`/`mshv`). | +| `--features` | | Additional cargo features. | +| `--volumes` | | Extra host volumes (`/a:/a#/b:/b`). | +| `--runtime` | `docker`| Container runtime (`docker`/`podman`). | + +Arguments after `--` are forwarded directly to `cargo build`. + +### Running tests + +```shell +scripts/dev_cli.sh tests [] [--libc musl|gnu] \ + [--hypervisor kvm|mshv] [--volumes /host:/ctr#...] \ + [--