pax_global_header00006660000000000000000000000064152247675010014523gustar00rootroot0000000000000052 comment=9ed824d6d08df3e96f7d5f50795d9449ac99f431 cloud-hypervisor-53.0/000077500000000000000000000000001522476750100147705ustar00rootroot00000000000000cloud-hypervisor-53.0/.config/000077500000000000000000000000001522476750100163135ustar00rootroot00000000000000cloud-hypervisor-53.0/.config/nextest.toml000066400000000000000000000021651522476750100207060ustar00rootroot00000000000000[profile.default] # Don't let one individual test run for more than 10 minutes slow-timeout = { period = "60s", terminate-after = 10 } [profile.integration] fail-fast = false retries = 3 [profile.common_tests] inherits = "integration" default-filter = 'test(common_parallel::) | test(common_sequential::) | test(aarch64_acpi::)' junit.path = "/root/workloads/junit/common.xml" [[profile.common_tests.overrides]] filter = 'test(common_sequential::)' # use up all the available test threads for each of the sequential tests # i.e. no other test can be running while a sequential test is running. threads-required = 'num-test-threads' [profile.dbus] inherits = "integration" default-filter = 'test(dbus_api::)' junit.path = "/root/workloads/junit/dbus.xml" [profile.fw_cfg] inherits = "integration" default-filter = 'test(fw_cfg::)' junit.path = "/root/workloads/junit/fw_cfg.xml" [profile.ivshmem] inherits = "integration" default-filter = 'test(ivshmem::)' junit.path = "/root/workloads/junit/ivshmem.xml" [profile.common_cvm] inherits = "integration" default-filter = 'test(common_cvm::)' junit.path = "/root/workloads/junit/cvm.xml" cloud-hypervisor-53.0/.editorconfig000066400000000000000000000007531522476750100174520ustar00rootroot00000000000000# 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 cloud-hypervisor-53.0/.github/000077500000000000000000000000001522476750100163305ustar00rootroot00000000000000cloud-hypervisor-53.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001522476750100205135ustar00rootroot00000000000000cloud-hypervisor-53.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011021522476750100231770ustar00rootroot00000000000000--- name: Bug report about: File a bug report title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behaviour: **Version** Output of `cloud-hypervisor --version`: Did you build from source, if so build command line (e.g. features): **VM configuration** What command line did you run (or JSON config data): Guest OS version details: Host OS version details: **Logs** Output of `cloud-hypervisor -v` from either standard error or via `--log-file`: Linux kernel output: cloud-hypervisor-53.0/.github/dependabot.yml000066400000000000000000000042441522476750100211640ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: cargo directories: - "/" - "/fuzz" schedule: interval: weekly allow: - dependency-name: "acpi_tables" - dependency-name: "iommufd-ioctls" - dependency-name: "kvm-bindings" - dependency-name: "kvm-ioctls" - dependency-name: "linux-loader" - dependency-name: "micro_http" - dependency-name: "mshv-bindings" - dependency-name: "mshv-ioctls" - dependency-name: "seccompiler" - dependency-name: "vfio-bindings" - dependency-name: "vfio-ioctls" - dependency-name: "vfio_user" - dependency-name: "vhost" - dependency-name: "vhost-user-backend" - dependency-name: "virtio-bindings" - dependency-name: "virtio-queue" - dependency-name: "vm-fdt" - dependency-name: "vm-memory" - dependency-name: "vmm-sys-util" groups: rust-vmm: patterns: - "*" - package-ecosystem: cargo directories: - "/" - "/fuzz" schedule: interval: weekly allow: - dependency-type: all cooldown: default-days: 7 semver-major-days: 14 semver-minor-days: 7 semver-patch-days: 3 ignore: - dependency-name: "acpi_tables" - dependency-name: "iommufd-ioctls" - dependency-name: "kvm-bindings" - dependency-name: "kvm-ioctls" - dependency-name: "linux-loader" - dependency-name: "micro_http" - dependency-name: "mshv-bindings" - dependency-name: "mshv-ioctls" - dependency-name: "seccompiler" - dependency-name: "vfio-bindings" - dependency-name: "vfio-ioctls" - dependency-name: "vfio_user" - dependency-name: "vhost" - dependency-name: "vhost-user-backend" - dependency-name: "virtio-bindings" - dependency-name: "virtio-queue" - dependency-name: "vm-fdt" - dependency-name: "vm-memory" - dependency-name: "vmm-sys-util" groups: non-rust-vmm: patterns: - "*" # Makes it possible to have another config for the same directory. # https://github.com/dependabot/dependabot-core/issues/1778#issuecomment-1988140219 target-branch: main - package-ecosystem: github-actions directory: "/" schedule: interval: daily open-pull-requests-limit: 1cloud-hypervisor-53.0/.github/workflows/000077500000000000000000000000001522476750100203655ustar00rootroot00000000000000cloud-hypervisor-53.0/.github/workflows/ci.yaml000066400000000000000000001072471522476750100216570ustar00rootroot00000000000000name: CI on: [pull_request, merge_group] permissions: contents: read pull-requests: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} cancel-in-progress: true jobs: preflight: name: preflight runs-on: ubuntu-latest outputs: full: ${{ steps.classify.outputs.full }} rust: ${{ steps.changes.outputs.rust }} cargo: ${{ steps.changes.outputs.cargo }} openapi: ${{ steps.changes.outputs.openapi }} dockerfile: ${{ steps.changes.outputs.dockerfile }} shell: ${{ steps.changes.outputs.shell }} ci: ${{ steps.changes.outputs.ci }} docs: ${{ steps.changes.outputs.docs }} steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - id: changes uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 with: filters: | rust: - '**/*.rs' - 'build.rs' - '**/Cargo.toml' - '**/Cargo.lock' - 'rust-toolchain.toml' cargo: - '**/Cargo.toml' - '**/Cargo.lock' openapi: - 'vmm/src/api/openapi/**' dockerfile: - 'resources/Dockerfile' shell: - '**/*.sh' - 'scripts/**' ci: - '.github/workflows/**' docs: - 'docs/**' - '**/*.md' - '.github/ISSUE_TEMPLATE/**' - 'LICENSES/**' - 'CODEOWNERS' - id: classify name: Classify changes run: | set -eufo pipefail full=false if [[ "${{ steps.changes.outputs.rust }}" == "true" \ || "${{ steps.changes.outputs.dockerfile }}" == "true" \ || "${{ steps.changes.outputs.shell }}" == "true" \ || "${{ steps.changes.outputs.ci }}" == "true" ]]; then full=true fi echo "full=$full" >> "$GITHUB_OUTPUT" echo "full=$full" dco: name: dco needs: [preflight] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Set up Python 3.x uses: actions/setup-python@v6 with: python-version: '3.x' - name: Check DCO if: github.event_name == 'pull_request' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eufo pipefail pip3 install -U dco-check dco-check -e "49699333+dependabot[bot]@users.noreply.github.com" gitlint: name: gitlint needs: [preflight] # PR-only: gitlint needs GITHUB_BASE_REF, unset on merge_group. if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 with: # PR head, not the merge ref, so gitlint sees the PR's commits. ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Set up Python 3.10 uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install --upgrade gitlint - name: Lint git commit messages run: | gitlint --commits "origin/$GITHUB_BASE_REF.." lychee: name: lychee needs: [preflight] if: needs.preflight.outputs.docs == 'true' || needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Get changed files in PR id: changed-files uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 with: base_sha: ${{ github.event.pull_request.base.sha }} - name: Verify Changed Files run: | set -eufo pipefail echo "--- tj-actions/changed-files Outputs ---" echo "any_changed: ${{ steps.changed-files.outputs.any_changed }}" echo "all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}" echo "added_files: ${{ steps.changed-files.outputs.added_files }}" echo "modified_files: ${{ steps.changed-files.outputs.modified_files }}" echo "deleted_files: ${{ steps.changed-files.outputs.deleted_files }}" echo "renamed_files: ${{ steps.changed-files.outputs.renamed_files }}" echo "----------------------------------------" if [ -n "${{ steps.changed-files.outputs.all_changed_files }}" ]; then echo "Detected changes: all_changed_files output is NOT empty." else echo "No changes detected: all_changed_files output IS empty." fi - name: Link Availability Check (Diff Only) if: ${{ steps.changed-files.outputs.all_changed_files != '' }} uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 with: args: --verbose --config .lychee.toml ${{ steps.changed-files.outputs.all_changed_files }} failIfEmpty: false fail: true taplo: name: taplo needs: [preflight] if: needs.preflight.outputs.cargo == 'true' runs-on: ubuntu-latest steps: - name: Code checkout uses: actions/checkout@v7 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install build dependencies run: sudo apt-get update && sudo apt-get -yqq install build-essential libssl-dev - name: Install taplo run: cargo install taplo-cli --locked - name: Check formatting run: taplo fmt --check audit: name: audit needs: [preflight] if: needs.preflight.outputs.cargo == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions-rust-lang/audit@v1 with: token: ${{ secrets.GITHUB_TOKEN }} shlint: name: shlint needs: [preflight] if: needs.preflight.outputs.shell == 'true' || needs.preflight.outputs.ci == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 - name: Run the shell script checkers uses: luizm/action-sh-checker@883217215b11c1fabbf00eb1a9a041f62d74c744 # v0.10.0 env: SHFMT_OPTS: -i 4 -d SHELLCHECK_OPTS: -x --source-path scripts hadolint: name: hadolint needs: [preflight] if: needs.preflight.outputs.dockerfile == 'true' runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v7 - name: Lint Dockerfile uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 with: dockerfile: ./resources/Dockerfile format: tty no-fail: false verbose: true failure-threshold: info reuse: name: reuse needs: [preflight] if: needs.preflight.outputs.full == 'true' || needs.preflight.outputs.cargo == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: REUSE Compliance Check uses: fsfe/reuse-action@v6 formatting: name: formatting needs: [preflight] if: needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest strategy: matrix: rust: [nightly] target: - x86_64-unknown-linux-gnu - aarch64-unknown-linux-musl env: RUSTFLAGS: -D warnings steps: - name: Code checkout uses: actions/checkout@v7 - name: Install Rust toolchain (${{ matrix.rust }}) uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} components: rustfmt - name: Formatting (rustfmt) run: cargo fmt --all -- --check - name: Formatting (fuzz) (rustfmt) run: cargo fmt --all --manifest-path fuzz/Cargo.toml -- --check package-consistency: name: package-consistency needs: [preflight] if: needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install dependencies run: sudo apt install -y python3 - name: Install Rust toolchain stable uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Check Rust VMM Package Consistency of root Workspace run: python3 scripts/package-consistency-check.py github.com/rust-vmm - name: Check Rust VMM Package Consistency of fuzz Workspace run: | set -eufo pipefail pushd fuzz python3 ../scripts/package-consistency-check.py github.com/rust-vmm popd fuzz-build: name: fuzz-build needs: [preflight] if: needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest strategy: matrix: rust: [nightly] target: [x86_64-unknown-linux-gnu] env: RUSTFLAGS: -D warnings steps: - name: Code checkout uses: actions/checkout@v7 - name: Install Rust toolchain (${{ matrix.rust }}) uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - name: Install Cargo fuzz run: cargo install cargo-fuzz - name: Fuzz Build run: cargo fuzz build - name: Fuzz Check run: cargo fuzz check openapi: name: openapi needs: [preflight] if: needs.preflight.outputs.openapi == 'true' runs-on: ubuntu-latest container: openapitools/openapi-generator-cli steps: - uses: actions/checkout@v7 - name: Validate OpenAPI run: | /usr/local/bin/docker-entrypoint.sh validate -i vmm/src/api/openapi/cloud-hypervisor.yaml typos: name: typos needs: [preflight] if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 quality: name: quality needs: [preflight] if: needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest # Beta clippy is non-blocking; continue-on-error below keeps the # aggregated needs.quality.result green when only beta fails. continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: rust: - beta - stable target: - aarch64-unknown-linux-gnu - aarch64-unknown-linux-musl - x86_64-unknown-linux-gnu - x86_64-unknown-linux-musl include: - rust: beta experimental: true - rust: stable experimental: false steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install Rust toolchain (${{ matrix.rust }}) uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} override: true components: clippy - name: Bisectability Check (default features) if: ${{ github.event_name == 'pull_request' && matrix.target == 'x86_64-unknown-linux-gnu' }} run: | set -eufo pipefail commits=$(git rev-list origin/${{ github.base_ref }}..${{ github.sha }}) for commit in $commits; do git checkout $commit; cargo check --tests --examples --all --target=${{ matrix.target }}; done git checkout ${{ github.sha }} - name: Clippy (kvm) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm" -- -D warnings - name: Clippy (mshv) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv" -- -D warnings - name: Clippy (mshv + kvm) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,kvm" -- -D warnings - name: Clippy (default features) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --tests --examples -- -D warnings - name: Clippy (default features + guest_debug) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --tests --examples --features "guest_debug" -- -D warnings - name: Clippy (default features + pvmemcontrol) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --tests --examples --features "pvmemcontrol" -- -D warnings - name: Clippy (default features + tracing) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --tests --examples --features "tracing" -- -D warnings - name: Clippy (default features + fw_cfg) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --target=${{ matrix.target }} --locked --all --all-targets --tests --examples --features "fw_cfg" -- -D warnings - name: Clippy (default features + ivshmem) uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --tests --examples --features "ivshmem" -- -D warnings - name: Clippy (kvm + sev_snp) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,sev_snp" -- -D warnings - name: Clippy (mshv + sev_snp) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,sev_snp" -- -D warnings - name: Clippy (mshv + igvm + sev_snp) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,igvm,sev_snp" -- -D warnings - name: Clippy (kvm + igvm) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,igvm" -- -D warnings - name: Clippy (mshv + igvm) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "mshv,igvm" -- -D warnings - name: Clippy (kvm + igvm + sev_snp + fw_cfg) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy cross-version: 3e0957637b49b1bbced23ad909170650c5b70635 toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --no-default-features --tests --examples --features "kvm,igvm,sev_snp,fw_cfg" -- -D warnings - name: Clippy (default features + sev_snp + igvm + fw_cfg) if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} uses: houseabsolute/actions-rust-cross@v1 with: command: clippy cross-version: 3e0957637b49b1bbced23ad909170650c5b70635 toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} args: --locked --all --all-targets --tests --examples --features "sev_snp,igvm,fw_cfg" -- -D warnings - name: Check build did not modify any files run: test -z "$(git status --porcelain)" build: name: build needs: [preflight] if: needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: rust: - stable - beta - nightly - "1.89.0" # MSRV — keep quoted. target: - x86_64-unknown-linux-gnu - x86_64-unknown-linux-musl steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install musl-gcc run: sudo apt install -y musl-tools - name: Install Rust toolchain (${{ matrix.rust }}) uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - name: Build (default features) run: cargo build --locked --bin cloud-hypervisor - name: Build (kvm) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm" - name: Build (default features + dbus_api) run: cargo build --locked --bin cloud-hypervisor --features "dbus_api" - name: Build (default features + guest_debug) run: cargo build --locked --bin cloud-hypervisor --features "guest_debug" - name: Build (default features + pvmemcontrol) run: cargo build --locked --bin cloud-hypervisor --features "pvmemcontrol" - name: Build (default features + fw_cfg) run: cargo build --locked --bin cloud-hypervisor --features "fw_cfg" - name: Build (default features + ivshmem) run: cargo build --locked --bin cloud-hypervisor --features "ivshmem" - name: Build (mshv) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv" - name: Build (mshv + igvm) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,igvm" - name: Build (mshv + sev_snp) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,sev_snp" - name: Build (mshv + igvm + sev_snp) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,igvm,sev_snp" - name: Build (kvm + sev_snp) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,sev_snp" - name: Build (kvm + igvm + sev_snp + fw_cfg) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,igvm,sev_snp,fw_cfg" - name: Build (kvm + igvm) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "kvm,igvm" - name: Build (mshv + kvm) run: cargo build --locked --bin cloud-hypervisor --no-default-features --features "mshv,kvm" - name: Release Build (default features) run: cargo build --locked --all --release --target=${{ matrix.target }} - name: Check build did not modify any files run: test -z "$(git status --porcelain)" build-riscv64: name: build-riscv64 needs: [preflight] if: needs.preflight.outputs.full == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: rust: - stable - "1.89.0" # MSRV — keep quoted. env: CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install riscv64 cross linker run: sudo apt-get update && sudo apt-get install -y gcc-riscv64-linux-gnu - name: Install Rust toolchain (${{ matrix.rust }}) uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: riscv64gc-unknown-linux-gnu - name: Build (kvm) run: cargo build --locked --package cloud-hypervisor --no-default-features --features "kvm" --target riscv64gc-unknown-linux-gnu - name: Check build did not modify any files run: test -z "$(git status --porcelain)" # garm-jammy + gnu: runs on PR and MQ. Other 3 matrix entries are in # integration-x86-64-mq (sibling, MQ-only, runs in parallel). integration-x86-64-pr: name: integration-x86-64-pr needs: [preflight, dco, quality, build] if: >- needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' timeout-minutes: 80 env: # Our runner has 16 cores (nproc). # We limit parallelism only to avoid exhausting disk space and memory # resources, not to save CPU resources. PARALLEL_INTEGRATION_TESTS_NUM: 12 runs-on: garm-jammy-16 steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install Docker run: | set -eufo pipefail sudo apt-get update sudo apt-get -y install ca-certificates curl gnupg curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt install -y docker-ce docker-ce-cli - name: Prepare for VDPA run: scripts/prepare_vdpa.sh - name: Run unit tests run: scripts/dev_cli.sh tests --unit --libc gnu - name: Load openvswitch module run: sudo modprobe openvswitch - name: Run integration tests timeout-minutes: 60 run: scripts/dev_cli.sh tests --integration --libc gnu # MQ-only: the 3 matrix entries that integration-x86-64-pr does not cover. integration-x86-64-mq: name: integration-x86-64-mq needs: [preflight, dco, quality, build] if: >- github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' timeout-minutes: 80 env: # Our runner has 16 cores (nproc). # We limit parallelism only to avoid exhausting disk space and memory # resources, not to save CPU resources. PARALLEL_INTEGRATION_TESTS_NUM: 12 strategy: fail-fast: false matrix: include: - {runner: garm-jammy, libc: musl} - {runner: garm-jammy-amd, libc: gnu} - {runner: garm-jammy-amd, libc: musl} # format() because `${{ matrix.runner }}-16` is not valid in runs-on. runs-on: ${{ format('{0}-16', matrix.runner) }} steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install Docker run: | set -eufo pipefail sudo apt-get update sudo apt-get -y install ca-certificates curl gnupg curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt install -y docker-ce docker-ce-cli - name: Prepare for VDPA run: scripts/prepare_vdpa.sh - name: Run unit tests run: scripts/dev_cli.sh tests --unit --libc ${{ matrix.libc }} - name: Load openvswitch module run: sudo modprobe openvswitch - name: Run integration tests timeout-minutes: 60 run: scripts/dev_cli.sh tests --integration --libc ${{ matrix.libc }} integration-arm64: name: integration-arm64 needs: [preflight, dco, quality, build] if: >- github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' timeout-minutes: 120 env: # Our runner has 80 cores (nproc). # We limit parallelism only to avoid exhausting disk space and memory # resources, not to save CPU resources. PARALLEL_INTEGRATION_TESTS_NUM: 25 runs-on: bookworm-arm64 steps: # arm64 runner user is "runner" (vfio's is "github-runner"). - name: Fix workspace permissions run: sudo chown -R runner:runner ${GITHUB_WORKSPACE} - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Run unit tests (musl) run: scripts/dev_cli.sh tests --unit --libc musl - name: Load openvswitch module run: sudo modprobe openvswitch - name: Run integration tests (musl) timeout-minutes: 60 run: scripts/dev_cli.sh tests --integration --libc musl - name: Install Azure CLI run: | set -eufo pipefail sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null echo "deb [arch=arm64] https://packages.microsoft.com/repos/azure-cli/ bookworm main" | sudo tee /etc/apt/sources.list.d/azure-cli.list sudo apt update sudo apt install -y azure-cli - name: Download Windows image shell: bash run: | set -eufo pipefail IMG_BASENAME=windows-11-iot-enterprise-aarch64.raw IMG_PATH=$HOME/workloads/$IMG_BASENAME IMG_GZ_PATH=$HOME/workloads/$IMG_BASENAME.gz IMG_GZ_BLOB_NAME=windows-11-iot-enterprise-aarch64-25h2-6.raw.gz cp "scripts/$IMG_BASENAME.sha1" "$HOME/workloads/" pushd "$HOME/workloads" if sha1sum "$IMG_BASENAME.sha1" --check; then exit fi popd mkdir -p "$HOME/workloads" rm -f "$IMG_PATH" "$IMG_GZ_PATH" az storage blob download --container-name private-images --file "$IMG_GZ_PATH" --name "$IMG_GZ_BLOB_NAME" --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}" gzip -d "$IMG_GZ_PATH" - name: Run Windows guest integration tests timeout-minutes: 30 run: scripts/dev_cli.sh tests --integration-windows --libc musl integration-vfio: name: integration-vfio needs: [preflight, dco, quality, build] if: >- github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' runs-on: vfio-nvidia env: AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }} steps: # vfio-nvidia runner user is "github-runner" (not "runner" like arm64). - name: Fix workspace permissions run: sudo chown -R github-runner:github-runner "${GITHUB_WORKSPACE}" - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Run VFIO integration tests timeout-minutes: 25 run: scripts/dev_cli.sh tests --integration-vfio # Most tests are failing with musl, see #6790 # - name: Run VFIO integration tests for musl # timeout-minutes: 25 # run: scripts/dev_cli.sh tests --integration-vfio --libc musl integration-windows: name: integration-windows needs: [preflight, dco, quality, build] if: >- github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' runs-on: garm-jammy-16 steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install Docker run: | set -eufo pipefail sudo apt-get update sudo apt-get -y install ca-certificates curl gnupg curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt install -y docker-ce docker-ce-cli - name: Install Azure CLI run: | set -eufo pipefail sudo apt install -y ca-certificates curl apt-transport-https lsb-release gnupg curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list sudo apt update sudo apt install -y azure-cli - name: Download Windows image run: | set -eufo pipefail mkdir $HOME/workloads az storage blob download --container-name private-images --file "$HOME/workloads/windows-server-2025-amd64-1.raw" --name windows-server-2025-amd64-1.raw --connection-string "${{ secrets.CH_PRIVATE_IMAGES }}" - name: Run Windows guest integration tests timeout-minutes: 15 run: scripts/dev_cli.sh tests --integration-windows - name: Run Windows guest integration tests for musl timeout-minutes: 15 run: scripts/dev_cli.sh tests --integration-windows --libc musl integration-mshv-x86-64: name: integration-mshv-x86-64 needs: [preflight, dco, quality, build] if: >- github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' timeout-minutes: 35 runs-on: mshv steps: # mshv runner user is "lsgunner" - name: Fix workspace and Docker socket permissions run: | sudo chown -R lsgrunner:lsgrunner ${GITHUB_WORKSPACE} sudo chmod 666 /var/run/docker.sock - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Prepare for VDPA run: scripts/prepare_vdpa.sh - name: Run integration tests timeout-minutes: 30 run: scripts/dev_cli.sh tests --integration # Rate-limiter host is not available # integration-rate-limiter: # name: integration-rate-limiter # needs: [preflight, dco, quality, build] # if: >- # github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' # runs-on: bare-metal-9950x # env: # AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }} # steps: # - name: Code checkout # uses: actions/checkout@v7 # with: # fetch-depth: 0 # - name: Run rate-limiter integration tests # timeout-minutes: 20 # run: scripts/dev_cli.sh tests --integration-rate-limiter integration-sev-snp: name: integration-sev-snp needs: [preflight, dco, quality, build] if: >- github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' timeout-minutes: 30 runs-on: noble-sevsnp steps: # Self-hosted runners reuse their workdir; a previous privileged # container run can leave root-owned files behind. - name: Fix workspace permissions run: sudo chown -R "$(id -un):$(id -gn)" "${GITHUB_WORKSPACE}" - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Sanity-check SEV-SNP prerequisites run: | set -eufo pipefail echo "Checking hypervisor device nodes..." test -e /dev/kvm || { echo "::error::/dev/kvm missing"; exit 1; } test -e /dev/sev || { echo "::error::/dev/sev missing"; exit 1; } echo "Checking staged IGVM/kernel artifacts..." test -d /usr/share/cloud-hypervisor/cvm \ || { echo "::error::/usr/share/cloud-hypervisor/cvm missing"; exit 1; } ls -l /usr/share/cloud-hypervisor/cvm - name: Run CVM (SEV-SNP) integration tests timeout-minutes: 20 run: scripts/dev_cli.sh tests --integration-cvm --hypervisor kvm # Rate-limiter host is not available # integration-rate-limiter: # name: integration-rate-limiter # needs: [preflight, dco, quality, build] # if: >- # github.event_name == 'merge_group' && needs.preflight.outputs.full == 'true' && needs.dco.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' # runs-on: bare-metal-9950x # env: # AUTH_DOWNLOAD_TOKEN: ${{ secrets.AUTH_DOWNLOAD_TOKEN }} # steps: # - name: Code checkout # uses: actions/checkout@v7 # with: # fetch-depth: 0 # - name: Run rate-limiter integration tests # timeout-minutes: 20 # run: scripts/dev_cli.sh tests --integration-rate-limiter # The single required-status check. Branch protection requires this one job. all-green: name: all-green needs: - audit - build - build-riscv64 - dco - formatting - fuzz-build - gitlint - hadolint - integration-arm64 - integration-sev-snp - integration-vfio - integration-mshv-x86-64 - integration-windows - integration-x86-64-mq - integration-x86-64-pr - openapi - package-consistency - preflight - quality - reuse - shlint - taplo - typos if: always() runs-on: ubuntu-latest steps: - name: Verify all dependencies succeeded or were skipped env: NEEDS_JSON: ${{ toJson(needs) }} run: | set -eufo pipefail echo "$NEEDS_JSON" | jq . # success or skipped = pass; failure or cancelled = red. echo "$NEEDS_JSON" | jq -e ' to_entries | map(select(.value.result != "success" and .value.result != "skipped")) | length == 0 ' >/dev/null cloud-hypervisor-53.0/.github/workflows/docker-image.yaml000066400000000000000000000067551522476750100236150ustar00rootroot00000000000000name: Cloud Hypervisor's Docker image update on: push: branches: main paths: resources/Dockerfile pull_request: paths: resources/Dockerfile concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} cancel-in-progress: true env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm64 runs-on: ubuntu-latest steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Code checkout uses: actions/checkout@v7 - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Login to ghcr if: ${{ github.event_name == 'push' }} uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push by digest id: build uses: docker/build-push-action@v7 with: file: ./resources/Dockerfile platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }} - name: Export digest if: ${{ github.event_name == 'push' }} run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest if: ${{ github.event_name == 'push' }} uses: actions/upload-artifact@v7 with: name: digests-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: runs-on: ubuntu-latest needs: build if: ${{ github.event_name == 'push' }} steps: - name: Download digests uses: actions/download-artifact@v8 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # generate Docker tags based on the following events/attributes tags: | type=raw,value=20260522-0 type=sha - name: Login to ghcr uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} cloud-hypervisor-53.0/.github/workflows/integration-metrics.yaml000066400000000000000000000026001522476750100252360ustar00rootroot00000000000000name: Cloud Hypervisor Tests (Metrics) on: push: branches: - main jobs: build: name: Tests (Metrics) runs-on: garm-jammy-16 env: METRICS_PUBLISH_KEY: ${{ secrets.METRICS_PUBLISH_KEY }} steps: - name: Code checkout uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install Docker run: | set -eufo pipefail sudo apt-get update sudo apt-get -y install ca-certificates curl gnupg curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg sudo chmod a+r /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt install -y docker-ce docker-ce-cli - name: Run metrics tests timeout-minutes: 60 run: scripts/dev_cli.sh tests --metrics -- --test-exclude micro_,block_qcow2 -- --report-file /root/workloads/metrics.json - name: Upload metrics report run: 'curl -X PUT https://ch-metrics.azurewebsites.net/api/publishmetrics -H "x-functions-key: $METRICS_PUBLISH_KEY" -T ~/workloads/metrics.json' cloud-hypervisor-53.0/.github/workflows/release.yaml000066400000000000000000000077631522476750100227060ustar00rootroot00000000000000name: Cloud Hypervisor Release on: [create, merge_group] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} cancel-in-progress: true env: GITHUB_TOKEN: ${{ github.token }} 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@v7 - 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.89.0" - name: Copy Release Binaries if: github.event_name == 'create' && github.event.ref_type == 'tag' shell: bash run: | cp target/${{ matrix.platform.target }}/release/cloud-hypervisor ./${{ matrix.platform.name_ch }} cp target/${{ matrix.platform.target }}/release/ch-remote ./${{ matrix.platform.name_ch_remote }} - name: Upload Release Artifacts if: github.event_name == 'create' && github.event.ref_type == 'tag' uses: actions/upload-artifact@v7 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@v7 with: path: cloud-hypervisor-${{ github.event.ref }}.tar.xz name: cloud-hypervisor-${{ github.event.ref }}.tar.xz - name: Create GitHub Release if: github.event_name == 'create' && github.event.ref_type == 'tag' uses: softprops/action-gh-release@v3 with: draft: true files: | ./${{ matrix.platform.name_ch }} ./${{ matrix.platform.name_ch_remote }} ./cloud-hypervisor-${{ github.event.ref }}.tar.xz cloud-hypervisor-53.0/.gitignore000066400000000000000000000002051522476750100167550ustar00rootroot00000000000000**/*.rs.bk **/Cargo.lock **/rusty-tags.vi /.agents /.cargo /.claude /.codex /.vscode /build /rpm/SOURCES /target /vendor __pycache__ cloud-hypervisor-53.0/.gitlint000066400000000000000000000003231522476750100164410ustar00rootroot00000000000000[general] extra-path=scripts/gitlint/rules regex-style-search=true ignore=body-max-line-length,body-hard-tab [ignore-by-author-name] regex=dependabot ignore=all # default 72 [title-max-length] line-length=72 cloud-hypervisor-53.0/.lychee.toml000066400000000000000000000025101522476750100172120ustar00rootroot00000000000000verbose = "info" exclude_path = [".lychee.toml"] exclude = [ # Availability of links below should be manually verified. # Page for intel TDX support, returns 403 while querying. '^https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html', # Page for TPM, returns 403 while querying. '^https://trustedcomputinggroup.org/wp-content/uploads/PC-Client-Specific-Platform-TPM-Profile-for-TPM-2p0-v1p05p_r14_pub.pdf', # GitHub user smibarber referenced in `CREDITS.md` no longer exist '^https://github.com/smibarber', # OSDev has added bot protection and accesses my result in 403 Forbidden. '^https://wiki.osdev.org', # Exclude all pages with $ in the URL since $XXX is a variable "\\$.*", # Exclude local files "file://.*", # ARM documentation returns 403 Forbidden for automated CI checks. '^http://infocenter\.arm\.com', '^https://developer\.arm\.com', # Ignore internal/unsupported protocols seen in logs '^tcp://192\.168\.1\.10', # Slack invite endpoints reject automated GETs and return 403. '^https://join\.slack\.com/t/', # Metrics publish endpoint only answers authenticated PUTs; a plain GET # returns 404. '^https://ch-metrics\.azurewebsites\.net/api/publishmetrics', ] # Exclude loopback addresses exclude_loopback = true max_retries = 3 retry_wait_time = 5 cloud-hypervisor-53.0/.reuse/000077500000000000000000000000001522476750100161715ustar00rootroot00000000000000cloud-hypervisor-53.0/.reuse/dep5000066400000000000000000000006711522476750100167550ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: cloud-hypervisor Upstream-Contact: <> Source: https://www.cloudhypervisor.org Files: docs/*.md *.md Copyright: 2024 License: CC-BY-4.0 Files: scripts/* test_data/* *.toml .git* .editorconfig fuzz/Cargo.lock fuzz/.gitignore resources/linux-config-* vmm/src/api/openapi/cloud-hypervisor.yaml CODEOWNERS Cargo.lock Copyright: 2024 License: Apache-2.0 cloud-hypervisor-53.0/.rustfmt.toml000066400000000000000000000001201522476750100174400ustar00rootroot00000000000000edition = "2024" group_imports="StdExternalCrate" imports_granularity="Module" cloud-hypervisor-53.0/.taplo.toml000066400000000000000000000002251522476750100170610ustar00rootroot00000000000000include = ["**/Cargo.toml"] [formatting] indent_string = " " # 2 spaces: keep in sync with .editorconfig reorder_arrays = true reorder_keys = true cloud-hypervisor-53.0/.typos.toml000066400000000000000000000011171522476750100171210ustar00rootroot00000000000000# Configuration for https://github.com/crate-ci/typos [files] extend-exclude = [ "hypervisor/src/kvm/x86_64/mod.rs", "resources/linux-config-*", ] [default.extend-words] CLASSE = "CLASSE" Dake = "Dake" EXTINT = "EXTINT" INOUT = "INOUT" MSIS = "MSIS" # MSIs (Message Signaled Interrupt) SME = "SME" # Secure Memory Encryption THR = "THR" # Transmitter Holding Register TRANSLATER = "TRANSLATER" ba = "ba" conectix = "conectix" liness = "liness" outout = "outout" [default.extend-identifiers] consts = "consts" fo = "fo" fpr = "fpr" # Public Linux API msg_controllen = "msg_controllen" cloud-hypervisor-53.0/AGENTS.md000066400000000000000000000103031522476750100162700ustar00rootroot00000000000000## For Humans This is a compact [AGENTS.md](https://agents.md/) file for Cloud Hypervisor. It is meant to help automated coding agents make useful changes that stay safe, reviewable, and compatible with the project's normal engineering constraints. ## For LLMs ### Project Context - Start with `README.md` for the project shape and `CONTRIBUTING.md` for the contribution rules, coding style, commit message guidance, and LLM assistance disclosure policy. Following `CONTRIBUTING.md` is crucial! - Respect `.editorconfig` when editing files, in addition to any language-specific formatter required by `CONTRIBUTING.md`. ### Change Guidelines - Prefer correctness, safety, and readability over micro-optimizations. Keep changes small, reviewable, and aligned with the existing crate/module boundaries. Avoid speculative changes and unrelated refactoring. - For API, config, migration, device model, or hypervisor boundary changes, consider the effect on all architectures and all backends. Changes to one backend can be okay if the other backend still functions properly and could be extended or modified later. - Follow Rust best practices and the style already present in the touched code. - Avoid new dependencies unless the benefit is clear and local alternatives are not enough. - Preserve existing behavior unless the requested change explicitly needs a behavior change; refactors must preserve behavior. Call out compatibility or migration implications. - Do not invent APIs, behavior, or requirements. If something is uncertain, state the uncertainty and proceed only with minimal, explicit assumptions. - For `thiserror`-style errors, start messages with a capital letter and keep the outer `Display` text short. Put all non-`#[source]` attributes in the message to improve helpfulness, but do not repeat a `#[source]` value inline: Cloud Hypervisor prints the full error chain, so only include the concrete failure text directly when there is no source to report. ### Safety and Domain Notes - Prefer safe Rust. If `unsafe` is necessary, keep it narrow, add a `SAFETY:` comment with the invariants, and make sure the surrounding code upholds them. - Assume concurrency matters. Avoid races, unsynchronized shared state, and implicit ordering assumptions; prefer clear ownership and synchronization. ### Build and Test Notes - Some workspace members require the `kvm` feature to build or test correctly. When a default build failure looks feature-related, retry the narrow command with `--features kvm` before widening the diagnosis. - Prefer narrow crate/test commands while iterating, then broaden verification when the touched surface justifies it. - Formatting currently needs nightly-only rustfmt features; use `cargo +nightly fmt --all`. - Add targeted unit tests for bug fixes and non-trivial logic where practical. Keep test scaffolding minimal and focused. - Integration tests live in `./cloud-hypervisor/tests/` and are normally driven by `./scripts/dev_cli.sh` / `./scripts/run_integration_tests_*.sh`. They need host privileges, workloads, and container setup. To build the integration-test code directly without the infrastructure from `./scripts`, set the Rust cfg `devcli_testenv` or simply build through `clippy` which automatically includes these code paths; otherwise the integration-test code is not included. ### Commit and Patch Formatting - Follow the rules in `CONTRIBUTING.md`, including reviewable commit structure, valid component prefixes, 72-column commit messages, and a `Signed-off-by` trailer. - Lines in a commit message that are allowed to exceed the 72-column limit are specified in `./scripts/gitlint/rules`. - For LLM-assisted changes, follow the disclosure guidance in `CONTRIBUTING.md`: use the project's `Assisted-by:` trailer when disclosure is needed, and do not add `Co-authored-by` or similar trailers unless that policy changes. Prefer explicit version numbers, such as `Assisted-by: Claude:Opus-4.7`, rather than `Assisted-by: Claude:Opus-4`. - Temporary allowances such as `#[allow(unused)]` or ignored tests are only acceptable if resolved within the same commit series or paired with a clear TODO referencing a ticket. Ask the developer if in doubt. cloud-hypervisor-53.0/CODEOWNERS000066400000000000000000000001621522476750100163620ustar00rootroot00000000000000# Add the list of code owners here (using their GitHub username) * @cloud-hypervisor/cloud-hypervisor-reviewers cloud-hypervisor-53.0/CODE_OF_CONDUCT.md000066400000000000000000000062141522476750100175720ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, as contributors and maintainers we pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include but is not limited to: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address without explicit consent * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at otc.community@intel.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ [homepage]: https://www.contributor-covenant.org cloud-hypervisor-53.0/CONTRIBUTING.md000066400000000000000000000242041522476750100172230ustar00rootroot00000000000000# Contributing to Cloud Hypervisor Cloud Hypervisor is an open source project licensed under the [Apache v2 License](https://opensource.org/licenses/Apache-2.0) and the [BSD 3 Clause](https://opensource.org/licenses/BSD-3-Clause) license. Individual files contain details of their licensing and changes to that file are under the same license unless the contribution changes the license of the file. When importing code from a third party project (e.g. Firecracker or crosvm) please respect the license of those projects. New code should be under the [Apache v2 License](https://opensource.org/licenses/Apache-2.0). Cloud Hypervisor's main supported architectures are `x86_64` and `aarch64`, and the main hypervisor backends are KVM and MSHV. `x86_64` with KVM gets the most regular exercise, but changes should not make the other supported architecture and backend combinations worse. ## Coding Style & Code Comments We use the [Rust Style] guide and enforce formatting and linting in CI, including `rustfmt`, `clippy`, and other common Rust quality checks, for every pull request. We adapt to best practices, new lints and new tooling as the ecosystem evolves. Code should **speak for itself** (for example, by using descriptive identifiers) and be **easy to read and maintain**. Beyond the conventions and tooling described above, contributors have _some_ room to apply their own style and preferred structure. Maintainers may still suggest refactorings where they believe readability, consistency, or maintainability can be improved. For new code, add documentation and comments where they **provide additional value**: * **Rustdoc** explains the API to its users. * **Inline comments** explain the code the reader, especially *why* it is written that way. * **Commit messages** explain the broader context of a change (for more information on commit messages, see below). Comments should be concise and add additional context or information to the code. Logging should be minimal and high signal. Use `info!` for important normal state changes that matter in production; use `warn!` or `error!` only for abnormal conditions. Keep `debug!` for focused diagnostics. Please find more information in [`docs/logging.md`](docs/logging.md). Error messages should be sentence-style: start with a capital letter and stay concise. For `thiserror`-style errors, put all non-`#[source]` attributes (if they provide clear value) in the outer `Display` text to improve helpfulness, but do not repeat a `#[source]` value there because Cloud Hypervisor prints the full chain elsewhere. [Rust Style]: https://github.com/rust-lang/rust/tree/HEAD/src/doc/style-guide/src ## Basic Checks ```sh # We currently rely on nightly-only formatting features cargo +nightly fmt --all cargo check --all-targets --tests cargo clippy --all-targets --tests # Please note that this will not execute integration tests. cargo test --all-targets --tests # To lint your last three commits gitlint --commits "HEAD~3..HEAD" ``` ### \[Optional\] Run Integration Tests _Caution: These tests are taking a long time to complete (40+ mins) and need special setup._ ```sh bash ./scripts/dev_cli.sh tests --integration -- --test-filter '' ``` ### Setup Commit Hook Please consider creating the following hook as `.git/hooks/pre-commit` in order to ensure basic correctness of your code. You can extend this further if you have specific features that you regularly develop against. ```sh #!/bin/sh cargo +nightly fmt --all -- --check || exit 1 cargo check --locked --all-targets --tests || exit 1 cargo clippy --locked --all-targets --tests -- -D warnings || exit 1 ``` You will need to `chmod +x .git/hooks/pre-commit` to have it run on every commit you make. ## Certificate of Origin In order to get a clear contribution chain of trust we use the [signed-off-by language](https://www.kernel.org/doc/Documentation/process/submitting-patches.rst) used by the Linux kernel project. ## Patch format & Git Commit Hygiene _We use **Patch** as synonym for **Commit**._ We require patches to: - 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: ``` 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 you want to merge your changes to `cloud-hypervisor`: 1. Fork the [cloud-hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) project into your github organization. 1. Within your fork, create a branch for your contribution. 1. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) against the main branch of the Cloud Hypervisor repository. 1. Each commit must comply with the Commit Hygiene guidelines above. 1. A pull request should address a single component or concern to keep review focused and approvals straightforward. 1. Once the pull request is approved it can be integrated. Please squash any changes done during review already into the corresponding commits instead of pushing `: addressing review for A`-style commits. ## Issue tracking If you have a problem, please let us know. We recommend using [github issues](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new) for formally reporting and documenting them. To quickly and informally bring something up to us, you can also reach out on [Slack](https://cloud-hypervisor.slack.com). ## Closing issues You can either close issues manually by adding the fixing commit SHA1 to the issue 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. cloud-hypervisor-53.0/CREDITS.md000066400000000000000000000022261522476750100164110ustar00rootroot00000000000000# Cloud Hypervisor Credits and Thanks The Cloud Hypervisor code is based on the [rust-vmm](https://github.com/rust-vmm), [Firecracker](https://firecracker-microvm.github.io/) and [crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/) projects implementations. Our thanks go to the contributors of those projects: ## crosvm * [Zach Reizner](https://github.com/zachreizner) * [Dylan Reid](https://github.com/dgreid) * [Daniel Verkamp](https://github.com/danielverkamp) * [Stephen Barber](https://github.com/smibarber) * [Chirantan Ekbote](https://github.com/jynnantonix) * [Jason D. Clinton](https://github.com/jclinton) * Sonny Rao ## Firecracker See the [Firecracker CREDITS](https://github.com/firecracker-microvm/firecracker/blob/master/CREDITS.md). ## rust-vmm * [Andreea Florescu](https://github.com/andreeaflorescu) <> * [Paolo Bonzini](https://github.com/zachreizner) * [Jiang Liu](https://github.com/jiangliu) cloud-hypervisor-53.0/Cargo.lock000066400000000000000000002222241522476750100167010ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "acpi_tables" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce821f856a3eb1d033287f2dcfcdf94276d7895dd5bdd8ca45ae17e7d33d4dd9" dependencies = [ "zerocopy", ] [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.61.2", ] [[package]] name = "anyhow" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "api_client" version = "0.1.0" dependencies = [ "thiserror", "vmm-sys-util", ] [[package]] name = "arc-swap" version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] [[package]] name = "arch" version = "0.1.0" dependencies = [ "anyhow", "byteorder", "fdt", "hypervisor", "libc", "linux-loader", "log", "proptest", "serde", "serde_json", "thiserror", "uuid", "vm-fdt", "vm-memory", "vmm-sys-util", ] [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "pin-project-lite", "slab", ] [[package]] name = "async-io" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-lock" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-signal" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-link", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2be5a46ba01b60005ae2c51a36a29cfe134bcacae2dd5cedcd4615fbaad1494b" dependencies = [ "proc-macro2", "quote", "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block" version = "0.1.0" dependencies = [ "bitflags 2.13.0", "byteorder", "cfg-if", "crc-any", "flate2", "io-uring", "libc", "log", "remain", "serde", "smallvec", "thiserror", "uuid", "virtio-bindings", "virtio-queue", "vm-memory", "vm-virtio", "vmm-sys-util", "zerocopy", "zstd", ] [[package]] name = "block-buffer" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "blocking" version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures", "rand_core 0.10.1", ] [[package]] name = "clap" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cloud-hypervisor" version = "53.0.0" dependencies = [ "api_client", "block", "clap", "dhat", "dirs", "env_logger", "event_monitor", "hypervisor", "jiff", "libc", "log", "net_util", "option_parser", "seccompiler", "serde_json", "signal-hook", "signal-hook-registry", "test_infra", "thiserror", "tracer", "vm-migration", "vmm", "vmm-sys-util", "wait-timeout", "zbus", ] [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concat-idents" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" dependencies = [ "quote", "syn", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 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 = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11b6a6cff688cb070cf8b2fc91d19a83da9f631dbfe75cb4d7289ae35a1ff3f9" dependencies = [ "debug-helper", ] [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-utils" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-common" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", "syn", ] [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", "syn", ] [[package]] name = "debug-helper" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80a4af69c60438a1a82af89d362f4729fd38db7b73f305a237636fad31ceb2bf" [[package]] name = "defmt" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" dependencies = [ "bitflags 1.3.2", "defmt-macros", ] [[package]] name = "defmt-macros" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" dependencies = [ "defmt-parser", "proc-macro2", "quote", "syn", ] [[package]] name = "defmt-parser" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ "thiserror", ] [[package]] name = "devices" version = "0.1.0" dependencies = [ "acpi_tables", "anyhow", "arch", "bitfield-struct 0.13.0", "bitflags 2.13.0", "byteorder", "event_monitor", "hypervisor", "libc", "linux-loader", "log", "num_enum", "pci", "serde", "thiserror", "tpm", "vm-allocator", "vm-device", "vm-memory", "vm-migration", "vmm-sys-util", "zerocopy", ] [[package]] name = "dhat" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" dependencies = [ "backtrace", "lazy_static", "mintex", "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.61.2", ] [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "endi" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "env_filter" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "epoll" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e74d68fe2927dbf47aa976d14d93db9b23dced457c7bb2bdc6925a16d31b736e" dependencies = [ "bitflags 2.13.0", "libc", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "event_monitor" version = "0.1.0" dependencies = [ "flume", "libc", "log", "serde", "serde_json", ] [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" dependencies = [ "getrandom 0.3.4", ] [[package]] name = "fdt" 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.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ "fastrand", "futures-core", "futures-sink", "spin", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "slab", ] [[package]] name = "gdbstub" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" dependencies = [ "bitflags 2.13.0", "cfg-if", "log", "managed", "num-traits", "pastey", ] [[package]] name = "gdbstub_arch" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c02bfe7bd65f42bcda751456869dfa1eb2bd1c36e309b9ec27f4888d41cf258" dependencies = [ "gdbstub", "num-traits", ] [[package]] name = "getrandom" 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 = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", ] [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hybrid-array" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hypervisor" version = "0.1.0" dependencies = [ "anyhow", "arc-swap", "bitfield-struct 0.13.0", "byteorder", "cfg-if", "concat-idents", "env_logger", "iced-x86", "igvm", "igvm_defs", "kvm-bindings", "kvm-ioctls", "libc", "log", "mshv-bindings", "mshv-ioctls", "open-enum", "serde", "serde_json", "serde_with", "thiserror", "vfio-ioctls", "vm-memory", "vmm-sys-util", "zerocopy", ] [[package]] name = "iced-x86" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b" dependencies = [ "lazy_static", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "igvm" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67578b05ebcdfa1aa0fe13f77a13bdd7d87036128898a327f1bf8e7356cf09cd" dependencies = [ "bitfield-struct 0.10.1", "crc32fast", "hex", "igvm_defs", "open-enum", "range_map_vec", "static_assertions", "thiserror", "tracing", "zerocopy", ] [[package]] name = "igvm_defs" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eedd8c64460676101062f9f2ecdeb52d8f43e622da6a6c5bf5158f4ef08b0906" dependencies = [ "bitfield-struct 0.10.1", "open-enum", "static_assertions", "zerocopy", ] [[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "io-uring" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0" dependencies = [ "bitflags 2.13.0", "cfg-if", "libc", ] [[package]] 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.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8050f03ef0c6979597140b6d20da076931fb7a06389b8b0eb971b8f65b271748" dependencies = [ "iommufd-bindings", "thiserror", "vmm-sys-util", ] [[package]] name = "ipnetwork" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" dependencies = [ "serde", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" dependencies = [ "defmt", "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde_core", ] [[package]] name = "jiff-static" version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jobserver" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", "wasm-bindgen", ] [[package]] name = "kvm-bindings" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11cf0ca75d59e9d298647c59cf6c5286fa048120caa77972a7a504a0824d234f" dependencies = [ "serde", "vmm-sys-util", "zerocopy", ] [[package]] name = "kvm-ioctls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ac372c120eb893b086d1a12027669cf2b478d1f71204021ffa7adf57948d63" dependencies = [ "bitflags 2.13.0", "kvm-bindings", "libc", "vmm-sys-util", ] [[package]] name = "landlock" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "635839550ae8b90d9fd2571460a6645dc0aec070225956ca7a2831ed31d2795d" dependencies = [ "enumflags2", "libc", "thiserror", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] [[package]] name = "libssh2-sys" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c04141a07bb0c0bc461cb657808764de571702a59bc5c726c400ac9a7625e3ab" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", ] [[package]] name = "libz-sys" version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linux-loader" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de72cb02c55ecffcf75fe78295926f872eb6eb0a58d629c58a8c324dc26380f6" dependencies = [ "vm-memory", ] [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "managed" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" [[package]] name = "memchr" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "micro_http" version = "0.1.0" source = "git+https://github.com/firecracker-microvm/micro-http?branch=main#5c2254d6cf4f32a668d0d8e57ba20bebad9d4fba" dependencies = [ "libc", "vmm-sys-util", ] [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mintex" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "mshv-bindings" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83303108160c2b7a7bdd25000ee679384e19471386d23e501ed832574c9229ef" dependencies = [ "libc", "num_enum", "serde", "serde_derive", "vmm-sys-util", "zerocopy", ] [[package]] name = "mshv-ioctls" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db4449ac7012237b133da366f5b32ce4af1f8caf770486e5a9d54f7f6b73c4c" dependencies = [ "libc", "mshv-bindings", "thiserror", "vmm-sys-util", ] [[package]] name = "net_util" version = "0.1.0" dependencies = [ "epoll", "getrandom 0.4.3", "libc", "log", "pnet", "pnet_datalink", "rate_limiter", "serde", "serde_json", "thiserror", "virtio-bindings", "virtio-queue", "vm-memory", "vm-virtio", "vmm-sys-util", ] [[package]] name = "no-std-net" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_enum" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", ] [[package]] name = "num_enum_derive" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] name = "object" version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "offload_daemon" version = "0.1.0" dependencies = [ "clap", "env_logger", "libc", "log", "serde_json", "thiserror", "vm-memory", "vm-migration", "vmm", "vmm-sys-util", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb2508143a400b3361812094d987dd5adc81f0f5294a46491be648d6c94cab5" dependencies = [ "open-enum-derive", ] [[package]] name = "open-enum-derive" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d1296fab5231654a5aec8bf9e87ba4e3938c502fc4c3c0425a00084c78944be" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-src" version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", "openssl-src", "pkg-config", "vcpkg", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "option_parser" version = "0.1.0" dependencies = [ "thiserror", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "pastey" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pci" version = "0.1.0" dependencies = [ "anyhow", "base64", "byteorder", "hypervisor", "libc", "log", "serde", "thiserror", "vfio-bindings", "vfio-ioctls", "vfio_user", "vm-allocator", "vm-device", "vm-memory", "vm-migration", "vmm-sys-util", ] [[package]] name = "performance-metrics" version = "0.1.0" dependencies = [ "block", "clap", "libc", "serde", "serde_json", "test_infra", "thiserror", "vm-memory", "vmm-sys-util", ] [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "pnet" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" dependencies = [ "ipnetwork", "pnet_base", "pnet_datalink", "pnet_packet", "pnet_sys", "pnet_transport", ] [[package]] name = "pnet_base" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" dependencies = [ "no-std-net", ] [[package]] name = "pnet_datalink" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" dependencies = [ "ipnetwork", "libc", "pnet_base", "pnet_sys", "winapi", ] [[package]] name = "pnet_macros" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" dependencies = [ "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "pnet_macros_support" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" dependencies = [ "glob", "pnet_base", "pnet_macros", "pnet_macros_support", ] [[package]] name = "pnet_sys" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" dependencies = [ "libc", "winapi", ] [[package]] name = "pnet_transport" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" dependencies = [ "libc", "pnet_base", "pnet_packet", "pnet_sys", ] [[package]] name = "polling" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "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.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" 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.13.0", "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.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", ] [[package]] name = "rand" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", "rand_core 0.10.1", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.5", ] [[package]] name = "rand_core" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "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]] name = "range_map_vec" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cc2191ec1fd850e3ede4cf09ccfd40a33df561111f73e96e1b7c3f9eee31328" [[package]] name = "rate_limiter" version = "0.1.0" dependencies = [ "epoll", "libc", "log", "thiserror", "vmm-sys-util", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.13.0", ] [[package]] name = "redox_users" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] name = "regex" version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "remain" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7ef12e84481ab4006cb942f8682bba28ece7270743e649442027c5db87df126" dependencies = [ "proc-macro2", "quote", "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.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "seccompiler" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae55de56877481d112a559bbc12667635fdaf5e005712fd4e2b2fa50ffc884" dependencies = [ "libc", ] [[package]] name = "serde" 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 = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "serde_repr" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_with" version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "serde_core", "serde_with_macros", ] [[package]] name = "serde_with_macros" version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", "syn", ] [[package]] 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 = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" 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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] [[package]] name = "ssh2" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c95eb3c09e378543395a3fa9796f897861862466ee331d59140ade4ea0dcfdfc" dependencies = [ "bitflags 2.13.0", "libc", "libssh2-sys", "parking_lot", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" 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.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "terminal_size" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", "windows-sys 0.61.2", ] [[package]] name = "test_infra" version = "0.1.0" dependencies = [ "dirs", "epoll", "libc", "rand 0.10.2", "serde_json", "ssh2", "thiserror", "vmm-sys-util", "wait-timeout", ] [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thousands" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" 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", ] [[package]] name = "tpm" version = "0.1.0" dependencies = [ "anyhow", "libc", "log", "thiserror", "vmm-sys-util", ] [[package]] name = "tracer" version = "0.1.0" dependencies = [ "libc", "log", "serde", "serde_json", ] [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", "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.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", "rand 0.10.2", "serde_core", "wasm-bindgen", ] [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vfio-bindings" 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.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b1110f3ab5703a0f788d569b1b4a2436e8cc434f384b3ef5d77283f8ee03d8" dependencies = [ "byteorder", "iommufd-bindings", "iommufd-ioctls", "kvm-bindings", "kvm-ioctls", "libc", "log", "mshv-bindings", "mshv-ioctls", "thiserror", "vfio-bindings", "vm-memory", "vmm-sys-util", ] [[package]] name = "vfio_user" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731c2582dd43f4f174ab47b4c933a1a9bb872d9d1b7f54c5867e12dbc1491b75" dependencies = [ "bitflags 2.13.0", "libc", "log", "serde", "serde_derive", "serde_json", "thiserror", "vfio-bindings", "vm-memory", "vmm-sys-util", ] [[package]] name = "vhost" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee90657203a8644e9a0860a0db6a7887d8ef0c7bc09fc22dfa4ae75df65bac86" dependencies = [ "bitflags 2.13.0", "libc", "uuid", "vm-memory", "vmm-sys-util", ] [[package]] name = "vhost-user-backend" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5925983d8fb537752ad3e26604c0a17abfa5de77cb6773a096c8a959c9eca0f" dependencies = [ "libc", "log", "vhost", "virtio-bindings", "virtio-queue", "vm-memory", "vmm-sys-util", ] [[package]] name = "vhost_user_block" version = "0.1.0" dependencies = [ "block", "clap", "env_logger", "libc", "log", "option_parser", "thiserror", "vhost", "vhost-user-backend", "virtio-bindings", "virtio-queue", "vm-memory", "vmm-sys-util", ] [[package]] name = "vhost_user_net" version = "0.1.0" dependencies = [ "clap", "env_logger", "epoll", "libc", "log", "net_util", "option_parser", "thiserror", "vhost", "vhost-user-backend", "virtio-bindings", "vm-memory", "vmm-sys-util", ] [[package]] name = "virtio-bindings" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868" [[package]] name = "virtio-devices" version = "0.1.0" dependencies = [ "anyhow", "block", "byteorder", "epoll", "event_monitor", "hypervisor", "libc", "log", "mshv-ioctls", "net_util", "pci", "rate_limiter", "seccompiler", "serde", "serde_with", "serial_buffer", "smallvec", "thiserror", "vhost", "virtio-bindings", "virtio-queue", "vm-allocator", "vm-device", "vm-memory", "vm-migration", "vm-virtio", "vmm-sys-util", ] [[package]] name = "virtio-queue" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e358084f32ed165fddb41d98ff1b7ff3c08b9611d8d6114a1b422e2e85688baf" dependencies = [ "libc", "log", "virtio-bindings", "vm-memory", "vmm-sys-util", ] [[package]] name = "vm-allocator" version = "0.1.0" dependencies = [ "arch", "libc", "thiserror", "vm-memory", ] [[package]] name = "vm-device" version = "0.1.0" dependencies = [ "hypervisor", "serde", "thiserror", "vfio-ioctls", "vm-memory", "vmm-sys-util", ] [[package]] name = "vm-fdt" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23" [[package]] name = "vm-memory" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380" dependencies = [ "arc-swap", "libc", "thiserror", "winapi", ] [[package]] name = "vm-migration" version = "0.1.0" dependencies = [ "anyhow", "itertools", "log", "rustls", "serde", "serde_json", "thiserror", "vm-memory", "zerocopy", ] [[package]] name = "vm-virtio" version = "0.1.0" dependencies = [ "log", "virtio-bindings", "virtio-queue", "vm-memory", ] [[package]] name = "vmm" version = "0.1.0" dependencies = [ "acpi_tables", "anyhow", "arch", "bitflags 2.13.0", "block", "blocking", "cfg-if", "clap", "devices", "dhat", "epoll", "event_monitor", "flume", "futures", "gdbstub", "gdbstub_arch", "hex", "hypervisor", "igvm", "igvm_defs", "iommufd-ioctls", "landlock", "libc", "linux-loader", "log", "micro_http", "mshv-bindings", "net_util", "option_parser", "pci", "range_map_vec", "rate_limiter", "seccompiler", "serde", "serde_json", "serde_with", "serial_buffer", "sha2", "signal-hook", "tempfile", "thiserror", "tracer", "uuid", "vfio-ioctls", "vfio_user", "vhost", "virtio-bindings", "virtio-devices", "vm-allocator", "vm-device", "vm-memory", "vm-migration", "vm-virtio", "vmm-sys-util", "zbus", "zerocopy", ] [[package]] name = "vmm-sys-util" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" dependencies = [ "bitflags 1.3.2", "libc", "serde", "serde_derive", ] [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zbus" version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast", "async-executor", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-lite", "hex", "libc", "ordered-stream", "rustix", "serde", "serde_repr", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zbus_names", "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", "winnow", "zvariant", ] [[package]] name = "zerocopy" version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[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 = [ "zstd-safe", ] [[package]] name = "zstd-safe" version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", ] [[package]] name = "zvariant" version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", "winnow", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", "syn", "winnow", ] cloud-hypervisor-53.0/Cargo.toml000066400000000000000000000065251522476750100167300ustar00rootroot00000000000000# 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 inherits = "release" strip = false [workspace] members = [ "api_client", "arch", "block", "cloud-hypervisor", "devices", "event_monitor", "hypervisor", "net_util", "offload_daemon", "option_parser", "pci", "performance-metrics", "rate_limiter", "serial_buffer", "test_infra", "tracer", "vhost_user_block", "vhost_user_net", "virtio-devices", "vm-allocator", "vm-device", "vm-migration", "vm-virtio", "vmm", ] package.edition = "2024" # Minimum buildable version: # Keep in sync with version in .github/workflows/build.yaml # Policy on MSRV (see #4318): # Can only be bumped if satisfying any of the following: # a.) A dependency requires it, # b.) If we want to use a new feature and that MSRV is at least 6 months old, # c.) There is a security issue that is addressed by the toolchain update. package.rust-version = "1.89.0" resolver = "3" [workspace.dependencies] # rust-vmm crates acpi_tables = "0.2.1" iommufd-ioctls = "0.2.0" kvm-bindings = "0.14.1" kvm-ioctls = "0.25.0" linux-loader = "0.13.2" mshv-bindings = "0.6.9" mshv-ioctls = "0.6.9" seccompiler = "0.5.0" vfio-bindings = { version = "0.6.2", default-features = false } vfio-ioctls = { version = "0.7.0", default-features = false } vfio_user = { version = "0.1.3", default-features = false } vhost = { version = "0.16.0", default-features = false } vhost-user-backend = { version = "0.22.0", default-features = false } virtio-bindings = "0.2.6" virtio-queue = "0.17.0" vm-fdt = "0.3.0" vm-memory = "0.17.1" vmm-sys-util = "0.15.0" # igvm crates igvm = "0.4.0" igvm_defs = "0.4.0" # serde crates serde = "1.0.228" serde_json = "1.0.150" serde_with = { version = "3.19.0", default-features = false } # other crates anyhow = "1.0.102" base64 = "0.22.1" bitflags = "2.11.1" byteorder = "1.5.0" cfg-if = "1.0.4" clap = "4.6.1" dhat = "0.3.3" dirs = "6.0.0" env_logger = "0.11.10" epoll = "4.4.0" flume = "0.12.0" itertools = "0.15.0" jiff = { version = "0.2", default-features = false, features = ["std"] } libc = "0.2.186" log = "0.4.30" rustls = { version = "0.23.40", default-features = false, features = [ "logging", "ring", "std", ] } sha2 = "0.11.0" signal-hook = "0.4.4" signal-hook-registry = "1.4.8" smallvec = "1.15.1" thiserror = "2.0.18" uuid = { version = "1.23.2" } wait-timeout = "0.2.1" zerocopy = { version = "0.8.50", default-features = false } [workspace.lints.clippy] # Any clippy lint (group) in alphabetical order: # https://rust-lang.github.io/rust-clippy/master/index.html # Groups all = "deny" # shorthand for the other groups but here for compleness complexity = "deny" correctness = "deny" perf = "deny" style = "deny" suspicious = "deny" # Individual Lints absolute_paths = "deny" assertions_on_result_states = "deny" if_not_else = "deny" manual_string_new = "deny" map_unwrap_or = "deny" needless_pass_by_value = "deny" redundant_else = "deny" semicolon_if_nothing_returned = "deny" undocumented_unsafe_blocks = "deny" uninlined_format_args = "deny" unnecessary_semicolon = "deny" [workspace.lints.rust] # `level = warn` is irrelevant here but mandatory for rustc/cargo unexpected_cfgs = { level = "warn", check-cfg = ['cfg(devcli_testenv)'] } cloud-hypervisor-53.0/LICENSES/000077500000000000000000000000001522476750100161755ustar00rootroot00000000000000cloud-hypervisor-53.0/LICENSES/Apache-2.0.txt000066400000000000000000000261361522476750100204240ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cloud-hypervisor-53.0/LICENSES/BSD-3-Clause.txt000066400000000000000000000030321522476750100207160ustar00rootroot00000000000000// Copyright 2017 The Chromium OS Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cloud-hypervisor-53.0/LICENSES/CC-BY-4.0.txt000066400000000000000000000445201522476750100200370ustar00rootroot00000000000000All documentations (e.g. files with extension `.md`) in this repository is covered by the following license: Attribution 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. cloud-hypervisor-53.0/MAINTAINERS.md000066400000000000000000000002531522476750100170640ustar00rootroot00000000000000# Maintainers - Sebastien Boeuf - @sboeuf - Robert Bradford - @rbradford - Bo Chen - @likebreath - Samuel Ortiz - @sameo - Wei Liu - @liuw - Michael Zhao - @michael2012z cloud-hypervisor-53.0/README.md000066400000000000000000000371501522476750100162550ustar00rootroot00000000000000- [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's main supported architectures are `x86-64` and `AArch64`, with functionality varying across these platforms. The functionality differences between `x86-64` and `AArch64` are documented in [#1125](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/1125). The `riscv64` architecture support is experimental and offers limited functionality. For more details and instructions, please refer to [riscv documentation](docs/riscv.md). ### 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 boots guests in one of two ways. The first is direct kernel boot, where a kernel image is passed to `--kernel`. The x86-64 kernel must be built with PVH support or be a bzImage. The second is firmware boot, where a firmware image is passed to `--firmware` and brings up the guest's normal boot loader. Two firmware options are supported, and which one works best depends on the guest OS. [Rust Hypervisor Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware) is a lightweight Rust-based PVH firmware. The edk2 UEFI firmware is called `CLOUDHV.fd` for x86-64 and `CLOUDHV_EFI.fd` for AArch64. Prebuilt binaries for both are available at their respective releases pages, [Rust Hypervisor Firmware](https://github.com/cloud-hypervisor/rust-hypervisor-firmware/releases/latest) and [our edk2 fork](https://github.com/cloud-hypervisor/edk2/releases/latest). The edk2 fork carries customizations required to boot AArch64 guests on cloud-hypervisor. See [docs/uefi.md](docs/uefi.md) for differences with upstream tianocore/edk2. ### Firmware Booting Cloud Hypervisor supports booting disk images containing all needed components to run cloud workloads, a.k.a. cloud images. The following sample commands will download an Ubuntu Cloud image, converting it into a format that Cloud Hypervisor can use and a firmware to boot the image with. ```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 \ --firmware ./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 ``` ## Booting: `--firmware` vs `--kernel` The following scenarios are supported by Cloud Hypervisor to bootstrap a VM, i.e., to load a payload/bootitem(s): - Provide firmware - Provide kernel \[+ cmdline\]\ [+ initrd\] Please note that our Cloud Hypervisor firmware (`hypervisor-fw`) has a Xen PVH boot entry, therefore it can also be booted via the `--kernel` parameter, as seen in some examples. ### Custom Kernel and Disk Image #### Building your Kernel Cloud Hypervisor also supports direct kernel boot. For x86-64, a `vmlinux` ELF kernel (compiled with PVH support) or a regular bzImage are supported. In order to support development there is a custom branch; however provided the required options are enabled any recent kernel will suffice. To build the kernel: ```shell # Clone the Cloud Hypervisor Linux branch $ git clone --depth 1 https://github.com/cloud-hypervisor/linux.git -b ch-6.16.9 linux-cloud-hypervisor $ pushd 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-53.0/api_client/000077500000000000000000000000001522476750100170775ustar00rootroot00000000000000cloud-hypervisor-53.0/api_client/Cargo.toml000066400000000000000000000004301522476750100210240ustar00rootroot00000000000000[package] authors = ["The Cloud Hypervisor Authors"] 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 cloud-hypervisor-53.0/api_client/src/000077500000000000000000000000001522476750100176665ustar00rootroot00000000000000cloud-hypervisor-53.0/api_client/src/lib.rs000066400000000000000000000155501522476750100210100ustar00rootroot00000000000000// Copyright © 2020 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::io::{self, Read, Write}; use std::os::unix::io::RawFd; use std::{num, str}; use thiserror::Error; use vmm_sys_util::errno; use vmm_sys_util::sock_ctrl_msg::ScmSocket; #[derive(Debug, Error)] pub enum Error { #[error("Error writing to or reading from HTTP socket")] Socket(#[source] io::Error), #[error("Error sending file descriptors")] SocketSendFds(#[source] errno::Error), #[error("Error parsing HTTP status code")] StatusCodeParsing(#[source] num::ParseIntError), #[error("HTTP output is missing protocol statement")] MissingProtocol, #[error("Error parsing HTTP Content-Length field")] ContentLengthParsing(#[source] num::ParseIntError), #[error("Server responded with error {0:?}: {1:?}")] ServerResponse( StatusCode, // TODO: Move `api` module from `vmm` to dedicated crate and use a common type definition Option< String, /* Untyped: Currently Vec of error messages from top to root cause */ >, ), } #[derive(Clone, Copy, Debug)] pub enum StatusCode { Continue, Ok, NoContent, BadRequest, NotFound, TooManyRequests, InternalServerError, NotImplemented, Unknown, } impl StatusCode { fn from_raw(code: usize) -> StatusCode { match code { 100 => StatusCode::Continue, 200 => StatusCode::Ok, 204 => StatusCode::NoContent, 400 => StatusCode::BadRequest, 404 => StatusCode::NotFound, 429 => StatusCode::TooManyRequests, 500 => StatusCode::InternalServerError, 501 => StatusCode::NotImplemented, _ => StatusCode::Unknown, } } fn parse(code: &str) -> Result { Ok(StatusCode::from_raw( code.trim().parse().map_err(Error::StatusCodeParsing)?, )) } fn is_server_error(self) -> bool { !matches!( self, StatusCode::Ok | StatusCode::Continue | StatusCode::NoContent ) } } fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> { let header_str = format!("{header}: "); res.find(&header_str) .map(|o| &res[o + header_str.len()..o + res[o..].find('\r').unwrap()]) } fn get_status_code(res: &str) -> Result { if let Some(o) = res.find("HTTP/1.1") { Ok(StatusCode::parse( &res[o + "HTTP/1.1 ".len()..res[o..].find('\r').unwrap()], )?) } else { Err(Error::MissingProtocol) } } fn parse_http_response(socket: &mut dyn Read) -> Result, Error> { let mut res = String::new(); let mut body_offset = None; let mut content_length: Option = None; loop { let mut bytes = vec![0; 256]; let count = socket.read(&mut bytes).map_err(Error::Socket)?; // If the return value is 0, the peer has performed an orderly shutdown. if count == 0 { break; } res.push_str(str::from_utf8(&bytes[0..count]).unwrap()); // End of headers if let Some(o) = res.find("\r\n\r\n") { body_offset = Some(o + "\r\n\r\n".len()); // With all headers available we can see if there is any body content_length = if let Some(length) = get_header(&res, "Content-Length") { Some(length.trim().parse().map_err(Error::ContentLengthParsing)?) } else { None }; if content_length.is_none() { 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..]))); let status_code = get_status_code(&res)?; if status_code.is_server_error() { Err(Error::ServerResponse(status_code, body_string)) } else { Ok(body_string) } } /// Make an API request using the fully qualified command name. /// For example, full_command could be "vm.create" or "vmm.ping". pub fn simple_api_full_command_with_fds_and_response( socket: &mut T, method: &str, full_command: &str, request_body: Option<&str>, request_fds: &[RawFd], ) -> Result, Error> { socket .send_with_fds( &[format!( "{method} /api/v1/{full_command} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n" ) .as_bytes()], request_fds, ) .map_err(Error::SocketSendFds)?; if let Some(request_body) = request_body { socket .write_all(format!("Content-Length: {}\r\n", request_body.len()).as_bytes()) .map_err(Error::Socket)?; } socket.write_all(b"\r\n").map_err(Error::Socket)?; if let Some(request_body) = request_body { socket .write_all(request_body.as_bytes()) .map_err(Error::Socket)?; } socket.flush().map_err(Error::Socket)?; parse_http_response(socket) } pub fn simple_api_full_command_with_fds( socket: &mut T, method: &str, full_command: &str, request_body: Option<&str>, request_fds: &[RawFd], ) -> Result<(), Error> { let response = simple_api_full_command_with_fds_and_response( socket, method, full_command, request_body, request_fds, )?; if let Some(response) = response { println!("{response}"); } Ok(()) } pub fn simple_api_full_command( socket: &mut T, method: &str, full_command: &str, request_body: Option<&str>, ) -> Result<(), Error> { simple_api_full_command_with_fds(socket, method, full_command, request_body, &[]) } pub fn simple_api_full_command_and_response( socket: &mut T, method: &str, full_command: &str, request_body: Option<&str>, ) -> Result, Error> { simple_api_full_command_with_fds_and_response(socket, method, full_command, request_body, &[]) } pub fn simple_api_command_with_fds( socket: &mut T, method: &str, c: &str, request_body: Option<&str>, request_fds: &[RawFd], ) -> Result<(), Error> { // Create the full VM command. For VMM commands, use // simple_api_full_command(). let full_command = format!("vm.{c}"); simple_api_full_command_with_fds(socket, method, &full_command, request_body, request_fds) } pub fn simple_api_command( socket: &mut T, method: &str, c: &str, request_body: Option<&str>, ) -> Result<(), Error> { simple_api_command_with_fds(socket, method, c, request_body, &[]) } cloud-hypervisor-53.0/arch/000077500000000000000000000000001522476750100157055ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/Cargo.toml000066400000000000000000000017411522476750100176400ustar00rootroot00000000000000[package] authors = ["The Chromium OS Authors"] edition.workspace = true name = "arch" rust-version.workspace = true version = "0.1.0" [features] default = [] fw_cfg = [] kvm = ["hypervisor/kvm"] sev_snp = [] tdx = [] [dependencies] anyhow = { workspace = true } byteorder = { workspace = true } hypervisor = { path = "../hypervisor" } libc = { workspace = true } linux-loader = { workspace = true, features = ["bzimage", "elf", "pe"] } log = { workspace = true } serde = { workspace = true, features = ["derive", "rc"] } thiserror = { workspace = true } uuid = { workspace = true } vm-memory = { workspace = true, features = ["backend-bitmap", "backend-mmap"] } vmm-sys-util = { workspace = true, features = ["with-serde"] } [dev-dependencies] proptest = "1.0.0" serde_json = { workspace = true } [target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies] fdt_parser = { version = "0.1.5", package = "fdt" } vm-fdt = { workspace = true } [lints] workspace = true cloud-hypervisor-53.0/arch/src/000077500000000000000000000000001522476750100164745ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/src/aarch64/000077500000000000000000000000001522476750100177245ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/src/aarch64/cache.rs000066400000000000000000000143431522476750100213420ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the THIRD-PARTY file. use std::fs; use std::path::Path; use log::warn; #[derive(Copy, Clone)] pub enum CacheLevel { /// L1 data cache L1D = 0, /// L1 instruction cache L1I = 1, /// L2 cache L2 = 2, /// L3 cache L3 = 3, } /// NOTE: cache size file directory example, /// "/sys/devices/system/cpu/cpu0/cache/index0/size". pub fn get_cache_size(cache_level: CacheLevel) -> u32 { let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string(); match cache_level { CacheLevel::L1D => file_directory += "/index0/size", CacheLevel::L1I => file_directory += "/index1/size", CacheLevel::L2 => file_directory += "/index2/size", CacheLevel::L3 => file_directory += "/index3/size", } let file_path = Path::new(&file_directory); if file_path.exists() { let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted."); // The content of the file is as simple as a size, like: "32K" let src = src.trim(); let src_digits: u32 = src[0..src.len() - 1].parse().unwrap(); let src_unit = &src[src.len() - 1..]; src_digits * match src_unit { "K" => 1u32 << 10, "M" => 1u32 << 20, "G" => 1u32 << 30, _ => 1, } } else { 0 } } /// NOTE: coherency_line_size file directory example, /// "/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size". pub fn get_cache_coherency_line_size(cache_level: CacheLevel) -> u32 { let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string(); match cache_level { CacheLevel::L1D => file_directory += "/index0/coherency_line_size", CacheLevel::L1I => file_directory += "/index1/coherency_line_size", CacheLevel::L2 => file_directory += "/index2/coherency_line_size", CacheLevel::L3 => file_directory += "/index3/coherency_line_size", } let file_path = Path::new(&file_directory); if file_path.exists() { let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted."); src.trim().parse::().unwrap() } else { 0 } } /// NOTE: number_of_sets file directory example, /// "/sys/devices/system/cpu/cpu0/cache/index0/number_of_sets". pub fn get_cache_number_of_sets(cache_level: CacheLevel) -> u32 { let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string(); match cache_level { CacheLevel::L1D => file_directory += "/index0/number_of_sets", CacheLevel::L1I => file_directory += "/index1/number_of_sets", CacheLevel::L2 => file_directory += "/index2/number_of_sets", CacheLevel::L3 => file_directory += "/index3/number_of_sets", } let file_path = Path::new(&file_directory); if file_path.exists() { let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted."); src.trim().parse::().unwrap() } else { 0 } } /// NOTE: shared_cpu_list file directory example, /// "/sys/devices/system/cpu/cpu0/cache/index0/shared_cpu_list". pub fn get_cache_shared(cache_level: CacheLevel) -> bool { let mut file_directory: String = "/sys/devices/system/cpu/cpu0/cache".to_string(); let mut result = true; match cache_level { CacheLevel::L1D | CacheLevel::L1I => result = false, CacheLevel::L2 => file_directory += "/index2/shared_cpu_list", CacheLevel::L3 => file_directory += "/index3/shared_cpu_list", } if !result { return false; } let file_path = Path::new(&file_directory); if file_path.exists() { let src = fs::read_to_string(file_directory).expect("File not exists or file corrupted."); let src = src.trim(); if src.is_empty() { result = false; } else { result = src.contains('-') || src.contains(','); } } else { result = false; } result } #[derive(Default, Copy, Clone, Debug)] pub struct CacheTopologyInfo { pub l1_d_cache_size: u32, pub l1_d_cache_line_size: u32, pub l1_d_cache_sets: u32, pub l1_i_cache_size: u32, pub l1_i_cache_line_size: u32, pub l1_i_cache_sets: u32, pub l2_cache_size: u32, pub l2_cache_line_size: u32, pub l2_cache_sets: u32, pub l3_cache_size: u32, pub l3_cache_line_size: u32, pub l3_cache_sets: u32, pub l2_cache_shared: bool, pub l3_cache_shared: bool, } /// Reads cache topology information from sysfs for cpu0. pub fn read_cache_topology() -> Option { let cache_path = Path::new("/sys/devices/system/cpu/cpu0/cache"); if !cache_path.exists() { warn!("Cache topology information is not available in sysfs."); return None; } let mut info = CacheTopologyInfo { l1_d_cache_size: get_cache_size(CacheLevel::L1D), l1_d_cache_line_size: get_cache_coherency_line_size(CacheLevel::L1D), l1_d_cache_sets: get_cache_number_of_sets(CacheLevel::L1D), l1_i_cache_size: get_cache_size(CacheLevel::L1I), l1_i_cache_line_size: get_cache_coherency_line_size(CacheLevel::L1I), l1_i_cache_sets: get_cache_number_of_sets(CacheLevel::L1I), l2_cache_size: get_cache_size(CacheLevel::L2), l2_cache_line_size: get_cache_coherency_line_size(CacheLevel::L2), l2_cache_sets: get_cache_number_of_sets(CacheLevel::L2), l3_cache_size: get_cache_size(CacheLevel::L3), l3_cache_line_size: get_cache_coherency_line_size(CacheLevel::L3), l3_cache_sets: get_cache_number_of_sets(CacheLevel::L3), l2_cache_shared: false, l3_cache_shared: false, }; if info.l2_cache_size != 0 { info.l2_cache_shared = get_cache_shared(CacheLevel::L2); } if info.l3_cache_size != 0 { info.l3_cache_shared = get_cache_shared(CacheLevel::L3); } Some(info) } cloud-hypervisor-53.0/arch/src/aarch64/fdt.rs000066400000000000000000001351001522476750100210470ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the THIRD-PARTY file. use std::collections::HashMap; use std::ffi::CStr; use std::fmt::Debug; use std::hash::BuildHasher; use std::sync::{Arc, Mutex}; use std::{cmp, result, str}; use byteorder::{BigEndian, ByteOrder}; use fdt_parser::node::FdtNode; use hypervisor::arch::aarch64::gic::Vgic; use hypervisor::arch::aarch64::regs::{ AARCH64_ARCH_TIMER_HYP_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ, AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_PMU_IRQ, }; use log::{debug, info}; use thiserror::Error; use vm_fdt::{FdtWriter, FdtWriterResult}; use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion}; use super::super::{DeviceType, GuestMemoryMmap, InitramfsConfig}; use super::cache::{CacheTopologyInfo, read_cache_topology}; use super::layout::{ GIC_V2M_COMPATIBLE, GICV2M_SPI_BASE, GICV2M_SPI_NUM, IRQ_BASE, MEM_32BIT_DEVICES_SIZE, MEM_32BIT_DEVICES_START, MEM_PCI_IO_SIZE, MEM_PCI_IO_START, PCI_HIGH_BASE, PCI_MMIO_CONFIG_SIZE_PER_SEGMENT, }; use crate::{NumaNodes, PciSpaceInfo}; // This is a value for uniquely identifying the FDT node declaring the interrupt controller. const GIC_PHANDLE: u32 = 1; // This is a value for uniquely identifying the FDT node declaring the MSI controller. const MSI_PHANDLE: u32 = 2; // This is a value for uniquely identifying the FDT node containing the clock definition. const CLOCK_PHANDLE: u32 = 3; // This is a value for uniquely identifying the FDT node containing the gpio controller. const GPIO_PHANDLE: u32 = 4; // This is a value for virtio-iommu. Now only one virtio-iommu device is supported. const VIRTIO_IOMMU_PHANDLE: u32 = 5; // NOTE: Keep FIRST_VCPU_PHANDLE the last PHANDLE defined. // This is a value for uniquely identifying the FDT node containing the first vCPU. // The last number of vCPU phandle depends on the number of vCPUs. const FIRST_VCPU_PHANDLE: u32 = 8; // This is a value for uniquely identifying the FDT node containing the L2 cache info const L2_CACHE_PHANDLE: u32 = 6; // This is a value for uniquely identifying the FDT node containing the L3 cache info const L3_CACHE_PHANDLE: u32 = 7; // Read the documentation specified when appending the root node to the FDT. const ADDRESS_CELLS: u32 = 0x2; const SIZE_CELLS: u32 = 0x2; // As per kvm tool and // https://www.kernel.org/doc/Documentation/devicetree/bindings/interrupt-controller/arm%2Cgic.txt // Look for "The 1st cell..." const GIC_FDT_IRQ_TYPE_SPI: u32 = 0; const GIC_FDT_IRQ_TYPE_PPI: u32 = 1; // From https://elixir.bootlin.com/linux/v4.9.62/source/include/dt-bindings/interrupt-controller/irq.h#L17 const IRQ_TYPE_EDGE_RISING: u32 = 1; const IRQ_TYPE_LEVEL_HI: u32 = 4; // Keys and Buttons // System Power Down const KEY_POWER: u32 = 116; /// Trait for devices to be added to the Flattened Device Tree. pub trait DeviceInfoForFdt { /// Returns the address where this device will be loaded. fn addr(&self) -> u64; /// Returns the associated interrupt for this device. fn irq(&self) -> u32; /// Returns the amount of memory that needs to be reserved for this device. fn length(&self) -> u64; } /// Errors thrown while configuring the Flattened Device Tree for aarch64. #[derive(Debug, Error)] pub enum Error { /// Failure in writing FDT in memory. #[error("Failure in writing FDT in memory")] WriteFdtToMemory(#[source] GuestMemoryError), } type Result = result::Result; /// Creates the flattened device tree for this aarch64 VM. #[expect(clippy::too_many_arguments)] pub fn create_fdt( guest_mem: &GuestMemoryMmap, cmdline: &str, vcpu_mpidr: &[u64], vcpu_topology: Option<(u16, u16, u16, u16)>, device_info: &HashMap<(DeviceType, String), T, S>, gic_device: &Arc>, initrd: &Option, pci_space_info: &[PciSpaceInfo], numa_nodes: &NumaNodes, virtio_iommu_bdf: Option, pmu_supported: bool, ) -> FdtWriterResult> { // Allocate stuff necessary for the holding the blob. let mut fdt = FdtWriter::new().unwrap(); // For an explanation why these nodes were introduced in the blob take a look at // the "Device Node Requirements" chapter of the Devicetree Specification. // https://www.devicetree.org/specifications/ // Header or the root node as per above mentioned documentation. let root_node = fdt.begin_node("")?; fdt.property_string("compatible", "linux,dummy-virt")?; // For info on #address-cells and size-cells read "Note about cells and address representation" // from the above mentioned txt file. fdt.property_u32("#address-cells", ADDRESS_CELLS)?; fdt.property_u32("#size-cells", SIZE_CELLS)?; // This is not mandatory but we use it to point the root node to the node // containing description of the interrupt controller for this VM. fdt.property_u32("interrupt-parent", GIC_PHANDLE)?; create_cpu_nodes(&mut fdt, vcpu_mpidr, vcpu_topology, numa_nodes)?; create_memory_node(&mut fdt, guest_mem, numa_nodes)?; create_chosen_node(&mut fdt, cmdline, initrd)?; create_gic_node(&mut fdt, gic_device)?; create_timer_node(&mut fdt)?; if pmu_supported { create_pmu_node(&mut fdt)?; } create_clock_node(&mut fdt)?; create_psci_node(&mut fdt)?; create_devices_node(&mut fdt, device_info)?; create_pci_nodes(&mut fdt, pci_space_info, virtio_iommu_bdf)?; if numa_nodes.len() > 1 { create_distance_map_node(&mut fdt, numa_nodes)?; } // End Header node. fdt.end_node(root_node)?; let fdt_final = fdt.finish()?; Ok(fdt_final) } pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Result<()> { // Write FDT to memory. guest_mem .write_slice(fdt_final, super::layout::FDT_START) .map_err(Error::WriteFdtToMemory)?; Ok(()) } // Following are the auxiliary function for creating the different nodes that we append to our FDT. fn create_cpu_nodes( fdt: &mut FdtWriter, vcpu_mpidr: &[u64], 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. let cpus_node = fdt.begin_node("cpus")?; fdt.property_u32("#address-cells", 0x1)?; fdt.property_u32("#size-cells", 0x0)?; let num_cpus = vcpu_mpidr.len(); let (threads_per_core, cores_per_die, dies_per_package, packages) = vcpu_topology.unwrap_or((1, 1, 1, 1)); let cores_per_package = cores_per_die * dies_per_package; let max_cpus: u32 = threads_per_core as u32 * cores_per_die as u32 * dies_per_package as u32 * packages as u32; // Add cache info. let cache_info = read_cache_topology(); let cache_exist = cache_info.is_some(); let CacheTopologyInfo { l1_d_cache_size, l1_d_cache_line_size, l1_d_cache_sets, l1_i_cache_size, l1_i_cache_line_size, l1_i_cache_sets, l2_cache_size, l2_cache_line_size, l2_cache_sets, l3_cache_size, l3_cache_line_size, l3_cache_sets, l2_cache_shared, l3_cache_shared, } = cache_info.unwrap_or_default(); // Arm boot protocol requires a minimal Device Tree // https://docs.kernel.org/arch/arm64/booting.html // As Generic initiators are supported only in ACPI // When a guest kernel does not boot under "acpi=force" mode it can // hang due to conflicting numa information present in FDT which // does not support Generic Initiators let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some()); if has_generic_initiator { info!("Skipping NUMA CPU node encoding in FDT with Generic Initiator devices"); } for (cpu_id, mpidr) in vcpu_mpidr.iter().enumerate().take(num_cpus) { let cpu_name = format!("cpu@{cpu_id:x}"); let cpu_node = fdt.begin_node(&cpu_name)?; fdt.property_string("device_type", "cpu")?; fdt.property_string("compatible", "arm,arm-v8")?; if num_cpus > 1 { // This is required on armv8 64-bit. See aforementioned documentation. fdt.property_string("enable-method", "psci")?; } // Set the field to first 24 bits of the MPIDR - Multiprocessor Affinity Register. // See http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0488c/BABHBJCI.html. fdt.property_u32("reg", (mpidr & 0x7FFFFF) as u32)?; fdt.property_u32("phandle", cpu_id as u32 + FIRST_VCPU_PHANDLE)?; // Skipping NUMA encoding in FDT when Generic Initiator devices // are present allowed such guest kernels to boot properly and // rely solely on ACPI tables to setup NUMA if numa_nodes.len() > 1 && !has_generic_initiator { for numa_node_idx in 0..numa_nodes.len() { let numa_node = numa_nodes.get(&(numa_node_idx as u32)); if numa_node.unwrap().cpus.contains(&(cpu_id as u32)) { fdt.property_u32("numa-node-id", numa_node_idx as u32)?; } } } if cache_exist && l1_d_cache_size != 0 && l1_i_cache_size != 0 { // Add cache info. fdt.property_u32("d-cache-size", l1_d_cache_size)?; fdt.property_u32("d-cache-line-size", l1_d_cache_line_size)?; fdt.property_u32("d-cache-sets", l1_d_cache_sets)?; fdt.property_u32("i-cache-size", l1_i_cache_size)?; fdt.property_u32("i-cache-line-size", l1_i_cache_line_size)?; fdt.property_u32("i-cache-sets", l1_i_cache_sets)?; if l2_cache_size != 0 && !l2_cache_shared { fdt.property_u32( "next-level-cache", cpu_id as u32 + max_cpus + FIRST_VCPU_PHANDLE + L2_CACHE_PHANDLE, )?; let l2_cache_name = "l2-cache0"; let l2_cache_node = fdt.begin_node(l2_cache_name)?; // PHANDLE is used to mark device node, and PHANDLE is unique. To avoid phandle // conflicts with other device nodes, consider the previous CPU PHANDLE, so the // CPU L2 cache PHANDLE must start from the largest CPU PHANDLE plus 1. fdt.property_u32( "phandle", cpu_id as u32 + max_cpus + FIRST_VCPU_PHANDLE + L2_CACHE_PHANDLE, )?; fdt.property_string("compatible", "cache")?; fdt.property_u32("cache-size", l2_cache_size)?; fdt.property_u32("cache-line-size", l2_cache_line_size)?; fdt.property_u32("cache-sets", l2_cache_sets)?; fdt.property_u32("cache-level", 2)?; if l3_cache_size != 0 && l3_cache_shared { let package_id: u32 = cpu_id as u32 / cores_per_package as u32; fdt.property_u32( "next-level-cache", package_id + num_cpus as u32 + max_cpus + FIRST_VCPU_PHANDLE + L2_CACHE_PHANDLE + L3_CACHE_PHANDLE, )?; } fdt.end_node(l2_cache_node)?; } } fdt.end_node(cpu_node)?; } if cache_exist && l3_cache_size != 0 && !l2_cache_shared && l3_cache_shared { let mut i: u32 = 0; while i < packages.into() { let l3_cache_name = "l3-cache0"; let l3_cache_node = fdt.begin_node(l3_cache_name)?; // ARM L3 cache is generally shared within the package (socket), so the // L3 cache node pointed to by the CPU in the package has the same L3 // cache PHANDLE. The L3 cache phandle must start from the largest L2 // cache PHANDLE plus 1 to avoid duplication. fdt.property_u32( "phandle", i + num_cpus as u32 + max_cpus + FIRST_VCPU_PHANDLE + L2_CACHE_PHANDLE + L3_CACHE_PHANDLE, )?; fdt.property_string("compatible", "cache")?; fdt.property_null("cache-unified")?; fdt.property_u32("cache-size", l3_cache_size)?; fdt.property_u32("cache-line-size", l3_cache_line_size)?; fdt.property_u32("cache-sets", l3_cache_sets)?; fdt.property_u32("cache-level", 3)?; fdt.end_node(l3_cache_node)?; i += 1; } } if let Some(topology) = vcpu_topology { let (threads_per_core, cores_per_die, dies_per_package, packages) = topology; let cores_per_package = cores_per_die * dies_per_package; let cpu_map_node = fdt.begin_node("cpu-map")?; // Create device tree nodes with regard of above mapping. for package_idx in 0..packages { let package_name = format!("socket{package_idx:x}"); let package_node = fdt.begin_node(&package_name)?; // Cluster is the container of cores, and it is mandatory in the CPU topology. // Add a default "cluster0" in each socket/package. let cluster_node = fdt.begin_node("cluster0")?; for core_idx in 0..cores_per_package { let core_name = format!("core{core_idx:x}"); let core_node = fdt.begin_node(&core_name)?; for thread_idx in 0..threads_per_core { let thread_name = format!("thread{thread_idx:x}"); let thread_node = fdt.begin_node(&thread_name)?; let cpu_idx = threads_per_core * cores_per_package * package_idx + threads_per_core * core_idx + thread_idx; fdt.property_u32("cpu", cpu_idx as u32 + FIRST_VCPU_PHANDLE)?; fdt.end_node(thread_node)?; } fdt.end_node(core_node)?; } fdt.end_node(cluster_node)?; fdt.end_node(package_node)?; } fdt.end_node(cpu_map_node)?; } else { debug!("Boot using device tree, CPU topology is not (correctly) specified"); } fdt.end_node(cpus_node)?; Ok(()) } fn create_memory_node( fdt: &mut FdtWriter, guest_mem: &GuestMemoryMmap, numa_nodes: &NumaNodes, ) -> FdtWriterResult<()> { // See https://github.com/torvalds/linux/blob/58ae0b51506802713aa0e9956d1853ba4c722c98/Documentation/devicetree/bindings/numa.txt // for NUMA setting in memory node. let has_generic_initiator = numa_nodes.values().any(|node| node.device_id.is_some()); if has_generic_initiator { info!("Skipping NUMA memory node encoding in FDT with Generic Initiator devices"); } // Skipping NUMA encoding in FDT when Generic Initiator devices // are present allowed guest kernels to boot and // rely solely on ACPI tables to setup NUMA if numa_nodes.len() > 1 && !has_generic_initiator { 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(); let mut node_memory_addr: u64 = 0; // Each memory zone of numa will have its own memory node, but // different numa nodes should not share same memory zones. for memory_region in numa_node.unwrap().memory_regions.iter() { let memory_region_start_addr: u64 = memory_region.start_addr().raw_value(); let memory_region_size: u64 = memory_region.size() as u64; mem_reg_prop.push(memory_region_start_addr); mem_reg_prop.push(memory_region_size); // Set the node address the first non-zero region address if node_memory_addr == 0 { node_memory_addr = memory_region_start_addr; } } // 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. let ram_regions = { let mut ram_regions = Vec::new(); let mut current_start = guest_mem .iter() .next() .map(GuestMemoryRegion::start_addr) .expect("GuestMemory must have one memory region at least") .raw_value(); let mut current_end = current_start; for (start, size) in guest_mem .iter() .map(|m| (m.start_addr().raw_value(), m.len())) { if current_end == start { // This zone is continuous with the previous one. current_end += size; } else { ram_regions.push((current_start, current_end)); current_start = start; current_end = start + size; } } ram_regions.push((current_start, current_end)); ram_regions }; if ram_regions.len() > 2 { panic!( "There should be up to two non-continuous regions, divided by the gap at the end of 32bit address space." ); } // Create the memory node for memory region before the gap { let (first_region_start, first_region_end) = ram_regions .first() .expect("There should be at last one memory region"); let ram_start = super::layout::RAM_START.raw_value(); let mem_32bit_reserved_start = super::layout::MEM_32BIT_RESERVED_START.raw_value(); if !((first_region_start <= &ram_start) && (first_region_end > &ram_start) && (first_region_end <= &mem_32bit_reserved_start)) { panic!( "Unexpected first memory region layout: (start: 0x{first_region_start:08x}, end: 0x{first_region_end:08x}). ram_start: 0x{ram_start:08x}, mem_32bit_reserved_start: 0x{mem_32bit_reserved_start:08x}" ); } let mem_size = first_region_end - ram_start; let mem_reg_prop = [ram_start, mem_size]; let memory_node_name = format!("memory@{ram_start:x}"); let memory_node = fdt.begin_node(&memory_node_name)?; fdt.property_string("device_type", "memory")?; fdt.property_array_u64("reg", &mem_reg_prop)?; fdt.end_node(memory_node)?; } // Create the memory map entry for memory region after the gap if any if let Some((second_region_start, second_region_end)) = ram_regions.get(1) { let ram_64bit_start = super::layout::RAM_64BIT_START.raw_value(); if second_region_start != &ram_64bit_start { panic!( "Unexpected second memory region layout: start: 0x{second_region_start:08x}, ram_64bit_start: 0x{ram_64bit_start:08x}" ); } let mem_size = second_region_end - ram_64bit_start; let mem_reg_prop = [ram_64bit_start, mem_size]; let memory_node_name = format!("memory@{ram_64bit_start:x}"); let memory_node = fdt.begin_node(&memory_node_name)?; fdt.property_string("device_type", "memory")?; fdt.property_array_u64("reg", &mem_reg_prop)?; fdt.end_node(memory_node)?; } } Ok(()) } fn create_chosen_node( fdt: &mut FdtWriter, cmdline: &str, initrd: &Option, ) -> FdtWriterResult<()> { let chosen_node = fdt.begin_node("chosen")?; fdt.property_string("bootargs", cmdline)?; if let Some(initrd_config) = initrd { let initrd_start = initrd_config.address.raw_value(); let initrd_end = initrd_config.address.raw_value() + initrd_config.size as u64; fdt.property_u64("linux,initrd-start", initrd_start)?; fdt.property_u64("linux,initrd-end", initrd_end)?; } fdt.end_node(chosen_node)?; Ok(()) } fn create_gic_node(fdt: &mut FdtWriter, gic_device: &Arc>) -> FdtWriterResult<()> { let gic_reg_prop = gic_device.lock().unwrap().device_properties(); let intc_node = fdt.begin_node("intc")?; fdt.property_string("compatible", gic_device.lock().unwrap().fdt_compatibility())?; fdt.property_null("interrupt-controller")?; // "interrupt-cells" field specifies the number of cells needed to encode an // interrupt source. The type shall be a and the value shall be 3 if no PPI affinity description // is required. fdt.property_u32("#interrupt-cells", 3)?; fdt.property_array_u64("reg", &gic_reg_prop)?; fdt.property_u32("phandle", GIC_PHANDLE)?; fdt.property_u32("#address-cells", 2)?; fdt.property_u32("#size-cells", 2)?; fdt.property_null("ranges")?; let gic_intr_prop = [ GIC_FDT_IRQ_TYPE_PPI, gic_device.lock().unwrap().fdt_maint_irq(), IRQ_TYPE_LEVEL_HI, ]; fdt.property_array_u32("interrupts", &gic_intr_prop)?; if gic_device.lock().unwrap().msi_compatible() { let msic_node = fdt.begin_node("msic")?; let msi_compatibility = gic_device.lock().unwrap().msi_compatibility().to_string(); fdt.property_string("compatible", msi_compatibility.as_str())?; fdt.property_null("msi-controller")?; fdt.property_u32("phandle", MSI_PHANDLE)?; let msi_reg_prop = gic_device.lock().unwrap().msi_properties(); fdt.property_array_u64("reg", &msi_reg_prop)?; if msi_compatibility == GIC_V2M_COMPATIBLE { fdt.property_u32("arm,msi-base-spi", GICV2M_SPI_BASE)?; fdt.property_u32("arm,msi-num-spis", GICV2M_SPI_NUM)?; } fdt.end_node(msic_node)?; } fdt.end_node(intc_node)?; Ok(()) } fn create_clock_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> { // The Advanced Peripheral Bus (APB) is part of the Advanced Microcontroller Bus Architecture // (AMBA) protocol family. It defines a low-cost interface that is optimized for minimal power // consumption and reduced interface complexity. // PCLK is the clock source and this node defines exactly the clock for the APB. let clock_node = fdt.begin_node("apb-pclk")?; fdt.property_string("compatible", "fixed-clock")?; fdt.property_u32("#clock-cells", 0x0)?; fdt.property_u32("clock-frequency", 24000000)?; fdt.property_string("clock-output-names", "clk24mhz")?; fdt.property_u32("phandle", CLOCK_PHANDLE)?; fdt.end_node(clock_node)?; Ok(()) } fn create_timer_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> { // See // https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/timer/arm%2Carch_timer.yaml // These are fixed interrupt numbers for the timer device. let irqs = [ AARCH64_ARCH_TIMER_PHYS_SECURE_IRQ, AARCH64_ARCH_TIMER_PHYS_NONSECURE_IRQ, AARCH64_ARCH_TIMER_VIRT_IRQ, AARCH64_ARCH_TIMER_HYP_IRQ, ]; let compatible = "arm,armv8-timer"; let mut timer_reg_cells: Vec = Vec::new(); for &irq in irqs.iter() { timer_reg_cells.push(GIC_FDT_IRQ_TYPE_PPI); timer_reg_cells.push(irq); timer_reg_cells.push(IRQ_TYPE_LEVEL_HI); } let timer_node = fdt.begin_node("timer")?; fdt.property_string("compatible", compatible)?; fdt.property_null("always-on")?; fdt.property_array_u32("interrupts", &timer_reg_cells)?; fdt.end_node(timer_node)?; Ok(()) } fn create_psci_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> { let compatible = "arm,psci-0.2"; let psci_node = fdt.begin_node("psci")?; fdt.property_string("compatible", compatible)?; // Two methods available: hvc and smc. // As per documentation, PSCI calls between a guest and hypervisor may use the HVC conduit instead of SMC. // So, since we are using kvm, we need to use hvc. fdt.property_string("method", "hvc")?; fdt.end_node(psci_node)?; Ok(()) } fn create_virtio_node( fdt: &mut FdtWriter, dev_info: &T, ) -> FdtWriterResult<()> { let device_reg_prop = [dev_info.addr(), dev_info.length()]; let irq = [GIC_FDT_IRQ_TYPE_SPI, dev_info.irq(), IRQ_TYPE_EDGE_RISING]; let virtio_node = fdt.begin_node(&format!("virtio_mmio@{:x}", dev_info.addr()))?; fdt.property_string("compatible", "virtio,mmio")?; fdt.property_array_u64("reg", &device_reg_prop)?; fdt.property_array_u32("interrupts", &irq)?; fdt.property_u32("interrupt-parent", GIC_PHANDLE)?; fdt.end_node(virtio_node)?; Ok(()) } fn create_serial_node( fdt: &mut FdtWriter, dev_info: &T, ) -> FdtWriterResult<()> { let compatible = b"arm,pl011\0arm,primecell\0"; let serial_reg_prop = [dev_info.addr(), dev_info.length()]; let irq = [ GIC_FDT_IRQ_TYPE_SPI, dev_info.irq() - IRQ_BASE, IRQ_TYPE_EDGE_RISING, ]; let serial_node = fdt.begin_node(&format!("pl011@{:x}", dev_info.addr()))?; fdt.property("compatible", compatible)?; fdt.property_array_u64("reg", &serial_reg_prop)?; fdt.property_u32("clocks", CLOCK_PHANDLE)?; fdt.property_string("clock-names", "apb_pclk")?; fdt.property_array_u32("interrupts", &irq)?; fdt.end_node(serial_node)?; Ok(()) } fn create_rtc_node( fdt: &mut FdtWriter, dev_info: &T, ) -> FdtWriterResult<()> { let compatible = b"arm,pl031\0arm,primecell\0"; let rtc_reg_prop = [dev_info.addr(), dev_info.length()]; let irq = [ GIC_FDT_IRQ_TYPE_SPI, dev_info.irq() - IRQ_BASE, IRQ_TYPE_LEVEL_HI, ]; let rtc_node = fdt.begin_node(&format!("rtc@{:x}", dev_info.addr()))?; fdt.property("compatible", compatible)?; fdt.property_array_u64("reg", &rtc_reg_prop)?; fdt.property_array_u32("interrupts", &irq)?; fdt.property_u32("clocks", CLOCK_PHANDLE)?; fdt.property_string("clock-names", "apb_pclk")?; fdt.end_node(rtc_node)?; Ok(()) } fn create_gpio_node( fdt: &mut FdtWriter, dev_info: &T, ) -> FdtWriterResult<()> { // PL061 GPIO controller node let compatible = b"arm,pl061\0arm,primecell\0"; let gpio_reg_prop = [dev_info.addr(), dev_info.length()]; let irq = [ GIC_FDT_IRQ_TYPE_SPI, dev_info.irq() - IRQ_BASE, IRQ_TYPE_EDGE_RISING, ]; let gpio_node = fdt.begin_node(&format!("pl061@{:x}", dev_info.addr()))?; fdt.property("compatible", compatible)?; fdt.property_array_u64("reg", &gpio_reg_prop)?; fdt.property_array_u32("interrupts", &irq)?; fdt.property_null("gpio-controller")?; fdt.property_u32("#gpio-cells", 2)?; fdt.property_u32("clocks", CLOCK_PHANDLE)?; fdt.property_string("clock-names", "apb_pclk")?; fdt.property_u32("phandle", GPIO_PHANDLE)?; fdt.end_node(gpio_node)?; // gpio-keys node let gpio_keys_node = fdt.begin_node("gpio-keys")?; fdt.property_string("compatible", "gpio-keys")?; fdt.property_u32("#size-cells", 0)?; fdt.property_u32("#address-cells", 1)?; let gpio_keys_poweroff_node = fdt.begin_node("button@1")?; fdt.property_string("label", "GPIO Key Poweroff")?; fdt.property_u32("linux,code", KEY_POWER)?; let gpios = [GPIO_PHANDLE, 3, 0]; fdt.property_array_u32("gpios", &gpios)?; fdt.end_node(gpio_keys_poweroff_node)?; fdt.end_node(gpio_keys_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>, ) -> FdtWriterResult<()> { // Create one temp Vec to store all virtio devices let mut ordered_virtio_device: Vec<&T> = Vec::new(); for ((device_type, _device_id), info) in dev_info { match device_type { DeviceType::Gpio => create_gpio_node(fdt, info)?, DeviceType::Rtc => create_rtc_node(fdt, info)?, DeviceType::Serial => create_serial_node(fdt, info)?, DeviceType::Virtio(_) => { ordered_virtio_device.push(info); } #[cfg(feature = "fw_cfg")] DeviceType::FwCfg => create_fw_cfg_node(fdt, info)?, } } // Sort out virtio devices by address from low to high and insert them into fdt table. ordered_virtio_device.sort_by_key(|&a| a.addr()); // Current address allocation strategy in cloud-hypervisor is: the first created device // will be allocated to higher address. Here we reverse the vector to make sure that // the older created device will appear in front of the newer created device in FDT. ordered_virtio_device.reverse(); for ordered_device_info in ordered_virtio_device.drain(..) { create_virtio_node(fdt, ordered_device_info)?; } Ok(()) } fn create_pmu_node(fdt: &mut FdtWriter) -> FdtWriterResult<()> { let compatible = "arm,armv8-pmuv3"; let irq = [GIC_FDT_IRQ_TYPE_PPI, AARCH64_PMU_IRQ, IRQ_TYPE_LEVEL_HI]; let pmu_node = fdt.begin_node("pmu")?; fdt.property_string("compatible", compatible)?; fdt.property_array_u32("interrupts", &irq)?; fdt.end_node(pmu_node)?; Ok(()) } fn create_pci_nodes( fdt: &mut FdtWriter, pci_device_info: &[PciSpaceInfo], virtio_iommu_bdf: Option, ) -> FdtWriterResult<()> { // Add node for PCIe controller. // See Documentation/devicetree/bindings/pci/host-generic-pci.txt in the kernel // and https://elinux.org/Device_Tree_Usage. // In multiple PCI segments setup, each PCI segment needs a PCI node. for pci_device_info_elem in pci_device_info.iter() { // EDK2 requires the PCIe high space above 4G address. // The actual space in CLH follows the RAM. If the RAM space is small, the PCIe high space // could fall below 4G. // Here we cut off PCI device space below 8G in FDT to workaround the EDK2 check. // But the address written in ACPI is not impacted. let (pci_device_base_64bit, pci_device_size_64bit) = if pci_device_info_elem.pci_device_space_start < PCI_HIGH_BASE.raw_value() { ( PCI_HIGH_BASE.raw_value(), pci_device_info_elem.pci_device_space_size - (PCI_HIGH_BASE.raw_value() - pci_device_info_elem.pci_device_space_start), ) } else { ( pci_device_info_elem.pci_device_space_start, pci_device_info_elem.pci_device_space_size, ) }; // There is no specific requirement of the 32bit MMIO range, and // therefore at least we can make these ranges 4K aligned. let pci_device_size_32bit: u64 = MEM_32BIT_DEVICES_SIZE / ((1 << 12) * pci_device_info.len() as u64) * (1 << 12); let pci_device_base_32bit: u64 = MEM_32BIT_DEVICES_START.0 + pci_device_size_32bit * pci_device_info_elem.pci_segment_id as u64; let ranges = [ // io addresses. Since AArch64 will not use IO address, // we can set the same IO address range for every segment. 0x1000000, 0_u32, 0_u32, (MEM_PCI_IO_START.0 >> 32) as u32, MEM_PCI_IO_START.0 as u32, (MEM_PCI_IO_SIZE >> 32) as u32, MEM_PCI_IO_SIZE as u32, // mmio addresses 0x2000000, // (ss = 10: 32-bit memory space) (pci_device_base_32bit >> 32) as u32, // PCI address pci_device_base_32bit as u32, (pci_device_base_32bit >> 32) as u32, // CPU address pci_device_base_32bit as u32, (pci_device_size_32bit >> 32) as u32, // size pci_device_size_32bit as u32, // device addresses 0x3000000, // (ss = 11: 64-bit memory space) (pci_device_base_64bit >> 32) as u32, // PCI address pci_device_base_64bit as u32, (pci_device_base_64bit >> 32) as u32, // CPU address pci_device_base_64bit as u32, (pci_device_size_64bit >> 32) as u32, // size pci_device_size_64bit as u32, ]; let bus_range = [0, 0]; // Only bus 0 let reg = [ pci_device_info_elem.mmio_config_address, PCI_MMIO_CONFIG_SIZE_PER_SEGMENT, ]; // See kernel document Documentation/devicetree/bindings/pci/pci-msi.txt let msi_map = [ // rid-base: A single cell describing the first RID matched by the entry. 0x0, // msi-controller: A single phandle to an MSI controller. MSI_PHANDLE, // msi-base: An msi-specifier describing the msi-specifier produced for the // first RID matched by the entry. (pci_device_info_elem.pci_segment_id as u32) << 8, // length: A single cell describing how many consecutive RIDs are matched // following the rid-base. 0x100, ]; let pci_node_name = format!("pci@{:x}", pci_device_info_elem.mmio_config_address); let pci_node = fdt.begin_node(&pci_node_name)?; fdt.property_string("compatible", "pci-host-ecam-generic")?; fdt.property_string("device_type", "pci")?; fdt.property_array_u32("ranges", &ranges)?; fdt.property_array_u32("bus-range", &bus_range)?; fdt.property_u32( "linux,pci-domain", pci_device_info_elem.pci_segment_id as u32, )?; fdt.property_u32("#address-cells", 3)?; fdt.property_u32("#size-cells", 2)?; fdt.property_array_u64("reg", ®)?; fdt.property_u32("#interrupt-cells", 1)?; fdt.property_null("interrupt-map")?; fdt.property_null("interrupt-map-mask")?; fdt.property_null("dma-coherent")?; fdt.property_array_u32("msi-map", &msi_map)?; fdt.property_u32("msi-parent", MSI_PHANDLE)?; if pci_device_info_elem.pci_segment_id == 0 && let Some(virtio_iommu_bdf) = virtio_iommu_bdf { // See kernel document Documentation/devicetree/bindings/pci/pci-iommu.txt // for 'iommu-map' attribute setting. let iommu_map = [ 0_u32, VIRTIO_IOMMU_PHANDLE, 0_u32, virtio_iommu_bdf, virtio_iommu_bdf + 1, VIRTIO_IOMMU_PHANDLE, virtio_iommu_bdf + 1, 0xffff - virtio_iommu_bdf, ]; fdt.property_array_u32("iommu-map", &iommu_map)?; // 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)?; } Ok(()) } 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. // 1. We use the word entry to describe a distance from a node to // its destination, e.g. 0 -> 1 = 20 is described as <0 1 20>. // 2. Each entry represents distance from first node to second node. // The distances are equal in either direction. // 3. The distance from a node to self (local distance) is represented // with value 10 and all internode distance should be represented with // a value greater than 10. // 4. distance-matrix should have entries in lexicographical ascending // order of nodes. numa_ids.sort_unstable(); // lexicographical order let mut distance_matrix = Vec::new(); // Iterate over actual numa IDs instead of 0..len() for numa_id in numa_ids.iter() { let numa_node = &numa_nodes[numa_id]; for dest_numa_id in numa_ids.iter() { if *numa_id == *dest_numa_id { distance_matrix.push(*numa_id); distance_matrix.push(*dest_numa_id); distance_matrix.push(10_u32); continue; } distance_matrix.push(*numa_id); distance_matrix.push(*dest_numa_id); // Use user-specified distance, checking both directions for symmetry let distance = if let Some(&dist) = numa_node.distances.get(dest_numa_id) { // Forward direction: current node -> dest node dist } else if let Some(dest_node) = numa_nodes.get(dest_numa_id) { // Reverse direction for symmetry: dest node -> current node dest_node.distances.get(numa_id).copied().unwrap_or(20) } else { // Default distance when neither direction is specified 20 }; distance_matrix.push(distance as u32); } } fdt.property_array_u32("distance-matrix", distance_matrix.as_ref())?; fdt.end_node(distance_map_node)?; Ok(()) } // Parse the DTB binary and print for debugging pub fn print_fdt(dtb: &[u8]) { match fdt_parser::Fdt::new(dtb) { Ok(fdt) => { if let Some(root) = fdt.find_node("/") { debug!("Printing the FDT:"); print_node(root, 0); } else { debug!("Failed to find root node in FDT for debugging."); } } Err(_) => debug!("Failed to parse FDT for debugging."), } } fn print_node(node: FdtNode<'_, '_>, n_spaces: usize) { debug!("{:indent$}{}/", "", node.name, indent = n_spaces); for property in node.properties() { let name = property.name; // If the property is 'compatible', its value requires special handling. // The u8 array could contain multiple null-terminated strings. // We copy the original array and simply replace all 'null' characters with spaces. let value = if name == "compatible" { let mut compatible = vec![0u8; 256]; let handled_value = property .value .iter() .map(|&c| if c == 0 { b' ' } else { c }) .collect::>(); let len = cmp::min(255, handled_value.len()); compatible[..len].copy_from_slice(&handled_value[..len]); compatible[..(len + 1)].to_vec() } else { property.value.to_vec() }; let value = &value; // Now the value can be either: // - A null-terminated C string, or // - Binary data // We follow a very simple logic to present the value: // - At first, try to convert it to CStr and print, // - If failed, print it as u32 array. let value_result = match CStr::from_bytes_with_nul(value) { Ok(value_cstr) => value_cstr.to_str().ok(), Err(_e) => None, }; if let Some(value_str) = value_result { debug!( "{:indent$}{} : {:#?}", "", name, value_str, indent = (n_spaces + 2) ); } else { let mut array = Vec::with_capacity(256); array.resize(value.len() / 4, 0u32); BigEndian::read_u32_into(value, &mut array); debug!( "{:indent$}{} : {:X?}", "", name, array, indent = (n_spaces + 2) ); } } // Print children nodes if there is any for child in node.children() { print_node(child, n_spaces + 2); } } #[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"); } } cloud-hypervisor-53.0/arch/src/aarch64/layout.rs000066400000000000000000000167701522476750100216220ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Memory layout of AArch64 guest: // // Physical +---------------------------------------------------------------+ // address | | // end | | // ~ ~ ~ ~ // | | // | Highmem PCI MMIO space | // | | // RAM end +---------------------------------------------------------------+ // (dynamic, | | // including | | // hotplug ~ ~ ~ ~ // memory) | | // | DRAM | // | | // | | // 4GB +---------------------------------------------------------------+ // | 32-bit devices hole | // 4GB-64M +---------------------------------------------------------------+ // | | // | | // | DRAM | // | | // | | // 1GB +---------------------------------------------------------------+ // | | // | PCI MMCONFIG space | // | | // 768 M +---------------------------------------------------------------+ // | | // | | // | PCI MMIO space | // | | // 256 M +---------------------------------------------------------------| // | | // | Legacy devices space | // | | // 144 M +---------------------------------------------------------------| // | | // | Reserved (now GIC is here) | // | | // 4 M +---------------------------------------------------------------+ // | UEFI flash | // 0GB +---------------------------------------------------------------+ // // 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; /// Below this address will reside the GIC, above this address will reside the MMIO devices. const MAPPED_IO_START: GuestAddress = GuestAddress(0x0900_0000); /// See kernel file arch/arm64/include/uapi/asm/kvm.h for the GIC related definitions. /// 0x08ff_0000 ~ 0x0900_0000 is reserved for GICv3 Distributor pub const GIC_V3_DIST_SIZE: u64 = 0x01_0000; pub const GIC_V3_DIST_START: GuestAddress = GuestAddress(MAPPED_IO_START.0 - GIC_V3_DIST_SIZE); /// Below 0x08ff_0000 is reserved for GICv3 Redistributor. /// The size defined here is for each vcpu. /// The total size is 'number_of_vcpu * GIC_V3_REDIST_SIZE' pub const GIC_V3_REDIST_SIZE: u64 = 0x02_0000; /// Below Redistributor area is GICv3 ITS pub const GIC_V3_ITS_SIZE: u64 = 0x02_0000; /// Space 0x0900_0000 ~ 0x0905_0000 is reserved for legacy devices. pub const LEGACY_SERIAL_MAPPED_IO_START: GuestAddress = MAPPED_IO_START; pub const LEGACY_RTC_MAPPED_IO_START: GuestAddress = GuestAddress(0x0901_0000); pub const LEGACY_GPIO_MAPPED_IO_START: GuestAddress = GuestAddress(0x0902_0000); /// Space 0x0905_0000 ~ 0x0906_0000 is reserved for pcie io address pub const MEM_PCI_IO_START: GuestAddress = GuestAddress(0x0905_0000); pub const MEM_PCI_IO_SIZE: u64 = 0x10000; /// Starting from 0x1000_0000 (256MiB) to 0x3000_0000 (768MiB) is used for PCIE MMIO pub const MEM_32BIT_DEVICES_START: GuestAddress = GuestAddress(0x1000_0000); pub const MEM_32BIT_DEVICES_SIZE: u64 = 0x2000_0000; /// PCI MMCONFIG space (start: after the device space at 1 GiB, length: 256MiB) pub const PCI_MMCONFIG_START: GuestAddress = GuestAddress(0x3000_0000); pub const PCI_MMCONFIG_SIZE: u64 = 256 << 20; // One bus with potentially 256 devices (32 slots x 8 functions). pub const PCI_MMIO_CONFIG_SIZE_PER_SEGMENT: u64 = 4096 * 256; /// Start of RAM. pub const RAM_START: GuestAddress = GuestAddress(0x4000_0000); /// 32-bit reserved area: 64MiB before 4GiB pub const MEM_32BIT_RESERVED_START: GuestAddress = GuestAddress(0xfc00_0000); pub const MEM_32BIT_RESERVED_SIZE: u64 = 0x0400_0000; /// TPM Address Range /// This Address range is specific to CRB Interface pub const TPM_START: GuestAddress = GuestAddress(0xfed4_0000); pub const TPM_SIZE: u64 = 0x1000; /// Start of 64-bit RAM. pub const RAM_64BIT_START: GuestAddress = GuestAddress(0x1_0000_0000); /// Kernel command line maximum size. /// As per `arch/arm64/include/uapi/asm/setup.h`. pub const CMDLINE_MAX_SIZE: usize = 2048; /// FDT is at the beginning of RAM. pub const FDT_START: GuestAddress = RAM_START; /// Maximum size of the device tree blob as specified in [the kernel /// documentation](https://www.kernel.org/doc/Documentation/arm64/booting.txt). pub const FDT_MAX_SIZE: u64 = 0x20_0000; /// 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(ACPI_START.0 + ACPI_MAX_SIZE); /// Pci high memory base pub const PCI_HIGH_BASE: GuestAddress = GuestAddress(0x2_0000_0000); // As per virt/kvm/arm/vgic/vgic-kvm-device.c we need // the number of interrupts our GIC will support to be: // * bigger than 32 // * less than 1023 and // * a multiple of 32. // We are setting up our interrupt controller to support a maximum of 256 interrupts. /// First usable interrupt on aarch64 pub const IRQ_BASE: u32 = 32; /// Number of supported interrupts pub const IRQ_NUM: u32 = 256; /// Base SPI interrupt number for the GICv2M MSI frame pub const GICV2M_SPI_BASE: u32 = 128; /// Total number of SPIs for the GICv2M MSI frame pub const GICV2M_SPI_NUM: u32 = 64; /// GICv2M compatible string pub const GIC_V2M_COMPATIBLE: &str = "arm,gic-v2m-frame"; cloud-hypervisor-53.0/arch/src/aarch64/mod.rs000066400000000000000000000145741522476750100210640ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 /// Module for cache info. pub mod cache; /// Module for the flattened device tree. pub mod fdt; /// Layout for this aarch64 system. pub mod layout; /// Module for loading UEFI binary. pub mod uefi; use std::collections::HashMap; use std::fmt::Debug; use std::hash::BuildHasher; use std::sync::{Arc, Mutex}; use hypervisor::arch::aarch64::gic::Vgic; use hypervisor::arch::aarch64::regs::MPIDR_EL1; use log::{Level, log_enabled}; use thiserror::Error; use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic}; pub use self::fdt::DeviceInfoForFdt; use crate::{DeviceType, GuestMemoryMmap, NumaNodes, PciSpaceInfo, RegionType}; pub const _NSIG: i32 = 65; /// Errors thrown while configuring aarch64 system. #[derive(Debug, Error)] pub enum Error { /// Failed to create a FDT. #[error("Failed to create a FDT")] SetupFdt, /// Failed to write FDT to memory. #[error("Failed to write FDT to memory")] WriteFdtToMemory(#[source] fdt::Error), /// Failed to create a GIC. #[error("Failed to create a GIC")] SetupGic, /// Failed to compute the initramfs address. #[error("Failed to compute the initramfs address")] InitramfsAddress, /// Error configuring the general purpose registers #[error("Error configuring the general purpose registers")] RegsConfiguration(#[source] hypervisor::HypervisorCpuError), /// Error configuring the MPIDR register #[error("Error configuring the MPIDR register")] VcpuRegMpidr(#[source] hypervisor::HypervisorCpuError), /// Error initializing PMU for vcpu #[error("Error initializing PMU for vcpu")] VcpuInitPmu, } #[derive(Debug, Copy, Clone)] /// Specifies the entry point address where the guest must start /// executing code. pub struct EntryPoint { /// Address in guest memory where the guest must start execution pub entry_addr: GuestAddress, } /// Configure the specified VCPU, and return its MPIDR. pub fn configure_vcpu( vcpu: &dyn hypervisor::Vcpu, id: u32, boot_setup: Option<(EntryPoint, &GuestMemoryAtomic)>, ) -> super::Result { if let Some((kernel_entry_point, _guest_memory)) = boot_setup { vcpu.setup_regs( id, kernel_entry_point.entry_addr.raw_value(), super::layout::FDT_START.raw_value(), ) .map_err(Error::RegsConfiguration)?; } let mpidr = vcpu.get_sys_reg(MPIDR_EL1).map_err(Error::VcpuRegMpidr)?; Ok(mpidr) } pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> { vec![ // 0 MiB ~ 256 MiB: UEFI, GIC and legacy devices ( GuestAddress(0), layout::MEM_32BIT_DEVICES_START.0 as usize, RegionType::Reserved, ), // 256 MiB ~ 768 MiB: MMIO space ( layout::MEM_32BIT_DEVICES_START, layout::MEM_32BIT_DEVICES_SIZE as usize, RegionType::SubRegion, ), // 768 MiB ~ 1 GiB: reserved. The leading 256M for PCIe MMCONFIG space ( layout::PCI_MMCONFIG_START, layout::PCI_MMCONFIG_SIZE as usize, RegionType::Reserved, ), // 1GiB ~ 4032 MiB: RAM before the gap ( layout::RAM_START, layout::MEM_32BIT_RESERVED_START.unchecked_offset_from(layout::RAM_START) as usize, RegionType::Ram, ), // 4GiB ~ inf: RAM after the gap (layout::RAM_64BIT_START, usize::MAX, RegionType::Ram), // Add the 32-bit reserved memory hole as a reserved region ( layout::MEM_32BIT_RESERVED_START, layout::MEM_32BIT_RESERVED_SIZE as usize, RegionType::Reserved, ), ] } /// Configures the system and should be called once per vm before starting vcpu threads. #[expect(clippy::too_many_arguments)] pub fn configure_system( guest_mem: &GuestMemoryMmap, cmdline: &str, vcpu_mpidr: &[u64], vcpu_topology: Option<(u16, u16, u16, u16)>, device_info: &HashMap<(DeviceType, String), T, S>, initrd: &Option, pci_space_info: &[PciSpaceInfo], virtio_iommu_bdf: Option, gic_device: &Arc>, numa_nodes: &NumaNodes, pmu_supported: bool, ) -> super::Result<()> { let fdt_final = fdt::create_fdt( guest_mem, cmdline, vcpu_mpidr, vcpu_topology, device_info, gic_device, initrd, pci_space_info, numa_nodes, virtio_iommu_bdf, pmu_supported, ) .map_err(|_| Error::SetupFdt)?; if log_enabled!(Level::Debug) { fdt::print_fdt(&fdt_final); } fdt::write_fdt_to_memory(&fdt_final, guest_mem).map_err(Error::WriteFdtToMemory)?; Ok(()) } /// Returns the memory address where the initramfs could be loaded. pub fn initramfs_load_addr( guest_mem: &GuestMemoryMmap, initramfs_size: usize, ) -> super::Result { let round_to_pagesize = |size| (size + (super::PAGE_SIZE - 1)) & !(super::PAGE_SIZE - 1); match guest_mem .last_addr() .checked_sub(round_to_pagesize(initramfs_size) as u64 - 1) { Some(offset) => { if guest_mem.address_in_range(offset) { Ok(offset.raw_value()) } else { Err(super::Error::PlatformSpecific(Error::InitramfsAddress)) } } None => Err(super::Error::PlatformSpecific(Error::InitramfsAddress)), } } pub fn get_host_cpu_phys_bits(hypervisor: &dyn hypervisor::Hypervisor) -> u8 { 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`, // we return the default value 40 here. 40 } else { host_cpu_phys_bits } } #[cfg(test)] mod unit_tests { use super::*; #[test] fn test_arch_memory_regions_dram() { let regions = arch_memory_regions(); assert_eq!(6, regions.len()); assert_eq!(layout::RAM_START, regions[3].0); assert_eq!(RegionType::Ram, regions[3].2); assert_eq!(RegionType::Reserved, regions[5].2); assert_eq!(RegionType::Ram, regions[4].2); } } cloud-hypervisor-53.0/arch/src/aarch64/uefi.rs000066400000000000000000000025741522476750100212320ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // // SPDX-License-Identifier: Apache-2.0 use std::io::{Read, Seek, SeekFrom}; use std::os::fd::AsFd; use std::result; use thiserror::Error; use vm_memory::{Bytes, GuestAddress, GuestMemory}; /// Errors thrown while loading UEFI binary #[derive(Debug, Error)] pub enum Error { /// Unable to seek to UEFI image start. #[error("Unable to seek to UEFI image start")] SeekUefiStart, /// Unable to seek to UEFI image end. #[error("Unable to seek to UEFI image end")] SeekUefiEnd, /// UEFI image too big. #[error("UEFI image too big")] UefiTooBig, /// Unable to read UEFI image #[error("Unable to read UEFI image")] ReadUefiImage, } type Result = 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) } cloud-hypervisor-53.0/arch/src/lib.rs000066400000000000000000000161701522476750100176150ustar00rootroot00000000000000// Copyright © 2024 Institute of Software, CAS. All rights reserved. // Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright © 2020, Oracle and/or its affiliates. // // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //! Implements platform specific functionality. //! Supported platforms: x86_64, aarch64, riscv64. use std::collections::BTreeMap; use std::str::FromStr; use std::sync::Arc; use std::{fmt, result}; use serde::de::{IntoDeserializer, value}; use serde::{Deserialize, Serialize}; use thiserror::Error; use vm_memory::bitmap::AtomicBitmap; type GuestMemoryMmap = vm_memory::GuestMemoryMmap; type GuestRegionMmap = vm_memory::GuestRegionMmap; /// Type for returning error code. #[derive(Debug, Error)] pub enum Error { #[cfg(target_arch = "x86_64")] #[error("Platform specific error (x86_64)")] PlatformSpecific(#[from] x86_64::Error), #[cfg(target_arch = "aarch64")] #[error("Platform specific error (aarch64)")] PlatformSpecific(#[from] aarch64::Error), #[cfg(target_arch = "riscv64")] #[error("Platform specific error (riscv64)")] PlatformSpecific(#[from] riscv64::Error), #[error("The memory map table extends past the end of guest memory")] MemmapTablePastRamEnd, #[error("Error writing memory map table to guest memory")] MemmapTableSetup(#[source] vm_memory::GuestMemoryError), #[error("Error generating memory map table")] MemmapTableGeneration, #[error("The hvm_start_info structure extends past the end of guest memory")] StartInfoPastRamEnd, #[error("Error writing hvm_start_info to guest memory")] StartInfoSetup(#[source] vm_memory::GuestMemoryError), #[error("Failed to compute initramfs address")] InitramfsAddress, #[error("Error writing module entry to guest memory")] ModlistSetup(#[source] vm_memory::GuestMemoryError), #[error("RSDP extends past the end of guest memory")] RsdpPastRamEnd, #[error("Failed to setup Zero Page for bzImage")] ZeroPageSetup(#[source] vm_memory::GuestMemoryError), #[error("Zero Page for bzImage past RAM end")] ZeroPagePastRamEnd, } /// 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 with only "host" as a selectable CPU profile. This trick is useful to prevent // excessive conditional compilation throughout the codebase. #[cfg(not(target_arch = "x86_64"))] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] /// A [`CpuProfile`] is a mechanism for ensuring live migration compatibility /// between host's with potentially different CPU models. pub enum CpuProfile { #[default] Host, } // Note that this trait impl is architecture agnostic and may thus reside here. impl FromStr for CpuProfile { type Err = value::Error; fn from_str(s: &str) -> result::Result { Self::deserialize(s.into_deserializer()) } } /// Type for memory region types. #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum RegionType { /// RAM type Ram, /// SubRegion memory region. /// A SubRegion is a memory region sub-region, allowing for a region /// to be split into sub regions managed separately. /// For example, the x86 32-bit memory hole is a SubRegion. SubRegion, /// Reserved type. /// A Reserved memory region is one that should not be used for memory /// allocation. This type can be used to prevent the VMM from allocating /// memory ranges in a specific address range. Reserved, } /// Module for aarch64 related functionality. #[cfg(target_arch = "aarch64")] pub mod aarch64; #[cfg(target_arch = "aarch64")] pub use aarch64::{ _NSIG, EntryPoint, arch_memory_regions, configure_system, configure_vcpu, fdt::DeviceInfoForFdt, get_host_cpu_phys_bits, initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, uefi, }; /// Module for riscv64 related functionality. #[cfg(target_arch = "riscv64")] pub mod riscv64; #[cfg(target_arch = "riscv64")] pub use riscv64::{ _NSIG, EntryPoint, arch_memory_regions, configure_system, configure_vcpu, fdt::DeviceInfoForFdt, get_host_cpu_phys_bits, initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, uefi, }; #[cfg(target_arch = "x86_64")] pub mod x86_64; #[cfg(target_arch = "x86_64")] pub use x86_64::{ _NSIG, CpuidConfig, CpuidFeatureEntry, EntryPoint, arch_memory_regions, configure_system, configure_vcpu, cpu_profile::CpuProfile, generate_common_cpuid, generate_ram_ranges, get_host_cpu_phys_bits, initramfs_load_addr, layout, layout::CMDLINE_MAX_SIZE, layout::CMDLINE_START, regs, }; /// Safe wrapper for `sysconf(_SC_PAGESIZE)`. #[cfg(target_arch = "x86_64")] #[inline(always)] fn pagesize() -> usize { // SAFETY: Trivially safe unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize } } #[derive(Clone, Default)] pub struct NumaNode { pub memory_regions: Vec>, pub hotplug_regions: Vec>, pub cpus: Vec, pub pci_segments: Vec, pub distances: BTreeMap, pub memory_zones: Vec, pub device_id: Option, } pub type NumaNodes = BTreeMap; /// Type for passing information about the initramfs in the guest memory. pub struct InitramfsConfig { /// Load address of initramfs in guest memory pub address: vm_memory::GuestAddress, /// Size of initramfs in guest memory pub size: usize, } /// Types of devices that can get attached to this platform. #[derive(Clone, Debug, PartialEq, Eq, Hash, Copy)] pub enum DeviceType { /// Device Type: Virtio. Virtio(u32), /// Device Type: Serial. #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] Serial, /// Device Type: RTC. #[cfg(target_arch = "aarch64")] Rtc, /// 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. pub const PAGE_SIZE: usize = 4096; impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{self:?}") } } /// Structure to describe MMIO device information #[derive(Clone, Debug)] #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] pub struct MmioDeviceInfo { pub addr: u64, pub len: u64, pub irq: u32, } /// Structure to describe PCI space information #[derive(Clone, Debug)] #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] pub struct PciSpaceInfo { pub pci_segment_id: u16, pub mmio_config_address: u64, pub pci_device_space_start: u64, pub pci_device_space_size: u64, } #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] impl DeviceInfoForFdt for MmioDeviceInfo { fn addr(&self) -> u64 { self.addr } fn irq(&self) -> u32 { self.irq } fn length(&self) -> u64 { self.len } } cloud-hypervisor-53.0/arch/src/riscv64/000077500000000000000000000000001522476750100177745ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/src/riscv64/fdt.rs000066400000000000000000000440511522476750100211230ustar00rootroot00000000000000// Copyright © 2024 Institute of Software, CAS. All rights reserved. // Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the THIRD-PARTY file. use std::collections::HashMap; use std::ffi::CStr; use std::fmt::Debug; use std::sync::{Arc, Mutex}; use std::{cmp, result, str}; use byteorder::{BigEndian, ByteOrder}; use hypervisor::arch::riscv64::aia::Vaia; use log::debug; use thiserror::Error; use vm_fdt::{FdtWriter, FdtWriterResult}; use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError, GuestMemoryRegion}; use super::super::{DeviceType, GuestMemoryMmap, InitramfsConfig}; use super::layout::{ IRQ_BASE, MEM_32BIT_DEVICES_SIZE, MEM_32BIT_DEVICES_START, MEM_PCI_IO_SIZE, MEM_PCI_IO_START, PCI_HIGH_BASE, PCI_MMIO_CONFIG_SIZE_PER_SEGMENT, }; use crate::PciSpaceInfo; const AIA_APLIC_PHANDLE: u32 = 1; const AIA_IMSIC_PHANDLE: u32 = 2; const CPU_INTC_BASE_PHANDLE: u32 = 3; const CPU_BASE_PHANDLE: u32 = 256 + CPU_INTC_BASE_PHANDLE; // Read the documentation specified when appending the root node to the FDT. const ADDRESS_CELLS: u32 = 0x2; const SIZE_CELLS: u32 = 0x2; // From https://elixir.bootlin.com/linux/v6.10/source/include/dt-bindings/interrupt-controller/irq.h#L14 const _IRQ_TYPE_EDGE_RISING: u32 = 1; const IRQ_TYPE_LEVEL_HI: u32 = 4; const S_MODE_EXT_IRQ: u32 = 9; /// Trait for devices to be added to the Flattened Device Tree. pub trait DeviceInfoForFdt { /// Returns the address where this device will be loaded. fn addr(&self) -> u64; /// Returns the associated interrupt for this device. fn irq(&self) -> u32; /// Returns the amount of memory that needs to be reserved for this device. fn length(&self) -> u64; } /// Errors thrown while configuring the Flattened Device Tree for riscv64. #[derive(Debug, Error)] pub enum Error { /// Failure in writing FDT in memory. #[error("Failure in writing FDT in memory")] WriteFdtToMemory(#[source] GuestMemoryError), } type Result = result::Result; /// Creates the flattened device tree for this riscv64 VM. #[expect(clippy::too_many_arguments)] pub fn create_fdt( guest_mem: &GuestMemoryMmap, cmdline: &str, num_vcpu: u32, isa_string: &str, device_info: &HashMap<(DeviceType, String), T, S>, aia_device: &Arc>, initrd: &Option, pci_space_info: &[PciSpaceInfo], timebase_frequency: u32, ) -> FdtWriterResult> { // Allocate stuff necessary for the holding the blob. let mut fdt = FdtWriter::new()?; // For an explanation why these nodes were introduced in the blob take a look at // https://github.com/devicetree-org/devicetree-specification/releases/tag/v0.4 // In chapter 3. // Header or the root node as per above mentioned documentation. let root_node = fdt.begin_node("")?; fdt.property_string("compatible", "linux,dummy-virt")?; // For info on #address-cells and size-cells resort to Table 3.1 Root Node // Properties fdt.property_u32("#address-cells", ADDRESS_CELLS)?; fdt.property_u32("#size-cells", SIZE_CELLS)?; create_cpu_nodes(&mut fdt, num_vcpu, isa_string, timebase_frequency)?; create_memory_node(&mut fdt, guest_mem)?; create_chosen_node(&mut fdt, cmdline, initrd)?; create_aia_node(&mut fdt, aia_device)?; create_devices_node(&mut fdt, device_info)?; create_pci_nodes(&mut fdt, pci_space_info)?; // End Header node. fdt.end_node(root_node)?; let fdt_final = fdt.finish()?; Ok(fdt_final) } pub fn write_fdt_to_memory(fdt_final: &[u8], guest_mem: &GuestMemoryMmap) -> Result<()> { // Write FDT to memory. guest_mem .write_slice(fdt_final, super::layout::FDT_START) .map_err(Error::WriteFdtToMemory)?; Ok(()) } // Following are the auxiliary function for creating the different nodes that we append to our FDT. fn create_cpu_nodes( fdt: &mut FdtWriter, num_cpus: u32, isa_string: &str, timebase_frequency: u32, ) -> FdtWriterResult<()> { // See https://elixir.bootlin.com/linux/v6.10/source/Documentation/devicetree/bindings/riscv/cpus.yaml let cpus = fdt.begin_node("cpus")?; // As per documentation, on RISC-V 64-bit systems value should be set to 1. fdt.property_u32("#address-cells", 0x01)?; fdt.property_u32("#size-cells", 0x0)?; fdt.property_u32("timebase-frequency", timebase_frequency)?; for cpu_index in 0..num_cpus { let cpu = fdt.begin_node(&format!("cpu@{cpu_index:x}"))?; fdt.property_string("device_type", "cpu")?; fdt.property_string("compatible", "riscv")?; fdt.property_string("mmu-type", "sv48")?; fdt.property_string("riscv,isa", isa_string)?; fdt.property_string("status", "okay")?; fdt.property_u32("reg", cpu_index)?; fdt.property_u32("phandle", CPU_BASE_PHANDLE + cpu_index)?; // interrupt controller node let intc_node = fdt.begin_node("interrupt-controller")?; fdt.property_string("compatible", "riscv,cpu-intc")?; fdt.property_u32("#interrupt-cells", 1u32)?; fdt.property_null("interrupt-controller")?; fdt.property_u32("phandle", CPU_INTC_BASE_PHANDLE + cpu_index)?; fdt.end_node(intc_node)?; fdt.end_node(cpu)?; } fdt.end_node(cpus)?; Ok(()) } fn create_memory_node(fdt: &mut FdtWriter, guest_mem: &GuestMemoryMmap) -> FdtWriterResult<()> { // Note: memory regions from "GuestMemory" are sorted and non-zero sized. let ram_regions = { let mut ram_regions = Vec::new(); let mut current_start = guest_mem .iter() .next() .map(GuestMemoryRegion::start_addr) .expect("GuestMemory must have one memory region at least") .raw_value(); let mut current_end = current_start; for (start, size) in guest_mem .iter() .map(|m| (m.start_addr().raw_value(), m.len())) { if current_end == start { // This zone is continuous with the previous one. current_end += size; } else { ram_regions.push((current_start, current_end)); current_start = start; current_end = start + size; } } ram_regions.push((current_start, current_end)); ram_regions }; let mut mem_reg_property = Vec::new(); for region in ram_regions { let mem_size = region.1 - region.0; mem_reg_property.push(region.0); mem_reg_property.push(mem_size); } let ram_start = super::layout::RAM_START.raw_value(); let memory_node_name = format!("memory@{ram_start:x}"); let memory_node = fdt.begin_node(&memory_node_name)?; fdt.property_string("device_type", "memory")?; fdt.property_array_u64("reg", &mem_reg_property)?; fdt.end_node(memory_node)?; Ok(()) } fn create_chosen_node( fdt: &mut FdtWriter, cmdline: &str, initrd: &Option, ) -> FdtWriterResult<()> { let chosen_node = fdt.begin_node("chosen")?; fdt.property_string("bootargs", cmdline)?; if let Some(initrd_config) = initrd { let initrd_start = initrd_config.address.raw_value(); let initrd_end = initrd_config.address.raw_value() + initrd_config.size as u64; fdt.property_u64("linux,initrd-start", initrd_start)?; fdt.property_u64("linux,initrd-end", initrd_end)?; } fdt.end_node(chosen_node)?; Ok(()) } fn create_aia_node(fdt: &mut FdtWriter, aia_device: &Arc>) -> FdtWriterResult<()> { // IMSIC if aia_device.lock().unwrap().msi_compatible() { use super::layout::IMSIC_START; let imsic_name = format!("imsics@{:x}", IMSIC_START.0); let imsic_node = fdt.begin_node(&imsic_name)?; fdt.property_string( "compatible", aia_device.lock().unwrap().imsic_compatibility(), )?; let imsic_reg_prop = aia_device.lock().unwrap().imsic_properties(); fdt.property_array_u32("reg", &imsic_reg_prop)?; fdt.property_u32("#interrupt-cells", 0u32)?; fdt.property_null("interrupt-controller")?; fdt.property_null("msi-controller")?; let imsic_num_ids = aia_device.lock().unwrap().imsic_num_ids(); fdt.property_u32("riscv,num-ids", imsic_num_ids)?; fdt.property_u32("phandle", AIA_IMSIC_PHANDLE)?; let mut irq_cells = Vec::new(); let num_cpus = aia_device.lock().unwrap().vcpu_count(); for i in 0..num_cpus { irq_cells.push(CPU_INTC_BASE_PHANDLE + i); irq_cells.push(S_MODE_EXT_IRQ); } fdt.property_array_u32("interrupts-extended", &irq_cells)?; fdt.end_node(imsic_node)?; } // APLIC use super::layout::APLIC_START; let aplic_name = format!("aplic@{:x}", APLIC_START.0); let aplic_node = fdt.begin_node(&aplic_name)?; fdt.property_string( "compatible", aia_device.lock().unwrap().aplic_compatibility(), )?; let reg_cells = aia_device.lock().unwrap().aplic_properties(); fdt.property_array_u32("reg", ®_cells)?; fdt.property_u32("#interrupt-cells", 2u32)?; fdt.property_null("interrupt-controller")?; // TODO complete num-srcs fdt.property_u32("riscv,num-sources", 96u32)?; fdt.property_u32("phandle", AIA_APLIC_PHANDLE)?; fdt.property_u32("msi-parent", AIA_IMSIC_PHANDLE)?; fdt.end_node(aplic_node)?; Ok(()) } fn create_serial_node( fdt: &mut FdtWriter, dev_info: &T, ) -> FdtWriterResult<()> { let serial_reg_prop = [dev_info.addr(), dev_info.length()]; let irq = [dev_info.irq() - IRQ_BASE, IRQ_TYPE_LEVEL_HI]; let serial_node = fdt.begin_node(&format!("serial@{:x}", dev_info.addr()))?; fdt.property_string("compatible", "ns16550a")?; fdt.property_array_u64("reg", &serial_reg_prop)?; fdt.property_u32("clock-frequency", 3686400)?; fdt.property_u32("interrupt-parent", AIA_APLIC_PHANDLE)?; fdt.property_array_u32("interrupts", &irq)?; fdt.end_node(serial_node)?; Ok(()) } fn create_devices_node( fdt: &mut FdtWriter, dev_info: &HashMap<(DeviceType, String), T, S>, ) -> FdtWriterResult<()> { for ((device_type, _device_id), info) in dev_info { match device_type { DeviceType::Serial => create_serial_node(fdt, info)?, DeviceType::Virtio(_) => unreachable!(), } } Ok(()) } fn create_pci_nodes(fdt: &mut FdtWriter, pci_device_info: &[PciSpaceInfo]) -> FdtWriterResult<()> { // Add node for PCIe controller. // See Documentation/devicetree/bindings/pci/host-generic-pci.txt in the kernel // and https://elinux.org/Device_Tree_Usage. // In multiple PCI segments setup, each PCI segment needs a PCI node. for pci_device_info_elem in pci_device_info.iter() { // EDK2 requires the PCIe high space above 4G address. // The actual space in CLH follows the RAM. If the RAM space is small, the PCIe high space // could fall below 4G. // Here we cut off PCI device space below 8G in FDT to workaround the EDK2 check. // But the address written in ACPI is not impacted. let (pci_device_base_64bit, pci_device_size_64bit) = if pci_device_info_elem.pci_device_space_start < PCI_HIGH_BASE.raw_value() { ( PCI_HIGH_BASE.raw_value(), pci_device_info_elem.pci_device_space_size - (PCI_HIGH_BASE.raw_value() - pci_device_info_elem.pci_device_space_start), ) } else { ( pci_device_info_elem.pci_device_space_start, pci_device_info_elem.pci_device_space_size, ) }; // There is no specific requirement of the 32bit MMIO range, and // therefore at least we can make these ranges 4K aligned. let pci_device_size_32bit: u64 = MEM_32BIT_DEVICES_SIZE / ((1 << 12) * pci_device_info.len() as u64) * (1 << 12); let pci_device_base_32bit: u64 = MEM_32BIT_DEVICES_START.0 + pci_device_size_32bit * pci_device_info_elem.pci_segment_id as u64; let ranges = [ // io addresses. Since AArch64 will not use IO address, // we can set the same IO address range for every segment. 0x1000000, 0_u32, 0_u32, (MEM_PCI_IO_START.0 >> 32) as u32, MEM_PCI_IO_START.0 as u32, (MEM_PCI_IO_SIZE >> 32) as u32, MEM_PCI_IO_SIZE as u32, // mmio addresses 0x2000000, // (ss = 10: 32-bit memory space) (pci_device_base_32bit >> 32) as u32, // PCI address pci_device_base_32bit as u32, (pci_device_base_32bit >> 32) as u32, // CPU address pci_device_base_32bit as u32, (pci_device_size_32bit >> 32) as u32, // size pci_device_size_32bit as u32, // device addresses 0x3000000, // (ss = 11: 64-bit memory space) (pci_device_base_64bit >> 32) as u32, // PCI address pci_device_base_64bit as u32, (pci_device_base_64bit >> 32) as u32, // CPU address pci_device_base_64bit as u32, (pci_device_size_64bit >> 32) as u32, // size pci_device_size_64bit as u32, ]; let bus_range = [0, 0]; // Only bus 0 let reg = [ pci_device_info_elem.mmio_config_address, PCI_MMIO_CONFIG_SIZE_PER_SEGMENT, ]; // See kernel document Documentation/devicetree/bindings/pci/pci-msi.txt let msi_map = [ // rid-base: A single cell describing the first RID matched by the entry. 0x0, // msi-controller: A single phandle to an MSI controller. AIA_IMSIC_PHANDLE, // msi-base: An msi-specifier describing the msi-specifier produced for the // first RID matched by the entry. (pci_device_info_elem.pci_segment_id as u32) << 8, // length: A single cell describing how many consecutive RIDs are matched // following the rid-base. 0x100, ]; let pci_node_name = format!("pci@{:x}", pci_device_info_elem.mmio_config_address); let pci_node = fdt.begin_node(&pci_node_name)?; fdt.property_string("compatible", "pci-host-ecam-generic")?; fdt.property_string("device_type", "pci")?; fdt.property_array_u32("ranges", &ranges)?; fdt.property_array_u32("bus-range", &bus_range)?; fdt.property_u32( "linux,pci-domain", pci_device_info_elem.pci_segment_id as u32, )?; fdt.property_u32("#address-cells", 3)?; fdt.property_u32("#size-cells", 2)?; fdt.property_array_u64("reg", ®)?; fdt.property_u32("#interrupt-cells", 1)?; fdt.property_null("interrupt-map")?; fdt.property_null("interrupt-map-mask")?; fdt.property_null("dma-coherent")?; fdt.property_array_u32("msi-map", &msi_map)?; fdt.property_u32("msi-parent", AIA_IMSIC_PHANDLE)?; fdt.end_node(pci_node)?; } Ok(()) } // Parse the DTB binary and print for debugging pub fn print_fdt(dtb: &[u8]) { match fdt_parser::Fdt::new(dtb) { Ok(fdt) => { if let Some(root) = fdt.find_node("/") { debug!("Printing the FDT:"); print_node(root, 0); } else { debug!("Failed to find root node in FDT for debugging."); } } Err(_) => debug!("Failed to parse FDT for debugging."), } } fn print_node(node: fdt_parser::node::FdtNode<'_, '_>, n_spaces: usize) { debug!("{:indent$}{}/", "", node.name, indent = n_spaces); for property in node.properties() { let name = property.name; // If the property is 'compatible', its value requires special handling. // The u8 array could contain multiple null-terminated strings. // We copy the original array and simply replace all 'null' characters with spaces. let value = if name == "compatible" { let mut compatible = vec![0u8; 256]; let handled_value = property .value .iter() .map(|&c| if c == 0 { b' ' } else { c }) .collect::>(); let len = cmp::min(255, handled_value.len()); compatible[..len].copy_from_slice(&handled_value[..len]); compatible[..(len + 1)].to_vec() } else { property.value.to_vec() }; let value = &value; // Now the value can be either: // - A null-terminated C string, or // - Binary data // We follow a very simple logic to present the value: // - At first, try to convert it to CStr and print, // - If failed, print it as u32 array. let value_result = match CStr::from_bytes_with_nul(value) { Ok(value_cstr) => value_cstr.to_str().ok(), Err(_e) => None, }; if let Some(value_str) = value_result { debug!( "{:indent$}{} : {:#?}", "", name, value_str, indent = (n_spaces + 2) ); } else { let mut array = Vec::with_capacity(256); array.resize(value.len() / 4, 0u32); BigEndian::read_u32_into(value, &mut array); debug!( "{:indent$}{} : {:X?}", "", name, array, indent = (n_spaces + 2) ); } } // Print children nodes if there is any for child in node.children() { print_node(child, n_spaces + 2); } } cloud-hypervisor-53.0/arch/src/riscv64/layout.rs000066400000000000000000000142041522476750100216600ustar00rootroot00000000000000// Copyright © 2024 Institute of Software, CAS. All rights reserved. // Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Memory layout of RISC-V 64-bit guest: // // Physical +---------------------------------------------------------------+ // address | | // end | | // ~ ~ ~ ~ // | | // | Highmem PCI MMIO space | // | | // RAM end +---------------------------------------------------------------+ // (dynamic, | | // including | | // hotplug ~ ~ ~ ~ // memory) | | // | DRAM | // | | // | | // | | // | | // 1 GB +---------------------------------------------------------------+ // | | // | PCI MMCONFIG space | // | | // 768 MB +---------------------------------------------------------------+ // | | // | | // | PCI MMIO space | // | | // 256 MB +---------------------------------------------------------------| // | | // | Legacy devices space | // | | // 128 MB +---------------------------------------------------------------| // | | // | IMSICs | // | | // 64 MB +---------------------------------------------------------------+ // | | // | APLICs | // | | // 4 MB +---------------------------------------------------------------+ // | UEFI flash | // 0 GB +---------------------------------------------------------------+ // // use vm_memory::GuestAddress; /// 0x0 ~ 0x40_0000 (4 MiB) is reserved to UEFI /// UEFI binary size is required less than 3 MiB, reserving 4 MiB is enough. pub const UEFI_START: GuestAddress = GuestAddress(0); pub const UEFI_SIZE: u64 = 0x040_0000; /// AIA related devices /// See https://elixir.bootlin.com/linux/v6.10/source/arch/riscv/include/uapi/asm/kvm.h /// 0x40_0000 ~ 0x0400_0000 (64 MiB) resides APLICs pub const APLIC_START: GuestAddress = GuestAddress(0x40_0000); pub const APLIC_SIZE: u64 = 0x4000; /// 0x0400_0000 ~ 0x0800_0000 (64 MiB) resides IMSICs pub const IMSIC_START: GuestAddress = GuestAddress(0x0400_0000); pub const IMSIC_SIZE: u64 = 0x1000; /// Below this address will reside the AIA, above this address will reside the MMIO devices. const MAPPED_IO_START: GuestAddress = GuestAddress(0x0800_0000); /// Space 0x0800_0000 ~ 0x1000_0000 is reserved for legacy devices. pub const LEGACY_SERIAL_MAPPED_IO_START: GuestAddress = MAPPED_IO_START; /// Space 0x0905_0000 ~ 0x0906_0000 is reserved for pcie io address pub const MEM_PCI_IO_START: GuestAddress = GuestAddress(0x0905_0000); pub const MEM_PCI_IO_SIZE: u64 = 0x1_0000; /// Starting from 0x1000_0000 (256MiB) to 0x3000_0000 (768MiB) is used for PCIE MMIO pub const MEM_32BIT_DEVICES_START: GuestAddress = GuestAddress(0x1000_0000); pub const MEM_32BIT_DEVICES_SIZE: u64 = 0x2000_0000; /// PCI MMCONFIG space (start: after the device space at 768MiB, length: 256MiB) pub const PCI_MMCONFIG_START: GuestAddress = GuestAddress(0x3000_0000); pub const PCI_MMCONFIG_SIZE: u64 = 256 << 20; // One bus with potentially 256 devices (32 slots x 8 functions). pub const PCI_MMIO_CONFIG_SIZE_PER_SEGMENT: u64 = 4096 * 256; /// Start of RAM. pub const RAM_START: GuestAddress = GuestAddress(0x4000_0000); /// Kernel command line maximum size on RISC-V. /// See https://elixir.bootlin.com/linux/v6.10/source/arch/riscv/include/uapi/asm/setup.h pub const CMDLINE_MAX_SIZE: usize = 1024; /// FDT is at the beginning of RAM. pub const FDT_START: GuestAddress = RAM_START; pub const FDT_MAX_SIZE: u64 = 0x1_0000; /// Put ACPI table above dtb pub const ACPI_START: GuestAddress = GuestAddress(RAM_START.0 + FDT_MAX_SIZE); pub const ACPI_MAX_SIZE: u64 = 0x20_0000; pub const RSDP_POINTER: GuestAddress = ACPI_START; /// Kernel start after FDT and ACPI pub const KERNEL_START: GuestAddress = GuestAddress(RAM_START.0 + FDT_MAX_SIZE); /// Pci high memory base pub const PCI_HIGH_BASE: GuestAddress = GuestAddress(0x2_0000_0000); /// First usable interrupt on riscv64 pub const IRQ_BASE: u32 = 0; // As per https://elixir.bootlin.com/linux/v6.10/source/arch/riscv/include/asm/kvm_host.h#L31 /// Number of supported interrupts pub const IRQ_NUM: u32 = 1023; cloud-hypervisor-53.0/arch/src/riscv64/mod.rs000066400000000000000000000161361522476750100211300ustar00rootroot00000000000000// Copyright © 2024 Institute of Software, CAS. All rights reserved. // Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 /// Module for the flattened device tree. pub mod fdt; /// Layout for this riscv64 system. pub mod layout; /// Module for loading UEFI binary. pub mod uefi; use std::collections::HashMap; use std::fmt::Debug; use std::fs::File; use std::io::{BufRead, BufReader}; use std::sync::{Arc, Mutex}; use hypervisor::arch::riscv64::aia::Vaia; use log::{Level, log_enabled}; use thiserror::Error; use vm_memory::{Address, GuestAddress, GuestMemory, GuestMemoryAtomic}; pub use self::fdt::DeviceInfoForFdt; use crate::{DeviceType, GuestMemoryMmap, PciSpaceInfo, RegionType}; pub const CLOUDHV_IRQCHIP_NUM_MSIS: u16 = 255; pub const CLOUDHV_IRQCHIP_NUM_SOURCES: u8 = 96; pub const CLOUDHV_IRQCHIP_NUM_PRIO_BITS: u8 = 3; pub const CLOUDHV_IRQCHIP_MAX_GUESTS_BITS: u8 = 3; pub const CLOUDHV_IRQCHIP_MAX_GUESTS: u8 = (1 << CLOUDHV_IRQCHIP_MAX_GUESTS_BITS) - 1; pub const _NSIG: i32 = 65; /// Errors thrown while configuring riscv64 system. #[derive(Debug, Error)] pub enum Error { /// Failed to create a FDT. #[error("Failed to create a FDT")] SetupFdt, /// Failed to write FDT to memory. #[error("Failed to write FDT to memory")] WriteFdtToMemory(#[source] fdt::Error), /// Failed to create a AIA. #[error("Failed to create a AIA")] SetupAia, /// Failed to compute the initramfs address. #[error("Failed to compute the initramfs address")] InitramfsAddress, /// Error configuring the general purpose registers #[error("Error configuring the general purpose registers")] RegsConfiguration(#[source] hypervisor::HypervisorCpuError), /// Error opening /proc/cpuinfo #[error("Error opening /proc/cpuinfo")] OpenCpuInfo(#[source] std::io::Error), /// Error reading /proc/cpuinfo #[error("Error reading /proc/cpuinfo")] ReadCpuInfo(#[source] std::io::Error), /// Invalid ISA string #[error("Invalid ISA string: {0}")] InvalidIsaString(String), /// Error parsing /proc/cpuinfo #[error("Error parsing /proc/cpuinfo")] CpuInfoParsing, } #[derive(Debug, Copy, Clone)] /// Specifies the entry point address where the guest must start /// executing code. pub struct EntryPoint { /// Address in guest memory where the guest must start execution pub entry_addr: GuestAddress, } /// Configure the specified VCPU, and return its MPIDR. pub fn configure_vcpu( vcpu: &dyn hypervisor::Vcpu, id: u32, boot_setup: Option<(EntryPoint, &GuestMemoryAtomic)>, ) -> super::Result<()> { if let Some((kernel_entry_point, _guest_memory)) = boot_setup { vcpu.setup_regs( id, kernel_entry_point.entry_addr.raw_value(), layout::FDT_START.raw_value(), ) .map_err(Error::RegsConfiguration)?; } Ok(()) } pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> { vec![ // 0 MiB ~ 256 MiB: AIA and legacy devices ( GuestAddress(0), layout::MEM_32BIT_DEVICES_START.0 as usize, RegionType::Reserved, ), // 256 MiB ~ 768 MiB: MMIO space ( layout::MEM_32BIT_DEVICES_START, layout::MEM_32BIT_DEVICES_SIZE as usize, RegionType::SubRegion, ), // 768 MiB ~ 1 GiB: reserved. The leading 256M for PCIe MMCONFIG space ( layout::PCI_MMCONFIG_START, layout::PCI_MMCONFIG_SIZE as usize, RegionType::Reserved, ), // 1GiB ~ inf: RAM (layout::RAM_START, usize::MAX, RegionType::Ram), ] } // Read the first "isa" string from /proc/cpuinfo and filter out the H extension, // while correctly preserving multi-letter extensions. fn isa_string_from_host() -> Result { 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. pub fn configure_system( guest_mem: &GuestMemoryMmap, cmdline: &str, num_vcpu: u32, device_info: &HashMap<(DeviceType, String), T, S>, initrd: &Option, pci_space_info: &[PciSpaceInfo], aia_device: &Arc>, timebase_frequency: u32, ) -> super::Result<()> { let isa_string = isa_string_from_host()?; let fdt_final = fdt::create_fdt( guest_mem, cmdline, num_vcpu, &isa_string, device_info, aia_device, initrd, pci_space_info, timebase_frequency, ) .map_err(|_| Error::SetupFdt)?; if log_enabled!(Level::Debug) { fdt::print_fdt(&fdt_final); } fdt::write_fdt_to_memory(&fdt_final, guest_mem).map_err(Error::WriteFdtToMemory)?; Ok(()) } /// Returns the memory address where the initramfs could be loaded. pub fn initramfs_load_addr( guest_mem: &GuestMemoryMmap, initramfs_size: usize, ) -> super::Result { let round_to_pagesize = |size| (size + (super::PAGE_SIZE - 1)) & !(super::PAGE_SIZE - 1); match guest_mem .last_addr() .checked_sub(round_to_pagesize(initramfs_size) as u64 - 1) { Some(offset) => { if guest_mem.address_in_range(offset) { Ok(offset.raw_value()) } else { Err(super::Error::PlatformSpecific(Error::InitramfsAddress)) } } None => Err(super::Error::PlatformSpecific(Error::InitramfsAddress)), } } pub fn get_host_cpu_phys_bits(_hypervisor: &dyn hypervisor::Hypervisor) -> u8 { 40 } #[cfg(test)] mod unit_tests { use super::*; #[test] fn test_arch_memory_regions_dram() { let regions = arch_memory_regions(); assert_eq!(4, regions.len()); assert_eq!(layout::RAM_START, regions[3].0); assert_eq!(RegionType::Ram, regions[3].2); } } cloud-hypervisor-53.0/arch/src/riscv64/uefi.rs000066400000000000000000000025741522476750100213020ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // // SPDX-License-Identifier: Apache-2.0 use std::io::{Read, Seek, SeekFrom}; use std::os::fd::AsFd; use std::result; use thiserror::Error; use vm_memory::{Bytes, GuestAddress, GuestMemory}; /// Errors thrown while loading UEFI binary #[derive(Debug, Error)] pub enum Error { /// Unable to seek to UEFI image start. #[error("Unable to seek to UEFI image start")] SeekUefiStart, /// Unable to seek to UEFI image end. #[error("Unable to seek to UEFI image end")] SeekUefiEnd, /// UEFI image too big. #[error("UEFI image too big")] UefiTooBig, /// Unable to read UEFI image #[error("Unable to read UEFI image")] ReadUefiImage, } type Result = 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) } cloud-hypervisor-53.0/arch/src/x86_64/000077500000000000000000000000001522476750100174325ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/src/x86_64/cpu_profile/000077500000000000000000000000001522476750100217415ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/src/x86_64/cpu_profile/cpuid_adjustments.rs000066400000000000000000000155171522476750100260450ustar00rootroot00000000000000// Copyright © 2026 Cyberus Technology GmbH // // SPDX-License-Identifier: Apache-2.0 // //! This module contains types associated with adjusting CPUID entries according //! to a selected CPU profile. use std::ops::RangeInclusive; use hypervisor::arch::x86::CpuIdEntry; use log::error; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::x86_64::{CpuidReg, deserialize_u32_hex, serialize_u32_hex}; /// Parameters for inspecting CPUID definitions. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct CpuidParameters { /// The leaf (EAX) parameter used with the CPUID instruction #[serde( serialize_with = "serialize_u32_hex", deserialize_with = "deserialize_u32_hex" )] pub leaf: u32, /// The sub-leaf (ECX) parameter used with the CPUID instruction #[serde( serialize_with = "serialize_range_hex", deserialize_with = "deserialize_range_hex" )] pub sub_leaf: RangeInclusive, /// 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) } /// Used for adjusting an entire cpuid output register (EAX, EBX, ECX or EDX). /// /// Instances of this struct typically adjust CPUID according to the following /// formula: `cpuid_reg_value = (self.mask & cpuid_reg_value) | self.replacements`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct CpuidOutputRegisterAdjustments { /// Packs values to be placed into the given CPUID output register. #[serde( serialize_with = "serialize_u32_hex", deserialize_with = "deserialize_u32_hex" )] pub replacements: u32, /// Used to zero out the area `replacements` occupy. This mask is not necessarily !replacements, as replacements /// may pack values of different types that occupy varying ranges of bits. /// /// Bit ranges within a CPUID output register that are **not** supposed to be replaced/overwritten should be set in /// this mask. #[serde( serialize_with = "serialize_u32_hex", deserialize_with = "deserialize_u32_hex" )] pub mask: u32, } /// Error type indicating that expected CPUID entries could not be found. /// /// This type does not record which entries could not be found as we do not /// expect this to be actionable at runtime. Instead we encourage logging such /// violations when and where they are detected. #[derive(Debug, Error)] #[error("Required CPUID entries not found")] pub struct MissingCpuidEntriesError; impl CpuidOutputRegisterAdjustments { /// Adjust the given `cpuid_output_register` by retaining and replacing values according to `self`. fn adjust(self, cpuid_output_register: &mut u32) { *cpuid_output_register &= self.mask; *cpuid_output_register |= self.replacements; } /// Adjust `cpuid` according to the given `adjustments`. /// /// The returned vector of cpuid entries covers the same CPUID (sub-) leaves as the given `cpuid` input, /// but values without matching [`CpuidParameters`] are zeroed out. /// /// # Errors /// /// An error is returned if an entry cannot be found for an adjustment describing non-zero replacements. pub(super) fn adjust_cpuid_entries( mut cpuid: Vec, adjustments: &[(CpuidParameters, 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), ] { // Lookup the adjustment corresponding to the entry's function/leaf and index/sub-leaf for each of the register. let register_adjustments: Option = adjustments.iter().find_map(|(param, adjustment)| { ((param.leaf == entry.function) && param.sub_leaf.contains(&entry.index) && (param.register == reg)) .then_some(*adjustment) }); match register_adjustments { Some(adjustment) => adjustment.adjust(reg_value), None => { // No matching cpuid parameters were found. We thus set the value of the register to 0. *reg_value = 0; } } } } Self::expected_entries_found(&cpuid, adjustments)?; Ok(cpuid) } /// Check that we found every value that was supposed to be replaced with something else than 0 /// /// IMPORTANT: This function assumes that the given `cpuid` has already been adjusted with the /// provided `adjustments`. fn expected_entries_found( cpuid: &[CpuIdEntry], adjustments: &[(CpuidParameters, Self)], ) -> Result<(), MissingCpuidEntriesError> { let mut missing_entry = false; for (param, adjustment) in adjustments { if adjustment.replacements == 0 { continue; } if !cpuid.iter().any(|entry| { (entry.function == param.leaf) && (param.sub_leaf.contains(&entry.index)) }) { error!( "cannot adjust CPU profile. No entry found matching the required parameters: {param:?}" ); missing_entry = true; } } if missing_entry { Err(MissingCpuidEntriesError) } else { Ok(()) } } } /// Data describing CPUID adjustments related to a CPU Profile. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CpuidProfileData { /// Adjustments necessary to become compatible with the desired target. pub adjustments: Vec<(CpuidParameters, CpuidOutputRegisterAdjustments)>, } cloud-hypervisor-53.0/arch/src/x86_64/cpu_profile/mod.rs000066400000000000000000000711211522476750100230700ustar00rootroot00000000000000// Copyright © 2026 Cyberus Technology GmbH // // SPDX-License-Identifier: Apache-2.0 // use hypervisor::CpuVendor; use hypervisor::arch::x86::CpuIdEntry; use crate::x86_64::cpu_profile::cpuid_adjustments::{ CpuidOutputRegisterAdjustments, CpuidProfileData, MissingCpuidEntriesError, }; use crate::x86_64::{AMX_TILECFG_BIT, AMX_TILEDATA_BIT, CpuidReg}; /// Mask indicating availability of the AMX TILECFG state component const TILECFG_MASK: u32 = 1_u32 << AMX_TILECFG_BIT; /// Mask indicating availability of the AMX TILEDATA state component const TILEDATA_MASK: u32 = 1_u32 << AMX_TILEDATA_BIT; pub mod cpuid_adjustments; // TODO: Auto generate the CpuProfile enum with a build script once we introduce user facing CPU profiles. /// A [`CpuProfile`] is a mechanism for ensuring live migration compatibility /// between hosts with potentially different CPU models. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum CpuProfile { #[default] Host, } impl CpuProfile { /// Adjust `cpuid` to the chosen CPU profile. /// /// The CPUID data obtained from the hypervisor is thus downgraded to the selected profile. /// /// This method does **not** perform any compatibility checks beyond /// ensuring that all expected (sub) leaves required by the CPU profile are present. /// /// The caller is responsible for ensuring compatibility of `cpuid` by the time it is /// utilized. /// /// If Intel AMX is not desired, then passing `amx = false` will permit missing (sub)-leaves /// that are **purely AMX related**. /// /// The Host profile guarantees that `cpuid` is returned without any modifications. pub(in crate::x86_64) fn adjust_cpuid( &self, cpuid: Vec, amx: bool, cpu_vendor: CpuVendor, ) -> Result, MissingCpuidEntriesError> { let Some(cpuid_profile_data) = self.cpuid_data() else { return Ok(cpuid); }; adjust_cpuid(cpuid_profile_data, cpuid, amx, cpu_vendor) } /// Obtain CPUID adjustment data related to the CPU profile. fn cpuid_data(&self) -> Option { // TODO: Auto generate this through a build script once // we introduce actual CPU profiles. match self { CpuProfile::Host => None, } } } /// See [`CpuProfile::adjust_cpuid`](CpuProfile::adjust_cpuid) fn adjust_cpuid( CpuidProfileData { mut adjustments }: CpuidProfileData, cpuid: Vec, amx: bool, cpu_vendor: CpuVendor, ) -> Result, MissingCpuidEntriesError> { if (!amx) && matches!(cpu_vendor, CpuVendor::Intel) { let amx_tilecfg_leaf = u32::from(AMX_TILECFG_BIT); let amx_tiledata_leaf = u32::from(AMX_TILEDATA_BIT); // In this case we invalidate tile state components and zero out all other purely AMX related leaves // in order to maximize our chances of finding all required (sub) leaves. for adj in adjustments.iter_mut() { if adj.0.sub_leaf.start() != adj.0.sub_leaf.end() { continue; } let sub_leaf = *adj.0.sub_leaf.start(); let leaf = adj.0.leaf; if (leaf == 0xd) && (sub_leaf == 0) && (adj.0.register == CpuidReg::EAX) { adj.1.mask &= !(TILECFG_MASK | TILEDATA_MASK); adj.1.replacements &= !(TILECFG_MASK | TILEDATA_MASK); } if (leaf == 0xd) && (sub_leaf == 1) && (adj.0.register == CpuidReg::ECX) { adj.1.mask &= !(TILECFG_MASK | TILEDATA_MASK); adj.1.replacements &= !(TILECFG_MASK | TILEDATA_MASK); } if (leaf == 0xd) && ((sub_leaf == amx_tilecfg_leaf) || (sub_leaf == amx_tiledata_leaf)) { adj.1.mask = 0; adj.1.replacements = 0; } // Tile Information (purely AMX related). if leaf == 0x1d { adj.1.mask = 0; adj.1.replacements = 0; } // TMUL information (purely AMX related) if leaf == 0x1e { adj.1.mask = 0; adj.1.replacements = 0; } } } CpuidOutputRegisterAdjustments::adjust_cpuid_entries(cpuid, &adjustments) } #[cfg(test)] mod unit_tests { use proptest::prelude::*; use super::{CpuIdEntry, CpuVendor, CpuidProfileData, CpuidReg, adjust_cpuid}; use crate::x86_64::cpu_profile::cpuid_adjustments::{ CpuidOutputRegisterAdjustments, CpuidParameters, }; use crate::x86_64::cpu_profile::{TILECFG_MASK, TILEDATA_MASK}; // Note that the tests for adjust_cpuid within this module tend to use much simpler inputs // than what it will be called with at runtime within Cloud hypervisor. We do this here in order // to keep each test focused on the behavioral aspect under test. /// Helper function that returns adjustments tied to purely AMX related leaves. fn amx_related_adjustments() -> Vec<(CpuidParameters, CpuidOutputRegisterAdjustments)> { let amx_adjustments_json = r#" [ [ { "leaf": "0xd", "sub_leaf": { "start": "0x11", "end": "0x11" }, "register": "EAX" }, { "replacements": "0x40", "mask": "0x0" } ], [ { "leaf": "0xd", "sub_leaf": { "start": "0x11", "end": "0x11" }, "register": "EBX" }, { "replacements": "0xac0", "mask": "0x0" } ], [ { "leaf": "0xd", "sub_leaf": { "start": "0x11", "end": "0x11" }, "register": "ECX" }, { "replacements": "0x2", "mask": "0x0" } ], [ { "leaf": "0xd", "sub_leaf": { "start": "0x12", "end": "0x12" }, "register": "EAX" }, { "replacements": "0x2000", "mask": "0x0" } ], [ { "leaf": "0xd", "sub_leaf": { "start": "0x12", "end": "0x12" }, "register": "EBX" }, { "replacements": "0xb00", "mask": "0x0" } ], [ { "leaf": "0xd", "sub_leaf": { "start": "0x12", "end": "0x12" }, "register": "ECX" }, { "replacements": "0x6", "mask": "0x0" } ], [ { "leaf": "0x1d", "sub_leaf": { "start": "0x0", "end": "0x0" }, "register": "EAX" }, { "replacements": "0x1", "mask": "0x0" } ], [ { "leaf": "0x1d", "sub_leaf": { "start": "0x1", "end": "0x1" }, "register": "EAX" }, { "replacements": "0x4002000", "mask": "0x0" } ], [ { "leaf": "0x1d", "sub_leaf": { "start": "0x1", "end": "0x1" }, "register": "EBX" }, { "replacements": "0x80040", "mask": "0x0" } ], [ { "leaf": "0x1d", "sub_leaf": { "start": "0x1", "end": "0x1" }, "register": "ECX" }, { "replacements": "0x10", "mask": "0x0" } ], [ { "leaf": "0x1e", "sub_leaf": { "start": "0x0", "end": "0x0" }, "register": "EAX" }, { "replacements": "0x0", "mask": "0x0" } ], [ { "leaf": "0x1e", "sub_leaf": { "start": "0x0", "end": "0x0" }, "register": "EBX" }, { "replacements": "0x4010", "mask": "0x0" } ] ]"#; serde_json::from_str(amx_adjustments_json).unwrap() } // Randonly generate three CPUID entries and construct some simple adjustments which we apply // through the `adjust_cpuid` function and assert that our expectations are met. proptest! { #[test] fn adjust_cpuid_simple_adjustments( leaf0 in any::(), leaf1 in any::(), leaf2 in any::(), a in any::(), b in any::(), c in any::(), d in any::(), ) { // Ensure that we have distinct leaves in this test let mut leaves = [leaf0, leaf1, leaf2]; leaves.sort_unstable(); for (l, i) in leaves.iter_mut().zip([0, 1, 2]) { *l = l.wrapping_add(i); } let [leaf0, leaf1, leaf2] = leaves; // The following leaves have some special handling that we test in later // more specialized tests. In this simple test we want to avoid them. let leaves_with_special_handling = { [0xd, 0x1d, 0x1e] }; let transform_leaf = |leaf: u32| { if leaves_with_special_handling.contains(&leaf) { // Ensures that we get a leaf different from any of the leaves that have special handling leaf | 0x1000 } else { leaf } }; let leaf0 = transform_leaf(leaf0); let leaf1 = transform_leaf(leaf1); let leaf2 = transform_leaf(leaf2); // The leaves should still be distinct assert!(leaf0 != leaf1); assert!(leaf0 != leaf2); assert!(leaf1 != leaf2); // We have now setup leaves to be used in this test // Let's now construct some simple adjustments // mask retaining bits 0,1,2 and 3 let first_four_bits_mask = 15; // Retain the first four bits of the register and overwrite the remaining bits with the value "42" let adjustment_u = CpuidOutputRegisterAdjustments { mask: first_four_bits_mask, replacements: 42 << 4, }; let assert_adjustment_u = |new_value: u32, old_value: u32| { assert_eq!(new_value & first_four_bits_mask, (old_value & first_four_bits_mask)); // Recall that we placed the value 42 into bits 31:4 assert_eq!(new_value >> 4, 42); }; // Set bits 0 and 28 and zero out the rest let adjustment_v = CpuidOutputRegisterAdjustments { replacements: 1 | (1 << 28), mask: 0 }; let assert_adjustment_v = |new_value: u32| { assert_eq!(new_value, 1 | (1 << 28)); }; // Make adjustment_u apply to EAX of leaf0 and EBX of leaf1. // // Make adjustment_v apply to EDX of leaf0 and ECX of leaf1. // // We do not specify any adjustment for leaf2. let cpuid_profile_data = CpuidProfileData { adjustments: vec![ ( CpuidParameters { leaf: leaf0, sub_leaf: 0..=0, register: CpuidReg::EAX, }, adjustment_u, ), ( CpuidParameters { leaf: leaf0, sub_leaf: 0..=0, register: CpuidReg::EDX, }, adjustment_v, ), ( CpuidParameters { leaf: leaf1, sub_leaf: 0..=0, register: CpuidReg::EBX, }, adjustment_u, ), ( CpuidParameters { leaf: leaf1, sub_leaf: 0..=0, register: CpuidReg::ECX, }, adjustment_v, ), ], }; // Construct cpuid entries consisting of leaves leaf0, leaf1 and leaf2. // The registers eax, ebx, ecx, edx are populated with the randomly generated values `a`, `b`, `c` and `d` // and we do not consider sub-leaves in this test. let cpuid = vec![ CpuIdEntry { function: leaf0, index: 0, flags: 0, eax: a, ebx: b, ecx: c, edx: d, }, CpuIdEntry { function: leaf1, index: 0, flags: 0, eax: a, ebx: b, ecx: c, edx: d, }, CpuIdEntry { function: leaf2, index: 0, flags: 0, eax: a, ebx: b, ecx: c, edx: d, }, ]; // Check that the output of `adjust_cpuid` contains the same CPUID leaves as the // `cpuid` vector we started with. let expected_num_entries = cpuid.len(); let mut found_entry_count = 0; let adjusted_cpuid = adjust_cpuid(cpuid_profile_data, cpuid, false, CpuVendor::Intel).unwrap(); // Iterate through our adjusted entries and assert that our expectations are met. for entry in adjusted_cpuid { let CpuIdEntry { function, index, flags, eax, ebx, ecx, edx, } = entry; if function == leaf0 { found_entry_count += 1; assert_adjustment_u(eax, a); assert_adjustment_v(edx); // ebx and ecx should be zeroed out assert_eq!(ebx, 0); assert_eq!(ecx, 0); } if function == leaf1 { found_entry_count += 1; assert_adjustment_u(ebx, b); assert_adjustment_v(ecx); // eax and edx should be zeroed out assert_eq!(eax, 0); assert_eq!(edx, 0); } if function == leaf2 { found_entry_count += 1; // All registers should be zeroed out assert_eq!(eax, ebx); assert_eq!(ebx, ecx); assert_eq!(ecx, edx); assert_eq!(edx, 0); } // Index and flags should not be altered. Since these were both // always 0 for all leaves in the original `cpuid` that should // remain the case. assert_eq!(index, 0); assert_eq!(flags, 0); } assert_eq!(expected_num_entries, found_entry_count); } } // Check that adjust_cpuid follows the prescribed adjustments on // specified subleaf ranges #[test] fn adjust_cpuid_works_with_subleaf_ranges() { // As in the real runtime case the Topology enumeration leaves should not be altered // by CPU profiles. In this test we thus define adjustment's that do not alter non-reserved // bits for the 0x1f leaf and its sub-leaves. let adjustments_json = r#" [ [ { "leaf": "0x1f", "sub_leaf": { "start": "0x0", "end": "0xffffffff" }, "register": "EAX" }, { "replacements": "0x0", "mask": "0x1f" } ], [ { "leaf": "0x1f", "sub_leaf": { "start": "0x0", "end": "0xffffffff" }, "register": "EBX" }, { "replacements": "0x0", "mask": "0xffff" } ], [ { "leaf": "0x1f", "sub_leaf": { "start": "0x0", "end": "0xffffffff" }, "register": "ECX" }, { "replacements": "0x0", "mask": "0xffff" } ], [ { "leaf": "0x1f", "sub_leaf": { "start": "0x0", "end": "0xffffffff" }, "register": "EDX" }, { "replacements": "0x0", "mask": "0xffffffff" } ] ]"#; let cpuid_profile_data = CpuidProfileData { adjustments: serde_json::from_str(adjustments_json).unwrap(), }; let cpuid = vec![ CpuIdEntry { function: 0x1f, index: 0, flags: 1, eax: 0x00000001, ebx: 0x00000002, ecx: 0x00000100, edx: 0x00000000, }, CpuIdEntry { function: 0x1f, index: 1, flags: 1, eax: 0x00000004, ebx: 0x00000008, ecx: 0x00000201, edx: 0x00000006, }, ]; let adjusted_cpuid = adjust_cpuid(cpuid_profile_data, cpuid.clone(), false, CpuVendor::Intel).unwrap(); // Check that cpuid was indeed not altered for entry in cpuid { let adjusted_entry = adjusted_cpuid .iter() .find(|e| (e.function == entry.function) && (e.index == entry.index)) .unwrap(); assert_eq!(entry.eax, adjusted_entry.eax); assert_eq!(entry.ebx, adjusted_entry.ebx); assert_eq!(entry.ecx, adjusted_entry.ecx); assert_eq!(entry.edx, adjusted_entry.edx); } } #[test] fn adjust_cpuid_fails_on_missing_entries() { let cpuid = vec![CpuIdEntry { function: 0x0, index: 0x0, eax: 0x20, ebx: 0x756e6547, ecx: 0x6c65746e, edx: 0x49656e69, flags: 0, }]; let cpuid_profile_data = CpuidProfileData { adjustments: vec![( CpuidParameters { leaf: 0x1, sub_leaf: 0x0..=0x0, register: CpuidReg::EAX, }, CpuidOutputRegisterAdjustments { replacements: 0x000806f8, mask: 0, }, )], }; let _ = adjust_cpuid(cpuid_profile_data, cpuid.clone(), false, CpuVendor::Intel).unwrap_err(); // Also check this for a purely AMX related leaves which has special handling let _ = adjust_cpuid( CpuidProfileData { adjustments: amx_related_adjustments(), }, cpuid, true, CpuVendor::Intel, ) .unwrap_err(); } // Check that if `amx = false` then AMX related leaves are zeroed out #[test] fn adjust_cpuid_no_amx_zeros_amx_leaves() { // Not AMX related let leaf_zero = CpuIdEntry { function: 0x0, index: 0x0, eax: 0x20, ebx: 0x756e6547, ecx: 0x6c65746e, edx: 0x49656e69, flags: 0, }; let cpuid = vec![ leaf_zero, // State components base leaf extracted from granite rapids the AMX related state component bits are set CpuIdEntry { function: 0xd, index: 0x0, flags: 1, eax: 0x000602e7, ebx: 0x00002b00, ecx: 0x00002b00, edx: 0x00000000, }, // TILECFG state CpuIdEntry { function: 0xd, index: 0x11, flags: 1, eax: 0x00000040, ebx: 0x00000ac0, ecx: 0x00000002, edx: 0x00000000, }, // TILEDATA state CpuIdEntry { function: 0xd, index: 0x12, flags: 1, eax: 0x00002000, ebx: 0x00000b00, ecx: 0x00000006, edx: 0x00000000, }, // Tile information base leaf CpuIdEntry { function: 0x1d, index: 0x0, flags: 1, eax: 0x00000001, ebx: 0x00000000, ecx: 0x00000000, edx: 0x00000000, }, // Tile Palette 1 CpuIdEntry { function: 0x1d, index: 0x1, flags: 1, eax: 0x04002000, ebx: 0x00080040, ecx: 0x00000010, edx: 0x00000000, }, // TMUL information base leaf CpuIdEntry { function: 0x1e, index: 0x0, flags: 1, eax: 0x00000000, ebx: 0x00004010, ecx: 0x00000000, edx: 0x00000000, }, ]; let adjustments: Vec<(CpuidParameters, CpuidOutputRegisterAdjustments)> = amx_related_adjustments() .into_iter() // leave leaf 0 untouched .chain([ ( CpuidParameters { leaf: 0x0, sub_leaf: 0x0..=0x0, register: CpuidReg::EAX, }, CpuidOutputRegisterAdjustments { replacements: 0, mask: u32::MAX, }, ), ( CpuidParameters { leaf: 0x0, sub_leaf: 0x0..=0x0, register: CpuidReg::EBX, }, CpuidOutputRegisterAdjustments { replacements: 0, mask: u32::MAX, }, ), ( CpuidParameters { leaf: 0x0, sub_leaf: 0x0..=0x0, register: CpuidReg::ECX, }, CpuidOutputRegisterAdjustments { replacements: 0, mask: u32::MAX, }, ), ( CpuidParameters { leaf: 0x0, sub_leaf: 0x0..=0x0, register: CpuidReg::EDX, }, CpuidOutputRegisterAdjustments { replacements: 0, mask: u32::MAX, }, ), ]) // Keep EAX of leaf 0xd so we see that the AMX-related state component bits get unset, regardless of what // the adjustment says .chain([( CpuidParameters { leaf: 0xd, sub_leaf: 0x0..=0x0, register: CpuidReg::EAX, }, CpuidOutputRegisterAdjustments { mask: u32::MAX, replacements: 0, }, )]) .collect(); let adjusted_cpuid = adjust_cpuid( CpuidProfileData { adjustments }, cpuid.clone(), false, CpuVendor::Intel, ) .unwrap(); // Check that leaf zero is left untouched as expected { let adjusted_leaf_zero = adjusted_cpuid .iter() .find(|entry| entry.function == 0x0) .unwrap(); assert_eq!(adjusted_leaf_zero.eax, leaf_zero.eax); assert_eq!(adjusted_leaf_zero.ebx, leaf_zero.ebx); assert_eq!(adjusted_leaf_zero.ecx, leaf_zero.ecx); assert_eq!(adjusted_leaf_zero.edx, leaf_zero.edx); } // Check that the TILECFG and TILEDATA state bits are now zeroed ut { let state_cmp_base_leaf = adjusted_cpuid .iter() .find(|entry| (entry.function == 0xd) && (entry.index == 0x0)) .unwrap(); // EAX should not have been zeroed out in its entirety assert!(state_cmp_base_leaf.eax != 0); // The TILECFG state bit should be unset assert_eq!(state_cmp_base_leaf.eax & TILECFG_MASK, 0); // The TILEDATA state bit should be unset assert_eq!(state_cmp_base_leaf.eax & TILEDATA_MASK, 0); } // Since all remaining entries we placed in `cpuid` are purely AMX related we now // expect them to be zeroed out for entry in adjusted_cpuid { if entry.function == 0 || (entry.function == 0xd && entry.index == 0x0) { continue; } assert_eq!(entry.eax, 0); assert_eq!(entry.ebx, 0); assert_eq!(entry.ecx, 0); assert_eq!(entry.edx, 0); } } // Check that if `amx = false` then missing purely AMX related leaves // do not lead to failure #[test] fn adjust_cpuid_no_amx_missing_amx_leaves_accepted() { let cpuid = vec![CpuIdEntry { function: 0x0, index: 0x0, eax: 0x20, ebx: 0x756e6547, ecx: 0x6c65746e, edx: 0x49656e69, flags: 0, }]; let _ = adjust_cpuid( CpuidProfileData { adjustments: amx_related_adjustments(), }, cpuid, false, CpuVendor::Intel, ) .unwrap(); } } cloud-hypervisor-53.0/arch/src/x86_64/helpers.rs000066400000000000000000000053371522476750100214520ustar00rootroot00000000000000// Copyright © 2026 Cyberus Technology GmbH // // SPDX-License-Identifier: Apache-2.0 // use std::result; use serde::{Deserialize, Deserializer, Serializer, de}; /// Serializes the given `input` as a hex string (starting with "0x"). /// /// As an example if `input:=5` then this function will feed the given /// `serializer` the string "0x5". pub(crate) fn serialize_u32_hex( input: &u32, serializer: S, ) -> result::Result { serializer.serialize_str(&format!("{input:#x}")) } /// Deserializes a u32 from a hex string representation. pub(crate) fn deserialize_u32_hex<'de, D: Deserializer<'de>>( deserializer: D, ) -> result::Result { let hex: &str = <&str>::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")) }) } #[cfg(test)] mod unit_tests { use proptest::prelude::*; use serde::{Deserialize, Serialize}; use super::*; #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] struct TestStruct { #[serde( serialize_with = "serialize_u32_hex", deserialize_with = "deserialize_u32_hex" )] foo: u32, #[serde( serialize_with = "serialize_u32_hex", deserialize_with = "deserialize_u32_hex" )] bar: u32, } // Check that our hex serializers satisfy the two following invariants // 1. Serialization followed by deserialization is the identity. // 2. Values of type u32 are serialized to strings starting with "0x" and then // a sub-string where all characters are ascii hex digits (with the letters [a-f] always in lowercase). proptest! { #[test] fn hex_serialization_works(foo in any::(), bar in any::()) { let t = TestStruct { foo , bar }; let t_string = serde_json::to_string(&t).unwrap(); let t_deserialized = serde_json::from_str(&t_string).unwrap(); prop_assert_eq!(t, t_deserialized); let t_json = serde_json::to_value(t).unwrap(); let check_str_invariants = |value: &str| { prop_assert!(value.starts_with("0x")); prop_assert!(value.as_bytes()[2..].iter().all(u8::is_ascii_hexdigit)); prop_assert!(!value.as_bytes()[2..].iter().any(u8::is_ascii_uppercase)); Ok(()) }; let foo_str = t_json.get("foo").unwrap().as_str().unwrap(); let bar_str = t_json.get("bar").unwrap().as_str().unwrap(); check_str_invariants(foo_str)?; check_str_invariants(bar_str)?; } } } cloud-hypervisor-53.0/arch/src/x86_64/interrupts.rs000066400000000000000000000024121522476750100222160ustar00rootroot00000000000000// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. use std::result; pub type Result = result::Result; // Defines poached from apicdef.h kernel header. pub const APIC_LVT0: usize = 0x350; pub const APIC_LVT1: usize = 0x360; pub const APIC_MODE_NMI: u32 = 0x4; pub const APIC_MODE_EXTINT: u32 = 0x7; pub fn set_apic_delivery_mode(reg: u32, mode: u32) -> u32 { ((reg) & !0x700) | ((mode) << 8) } /// Configures LAPICs. LAPIC0 is set for external interrupts, LAPIC1 is set for NMI. /// /// # Arguments /// * `vcpu` - The VCPU object to configure. pub fn set_lint(vcpu: &dyn hypervisor::Vcpu) -> Result<()> { let mut klapic = vcpu.get_lapic()?; let lvt_lint0 = klapic.get_klapic_reg(APIC_LVT0); klapic.set_klapic_reg( APIC_LVT0, set_apic_delivery_mode(lvt_lint0, APIC_MODE_EXTINT), ); let lvt_lint1 = klapic.get_klapic_reg(APIC_LVT1); klapic.set_klapic_reg(APIC_LVT1, set_apic_delivery_mode(lvt_lint1, APIC_MODE_NMI)); vcpu.set_lapic(&klapic) } cloud-hypervisor-53.0/arch/src/x86_64/layout.rs000066400000000000000000000110731522476750100213170ustar00rootroot00000000000000// Copyright © 2020, Oracle and/or its affiliates. // // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. use vm_memory::GuestAddress; /* Memory layout documentation and constants ~~~~~~ ~~~~~~ ~~~~~~~~~~~~~ ~~~ ~~~~~~~~~ Constants are in order and grouped by range. Take care to update all references when making changes and keep them in order. */ // ** Low RAM (start: 0, length: 640KiB) ** pub const LOW_RAM_START: GuestAddress = GuestAddress(0x0); // == Fixed addresses within the "Low RAM" range: == // Location of EBDA address pub const EBDA_POINTER: GuestAddress = GuestAddress(0x40e); // Initial GDT/IDT needed to boot kernel pub const BOOT_GDT_START: GuestAddress = GuestAddress(0x500); pub const BOOT_IDT_START: GuestAddress = GuestAddress(0x520); /// Address for the hvm_start_info struct used in PVH boot pub const PVH_INFO_START: GuestAddress = GuestAddress(0x6000); /// Starting address of array of modules of hvm_modlist_entry type. /// Used to enable initrd support using the PVH boot ABI. pub const MODLIST_START: GuestAddress = GuestAddress(0x6040); /// Address of memory map table used in PVH boot. Can overlap /// with the zero page address since they are mutually exclusive. pub const MEMMAP_START: GuestAddress = GuestAddress(0x7000); /// The 'zero page', a.k.a linux kernel bootparams. pub const ZERO_PAGE_START: GuestAddress = GuestAddress(0x7000); /// Initial stack for the boot CPU. pub const BOOT_STACK_START: GuestAddress = GuestAddress(0x8000); pub const BOOT_STACK_POINTER: GuestAddress = GuestAddress(0x8ff0); // Initial pagetables. pub const PML5_START: GuestAddress = GuestAddress(0x9000); pub const PML4_START: GuestAddress = GuestAddress(0xa000); pub const PDPTE_START: GuestAddress = GuestAddress(0xb000); pub const PDE_START: GuestAddress = GuestAddress(0xc000); /// Kernel command line start address. pub const CMDLINE_START: GuestAddress = GuestAddress(0x20000); /// Kernel command line start address maximum size. pub const CMDLINE_MAX_SIZE: usize = 0x10000; // MPTABLE, describing VCPUS. pub const MPTABLE_START: GuestAddress = GuestAddress(0x9fc00); // == End of "Low RAM" range. == // ** EBDA reserved area (start: 640KiB, length: 384KiB) ** pub const EBDA_START: GuestAddress = GuestAddress(0xa0000); // == Fixed constants within the "EBDA" range == // ACPI RSDP table pub const RSDP_POINTER: GuestAddress = EBDA_START; pub const SMBIOS_START: u64 = 0xf0000; // First possible location per the spec. // == End of "EBDA" range == // ** High RAM (start: 1MiB, length: 3071MiB) ** pub const HIGH_RAM_START: GuestAddress = GuestAddress(0x100000); // == No fixed addresses in the "High RAM" range == // ** 32-bit reserved area (start: 3GiB, length: 896MiB) ** pub const MEM_32BIT_RESERVED_START: GuestAddress = GuestAddress(0xc000_0000); pub const MEM_32BIT_RESERVED_SIZE: u64 = PCI_MMCONFIG_SIZE + MEM_32BIT_DEVICES_SIZE; // == Fixed constants within the "32-bit reserved" range == // Sub range: 32-bit PCI devices (start: 3GiB, length: 640Mib) pub const MEM_32BIT_DEVICES_START: GuestAddress = MEM_32BIT_RESERVED_START; pub const MEM_32BIT_DEVICES_SIZE: u64 = 640 << 20; // PCI MMCONFIG space (start: after the device space, length: 256MiB) pub const PCI_MMCONFIG_START: GuestAddress = GuestAddress(MEM_32BIT_DEVICES_START.0 + MEM_32BIT_DEVICES_SIZE); pub const PCI_MMCONFIG_SIZE: u64 = 256 << 20; // One bus with potentially 256 devices (32 slots x 8 functions). pub const PCI_MMIO_CONFIG_SIZE_PER_SEGMENT: u64 = 4096 * 256; // TSS is 3 pages after the PCI MMCONFIG space pub const KVM_TSS_START: GuestAddress = GuestAddress(PCI_MMCONFIG_START.0 + PCI_MMCONFIG_SIZE); pub const KVM_TSS_SIZE: u64 = (3 * 4) << 10; // Identity map is a one page region after the TSS pub const KVM_IDENTITY_MAP_START: GuestAddress = GuestAddress(KVM_TSS_START.0 + KVM_TSS_SIZE); pub const KVM_IDENTITY_MAP_SIZE: u64 = 4 << 10; /// TPM Address Range /// This Address range is specific to CRB Interface pub const TPM_START: GuestAddress = GuestAddress(0xfed4_0000); pub const TPM_SIZE: u64 = 0x1000; // IOAPIC pub const IOAPIC_START: GuestAddress = GuestAddress(0xfec0_0000); pub const IOAPIC_SIZE: u64 = 0x20; // APIC pub const APIC_START: GuestAddress = GuestAddress(0xfee0_0000); // == End of "32-bit reserved" range. == // ** 64-bit RAM start (start: 4GiB, length: varies) ** pub const RAM_64BIT_START: GuestAddress = GuestAddress(0x1_0000_0000); cloud-hypervisor-53.0/arch/src/x86_64/mod.rs000066400000000000000000001702401522476750100205630ustar00rootroot00000000000000// Copyright © 2020, Oracle and/or its affiliates. // // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. pub mod cpu_profile; pub mod interrupts; pub mod layout; pub mod regs; #[cfg(feature = "tdx")] pub mod tdx; mod helpers; mod mpspec; mod mptable; mod smbios; use std::arch::x86_64; use helpers::{deserialize_u32_hex, serialize_u32_hex}; use hypervisor::arch::x86::{CPUID_FLAG_VALID_INDEX, CpuIdEntry}; use hypervisor::{CpuVendor, HypervisorCpuError, HypervisorError}; use linux_loader::loader::bootparam::{boot_params, setup_header}; use linux_loader::loader::elf::start_info::{ hvm_memmap_table_entry, hvm_modlist_entry, hvm_start_info, }; use log::{debug, error, info}; pub use smbios::{SmbiosChassisConfig, SmbiosConfig, SmbiosSystem}; use thiserror::Error; use vm_memory::{ Address, Bytes, GuestAddress, GuestAddressSpace, GuestMemory, GuestMemoryAtomic, GuestMemoryRegion, }; use vmm_sys_util::fam; use crate::x86_64::cpu_profile::cpuid_adjustments::MissingCpuidEntriesError; use crate::{CpuProfile, GuestMemoryMmap, InitramfsConfig, RegionType}; // 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 const AMX_TILECFG_BIT: u8 = 17; // AMX tile cfg state component bit const AMX_TILEDATA_BIT: u8 = 18; // AMX tile data state component bit // KVM feature bits #[cfg(feature = "tdx")] const KVM_FEATURE_CLOCKSOURCE_BIT: u8 = 0; #[cfg(feature = "tdx")] const KVM_FEATURE_CLOCKSOURCE2_BIT: u8 = 3; #[cfg(feature = "tdx")] const KVM_FEATURE_CLOCKSOURCE_STABLE_BIT: u8 = 24; #[cfg(feature = "tdx")] const KVM_FEATURE_ASYNC_PF_BIT: u8 = 4; #[cfg(feature = "tdx")] 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)] /// Specifies the entry point address where the guest must start /// executing code, as well as which of the supported boot protocols /// is to be used to configure the guest initial state. pub struct EntryPoint { /// Address in guest memory where the guest must start execution pub entry_addr: GuestAddress, /// This field is used for bzImage to fill the zero page pub setup_header: Option, } const E820_RAM: u32 = 1; const E820_RESERVED: u32 = 2; pub struct CpuidConfig { pub phys_bits: u8, pub kvm_hyperv: bool, #[cfg(feature = "tdx")] pub tdx: bool, pub amx: bool, pub profile: CpuProfile, } #[derive(Debug, Error)] pub enum Error { /// Error writing MP table to memory. #[error("Error writing MP table to memory")] MpTableSetup(#[source] mptable::Error), /// Error configuring the general purpose registers #[error("Error configuring the general purpose registers")] RegsConfiguration(#[source] regs::Error), /// Error configuring the special registers #[error("Error configuring the special registers")] SregsConfiguration(#[source] regs::Error), /// Error configuring the floating point related registers #[error("Error configuring the floating point related registers")] FpuConfiguration(#[source] regs::Error), /// Error configuring the MSR registers #[error("Error configuring the MSR registers")] MsrsConfiguration(#[source] regs::Error), /// Failed to set supported CPUs. #[error("Failed to set supported CPUs")] SetSupportedCpusFailed(#[source] anyhow::Error), /// Cannot set the local interruption due to bad configuration. #[error("Cannot set the local interruption due to bad configuration")] LocalIntConfiguration(#[source] anyhow::Error), /// Error setting up SMBIOS table #[error("Error setting up SMBIOS table")] SmbiosSetup(#[source] smbios::Error), /// Error getting supported CPUID through the hypervisor (kvm/mshv) API #[error("Error getting supported CPUID through the hypervisor API")] CpuidGetSupported(#[source] HypervisorError), /// Error populating CPUID with KVM HyperV emulation details #[error("Error populating CPUID with KVM HyperV emulation details")] CpuidKvmHyperV(#[source] fam::Error), /// Error populating CPUID with CPU identification #[error("Error populating CPUID with CPU identification")] CpuidIdentification(#[source] fam::Error), /// Error checking CPUID compatibility #[error("Error checking CPUID compatibility")] CpuidCheckCompatibility, /// Error checking if CPUID is compatible with profile #[error( "The selected CPU profile cannot be utilized because the host's CPUID entries are not compatible with the profile" )] CpuProfileCpuidIncompatibility, /// Error because TDX cannot be enabled when a custom (non host) CPU profile has been selected #[error("TDX cannot be enabled when a custom CPU profile has been selected")] CpuProfileTdxIncompatibility, /// Error when trying to apply a CPU profile because a necessary CPUID entry was not found #[error( "The selected CPU profile cannot be utilized because a necessary CPUID entry was not found" )] MissingExpectedCpuidEntry(#[source] MissingCpuidEntriesError), // Error writing EBDA address #[error("Error writing EBDA address")] EbdaSetup(#[source] vm_memory::GuestMemoryError), // Error getting CPU TSC frequency #[error("Error getting CPU TSC frequency")] GetTscFrequency(#[source] HypervisorCpuError), /// Error retrieving TDX capabilities through the hypervisor (kvm/mshv) API #[cfg(feature = "tdx")] #[error("Error retrieving TDX capabilities through the hypervisor API")] TdxCapabilities(#[source] HypervisorError), /// Failed to configure E820 map for bzImage #[error("Failed to configure E820 map for bzImage")] E820Configuration, } pub fn get_x2apic_id(cpu_id: u32, topology: Option<(u16, u16, u16, u16)>) -> u32 { if let Some(t) = topology { 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); let die_id = cpu_id / ((t.0 * t.1) as u32) % (t.2 as u32); let socket_id = cpu_id / ((t.0 * t.1 * t.2) as u32); return thread_id | (core_id << thread_mask_width) | (die_id << (thread_mask_width + core_mask_width)) | (socket_id << (thread_mask_width + core_mask_width + die_mask_width)); } cpu_id } 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, serde::Deserialize, serde::Serialize)] pub enum CpuidReg { EAX, EBX, ECX, EDX, } pub struct CpuidPatch { pub function: u32, pub index: u32, pub flags_bit: Option, pub eax_bit: Option, pub ebx_bit: Option, pub ecx_bit: Option, pub edx_bit: Option, } impl CpuidPatch { pub fn get_cpuid_reg( cpuid: &[CpuIdEntry], function: u32, index: Option, reg: CpuidReg, ) -> Option { for entry in cpuid.iter() { if entry.function == function && (index.is_none() || index.unwrap() == entry.index) { return match reg { CpuidReg::EAX => Some(entry.eax), CpuidReg::EBX => Some(entry.ebx), CpuidReg::ECX => Some(entry.ecx), CpuidReg::EDX => Some(entry.edx), }; } } None } pub fn set_cpuid_reg( cpuid: &mut Vec, function: u32, index: Option, reg: CpuidReg, value: u32, ) { let mut entry_found = false; for entry in cpuid.iter_mut() { if entry.function == function && (index.is_none() || index.unwrap() == entry.index) { entry_found = true; match reg { CpuidReg::EAX => { entry.eax = value; } CpuidReg::EBX => { entry.ebx = value; } CpuidReg::ECX => { entry.ecx = value; } CpuidReg::EDX => { entry.edx = value; } } } } if entry_found { return; } // Entry not found, so let's add it. if let Some(index) = index { let mut entry = CpuIdEntry { function, index, flags: CPUID_FLAG_VALID_INDEX, ..Default::default() }; match reg { CpuidReg::EAX => { entry.eax = value; } CpuidReg::EBX => { entry.ebx = value; } CpuidReg::ECX => { entry.ecx = value; } CpuidReg::EDX => { entry.edx = value; } } cpuid.push(entry); } } 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 { if let Some(flags_bit) = patch.flags_bit { entry.flags |= 1 << flags_bit; } if let Some(eax_bit) = patch.eax_bit { entry.eax |= 1 << eax_bit; } if let Some(ebx_bit) = patch.ebx_bit { entry.ebx |= 1 << ebx_bit; } if let Some(ecx_bit) = patch.ecx_bit { entry.ecx |= 1 << ecx_bit; } if let Some(edx_bit) = patch.edx_bit { entry.edx |= 1 << edx_bit; } } } } } pub fn is_feature_enabled( cpuid: &[CpuIdEntry], function: u32, index: u32, reg: CpuidReg, feature_bit: usize, ) -> bool { let mask = 1 << feature_bit; for entry in cpuid { if entry.function == function && entry.index == index { let reg_val = match reg { CpuidReg::EAX => entry.eax, CpuidReg::EBX => entry.ebx, CpuidReg::ECX => entry.ecx, CpuidReg::EDX => entry.edx, }; return (reg_val & mask) == mask; } } false } } #[derive(Debug)] enum CpuidCompatibleCheck { BitwiseSubset, // bitwise subset Equal, // equal in value NumNotGreater, // smaller or equal as a number } pub struct CpuidFeatureEntry { function: u32, index: u32, feature_reg: CpuidReg, compatible_check: CpuidCompatibleCheck, } impl CpuidFeatureEntry { fn checked_feature_entry_list() -> Vec { vec![ // The following list includes all hardware features bits from // the CPUID Wiki Page: https://en.wikipedia.org/wiki/CPUID // Leaf 0x1, ECX/EDX, feature bits CpuidFeatureEntry { function: 1, index: 0, feature_reg: CpuidReg::ECX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 1, index: 0, feature_reg: CpuidReg::EDX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, // Leaf 0x7, EAX/EBX/ECX/EDX, extended features CpuidFeatureEntry { function: 7, index: 0, feature_reg: CpuidReg::EAX, compatible_check: CpuidCompatibleCheck::NumNotGreater, }, CpuidFeatureEntry { function: 7, index: 0, feature_reg: CpuidReg::EBX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 7, index: 0, feature_reg: CpuidReg::ECX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 7, index: 0, feature_reg: CpuidReg::EDX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, // Leaf 0x7 subleaf 0x1, EAX, extended features CpuidFeatureEntry { function: 7, index: 1, feature_reg: CpuidReg::EAX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, // Leaf 0x8000_0001, ECX/EDX, CPUID features bits CpuidFeatureEntry { function: 0x8000_0001, index: 0, feature_reg: CpuidReg::ECX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 0x8000_0001, index: 0, feature_reg: CpuidReg::EDX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, // 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, index: 0, feature_reg: CpuidReg::EAX, compatible_check: CpuidCompatibleCheck::NumNotGreater, }, CpuidFeatureEntry { function: 0x4000_0000, index: 0, feature_reg: CpuidReg::EBX, compatible_check: CpuidCompatibleCheck::Equal, }, CpuidFeatureEntry { function: 0x4000_0000, index: 0, feature_reg: CpuidReg::ECX, compatible_check: CpuidCompatibleCheck::Equal, }, CpuidFeatureEntry { function: 0x4000_0000, index: 0, feature_reg: CpuidReg::EDX, compatible_check: CpuidCompatibleCheck::Equal, }, // Leaf 0x4000_0001, EAX/EBX/ECX/EDX, KVM CPUID features CpuidFeatureEntry { function: 0x4000_0001, index: 0, feature_reg: CpuidReg::EAX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 0x4000_0001, index: 0, feature_reg: CpuidReg::EBX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 0x4000_0001, index: 0, feature_reg: CpuidReg::ECX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, CpuidFeatureEntry { function: 0x4000_0001, index: 0, feature_reg: CpuidReg::EDX, compatible_check: CpuidCompatibleCheck::BitwiseSubset, }, ] } fn get_features_from_cpuid( cpuid: &[CpuIdEntry], feature_entry_list: &[CpuidFeatureEntry], ) -> Vec { let mut features = vec![0; feature_entry_list.len()]; for (i, feature_entry) in feature_entry_list.iter().enumerate() { for cpuid_entry in cpuid { if cpuid_entry.function == feature_entry.function && cpuid_entry.index == feature_entry.index { match feature_entry.feature_reg { CpuidReg::EAX => { features[i] = cpuid_entry.eax; } CpuidReg::EBX => { features[i] = cpuid_entry.ebx; } CpuidReg::ECX => { features[i] = cpuid_entry.ecx; } CpuidReg::EDX => { features[i] = cpuid_entry.edx; } } break; } } } features } /// The function returns `Error` (a.k.a. "incompatible"), when the CPUID features from `src_vm_cpuid` /// is not a subset of those of the `dest_vm_cpuid`. pub fn check_cpuid_compatibility( src_vm_cpuid: &[CpuIdEntry], dest_vm_cpuid: &[CpuIdEntry], ) -> Result<(), Error> { Self::check_cpuid_compatibility_with_descriptions( src_vm_cpuid, "source VM", dest_vm_cpuid, "destination VM", ) } /// Similar to `check_cpuid_compatibility`, but with the possibility to change /// the description of the source and destination for logging purposes. fn check_cpuid_compatibility_with_descriptions( src_vm_cpuid: &[CpuIdEntry], src_description: &str, dest_vm_cpuid: &[CpuIdEntry], dest_description: &str, ) -> Result<(), Error> { let feature_entry_list = &Self::checked_feature_entry_list(); let src_vm_features = Self::get_features_from_cpuid(src_vm_cpuid, feature_entry_list); 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 let mut compatible = true; for (i, (src_vm_feature, dest_vm_feature)) in src_vm_features .iter() .zip(dest_vm_features.iter()) .enumerate() { let entry = &feature_entry_list[i]; let entry_compatible = match entry.compatible_check { CpuidCompatibleCheck::BitwiseSubset => { let different_feature_bits = src_vm_feature ^ dest_vm_feature; let src_vm_feature_bits_only = different_feature_bits & src_vm_feature; src_vm_feature_bits_only == 0 } CpuidCompatibleCheck::Equal => src_vm_feature == dest_vm_feature, CpuidCompatibleCheck::NumNotGreater => src_vm_feature <= dest_vm_feature, }; if !entry_compatible { error!( "Detected incompatible CPUID entry: leaf={:#04x} (subleaf={:#04x}), register='{:?}', \ compatible_check='{:?}', {src_description} feature='{:#04x}', {dest_description} feature='{:#04x}'.", entry.function, entry.index, entry.feature_reg, entry.compatible_check, src_vm_feature, dest_vm_feature ); compatible = false; } } if compatible { info!("No CPU incompatibility detected."); Ok(()) } else { Err(Error::CpuidCheckCompatibility) } } } /// Generate the CPUID entries intended for every vCPU. /// /// ## CPU profiles /// /// This function takes the CPU profile given in `config` into account and returns compatible CPUID entries /// if possible. /// /// An error is returned when the CPUID entries obtained from the hypervisor do not satisfy the requirements /// to apply the selected CPU profile. pub fn generate_common_cpuid( hypervisor: &dyn hypervisor::Hypervisor, config: &CpuidConfig, ) -> 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 let hypervisor_cpuid = unsafe { x86_64::__cpuid(0x4000_0000) }; let mut identifier: [u8; 12] = [0; 12]; identifier[0..4].copy_from_slice(&hypervisor_cpuid.ebx.to_le_bytes()[..]); identifier[4..8].copy_from_slice(&hypervisor_cpuid.ecx.to_le_bytes()[..]); identifier[8..12].copy_from_slice(&hypervisor_cpuid.edx.to_le_bytes()[..]); info!( "Running under nested virtualisation. Hypervisor string: {}", String::from_utf8_lossy(&identifier) ); } info!( "Generating guest CPUID with physical address size: {}", config.phys_bits ); // Supported CPUID let mut cpuid = hypervisor .get_supported_cpuid() .map_err(Error::CpuidGetSupported)?; let is_non_host_profile = !matches!(config.profile, CpuProfile::Host); #[cfg(feature = "tdx")] if config.tdx { if is_non_host_profile { // TDX is not supported by CPU profiles other than host for the time being. return Err(Error::CpuProfileTdxIncompatibility.into()); } common_cpuid_tdx_configuration(&mut cpuid, hypervisor)?; } // Copy CPU identification string for i in 0x8000_0002..=0x8000_0004 { cpuid.retain(|c| c.function != i); // SAFETY: call cpuid with valid leaves #[allow(unused_unsafe)] let leaf = unsafe { x86_64::__cpuid(i) }; cpuid.push(CpuIdEntry { function: i, eax: leaf.eax, ebx: leaf.ebx, ecx: leaf.ecx, edx: leaf.edx, ..Default::default() }); } let cpuid_profile = if is_non_host_profile { let cpuid_profile = config .profile .adjust_cpuid(cpuid.clone(), config.amx, hypervisor.get_cpu_vendor()) .map_err(Error::MissingExpectedCpuidEntry)?; required_common_cpuid_updates( cpuid_profile, config, #[cfg(feature = "kvm")] hypervisor.hypervisor_type(), ) } else { Vec::new() }; let cpuid_host = required_common_cpuid_updates( cpuid, config, #[cfg(feature = "kvm")] hypervisor.hypervisor_type(), ); // If we want to apply a CPU profile we need to check that it remains compatible with `cpuid_host` if is_non_host_profile { CpuidFeatureEntry::check_cpuid_compatibility_with_descriptions( &cpuid_profile, "CPU Profile", &cpuid_host, "Host VM", ) .map_err(|_| Error::CpuProfileCpuidIncompatibility)?; Ok(cpuid_profile) } else { Ok(cpuid_host) } } /// Apply updates to common CPUID (not vCPU specific) that are necessary regardless of /// the chosen CPU profile. fn required_common_cpuid_updates( mut cpuid: Vec, config: &CpuidConfig, #[cfg(feature = "kvm")] hypervisor_type: hypervisor::HypervisorType, ) -> Vec { #[allow(unused_mut)] let mut cpuid_patches = vec![ // Patch hypervisor bit CpuidPatch { function: 1, index: 0, flags_bit: None, eax_bit: None, ebx_bit: None, ecx_bit: Some(HYPERVISOR_ECX_BIT), edx_bit: None, }, // Enable MTRR feature CpuidPatch { function: 1, index: 0, flags_bit: None, eax_bit: None, ebx_bit: None, ecx_bit: None, edx_bit: Some(MTRR_EDX_BIT), }, ]; #[cfg(feature = "kvm")] if matches!(hypervisor_type, hypervisor::HypervisorType::Kvm) { // Patch tsc deadline timer bit cpuid_patches.push(CpuidPatch { function: 1, index: 0, flags_bit: None, eax_bit: None, ebx_bit: None, ecx_bit: Some(TSC_DEADLINE_TIMER_ECX_BIT), edx_bit: None, }); } CpuidPatch::patch_cpuid(&mut cpuid, &cpuid_patches); // Update some existing CPUID for entry in cpuid.as_mut_slice().iter_mut() { #[allow(unused_unsafe)] 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); } } // Tile Information (purely AMX related). 0x1d if !config.amx => { entry.eax = 0; entry.ebx = 0; entry.ecx = 0; entry.edx = 0; } // TMUL information (purely AMX related) 0x1e 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 0x8000_0005 if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 // SAFETY: cpuid called with valid leaves && unsafe { x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0005 => { // SAFETY: cpuid called with valid leaves let leaf = unsafe { 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 0x8000_0006 if entry.eax == 0 && entry.ebx == 0 && entry.ecx == 0 && entry.edx == 0 // SAFETY: cpuid called with valid leaves && unsafe { x86_64::__cpuid(0x8000_0000).eax } >= 0x8000_0006 => { // SAFETY: cpuid called with valid leaves let leaf = unsafe { x86_64::__cpuid(0x8000_0006) }; entry.eax = leaf.eax; entry.ebx = leaf.ebx; entry.ecx = leaf.ecx; entry.edx = leaf.edx; } // Set CPU physical bits and guest physical bits 0x8000_0008 => { entry.eax = (entry.eax & 0xff00_ff00) | (config.phys_bits as u32 & 0xff) | ((config.phys_bits as u32 & 0xff) << 16); } 0x4000_0001 => { // Enable KVM_FEATURE_MSI_EXT_DEST_ID. This allows the guest to target // 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)); } } _ => {} } } 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 << 0) // AccessVpRunTimeReg | (1 << 1) // AccessPartitionReferenceCounter | (1 << 2) // AccessSynicRegs | (1 << 3) // AccessSyntheticTimerRegs | (1 << 4) // AccessIntrCtrlRegs (APIC access MSRs / VP Assist EOI) | (1 << 5) // AccessHypercallMsrs | (1 << 6) // AccessVpIndex | (1 << 9) // AccessPartitionReferenceTsc | (1 << 11), // AccessFrequencyMsrs (TSC/APIC frequency MSRs) edx: (1 << 3) // CPU dynamic partitioning | (1 << 4) // FastHypercall (XMM register hypercall input) | (1 << 8), // ExtendedGvaRangesForFlushVirtualAddressList ..Default::default() }); cpuid.push(CpuIdEntry { function: 0x4000_0004, // Recommendation hints to Hyper-V-aware guests. Bit semantics per // Microsoft Hypervisor Top-Level Functional Specification 7.4.5. eax: (1 << 1) // LocalTlbFlushRecommended | (1 << 2) // RemoteTlbFlushRecommended | (1 << 3) // ApicAccessRecommended (VP Assist page MSR EOI/ICR/TPR) | (1 << 5) // RelaxedTimingRecommended | (1 << 9) // DeprecatingAeoiRecommended (keeps APICv on with SynIC) | (1 << 10), // ClusterIpiRecommended (HvCallSendSyntheticClusterIpi) ebx: 0xfff, // Suggested spinlock retry attempts before trapping to host ..Default::default() }); for i in 0x4000_0005..=0x4000_000a { cpuid.push(CpuIdEntry { function: i, ..Default::default() }); } } cpuid } #[cfg(feature = "tdx")] fn common_cpuid_tdx_configuration( cpuid: &mut [CpuIdEntry], hypervisor: &dyn hypervisor::Hypervisor, ) -> super::Result<()> { let caps = hypervisor .tdx_capabilities() .map_err(Error::TdxCapabilities)?; info!("TDX capabilities {caps:#?}"); for entry in cpuid.iter_mut().filter(|entry| entry.function == 0xd) { let xcr0_mask: u64 = 0x82ff; let xss_mask: u64 = !xcr0_mask; if entry.index == 0 { entry.eax &= (caps.xfam_fixed0 as u32) & (xcr0_mask as u32); entry.eax |= (caps.xfam_fixed1 as u32) & (xcr0_mask as u32); entry.edx &= ((caps.xfam_fixed0 & xcr0_mask) >> 32) as u32; entry.edx |= ((caps.xfam_fixed1 & xcr0_mask) >> 32) as u32; } else if entry.index == 1 { entry.ecx &= (caps.xfam_fixed0 as u32) & (xss_mask as u32); entry.ecx |= (caps.xfam_fixed1 as u32) & (xss_mask as u32); entry.edx &= ((caps.xfam_fixed0 & xss_mask) >> 32) as u32; entry.edx |= ((caps.xfam_fixed1 & xss_mask) >> 32) as u32; } } Ok(()) } #[expect(clippy::too_many_arguments)] pub fn configure_vcpu( vcpu: &dyn hypervisor::Vcpu, id: u32, boot_setup: Option<(EntryPoint, &GuestMemoryAtomic)>, cpuid: Vec, kvm_hyperv: bool, cpu_vendor: CpuVendor, topology: (u16, u16, u16, u16), nested: bool, setup_registers: bool, ) -> super::Result<()> { let x2apic_id = get_x2apic_id(id, Some(topology)); // Per vCPU CPUID changes; common are handled via generate_common_cpuid() let mut cpuid = cpuid; CpuidPatch::set_cpuid_reg(&mut cpuid, 0xb, None, CpuidReg::EDX, x2apic_id); CpuidPatch::set_cpuid_reg(&mut cpuid, 0x1f, None, CpuidReg::EDX, x2apic_id); if matches!(cpu_vendor, CpuVendor::AMD) { CpuidPatch::set_cpuid_reg(&mut cpuid, 0x8000_001e, Some(0), CpuidReg::EAX, x2apic_id); } // Set ApicId in cpuid for each vcpu - found in cpuid ebx when eax = 1 let mut apic_id_patched = false; for entry in &mut cpuid { if entry.function == 1 { 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); 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 && 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 { 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}"); } vcpu.set_cpuid2(&cpuid) .map_err(|e| Error::SetSupportedCpusFailed(e.into()))?; if kvm_hyperv { vcpu.enable_hyperv_synic().unwrap(); } regs::setup_msrs(vcpu).map_err(Error::MsrsConfiguration)?; if let Some((kernel_entry_point, guest_memory)) = boot_setup { if setup_registers { regs::setup_regs(vcpu, kernel_entry_point).map_err(Error::RegsConfiguration)?; // CPUs are required (by Intel sdm spec) to boot in x2apic mode if any // of the apic IDs is larger than 255. Experimentally, the Linux kernel // does not recognize the last vCPU if x2apic is not enabled when // there are 256 vCPUs in a flat hierarchy (i.e. max x2apic ID is 255), // so we need to enable x2apic in this case as well. let enable_x2_apic_mode = get_max_x2apic_id(topology) > MAX_SUPPORTED_CPUS_LEGACY; regs::setup_sregs(&guest_memory.memory(), vcpu, enable_x2_apic_mode) .map_err(Error::SregsConfiguration)?; } regs::setup_fpu(vcpu).map_err(Error::FpuConfiguration)?; } interrupts::set_lint(vcpu).map_err(|e| Error::LocalIntConfiguration(e.into()))?; Ok(()) } /// Returns a Vec of the valid memory addresses. /// /// These should be used to configure the GuestMemory structure for the platform. /// For x86_64 all addresses are valid from the start of the kernel except a /// carve out at the end of 32bit address space. pub fn arch_memory_regions() -> Vec<(GuestAddress, usize, RegionType)> { vec![ // 0 GiB ~ 3GiB: memory before the gap ( GuestAddress(0), layout::MEM_32BIT_RESERVED_START.raw_value() as usize, RegionType::Ram, ), // 4 GiB ~ inf: memory after the gap (layout::RAM_64BIT_START, usize::MAX, RegionType::Ram), // 3 GiB ~ 3712 MiB: 32-bit device memory hole ( layout::MEM_32BIT_RESERVED_START, layout::MEM_32BIT_DEVICES_SIZE as usize, RegionType::SubRegion, ), // 3712 MiB ~ 3968 MiB: 32-bit reserved memory hole ( layout::MEM_32BIT_RESERVED_START.unchecked_add(layout::MEM_32BIT_DEVICES_SIZE), (layout::MEM_32BIT_RESERVED_SIZE - layout::MEM_32BIT_DEVICES_SIZE) as usize, RegionType::Reserved, ), ] } /// Configures the system and should be called once per vm before starting vcpu threads. /// /// # Arguments /// /// * `guest_mem` - The memory to be used by the guest. /// * `cmdline_addr` - Address in `guest_mem` where the kernel command line was loaded. /// * `cmdline_size` - Size of the kernel command line in bytes including the null terminator. /// * `num_cpus` - Number of virtual CPUs the guest will have. #[expect(clippy::too_many_arguments)] pub fn configure_system( guest_mem: &GuestMemoryMmap, cmdline_addr: GuestAddress, cmdline_size: usize, initramfs: &Option, _num_cpus: u32, setup_header: Option, rsdp_addr: Option, 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, 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); let offset = GuestAddress((offset.0 + 16) & !0xf); 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 && rsdp_addr.0 > guest_mem.last_addr().0 { return Err(super::Error::RsdpPastRamEnd); } match setup_header { Some(hdr) => configure_32bit_entry( guest_mem, cmdline_addr, cmdline_size, initramfs, hdr, rsdp_addr, ), None => configure_pvh(guest_mem, cmdline_addr, initramfs, rsdp_addr), } } type RamRange = (u64, u64); /// Returns usable physical memory ranges for the guest /// These should be used to create e820_RAM memory maps pub fn generate_ram_ranges(guest_mem: &GuestMemoryMmap) -> super::Result> { // Merge continuous memory regions into one region. // Note: memory regions from "GuestMemory" are sorted and non-zero sized. let ram_regions = { let mut ram_regions = Vec::new(); let mut current_start = guest_mem .iter() .next() .map(GuestMemoryRegion::start_addr) .expect("GuestMemory must have one memory region at least") .raw_value(); let mut current_end = current_start; for (start, size) in guest_mem .iter() .map(|m| (m.start_addr().raw_value(), m.len())) { if current_end == start { // This zone is continuous with the previous one. current_end += size; } else { ram_regions.push((current_start, current_end)); current_start = start; current_end = start + size; } } ram_regions.push((current_start, current_end)); ram_regions }; // Create the memory map entry for memory region before the gap let mut ram_ranges = vec![]; // Generate the first usable physical memory range before the gap. The e820 map // should only report memory above 1MiB. let first_ram_range = { let (first_region_start, first_region_end) = ram_regions .first() .ok_or(super::Error::MemmapTableGeneration)?; let high_ram_start = layout::HIGH_RAM_START.raw_value(); let mem_32bit_reserved_start = layout::MEM_32BIT_RESERVED_START.raw_value(); if !((first_region_start <= &high_ram_start) && (first_region_end > &high_ram_start) && (first_region_end <= &mem_32bit_reserved_start)) { error!( "Unexpected first memory region layout: (start: 0x{first_region_start:08x}, end: 0x{first_region_end:08x}). high_ram_start: 0x{high_ram_start:08x}, mem_32bit_reserved_start: 0x{mem_32bit_reserved_start:08x}" ); return Err(super::Error::MemmapTableGeneration); } info!( "first usable physical memory range, start: 0x{high_ram_start:08x}, end: 0x{first_region_end:08x}" ); (high_ram_start, *first_region_end) }; ram_ranges.push(first_ram_range); // Generate additional usable physical memory range after the gap if any. for ram_region in ram_regions.iter().skip(1) { info!( "found usable physical memory range, start: 0x{:08x}, end: 0x{:08x}", ram_region.0, ram_region.1 ); ram_ranges.push(*ram_region); } Ok(ram_ranges) } fn configure_pvh( guest_mem: &GuestMemoryMmap, cmdline_addr: GuestAddress, initramfs: &Option, rsdp_addr: Option, ) -> super::Result<()> { const XEN_HVM_START_MAGIC_VALUE: u32 = 0x336ec578; let mut start_info = hvm_start_info { magic: XEN_HVM_START_MAGIC_VALUE, version: 1, // pvh has version 1 nr_modules: 0, cmdline_paddr: cmdline_addr.raw_value(), memmap_paddr: layout::MEMMAP_START.raw_value(), ..Default::default() }; if let Some(rsdp_addr) = rsdp_addr { start_info.rsdp_paddr = rsdp_addr.0; } if let Some(initramfs_config) = initramfs { // The initramfs has been written to guest memory already, here we just need to // create the module structure that describes it. let ramdisk_mod = hvm_modlist_entry { paddr: initramfs_config.address.raw_value(), size: initramfs_config.size as u64, ..Default::default() }; start_info.nr_modules += 1; start_info.modlist_paddr = layout::MODLIST_START.raw_value(); // Write the modlist struct to guest memory. guest_mem .write_obj(ramdisk_mod, layout::MODLIST_START) .map_err(super::Error::ModlistSetup)?; } // Vector to hold the memory maps which needs to be written to guest memory // at MEMMAP_START after all of the mappings are recorded. let mut memmap: Vec = Vec::new(); // Create the memory map entries. add_memmap_entry(&mut memmap, 0, layout::EBDA_START.raw_value(), E820_RAM); // Get usable physical memory ranges let ram_ranges = generate_ram_ranges(guest_mem)?; // Create e820 memory map entries for ram_range in ram_ranges { info!( "create_memmap_entry, start: 0x{:08x}, end: 0x{:08x}", ram_range.0, ram_range.1 ); add_memmap_entry( &mut memmap, ram_range.0, ram_range.1 - ram_range.0, E820_RAM, ); } add_memmap_entry( &mut memmap, layout::PCI_MMCONFIG_START.0, layout::PCI_MMCONFIG_SIZE, E820_RESERVED, ); start_info.memmap_entries = memmap.len() as u32; // Copy the vector with the memmap table to the MEMMAP_START address // which is already saved in the memmap_paddr field of hvm_start_info struct. let mut memmap_start_addr = layout::MEMMAP_START; guest_mem .checked_offset( memmap_start_addr, size_of::() * start_info.memmap_entries as usize, ) .ok_or(super::Error::MemmapTablePastRamEnd)?; // For every entry in the memmap vector, write it to guest memory. for memmap_entry in memmap { guest_mem .write_obj(memmap_entry, memmap_start_addr) .map_err(super::Error::MemmapTableSetup)?; memmap_start_addr = memmap_start_addr.unchecked_add(size_of::() as u64); } // The hvm_start_info struct itself must be stored at PVH_START_INFO // address, and %rbx will be initialized to contain PVH_INFO_START prior to // starting the guest, as required by the PVH ABI. let start_info_addr = layout::PVH_INFO_START; guest_mem .checked_offset(start_info_addr, size_of::()) .ok_or(super::Error::StartInfoPastRamEnd)?; // Write the start_info struct to guest memory. guest_mem .write_obj(start_info, start_info_addr) .map_err(super::Error::StartInfoSetup)?; Ok(()) } fn configure_32bit_entry( guest_mem: &GuestMemoryMmap, cmdline_addr: GuestAddress, cmdline_size: usize, initramfs: &Option, setup_hdr: setup_header, rsdp_addr: Option, ) -> super::Result<()> { const KERNEL_LOADER_OTHER: u8 = 0xff; // Use the provided setup header let mut params = boot_params { hdr: setup_hdr, ..Default::default() }; // Common bootparams settings if params.hdr.type_of_loader == 0 { params.hdr.type_of_loader = KERNEL_LOADER_OTHER; } params.hdr.cmd_line_ptr = cmdline_addr.raw_value() as u32; params.hdr.cmdline_size = cmdline_size as u32; if let Some(initramfs_config) = initramfs { params.hdr.ramdisk_image = initramfs_config.address.raw_value() as u32; params.hdr.ramdisk_size = initramfs_config.size as u32; } add_e820_entry(&mut params, 0, layout::EBDA_START.raw_value(), E820_RAM)?; let mem_end = guest_mem.last_addr(); if mem_end < layout::MEM_32BIT_RESERVED_START { add_e820_entry( &mut params, layout::HIGH_RAM_START.raw_value(), mem_end.unchecked_offset_from(layout::HIGH_RAM_START) + 1, E820_RAM, )?; } else { add_e820_entry( &mut params, layout::HIGH_RAM_START.raw_value(), layout::MEM_32BIT_RESERVED_START.unchecked_offset_from(layout::HIGH_RAM_START), E820_RAM, )?; if mem_end > layout::RAM_64BIT_START { add_e820_entry( &mut params, layout::RAM_64BIT_START.raw_value(), mem_end.unchecked_offset_from(layout::RAM_64BIT_START) + 1, E820_RAM, )?; } } add_e820_entry( &mut params, layout::PCI_MMCONFIG_START.0, layout::PCI_MMCONFIG_SIZE, E820_RESERVED, )?; if let Some(rsdp_addr) = rsdp_addr { params.acpi_rsdp_addr = rsdp_addr.0; } let zero_page_addr = layout::ZERO_PAGE_START; guest_mem .checked_offset(zero_page_addr, size_of::()) .ok_or(super::Error::ZeroPagePastRamEnd)?; guest_mem .write_obj(params, zero_page_addr) .map_err(super::Error::ZeroPageSetup)?; Ok(()) } /// Add an e820 region to the e820 map. /// Returns Ok(()) if successful, or an error if there is no space left in the map. fn add_e820_entry( params: &mut boot_params, addr: u64, size: u64, mem_type: u32, ) -> Result<(), Error> { if params.e820_entries >= params.e820_table.len() as u8 { return Err(Error::E820Configuration); } params.e820_table[params.e820_entries as usize].addr = addr; params.e820_table[params.e820_entries as usize].size = size; params.e820_table[params.e820_entries as usize].type_ = mem_type; params.e820_entries += 1; Ok(()) } fn add_memmap_entry(memmap: &mut Vec, addr: u64, size: u64, mem_type: u32) { // Add the table entry to the vector memmap.push(hvm_memmap_table_entry { addr, size, type_: mem_type, reserved: 0, }); } /// Returns the memory address where the initramfs could be loaded. pub fn initramfs_load_addr( guest_mem: &GuestMemoryMmap, initramfs_size: usize, ) -> super::Result { let first_region = guest_mem .find_region(GuestAddress::new(0)) .ok_or(super::Error::InitramfsAddress)?; // It's safe to cast to usize because the size of a region can't be greater than usize. let lowmem_size = first_region.len() as usize; if lowmem_size < initramfs_size { return Err(super::Error::InitramfsAddress); } let aligned_addr: u64 = ((lowmem_size - initramfs_size) & !(crate::pagesize() - 1)) as u64; Ok(aligned_addr) } 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); if leaf.eax >= 0x8000_0008 { let leaf = x86_64::__cpuid(0x8000_0008); (leaf.eax & 0xff) as u8 } else { 36 } } } fn update_cpuid_topology( cpuid: &mut Vec, threads_per_core: u16, cores_per_die: u16, dies_per_package: u16, packages: u16, cpu_vendor: CpuVendor, id: u32, ) { let x2apic_id = get_x2apic_id( id, Some((threads_per_core, cores_per_die, dies_per_package, packages)), ); // 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 &= !(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( cpuid, 0xb, Some(0), CpuidReg::EBX, u32::from(threads_per_core), ); CpuidPatch::set_cpuid_reg(cpuid, 0xb, Some(0), CpuidReg::ECX, 1 << 8); CpuidPatch::set_cpuid_reg(cpuid, 0xb, Some(1), CpuidReg::EAX, die_width); CpuidPatch::set_cpuid_reg( cpuid, 0xb, Some(1), CpuidReg::EBX, u32::from(dies_per_package * cores_per_die * threads_per_core), ); 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( cpuid, 0x1f, Some(0), CpuidReg::EBX, 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( cpuid, 0x1f, Some(1), CpuidReg::EBX, 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( cpuid, 0x1f, Some(2), CpuidReg::EBX, 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( cpuid, 0x8000_001e, Some(0), CpuidReg::EBX, ((threads_per_core as u32 - 1) << 8) | (x2apic_id & 0xff), ); CpuidPatch::set_cpuid_reg( cpuid, 0x8000_001e, Some(0), CpuidReg::ECX, ((dies_per_package as u32 - 1) << 8) | (thread_width + die_width) & 0xff, ); CpuidPatch::set_cpuid_reg(cpuid, 0x8000_001e, Some(0), CpuidReg::EDX, 0); if cores_per_die * threads_per_core > 1 { let ecx = CpuidPatch::get_cpuid_reg(cpuid, 0x8000_0001, Some(0), CpuidReg::ECX).unwrap_or(0); CpuidPatch::set_cpuid_reg( cpuid, 0x8000_0001, Some(0), CpuidReg::ECX, ecx | (1u32 << 1) | (1u32 << 22), ); CpuidPatch::set_cpuid_reg( cpuid, 0x0000_0001, Some(0), CpuidReg::EBX, (x2apic_id << 24) | (8 << 8) | (((cores_per_die * threads_per_core) as u32) << 16), ); let cpuid_patches = vec![ // Patch tsc deadline timer bit CpuidPatch { function: 1, index: 0, flags_bit: None, eax_bit: None, ebx_bit: None, ecx_bit: None, edx_bit: Some(28), }, ]; CpuidPatch::patch_cpuid(cpuid, &cpuid_patches); CpuidPatch::set_cpuid_reg( cpuid, 0x8000_0008, Some(0), CpuidReg::ECX, ((thread_width + core_width + die_width) << 12) | ((cores_per_die * threads_per_core) - 1) as u32, ); } else { CpuidPatch::set_cpuid_reg(cpuid, 0x8000_0008, Some(0), CpuidReg::ECX, 0u32); } } } #[cfg(test)] mod unit_tests { use linux_loader::loader::bootparam::boot_e820_entry; use super::*; #[test] fn regions_base_addr() { let regions = arch_memory_regions(); assert_eq!(4, regions.len()); assert_eq!(GuestAddress(0), regions[0].0); assert_eq!(GuestAddress(1 << 32), regions[1].0); } #[test] fn test_system_configuration() { let no_vcpus = 4; let gm = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap(); let config_err = configure_system( &gm, GuestAddress(0), 0, &None, 1, None, Some(layout::RSDP_POINTER), None, None, ); config_err.unwrap_err(); // Now assigning some memory that falls before the 32bit memory hole. let arch_mem_regions = arch_memory_regions(); let ram_regions: Vec<(GuestAddress, usize)> = arch_mem_regions .iter() .filter(|r| r.2 == RegionType::Ram && r.1 != usize::MAX) .map(|r| (r.0, r.1)) .collect(); let gm = GuestMemoryMmap::from_ranges(&ram_regions).unwrap(); configure_system( &gm, GuestAddress(0), 0, &None, no_vcpus, None, None, None, None, ) .unwrap(); // Now assigning some memory that falls after the 32bit memory hole. let arch_mem_regions = arch_memory_regions(); let ram_regions: Vec<(GuestAddress, usize)> = arch_mem_regions .iter() .filter(|r| r.2 == RegionType::Ram) .map(|r| { if r.1 == usize::MAX { (r.0, 128 << 20) } else { (r.0, r.1) } }) .collect(); let gm = GuestMemoryMmap::from_ranges(&ram_regions).unwrap(); configure_system( &gm, GuestAddress(0), 0, &None, no_vcpus, None, None, None, None, ) .unwrap(); configure_system( &gm, GuestAddress(0), 0, &None, no_vcpus, None, None, None, None, ) .unwrap(); } #[test] fn test_add_e820_entry() { let e820_table = [(boot_e820_entry { addr: 0x1, size: 4, type_: 1, }); 128]; let expected_params = boot_params { e820_table, e820_entries: 1, ..Default::default() }; let mut params: boot_params = Default::default(); add_e820_entry( &mut params, e820_table[0].addr, e820_table[0].size, e820_table[0].type_, ) .unwrap(); assert_eq!( format!("{:?}", params.e820_table[0]), format!("{:?}", expected_params.e820_table[0]) ); assert_eq!(params.e820_entries, expected_params.e820_entries); // Exercise the scenario where the field storing the length of the e820 entry table is // is bigger than the allocated memory. params.e820_entries = params.e820_table.len() as u8 + 1; add_e820_entry( &mut params, e820_table[0].addr, e820_table[0].size, e820_table[0].type_, ) .unwrap_err(); } #[test] fn test_add_memmap_entry() { let mut memmap: Vec = Vec::new(); let expected_memmap = vec![ hvm_memmap_table_entry { addr: 0x0, size: 0x1000, type_: E820_RAM, ..Default::default() }, hvm_memmap_table_entry { addr: 0x10000, size: 0xa000, type_: E820_RESERVED, ..Default::default() }, ]; add_memmap_entry(&mut memmap, 0, 0x1000, E820_RAM); add_memmap_entry(&mut memmap, 0x10000, 0xa000, E820_RESERVED); assert_eq!(format!("{memmap:?}"), format!("{expected_memmap:?}")); } #[test] fn test_get_x2apic_id() { 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, 1))); assert_eq!(x2apic_id, 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, 1))); assert_eq!(x2apic_id, 8); 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, 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))); } } cloud-hypervisor-53.0/arch/src/x86_64/mpspec.rs000066400000000000000000000133531522476750100212740ustar00rootroot00000000000000// Copyright 2017 The Chromium OS Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. use std::os::raw; use vm_memory::ByteValued; pub const MP_PROCESSOR: raw::c_uint = 0; pub const MP_BUS: raw::c_uint = 1; pub const MP_IOAPIC: raw::c_uint = 2; pub const MP_INTSRC: raw::c_uint = 3; pub const MP_LINTSRC: raw::c_uint = 4; pub const CPU_ENABLED: raw::c_uint = 1; pub const CPU_BOOTPROCESSOR: raw::c_uint = 2; pub const MPC_APIC_USABLE: raw::c_uint = 1; pub const MP_IRQDIR_DEFAULT: raw::c_uint = 0; #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpf_intel { pub signature: [raw::c_uchar; 4usize], pub physptr: raw::c_uint, pub length: raw::c_uchar, pub specification: raw::c_uchar, pub checksum: raw::c_uchar, pub feature1: raw::c_uchar, pub feature2: raw::c_uchar, pub feature3: raw::c_uchar, pub feature4: raw::c_uchar, pub feature5: raw::c_uchar, } const _: () = assert!(size_of::() == 16); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpf_intel {} #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_table { pub signature: [raw::c_uchar; 4usize], pub length: raw::c_ushort, pub spec: raw::c_uchar, pub checksum: raw::c_uchar, pub oem: [raw::c_uchar; 8usize], pub productid: [raw::c_uchar; 12usize], pub oemptr: raw::c_uint, pub oemsize: raw::c_ushort, pub oemcount: raw::c_ushort, pub lapic: raw::c_uint, pub reserved: raw::c_uint, } const _: () = { assert!(size_of::() == 4 + 2 + 1 + 1 + 8 + 12 + 4 + 2 + 2 + 4 + 4); assert!(size_of::() == 4); assert!(size_of::() == 2); assert!(size_of::() == 1); }; // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_table {} #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_cpu { pub type_: raw::c_uchar, pub apicid: raw::c_uchar, pub apicver: raw::c_uchar, pub cpuflag: raw::c_uchar, pub cpufeature: raw::c_uint, pub featureflag: raw::c_uint, pub reserved: [raw::c_uint; 2usize], } const _: () = assert!(size_of::() == 20); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_cpu {} #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_bus { pub type_: raw::c_uchar, pub busid: raw::c_uchar, pub bustype: [raw::c_uchar; 6usize], } const _: () = assert!(size_of::() == 8); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_bus {} #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_ioapic { pub type_: raw::c_uchar, pub apicid: raw::c_uchar, pub apicver: raw::c_uchar, pub flags: raw::c_uchar, pub apicaddr: raw::c_uint, } const _: () = assert!(size_of::() == 8); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_ioapic {} #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_intsrc { pub type_: raw::c_uchar, pub irqtype: raw::c_uchar, pub irqflag: raw::c_ushort, pub srcbus: raw::c_uchar, pub srcbusirq: raw::c_uchar, pub dstapic: raw::c_uchar, pub dstirq: raw::c_uchar, } const _: () = assert!(size_of::() == 8); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_intsrc {} pub const MP_IRQ_SOURCE_TYPES_MP_INT: raw::c_uint = 0; pub const MP_IRQ_SOURCE_TYPES_MP_NMI: raw::c_uint = 1; pub const MP_IRQ_SOURCE_TYPES_MP_EXT_INT: raw::c_uint = 3; #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_lintsrc { pub type_: raw::c_uchar, pub irqtype: raw::c_uchar, pub irqflag: raw::c_ushort, pub srcbusid: raw::c_uchar, pub srcbusirq: raw::c_uchar, pub destapic: raw::c_uchar, pub destapiclint: raw::c_uchar, } const _: () = assert!(size_of::() == 8); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_lintsrc {} #[repr(C)] #[derive(Debug, Default, Copy, Clone)] pub struct mpc_oemtable { pub signature: [raw::c_uchar; 4usize], pub length: raw::c_ushort, pub rev: raw::c_uchar, pub checksum: raw::c_uchar, pub mpc: [raw::c_uchar; 8usize], } const _: () = assert!(size_of::() == 16); // SAFETY: all members of this struct are plain integers // and the sum of their sizes is the size of the struct, so // padding and reserved values are not possible as there // would be nowhere for them to exist. unsafe impl ByteValued for mpc_oemtable {} cloud-hypervisor-53.0/arch/src/x86_64/mptable.rs000066400000000000000000000377551522476750100214450ustar00rootroot00000000000000// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. use std::result; use libc::c_uchar; use log::{info, warn}; use thiserror::Error; use vm_memory::{Address, ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError}; use super::MAX_SUPPORTED_CPUS_LEGACY; use crate::GuestMemoryMmap; use crate::layout::{APIC_START, HIGH_RAM_START, IOAPIC_START}; use crate::x86_64::{get_x2apic_id, mpspec}; // This is a workaround to the Rust enforcement specifying that any implementation of a foreign // trait (in this case `ByteValued`) where: // * the type that is implementing the trait is foreign or // * all of the parameters being passed to the trait (if there are any) are also foreign // is prohibited. #[derive(Copy, Clone, Default)] struct MpcBusWrapper(mpspec::mpc_bus); #[derive(Copy, Clone, Default)] struct MpcCpuWrapper(mpspec::mpc_cpu); #[derive(Copy, Clone, Default)] struct MpcIntsrcWrapper(mpspec::mpc_intsrc); #[derive(Copy, Clone, Default)] struct MpcIoapicWrapper(mpspec::mpc_ioapic); #[derive(Copy, Clone, Default)] struct MpcTableWrapper(mpspec::mpc_table); #[derive(Copy, Clone, Default)] struct MpcLintsrcWrapper(mpspec::mpc_lintsrc); #[derive(Copy, Clone, Default)] struct MpfIntelWrapper(mpspec::mpf_intel); // SAFETY: These `mpspec` wrapper types are only data, reading them from data is a safe initialization. unsafe impl ByteValued for MpcBusWrapper {} // SAFETY: see above unsafe impl ByteValued for MpcCpuWrapper {} // SAFETY: see above unsafe impl ByteValued for MpcIntsrcWrapper {} // SAFETY: see above unsafe impl ByteValued for MpcIoapicWrapper {} // SAFETY: see above unsafe impl ByteValued for MpcTableWrapper {} // SAFETY: see above unsafe impl ByteValued for MpcLintsrcWrapper {} // SAFETY: see above unsafe impl ByteValued for MpfIntelWrapper {} #[derive(Debug, Error)] pub enum Error { /// There was too little guest memory to store the entire MP table. #[error("There was too little guest memory to store the entire MP table")] NotEnoughMemory, /// The MP table has too little address space to be stored. #[error("The MP table has too little address space to be stored")] AddressOverflow, /// Failure while zeroing out the memory for the MP table. #[error("Failure while zeroing out the memory for the MP table")] Clear(#[source] GuestMemoryError), /// Failure to write the MP floating pointer. #[error("Failure to write the MP floating pointer")] WriteMpfIntel(#[source] GuestMemoryError), /// Failure to write MP CPU entry. #[error("Failure to write MP CPU entry")] WriteMpcCpu(#[source] GuestMemoryError), /// Failure to write MP ioapic entry. #[error("Failure to write MP ioapic entry")] WriteMpcIoapic(#[source] GuestMemoryError), /// Failure to write MP bus entry. #[error("Failure to write MP bus entry")] WriteMpcBus(#[source] GuestMemoryError), /// Failure to write MP interrupt source entry. #[error("Failure to write MP interrupt source entry")] WriteMpcIntsrc(#[source] GuestMemoryError), /// Failure to write MP local interrupt source entry. #[error("Failure to write MP local interrupt source entry")] WriteMpcLintsrc(#[source] GuestMemoryError), /// Failure to write MP table header. #[error("Failure to write MP table header")] WriteMpcTable(#[source] GuestMemoryError), } pub type Result = result::Result; // Most of these variables are sourced from the Intel MP Spec 1.4. const SMP_MAGIC_IDENT: &[c_uchar; 4] = b"_MP_"; const MPC_SIGNATURE: &[c_uchar; 4] = b"PCMP"; const MPC_SPEC: u8 = 4; const MPC_OEM: &[c_uchar; 8] = b"FC "; const MPC_PRODUCT_ID: &[c_uchar; 12] = &[b'0'; 12]; const BUS_TYPE_ISA: &[c_uchar; 6] = b"ISA "; const APIC_VERSION: u8 = 0x14; const CPU_STEPPING: u32 = 0x600; const CPU_FEATURE_APIC: u32 = 0x200; const CPU_FEATURE_FPU: u32 = 0x001; fn compute_checksum(v: &T) -> u8 { let mut checksum: u8 = 0; for i in v.as_slice().iter() { checksum = checksum.wrapping_add(*i); } checksum } fn mpf_intel_compute_checksum(v: &mpspec::mpf_intel) -> u8 { let checksum = compute_checksum(v).wrapping_sub(v.checksum); (!checksum).wrapping_add(1) } fn compute_mp_size(num_cpus: u32) -> usize { size_of::() + size_of::() + size_of::() * (num_cpus as usize) + size_of::() + size_of::() + size_of::() * 16 + size_of::() * 2 } /// Performs setup of the MP table for the given `num_cpus`. pub fn setup_mptable( offset: GuestAddress, mem: &GuestMemoryMmap, num_cpus: u32, topology: Option<(u16, u16, u16, u16)>, ) -> Result<()> { if num_cpus > 0 { let cpu_id_max = num_cpus - 1; let x2apic_id_max = get_x2apic_id(cpu_id_max, topology); if x2apic_id_max >= MAX_SUPPORTED_CPUS_LEGACY { info!("Skipping mptable creation due to too many CPUs"); return Ok(()); } } // Used to keep track of the next base pointer into the MP table. let mut base_mp = offset; let mp_size = compute_mp_size(num_cpus); if offset.unchecked_add(mp_size as u64) >= HIGH_RAM_START { warn!("Skipping mptable creation due to insufficient space"); return Ok(()); } let mut checksum: u8 = 0; 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. if let Some(end_mp) = base_mp.checked_add((mp_size - 1) as u64) { if !mem.address_in_range(end_mp) { return Err(Error::NotEnoughMemory); } } else { return Err(Error::AddressOverflow); } mem.read_exact_volatile_from(base_mp, &mut vec![0; mp_size].as_slice(), mp_size) .map_err(Error::Clear)?; { let mut mpf_intel = MpfIntelWrapper(mpspec::mpf_intel::default()); let size = size_of::() as u64; mpf_intel.0.signature = *SMP_MAGIC_IDENT; mpf_intel.0.length = 1; mpf_intel.0.specification = 4; mpf_intel.0.physptr = (base_mp.raw_value() + size) as u32; mpf_intel.0.checksum = mpf_intel_compute_checksum(&mpf_intel.0); mem.write_obj(mpf_intel, base_mp) .map_err(Error::WriteMpfIntel)?; base_mp = base_mp.unchecked_add(size); } // We set the location of the mpc_table here but we can't fill it out until we have the length // of the entire table later. let table_base = base_mp; base_mp = base_mp.unchecked_add(size_of::() as u64); { let size = size_of::(); for cpu_id in 0..num_cpus { let mut mpc_cpu = MpcCpuWrapper(mpspec::mpc_cpu::default()); mpc_cpu.0.type_ = mpspec::MP_PROCESSOR as u8; mpc_cpu.0.apicid = get_x2apic_id(cpu_id, topology) as u8; mpc_cpu.0.apicver = APIC_VERSION; mpc_cpu.0.cpuflag = mpspec::CPU_ENABLED as u8 | if cpu_id == 0 { mpspec::CPU_BOOTPROCESSOR as u8 } else { 0 }; mpc_cpu.0.cpufeature = CPU_STEPPING; mpc_cpu.0.featureflag = CPU_FEATURE_APIC | CPU_FEATURE_FPU; mem.write_obj(mpc_cpu, base_mp) .map_err(Error::WriteMpcCpu)?; base_mp = base_mp.unchecked_add(size as u64); checksum = checksum.wrapping_add(compute_checksum(&mpc_cpu.0)); } } { let size = size_of::(); let mut mpc_bus = MpcBusWrapper(mpspec::mpc_bus::default()); mpc_bus.0.type_ = mpspec::MP_BUS as u8; mpc_bus.0.busid = 0; mpc_bus.0.bustype = *BUS_TYPE_ISA; mem.write_obj(mpc_bus, base_mp) .map_err(Error::WriteMpcBus)?; base_mp = base_mp.unchecked_add(size as u64); checksum = checksum.wrapping_add(compute_checksum(&mpc_bus.0)); } { let size = size_of::(); let mut mpc_ioapic = MpcIoapicWrapper(mpspec::mpc_ioapic::default()); mpc_ioapic.0.type_ = mpspec::MP_IOAPIC as u8; mpc_ioapic.0.apicid = ioapicid; mpc_ioapic.0.apicver = APIC_VERSION; mpc_ioapic.0.flags = mpspec::MPC_APIC_USABLE as u8; mpc_ioapic.0.apicaddr = IOAPIC_START.0 as u32; mem.write_obj(mpc_ioapic, base_mp) .map_err(Error::WriteMpcIoapic)?; base_mp = base_mp.unchecked_add(size as u64); checksum = checksum.wrapping_add(compute_checksum(&mpc_ioapic.0)); } // Per kvm_setup_default_irq_routing() in kernel for i in 0..16 { let size = size_of::(); let mut mpc_intsrc = MpcIntsrcWrapper(mpspec::mpc_intsrc::default()); mpc_intsrc.0.type_ = mpspec::MP_INTSRC as u8; mpc_intsrc.0.irqtype = mpspec::MP_IRQ_SOURCE_TYPES_MP_INT as u8; mpc_intsrc.0.irqflag = mpspec::MP_IRQDIR_DEFAULT as u16; mpc_intsrc.0.srcbus = 0; mpc_intsrc.0.srcbusirq = i; mpc_intsrc.0.dstapic = ioapicid; mpc_intsrc.0.dstirq = i; mem.write_obj(mpc_intsrc, base_mp) .map_err(Error::WriteMpcIntsrc)?; base_mp = base_mp.unchecked_add(size as u64); checksum = checksum.wrapping_add(compute_checksum(&mpc_intsrc.0)); } { let size = size_of::(); let mut mpc_lintsrc = MpcLintsrcWrapper(mpspec::mpc_lintsrc::default()); mpc_lintsrc.0.type_ = mpspec::MP_LINTSRC as u8; mpc_lintsrc.0.irqtype = mpspec::MP_IRQ_SOURCE_TYPES_MP_EXT_INT as u8; mpc_lintsrc.0.irqflag = mpspec::MP_IRQDIR_DEFAULT as u16; mpc_lintsrc.0.srcbusid = 0; mpc_lintsrc.0.srcbusirq = 0; mpc_lintsrc.0.destapic = 0; mpc_lintsrc.0.destapiclint = 0; mem.write_obj(mpc_lintsrc, base_mp) .map_err(Error::WriteMpcLintsrc)?; base_mp = base_mp.unchecked_add(size as u64); checksum = checksum.wrapping_add(compute_checksum(&mpc_lintsrc.0)); } { let size = size_of::(); let mut mpc_lintsrc = MpcLintsrcWrapper(mpspec::mpc_lintsrc::default()); mpc_lintsrc.0.type_ = mpspec::MP_LINTSRC as u8; mpc_lintsrc.0.irqtype = mpspec::MP_IRQ_SOURCE_TYPES_MP_NMI as u8; mpc_lintsrc.0.irqflag = mpspec::MP_IRQDIR_DEFAULT as u16; mpc_lintsrc.0.srcbusid = 0; mpc_lintsrc.0.srcbusirq = 0; mpc_lintsrc.0.destapic = 0xFF; /* to all local APICs */ mpc_lintsrc.0.destapiclint = 1; mem.write_obj(mpc_lintsrc, base_mp) .map_err(Error::WriteMpcLintsrc)?; base_mp = base_mp.unchecked_add(size as u64); checksum = checksum.wrapping_add(compute_checksum(&mpc_lintsrc.0)); } // At this point we know the size of the mp_table. let table_end = base_mp; { let mut mpc_table = MpcTableWrapper(mpspec::mpc_table::default()); mpc_table.0.signature = *MPC_SIGNATURE; mpc_table.0.length = table_end.unchecked_offset_from(table_base) as u16; mpc_table.0.spec = MPC_SPEC; mpc_table.0.oem = *MPC_OEM; mpc_table.0.productid = *MPC_PRODUCT_ID; mpc_table.0.lapic = APIC_START.0 as u32; checksum = checksum.wrapping_add(compute_checksum(&mpc_table.0)); mpc_table.0.checksum = (!checksum).wrapping_add(1); mem.write_obj(mpc_table, table_base) .map_err(Error::WriteMpcTable)?; } Ok(()) } #[cfg(test)] mod unit_tests { use vm_memory::bitmap::BitmapSlice; use vm_memory::{GuestUsize, VolatileMemoryError, VolatileSlice, WriteVolatile}; use super::*; use crate::layout::MPTABLE_START; fn table_entry_size(type_: u8) -> usize { match type_ as u32 { mpspec::MP_PROCESSOR => size_of::(), mpspec::MP_BUS => size_of::(), mpspec::MP_IOAPIC => size_of::(), mpspec::MP_INTSRC => size_of::(), mpspec::MP_LINTSRC => size_of::(), _ => panic!("unrecognized mpc table entry type: {type_}"), } } #[test] fn bounds_check() { let num_cpus = 4; let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus))]).unwrap(); setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap(); } #[test] fn bounds_check_fails() { let num_cpus = 4; let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus) - 1)]) .unwrap(); setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap_err(); } #[test] fn mpf_intel_checksum() { let num_cpus = 1; let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus))]).unwrap(); setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap(); let mpf_intel: MpfIntelWrapper = mem.read_obj(MPTABLE_START).unwrap(); assert_eq!( mpf_intel_compute_checksum(&mpf_intel.0), mpf_intel.0.checksum ); } #[test] fn mpc_table_checksum() { let num_cpus = 4; let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(num_cpus))]).unwrap(); setup_mptable(MPTABLE_START, &mem, num_cpus, None).unwrap(); let mpf_intel: MpfIntelWrapper = mem.read_obj(MPTABLE_START).unwrap(); let mpc_offset = GuestAddress(mpf_intel.0.physptr as GuestUsize); let mpc_table: MpcTableWrapper = mem.read_obj(mpc_offset).unwrap(); struct Sum(u8); impl WriteVolatile for Sum { fn write_volatile( &mut self, buf: &VolatileSlice, ) -> result::Result { let mut tmp = vec![0u8; buf.len()]; tmp.write_all_volatile(buf)?; for v in tmp.iter() { self.0 = self.0.wrapping_add(*v); } Ok(buf.len()) } } let mut sum = Sum(0); mem.write_volatile_to(mpc_offset, &mut sum, mpc_table.0.length as usize) .unwrap(); assert_eq!(sum.0, 0); } #[test] fn cpu_entry_count() { let mem = GuestMemoryMmap::from_ranges(&[( MPTABLE_START, compute_mp_size(MAX_SUPPORTED_CPUS_LEGACY), )]) .unwrap(); 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(); let mpc_offset = GuestAddress(mpf_intel.0.physptr as GuestUsize); let mpc_table: MpcTableWrapper = mem.read_obj(mpc_offset).unwrap(); let mpc_end = mpc_offset .checked_add(mpc_table.0.length as GuestUsize) .unwrap(); let mut entry_offset = mpc_offset .checked_add(size_of::() as GuestUsize) .unwrap(); let mut cpu_count = 0; while entry_offset < mpc_end { let entry_type: u8 = mem.read_obj(entry_offset).unwrap(); entry_offset = entry_offset .checked_add(table_entry_size(entry_type) as GuestUsize) .unwrap(); assert!(entry_offset <= mpc_end); if entry_type as u32 == mpspec::MP_PROCESSOR { cpu_count += 1; } } assert_eq!(cpu_count, i); } } #[test] fn cpu_entry_count_max() { let cpus = MAX_SUPPORTED_CPUS_LEGACY + 1; let mem = GuestMemoryMmap::from_ranges(&[(MPTABLE_START, compute_mp_size(cpus))]).unwrap(); setup_mptable(MPTABLE_START, &mem, cpus, None).unwrap(); } } cloud-hypervisor-53.0/arch/src/x86_64/regs.rs000066400000000000000000000200251522476750100207370ustar00rootroot00000000000000// Copyright © 2020, Oracle and/or its affiliates. // // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. use std::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 thiserror::Error; use vm_memory::{Address, Bytes, GuestMemory, GuestMemoryError}; use crate::layout::{ BOOT_GDT_START, BOOT_IDT_START, BOOT_STACK_POINTER, PVH_INFO_START, ZERO_PAGE_START, }; use crate::{EntryPoint, GuestMemoryMmap}; #[derive(Debug, Error)] pub enum Error { /// Failed to get SREGs for this CPU. #[error("Failed to get SREGs for this CPU")] GetStatusRegisters(#[source] hypervisor::HypervisorCpuError), /// Failed to set base registers for this CPU. #[error("Failed to set base registers for this CPU")] SetBaseRegisters(#[source] hypervisor::HypervisorCpuError), /// Failed to configure the FPU. #[error("Failed to configure the FPU")] SetFpuRegisters(#[source] hypervisor::HypervisorCpuError), /// Setting up MSRs failed. #[error("Setting up MSRs failed")] SetModelSpecificRegisters(#[source] hypervisor::HypervisorCpuError), /// Failed to set SREGs for this CPU. #[error("Failed to set SREGs for this CPU")] SetStatusRegisters(#[source] hypervisor::HypervisorCpuError), /// Checking the GDT address failed. #[error("Checking the GDT address failed")] CheckGdtAddr, /// Writing the GDT to RAM failed. #[error("Writing the GDT to RAM failed")] WriteGdt(#[source] GuestMemoryError), /// Writing the IDT to RAM failed. #[error("Writing the IDT to RAM failed")] WriteIdt(#[source] GuestMemoryError), /// Writing PDPTE to RAM failed. #[error("Writing PDPTE to RAM failed")] WritePdpteAddress(#[source] GuestMemoryError), /// Writing PDE to RAM failed. #[error("Writing PDE to RAM failed")] WritePdeAddress(#[source] GuestMemoryError), /// Writing PML4 to RAM failed. #[error("Writing PML4 to RAM failed")] WritePml4Address(#[source] GuestMemoryError), /// Writing PML5 to RAM failed. #[error("Writing PML5 to RAM failed")] WritePml5Address(#[source] GuestMemoryError), } pub type Result = result::Result; /// Configure Floating-Point Unit (FPU) registers for a given CPU. /// /// # Arguments /// /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. pub fn setup_fpu(vcpu: &dyn hypervisor::Vcpu) -> Result<()> { let fpu: FpuState = FpuState { fcw: 0x37f, mxcsr: 0x1f80, ..Default::default() }; vcpu.set_fpu(&fpu).map_err(Error::SetFpuRegisters) } /// Configure Model Specific Registers (MSRs) for a given CPU. /// /// # Arguments /// /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. pub fn setup_msrs(vcpu: &dyn hypervisor::Vcpu) -> Result<()> { vcpu.set_msrs(vcpu.boot_msr_entries()) .map_err(Error::SetModelSpecificRegisters)?; Ok(()) } /// Configure base registers for a given CPU. /// /// # Arguments /// /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. /// * `entry_point` - Description of the boot entry to set up. pub fn setup_regs(vcpu: &dyn hypervisor::Vcpu, entry_point: EntryPoint) -> Result<()> { let mut regs = vcpu.create_standard_regs(); match entry_point.setup_header { None => { regs.set_rflags(0x0000000000000002u64); regs.set_rip(entry_point.entry_addr.raw_value()); regs.set_rbx(PVH_INFO_START.raw_value()); } Some(_) => { regs.set_rflags(0x0000000000000002u64); regs.set_rip(entry_point.entry_addr.raw_value()); regs.set_rsp(BOOT_STACK_POINTER.raw_value()); regs.set_rsi(ZERO_PAGE_START.raw_value()); } } vcpu.set_regs(®s).map_err(Error::SetBaseRegisters) } /// Configures the segment registers and system page tables for a given CPU. /// /// # Arguments /// /// * `mem` - The memory that will be passed to the guest. /// * `vcpu` - Structure for the VCPU that holds the VCPU's fd. pub fn setup_sregs( mem: &GuestMemoryMmap, vcpu: &dyn hypervisor::Vcpu, enable_x2_apic_mode: bool, ) -> Result<()> { let mut sregs: SpecialRegisters = vcpu.get_sregs().map_err(Error::GetStatusRegisters)?; configure_segments_and_sregs(mem, &mut sregs, enable_x2_apic_mode)?; vcpu.set_sregs(&sregs).map_err(Error::SetStatusRegisters) } const BOOT_GDT_MAX: usize = 4; fn write_gdt_table(table: &[u64], guest_mem: &GuestMemoryMmap) -> Result<()> { let boot_gdt_addr = BOOT_GDT_START; for (index, entry) in table.iter().enumerate() { let addr = guest_mem .checked_offset(boot_gdt_addr, index * size_of::()) .ok_or(Error::CheckGdtAddr)?; guest_mem.write_obj(*entry, addr).map_err(Error::WriteGdt)?; } Ok(()) } fn write_idt_value(val: u64, guest_mem: &GuestMemoryMmap) -> Result<()> { let boot_idt_addr = BOOT_IDT_START; guest_mem .write_obj(val, boot_idt_addr) .map_err(Error::WriteIdt) } 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 [ gdt_entry(0, 0, 0), // NULL gdt_entry(0xc09b, 0, 0xffffffff), // CODE gdt_entry(0xc093, 0, 0xffffffff), // DATA gdt_entry(0x008b, 0, 0x67), // TSS ] }; let code_seg = segment_from_gdt(gdt_table[1], 1); let data_seg = segment_from_gdt(gdt_table[2], 2); let tss_seg = segment_from_gdt(gdt_table[3], 3); // Write segments write_gdt_table(&gdt_table[..], mem)?; sregs.gdt.base = BOOT_GDT_START.raw_value(); sregs.gdt.limit = size_of_val(&gdt_table) as u16 - 1; write_idt_value(0, mem)?; sregs.idt.base = BOOT_IDT_START.raw_value(); sregs.idt.limit = size_of::() as u16 - 1; sregs.cs = code_seg; sregs.ds = data_seg; sregs.es = data_seg; sregs.fs = data_seg; sregs.gs = data_seg; sregs.ss = data_seg; sregs.tr = tss_seg; sregs.cr0 = CR0_PE; sregs.cr4 = 0; if enable_x2_apic_mode { const X2APIC_ENABLE_BIT: u64 = 1 << 10; sregs.apic_base |= X2APIC_ENABLE_BIT; } Ok(()) } #[cfg(test)] mod unit_tests { use vm_memory::GuestAddress; use super::*; fn create_guest_mem() -> GuestMemoryMmap { GuestMemoryMmap::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap() } fn read_u64(gm: &GuestMemoryMmap, offset: GuestAddress) -> u64 { gm.read_obj(offset).unwrap() } #[test] fn segments_and_sregs() { let mut sregs: SpecialRegisters = Default::default(); let gm = create_guest_mem(); configure_segments_and_sregs(&gm, &mut sregs, false).unwrap(); assert_eq!(0x0, read_u64(&gm, BOOT_GDT_START)); assert_eq!( 0xcf9b000000ffff, read_u64(&gm, BOOT_GDT_START.unchecked_add(8)) ); assert_eq!( 0xcf93000000ffff, read_u64(&gm, BOOT_GDT_START.unchecked_add(16)) ); assert_eq!( 0x8b0000000067, read_u64(&gm, BOOT_GDT_START.unchecked_add(24)) ); assert_eq!(0x0, read_u64(&gm, BOOT_IDT_START)); assert_eq!(0, sregs.cs.base); assert_eq!(0xffffffff, sregs.ds.limit); assert_eq!(0x10, sregs.es.selector); assert_eq!(1, sregs.fs.present); assert_eq!(1, sregs.gs.g); assert_eq!(0, sregs.ss.avl); assert_eq!(0, sregs.tr.base); assert_eq!(0, sregs.tr.g); assert_eq!(0x67, sregs.tr.limit); assert_eq!(0xb, sregs.tr.type_); assert_eq!(0, sregs.tr.avl); assert_eq!(CR0_PE, sregs.cr0); assert_eq!(0, sregs.cr4); } } cloud-hypervisor-53.0/arch/src/x86_64/smbios.rs000066400000000000000000000547201522476750100213040ustar00rootroot00000000000000// Copyright © 2020 Intel Corporation // // Copyright 2019 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 file. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::result; use thiserror::Error; use uuid::Uuid; use vm_memory::{Address, ByteValued, Bytes, GuestAddress}; use crate::GuestMemoryMmap; use crate::layout::SMBIOS_START; #[derive(Debug, Error)] pub enum Error { /// There was too little guest memory to store the entire SMBIOS table. #[error("There was too little guest memory to store the SMBIOS table")] NotEnoughMemory, /// The SMBIOS table has too little address space to be stored. #[error("The SMBIOS table has too little address space to be stored")] AddressOverflow, /// Failure while zeroing out the memory for the SMBIOS table. #[error("Failure while zeroing out the memory for the SMBIOS table")] Clear, /// Failure to write SMBIOS entrypoint structure #[error("Failure to write SMBIOS entrypoint structure")] WriteSmbiosEp(#[source] vm_memory::GuestMemoryError), /// Failure to write additional data to memory #[error("Failure to write additional data to memory")] WriteData(#[source] vm_memory::GuestMemoryError), /// Failure to parse uuid, uuid format may be error #[error("Failure to parse uuid: {1}")] ParseUuid(#[source] uuid::Error, String), /// SMBIOS string index overflow (u8 limit reached). #[error("SMBIOS string index overflow (u8 limit reached: {})", u8::MAX)] TooManyStrings, } pub type Result = result::Result; // 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 mut checksum: u8 = 0; for i in v.as_slice().iter() { checksum = checksum.wrapping_add(*i); } (!checksum).wrapping_add(1) } #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct Smbios30Entrypoint { signature: [u8; 5usize], checksum: u8, length: u8, majorver: u8, minorver: u8, docrev: u8, revision: u8, reserved: u8, max_size: u32, physptr: u64, } #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosBiosInfo { r#type: u8, length: u8, handle: u16, vendor: u8, version: u8, start_addr: u16, release_date: u8, rom_size: u8, characteristics: u64, characteristics_ext1: u8, characteristics_ext2: u8, } #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosSysInfo { r#type: u8, length: u8, handle: u16, manufacturer: u8, product_name: u8, version: u8, serial_number: u8, uuid: [u8; 16usize], wake_up_type: u8, sku: u8, family: u8, } #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosOemStrings { r#type: u8, length: u8, handle: u16, count: u8, } /// SMBIOS Chassis Table (Type 3) as defined in DMTF SMBIOS 3.9.0: /// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf /// Note: trailing fields are omitted, so this structure is not complete. #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosChassis { r#type: u8, length: u8, handle: u16, manufacturer: u8, chassis_type: u8, version: u8, serial_number: u8, asset_tag: u8, bootup_state: u8, power_supply_state: u8, thermal_state: u8, security_status: u8, oem_defined: u32, height: u8, number_of_power_cords: u8, contained_element_count: u8, contained_element_record_length: u8, // followed by contained element records (optional, variable-length) // followed by sku_number: u8, rack_type: u8, rack_height: u8 } #[repr(C, packed)] #[derive(Default, Copy, Clone)] struct SmbiosEndOfTable { r#type: u8, length: u8, handle: u16, } // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for Smbios30Entrypoint {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosBiosInfo {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosSysInfo {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosOemStrings {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosChassis {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for SmbiosEndOfTable {} fn write_and_incr( mem: &GuestMemoryMmap, val: T, mut curptr: GuestAddress, ) -> Result { mem.write_obj(val, curptr).map_err(Error::WriteData)?; curptr = curptr .checked_add(size_of::() as u64) .ok_or(Error::NotEnoughMemory)?; Ok(curptr) } fn write_string( mem: &GuestMemoryMmap, val: &str, mut curptr: GuestAddress, ) -> Result { for c in val.as_bytes().iter() { curptr = write_and_incr(mem, *c, curptr)?; } curptr = write_and_incr(mem, 0u8, curptr)?; Ok(curptr) } 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, curptr: &mut GuestAddress, handle: &mut u16, system: Option<&SmbiosSystem>, ) -> Result<()> { *handle += 1; let manufacturer = system .and_then(|s| s.manufacturer.as_deref()) .unwrap_or(DEFAULT_SYSTEM_MANUFACTURER); let product = system .and_then(|s| s.product_name.as_deref()) .unwrap_or(DEFAULT_SYSTEM_PRODUCT_NAME); let version = system.and_then(|s| s.version.as_deref()); let serial = system.and_then(|s| s.serial_number.as_deref()); let uuid = system.and_then(|s| s.uuid.as_deref()); let sku = system.and_then(|s| s.sku_number.as_deref()); let family = system.and_then(|s| s.family.as_deref()); let uuid_number = uuid .map(Uuid::parse_str) .transpose() .map_err(|e| Error::ParseUuid(e, uuid.unwrap().to_string()))? .unwrap_or(Uuid::nil()); let mut next = 1u8; let manufacturer_idx = alloc_index(&mut next, true)?; let product_idx = alloc_index(&mut next, true)?; let version_idx = alloc_index(&mut next, version.is_some())?; let serial_idx = alloc_index(&mut next, serial.is_some())?; let sku_idx = alloc_index(&mut next, sku.is_some())?; let family_idx = alloc_index(&mut next, family.is_some())?; let sys = SmbiosSysInfo { r#type: SYSTEM_INFORMATION, length: size_of::() as u8, handle: *handle, manufacturer: manufacturer_idx, product_name: product_idx, version: version_idx, serial_number: serial_idx, uuid: uuid_number.to_bytes_le(), wake_up_type: SYSTEM_WAKE_UP_TYPE_UNKNOWN, sku: sku_idx, family: family_idx, }; *curptr = write_and_incr(mem, sys, *curptr)?; *curptr = write_string(mem, manufacturer, *curptr)?; *curptr = write_string(mem, product, *curptr)?; *curptr = write_opt_string(mem, version, *curptr)?; *curptr = write_opt_string(mem, serial, *curptr)?; *curptr = write_opt_string(mem, sku, *curptr)?; *curptr = write_opt_string(mem, family, *curptr)?; *curptr = write_and_incr(mem, 0u8, *curptr)?; Ok(()) } fn write_type3_chassis( mem: &GuestMemoryMmap, curptr: &mut GuestAddress, handle: &mut u16, chassis: &SmbiosChassisConfig, ) -> Result<()> { *handle += 1; let asset_tag = chassis.asset_tag.as_deref(); let mut next = 1u8; let asset_idx = alloc_index(&mut next, asset_tag.is_some())?; let ch = SmbiosChassis { r#type: SYSTEM_ENCLOSURE, length: size_of::() 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(size_of::() as u64) .ok_or(Error::NotEnoughMemory)?; let mut curptr = physptr; let mut handle = 0; { handle += 1; let smbios_biosinfo = SmbiosBiosInfo { r#type: BIOS_INFORMATION, length: size_of::() as u8, handle, vendor: 1, // First string written in this section version: 2, // Second string written in this section characteristics: PCI_SUPPORTED, characteristics_ext2: IS_VIRTUAL_MACHINE, ..Default::default() }; curptr = write_and_incr(mem, smbios_biosinfo, curptr)?; curptr = write_string(mem, "cloud-hypervisor", curptr)?; curptr = write_string(mem, "0", curptr)?; curptr = write_and_incr(mem, 0u8, curptr)?; } write_type1_system(mem, &mut curptr, &mut handle, system)?; if let Some(chassis) = chassis { write_type3_chassis(mem, &mut curptr, &mut handle, chassis)?; } if !oem_strings.is_empty() { handle += 1; let smbios_oemstrings = SmbiosOemStrings { r#type: OEM_STRINGS, length: size_of::() as u8, handle, count: oem_strings.len() as u8, }; curptr = write_and_incr(mem, smbios_oemstrings, curptr)?; for s in oem_strings { curptr = write_string(mem, s, curptr)?; } curptr = write_string_terminator(mem, curptr, true)?; } { handle += 1; let smbios_end = SmbiosEndOfTable { r#type: END_OF_TABLE, length: size_of::() as u8, handle, }; curptr = write_and_incr(mem, smbios_end, curptr)?; curptr = write_and_incr(mem, 0u8, curptr)?; curptr = write_and_incr(mem, 0u8, curptr)?; } { let mut smbios_ep = Smbios30Entrypoint { signature: *SM3_MAGIC_IDENT, length: size_of::() as u8, // SMBIOS rev 3.2.0 majorver: 0x03, minorver: 0x02, docrev: 0x00, revision: 0x01, // SMBIOS 3.0 max_size: curptr.unchecked_offset_from(physptr) as u32, physptr: physptr.0, ..Default::default() }; smbios_ep.checksum = compute_checksum(&smbios_ep); mem.write_obj(smbios_ep, GuestAddress(SMBIOS_START)) .map_err(Error::WriteSmbiosEp)?; } Ok(curptr.unchecked_offset_from(physptr) + size_of::() as u64) } #[cfg(test)] mod unit_tests { use super::*; /// Collects all strings after a SMBIOS structure, stopping at the double-NUL terminator and returns next addr. fn read_string_set(mem: &GuestMemoryMmap, addr: GuestAddress) -> (Vec, GuestAddress) { let mut cur = addr; let read_byte = |addr: GuestAddress| -> u8 { mem.read_obj(addr).unwrap() }; // SMBIOS string-set: NUL-terminated strings, terminated by an extra NUL. // Empty string-set is exactly "\0\0". if read_byte(cur) == 0 { let next = cur.checked_add(1).unwrap(); assert_eq!(read_byte(next), 0); return (Vec::new(), next.checked_add(1).unwrap()); } let mut strings = Vec::new(); loop { let mut bytes = Vec::new(); loop { let b = read_byte(cur); cur = cur.checked_add(1).unwrap(); if b == 0 { break; } bytes.push(b); } strings.push(String::from_utf8(bytes).unwrap()); // If the next byte is NUL, that's the extra terminator. if read_byte(cur) == 0 { cur = cur.checked_add(1).unwrap(); break; } } (strings, cur) } #[test] fn entrypoint_checksum() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); setup_smbios(&mem, None).unwrap(); let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap(); assert_eq!(compute_checksum(&smbios_ep), 0); } #[test] fn entrypoint_struct_size() { assert_eq!( size_of::(), 0x18usize, concat!("Size of: ", stringify!(Smbios30Entrypoint)) ); assert_eq!( size_of::(), 0x14usize, concat!("Size of: ", stringify!(SmbiosBiosInfo)) ); assert_eq!( size_of::(), 0x1busize, concat!("Size of: ", stringify!(SmbiosSysInfo)) ); } #[test] fn smbios_chassis_empty_string_set_has_double_null() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); let smbios = SmbiosConfig { chassis: Some(SmbiosChassisConfig::default()), ..Default::default() }; setup_smbios(&mem, Some(&smbios)).unwrap(); let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap(); let mut cur = GuestAddress(smbios_ep.physptr); let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap(); cur = cur.checked_add(bios.length as u64).unwrap(); let (_, next) = read_string_set(&mem, cur); cur = next; let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap(); cur = cur.checked_add(sys.length as u64).unwrap(); let (_, next) = read_string_set(&mem, cur); cur = next; let chassis: SmbiosChassis = mem.read_obj(cur).unwrap(); cur = cur.checked_add(chassis.length as u64).unwrap(); // SMBIOS DSP0134 §6.1.3: empty string-set ends with double NUL. let b0: u8 = mem.read_obj(cur).unwrap(); let b1: u8 = mem.read_obj(cur.checked_add(1).unwrap()).unwrap(); assert_eq!(b0, 0); assert_eq!(b1, 0); cur = cur.checked_add(2).unwrap(); let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap(); assert_eq!(end.r#type, END_OF_TABLE); } #[test] fn smbios_chassis_oem_strings_layout() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); let smbios = SmbiosConfig { chassis: Some(SmbiosChassisConfig { asset_tag: Some("rack1".to_string()), }), oem_strings: ["o1".to_string(), "o2".to_string()].into(), ..Default::default() }; setup_smbios(&mem, Some(&smbios)).unwrap(); let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap(); let mut cur = GuestAddress(smbios_ep.physptr); let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap(); cur = cur.checked_add(bios.length as u64).unwrap(); let (_, next) = read_string_set(&mem, cur); cur = next; let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap(); cur = cur.checked_add(sys.length as u64).unwrap(); let (_, next) = read_string_set(&mem, cur); cur = next; let chassis: SmbiosChassis = mem.read_obj(cur).unwrap(); assert_eq!(chassis.r#type, SYSTEM_ENCLOSURE); assert_eq!(chassis.asset_tag, 1); cur = cur.checked_add(chassis.length as u64).unwrap(); let (chassis_strings, next) = read_string_set(&mem, cur); assert_eq!(chassis_strings, vec!["rack1"]); cur = next; let oem: SmbiosOemStrings = mem.read_obj(cur).unwrap(); assert_eq!(oem.r#type, OEM_STRINGS); assert_eq!(oem.count, 2); cur = cur.checked_add(oem.length as u64).unwrap(); let (oem_strings, next) = read_string_set(&mem, cur); assert_eq!(oem_strings, vec!["o1", "o2"]); cur = next; let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap(); assert_eq!(end.r#type, END_OF_TABLE); } #[test] fn smbios_strings_terminators_default() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); setup_smbios(&mem, None).unwrap(); let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap(); let mut cur = GuestAddress(smbios_ep.physptr); let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap(); assert_eq!(bios.r#type, BIOS_INFORMATION); cur = cur.checked_add(bios.length as u64).unwrap(); let (bios_strings, next) = read_string_set(&mem, cur); assert_eq!(bios_strings, vec!["cloud-hypervisor", "0"]); cur = next; let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap(); assert_eq!(sys.r#type, SYSTEM_INFORMATION); assert_eq!(sys.manufacturer, 1); assert_eq!(sys.product_name, 2); assert_eq!(sys.version, 0); assert_eq!(sys.serial_number, 0); assert_eq!(sys.sku, 0); assert_eq!(sys.family, 0); cur = cur.checked_add(sys.length as u64).unwrap(); let (sys_strings, next) = read_string_set(&mem, cur); assert_eq!( sys_strings, vec![DEFAULT_SYSTEM_MANUFACTURER, DEFAULT_SYSTEM_PRODUCT_NAME] ); cur = next; let end: SmbiosEndOfTable = mem.read_obj(cur).unwrap(); assert_eq!(end.r#type, END_OF_TABLE); } #[test] fn smbios_strings_too_many() { let mut next = 1u8; for _ in 0..255 { alloc_index(&mut next, true).unwrap(); } let err = alloc_index(&mut next, true).unwrap_err(); assert!(matches!(err, Error::TooManyStrings)); } #[test] fn smbios_uuid_invalid_rejected() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); let smbios = SmbiosConfig { system: Some(SmbiosSystem { uuid: Some("not-a-uuid".to_string()), ..Default::default() }), ..Default::default() }; let err = setup_smbios(&mem, Some(&smbios)).unwrap_err(); assert!(matches!(err, Error::ParseUuid(_, _))); } #[test] fn smbios_uuid_written_le() { let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(SMBIOS_START), 4096)]).unwrap(); let uuid_str = "00112233-4455-6677-8899-aabbccddeeff"; let smbios = SmbiosConfig { system: Some(SmbiosSystem { uuid: Some(uuid_str.to_string()), ..Default::default() }), ..Default::default() }; setup_smbios(&mem, Some(&smbios)).unwrap(); let smbios_ep: Smbios30Entrypoint = mem.read_obj(GuestAddress(SMBIOS_START)).unwrap(); let mut cur = GuestAddress(smbios_ep.physptr); let bios: SmbiosBiosInfo = mem.read_obj(cur).unwrap(); cur = cur.checked_add(bios.length as u64).unwrap(); let (_, next) = read_string_set(&mem, cur); cur = next; let sys: SmbiosSysInfo = mem.read_obj(cur).unwrap(); assert_eq!(sys.uuid, Uuid::parse_str(uuid_str).unwrap().to_bytes_le()); } #[test] fn smbios_write_fails_with_too_small_memory() { let mem = GuestMemoryMmap::from_ranges(&[( GuestAddress(SMBIOS_START), size_of::(), )]) .unwrap(); let err = setup_smbios(&mem, None).unwrap_err(); assert!(matches!(err, Error::WriteData(_))); } } cloud-hypervisor-53.0/arch/src/x86_64/tdx/000077500000000000000000000000001522476750100202315ustar00rootroot00000000000000cloud-hypervisor-53.0/arch/src/x86_64/tdx/mod.rs000066400000000000000000000403401522476750100213570ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::fs::File; use std::io::{self, Read, Seek, SeekFrom}; use std::slice; use std::str::FromStr; use log::{debug, info}; use thiserror::Error; use uuid::Uuid; use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemoryError}; use crate::GuestMemoryMmap; #[derive(Error, Debug)] pub enum TdvfError { #[error("Failed read TDVF descriptor")] ReadDescriptor(#[source] io::Error), #[error("Failed read TDVF descriptor offset")] ReadDescriptorOffset(#[source] io::Error), #[error("Failed read GUID table")] ReadGuidTable(#[source] io::Error), #[error("Invalid descriptor signature")] InvalidDescriptorSignature, #[error("Invalid descriptor size")] InvalidDescriptorSize, #[error("Invalid descriptor version")] InvalidDescriptorVersion, #[error("Failed to write HOB details to guest memory")] GuestMemoryWriteHob(#[source] GuestMemoryError), #[error("Failed to create Uuid")] UuidCreation(#[source] uuid::Error), } const TABLE_FOOTER_GUID: &str = "96b582de-1fb2-45f7-baea-a366c55a082d"; const TDVF_METADATA_OFFSET_GUID: &str = "e47a6535-984a-4798-865e-4685a7bf8ec2"; // TDVF_DESCRIPTOR #[repr(C, packed)] #[derive(Default)] pub struct TdvfDescriptor { signature: [u8; 4], length: u32, version: u32, num_sections: u32, // NumberOfSectionEntry } // TDVF_SECTION #[repr(C, packed)] #[derive(Clone, Copy, Default, Debug)] pub struct TdvfSection { pub data_offset: u32, pub data_size: u32, // RawDataSize pub address: u64, // MemoryAddress pub size: u64, // MemoryDataSize pub r#type: TdvfSectionType, pub attributes: u32, } #[repr(u32)] #[derive(Clone, Copy, Debug, Default)] pub enum TdvfSectionType { Bfv, Cfv, TdHob, TempMem, PermMem, Payload, PayloadParam, #[default] Reserved = 0xffffffff, } fn tdvf_descriptor_offset(file: &mut File) -> Result<(SeekFrom, bool), TdvfError> { // Let's first try to identify the presence of the table footer GUID file.seek(SeekFrom::End(-0x30)) .map_err(TdvfError::ReadGuidTable)?; let mut table_footer_guid: [u8; 16] = [0; 16]; file.read_exact(&mut table_footer_guid) .map_err(TdvfError::ReadGuidTable)?; let uuid = Uuid::from_slice_le(table_footer_guid.as_slice()).map_err(TdvfError::UuidCreation)?; let expected_uuid = Uuid::from_str(TABLE_FOOTER_GUID).map_err(TdvfError::UuidCreation)?; if uuid == expected_uuid { // Retrieve the table size file.seek(SeekFrom::End(-0x32)) .map_err(TdvfError::ReadGuidTable)?; let mut table_size: [u8; 2] = [0; 2]; file.read_exact(&mut table_size) .map_err(TdvfError::ReadGuidTable)?; let table_size = u16::from_le_bytes(table_size) as usize; let mut table: Vec = vec![0; table_size]; // Read the entire table file.seek(SeekFrom::End(-(table_size as i64 + 0x20))) .map_err(TdvfError::ReadGuidTable)?; file.read_exact(table.as_mut_slice()) .map_err(TdvfError::ReadGuidTable)?; // Let's start from the top and go backward down the table. // We start after the footer GUID and the table length. let mut offset = table_size - 18; debug!("Parsing GUID structure"); while offset >= 18 { let entry_uuid = Uuid::from_slice_le(&table[offset - 16..offset]) .map_err(TdvfError::UuidCreation)?; let entry_size = u16::from_le_bytes(table[offset - 18..offset - 16].try_into().unwrap()) as usize; debug!( "Entry GUID = {}, size = {}", entry_uuid.hyphenated(), entry_size ); // Avoid going through an infinite loop if the entry size is 0 if entry_size == 0 { break; } offset -= entry_size; let expected_uuid = Uuid::from_str(TDVF_METADATA_OFFSET_GUID).map_err(TdvfError::UuidCreation)?; if entry_uuid == expected_uuid && entry_size == 22 { return Ok(( SeekFrom::End( -(u32::from_le_bytes(table[offset..offset + 4].try_into().unwrap()) as i64), ), true, )); } } } // If we end up here, this means the firmware doesn't support the new way // of exposing the TDVF descriptor offset through the table of GUIDs. // That's why we fallback onto the deprecated method. // The 32-bit offset to the TDVF metadata is located 32 bytes from // the end of the file. // See "TDVF Metadata Pointer" in "TDX Virtual Firmware Design Guide file.seek(SeekFrom::End(-0x20)) .map_err(TdvfError::ReadDescriptorOffset)?; let mut descriptor_offset: [u8; 4] = [0; 4]; file.read_exact(&mut descriptor_offset) .map_err(TdvfError::ReadDescriptorOffset)?; Ok(( SeekFrom::Start(u32::from_le_bytes(descriptor_offset) as u64), false, )) } pub fn parse_tdvf_sections(file: &mut File) -> Result<(Vec, bool), TdvfError> { let (descriptor_offset, guid_found) = tdvf_descriptor_offset(file)?; file.seek(descriptor_offset) .map_err(TdvfError::ReadDescriptor)?; let mut descriptor: TdvfDescriptor = Default::default(); // SAFETY: we read exactly the size of the descriptor header file.read_exact(unsafe { slice::from_raw_parts_mut((&raw mut descriptor).cast(), size_of::()) }) .map_err(TdvfError::ReadDescriptor)?; if &descriptor.signature != b"TDVF" { return Err(TdvfError::InvalidDescriptorSignature); } if descriptor.length as usize != size_of::() + size_of::() * descriptor.num_sections as usize { return Err(TdvfError::InvalidDescriptorSize); } if descriptor.version != 1 { return Err(TdvfError::InvalidDescriptorVersion); } let mut sections = Vec::new(); sections.resize_with(descriptor.num_sections as usize, TdvfSection::default); // SAFETY: we read exactly the advertised sections file.read_exact(unsafe { slice::from_raw_parts_mut( sections.as_mut_ptr().cast(), descriptor.num_sections as usize * size_of::(), ) }) .map_err(TdvfError::ReadDescriptor)?; Ok((sections, guid_found)) } #[repr(u16)] #[derive(Copy, Clone, Debug, Default)] enum HobType { Handoff = 0x1, ResourceDescriptor = 0x3, GuidExtension = 0x4, #[default] Unused = 0xfffe, EndOfHobList = 0xffff, } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] struct HobHeader { r#type: HobType, length: u16, reserved: u32, } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] struct HobHandoffInfoTable { header: HobHeader, version: u32, boot_mode: u32, efi_memory_top: u64, efi_memory_bottom: u64, efi_free_memory_top: u64, efi_free_memory_bottom: u64, efi_end_of_hob_list: u64, } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] struct EfiGuid { data1: u32, data2: u16, data3: u16, data4: [u8; 8], } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] struct HobResourceDescriptor { header: HobHeader, owner: EfiGuid, resource_type: u32, resource_attribute: u32, physical_start: u64, resource_length: u64, } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] struct HobGuidType { header: HobHeader, name: EfiGuid, } #[repr(u32)] #[derive(Clone, Copy, Debug, Default)] pub enum PayloadImageType { #[default] ExecutablePayload, BzImage, RawVmLinux, } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] pub struct PayloadInfo { pub image_type: PayloadImageType, pub entry_point: u64, } #[repr(C, packed)] #[derive(Copy, Clone, Default, Debug)] struct TdPayload { guid_type: HobGuidType, payload_info: PayloadInfo, } // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for HobHeader {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for HobHandoffInfoTable {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for HobResourceDescriptor {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for HobGuidType {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for PayloadInfo {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for TdPayload {} pub struct TdHob { start_offset: u64, current_offset: u64, } fn align_hob(v: u64) -> u64 { v.div_ceil(8) * 8 } impl TdHob { fn update_offset(&mut self) { self.current_offset = align_hob(self.current_offset + size_of::() as u64); } pub fn start(offset: u64) -> TdHob { // Leave a gap to place the HandoffTable at the start as it can only be filled in later let mut hob = TdHob { start_offset: offset, current_offset: offset, }; hob.update_offset::(); hob } pub fn finish(&mut self, mem: &GuestMemoryMmap) -> Result<(), TdvfError> { // Write end let end = HobHeader { r#type: HobType::EndOfHobList, length: size_of::() as u16, reserved: 0, }; info!("Writing HOB end {:x} {:x?}", self.current_offset, end); mem.write_obj(end, GuestAddress(self.current_offset)) .map_err(TdvfError::GuestMemoryWriteHob)?; self.update_offset::(); // Write handoff, delayed as it needs end of HOB list let efi_end_of_hob_list = self.current_offset; let handoff = HobHandoffInfoTable { header: HobHeader { r#type: HobType::Handoff, length: size_of::() as u16, reserved: 0, }, version: 0x9, boot_mode: 0, efi_memory_top: 0, efi_memory_bottom: 0, efi_free_memory_top: 0, efi_free_memory_bottom: 0, efi_end_of_hob_list, }; info!("Writing HOB start {:x} {:x?}", self.start_offset, handoff); mem.write_obj(handoff, GuestAddress(self.start_offset)) .map_err(TdvfError::GuestMemoryWriteHob) } pub fn add_resource( &mut self, mem: &GuestMemoryMmap, physical_start: u64, resource_length: u64, resource_type: u32, resource_attribute: u32, ) -> Result<(), TdvfError> { let resource_descriptor = HobResourceDescriptor { header: HobHeader { r#type: HobType::ResourceDescriptor, length: size_of::() as u16, reserved: 0, }, owner: EfiGuid::default(), resource_type, resource_attribute, physical_start, resource_length, }; info!( "Writing HOB resource {:x} {:x?}", self.current_offset, resource_descriptor ); mem.write_obj(resource_descriptor, GuestAddress(self.current_offset)) .map_err(TdvfError::GuestMemoryWriteHob)?; self.update_offset::(); Ok(()) } pub fn add_memory_resource( &mut self, mem: &GuestMemoryMmap, physical_start: u64, resource_length: u64, ram: bool, guid_found: bool, ) -> Result<(), TdvfError> { self.add_resource( mem, physical_start, resource_length, if ram { if guid_found { 0x7 /* EFI_RESOURCE_MEMORY_UNACCEPTED */ } else { 0 /* EFI_RESOURCE_SYSTEM_MEMORY */ } } else if guid_found { 0 /* EFI_RESOURCE_SYSTEM_MEMORY */ } else { 0x5 /*EFI_RESOURCE_MEMORY_RESERVED */ }, /* TODO: * QEMU currently fills it in like this: * EFI_RESOURCE_ATTRIBUTE_PRESENT | EFI_RESOURCE_ATTRIBUTE_INITIALIZED | EFI_RESOURCE_ATTRIBUTE_TESTED * which differs from the spec (due to TDVF implementation issue?) */ 0x7, ) } pub fn add_mmio_resource( &mut self, mem: &GuestMemoryMmap, physical_start: u64, resource_length: u64, ) -> Result<(), TdvfError> { self.add_resource( mem, physical_start, resource_length, 0x1, /* EFI_RESOURCE_MEMORY_MAPPED_IO */ /* * EFI_RESOURCE_ATTRIBUTE_PRESENT | EFI_RESOURCE_ATTRIBUTE_INITIALIZED | EFI_RESOURCE_ATTRIBUTE_UNCACHEABLE */ 0x403, ) } pub fn add_acpi_table( &mut self, mem: &GuestMemoryMmap, table_content: &[u8], ) -> Result<(), TdvfError> { // We already know the HobGuidType size is 8 bytes multiple, but we // need the total size to be 8 bytes multiple. That is why the ACPI // table size must be 8 bytes multiple as well. let length = size_of::() as u16 + align_hob(table_content.len() as u64) as u16; let hob_guid_type = HobGuidType { header: HobHeader { r#type: HobType::GuidExtension, length, reserved: 0, }, // ACPI_TABLE_HOB_GUID // 0x6a0c5870, 0xd4ed, 0x44f4, {0xa1, 0x35, 0xdd, 0x23, 0x8b, 0x6f, 0xc, 0x8d } name: EfiGuid { data1: 0x6a0c_5870, data2: 0xd4ed, data3: 0x44f4, data4: [0xa1, 0x35, 0xdd, 0x23, 0x8b, 0x6f, 0xc, 0x8d], }, }; info!( "Writing HOB ACPI table {:x} {:x?} {:x?}", self.current_offset, hob_guid_type, table_content ); mem.write_obj(hob_guid_type, GuestAddress(self.current_offset)) .map_err(TdvfError::GuestMemoryWriteHob)?; let current_offset = self.current_offset + size_of::() as u64; // In case the table is quite large, let's make sure we can handle // retrying until everything has been correctly copied. let mut offset: usize = 0; loop { let bytes_written = mem .write( &table_content[offset..], GuestAddress(current_offset + offset as u64), ) .map_err(TdvfError::GuestMemoryWriteHob)?; offset += bytes_written; if offset >= table_content.len() { break; } } self.current_offset += length as u64; Ok(()) } pub fn add_payload( &mut self, mem: &GuestMemoryMmap, payload_info: PayloadInfo, ) -> Result<(), TdvfError> { let payload = TdPayload { guid_type: HobGuidType { header: HobHeader { r#type: HobType::GuidExtension, length: size_of::() as u16, reserved: 0, }, // HOB_PAYLOAD_INFO_GUID // 0xb96fa412, 0x461f, 0x4be3, {0x8c, 0xd, 0xad, 0x80, 0x5a, 0x49, 0x7a, 0xc0 name: EfiGuid { data1: 0xb96f_a412, data2: 0x461f, data3: 0x4be3, data4: [0x8c, 0xd, 0xad, 0x80, 0x5a, 0x49, 0x7a, 0xc0], }, }, payload_info, }; info!( "Writing HOB TD_PAYLOAD {:x} {:x?}", self.current_offset, payload ); mem.write_obj(payload, GuestAddress(self.current_offset)) .map_err(TdvfError::GuestMemoryWriteHob)?; self.update_offset::(); Ok(()) } } #[cfg(test)] mod unit_tests { use super::*; #[test] #[ignore] fn test_parse_tdvf_sections() { let mut f = File::open("tdvf.fd").unwrap(); let (sections, _) = parse_tdvf_sections(&mut f).unwrap(); for section in sections { eprintln!("{section:x?}"); } } } cloud-hypervisor-53.0/block/000077500000000000000000000000001522476750100160625ustar00rootroot00000000000000cloud-hypervisor-53.0/block/Cargo.toml000066400000000000000000000020201522476750100200040ustar00rootroot00000000000000[package] authors = ["The Chromium OS Authors", "The Cloud Hypervisor Authors"] edition.workspace = true name = "block" rust-version.workspace = true version = "0.1.0" [features] default = [] io_uring = ["dep:io-uring"] test-utils = [] [dependencies] bitflags = { workspace = true } byteorder = { workspace = true } crc-any = "3.0.0" flate2 = "1.1" io-uring = { version = "0.7.12", optional = true } libc = { workspace = true } log = { workspace = true } remain = "0.2.15" serde = { workspace = true, features = ["derive"] } smallvec = { workspace = true } thiserror = { workspace = true } uuid = { workspace = true, features = ["v4"] } virtio-bindings = { workspace = true } virtio-queue = { workspace = true } vm-memory = { workspace = true, features = [ "backend-atomic", "backend-bitmap", "backend-mmap", ] } vm-virtio = { path = "../vm-virtio" } vmm-sys-util = { workspace = true } zerocopy = { workspace = true, features = ["derive"] } zstd = "0.13" [dev-dependencies] cfg-if = { workspace = true } [lints] workspace = true cloud-hypervisor-53.0/block/src/000077500000000000000000000000001522476750100166515ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/aligned_buffer.rs000066400000000000000000000231411522476750100221540ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 use std::alloc::{Layout, alloc_zeroed, dealloc}; use std::os::unix::fs::FileExt; use std::{io, slice}; /// RAII aligned heap buffer for O_DIRECT I/O. /// /// Handles the alignment math for offset and length, allocating a buffer /// that satisfies O_DIRECT constraints. The caller's logical data lives /// at `as_slice()`/`as_mut_slice()` (accounting for head padding when the /// requested offset is not alignment-aligned). The full aligned region is /// used internally for pread/pwrite via `FileExt`. pub(crate) struct AlignedBuffer { ptr: *mut u8, layout: Layout, head_pad: usize, user_len: usize, aligned_len: usize, aligned_offset: u64, } impl AlignedBuffer { /// Create a new aligned buffer for I/O at `offset` of `len` bytes with /// the given `alignment` requirement. /// /// When offset and length are already aligned, `head_pad == 0` and the /// full buffer equals the user's logical portion (no overhead). pub fn new(offset: u64, len: usize, alignment: usize) -> io::Result { if alignment == 0 || !alignment.is_power_of_two() { return Err(io::Error::new( io::ErrorKind::InvalidInput, "alignment must be a non-zero power of two", )); } let mask = alignment as u64 - 1; let aligned_offset = offset & !mask; let head_pad = (offset - aligned_offset) as usize; let min_len = head_pad .checked_add(len) .ok_or_else(|| io::Error::other("aligned buffer length overflow"))?; let aligned_len = if min_len == 0 { 0 } else { let remainder = min_len % alignment; if remainder == 0 { min_len } else { min_len .checked_add(alignment - remainder) .ok_or_else(|| io::Error::other("aligned buffer length overflow"))? } }; // alloc_zeroed is UB on a zero-sized layout, so round the allocation // up to one alignment unit for the zero-length case. The padding is // never exposed: as_slice/full_slice report aligned_len/user_len (0). let layout = Layout::from_size_align(aligned_len.max(alignment), alignment) .map_err(|e| io::Error::other(format!("invalid aligned layout: {e}")))?; // SAFETY: layout has non-zero size. let ptr = unsafe { alloc_zeroed(layout) }; if ptr.is_null() { return Err(io::Error::new( io::ErrorKind::OutOfMemory, "aligned allocation failed", )); } Ok(AlignedBuffer { ptr, layout, head_pad, user_len: len, aligned_len, aligned_offset, }) } /// The caller's logical portion of the buffer (read-only). pub fn as_slice(&self) -> &[u8] { // SAFETY: ptr is valid for layout.size() bytes; head_pad + user_len <= layout.size(). unsafe { slice::from_raw_parts(self.ptr.add(self.head_pad), self.user_len) } } /// The caller's logical portion of the buffer (mutable). pub fn as_mut_slice(&mut self) -> &mut [u8] { // SAFETY: ptr is valid for layout.size() bytes; head_pad + user_len <= layout.size(). unsafe { slice::from_raw_parts_mut(self.ptr.add(self.head_pad), self.user_len) } } fn full_slice(&self) -> &[u8] { // SAFETY: ptr is valid for layout.size() bytes; aligned_len <= layout.size(). unsafe { slice::from_raw_parts(self.ptr, self.aligned_len) } } fn full_mut_slice(&mut self) -> &mut [u8] { // SAFETY: ptr is valid for layout.size() bytes; aligned_len <= layout.size(). unsafe { slice::from_raw_parts_mut(self.ptr, self.aligned_len) } } /// Read into the buffer from `f`, tolerating a short read at EOF. /// /// Returns the number of caller-logical bytes now valid in `as_slice()`, /// accounting for head padding and any short read. pub fn read_from(&mut self, f: &impl FileExt) -> io::Result { let mut total = 0usize; while total < self.aligned_len { let offset = self .aligned_offset .checked_add(total as u64) .ok_or_else(|| io::Error::other("aligned buffer offset overflow"))?; match f.read_at(&mut self.full_mut_slice()[total..], offset) { Ok(0) => break, Ok(n) => total += n, Err(e) if e.kind() == io::ErrorKind::Interrupted => {} Err(e) => return Err(e), } } Ok(total.saturating_sub(self.head_pad).min(self.user_len)) } /// Write the full aligned region from this buffer to `f`. pub fn write_to(&self, f: &impl FileExt) -> io::Result<()> { f.write_all_at(self.full_slice(), self.aligned_offset) } } impl Drop for AlignedBuffer { fn drop(&mut self) { // SAFETY: ptr was allocated by alloc_zeroed with self.layout. unsafe { dealloc(self.ptr, self.layout) }; } } // SAFETY: The buffer is a plain heap allocation with no interior references. unsafe impl Send for AlignedBuffer {} #[cfg(test)] mod tests { use std::io::Write; use std::os::unix::fs::FileExt; use vmm_sys_util::tempfile::TempFile; use super::*; fn create_pattern_file(size: usize) -> TempFile { let tf = TempFile::new().unwrap(); let pattern: Vec = (0..size).map(|i| (i % 251) as u8).collect(); tf.as_file().write_all(&pattern).unwrap(); tf.as_file().sync_all().unwrap(); tf } #[test] fn test_read_aligned() { let size = 4096usize; let tf = create_pattern_file(size); let alignment = 512; let mut abuf = AlignedBuffer::new(0, size, alignment).unwrap(); abuf.read_from(tf.as_file()).unwrap(); let expected: Vec = (0..size).map(|i| (i % 251) as u8).collect(); assert_eq!(abuf.as_slice(), &expected[..]); } #[test] fn test_zero_len_is_noop() { let tf = create_pattern_file(512); let mut abuf = AlignedBuffer::new(100, 0, 512).unwrap(); abuf.read_from(tf.as_file()).unwrap(); abuf.write_to(tf.as_file()).unwrap(); assert!(abuf.as_slice().is_empty()); assert!(abuf.as_mut_slice().is_empty()); } #[test] fn test_read_unaligned_offset() { let file_size = 8192usize; let tf = create_pattern_file(file_size); let alignment = 512; let offset = 100u64; let len = 200usize; let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap(); abuf.read_from(tf.as_file()).unwrap(); let expected: Vec = (offset as usize..offset as usize + len) .map(|i| (i % 251) as u8) .collect(); assert_eq!(abuf.as_slice(), &expected[..]); } #[test] fn test_write_aligned() { let size = 4096usize; let tf = create_pattern_file(size); let alignment = 512; let data: Vec = (0..size).map(|i| ((i + 1) % 251) as u8).collect(); let mut abuf = AlignedBuffer::new(0, size, alignment).unwrap(); abuf.as_mut_slice().copy_from_slice(&data); abuf.write_to(tf.as_file()).unwrap(); let mut readback = vec![0u8; size]; tf.as_file().read_exact_at(&mut readback, 0).unwrap(); assert_eq!(readback, data); } #[test] fn test_write_unaligned_offset_rmw() { let file_size = 8192usize; let tf = create_pattern_file(file_size); let alignment = 512; let offset = 100u64; let len = 200usize; let data: Vec = (0..len).map(|i| ((i + 1) % 239) as u8).collect(); let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap(); abuf.read_from(tf.as_file()).unwrap(); abuf.as_mut_slice().copy_from_slice(&data); abuf.write_to(tf.as_file()).unwrap(); let mut whole = vec![0u8; file_size]; tf.as_file().read_exact_at(&mut whole, 0).unwrap(); let before: Vec = (0..offset as usize).map(|i| (i % 251) as u8).collect(); assert_eq!(&whole[..offset as usize], &before[..]); assert_eq!(&whole[offset as usize..offset as usize + len], &data[..]); let after_start = offset as usize + len; let after: Vec = (after_start..file_size).map(|i| (i % 251) as u8).collect(); assert_eq!(&whole[after_start..], &after[..]); } #[test] fn test_4096_alignment() { let file_size = 16384usize; let tf = create_pattern_file(file_size); let alignment = 4096; let offset = 4096u64; let len = 4096usize; let data: Vec = (0..len).map(|i| ((i + 1) % 239) as u8).collect(); let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap(); abuf.read_from(tf.as_file()).unwrap(); abuf.as_mut_slice().copy_from_slice(&data); abuf.write_to(tf.as_file()).unwrap(); let mut abuf = AlignedBuffer::new(offset, len, alignment).unwrap(); abuf.read_from(tf.as_file()).unwrap(); assert_eq!(abuf.as_slice(), &data[..]); let mut whole = vec![0u8; file_size]; tf.as_file().read_exact_at(&mut whole, 0).unwrap(); let before: Vec = (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[..]); } } cloud-hypervisor-53.0/block/src/aligned_file.rs000066400000000000000000000310761522476750100216300ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 use std::fs::{File, Metadata}; use std::io; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::FileExt; use std::os::unix::io::{AsRawFd, RawFd}; use vmm_sys_util::file_traits::FileSync; use vmm_sys_util::seek_hole::SeekHole; use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; use crate::aligned_buffer::AlignedBuffer; use crate::{SECTOR_SIZE, probe_direct_alignment}; /// True when `buf_ptr`/`len`/`offset` already satisfy `alignment` /// (`alignment == 0` means no O_DIRECT, so everything is "aligned"). fn is_aligned(alignment: usize, buf_ptr: usize, len: usize, offset: u64) -> bool { alignment == 0 || (buf_ptr.is_multiple_of(alignment) && len.is_multiple_of(alignment) && offset.is_multiple_of(alignment as u64)) } /// A `File` that transparently satisfies O_DIRECT alignment requirements. /// /// `alignment == 0` means no O_DIRECT (all I/O passes straight through). /// For unaligned requests under O_DIRECT, I/O is bounced through an /// `AlignedBuffer` (read-modify-write for writes). #[derive(Debug)] pub struct AlignedFile { file: File, alignment: usize, } impl AlignedFile { /// Wrap `file`, querying the O_DIRECT block alignment when `direct_io`. pub fn new(file: File, direct_io: bool) -> Self { let alignment = if direct_io { probe_direct_alignment(file.as_raw_fd()).unwrap_or(SECTOR_SIZE) as usize } else { 0 }; AlignedFile { file, alignment } } pub fn alignment(&self) -> usize { self.alignment } pub fn file(&self) -> &File { &self.file } pub fn file_mut(&mut self) -> &mut File { &mut self.file } pub fn try_clone(&self) -> io::Result { Ok(AlignedFile { file: self.file.try_clone()?, alignment: self.alignment, }) } pub fn set_len(&self, size: u64) -> io::Result<()> { self.file.set_len(size) } pub fn metadata(&self) -> io::Result { self.file.metadata() } pub fn sync_all(&self) -> io::Result<()> { self.file.sync_all() } pub fn sync_data(&self) -> io::Result<()> { self.file.sync_data() } pub fn is_direct(&self) -> bool { self.alignment != 0 } pub fn is_writable(&self) -> bool { // SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) }; if flags < 0 { return false; } let access_mode = flags & libc::O_ACCMODE; access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR } /// Wrap `file` with an explicit alignment, bypassing the probe. Used by /// tests to force the bounce/RMW path without a real O_DIRECT fd. #[cfg(test)] pub fn with_alignment(file: File, alignment: usize) -> Self { AlignedFile { file, alignment } } /// Read `len` bytes at `offset` through an aligned bounce buffer. pub(crate) fn read_unaligned( &self, offset: u64, len: usize, scatter: impl FnOnce(&[u8]) -> io::Result<()>, ) -> io::Result { let mut abuf = AlignedBuffer::new(offset, len, self.alignment)?; let n = abuf.read_from(&self.file)?; scatter(&abuf.as_slice()[..n])?; Ok(n) } /// Write `len` bytes at `offset` through an aligned bounce buffer. pub(crate) fn write_unaligned( &self, offset: u64, len: usize, gather: impl FnOnce(&mut [u8]) -> io::Result<()>, ) -> io::Result { let mut abuf = AlignedBuffer::new(offset, len, self.alignment)?; abuf.read_from(&self.file)?; // RMW: preserve head/tail padding gather(abuf.as_mut_slice())?; abuf.write_to(&self.file)?; Ok(len) } } impl FileExt for AlignedFile { fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { if buf.is_empty() { return Ok(0); } if is_aligned(self.alignment, buf.as_ptr() as usize, buf.len(), offset) { return self.file.read_at(buf, offset); } self.read_unaligned(offset, buf.len(), |data| { buf[..data.len()].copy_from_slice(data); Ok(()) }) } fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { if buf.is_empty() { return Ok(0); } if is_aligned(self.alignment, buf.as_ptr() as usize, buf.len(), offset) { return self.file.write_at(buf, offset); } self.write_unaligned(offset, buf.len(), |dst| { dst.copy_from_slice(buf); Ok(()) }) } } impl WriteZeroesAt for AlignedFile { fn write_zeroes_at(&mut self, offset: u64, length: usize) -> io::Result { self.file.write_zeroes_at(offset, length) } } impl PunchHole for AlignedFile { fn punch_hole(&mut self, offset: u64, length: u64) -> io::Result<()> { self.file.punch_hole(offset, length) } } impl FileSync for AlignedFile { fn fsync(&mut self) -> io::Result<()> { self.file.fsync() } } impl SeekHole for AlignedFile { fn seek_hole(&mut self, offset: u64) -> io::Result> { self.file.seek_hole(offset) } fn seek_data(&mut self, offset: u64) -> io::Result> { self.file.seek_data(offset) } } impl Clone for AlignedFile { fn clone(&self) -> Self { self.try_clone().expect("AlignedFile cloning failed") } } impl AsRawFd for AlignedFile { fn as_raw_fd(&self) -> RawFd { self.file.as_raw_fd() } } impl AsFd for AlignedFile { fn as_fd(&self) -> BorrowedFd<'_> { self.file.as_fd() } } #[cfg(test)] mod tests { use std::io::Write; use std::os::unix::fs::FileExt; use vmm_sys_util::tempfile::TempFile; use super::*; fn pattern_file(size: usize) -> TempFile { let tf = TempFile::new().unwrap(); let p: Vec = (0..size).map(|i| (i % 251) as u8).collect(); tf.as_file().write_all(&p).unwrap(); tf.as_file().sync_all().unwrap(); tf } fn forced(file: File, alignment: usize) -> AlignedFile { AlignedFile { file, alignment } } #[test] fn new_probes_alignment_and_accessors() { let tf = pattern_file(8192); // Not O_DIRECT, so new() falls back to SECTOR_SIZE (512). let mut af = AlignedFile::new(tf.as_file().try_clone().unwrap(), true); assert_eq!(af.alignment(), 512); let _ = af.file(); let _ = af.file_mut(); let _ = af.try_clone().unwrap(); let plain = AlignedFile::new(tf.as_file().try_clone().unwrap(), false); assert_eq!(plain.alignment(), 0); } #[test] fn read_unaligned_offset_matches_contents() { let tf = pattern_file(8192); let af = forced(tf.as_file().try_clone().unwrap(), 512); let mut buf = vec![0u8; 200]; assert_eq!(af.read_at(&mut buf, 100).unwrap(), 200); let want: Vec = (100..300).map(|i| (i % 251) as u8).collect(); assert_eq!(buf, want); } #[test] fn read_unaligned_short_at_eof() { let tf = pattern_file(100); let af = forced(tf.as_file().try_clone().unwrap(), 512); let mut buf = vec![0u8; 200]; assert_eq!(af.read_at(&mut buf, 10).unwrap(), 90); } #[test] fn write_unaligned_offset_is_rmw() { let tf = pattern_file(8192); let af = forced(tf.as_file().try_clone().unwrap(), 512); let data: Vec = (0..200).map(|i| ((i + 1) % 239) as u8).collect(); assert_eq!(af.write_at(&data, 100).unwrap(), 200); let mut whole = vec![0u8; 8192]; tf.as_file().read_exact_at(&mut whole, 0).unwrap(); let before: Vec = (0..100).map(|i| (i % 251) as u8).collect(); assert_eq!(&whole[..100], &before[..]); assert_eq!(&whole[100..300], &data[..]); let after: Vec = (300..8192).map(|i| (i % 251) as u8).collect(); assert_eq!(&whole[300..], &after[..]); } #[test] fn aligned_passthrough_roundtrip() { let tf = pattern_file(4096); let af = forced(tf.as_file().try_clone().unwrap(), 512); let mut buf = vec![0u8; 512]; assert_eq!(af.read_at(&mut buf, 512).unwrap(), 512); let want: Vec = (512..1024).map(|i| (i % 251) as u8).collect(); assert_eq!(buf, want); } #[test] fn no_alignment_is_plain_passthrough() { let tf = pattern_file(100); let af = forced(tf.as_file().try_clone().unwrap(), 0); let mut buf = vec![0u8; 50]; assert_eq!(af.read_at(&mut buf, 10).unwrap(), 50); } #[test] fn test_unaligned_read_beyond_eof_returns_zero() { let tf = pattern_file(100); let af = forced(tf.as_file().try_clone().unwrap(), 512); let mut buf = vec![0u8; 16]; assert_eq!(af.read_at(&mut buf, 200).unwrap(), 0); } #[test] fn test_unaligned_write_extends_at_eof() { let file_size = 100usize; let tf = pattern_file(file_size); let af = forced(tf.as_file().try_clone().unwrap(), 512); let data = b"xyz"; assert_eq!(af.write_at(data, file_size as u64).unwrap(), data.len()); let mut readback = vec![0u8; file_size + data.len()]; tf.as_file().read_exact_at(&mut readback, 0).unwrap(); let expected_prefix: Vec = (0..file_size).map(|i| (i % 251) as u8).collect(); assert_eq!(&readback[..file_size], &expected_prefix[..]); assert_eq!(&readback[file_size..], data); } #[test] fn test_empty_unaligned_io_is_noop() { let tf = pattern_file(100); let af = forced(tf.as_file().try_clone().unwrap(), 512); let mut read_buf = []; assert_eq!(af.read_at(&mut read_buf, 1).unwrap(), 0); assert_eq!(af.write_at(&[], 1).unwrap(), 0); } #[test] fn read_unaligned_scatters_in_a_single_copy() { let file = pattern_file(8192); let aligned_file = forced(file.as_file().try_clone().unwrap(), 512); let mut out = vec![0u8; 200]; let n = aligned_file .read_unaligned(100, 200, |data| { out.copy_from_slice(data); Ok(()) }) .unwrap(); assert_eq!(n, 200); let want: Vec = (100..300).map(|i| (i % 251) as u8).collect(); assert_eq!(out, want); } #[test] fn read_unaligned_closure_short_at_eof() { let file = pattern_file(100); let aligned_file = forced(file.as_file().try_clone().unwrap(), 512); let mut seen = 0usize; let n = aligned_file .read_unaligned(10, 200, |data| { seen = data.len(); Ok(()) }) .unwrap(); assert_eq!(n, 90); assert_eq!(seen, 90); } #[test] fn write_unaligned_gather_is_rmw() { let file = pattern_file(8192); let aligned_file = forced(file.as_file().try_clone().unwrap(), 512); let data: Vec = (0..200).map(|i| ((i + 1) % 239) as u8).collect(); let n = aligned_file .write_unaligned(100, 200, |buf| { buf.copy_from_slice(&data); Ok(()) }) .unwrap(); assert_eq!(n, 200); let mut whole = vec![0u8; 8192]; file.as_file().read_exact_at(&mut whole, 0).unwrap(); let before: Vec = (0..100).map(|i| (i % 251) as u8).collect(); assert_eq!(&whole[..100], &before[..]); assert_eq!(&whole[100..300], &data[..]); let after: Vec = (300..8192).map(|i| (i % 251) as u8).collect(); assert_eq!(&whole[300..], &after[..]); } #[test] fn read_unaligned_propagates_closure_error() { let file = pattern_file(8192); let aligned_file = forced(file.as_file().try_clone().unwrap(), 512); let err = aligned_file .read_unaligned(100, 200, |_| { Err(io::Error::new(io::ErrorKind::InvalidInput, "boom")) }) .unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } #[test] fn write_unaligned_propagates_closure_error() { let file = pattern_file(8192); let aligned_file = forced(file.as_file().try_clone().unwrap(), 512); let err = aligned_file .write_unaligned(100, 200, |_| { Err(io::Error::new(io::ErrorKind::InvalidInput, "boom")) }) .unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } } cloud-hypervisor-53.0/block/src/disk_file.rs000066400000000000000000000143011522476750100211470ustar00rootroot00000000000000// 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 {} cloud-hypervisor-53.0/block/src/error.rs000066400000000000000000000161021522476750100203500ustar00rootroot00000000000000// 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; cloud-hypervisor-53.0/block/src/factory.rs000066400000000000000000000224371522476750100206760ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! Disk image factory. //! //! [`open_disk`] is the single entry point for opening a disk image. //! It opens the file, detects the image format, probes async I/O //! support, and constructs the appropriate backend. Callers receive //! a trait object that is ready for use by virtio queue workers. use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use std::sync::OnceLock; use std::{fmt, fs}; use log::info; #[cfg(feature = "io_uring")] use crate::block_io_uring_is_supported; use crate::disk_file::AsyncFullDiskFile; use crate::error::{BlockError, BlockErrorKind, BlockResult}; use crate::formats::qcow::QcowDisk; use crate::formats::raw::{RawBackend, RawDisk}; use crate::formats::vhd::VhdDisk; use crate::formats::vhdx::VhdxDisk; use crate::{ ImageType, block_aio_is_supported, detect_image_type, open_disk_image, preallocate_disk, }; /// Options for opening a disk image via [`open_disk`]. pub struct DiskOpenOptions<'a> { pub path: &'a Path, pub readonly: bool, pub direct: bool, pub sparse: bool, pub backing_files: bool, pub disable_io_uring: bool, pub disable_aio: bool, } /// Result of [`open_disk`], carrying the detected image type alongside /// the constructed backend. pub struct OpenedDisk { pub image_type: ImageType, pub disk: Box, } 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( VhdxDisk::new(file, options.direct).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( VhdDisk::new(file, true, options.direct).map_err(|e| e.with_path(options.path))?, )); } info!("io_uring runtime probe failed for fixed VHD, using synchronous backend"); } info!("Opening fixed VHD disk file with synchronous backend"); Ok(Box::new( VhdDisk::new(file, false, options.direct).map_err(|e| e.with_path(options.path))?, )) } fn open_raw( file: fs::File, options: &DiskOpenOptions<'_>, ) -> BlockResult> { if !options.readonly && !options.sparse { preallocate_disk(&file, options.path); } #[cfg(feature = "io_uring")] if !options.disable_io_uring { if io_uring_supported() { info!("Opening RAW disk file with io_uring backend"); return Ok(Box::new(RawDisk::new( file, RawBackend::IoUring, options.direct, ))); } info!("io_uring runtime probe failed for RAW, trying next backend"); } if !options.disable_aio { if aio_supported() { info!("Opening RAW disk file with AIO backend"); return Ok(Box::new(RawDisk::new( file, RawBackend::Aio, options.direct, ))); } info!("AIO runtime probe failed for RAW, using synchronous backend"); } info!("Opening RAW disk file with synchronous backend"); Ok(Box::new(RawDisk::new( file, RawBackend::Sync, options.direct, ))) } fn open_qcow2( file: fs::File, options: &DiskOpenOptions<'_>, ) -> BlockResult> { #[cfg(feature = "io_uring")] if !options.disable_io_uring { if io_uring_supported() { info!("Opening QCOW2 disk file with io_uring backend"); return Ok(Box::new( QcowDisk::new( file, options.direct, options.backing_files, options.sparse, true, ) .map_err(|e| e.with_path(options.path))?, )); } info!("io_uring runtime probe failed for QCOW2, using synchronous backend"); } info!("Opening QCOW2 disk file with synchronous backend"); Ok(Box::new( QcowDisk::new( file, options.direct, options.backing_files, options.sparse, false, ) .map_err(|e| e.with_path(options.path))?, )) } #[cfg(test)] mod unit_tests { use std::path::Path; use vmm_sys_util::tempfile::TempFile; use super::*; use crate::formats::qcow; fn default_options(path: &Path) -> DiskOpenOptions<'_> { DiskOpenOptions { path, readonly: false, direct: false, sparse: false, backing_files: false, disable_io_uring: true, disable_aio: true, } } #[test] fn nonexistent_path_returns_error() { let path = Path::new("/tmp/no_such_disk_image.raw"); let options = default_options(path); match open_disk(&options) { Err(e) => assert_eq!(e.kind(), BlockErrorKind::Io), Ok(_) => panic!("expected error for nonexistent path"), } } #[test] fn detect_raw_image() { let tmp = TempFile::new().unwrap(); tmp.as_file().set_len(1 << 20).unwrap(); let path = tmp.as_path().to_owned(); let options = default_options(&path); let opened = open_disk(&options).unwrap(); assert_eq!(opened.image_type, ImageType::Raw); } #[test] fn detect_qcow2_image() { let tmp = qcow::QcowTempDisk::new(100 * 1024 * 1024, None, false, true, false) .unwrap() .into_tempfile(); let path = tmp.as_path().to_owned(); let options = default_options(&path); let opened = open_disk(&options).unwrap(); assert_eq!(opened.image_type, ImageType::Qcow2); } #[test] fn open_readonly() { let tmp = TempFile::new().unwrap(); tmp.as_file().set_len(1 << 20).unwrap(); let path = tmp.as_path().to_owned(); let mut options = default_options(&path); options.readonly = true; let opened = open_disk(&options).unwrap(); assert_eq!(opened.image_type, ImageType::Raw); } #[test] fn sync_fallback_when_async_disabled() { let tmp = TempFile::new().unwrap(); let size = 1u64 << 20; tmp.as_file().set_len(size).unwrap(); let path = tmp.as_path().to_owned(); let options = DiskOpenOptions { path: &path, readonly: false, direct: false, sparse: false, backing_files: false, disable_io_uring: true, disable_aio: true, }; let opened = open_disk(&options).unwrap(); assert_eq!(opened.image_type, ImageType::Raw); assert_eq!(opened.disk.logical_size().unwrap(), size); } } cloud-hypervisor-53.0/block/src/formats/000077500000000000000000000000001522476750100203245ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/formats/mod.rs000066400000000000000000000005151522476750100214520ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! Disk format implementations. //! //! Each format lives in its own submodule with a `DiskFile` wrapper, //! format specific internals, and sync/async I/O workers. pub mod qcow; pub mod raw; pub mod vhd; pub mod vhdx; cloud-hypervisor-53.0/block/src/formats/qcow/000077500000000000000000000000001522476750100212755ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/formats/qcow/backing.rs000066400000000000000000000143151522476750100232450ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! Thread safe backing file readers for QCOW2 images. use std::fs::File; use std::io; use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; use std::os::unix::fs::FileExt; use std::sync::Arc; use super::decoder::Decoder; use super::metadata::{BackingRead, ClusterReadMapping, QcowMetadata}; use super::parser::{BackingFile, BackingKind, Error as QcowError}; use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; use crate::formats::qcow::common::decompress_cluster; /// Raw backing file using position-independent reads on a duplicated fd. pub(crate) struct RawBacking { pub(crate) file: File, pub(crate) virtual_size: u64, } // SAFETY: The only I/O operation is read_at which is position independent // and safe for concurrent use from multiple threads. unsafe impl Sync for RawBacking {} impl BackingRead for RawBacking { fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> { if address >= self.virtual_size { buf.fill(0); return Ok(()); } let available = (self.virtual_size - address) as usize; if available >= buf.len() { self.file.read_exact_at(buf, address) } else { self.file.read_exact_at(&mut buf[..available], address)?; buf[available..].fill(0); Ok(()) } } } /// QCOW2 image used as a backing file for another QCOW2 image. /// /// Resolves guest offsets through the QCOW2 cluster mapping (L1/L2 /// tables, refcounts) before reading the underlying data. Read only /// because backing files never receive writes. Nested backing chains /// are handled recursively via the optional `backing_file` field. pub(crate) struct Qcow2Backing { pub(crate) metadata: Arc, pub(crate) data_file: File, pub(crate) backing_file: Option>, pub(crate) cluster_size: u64, pub(crate) decoder: Arc, } // SAFETY: All reads go through QcowMetadata which uses RwLock // and read_exact_at which is position independent and thread safe. unsafe impl Sync for Qcow2Backing {} impl BackingRead for Qcow2Backing { fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> { let virtual_size = self.metadata.virtual_size(); if address >= virtual_size { buf.fill(0); return Ok(()); } let available = (virtual_size - address) as usize; if available < buf.len() { self.read_clusters(address, &mut buf[..available])?; buf[available..].fill(0); return Ok(()); } self.read_clusters(address, buf) } } impl Qcow2Backing { fn read_clusters(&self, address: u64, buf: &mut [u8]) -> io::Result<()> { let total_len = buf.len(); let has_backing = self.backing_file.is_some(); let mappings = self .metadata .map_clusters_for_read(address, total_len, has_backing)?; let mut buf_offset = 0usize; for mapping in mappings { match mapping { ClusterReadMapping::Zero { length } => { buf[buf_offset..buf_offset + length as usize].fill(0); buf_offset += length as usize; } ClusterReadMapping::Allocated { offset: host_offset, length, } => { self.data_file.read_exact_at( &mut buf[buf_offset..buf_offset + length as usize], host_offset, )?; buf_offset += length as usize; } ClusterReadMapping::Compressed { host_offset, compressed_size, cluster_offset, length, } => { let mut compressed = vec![0u8; compressed_size]; self.data_file.read_exact_at(&mut compressed, host_offset)?; let decompressed = decompress_cluster( &compressed, self.cluster_size as usize, &*self.decoder, )?; buf[buf_offset..buf_offset + length] .copy_from_slice(&decompressed[cluster_offset..cluster_offset + length]); buf_offset += length; } ClusterReadMapping::Backing { offset: backing_offset, length, } => { self.backing_file.as_ref().unwrap().read_at( backing_offset, &mut buf[buf_offset..buf_offset + length as usize], )?; buf_offset += length as usize; } } } Ok(()) } } impl Drop for Qcow2Backing { fn drop(&mut self) { self.metadata.shutdown(); } } /// Construct a thread safe backing file reader. pub(super) fn shared_backing_from(bf: BackingFile) -> BlockResult> { 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 file = File::from(dup_fd(raw_file.as_fd())?); Ok(Arc::new(RawBacking { file, virtual_size })) } BackingKind::Qcow { inner, backing } => { let data_file = File::from(dup_fd(inner.raw_file.as_fd())?); let metadata = Arc::new(QcowMetadata::new(*inner)); Ok(Arc::new(Qcow2Backing { cluster_size: metadata.cluster_size(), decoder: metadata.decoder(), metadata, data_file, backing_file: backing.map(|bf| shared_backing_from(*bf)).transpose()?, })) } } } cloud-hypervisor-53.0/block/src/formats/qcow/common.rs000066400000000000000000000132211522476750100231320ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! Shared helpers for QCOW2 sync and async backends. use std::io; use super::decoder::Decoder; /// Decompress a full QCOW2 cluster from compressed data. /// /// Returns a `cluster_size` byte buffer with the decompressed cluster /// content. Fails if the decoder does not produce exactly `cluster_size` /// bytes. pub(super) fn decompress_cluster( compressed: &[u8], cluster_size: usize, decoder: &dyn Decoder, ) -> io::Result> { let mut decompressed = vec![0u8; cluster_size]; let n = decoder .decode(compressed, &mut decompressed) .map_err(|_| io::Error::from_raw_os_error(libc::EIO))?; if n != cluster_size { return Err(io::Error::from_raw_os_error(libc::EIO)); } Ok(decompressed) } #[cfg(test)] pub(crate) mod unit_tests { use std::fs::File; use std::io::Write; use std::os::unix::fs::FileExt; use flate2::Compression; use flate2::write::DeflateEncoder; use super::super::decoder::ZlibDecoder; use super::decompress_cluster; const COMPRESSED_FLAG: u64 = 1 << 62; const CLUSTER_USED_FLAG: u64 = 1 << 63; const COMPRESSED_SECTOR_SIZE: u64 = 512; const HEADER_CLUSTER_BITS_OFFSET: u64 = 20; const HEADER_L1_SIZE_OFFSET: u64 = 36; const HEADER_L1_TABLE_OFFSET: u64 = 40; const L1_L2_ADDR_MASK: u64 = 0x00ff_ffff_ffff_fe00; fn make_compressed_l2_entry(host_offset: u64, compressed_len: usize, cluster_bits: u32) -> u64 { let compressed_size_shift = 62 - (cluster_bits - 8); let intra_sector_offset = host_offset & (COMPRESSED_SECTOR_SIZE - 1); let total_bytes = compressed_len as u64 + intra_sector_offset; let nsectors = total_bytes.div_ceil(COMPRESSED_SECTOR_SIZE); let addr_part = host_offset & ((1 << compressed_size_shift) - 1); let size_part = (nsectors - 1) << compressed_size_shift; COMPRESSED_FLAG | size_part | addr_part } /// Compress every allocated cluster in a QCOW2 image file in place. pub fn compress_allocated_clusters(file: &mut File) { let mut buf4 = [0u8; 4]; file.read_exact_at(&mut buf4, HEADER_CLUSTER_BITS_OFFSET) .unwrap(); let cluster_bits = u32::from_be_bytes(buf4); let cluster_size = 1u64 << cluster_bits; file.read_exact_at(&mut buf4, HEADER_L1_SIZE_OFFSET) .unwrap(); let l1_size = u32::from_be_bytes(buf4); let mut buf8 = [0u8; 8]; file.read_exact_at(&mut buf8, HEADER_L1_TABLE_OFFSET) .unwrap(); let l1_table_offset = u64::from_be_bytes(buf8); let entries_per_l2 = cluster_size / 8; let mut append_offset = file.metadata().unwrap().len(); append_offset = (append_offset + 511) & !511; for l1_idx in 0..l1_size as u64 { let l1_entry_offset = l1_table_offset + l1_idx * 8; file.read_exact_at(&mut buf8, l1_entry_offset).unwrap(); let l1_entry = u64::from_be_bytes(buf8); let l2_table_addr = l1_entry & L1_L2_ADDR_MASK; if l2_table_addr == 0 { continue; } for l2_idx in 0..entries_per_l2 { let l2_entry_offset = l2_table_addr + l2_idx * 8; file.read_exact_at(&mut buf8, l2_entry_offset).unwrap(); let l2_entry = u64::from_be_bytes(buf8); if l2_entry & CLUSTER_USED_FLAG == 0 || l2_entry & COMPRESSED_FLAG != 0 { continue; } let host_cluster_addr = l2_entry & L1_L2_ADDR_MASK; if host_cluster_addr == 0 { continue; } let mut cluster_data = vec![0u8; cluster_size as usize]; file.read_exact_at(&mut cluster_data, host_cluster_addr) .unwrap(); let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); encoder.write_all(&cluster_data).unwrap(); let compressed = encoder.finish().unwrap(); file.write_all_at(&compressed, append_offset).unwrap(); let padded_len = (compressed.len() + 511) & !511; if padded_len > compressed.len() { let padding = vec![0u8; padded_len - compressed.len()]; file.write_all_at(&padding, append_offset + compressed.len() as u64) .unwrap(); } let new_entry = make_compressed_l2_entry(append_offset, compressed.len(), cluster_bits); file.write_all_at(&new_entry.to_be_bytes(), l2_entry_offset) .unwrap(); append_offset += padded_len as u64; } } file.flush().unwrap(); } #[test] fn test_decompress_cluster() { let cluster_size = 65536; let original: Vec = (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)); } } cloud-hypervisor-53.0/block/src/formats/qcow/decoder.rs000066400000000000000000000051721522476750100232550ustar00rootroot00000000000000// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 use std::{io, result}; use thiserror::Error; #[derive(Debug, Error)] pub enum Error { #[error("Zlib decompress error")] ZlibDecompress(#[source] flate2::DecompressError), #[error("Zlib unexpected status: {0:?}")] ZlibUnexpectedStatus(flate2::Status), #[error("Zstd decompress error")] ZstdDecompress(#[source] io::Error), #[error("Zstd: failed to fill buffer")] ZstdFillBuffer(#[source] io::Error), } pub(super) type Result = 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(super) 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(super) 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(); } } cloud-hypervisor-53.0/block/src/formats/qcow/engine_sync.rs000066400000000000000000002161121522476750100241470ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::cmp::min; use std::collections::VecDeque; use std::os::unix::fs::FileExt; use std::sync::Arc; use vmm_sys_util::eventfd::EventFd; use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; use super::common::decompress_cluster; use super::decoder::Decoder; use super::metadata::{ BackingRead, ClusterReadMapping, ClusterWriteMapping, DeallocAction, QcowMetadata, }; use super::qcow_raw_file::QcowRawFile; use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult}; pub(super) struct QcowSync { metadata: Arc, data_file: QcowRawFile, /// See the backing_file field on QcowDisk. backing_file: Option>, sparse: bool, cluster_size: u64, decoder: Arc, eventfd: EventFd, completion_list: VecDeque, } impl QcowSync { pub(crate) fn new( metadata: Arc, data_file: QcowRawFile, backing_file: Option>, sparse: bool, ) -> Self { QcowSync { cluster_size: metadata.cluster_size(), decoder: metadata.decoder(), metadata, data_file, backing_file, sparse, eventfd: EventFd::new(libc::EFD_NONBLOCK) .expect("Failed creating EventFd for QcowSync"), 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); } } } fn read_operation(&mut self, op: &mut AsyncIoOperation) -> AsyncIoResult { let address = op.offset() as u64; let total_len = op.total_len(); 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 } => { op.fill_zeroes_at(buf_offset, length as usize) .map_err(AsyncIoError::ReadVectored)?; buf_offset += length as usize; } ClusterReadMapping::Allocated { offset: host_offset, length, } => { let len = length as usize; let mut buf = vec![0u8; len]; self.data_file .file() .read_exact_at(&mut buf, host_offset) .map_err(AsyncIoError::ReadVectored)?; op.write_bytes_at(buf_offset, &buf) .map_err(AsyncIoError::ReadVectored)?; buf_offset += len; } ClusterReadMapping::Compressed { host_offset, compressed_size, cluster_offset, length, } => { let mut compressed = vec![0u8; compressed_size]; self.data_file .file() .read_exact_at(&mut compressed, host_offset) .map_err(AsyncIoError::ReadVectored)?; let decompressed = decompress_cluster(&compressed, self.cluster_size as usize, &*self.decoder) .map_err(AsyncIoError::ReadVectored)?; op.write_bytes_at( buf_offset, &decompressed[cluster_offset..cluster_offset + length], ) .map_err(AsyncIoError::ReadVectored)?; buf_offset += length; } ClusterReadMapping::Backing { offset: backing_offset, length, } => { let mut buf = vec![0u8; length as usize]; self.backing_file .as_ref() .unwrap() .read_at(backing_offset, &mut buf) .map_err(AsyncIoError::ReadVectored)?; op.write_bytes_at(buf_offset, &buf) .map_err(AsyncIoError::ReadVectored)?; buf_offset += length as usize; } } } Ok(total_len) } fn write_operation(&mut self, op: &AsyncIoOperation) -> AsyncIoResult { let address = op.offset() as u64; let total_len = op.total_len(); let mut buf_offset = 0usize; while buf_offset < total_len { let curr_addr = address + buf_offset as u64; let intra_offset = curr_addr & (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, } => { let mut buf = vec![0u8; count]; op.read_bytes_at(buf_offset, &mut buf) .map_err(AsyncIoError::WriteVectored)?; self.data_file .file() .write_all_at(&buf, host_offset) .map_err(AsyncIoError::WriteVectored)?; } } buf_offset += count; } Ok(total_len) } } impl AsyncIo for QcowSync { fn notifier(&self) -> &EventFd { &self.eventfd } fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> { let is_read = op.is_read(); let total_len = if is_read { self.read_operation(&mut op)? } else { self.write_operation(&op)? }; self.completion_list .push_back(AsyncIoCompletion::from_operation(op, 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(AsyncIoCompletion::new(user_data, 0, None)); self.eventfd.write(1).unwrap(); } Ok(()) } fn next_completed_request(&mut self) -> Option { self.completion_list.pop_front() } fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { let result = self .metadata .deallocate_bytes( offset, length as usize, self.sparse, false, 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(AsyncIoCompletion::new(user_data, 0, None)); 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(AsyncIoCompletion::new(user_data, errno, None)); self.eventfd.write(1).unwrap(); Ok(()) } } } fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { let result = self .metadata .deallocate_bytes( offset, length as usize, self.sparse, true, self.backing_file.as_deref(), ) .map_err(AsyncIoError::WriteZeroes); match result { Ok(actions) => { for action in &actions { self.apply_dealloc_action(action); } self.completion_list .push_back(AsyncIoCompletion::new(user_data, 0, None)); self.eventfd.write(1).unwrap(); Ok(()) } Err(e) => { let errno = if let AsyncIoError::WriteZeroes(ref io_err) = e { -io_err.raw_os_error().unwrap_or(libc::EIO) } else { -libc::EIO }; self.completion_list .push_back(AsyncIoCompletion::new(user_data, errno, None)); self.eventfd.write(1).unwrap(); Ok(()) } } } } #[cfg(test)] mod unit_tests { use std::fs::{File, OpenOptions, create_dir}; use std::io::Write; use std::os::unix::fs::FileExt; use std::path::Path; use std::sync::Arc; use std::{env, thread}; use vmm_sys_util::tempdir::TempDir; use vmm_sys_util::tempfile::TempFile; use super::*; use crate::aligned_file::AlignedFile; use crate::async_io::{AsyncIoCompletion, OwnedIoBuffer}; use crate::disk_file::{AsyncDiskFile, DiskSize, Resizable}; use crate::error::BlockErrorKind; use crate::formats::qcow; use crate::formats::qcow::common::unit_tests::compress_allocated_clusters; use crate::formats::qcow::{ BackingFileConfig, Error as QcowError, ImageType, QcowDisk, QcowHeader, QcowTempDisk, }; const TEST_L1_L2_ADDR_MASK: u64 = 0x00ff_ffff_ffff_fe00; const TEST_HEADER_L1_TABLE_OFFSET: u64 = 40; const TEST_CLUSTER_USED_FLAG: u64 = 1 << 63; const TEST_COMPRESSED_FLAG: u64 = 1 << 62; const TEST_ZERO_FLAG: u64 = 1; const TEST_OUT_OF_BOUNDS_CLUSTER: u64 = 0x0000_0001_4000_0000; fn read_be_u64_at(file: &mut File, offset: u64) -> u64 { let mut bytes = [0u8; 8]; file.read_exact_at(&mut bytes, offset).unwrap(); u64::from_be_bytes(bytes) } fn write_be_u64_at(file: &mut File, offset: u64, value: u64) { file.write_all_at(&value.to_be_bytes(), offset).unwrap(); } fn first_l2_entry_offset(file: &mut File) -> u64 { let l1_table_offset = read_be_u64_at(file, TEST_HEADER_L1_TABLE_OFFSET); let l1_entry = read_be_u64_at(file, l1_table_offset); let l2_table_addr = l1_entry & TEST_L1_L2_ADDR_MASK; assert_ne!(l2_table_addr, 0); l2_table_addr } fn set_low_bit_on_first_compressed_l2_entry(file: &mut File) { let l2_entry_offset = first_l2_entry_offset(file); let l2_entry = read_be_u64_at(file, l2_entry_offset); assert_ne!(l2_entry & TEST_COMPRESSED_FLAG, 0); write_be_u64_at(file, l2_entry_offset, l2_entry | TEST_ZERO_FLAG); file.sync_all().unwrap(); } fn set_first_l2_entry(file: &mut File, l2_entry: u64) { let l2_entry_offset = first_l2_entry_offset(file); write_be_u64_at(file, l2_entry_offset, l2_entry); file.sync_all().unwrap(); } fn first_l2_entry(file: &mut File) -> u64 { let l2_entry_offset = first_l2_entry_offset(file); read_be_u64_at(file, l2_entry_offset) } fn qcow_header_is_corrupt(file: &File) -> bool { let raw = AlignedFile::new(file.try_clone().unwrap(), false); QcowHeader::new(&raw).unwrap().is_corrupt() } fn create_disk_with_data( file_size: u64, data: &[u8], offset: u64, sparse: bool, direct_io: bool, ) -> (TempFile, QcowDisk) { let temp_file = if data.is_empty() { QcowTempDisk::new(file_size, None, false, sparse, false) .unwrap() .into_tempfile() } else { let tmp_disk = QcowTempDisk::new(file_size, None, false, sparse, false).unwrap(); tmp_disk.disk().write_all_at(offset, data); tmp_disk.into_tempfile() }; let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), direct_io, false, sparse, false, ) .unwrap(); (temp_file, disk) } fn create_overlay_disk_with_raw_backing_pattern( file_size: u64, value: u8, direct_io: bool, ) -> (TempFile, TempFile, QcowDisk) { let backing_temp = TempFile::new().unwrap(); let backing_data = vec![value; file_size as usize]; backing_temp.as_file().write_all(&backing_data).unwrap(); backing_temp.as_file().sync_all().unwrap(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( overlay_temp.as_file().try_clone().unwrap(), direct_io, true, true, false, ) .unwrap(); (backing_temp, overlay_temp, disk) } fn completion_tuple(completion: &AsyncIoCompletion) -> (u64, i32) { (completion.user_data, completion.result) } fn next_completion(async_io: &mut dyn AsyncIo) -> (u64, i32) { completion_tuple(&async_io.next_completed_request().unwrap()) } fn async_read(disk: &QcowDisk, offset: u64, len: usize) -> Vec { let mut async_io = disk.create_async_io(1).unwrap(); async_io .read_to_vec( offset as libc::off_t, OwnedIoBuffer::from_vec(vec![0xFF; len]), 1, ) .unwrap(); let mut completion = async_io.next_completed_request().unwrap(); let (user_data, result) = completion_tuple(&completion); assert_eq!(user_data, 1); assert_eq!(result as usize, len, "read should return requested length"); match completion.buffer.take() { Some(buffer) => buffer.as_slice().to_vec(), other => panic!("unexpected read completion: {other:?}"), } } fn async_write(disk: &QcowDisk, offset: u64, data: &[u8]) { let mut async_io = disk.create_async_io(1).unwrap(); async_io .write_from_vec( offset as libc::off_t, OwnedIoBuffer::from_vec(data.to_vec()), 1, ) .unwrap(); let (user_data, result) = next_completion(async_io.as_mut()); assert_eq!(user_data, 1); assert_eq!(result as usize, data.len()); } #[test] fn test_qcow_sync_rejects_out_of_bounds_allocated_l2_entry_on_read() { let data = vec![0x5a; 4096]; let (temp_file, disk) = create_disk_with_data(100 * 1024 * 1024, &data, 0, true, false); let mut file = temp_file.as_file().try_clone().unwrap(); set_first_l2_entry( &mut file, TEST_CLUSTER_USED_FLAG | TEST_OUT_OF_BOUNDS_CLUSTER, ); let mut async_io = disk.create_async_io(1).unwrap(); let err = async_io .read_to_vec(0, OwnedIoBuffer::from_vec(vec![0u8; 512]), 1) .expect_err("out-of-bounds allocated L2 entry must fail"); match err { AsyncIoError::ReadVectored(e) => assert_eq!(e.raw_os_error(), Some(libc::EIO)), other => panic!("unexpected error: {other:?}"), } assert!( qcow_header_is_corrupt(&file), "out-of-bounds allocated L2 entry should set the corrupt bit" ); } #[test] fn test_qcow_sync_rejects_out_of_bounds_allocated_l2_entry_on_write() { let data = vec![0x5a; 4096]; let (temp_file, disk) = create_disk_with_data(100 * 1024 * 1024, &data, 0, true, false); let mut file = temp_file.as_file().try_clone().unwrap(); set_first_l2_entry( &mut file, TEST_CLUSTER_USED_FLAG | TEST_OUT_OF_BOUNDS_CLUSTER, ); let mut async_io = disk.create_async_io(1).unwrap(); let overwrite = vec![0x11u8; 512]; let err = async_io .write_from_vec(0, OwnedIoBuffer::from_vec(overwrite), 1) .expect_err("out-of-bounds allocated L2 entry must fail"); match err { AsyncIoError::WriteVectored(e) => assert_eq!(e.raw_os_error(), Some(libc::EIO)), other => panic!("unexpected error: {other:?}"), } assert!( qcow_header_is_corrupt(&file), "out-of-bounds allocated L2 entry should set the corrupt bit" ); } #[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) = next_completion(async_io.as_mut()); 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) = next_completion(async_io.as_mut()); 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_write_zeroes_compressed_entry_checks_compressed_before_zero_bit() { let cluster_size = 1u64 << 16; let data = vec![0xEE; cluster_size as usize]; let (temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, 0, true, false); drop(disk); compress_allocated_clusters(&mut temp.as_file().try_clone().unwrap()); set_low_bit_on_first_compressed_l2_entry(&mut temp.as_file().try_clone().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.write_zeroes(0, cluster_size, 200).unwrap(); let (user_data, result) = next_completion(async_io.as_mut()); assert_eq!(user_data, 200); assert_eq!(result, 0); async_io.fsync(Some(201)).unwrap(); let (user_data, result) = next_completion(async_io.as_mut()); assert_eq!(user_data, 201); assert_eq!(result, 0); drop(async_io); drop(disk); let l2_entry = first_l2_entry(&mut temp.as_file().try_clone().unwrap()); assert_eq!(l2_entry, 0); } #[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); // Populate four 64 KiB regions at 128 KiB strides so the subsequent // punch_hole calls have allocated clusters to operate on. { let disk = QcowDisk::new( _temp.as_file().try_clone().unwrap(), false, false, true, false, ) .unwrap(); for i in 0..4u64 { disk.write_all_at(i * 128 * 1024, &data); } } 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) = next_completion(async_io.as_mut()); assert_eq!(ud, 1); assert_eq!(res, 0); let (ud, res) = next_completion(async_io.as_mut()); assert_eq!(ud, 2); assert_eq!(res, 0); let (ud, res) = next_completion(async_io.as_mut()); 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) = next_completion(async_io1.as_mut()); 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) = next_completion(async_io1.as_mut()); 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) = next_completion(async_io.as_mut()); 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 backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let 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 create_raw_backing(path: &Path, pattern: &[u8]) { let mut backing_file = File::create(path).unwrap(); backing_file.write_all(pattern).unwrap(); backing_file.sync_all().unwrap(); } fn create_qcow2_overlay(overlay_path: &Path, backing_path: &str, file_size: u64) { let file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(true) .open(overlay_path) .unwrap(); let backing_config = BackingFileConfig { path: backing_path.to_string(), format: Some(ImageType::Raw), }; qcow::create_image(&file, file_size, Some(&backing_config)).unwrap(); } fn create_qcow2_overlay_header(overlay_path: &Path, backing_path: &str, file_size: u64) { let file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(true) .open(overlay_path) .unwrap(); let header = QcowHeader::create_for_size_and_path(3, file_size, Some(backing_path)).unwrap(); let raw = AlignedFile::new(file, false); header.write_to(&raw).unwrap(); raw.sync_all().unwrap(); } #[test] fn test_relative_backing_file_read() { let test_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); let cwd = env::current_dir().unwrap(); assert_ne!(cwd.as_path(), test_dir.as_path()); let backing_path = test_dir.as_path().join("backing.raw"); let overlay_path = test_dir.as_path().join("overlay.qcow2"); let cluster_size = 1u64 << 16; let file_size = cluster_size * 2; let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); create_raw_backing(&backing_path, &pattern); create_qcow2_overlay(&overlay_path, "backing.raw", file_size); let disk = QcowDisk::new(File::open(&overlay_path).unwrap(), false, true, true, false).unwrap(); let buf = async_read(&disk, 0, cluster_size as usize); assert_eq!( &buf[..], &pattern[..cluster_size as usize], "Relative backing file should resolve from the overlay image directory" ); } #[test] fn test_missing_relative_backing_file_error_uses_resolved_path() { let test_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); let overlay_path = test_dir.as_path().join("overlay.qcow2"); let cluster_size = 1u64 << 16; let file_size = cluster_size * 2; create_qcow2_overlay_header(&overlay_path, "missing.raw", file_size); let err = QcowDisk::new(File::open(&overlay_path).unwrap(), false, true, true, false) .unwrap_err(); assert!(matches!(err.kind(), BlockErrorKind::Io)); let expected_path = test_dir .as_path() .join("missing.raw") .to_string_lossy() .into_owned(); match err.downcast_ref::() { Some(QcowError::BackingFileIo(path, _)) => { assert_eq!(path.as_str(), expected_path.as_str()); } other => panic!("unexpected error: {other:?}"), } } #[test] fn test_relative_backing_file_with_parent_components() { let test_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); let overlay_dir = test_dir.as_path().join("overlay"); let sibling_dir = test_dir.as_path().join("sibling"); create_dir(&overlay_dir).unwrap(); create_dir(&sibling_dir).unwrap(); let backing_path = sibling_dir.join("backing.raw"); let overlay_path = overlay_dir.join("overlay.qcow2"); let cluster_size = 1u64 << 16; let file_size = cluster_size * 2; let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); create_raw_backing(&backing_path, &pattern); create_qcow2_overlay(&overlay_path, "../sibling/backing.raw", file_size); let disk = QcowDisk::new(File::open(&overlay_path).unwrap(), false, true, true, false).unwrap(); let buf = async_read(&disk, 0, cluster_size as usize); assert_eq!( &buf[..], &pattern[..cluster_size as usize], "Relative backing file with parent components should resolve from the overlay image directory" ); } #[test] fn test_absolute_backing_file_path_read() { let test_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); let backing_path = test_dir.as_path().join("backing.raw"); let overlay_path = test_dir.as_path().join("overlay.qcow2"); let cluster_size = 1u64 << 16; let file_size = cluster_size * 2; let pattern: Vec = (0..file_size as usize).map(|i| (i % 251) as u8).collect(); create_raw_backing(&backing_path, &pattern); create_qcow2_overlay(&overlay_path, backing_path.to_str().unwrap(), file_size); let disk = QcowDisk::new(File::open(&overlay_path).unwrap(), false, true, true, false).unwrap(); let buf = async_read(&disk, 0, cluster_size as usize); assert_eq!( &buf[..], &pattern[..cluster_size as usize], "Absolute backing file path should be used as is" ); } #[test] fn test_relative_backing_file_falls_back_for_fd_without_filesystem_path() { let overlay_temp = TempFile::new().unwrap(); let cluster_size = 1u64 << 16; let file_size = cluster_size * 2; { let file = overlay_temp.as_file().try_clone().unwrap(); let header = QcowHeader::create_for_size_and_path(3, file_size, Some("missing.raw")).unwrap(); let raw = AlignedFile::new(file, false); header.write_to(&raw).unwrap(); raw.sync_all().unwrap(); } let overlay_file = overlay_temp.into_file(); let err = QcowDisk::new(overlay_file, false, true, true, false).unwrap_err(); assert!(matches!(err.kind(), BlockErrorKind::Io)); match err.downcast_ref::() { Some(QcowError::BackingFileIo(path, _)) => assert_eq!(path, "missing.raw"), other => panic!("unexpected error: {other:?}"), } } fn test_write_zeroes_unallocated_overlay_with_backing_must_read_zero_impl(direct_io: bool) { let cluster_size = 1u64 << 16; let file_size = cluster_size * 4; let offset = cluster_size; let (_backing_temp, _overlay_temp, disk) = create_overlay_disk_with_raw_backing_pattern(file_size, 0xAB, direct_io); let mut async_io = disk.create_async_io(1).unwrap(); async_io.write_zeroes(offset, cluster_size, 42).unwrap(); let (user_data, result) = next_completion(async_io.as_mut()); assert_eq!(user_data, 42); assert_eq!(result, 0); drop(async_io); let buf = async_read(&disk, offset, cluster_size as usize); assert!( buf.iter().all(|&b| b == 0), "zeroed unallocated overlay cluster exposed backing data" ); } #[test] fn test_write_zeroes_unallocated_overlay_with_backing_must_read_zero() { test_write_zeroes_unallocated_overlay_with_backing_must_read_zero_impl(false); } #[test] fn test_write_zeroes_unallocated_overlay_with_backing_must_read_zero_direct_io() { test_write_zeroes_unallocated_overlay_with_backing_must_read_zero_impl(true); } fn test_partial_write_after_write_zeroes_must_not_reintroduce_backing_data_impl( direct_io: bool, ) { let cluster_size = 1u64 << 16; let file_size = cluster_size * 4; let offset = cluster_size; let patch_offset = 0x4000usize; let patch_len = 0x1000usize; let (_backing_temp, _overlay_temp, disk) = create_overlay_disk_with_raw_backing_pattern(file_size, 0xAB, direct_io); let mut async_io = disk.create_async_io(1).unwrap(); async_io.write_zeroes(offset, cluster_size, 42).unwrap(); let (_user_data, result) = next_completion(async_io.as_mut()); assert_eq!(result, 0); drop(async_io); let patch = [0x99u8; 0x1000]; async_write(&disk, offset + patch_offset as u64, &patch[..patch_len]); let buf = async_read(&disk, offset, cluster_size as usize); assert!(buf[..patch_offset].iter().all(|&b| b == 0)); assert!( buf[patch_offset..patch_offset + patch_len] .iter() .all(|&b| b == 0x99) ); assert!(buf[patch_offset + patch_len..].iter().all(|&b| b == 0)); } #[test] fn test_partial_write_after_write_zeroes_must_not_reintroduce_backing_data() { test_partial_write_after_write_zeroes_must_not_reintroduce_backing_data_impl(false); } #[test] fn test_partial_write_after_write_zeroes_must_not_reintroduce_backing_data_direct_io() { test_partial_write_after_write_zeroes_must_not_reintroduce_backing_data_impl(true); } fn test_backing_file_read_qcow2_backing_impl(direct_io: bool) { 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 = QcowTempDisk::new(file_size, None, false, true, false).unwrap(); backing.disk().write_all_at(0, &pattern); let backing_temp = backing.into_tempfile(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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 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 backing = QcowTempDisk::new(file_size, None, false, true, false).unwrap(); backing.disk().write_all_at(0, &pattern); let backing_temp = backing.into_tempfile(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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_pattern = vec![0xBBu8; cluster_size as usize]; let mid = QcowTempDisk::new( file_size, Some(&BackingFileConfig { path: base_path, format: Some(ImageType::Raw), }), false, true, false, ) .unwrap(); mid.disk().write_all_at(0, &mid_pattern); let mid_temp = mid.into_tempfile(); 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_pattern = vec![0xCCu8; cluster_size as usize]; let overlay = QcowTempDisk::new( file_size, Some(&BackingFileConfig { path: mid_path, format: Some(ImageType::Qcow2), }), false, true, false, ) .unwrap(); overlay.disk().write_all_at(cluster_size, &overlay_pattern); let overlay_temp = overlay.into_tempfile(); 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 = QcowTempDisk::new(file_size, None, false, true, false).unwrap(); backing.disk().write_all_at(0, &pattern); let backing_temp = backing.into_tempfile(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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 = QcowTempDisk::new(backing_size, None, false, true, false).unwrap(); backing .disk() .write_all_at(0, &vec![0xAA; backing_size as usize]); let backing_temp = backing.into_tempfile(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay_temp = QcowTempDisk::new(overlay_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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_data = vec![0xBBu8; backing_size as usize]; let backing = QcowTempDisk::new(backing_size, None, false, true, false).unwrap(); backing.disk().write_all_at(0, &backing_data); let backing_temp = backing.into_tempfile(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay_temp = QcowTempDisk::new(overlay_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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 backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(overlay_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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 = QcowTempDisk::new(file_size, None, false, true, false).unwrap(); backing.disk().write_all_at(0, &pattern); let backing_temp = backing.into_tempfile(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); 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 backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let 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) = next_completion(async_io.as_mut()); 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 backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let 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) = next_completion(aio.as_mut()); 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 backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let 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 total = a.len() + b.len() + c.len(); let mut write_buf = Vec::with_capacity(total); write_buf.extend_from_slice(&a); write_buf.extend_from_slice(&b); write_buf.extend_from_slice(&c); let mut aio = disk.create_async_io(1).unwrap(); aio.write_from_vec(0, OwnedIoBuffer::from_vec(write_buf), 1) .unwrap(); let (ud, res) = next_completion(aio.as_mut()); assert_eq!(ud, 1); assert_eq!(res as usize, total); aio.fsync(Some(2)).unwrap(); drop(aio); let mut aio = disk.create_async_io(1).unwrap(); aio.read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; total]), 10) .unwrap(); let mut completion = aio.next_completed_request().unwrap(); let (ud, res) = completion_tuple(&completion); assert_eq!(ud, 10); assert_eq!(res as usize, total); drop(aio); let got = match completion.buffer.take() { Some(buffer) => buffer.as_slice().to_vec(), other => panic!("unexpected read completion: {other:?}"), }; // 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); } #[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); } } cloud-hypervisor-53.0/block/src/formats/qcow/engine_uring.rs000066400000000000000000001132671522476750100243260ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! QCOW2 async disk backend. use std::cmp::min; use std::io; use std::os::unix::fs::FileExt; 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 super::common::decompress_cluster; use super::decoder::Decoder; use super::metadata::{ BackingRead, ClusterReadMapping, ClusterWriteMapping, DeallocAction, QcowMetadata, }; use super::qcow_raw_file::QcowRawFile; use crate::async_io::{ AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, UringDataIo, }; /// Per queue QCOW2 I/O worker using io_uring. /// /// Reads against fully allocated single mapping clusters are submitted /// to io_uring for true asynchronous completion. All other cluster /// types (zero, compressed, backing) and multi mapping reads fall back /// to synchronous I/O with synthetic completions. /// /// Writes are synchronous because metadata allocation must complete /// before the host offset is known. pub(super) struct QcowAsync { metadata: Arc, // Drop before data_file so pending SQEs can be submitted while fd is valid. data_io: UringDataIo, data_file: QcowRawFile, backing_file: Option>, sparse: bool, cluster_size: u64, decoder: Arc, } impl QcowAsync { pub(crate) fn new( metadata: Arc, data_file: QcowRawFile, backing_file: Option>, sparse: bool, ring_depth: u32, ) -> io::Result { Ok(QcowAsync { cluster_size: metadata.cluster_size(), decoder: metadata.decoder(), metadata, data_io: UringDataIo::new(ring_depth)?, data_file, backing_file, sparse, }) } 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); } } } fn async_error_result(error: &AsyncIoError) -> i32 { let io_error = match error { AsyncIoError::ReadVectored(e) | AsyncIoError::WriteVectored(e) | AsyncIoError::SubmitBatchRequests(e) | AsyncIoError::Fsync(e) | AsyncIoError::PunchHole(e) | AsyncIoError::WriteZeroes(e) => e, }; -io_error.raw_os_error().unwrap_or(libc::EIO) } fn inject_operation_completion(&mut self, op: AsyncIoOperation, result: i32) { self.data_io .inject_completion(AsyncIoCompletion::from_operation(op, result)); } fn prepare_read_operation( &mut self, mut op: AsyncIoOperation, ) -> Result, Box<(AsyncIoOperation, AsyncIoError)>> { let total_len = op.total_len(); let host_offset = match Self::resolve_read( &self.metadata, &self.data_file, &self.backing_file, op.offset() as u64, &mut op, total_len, self.cluster_size, &*self.decoder, ) { Ok(host_offset) => host_offset, Err(e) => return Err(Box::new((op, e))), }; if let Some(host_offset) = host_offset { op.set_offset(host_offset as libc::off_t); Ok(Some(op)) } else { self.inject_operation_completion(op, total_len as i32); Ok(None) } } fn complete_write_operation_sync( &mut self, op: AsyncIoOperation, ) -> Result<(), Box<(AsyncIoOperation, AsyncIoError)>> { // TODO Make writes async. // Writes are synchronous. Async writes require a multi step // state machine for COW (backing read, cluster allocation, data // write, L2 commit) with per request buffer lifetime tracking // and write ordering. let total_len = op.total_len(); if let Err(e) = Self::cow_write_sync( op.offset() as u64, &op, &self.metadata, &self.data_file, &self.backing_file, self.cluster_size, ) { return Err(Box::new((op, e))); } self.inject_operation_completion(op, total_len as i32); Ok(()) } } impl AsyncIo for QcowAsync { fn notifier(&self) -> &EventFd { self.data_io.notifier() } fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> { if op.is_read() { match self.prepare_read_operation(op) { Ok(Some(op)) => { self.data_io .submit_operation(self.data_file.as_raw_fd(), op) .map_err(AsyncIoError::ReadVectored)?; } Ok(None) => {} Err(e) => return Err(e.1), } Ok(()) } else { self.complete_write_operation_sync(op).map_err(|e| e.1) } } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { self.metadata.flush().map_err(AsyncIoError::Fsync)?; if let Some(user_data) = user_data { self.data_io .inject_completion(AsyncIoCompletion::new(user_data, 0, None)); } Ok(()) } fn next_completed_request(&mut self) -> Option { self.data_io.next_completion() } fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { let result = self .metadata .deallocate_bytes( offset, length as usize, self.sparse, false, self.backing_file.as_deref(), ) .map_err(AsyncIoError::PunchHole); match result { Ok(actions) => { for action in &actions { self.apply_dealloc_action(action); } self.data_io .inject_completion(AsyncIoCompletion::new(user_data, 0, None)); 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.data_io .inject_completion(AsyncIoCompletion::new(user_data, errno, None)); Ok(()) } } } fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { let result = self .metadata .deallocate_bytes( offset, length as usize, self.sparse, true, self.backing_file.as_deref(), ) .map_err(AsyncIoError::WriteZeroes); match result { Ok(actions) => { for action in &actions { self.apply_dealloc_action(action); } self.data_io .inject_completion(AsyncIoCompletion::new(user_data, 0, None)); Ok(()) } Err(e) => { let errno = if let AsyncIoError::WriteZeroes(ref io_err) = e { -io_err.raw_os_error().unwrap_or(libc::EIO) } else { -libc::EIO }; self.data_io .inject_completion(AsyncIoCompletion::new(user_data, errno, None)); Ok(()) } } } fn batch_requests_enabled(&self) -> bool { true } fn submit_batch_requests(&mut self, batch_request: Vec) -> AsyncIoResult<()> { let mut async_reads = Vec::new(); for op in batch_request { if op.is_read() { match self.prepare_read_operation(op) { Ok(Some(op)) => async_reads.push(op), Ok(None) => {} Err(boxed) => { let (op, e) = *boxed; // The operation was not submitted to the kernel. Accept // it at the qcow layer and surface the failure through // the common completion path so batch acceptance remains // all-or-none for the virtqueue. let result = Self::async_error_result(&e); self.inject_operation_completion(op, result); } } } else if let Err(boxed) = self.complete_write_operation_sync(op) { let (op, e) = *boxed; let result = Self::async_error_result(&e); self.inject_operation_completion(op, result); } } if !async_reads.is_empty() { self.data_io .submit_batch(self.data_file.as_raw_fd(), async_reads) .map_err(AsyncIoError::SubmitBatchRequests)?; } Ok(()) } } impl QcowAsync { /// Resolves read mappings for a guest read request. /// /// Returns `Some(host_offset)` if the entire read falls within a single /// allocated cluster (fast path). Otherwise handles the read /// synchronously via `scatter_read_sync` and returns `None`. #[expect(clippy::too_many_arguments)] fn resolve_read( metadata: &QcowMetadata, data_file: &QcowRawFile, backing_file: &Option>, address: u64, op: &mut AsyncIoOperation, total_len: 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 !data_file.file().is_direct() && mappings.len() == 1 && let ClusterReadMapping::Allocated { offset: host_offset, length, } = &mappings[0] && *length as usize == total_len { return Ok(Some(*host_offset)); } Self::scatter_read_sync(mappings, op, data_file, backing_file, cluster_size, decoder)?; Ok(None) } /// Scatter-read cluster mappings synchronously into an owned operation. fn scatter_read_sync( mappings: Vec, op: &mut AsyncIoOperation, data_file: &QcowRawFile, backing_file: &Option>, cluster_size: u64, decoder: &dyn Decoder, ) -> AsyncIoResult<()> { let mut buf_offset = 0usize; for mapping in mappings { match mapping { ClusterReadMapping::Zero { length } => { op.fill_zeroes_at(buf_offset, length as usize) .map_err(AsyncIoError::ReadVectored)?; buf_offset += length as usize; } ClusterReadMapping::Allocated { offset: host_offset, length, } => { let len = length as usize; let mut buf = vec![0u8; len]; data_file .file() .read_exact_at(&mut buf, host_offset) .map_err(AsyncIoError::ReadVectored)?; op.write_bytes_at(buf_offset, &buf) .map_err(AsyncIoError::ReadVectored)?; buf_offset += len; } ClusterReadMapping::Compressed { host_offset, compressed_size, cluster_offset, length, } => { let mut compressed = vec![0u8; compressed_size]; data_file .file() .read_exact_at(&mut compressed, host_offset) .map_err(AsyncIoError::ReadVectored)?; let decompressed = decompress_cluster(&compressed, cluster_size as usize, decoder) .map_err(AsyncIoError::ReadVectored)?; op.write_bytes_at( buf_offset, &decompressed[cluster_offset..cluster_offset + length], ) .map_err(AsyncIoError::ReadVectored)?; buf_offset += length; } ClusterReadMapping::Backing { offset: backing_offset, length, } => { let mut buf = vec![0u8; length as usize]; backing_file .as_ref() .unwrap() .read_at(backing_offset, &mut buf) .map_err(AsyncIoError::ReadVectored)?; op.write_bytes_at(buf_offset, &buf) .map_err(AsyncIoError::ReadVectored)?; buf_offset += length as usize; } } } Ok(()) } /// Write owned operation data cluster-by-cluster with COW from backing file. fn cow_write_sync( address: u64, op: &AsyncIoOperation, metadata: &QcowMetadata, data_file: &QcowRawFile, backing_file: &Option>, cluster_size: u64, ) -> AsyncIoResult<()> { let total_len = op.total_len(); let mut buf_offset = 0usize; while buf_offset < total_len { let curr_addr = address + buf_offset as u64; let intra_offset = curr_addr & (cluster_size - 1); let remaining_in_cluster = (cluster_size - intra_offset) as usize; let count = min(total_len - buf_offset, remaining_in_cluster); let backing_data = if let Some(backing) = backing_file .as_ref() .filter(|_| intra_offset != 0 || count < cluster_size as usize) { let cluster_begin = curr_addr - intra_offset; let mut data = vec![0u8; cluster_size as usize]; backing .read_at(cluster_begin, &mut data) .map_err(AsyncIoError::WriteVectored)?; Some(data) } else { None }; let mapping = metadata .map_cluster_for_write(curr_addr, backing_data) .map_err(AsyncIoError::WriteVectored)?; match mapping { ClusterWriteMapping::Allocated { offset: host_offset, } => { let mut buf = vec![0u8; count]; op.read_bytes_at(buf_offset, &mut buf) .map_err(AsyncIoError::WriteVectored)?; data_file .file() .write_all_at(&buf, host_offset) .map_err(AsyncIoError::WriteVectored)?; } } buf_offset += count; } Ok(()) } } #[cfg(test)] mod unit_tests { use std::io::Write; use std::sync::Arc; use std::{mem, thread}; use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap}; use vmm_sys_util::tempfile::TempFile; use super::*; use crate::SECTOR_SIZE; use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, GuestMemoryTarget, OwnedIoBuffer}; use crate::disk_file::AsyncDiskFile; use crate::formats::qcow::common::unit_tests::compress_allocated_clusters; use crate::formats::qcow::{BackingFileConfig, ImageType, QcowDisk, QcowTempDisk}; fn create_disk_with_data( file_size: u64, data: &[u8], offset: u64, sparse: bool, ) -> (TempFile, QcowDisk) { let temp_file = if data.is_empty() { QcowTempDisk::new(file_size, None, false, sparse, true) .unwrap() .into_tempfile() } else { let tmp_disk = QcowTempDisk::new(file_size, None, false, sparse, true).unwrap(); tmp_disk.disk().write_all_at(offset, data); tmp_disk.into_tempfile() }; let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, sparse, true, ) .unwrap(); (temp_file, disk) } fn create_overlay_disk_with_raw_backing_pattern( file_size: u64, value: u8, ) -> (TempFile, TempFile, QcowDisk) { let backing_temp = TempFile::new().unwrap(); let backing_data = vec![value; file_size as usize]; backing_temp.as_file().write_all(&backing_data).unwrap(); backing_temp.as_file().sync_all().unwrap(); let backing_path = backing_temp.as_path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Raw), }; let overlay_temp = QcowTempDisk::new(file_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( overlay_temp.as_file().try_clone().unwrap(), false, true, true, true, ) .unwrap(); (backing_temp, overlay_temp, disk) } fn wait_for_completion(async_io: &mut dyn AsyncIo) -> AsyncIoCompletion { loop { if let Some(c) = async_io.next_completed_request() { return c; } // Block until the eventfd is signaled (io_uring or synthetic). let fd = async_io.notifier().as_raw_fd(); let mut val = 0u64; // SAFETY: reading 8 bytes from a valid eventfd. unsafe { libc::read(fd, (&raw mut val).cast(), 8); } } } fn completion_tuple(completion: &AsyncIoCompletion) -> (u64, i32) { (completion.user_data, completion.result) } fn async_write(disk: &QcowDisk, offset: u64, data: &[u8]) { let mut async_io = disk.create_async_io(1).unwrap(); async_io .write_from_vec( offset as libc::off_t, OwnedIoBuffer::from_vec(data.to_vec()), 2, ) .unwrap(); let completion = wait_for_completion(async_io.as_mut()); let (user_data, result) = completion_tuple(&completion); assert_eq!(user_data, 2); assert_eq!( result as usize, data.len(), "write should return requested length" ); } fn async_read(disk: &QcowDisk, offset: u64, len: usize) -> Vec { let mut async_io = disk.create_async_io(1).unwrap(); async_io .read_to_vec( offset as libc::off_t, OwnedIoBuffer::from_vec(vec![0xFF; len]), 1, ) .unwrap(); let mut completion = wait_for_completion(async_io.as_mut()); let (user_data, result) = completion_tuple(&completion); assert_eq!(user_data, 1); assert_eq!(result as usize, len, "read should return requested length"); match completion.buffer.take() { Some(buffer) => buffer.as_slice().to_vec(), other => panic!("unexpected read completion: {other:?}"), } } #[test] fn test_qcow_async_punch_hole_completion() { let data = vec![0xDD; 128 * 1024]; let offset = 0u64; let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true); let mut async_io = disk.create_async_io(1).unwrap(); async_io.punch_hole(offset, data.len() as u64, 100).unwrap(); let completion = async_io.next_completed_request().unwrap(); let (user_data, result) = completion_tuple(&completion); assert_eq!(user_data, 100); assert_eq!(result, 0, "punch_hole should succeed"); drop(async_io); let read_buf = async_read(&disk, offset, data.len()); assert!( read_buf.iter().all(|&b| b == 0), "Punched hole should read as zeros" ); } #[test] fn test_qcow_async_write_zeroes_completion() { let data = vec![0xAA; 128 * 1024]; let offset = 0u64; let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true); let mut async_io = disk.create_async_io(1).unwrap(); async_io .write_zeroes(offset, data.len() as u64, 200) .unwrap(); let completion = async_io.next_completed_request().unwrap(); let (user_data, result) = completion_tuple(&completion); assert_eq!(user_data, 200); assert_eq!(result, 0, "write_zeroes should succeed"); drop(async_io); let read_buf = async_read(&disk, offset, data.len()); assert!( read_buf.iter().all(|&b| b == 0), "Write zeroes region should read as zeros" ); } #[test] fn test_qcow_async_write_zeroes_unallocated_overlay_with_backing_must_read_zero() { let cluster_size = 1u64 << 16; let file_size = cluster_size * 4; let offset = cluster_size; let (_backing_temp, _overlay_temp, disk) = create_overlay_disk_with_raw_backing_pattern(file_size, 0xAB); let mut async_io = disk.create_async_io(1).unwrap(); async_io.write_zeroes(offset, cluster_size, 201).unwrap(); let completion = wait_for_completion(async_io.as_mut()); let (user_data, result) = completion_tuple(&completion); assert_eq!(user_data, 201); assert_eq!(result, 0, "write_zeroes should succeed"); drop(async_io); let read_buf = async_read(&disk, offset, cluster_size as usize); assert!( read_buf.iter().all(|&b| b == 0), "zeroed unallocated overlay cluster exposed backing data" ); } #[test] fn test_qcow_async_write_read_roundtrip() { let file_size = 100 * 1024 * 1024; let temp_file = QcowTempDisk::new(file_size, None, false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, true, true, ) .unwrap(); let pattern: Vec = (0..128 * 1024).map(|i| (i % 251) as u8).collect(); let offset = 64 * 1024; async_write(&disk, offset, &pattern); let read_buf = async_read(&disk, offset, pattern.len()); assert_eq!(read_buf, pattern, "read should match written data"); } #[test] fn test_qcow_async_read_spanning_cluster_boundary() { let cluster_size: u64 = 65536; let file_size = 100 * 1024 * 1024; // Write distinct patterns into two adjacent clusters. let pattern_a = vec![0xAA; cluster_size as usize]; let pattern_b = vec![0xBB; cluster_size as usize]; let (_temp, disk) = create_disk_with_data(file_size, &pattern_a, 0, true); async_write(&disk, cluster_size, &pattern_b); // Read across the boundary: last 4K of cluster 0 + first 4K of cluster 1. let read_offset = cluster_size - 4096; let read_len = 8192; let buf = async_read(&disk, read_offset, read_len); assert!( buf[..4096].iter().all(|&b| b == 0xAA), "first half should come from cluster 0" ); assert!( buf[4096..].iter().all(|&b| b == 0xBB), "second half should come from cluster 1" ); } #[test] fn test_qcow_async_sync_read_to_guest_memory() { let cluster_size = 65536usize; let file_size = 100 * 1024 * 1024; let data: Vec = (0..cluster_size * 2).map(|i| (i % 251) as u8).collect(); let (_temp, disk) = create_disk_with_data(file_size, &data, 0, true); let mem = Arc::new( GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0x1000), 0x4000)]).unwrap(), ); let ranges = [(GuestAddress(0x1000), 2048), (GuestAddress(0x2000), 2048)]; let target = GuestMemoryTarget::new(Arc::clone(&mem), &ranges).unwrap(); let read_offset = cluster_size as u64 - 2048; let mut async_io = disk.create_async_io(1).unwrap(); async_io .read_to_memory(read_offset as libc::off_t, target, 55) .unwrap(); let completion = wait_for_completion(async_io.as_mut()); assert_eq!(completion_tuple(&completion), (55, 4096)); assert!(completion.buffer.is_none()); let mut first = vec![0u8; 2048]; let mut second = vec![0u8; 2048]; mem.read_slice(&mut first, GuestAddress(0x1000)).unwrap(); mem.read_slice(&mut second, GuestAddress(0x2000)).unwrap(); let expected = &data[read_offset as usize..read_offset as usize + 4096]; assert_eq!(&first[..], &expected[..2048]); assert_eq!(&second[..], &expected[2048..]); } #[test] fn test_qcow_async_sync_write_from_guest_memory() { let file_size = 100 * 1024 * 1024; let temp_file = QcowTempDisk::new(file_size, None, false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, true, true, ) .unwrap(); let mem = Arc::new( GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0x1000), 0x4000)]).unwrap(), ); let first = vec![0x5a; 2048]; let second = vec![0xc3; 2048]; mem.write_slice(&first, GuestAddress(0x1000)).unwrap(); mem.write_slice(&second, GuestAddress(0x2000)).unwrap(); let ranges = [(GuestAddress(0x1000), 2048), (GuestAddress(0x2000), 2048)]; let target = GuestMemoryTarget::new(Arc::clone(&mem), &ranges).unwrap(); let mut async_io = disk.create_async_io(1).unwrap(); async_io.write_from_memory(4096, target, 56).unwrap(); let completion = wait_for_completion(async_io.as_mut()); assert_eq!(completion_tuple(&completion), (56, 4096)); drop(async_io); let mut expected = first; expected.extend_from_slice(&second); let read_buf = async_read(&disk, 4096, expected.len()); assert_eq!(read_buf, expected); } #[test] fn test_qcow_async_batch_mixed_requests() { let file_size = 100 * 1024 * 1024; let temp_file = QcowTempDisk::new(file_size, None, false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, true, true, ) .unwrap(); let mut async_io = disk.create_async_io(8).unwrap(); // Prepare write data for two regions. let write_a = vec![0xAA; 4096]; let write_b = vec![0xBB; 4096]; let offset_a: u64 = 0; let offset_b: u64 = 65536; let batch = vec![ AsyncIoOperation::write_from_vec( offset_a as libc::off_t, OwnedIoBuffer::from_vec(write_a.clone()), 10, ), AsyncIoOperation::write_from_vec( offset_b as libc::off_t, OwnedIoBuffer::from_vec(write_b.clone()), 20, ), ]; async_io.submit_batch_requests(batch).unwrap(); let mut completions = [ completion_tuple(&wait_for_completion(async_io.as_mut())), completion_tuple(&wait_for_completion(async_io.as_mut())), ]; completions.sort_by_key(|c| c.0); assert_eq!(completions[0], (10, 4096)); assert_eq!(completions[1], (20, 4096)); drop(async_io); // Batch read both regions back. let mut async_io = disk.create_async_io(8).unwrap(); let read_batch = vec![ AsyncIoOperation::read_to_vec( offset_a as libc::off_t, OwnedIoBuffer::from_vec(vec![0; 4096]), 30, ), AsyncIoOperation::read_to_vec( offset_b as libc::off_t, OwnedIoBuffer::from_vec(vec![0; 4096]), 40, ), ]; async_io.submit_batch_requests(read_batch).unwrap(); let mut completion_a = wait_for_completion(async_io.as_mut()); let mut completion_b = wait_for_completion(async_io.as_mut()); if completion_a.user_data > completion_b.user_data { mem::swap(&mut completion_a, &mut completion_b); } assert_eq!(completion_tuple(&completion_a), (30, 4096)); assert_eq!(completion_tuple(&completion_b), (40, 4096)); let read_a = match completion_a.buffer.take() { Some(buffer) => buffer.as_slice().to_vec(), other => panic!("unexpected read completion A: {other:?}"), }; let read_b = match completion_b.buffer.take() { Some(buffer) => buffer.as_slice().to_vec(), other => panic!("unexpected read completion B: {other:?}"), }; assert_eq!(read_a, write_a, "batch read A should match written data"); assert_eq!(read_b, write_b, "batch read B should match written data"); } #[test] fn test_qcow_async_read_unallocated() { let file_size = 100 * 1024 * 1024; let temp_file = QcowTempDisk::new(file_size, None, false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, true, true, ) .unwrap(); let buf = async_read(&disk, 0, 128 * 1024); assert!( buf.iter().all(|&b| b == 0), "unallocated region should read as zeroes" ); } #[test] fn test_qcow_async_sub_cluster_write() { let cluster_size = 65536usize; let file_size = 100 * 1024 * 1024; let temp_file = QcowTempDisk::new(file_size, None, false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, true, true, ) .unwrap(); // Write 4K into the middle of a cluster. let write_offset = 4096u64; let write_len = 4096; let pattern = vec![0xCC; write_len]; async_write(&disk, write_offset, &pattern); // Read the entire cluster back. let buf = async_read(&disk, 0, cluster_size); assert!( buf[..write_offset as usize].iter().all(|&b| b == 0), "bytes before the write should be zero" ); assert_eq!( &buf[write_offset as usize..write_offset as usize + write_len], &pattern[..], "written region should match" ); assert!( buf[write_offset as usize + write_len..] .iter() .all(|&b| b == 0), "bytes after the write should be zero" ); } #[test] fn test_qcow_async_write_after_punch_hole() { let data = vec![0xAA; 64 * 1024]; let offset = 0u64; let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true); let buf = async_read(&disk, offset, data.len()); assert!(buf.iter().all(|&b| b == 0xAA)); let mut async_io = disk.create_async_io(1).unwrap(); async_io.punch_hole(offset, data.len() as u64, 10).unwrap(); let result = wait_for_completion(async_io.as_mut()).result; assert_eq!(result, 0); drop(async_io); let buf = async_read(&disk, offset, data.len()); assert!( buf.iter().all(|&b| b == 0), "should be zero after punch hole" ); let new_data = vec![0xBB; 64 * 1024]; async_write(&disk, offset, &new_data); let buf = async_read(&disk, offset, new_data.len()); assert_eq!(buf, new_data, "should read new data after rewrite"); } #[test] fn test_qcow_async_large_sequential_io() { let cluster_size = 64 * 1024; let num_clusters = 8; let total_len = cluster_size * num_clusters; let offset = 0u64; let mut data = vec![0u8; total_len]; for (i, chunk) in data.chunks_mut(cluster_size).enumerate() { chunk.fill((i + 1) as u8); } let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true); let buf = async_read(&disk, offset, total_len); assert_eq!(buf.len(), total_len); for (i, chunk) in buf.chunks(cluster_size).enumerate() { assert!( chunk.iter().all(|&b| b == (i + 1) as u8), "cluster {i} mismatch" ); } } #[test] fn test_qcow_async_alignment_without_direct_io() { let file_size = 100 * 1024 * 1024; let temp_file = QcowTempDisk::new(file_size, None, false, true, false) .unwrap() .into_tempfile(); let disk = QcowDisk::new( temp_file.as_file().try_clone().unwrap(), false, false, true, true, ) .unwrap(); let async_io = disk.create_async_io(1).unwrap(); assert_eq!(async_io.alignment(), SECTOR_SIZE); } #[test] fn test_qcow_async_alignment_with_direct_io() { let tmp_disk = match QcowTempDisk::new(100 * 1024 * 1024, None, true, true, true) { Ok(d) => d, Err(_) => { eprintln!("skipping: O_DIRECT not supported on this filesystem"); return; } }; let async_io = tmp_disk.disk().create_async_io(1).unwrap(); assert!(async_io.alignment() >= SECTOR_SIZE); } #[test] fn test_qcow_async_sub_sector_read_with_direct_io() { let tmp_disk = match QcowTempDisk::new(100 * 1024 * 1024, None, true, true, true) { Ok(d) => d, Err(_) => { eprintln!("skipping: O_DIRECT not supported on this filesystem"); return; } }; let pattern = vec![0xAB; 65536]; async_write(tmp_disk.disk(), 0, &pattern); let buf = async_read(tmp_disk.disk(), 0, 512); assert!( buf.iter().all(|&b| b == 0xAB), "sub-sector O_DIRECT read should return written data" ); } #[test] fn test_qcow_async_direct_io_write_read_roundtrip() { let tmp_disk = match QcowTempDisk::new(100 * 1024 * 1024, None, true, true, true) { Ok(d) => d, Err(_) => { eprintln!("skipping: O_DIRECT not supported on this filesystem"); return; } }; let pattern: Vec = (0..128 * 1024).map(|i| (i % 251) as u8).collect(); async_write(tmp_disk.disk(), 0, &pattern); let buf = async_read(tmp_disk.disk(), 0, pattern.len()); assert_eq!(buf, pattern, "O_DIRECT roundtrip should match"); } #[test] fn test_compressed_read_multi_queue() { let cluster_size = 65536usize; let data: Vec = (0..=255).cycle().take(cluster_size).collect(); let (temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, 0, false); drop(disk); compress_allocated_clusters(&mut temp.as_file().try_clone().unwrap()); let disk = Arc::new( QcowDisk::new( temp.as_file().try_clone().unwrap(), false, false, false, true, ) .unwrap(), ); let handles: Vec<_> = (0..4) .map(|_| { let disk = Arc::clone(&disk); let expected = data.clone(); thread::spawn(move || { let mut async_io = disk.create_async_io(1).unwrap(); async_io .read_to_vec(0, OwnedIoBuffer::from_vec(vec![0xFF; cluster_size]), 1) .unwrap(); let mut completion = wait_for_completion(async_io.as_mut()); let result = completion.result; assert_eq!(result as usize, cluster_size); let buf = match completion.buffer.take() { Some(buffer) => buffer.as_slice().to_vec(), other => panic!("unexpected read completion: {other:?}"), }; assert_eq!(buf, expected); }) }) .collect(); for h in handles { h.join().unwrap(); } } } cloud-hypervisor-53.0/block/src/formats/qcow/header.rs000066400000000000000000000643731522476750100231100ustar00rootroot00000000000000// Copyright 2018 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. // // Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! QCOW2 header parsing, validation, and creation. use std::fmt::{Display, Formatter, Result as FmtResult}; use std::os::unix::fs::FileExt; use std::str::FromStr; use bitflags::bitflags; use vmm_sys_util::file_traits::FileSync; use zerocopy::big_endian::{U32 as BeU32, U64 as BeU64}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use super::decoder::{Decoder, ZlibDecoder, ZstdDecoder}; use super::parser::{Error, Result}; use super::util::{div_round_up_u32, div_round_up_u64}; use crate::aligned_file::AlignedFile; use crate::error::{BlockError, BlockErrorKind, BlockResult}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ImageType { Raw, Qcow2, } impl Display for ImageType { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { ImageType::Raw => write!(f, "raw"), ImageType::Qcow2 => write!(f, "qcow2"), } } } impl FromStr for ImageType { type Err = Error; fn from_str(s: &str) -> Result { 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, } /// On-disk layout of the bare qcow2 header shared by v2 and v3 (72 bytes). #[repr(C)] #[derive(FromBytes, IntoBytes, KnownLayout, Immutable)] struct RawHeaderV2 { magic: BeU32, version: BeU32, backing_file_offset: BeU64, backing_file_size: BeU32, cluster_bits: BeU32, size: BeU64, crypt_method: BeU32, l1_size: BeU32, l1_table_offset: BeU64, refcount_table_offset: BeU64, refcount_table_clusters: BeU32, nb_snapshots: BeU32, snapshots_offset: BeU64, } impl RawHeaderV2 { fn from_header(header: &QcowHeader) -> Self { Self { magic: BeU32::new(header.magic), version: BeU32::new(header.version), backing_file_offset: BeU64::new(header.backing_file_offset), backing_file_size: BeU32::new(header.backing_file_size), cluster_bits: BeU32::new(header.cluster_bits), size: BeU64::new(header.size), crypt_method: BeU32::new(header.crypt_method), l1_size: BeU32::new(header.l1_size), l1_table_offset: BeU64::new(header.l1_table_offset), refcount_table_offset: BeU64::new(header.refcount_table_offset), refcount_table_clusters: BeU32::new(header.refcount_table_clusters), nb_snapshots: BeU32::new(header.nb_snapshots), snapshots_offset: BeU64::new(header.snapshots_offset), } } } /// On-disk layout of the fields v3 adds after the bare header (32 bytes). #[repr(C)] #[derive(FromBytes, IntoBytes, KnownLayout, Immutable)] struct RawHeaderV3Tail { incompatible_features: BeU64, compatible_features: BeU64, autoclear_features: BeU64, refcount_order: BeU32, header_size: BeU32, } impl RawHeaderV3Tail { fn from_header(header: &QcowHeader) -> Self { Self { incompatible_features: BeU64::new(header.incompatible_features), compatible_features: BeU64::new(header.compatible_features), autoclear_features: BeU64::new(header.autoclear_features), refcount_order: BeU32::new(header.refcount_order), header_size: BeU32::new(header.header_size), } } } #[repr(C)] #[derive(FromBytes, IntoBytes, KnownLayout, Immutable)] struct ExtensionHeader { extension_type: BeU32, length: BeU32, } impl ExtensionHeader { fn end() -> Self { Self { extension_type: BeU32::new(HEADER_EXT_END), length: BeU32::ZERO, } } } impl QcowHeader { /// Read header extensions, optionally collecting feature names for error reporting. pub(super) fn read_header_extensions( f: &AlignedFile, header: &mut QcowHeader, mut feature_table: Option<&mut Vec<(u8, String)>>, ) -> Result<()> { // Extensions start directly after the header. let mut offset = header.header_size as u64; loop { let mut field = [0u8; size_of::()]; f.read_exact_at(&mut field, offset) .map_err(Error::ReadingHeader)?; offset += field.len() as u64; let extension = ExtensionHeader::read_from_bytes(&field).expect("buffer covers extension header"); let ext_type = extension.extension_type.get(); if ext_type == HEADER_EXT_END { break; } let ext_length = extension.length.get(); match ext_type { HEADER_EXT_BACKING_FORMAT => { let mut format_bytes = vec![0u8; ext_length as usize]; f.read_exact_at(&mut format_bytes, offset) .map_err(Error::ReadingHeader)?; offset += format_bytes.len() as u64; let format_str = String::from_utf8(format_bytes) .map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?; if let Some(backing_file) = &mut header.backing_file { backing_file.format = Some(format_str.parse()?); } } HEADER_EXT_FEATURE_NAME_TABLE if feature_table.is_some() => { const FEATURE_NAME_ENTRY_SIZE: usize = 1 + 1 + 46; // type + bit + name let mut data = vec![0u8; ext_length as usize]; f.read_exact_at(&mut data, offset) .map_err(Error::ReadingHeader)?; offset += data.len() as u64; let table = feature_table.as_mut().unwrap(); for entry in data.as_chunks::().0 { if entry[0] == FEAT_TYPE_INCOMPATIBLE { let bit_number = entry[1]; let name_bytes = &entry[2..]; let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(46); let name = String::from_utf8_lossy(&name_bytes[..name_len]).to_string(); table.push((bit_number, name)); } } } _ => { // Skip unknown extension offset += ext_length as u64; } } // Skip to the next 8 byte boundary let padding = (8 - (ext_length % 8)) % 8; offset += padding as u64; } Ok(()) } /// Creates a QcowHeader from a reference to a file. pub fn new(f: &AlignedFile) -> Result { // The bare header fits in V3_BARE_HEADER_SIZE plus the optional // compression field. Read it once, then decode each region as a typed // view whose layout matches the on-disk header. let mut buf = [0u8; V3_BARE_HEADER_SIZE as usize + size_of::()]; f.read_exact_at(&mut buf, 0).map_err(Error::ReadingHeader)?; // `buf` is always larger than the views, and the views are unaligned, // so the casts cannot fail. let (v2, tail) = RawHeaderV2::ref_from_prefix(&buf).expect("buffer covers the v2 header"); let magic = v2.magic.get(); if magic != QCOW_MAGIC { return Err(Error::InvalidMagic); } let version = v2.version.get(); let mut header = QcowHeader { magic, version, backing_file_offset: v2.backing_file_offset.get(), backing_file_size: v2.backing_file_size.get(), cluster_bits: v2.cluster_bits.get(), size: v2.size.get(), crypt_method: v2.crypt_method.get(), l1_size: v2.l1_size.get(), l1_table_offset: v2.l1_table_offset.get(), refcount_table_offset: v2.refcount_table_offset.get(), refcount_table_clusters: v2.refcount_table_clusters.get(), nb_snapshots: v2.nb_snapshots.get(), snapshots_offset: v2.snapshots_offset.get(), incompatible_features: 0, compatible_features: 0, autoclear_features: 0, refcount_order: DEFAULT_REFCOUNT_ORDER, header_size: V2_BARE_HEADER_SIZE, compression_type: CompressionType::Zlib, backing_file: None, }; if version != 2 { let (v3, rest) = RawHeaderV3Tail::ref_from_prefix(tail).expect("buffer covers the v3 header"); header.incompatible_features = v3.incompatible_features.get(); header.compatible_features = v3.compatible_features.get(); header.autoclear_features = v3.autoclear_features.get(); header.refcount_order = v3.refcount_order.get(); header.header_size = v3.header_size.get(); if version == 3 && header.header_size > V3_BARE_HEADER_SIZE { let (compression, _) = BeU64::ref_from_prefix(rest).expect("buffer covers the compression field"); let raw_compression_type = compression.get() >> (64 - 8); header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB { Ok(CompressionType::Zlib) } else if raw_compression_type == COMPRESSION_TYPE_ZSTD { Ok(CompressionType::Zstd) } else { Err(Error::UnsupportedCompressionType) }?; } } if header.backing_file_size > MAX_BACKING_FILE_SIZE { return Err(Error::BackingFileTooLong(header.backing_file_size as usize)); } if header.backing_file_offset == 0 && header.backing_file_size != 0 { return Err(Error::BackingFileSizeWithoutOffset( header.backing_file_size, )); } if header.backing_file_offset != 0 && header.backing_file_size == 0 { return Err(Error::BackingFileOffsetWithoutSize( header.backing_file_offset, )); } if header.backing_file_offset != 0 { let cluster_size = 1u64 .checked_shl(header.cluster_bits) .ok_or(Error::InvalidClusterSize)?; if header.backing_file_offset < u64::from(header.header_size) { return Err(Error::BackingFileOverlapsHeader( header.backing_file_offset, header.backing_file_size, header.header_size, )); } if header.backing_file_offset >= cluster_size || header.backing_file_offset + u64::from(header.backing_file_size) > cluster_size { return Err(Error::BackingFileOutsideFirstCluster( header.backing_file_offset, header.backing_file_size, cluster_size, )); } let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize]; f.read_exact_at(&mut backing_file_name_bytes, header.backing_file_offset) .map_err(Error::ReadingHeader)?; let path = String::from_utf8(backing_file_name_bytes) .map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?; header.backing_file = Some(BackingFileConfig { path, format: None }); } if version == 3 { // Check for unsupported incompatible features first let features = IncompatFeatures::from_bits_retain(header.incompatible_features); let unsupported = features - IncompatFeatures::SUPPORTED; if !unsupported.is_empty() { // Read extensions only to get feature names for error reporting let mut feature_table = Vec::new(); if header.header_size > V3_BARE_HEADER_SIZE { let _ = Self::read_header_extensions(f, &mut header, Some(&mut feature_table)); } return Err(Error::UnsupportedFeature(MissingFeatureError::new( unsupported, feature_table, ))); } // Features OK, now read extensions normally if header.header_size > V3_BARE_HEADER_SIZE { Self::read_header_extensions(f, &mut header, None)?; } } Ok(header) } pub fn get_decoder(&self) -> Box { 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 `f`. pub fn write_to(&self, f: &AlignedFile) -> Result<()> { // Build the header in memory, then write it in one positional write. let mut buf = Vec::new(); let v2 = RawHeaderV2::from_header(self); buf.extend_from_slice(v2.as_bytes()); if self.version == 3 { let v3 = RawHeaderV3Tail::from_header(self); buf.extend_from_slice(v3.as_bytes()); if self.header_size > V3_BARE_HEADER_SIZE { let compression_type = match &self.compression_type { CompressionType::Zlib => COMPRESSION_TYPE_ZLIB, CompressionType::Zstd => COMPRESSION_TYPE_ZSTD, }; let compression_type = BeU64::new(compression_type << (64 - 8)); buf.extend_from_slice(compression_type.as_bytes()); } let end_extension = ExtensionHeader::end(); buf.extend_from_slice(end_extension.as_bytes()); } f.write_all_at(&buf, 0).map_err(Error::WritingHeader)?; if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) { let offset = if self.backing_file_offset > 0 { self.backing_file_offset } else { buf.len() as u64 }; f.write_all_at(backing_file_path.as_bytes(), offset) .map_err(Error::WritingHeader)?; } // Set the file length by writing a zero to the last byte. This also // zeros the l1 and refcount table clusters. let cluster_size = 0x01u64 << self.cluster_bits; let refcount_blocks_size = u64::from(self.refcount_table_clusters) * cluster_size; f.write_all_at( &[0u8], self.refcount_table_offset + refcount_blocks_size - 2, ) .map_err(Error::WritingHeader)?; Ok(()) } /// Write only the incompatible_features field to the file at its fixed offset. fn write_incompatible_features(&self, file: &AlignedFile) -> BlockResult<()> { if self.version != 3 { return Ok(()); } file.write_all_at( &self.incompatible_features.to_be_bytes(), V2_BARE_HEADER_SIZE as u64, ) .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::WritingHeader(e)))?; Ok(()) } /// Set or clear the dirty bit for QCOW2 v3 images. /// /// When `dirty` is true, sets the bit to indicate the image is in use. /// When `dirty` is false, clears the bit to indicate a clean shutdown. pub fn set_dirty_bit(&mut self, file: &mut AlignedFile, dirty: bool) -> BlockResult<()> { if self.version == 3 { if dirty { self.incompatible_features |= IncompatFeatures::DIRTY.bits(); } else { self.incompatible_features &= !IncompatFeatures::DIRTY.bits(); } self.write_incompatible_features(file)?; file.fsync() .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?; } Ok(()) } /// Set the corrupt bit for QCOW2 v3 images. /// /// This marks the image as corrupted. Once set, the image can only be /// opened read-only until repaired. pub fn set_corrupt_bit(&mut self, file: &mut AlignedFile) -> BlockResult<()> { if self.version == 3 { self.incompatible_features |= IncompatFeatures::CORRUPT.bits(); self.write_incompatible_features(file)?; file.fsync() .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SyncingHeader(e)))?; } Ok(()) } pub fn is_corrupt(&self) -> bool { IncompatFeatures::from_bits_truncate(self.incompatible_features) .contains(IncompatFeatures::CORRUPT) } /// Clear all autoclear feature bits for QCOW2 v3 images. /// /// These bits indicate features that can be safely disabled when modified /// by software that doesn't understand them. pub fn clear_autoclear_features(&mut self, file: &mut AlignedFile) -> Result<()> { if self.version == 3 && self.autoclear_features != 0 { self.autoclear_features = 0; file.write_all_at(&0u64.to_be_bytes(), AUTOCLEAR_FEATURES_OFFSET) .map_err(Error::WritingHeader)?; file.fsync().map_err(Error::SyncingHeader)?; } Ok(()) } } pub(super) fn max_refcount_clusters( refcount_order: u32, cluster_size: u32, num_clusters: u32, ) -> u64 { // Use u64 as the product of the u32 inputs can overflow. let refcount_bits = 0x01u64 << u64::from(refcount_order); let cluster_bits = u64::from(cluster_size) * 8; let for_data = div_round_up_u64(u64::from(num_clusters) * refcount_bits, cluster_bits); let for_refcounts = div_round_up_u64(for_data * refcount_bits, cluster_bits); for_data + for_refcounts } /// Returns an Error if the given offset doesn't align to a cluster boundary. pub(super) fn offset_is_cluster_boundary(offset: u64, cluster_bits: u32) -> Result<()> { if offset & ((0x01 << cluster_bits) - 1) != 0 { return Err(Error::InvalidOffset(offset)); } Ok(()) } cloud-hypervisor-53.0/block/src/formats/qcow/metadata.rs000066400000000000000000001254721522476750100234360ustar00rootroot00000000000000// 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::os::unix::fs::FileExt; use std::sync::{Arc, RwLock}; use std::{io, mem}; use libc::{EINVAL, EIO}; use vmm_sys_util::write_zeroes::WriteZeroesAt; use super::decoder::Decoder; use super::qcow_raw_file::QcowRawFile; 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_make_zero_plain, 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(super) enum ClusterReadMapping { /// The cluster is not allocated and the guest should see zeros. /// This covers both truly unallocated clusters where the L1 or L2 /// entry is zero and clusters with the ZERO flag set. 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(super) enum ClusterWriteMapping { /// The write target is at the given host file offset. /// This covers both already allocated clusters and freshly allocated ones. /// The offset is the exact byte position combining cluster base and /// 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(super) enum DeallocAction { /// Punch a hole at the given host file offset for a full cluster. PunchHole { host_offset: u64, length: u64 }, /// Write zeros at the given host file offset for a partial cluster. 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(super) 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(super) 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(super) 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(super) fn flush(&self) -> io::Result<()> { let mut inner = self.inner.write().unwrap(); inner.sync_caches()?; let mut unref = mem::take(&mut inner.unref_clusters); inner.avail_clusters.append(&mut unref); Ok(()) } /// Flushes dirty metadata caches and clears the dirty bit for /// clean shutdown. pub(super) 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(super) fn resize(&self, new_size: u64) -> io::Result<()> { let mut inner = self.inner.write().unwrap(); inner.resize(new_size) } /// Deallocates a range of bytes. Full clusters are deallocated via metadata. /// If `zero_marker` is true, full-cluster deallocation records a logical /// zero instead of an empty entry where backing data could otherwise be /// exposed. Partial clusters need the caller to write zeros. This method /// returns a list of actions the caller should take. pub(crate) fn deallocate_bytes( &self, address: u64, length: usize, sparse: bool, zero_marker: bool, 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 = inner.header.size; let cluster_size = inner.raw_file.cluster_size(); let remaining_in_file = file_end.saturating_sub(address); let write_count = min(length as u64, remaining_in_file) as usize; 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, zero_marker && backing_file.is_some(), )?; 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(super) fn virtual_size(&self) -> u64 { self.inner.read().unwrap().header.size } pub(super) fn cluster_size(&self) -> u64 { self.inner.read().unwrap().raw_file.cluster_size() } /// Returns the shared decoder matching the image compression type. pub(super) fn decoder(&self) -> Arc { Arc::clone(&self.decoder) } #[cfg(test)] pub fn header(&self) -> QcowHeader { self.inner.read().unwrap().header.clone() } #[cfg(test)] pub fn cluster_refcount(&self, address: u64) -> io::Result { let mut inner = self.inner.write().unwrap(); let QcowState { refcounts, raw_file, .. } = &mut *inner; refcounts .get_cluster_refcount(raw_file, address) .map_err(|e| io::Error::other(format!("get_cluster_refcount: {e}"))) } } impl QcowState { /// 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) { Ok(Some(ClusterReadMapping::Zero { length: count as u64, })) } else { let cluster_addr = l2_entry_std_cluster_addr(l2_entry); if !self.is_refcount_addressable_cluster_offset(cluster_addr) { // 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) { Ok(ClusterReadMapping::Zero { length: count as u64, }) } else { let cluster_addr = l2_entry_std_cluster_addr(l2_entry); self.reject_invalid_cluster_offset(cluster_addr)?; 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, } } } /// 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)?; let nwritten = self .raw_file .file_mut() .write_at(&decompressed_cluster, cluster_addr)?; if nwritten != decompressed_cluster.len() { self.set_corrupt_bit_best_effort(); return Err(io::Error::from_raw_os_error(EIO)); } 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 = if l2_entry_is_zero(l2_entry) { self.append_zeroed_data_cluster()? } else { self.append_data_cluster(backing_data)? }; self.update_cluster_addr(l1_index, l2_index, cluster_addr, &mut set_refcounts)?; cluster_addr } else { let cluster_addr = l2_entry_std_cluster_addr(l2_entry); self.reject_invalid_cluster_offset(cluster_addr)?; 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 } fn is_refcount_addressable_cluster_offset(&self, cluster_addr: u64) -> bool { cluster_addr & (self.raw_file.cluster_size() - 1) == 0 && cluster_addr <= self.refcounts.max_valid_cluster_offset() } fn reject_invalid_cluster_offset(&mut self, cluster_addr: u64) -> io::Result<()> { if self.is_refcount_addressable_cluster_offset(cluster_addr) { Ok(()) } else { self.set_corrupt_bit_best_effort(); Err(io::Error::from_raw_os_error(EIO)) } } // -- Cache and allocation operations requiring exclusive access -- /// Populates the L2 cache for read operations without allocation. fn cache_l2_cluster(&mut self, l1_index: usize, l2_addr_disk: u64) -> io::Result<()> { if !self.l2_cache.contains_key(l1_index) { self.reject_invalid_cluster_offset(l2_addr_disk)?; let l2_table = VecCache::from_vec(self.raw_file.read_pointer_cluster(l2_addr_disk, None)?); let l1_table = &self.l1_table; 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 { self.reject_invalid_cluster_offset(l2_addr_disk)?; VecCache::from_vec(self.raw_file.read_pointer_cluster(l2_addr_disk, None)?) }; let l1_table = &self.l1_table; 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) } /// Allocates a data cluster and zeroes it without building a cluster-sized buffer. fn append_zeroed_data_cluster(&mut self) -> io::Result { let new_addr = self.get_new_cluster(None)?; let cluster_size = self.raw_file.cluster_size() as usize; self.raw_file .file_mut() .write_zeroes_at(new_addr, cluster_size)?; self.set_cluster_refcount_track_freed(new_addr, 1)?; Ok(new_addr) } /// Updates the L1 and L2 tables to point to a new cluster address. fn update_cluster_addr( &mut self, 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.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.physical_size()?; let new_l1_offset = self.raw_file.cluster_address(file_size + cluster_size - 1); let new_file_end = new_l1_offset + new_l1_clusters * cluster_size; 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.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. If `zero_marker` is true, empty entries are replaced with /// logical-zero entries so reads do not fall through to backing data. /// /// Returns None if no host punch_hole is needed. pub(super) fn deallocate_cluster( &mut self, address: u64, sparse: bool, zero_marker: bool, ) -> io::Result> { 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 dealloc_entry = if zero_marker { l2_entry_make_zero_plain() } else { 0 }; if l2_addr_disk == 0 { if zero_marker { if let Some(new_addr) = self.cache_l2_cluster_alloc(l1_index, l2_addr_disk)? { self.set_cluster_refcount_track_freed(new_addr, 1)?; } self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry; } return Ok(None); } self.cache_l2_cluster(l1_index, l2_addr_disk)?; let l2_entry = self.l2_cache.get(l1_index).unwrap()[l2_index]; if l2_entry_is_empty(l2_entry) { if zero_marker { self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry; } return Ok(None); } // Compressed entries may use bit 0 as part of their layout, so they // must be classified before zero-flagged standard entries. if l2_entry_is_compressed(l2_entry) { self.deallocate_compressed_cluster(l2_entry)?; self.l2_cache.get_mut(l1_index).unwrap()[l2_index] = dealloc_entry; return Ok(None); } if l2_entry_is_zero(l2_entry) { return Ok(None); } let cluster_addr = l2_entry_std_cluster_addr(l2_entry); self.reject_invalid_cluster_offset(cluster_addr)?; let refcount = self .refcounts .get_cluster_refcount(&mut self.raw_file, cluster_addr) .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] = dealloc_entry; 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] = dealloc_entry; } 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); let mut compressed = vec![0u8; compressed_size]; self.raw_file .file_mut() .read_exact_at(&mut compressed, compressed_addr)?; let decoder = self.header.get_decoder(); let cluster_size = self.raw_file.cluster_size() as usize; let mut decompressed = vec![0u8; cluster_size]; 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}"); } } } cloud-hypervisor-53.0/block/src/formats/qcow/mod.rs000066400000000000000000000321731522476750100224300ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! QCOW2 disk image format. //! //! Provides [`QcowDisk`], the `DiskFile` wrapper for QCOW2 images //! with backing file and compression support. mod backing; mod common; mod decoder; mod engine_sync; #[cfg(feature = "io_uring")] mod engine_uring; mod header; mod metadata; mod parser; mod qcow_raw_file; mod refcount; mod util; mod vec_cache; use std::fs::File; use std::os::unix::io::AsRawFd; #[cfg(any(test, feature = "test-utils"))] use std::path::Path; use std::sync::Arc; use std::{fmt, io}; pub use parser::{ BackingFileConfig, CompressionType, Error, ImageType, IncompatFeatures, MissingFeatureError, QcowHeader, }; #[cfg(any(test, feature = "test-utils"))] use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap}; #[cfg(any(test, feature = "test-utils"))] use vmm_sys_util::tempfile::TempFile; use self::backing::shared_backing_from; use self::engine_sync::QcowSync; #[cfg(feature = "io_uring")] use self::engine_uring::QcowAsync; use self::metadata::{BackingRead, QcowMetadata}; use self::parser::{MAX_NESTING_DEPTH, parse_qcow}; use self::qcow_raw_file::QcowRawFile; use crate::aligned_file::AlignedFile; #[cfg(any(test, feature = "test-utils"))] use crate::async_io::GuestMemoryTarget; use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; use crate::disk_file; #[cfg(any(test, feature = "test-utils"))] use crate::disk_file::AsyncDiskFile; use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; /// Unified DiskFile wrapper for QCOW2 disk images. /// /// Holds the in memory QCOW2 metadata, the data file, and an optional /// backing file. The metadata is wrapped in an `Arc` because /// [`QcowSync`] and [`QcowAsync`] I/O workers receive a clone when /// they are created via [`create_async_io`](DiskFile::create_async_io). /// The backing file is likewise shared with workers through an `Arc`. /// /// The `sparse` flag controls whether the image advertises discard /// support to the guest. The `use_io_uring` flag selects between the /// [`QcowSync`] and [`QcowAsync`] I/O backends. Both are recorded at /// construction time and propagated through [`try_clone`](DiskFile::try_clone). pub struct QcowDisk { metadata: Arc, 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 = AlignedFile::new(file, direct_io); let (inner, backing_file, sparse) = parse_qcow(raw_file, max_nesting_depth, sparse) .map_err(|e| { let e = if !backing_files && matches!(e.kind(), BlockErrorKind::Overflow) { e.with_kind(BlockErrorKind::UnsupportedFeature) } else { e }; e.with_op(ErrorOp::Open) })?; let data_raw_file = inner.raw_file.clone(); Ok(QcowDisk { metadata: Arc::new(QcowMetadata::new(inner)), backing_file: backing_file.map(shared_backing_from).transpose()?, sparse, data_raw_file, use_io_uring, }) } /// Synchronous write convenience for tests and benchmarks. #[cfg(any(test, feature = "test-utils"))] pub fn write_all_at(&self, offset: u64, data: &[u8]) { let mut async_io = self.create_async_io(1).unwrap(); let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), data.len())]).unwrap()); mem.write_slice(data, GuestAddress(0)).unwrap(); let range = [(GuestAddress(0), data.len() as u32)]; let target = GuestMemoryTarget::new(Arc::clone(&mem), &range).unwrap(); async_io .write_from_memory(offset as libc::off_t, target, 0) .unwrap(); while async_io.next_completed_request().is_some() {} } /// Synchronous read convenience for tests and benchmarks. #[cfg(test)] pub fn read_all_at(&self, offset: u64, len: usize) -> Vec { let mut async_io = self.create_async_io(1).unwrap(); let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), len)]).unwrap()); let range = [(GuestAddress(0), len as u32)]; let target = GuestMemoryTarget::new(Arc::clone(&mem), &range).unwrap(); async_io .read_to_memory(offset as libc::off_t, target, 0) .unwrap(); while async_io.next_completed_request().is_some() {} let mut buf = vec![0u8; len]; mem.read_slice(&mut buf, GuestAddress(0)).unwrap(); buf } #[cfg(test)] fn metadata(&self) -> &QcowMetadata { &self.metadata } } /// Writes a fresh qcow2 layout into `file` #[cfg(any(test, feature = "test-utils"))] pub(crate) fn create_image( file: &File, virtual_size: u64, backing_config: Option<&BackingFileConfig>, ) -> BlockResult<()> { let path = backing_config.map(|cfg| cfg.path.as_str()); let mut header = QcowHeader::create_for_size_and_path(3, virtual_size, path) .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; if let Some(cfg) = backing_config && let Some(backing_file) = &mut header.backing_file { backing_file.format = cfg.format; } let raw = AlignedFile::new( file.try_clone() .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?, false, ); header .write_to(&raw) .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; let (inner, _backing, _sparse) = parse_qcow(raw, MAX_NESTING_DEPTH, true)?; // Flush dirty caches and clear the dirty bit QcowMetadata::new(inner).shutdown(); Ok(()) } /// Helper struct to create a new qcow2 image in a temporary file. #[cfg(any(test, feature = "test-utils"))] pub struct QcowTempDisk { tmp: TempFile, disk: QcowDisk, } #[cfg(any(test, feature = "test-utils"))] impl QcowTempDisk { /// Creates a new qcow2 image in a temporary file with optional /// backing file. Flags are passed to QcowDisk::new. pub fn new( virtual_size: u64, backing_config: Option<&BackingFileConfig>, direct_io: bool, sparse: bool, use_io_uring: bool, ) -> BlockResult { let tmp = TempFile::new().map_err(io::Error::from)?; create_image(tmp.as_file(), virtual_size, backing_config)?; let file = tmp .as_file() .try_clone() .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?; let disk = QcowDisk::new( file, direct_io, backing_config.is_some(), sparse, use_io_uring, )?; Ok(Self { tmp, disk }) } pub fn path(&self) -> &Path { self.tmp.as_path() } pub fn as_file(&self) -> &File { self.tmp.as_file() } pub fn disk(&self) -> &QcowDisk { &self.disk } /// Drops the disk handle and returns the underlying TempFile. pub fn into_tempfile(self) -> TempFile { self.tmp } } impl Drop for QcowDisk { fn drop(&mut self) { self.metadata.shutdown(); } } impl disk_file::DiskSize for QcowDisk { fn logical_size(&self) -> BlockResult { 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 super::*; use crate::async_io::AsyncIo; use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize}; const TEST_SIZE: u64 = 0x5566_7788; fn make_qcow_file() -> File { QcowTempDisk::new(TEST_SIZE, None, false, true, false) .unwrap() .into_tempfile() .into_file() } #[test] fn new_sync_returns_correct_size() { let file = make_qcow_file(); let disk = QcowDisk::new(file, false, false, true, false).unwrap(); assert_eq!(disk.logical_size().unwrap(), TEST_SIZE); } fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_batch: bool) { let io: Box = 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()); } } cloud-hypervisor-53.0/block/src/formats/qcow/parser.rs000066400000000000000000002232351522476750100231460ustar00rootroot00000000000000// Copyright 2018 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::cmp::{max, min}; use std::fmt::{Debug, Formatter, Result as FmtResult}; use std::fs::{OpenOptions, read_link}; use std::os::fd::AsRawFd; use std::os::unix::fs::FileExt; use std::path::Path; use std::{io, result, str}; use log::warn; use remain::sorted; use thiserror::Error; pub use super::header::{ BackingFileConfig, CompressionType, ImageType, IncompatFeatures, MissingFeatureError, QcowHeader, }; use super::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 super::qcow_raw_file::QcowRawFile; use super::refcount::RefCount; pub(crate) use super::util::MAX_NESTING_DEPTH; use super::util::{L1_TABLE_OFFSET_MASK, L2_TABLE_OFFSET_MASK, div_round_up_u64}; use super::vec_cache::{CacheMap, VecCache}; use super::{metadata, refcount}; use crate::aligned_file::AlignedFile; use crate::error::{BlockError, BlockErrorKind, BlockResult}; use crate::query_device_size; #[sorted] #[derive(Debug, Error)] pub enum Error { #[error("Backing file I/O error: {0}")] BackingFileIo(String /* path */, #[source] io::Error), #[error("Backing file offset {0:#x} with zero size")] BackingFileOffsetWithoutSize(u64), #[error("Backing file open error: {0}")] BackingFileOpen(String /* path */, #[source] Box), #[error( "Backing file name at offset {0:#x} length {1:#x} lies outside first cluster of {2:#x}" )] BackingFileOutsideFirstCluster(u64, u32, u64), #[error("Backing file name at offset {0:#x} length {1:#x} overlaps header of size {2:#x}")] BackingFileOverlapsHeader(u64, u32, u32), #[error("Backing file size {0:#x} with zero offset")] BackingFileSizeWithoutOffset(u32), #[error("Backing file support is disabled")] BackingFilesDisabled, #[error("Backing file name is too long: {0} bytes over")] BackingFileTooLong(usize), #[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}")] FileTooBig(u64), #[error("Failed to get file size")] GettingFileSize(#[source] io::Error), #[error("Failed to get refcount")] GettingRefcount(#[source] refcount::Error), #[error("Failed to parse filename")] InvalidBackingFileName(#[source] str::Utf8Error), #[error("Invalid cluster index")] InvalidClusterIndex, #[error("Invalid cluster size")] InvalidClusterSize, #[error("Invalid index")] InvalidIndex, #[error("Invalid L1 table offset")] InvalidL1TableOffset, #[error("Invalid L1 table size: {0}")] InvalidL1TableSize(u32), #[error("Invalid magic")] InvalidMagic, #[error("Invalid offset: {0}")] InvalidOffset(u64), #[error("Invalid refcount table offset")] InvalidRefcountTableOffset, #[error("Invalid refcount table size: {0}")] InvalidRefcountTableSize(u64), #[error("Maximum disk nesting depth exceeded")] MaxNestingDepthExceeded, #[error("No free clusters")] NoFreeClusters, #[error("No refcount clusters")] NoRefcountClusters, #[error("Not enough space for refcounts")] NotEnoughSpaceForRefcounts, #[error("Failed to open file {0}")] OpeningFile(#[source] io::Error), #[error("Failed to read data")] ReadingData(#[source] io::Error), #[error("Failed to read header")] ReadingHeader(#[source] io::Error), #[error("Failed to read pointers")] ReadingPointers(#[source] io::Error), #[error("Failed to read ref count block")] ReadingRefCountBlock(#[source] refcount::Error), #[error("Failed to read ref counts")] 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 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}")] UnsupportedVersion(u32), #[error("Failed to write data")] WritingData(#[source] io::Error), #[error("Failed to write header")] WritingHeader(#[source] io::Error), } pub(super) type Result = result::Result; /// Concrete backing file variants. pub(crate) enum BackingKind { /// Raw backing file. Raw(AlignedFile), /// QCOW2 backing parsed into metadata and raw file. Qcow { inner: Box, backing: Option>, }, } /// Backing file wrapper pub(crate) struct BackingFile { kind: BackingKind, virtual_size: u64, } 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); }; // Check nesting depth - applies to any backing file if max_nesting_depth == 0 { return Err(BlockError::new( BlockErrorKind::Overflow, Error::MaxNestingDepthExceeded, )); } 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 mut raw_file = AlignedFile::new(backing_raw_file, direct_io); // 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)?, }; let (kind, virtual_size) = match backing_format { ImageType::Raw => { let size = query_device_size(raw_file.file()) .map_err(|e| { BlockError::new( BlockErrorKind::Io, Error::BackingFileIo(config.path.clone(), e), ) })? .0; (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 })) } /// Consume and return the kind and virtual size. pub(crate) fn into_kind(self) -> (BackingKind, u64) { (self.kind, self.virtual_size) } } impl Debug for BackingFile { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.debug_struct("BackingFile").finish() } } /// Parses and validates a QCOW2 image file, returning the metadata, backing /// file and sparse flag. /// /// Used by [`crate::formats::qcow::QcowDisk`] when opening an image. pub(crate) fn parse_qcow( file: AlignedFile, max_nesting_depth: u32, sparse: bool, ) -> BlockResult<(metadata::QcowState, Option, bool)> { let mut header = QcowHeader::new(&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(); // QCOW2 relative backing paths are resolved from the image that stores // them. Resolve only the config passed to BackingFile::new(), leaving the // header copy unchanged so the original backing filename is preserved. let backing_file_config = header.backing_file.as_ref().map(|config| { let mut config = config.clone(); if !Path::new(&config.path).is_absolute() && let Ok(disk_path) = read_link(format!("/proc/self/fd/{}", file.as_raw_fd())) && disk_path.exists() && let Some(parent) = disk_path.parent() { config.path = parent.join(&config.path).to_string_lossy().into_owned(); } config }); let backing_file = BackingFile::new( backing_file_config.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; let mut first_refblock_bytes = [0u8; 8]; file.read_exact_at(&mut first_refblock_bytes, header.refcount_table_offset) .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?; let first_refblock_addr = u64::from_be_bytes(first_refblock_bytes); if first_refblock_addr != 0 { let mut refcount_bytes = [0u8; 2]; file.read_exact_at(&mut refcount_bytes, first_refblock_addr) .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?; let first_cluster_refcount = u16::from_be_bytes(refcount_bytes); 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 { 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 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)) } 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 [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 [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, max_refcount, refcount_bits, )?; } Ok(()) } // Traverse the L1 and L2 tables to find all reachable data clusters. fn set_data_refcounts( 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( header.l1_table_offset, u64::from(header.l1_size), Some(L1_TABLE_OFFSET_MASK), ) .map_err(Error::ReadingPointers)?; for l1_index in 0..header.l1_size as usize { 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, max_refcount, refcount_bits, )?; // Read the L2 table and find all referenced data clusters. let l2_table = raw_file .read_pointer_table( l2_addr_disk, cluster_size / size_of::() as u64, Some(L2_TABLE_OFFSET_MASK), ) .map_err(Error::ReadingPointers)?; for data_cluster_addr in l2_table { if data_cluster_addr != 0 { add_ref( refcounts, cluster_size, data_cluster_addr, max_refcount, refcount_bits, )?; } } } } Ok(()) } // Add references to the top-level refcount table clusters. fn set_refcount_table_refcounts( 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) { add_ref( refcounts, cluster_size, refcount_table_offset + i * cluster_size, max_refcount, refcount_bits, )?; } Ok(()) } // Allocate clusters for refblocks. // This needs to be done last so that we have the correct refcounts for all other // clusters. fn alloc_refblocks( 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; for refblock_addr in &mut ref_table { loop { if first_free_cluster >= refcounts.len() as u64 { return Err(Error::NotEnoughSpaceForRefcounts); } if refcounts[first_free_cluster as usize] == 0 { break; } first_free_cluster += 1; } *refblock_addr = first_free_cluster * cluster_size; add_ref( refcounts, cluster_size, *refblock_addr, max_refcount, refcount_bits, )?; first_free_cluster += 1; } Ok(ref_table) } // Write the updated reference count blocks and reftable. fn write_refblocks( refcounts: &[u64], mut header: QcowHeader, ref_table: &[u64], raw_file: &mut QcowRawFile, refcount_block_entries: u64, ) -> Result<()> { // Rewrite the header with lazy refcounts enabled while we are rebuilding the tables. header.compatible_features |= COMPATIBLE_FEATURES_LAZY_REFCOUNTS; header.write_to(raw_file.file_mut())?; for (i, refblock_addr) in ref_table.iter().enumerate() { // Write a block of refcounts to the location indicated by refblock_addr. let refblock_start = i * (refcount_block_entries as usize); let refblock_end = min( refcounts.len(), refblock_start + refcount_block_entries as usize, ); let refblock = &refcounts[refblock_start..refblock_end]; raw_file .write_refcount_block(*refblock_addr, refblock) .map_err(Error::WritingHeader)?; // 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![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 + byte_offset, &refblock_padding) .map_err(Error::WritingHeader)?; } } // Rewrite the top-level refcount table. raw_file .write_pointer_table_direct(header.refcount_table_offset, ref_table.iter()) .map_err(Error::WritingHeader)?; // Rewrite the header again, now with lazy refcounts disabled. header.compatible_features &= !COMPATIBLE_FEATURES_LAZY_REFCOUNTS; header.write_to(raw_file.file_mut())?; Ok(()) } let cluster_size = raw_file.cluster_size(); let file_size = raw_file .file_mut() .metadata() .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::GettingFileSize(e)))? .len(); let refcount_bits = 1u64 << header.refcount_order; 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); let l1_clusters = div_round_up_u64(l2_clusters, pointers_per_cluster); let header_clusters = div_round_up_u64(size_of::() as u64, cluster_size); let max_clusters = data_clusters + l2_clusters + l1_clusters + header_clusters; let mut max_valid_cluster_index = max_clusters; let refblock_clusters = div_round_up_u64(max_valid_cluster_index, refcount_block_entries); let reftable_clusters = div_round_up_u64(refblock_clusters, pointers_per_cluster); // Account for refblocks and the ref table size needed to address them. let refblocks_for_refs = div_round_up_u64( refblock_clusters + reftable_clusters, refcount_block_entries, ); let reftable_clusters_for_refs = div_round_up_u64(refblocks_for_refs, refcount_block_entries); max_valid_cluster_index += refblock_clusters + reftable_clusters; max_valid_cluster_index += refblocks_for_refs + reftable_clusters_for_refs; if max_valid_cluster_index > MAX_RAM_POINTER_TABLE_SIZE { 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(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, 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, max_refcount, refcount_bits, ) .map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; // Write updated reference counts and point the reftable at them. write_refblocks( &refcounts, header, &ref_table, raw_file, refcount_block_entries, ) .map_err(|e| BlockError::new(BlockErrorKind::Io, e)) } /// Detect the type of an image file by checking for a valid qcow2 header. pub(super) fn detect_image_type(file: &mut AlignedFile) -> BlockResult { let mut magic_bytes = [0u8; 4]; file.read_exact_at(&mut magic_bytes, 0) .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::ReadingHeader(e)))?; let magic = u32::from_be_bytes(magic_bytes); let image_type = if magic == QCOW_MAGIC { ImageType::Qcow2 } else { ImageType::Raw }; Ok(image_type) } #[cfg(test)] mod unit_tests { use std::error::Error as StdError; use std::fs::{File, OpenOptions}; use std::io::Write; use std::os::unix::fs::FileExt; use std::path::Path; use vmm_sys_util::tempdir::TempDir; use vmm_sys_util::tempfile::TempFile; use super::super::header::{ AUTOCLEAR_FEATURES_OFFSET, DEFAULT_CLUSTER_BITS, DEFAULT_REFCOUNT_ORDER, HEADER_EXT_BACKING_FORMAT, HEADER_EXT_END, V2_BARE_HEADER_SIZE, V3_BARE_HEADER_SIZE, }; use super::super::util::{self, ZERO_FLAG}; use super::*; use crate::formats::qcow::{QcowDisk, QcowTempDisk}; fn valid_header_v3() -> Vec { vec![ 0x51u8, 0x46, 0x49, 0xfb, // magic 0x00, 0x00, 0x00, 0x03, // version 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset 0x00, 0x00, 0x00, 0x00, // backing file size 0x00, 0x00, 0x00, 0x10, // cluster_bits 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, // size 0x00, 0x00, 0x00, 0x00, // crypt method 0x00, 0x00, 0x01, 0x00, // L1 size 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset 0x00, 0x00, 0x00, 0x03, // refcount table clusters 0x00, 0x00, 0x00, 0x00, // nb snapshots 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // incompatible_features 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compatible_features 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // autoclear_features 0x00, 0x00, 0x00, 0x04, // refcount_order 0x00, 0x00, 0x00, 0x68, // header_length ] } fn valid_header_v2() -> Vec { vec![ 0x51u8, 0x46, 0x49, 0xfb, // magic 0x00, 0x00, 0x00, 0x02, // version 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset 0x00, 0x00, 0x00, 0x00, // backing file size 0x00, 0x00, 0x00, 0x10, // cluster_bits 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, // size 0x00, 0x00, 0x00, 0x00, // crypt method 0x00, 0x00, 0x01, 0x00, // L1 size 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset 0x00, 0x00, 0x00, 0x03, // refcount table clusters 0x00, 0x00, 0x00, 0x00, // nb snapshots 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset ] } // Test case found by clusterfuzz to allocate excessive memory. fn test_huge_header() -> Vec { vec![ 0x51, 0x46, 0x49, 0xfb, // magic 0x00, 0x00, 0x00, 0x03, // version 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset 0x00, 0x00, 0x00, 0x00, // backing file size 0x00, 0x00, 0x00, 0x09, // cluster_bits 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // size 0x00, 0x00, 0x00, 0x00, // crypt method 0x00, 0x00, 0x01, 0x00, // L1 size 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset 0x00, 0x00, 0x00, 0x03, // refcount table clusters 0x00, 0x00, 0x00, 0x00, // nb snapshots 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // incompatible_features 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compatible_features 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // autoclear_features 0x00, 0x00, 0x00, 0x04, // refcount_order 0x00, 0x00, 0x00, 0x68, // header_length ] } fn basic_file(header: &[u8]) -> AlignedFile { let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false); disk_file.write_all_at(header, 0).unwrap(); disk_file.set_len(0x1_0000_0000).unwrap(); disk_file } fn with_basic_file(header: &[u8], mut testfn: F) where F: FnMut(AlignedFile), { testfn(basic_file(header)); // File closed when the function exits. } fn tempfile_with_header(header: &[u8]) -> TempFile { let temp = TempFile::new().unwrap(); let mut file = temp.as_file().try_clone().unwrap(); file.write_all(header).unwrap(); file.set_len(0x1_0000_0000).unwrap(); file.sync_all().unwrap(); temp } fn try_open_header(header: &[u8]) -> BlockResult { let temp = tempfile_with_header(header); let file = temp.into_file(); QcowDisk::new(file, false, false, true, false) } fn try_open_qcow_header(header: &QcowHeader, backing_files: bool) -> BlockResult { let temp = TempFile::new().unwrap(); let raw = AlignedFile::new(temp.as_file().try_clone().unwrap(), false); header.write_to(&raw).expect("write header"); drop(raw); let file = temp.into_file(); QcowDisk::new(file, false, backing_files, true, false) } #[test] fn detect_image_type_recognizes_qcow2_magic() { let mut file = basic_file(&valid_header_v3()); assert_eq!(detect_image_type(&mut file).unwrap(), ImageType::Qcow2); } #[test] fn detect_image_type_treats_other_magic_as_raw() { let mut file = basic_file(&[0u8; 16]); assert_eq!(detect_image_type(&mut file).unwrap(), ImageType::Raw); } #[test] fn default_header_v2() { let header = QcowHeader::create_for_size_and_path(2, 0x10_0000, None) .expect("Failed to create header."); try_open_qcow_header(&header, false) .expect("Failed to create QcowDisk from default header"); } #[test] fn default_header_v3() { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None) .expect("Failed to create header."); try_open_qcow_header(&header, false) .expect("Failed to create QcowDisk from default header"); } #[test] fn header_read() { with_basic_file(&valid_header_v2(), |disk_file: AlignedFile| { let header = QcowHeader::new(&disk_file).expect("Failed to create Header."); assert_eq!(header.version, 2); assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER); assert_eq!(header.header_size, V2_BARE_HEADER_SIZE); }); with_basic_file(&valid_header_v3(), |disk_file: AlignedFile| { let header = QcowHeader::new(&disk_file).expect("Failed to create Header."); assert_eq!(header.version, 3); assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER); assert_eq!(header.header_size, V3_BARE_HEADER_SIZE); }); } #[test] fn header_write_matches_qcow2_layout() { for expected in [valid_header_v2(), valid_header_v3()] { let header = QcowHeader::new(&basic_file(&expected)).expect("Failed to read header."); let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false); header .write_to(&disk_file) .expect("Failed to write header."); let mut actual = vec![0; expected.len()]; disk_file.read_exact_at(&mut actual, 0).unwrap(); assert_eq!(actual, expected); } let mut expected = valid_header_v3(); expected[72..80].copy_from_slice(&IncompatFeatures::COMPRESSION.bits().to_be_bytes()); expected[100..104].copy_from_slice(&(V3_BARE_HEADER_SIZE + 8).to_be_bytes()); expected.extend_from_slice(&(1u64 << (64 - 8)).to_be_bytes()); expected.extend_from_slice(&HEADER_EXT_END.to_be_bytes()); expected.extend_from_slice(&0u32.to_be_bytes()); let header = QcowHeader::new(&basic_file(&expected)).expect("Failed to read zstd header."); let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false); header .write_to(&disk_file) .expect("Failed to write header."); let mut actual = vec![0; expected.len()]; disk_file.read_exact_at(&mut actual, 0).unwrap(); assert_eq!(actual, expected); } #[test] fn header_v2_with_backing() { let header = QcowHeader::create_for_size_and_path(2, 0x10_0000, Some("/my/path/to/a/file")) .expect("Failed to create header."); let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false); header .write_to(&disk_file) .expect("Failed to write header to shm."); let read_header = QcowHeader::new(&disk_file).expect("Failed to create header."); assert_eq!( header.backing_file.as_ref().map(|bf| bf.path.clone()), Some(String::from("/my/path/to/a/file")) ); assert_eq!( read_header.backing_file.as_ref().map(|bf| &bf.path), header.backing_file.as_ref().map(|bf| &bf.path) ); } #[test] fn header_v3_with_backing() { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, Some("/my/path/to/a/file")) .expect("Failed to create header."); let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false); header .write_to(&disk_file) .expect("Failed to write header to shm."); let read_header = QcowHeader::new(&disk_file).expect("Failed to create header."); assert_eq!( header.backing_file.as_ref().map(|bf| bf.path.clone()), Some(String::from("/my/path/to/a/file")) ); assert_eq!( read_header.backing_file.as_ref().map(|bf| &bf.path), header.backing_file.as_ref().map(|bf| &bf.path) ); } /// Write a header to a fresh file with backing_file_offset and /// backing_file_size patched. Panics on setup failures, returns /// the parse result of the patched header. fn read_header_with_patched_backing(offset: u64, size: u32) -> Result { let mut header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None) .expect("Failed to create header."); header.backing_file_offset = offset; header.backing_file_size = size; let disk_file: AlignedFile = AlignedFile::new( TempFile::new() .expect("Failed to create temp file.") .into_file(), false, ); header .write_to(&disk_file) .expect("Failed to write header."); QcowHeader::new(&disk_file) } #[test] fn backing_file_offset_at_cluster_boundary() { let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS; let err = read_header_with_patched_backing(cluster_size, 1).unwrap_err(); assert!(matches!( err, Error::BackingFileOutsideFirstCluster(_, _, _) )); } #[test] fn backing_file_offset_past_cluster() { let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS; let err = read_header_with_patched_backing(cluster_size + 4096, 16).unwrap_err(); assert!(matches!( err, Error::BackingFileOutsideFirstCluster(_, _, _) )); } #[test] fn backing_file_end_past_cluster() { let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS; let err = read_header_with_patched_backing(cluster_size - 4, 16).unwrap_err(); assert!(matches!( err, Error::BackingFileOutsideFirstCluster(_, _, _) )); } #[test] fn backing_file_offset_inside_header() { let err = read_header_with_patched_backing(64, 16).unwrap_err(); assert!(matches!(err, Error::BackingFileOverlapsHeader(_, _, _))); } #[test] fn backing_file_size_without_offset() { let err = read_header_with_patched_backing(0, 16).unwrap_err(); assert!(matches!(err, Error::BackingFileSizeWithoutOffset(16))); } #[test] fn backing_file_offset_without_size() { let err = read_header_with_patched_backing(1024, 0).unwrap_err(); assert!(matches!(err, Error::BackingFileOffsetWithoutSize(1024))); } #[test] fn backing_file_fits_at_cluster_end() { let cluster_size = 1u64 << DEFAULT_CLUSTER_BITS; let header = read_header_with_patched_backing(cluster_size - 16, 16).expect("Header should parse."); assert_eq!(header.backing_file_offset, cluster_size - 16); assert_eq!(header.backing_file_size, 16); } /// Helper to create a test file with header extensions fn create_header_with_extension(ext_type: u32, ext_data: &[u8]) -> (AlignedFile, QcowHeader) { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None) .expect("Failed to create header."); let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false); header.write_to(&disk_file).unwrap(); // Build the extension area and write it positionally after the header. let mut ext = Vec::new(); ext.extend_from_slice(&ext_type.to_be_bytes()); ext.extend_from_slice(&(ext_data.len() as u32).to_be_bytes()); ext.extend_from_slice(ext_data); // Pad to the next 8 byte boundary. let padding = (8 - (ext_data.len() % 8)) % 8; ext.resize(ext.len() + padding, 0); ext.extend_from_slice(&HEADER_EXT_END.to_be_bytes()); disk_file .write_all_at(&ext, header.header_size as u64) .unwrap(); (disk_file, header) } #[test] fn read_header_extensions_unknown_extension() { let (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(&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 (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(&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 (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(&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 (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(&disk_file, &mut header, None); assert!(matches!( result.unwrap_err(), Error::UnsupportedBackingFileFormat(_) )); } #[test] fn read_header_extensions_invalid_utf8() { let (disk_file, mut header) = create_header_with_extension( HEADER_EXT_BACKING_FORMAT, &[0xFF, 0xFE, 0xFD], // invalid UTF-8 ); let result = QcowHeader::read_header_extensions(&disk_file, &mut header, None); // Should fail with InvalidBackingFileName error assert!(matches!( result.unwrap_err(), Error::InvalidBackingFileName(_) )); } #[test] fn no_backing_file() { // No backing file declared; opening with backing_files=false should succeed. let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None) .expect("Failed to create header."); try_open_qcow_header(&header, false).unwrap(); } #[test] fn disable_backing_file() { // Backing file is declared but backing_files=false disables it. QcowDisk // maps Overflow -> UnsupportedFeature when backing files are disabled. let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, Some("/path/to/backing/file")) .expect("Failed to create header."); let err = try_open_qcow_header(&header, false).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::MaxNestingDepthExceeded)); } /// Create a qcow2 file with itself as its backing file. /// /// Without configuration `max_nesting_depth`, this will cause infinite recursion when loading /// the file until stack overflow. fn new_self_referential_qcow(path: &Path) -> Result<()> { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, path.to_str())?; let disk_file = AlignedFile::new( File::create(path).expect("Failed to create image file."), false, ); header.write_to(&disk_file)?; Ok(()) } #[test] fn max_nesting_backing() { let test_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); let img_path = test_dir.as_path().join("test.img"); new_self_referential_qcow(img_path.as_path()).unwrap(); let err = QcowDisk::new( File::open(img_path.as_path()).expect("Failed to open qcow image file"), false, true, true, false, ) .expect_err("Opening qcow file with itself as backing file should fail."); // This type of error is complex. For comparing easily, we can check if it contains the // type name after formatting. assert!(format!("{err:?}").contains(&format!("{:?}", Error::MaxNestingDepthExceeded))); // This should recursively call the function MAX_NESTING_DEPTH times before throwing the // error, so `BackingFileOpen` should appear that many times. assert_eq!( format!("{err:?}") .matches("BackingFileOpen") .collect::>() .len() as u32, MAX_NESTING_DEPTH, ); } #[test] fn invalid_magic() { let invalid_header = vec![0x51u8, 0x46, 0x4a, 0xfb]; try_open_header(&invalid_header).expect_err("Invalid header worked."); } #[test] fn invalid_refcount_order() { let mut header = valid_header_v3(); header[99] = 7; try_open_header(&header).expect_err("Invalid refcount order worked."); } #[test] fn invalid_cluster_bits() { let mut header = valid_header_v3(); header[23] = 3; try_open_header(&header).expect_err("Failed to create file."); } #[test] fn test_header_huge_file() { let header = test_huge_header(); try_open_header(&header).expect_err("Failed to create file."); } #[test] fn test_header_crazy_file_size_rejected() { let mut header = valid_header_v3(); header[24..32].copy_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1e]); try_open_header(&header).expect_err("Failed to create file."); } #[test] fn test_huge_l1_table() { let mut header = valid_header_v3(); header[36] = 0x12; try_open_header(&header).expect_err("Failed to create file."); } #[test] fn test_header_1_tb_file_min_cluster() { let mut header = test_huge_header(); header[24] = 0; header[26] = 1; header[31] = 0; // 1 TB with the min cluster size makes the arrays too big, it should fail. try_open_header(&header).expect_err("Failed to create file."); } #[test] fn test_header_1_tb_file() { let mut header = test_huge_header(); // reset to 1 TB size. header[24] = 0; header[26] = 1; header[31] = 0; // set cluster_bits header[23] = 16; let disk = try_open_header(&header).expect("Failed to create file."); // Write a sentinel value near the end of the virtual disk. let value = 0x0000_0040_3f00_ffffu64; disk.write_all_at(0x100_0000_0000 - 8, &value.to_le_bytes()); } #[test] fn test_header_huge_num_refcounts() { let mut header = valid_header_v3(); header[56..60].copy_from_slice(&[0x02, 0x00, 0xe8, 0xff]); try_open_header(&header).expect_err("Created disk with crazy refcount clusters"); } #[test] fn test_header_huge_refcount_offset() { let mut header = valid_header_v3(); header[48..56].copy_from_slice(&[0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x02, 0x00]); try_open_header(&header).expect_err("Created disk with crazy refcount offset"); } #[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 = util::COMPRESSED_FLAG; let compressed_entry_with_low_bit: u64 = util::COMPRESSED_FLAG | ZERO_FLAG; assert!(util::l2_entry_is_empty(empty_entry)); assert!(!util::l2_entry_is_empty(standard_entry)); assert!(!util::l2_entry_is_compressed(standard_entry)); assert!(util::l2_entry_is_compressed(compressed_entry)); assert!(util::l2_entry_is_compressed(compressed_entry_with_low_bit)); assert!(!util::l2_entry_is_zero(standard_entry)); assert!(util::l2_entry_is_zero(zero_flag_entry)); assert!(util::l2_entry_is_zero(compressed_entry_with_low_bit)); // Note: l2_entry_is_zero() only checks bit 0, so compressed entries // must be checked before interpreting bit 0 as a zero flag. } #[test] fn rebuild_refcounts() { // A v3 header where the first refblock pointer is zero forces the // refcount rebuild path inside parse_qcow. let header = valid_header_v3(); let disk = try_open_header(&header).expect("Failed to open and rebuild refcounts"); // After rebuild the first cluster (header) must have refcount > 0. let refcount = disk.metadata().cluster_refcount(0).unwrap(); assert!(refcount > 0, "header cluster refcount should be set"); } /// 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; try_open_header(&header).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; let disk = try_open_header(&header).unwrap(); let test_data = b"test data for refcount"; disk.write_all_at(0, test_data); assert_eq!(&disk.read_all_at(0, test_data.len()), test_data); // Write to another cluster disk.write_all_at(0x10000, test_data); assert_eq!(&disk.read_all_at(0x10000, test_data.len()), 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; let disk = try_open_header(&header).unwrap(); // Write then overwrite at offset 0. disk.write_all_at(0, b"initial data here!!!"); let new_data = b"overwritten data!!!!"; disk.write_all_at(0, new_data); assert_eq!(&disk.read_all_at(0, new_data.len()), new_data); // Allocate multiple clusters let cluster_size = 0x10000u64; for i in 1..4u64 { disk.write_all_at(i * cluster_size, b"cluster data"); } for i in 1..4u64 { assert_eq!(&disk.read_all_at(i * cluster_size, 12), 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; let disk = try_open_header(&header).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 { disk.write_all_at(i * l2_coverage, b"eviction test"); } // Verify evicted regions can be re-read for i in [0u64, 1, 50, 100, 109] { assert_eq!(&disk.read_all_at(i * l2_coverage, 13), 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 = TempFile::new().unwrap().into_file(); let cluster_size = 0x10000u64; file.set_len(cluster_size * 2).unwrap(); let raw = AlignedFile::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 = TempFile::new().unwrap().into_file(); let cluster_size = 0x10000u64; file.set_len(cluster_size * 2).unwrap(); let raw = AlignedFile::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 = 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 = AlignedFile::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() .write_all_at(&(cluster_size * 2).to_be_bytes(), refcount_table_offset) .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" ); } } // 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); let result = try_open_header(&header); 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); let err = try_open_header(&header).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); let err = try_open_header(&header).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); let err = try_open_header(&header).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)); let err = try_open_header(&header).unwrap_err(); assert!(matches!(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); let err = try_open_header(&header).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:?}" ); } /// Reads the incompatible_features u64 at its v3 offset from a file. fn read_incompat_features(path: &Path) -> u64 { let file = OpenOptions::new().read(true).open(path).unwrap(); let mut buf = [0u8; 8]; file.read_exact_at(&mut buf, V2_BARE_HEADER_SIZE as u64) .unwrap(); u64::from_be_bytes(buf) } /// Reads the autoclear_features u64 at its v3 offset from a file. fn read_autoclear_features(path: &Path) -> u64 { let file = OpenOptions::new().read(true).open(path).unwrap(); let mut buf = [0u8; 8]; file.read_exact_at(&mut buf, AUTOCLEAR_FEATURES_OFFSET) .unwrap(); u64::from_be_bytes(buf) } #[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 temp = tempfile_with_header(&valid_header_v3()); let path = temp.as_path().to_owned(); assert_eq!( read_incompat_features(&path) & IncompatFeatures::DIRTY.bits(), 0, "Dirty bit should not be set initially" ); let file = temp.as_file().try_clone().unwrap(); let disk = QcowDisk::new(file, false, false, true, false).unwrap(); assert_ne!( read_incompat_features(&path) & IncompatFeatures::DIRTY.bits(), 0, "Dirty bit should be set while file is open" ); drop(disk); assert_eq!( read_incompat_features(&path) & 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 temp = tempfile_with_header(&valid_header_v2()); let disk = QcowDisk::new(temp.into_file(), false, false, true, false).unwrap(); assert_eq!(disk.metadata().header().version, 2); } #[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 temp = tempfile_with_header(&valid_header_v3()); let temp_path = temp.as_path().to_owned(); let readonly_file = OpenOptions::new() .read(true) .write(false) .open(&temp_path) .unwrap(); // Opening as a QcowDisk should not set the dirty bit for read-only files. let _disk = QcowDisk::new(readonly_file, false, false, true, false).unwrap(); assert_eq!( read_incompat_features(&temp_path) & IncompatFeatures::DIRTY.bits(), 0, "Dirty bit should not be written for read-only files" ); } #[test] fn autoclear_features_cleared_on_open() { let temp = tempfile_with_header(&header_v3_with_autoclear_features(0xFFFF_FFFF_FFFF_FFFF)); let path = temp.as_path().to_owned(); assert_eq!( read_autoclear_features(&path), 0xFFFF_FFFF_FFFF_FFFF, "Autoclear features should be set initially" ); let file = temp.as_file().try_clone().unwrap(); { let _disk = QcowDisk::new(file, false, false, true, false).unwrap(); } assert_eq!( read_autoclear_features(&path), 0, "Autoclear features should be cleared after open for write" ); } #[test] fn autoclear_features_not_cleared_for_readonly() { let temp = tempfile_with_header(&header_v3_with_autoclear_features(0xFFFF_FFFF_FFFF_FFFF)); let path = temp.as_path().to_owned(); let readonly_file = OpenOptions::new() .read(true) .write(false) .open(&path) .unwrap(); let _disk = QcowDisk::new(readonly_file, false, false, true, false).unwrap(); drop(_disk); assert_eq!( read_autoclear_features(&path), 0xFFFF_FFFF_FFFF_FFFF, "Autoclear features should NOT be cleared for read-only files" ); } #[test] fn autoclear_features_v2_ignored() { let temp = tempfile_with_header(&valid_header_v2()); let disk = QcowDisk::new(temp.into_file(), false, false, true, false).unwrap(); let header = disk.metadata().header(); assert_eq!(header.version, 2); assert_eq!(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()); let err = try_open_header(&header).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()); let temp = tempfile_with_header(&header); let readonly_file = OpenOptions::new() .read(true) .write(false) .open(temp.as_path()) .unwrap(); let disk = QcowDisk::new(readonly_file, false, false, true, false) .expect("Corrupt image should be openable read-only"); assert!( disk.metadata().header().is_corrupt(), "Corrupt bit should be set" ); } #[test] fn resize_grow_within_l1() { use crate::disk_file::{DiskSize, Resizable}; let temp = QcowTempDisk::new(0x10_0000, None, false, true, false).unwrap(); let mut disk = QcowDisk::new( temp.as_file().try_clone().unwrap(), false, false, true, false, ) .unwrap(); let original_size = disk.logical_size().unwrap(); assert_eq!(original_size, 0x10_0000); disk.resize(original_size) .expect("Resize to same size should succeed"); assert_eq!(disk.logical_size().unwrap(), original_size); } #[test] fn resize_grow_with_l1_growth() { use crate::disk_file::{DiskSize, Resizable}; let initial_size = 1024 * 1024; // 1 MB let new_size = 600 * 1024 * 1024; // 600 MB let temp = QcowTempDisk::new(initial_size, None, false, true, false).unwrap(); let mut disk = QcowDisk::new( temp.as_file().try_clone().unwrap(), false, false, true, false, ) .unwrap(); let original_l1_size = disk.metadata().header().l1_size; assert_eq!(disk.logical_size().unwrap(), initial_size); let test_data = b"Hello, QCOW resize test!"; disk.write_all_at(0, test_data); disk.resize(new_size).expect("Resize should succeed"); assert_eq!(disk.logical_size().unwrap(), new_size); let new_l1_size = disk.metadata().header().l1_size; assert!(new_l1_size > original_l1_size); // Verify original data is still intact assert_eq!(&disk.read_all_at(0, test_data.len()), test_data); let new_offset = new_size - 0x10000; // 64KB before end let new_data = b"Data at new end!"; disk.write_all_at(new_offset, new_data); assert_eq!(&disk.read_all_at(new_offset, new_data.len()), new_data); } #[test] fn resize_shrink_fails() { use crate::async_io::DiskFileError; use crate::disk_file::{DiskSize, Resizable}; let temp = QcowTempDisk::new(0x10_0000, None, false, true, false).unwrap(); let mut disk = QcowDisk::new( temp.as_file().try_clone().unwrap(), false, false, true, false, ) .unwrap(); let original_size = disk.logical_size().unwrap(); let smaller_size = original_size / 2; let err = disk.resize(smaller_size).unwrap_err(); let inner = err .source_ref() .and_then(|s| s.downcast_ref::()) .expect("expected DiskFileError source"); assert!( matches!(inner, DiskFileError::ResizeError(io_err) if io_err.to_string().contains("shrinking")), "expected ResizeError describing shrink, got {inner:?}", ); assert_eq!(disk.logical_size().unwrap(), original_size); } #[test] fn resize_with_backing_file_fails() { use crate::disk_file::{DiskSize, Resizable}; let backing_size = 1024 * 1024; let backing = QcowTempDisk::new(backing_size, None, false, true, false).unwrap(); let backing_path = backing.path().to_str().unwrap().to_string(); let backing_config = BackingFileConfig { path: backing_path, format: Some(ImageType::Qcow2), }; let overlay = QcowTempDisk::new(backing_size, Some(&backing_config), false, true, false) .unwrap() .into_tempfile(); let mut disk = QcowDisk::new( overlay.as_file().try_clone().unwrap(), false, true, true, false, ) .unwrap(); assert_eq!(disk.logical_size().unwrap(), backing_size); let err = disk.resize(backing_size * 2).unwrap_err(); assert!(matches!(err.kind(), BlockErrorKind::UnsupportedFeature)); assert_eq!(disk.logical_size().unwrap(), backing_size); } } cloud-hypervisor-53.0/block/src/formats/qcow/qcow_raw_file.rs000066400000000000000000000443031522476750100244700ustar00rootroot00000000000000// Copyright 2018 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::fmt::Debug; use std::io::{self, Write}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::os::unix::fs::FileExt; use byteorder::{BigEndian, WriteBytesExt}; use vmm_sys_util::write_zeroes::WriteZeroesAt; use crate::aligned_file::AlignedFile; // Type aliases for the refcount read/write function pointers type RefcountReader = fn(&mut AlignedFile, u64, usize) -> io::Result>; type RefcountWriter = fn(&mut AlignedFile, u64, &[u64]) -> io::Result<()>; /// Big-endian file access trait. pub(super) trait BeUint: Sized + Copy { fn from_be_slice(bytes: &[u8]) -> u64; fn write_be(w: &mut W, val: Self) -> io::Result<()>; } impl BeUint for u8 { #[inline(always)] fn from_be_slice(bytes: &[u8]) -> u64 { bytes[0] as u64 } #[inline(always)] fn write_be(w: &mut W, val: Self) -> io::Result<()> { w.write_u8(val) } } impl BeUint for u16 { #[inline(always)] fn from_be_slice(bytes: &[u8]) -> u64 { u16::from_be_bytes([bytes[0], bytes[1]]) as u64 } #[inline(always)] fn write_be(w: &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 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 write_be(w: &mut W, val: Self) -> io::Result<()> { w.write_u64::(val) } } /// Read byte-aligned refcounts. fn read_refcount( file: &mut AlignedFile, offset: u64, count: usize, ) -> io::Result> { let bytes_per_entry = size_of::(); let mut data = vec![0u8; count * bytes_per_entry]; file.read_exact_at(&mut data, offset)?; Ok(data .chunks_exact(bytes_per_entry) .map(T::from_be_slice) .collect()) } /// Write byte-aligned refcounts. fn write_refcount>( file: &mut AlignedFile, offset: u64, table: &[u64], ) -> io::Result<()> where >::Error: Debug, { let bytes_per_entry = size_of::(); let mut buffer = Vec::with_capacity(table.len() * bytes_per_entry); for &val in table { let converted = T::try_from(val).expect("refcount values are validated on increment"); T::write_be(&mut buffer, converted)?; } file.write_all_at(&buffer, offset) } /// Read sub-byte refcounts. Bit 0 is the least significant bit. fn read_refcount_subbyte( file: &mut AlignedFile, offset: u64, 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_at(&mut bytes, offset)?; let mut table = vec![0u64; count]; for (i, val) in table.iter_mut().enumerate() { let byte_idx = i / entries_per_byte; let bit_offset = (i % entries_per_byte) * BITS; *val = (bytes[byte_idx] as u64 >> bit_offset) & mask; } Ok(table) } /// Write sub-byte refcounts. Bit 0 is the least significant bit. fn write_refcount_subbyte( file: &mut AlignedFile, offset: u64, table: &[u64], ) -> io::Result<()> { const { assert!(BITS == 1 || BITS == 2 || BITS == 4) }; let entries_per_byte = 8 / BITS; let mask = (1u64 << BITS) - 1; let mut buffer = Vec::with_capacity(table.len().div_ceil(entries_per_byte)); for chunk in table.chunks(entries_per_byte) { let mut byte = 0u8; for (i, &val) in chunk.iter().enumerate() { let bit_offset = i * BITS; byte |= ((val & mask) << bit_offset) as u8; } buffer.push(byte); } file.write_all_at(&buffer, offset) } /// A qcow file. Allows reading/writing clusters and appending clusters. #[derive(Debug)] pub(super) struct QcowRawFile { file: AlignedFile, cluster_size: u64, cluster_mask: u64, refcount_block_entries: u64, read_refcount_fn: RefcountReader, write_refcount_fn: RefcountWriter, } impl QcowRawFile { /// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not /// a power of two or refcount_bits is invalid. pub(super) fn from(file: AlignedFile, cluster_size: u64, refcount_bits: u64) -> Option { 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, }) } /// Reads `count` 64 bit offsets and returns them as a vector. /// `mask` optionally `&`s out some of the bits on the file. pub(super) fn read_pointer_table( &mut self, offset: u64, count: u64, mask: Option, ) -> io::Result> { let mut bytes = vec![0u8; count as usize * size_of::()]; self.file.read_exact_at(&mut bytes, offset)?; let m = mask.unwrap_or(u64::MAX); let table = bytes .as_chunks::<{ size_of::() }>() .0 .iter() .map(|c| u64::from_be_bytes(*c) & m) .collect(); Ok(table) } /// Reads a cluster's worth of 64 bit offsets and returns them as a vector. /// `mask` optionally `&`s out some of the bits on the file. pub(super) fn read_pointer_cluster( &mut self, offset: u64, mask: Option, ) -> io::Result> { let count = self.cluster_size / size_of::() as u64; self.read_pointer_table(offset, count, mask) } /// Writes a pointer table to `offset` in the file. /// Entries are computed on-the-fly by the callback. /// /// The callback may perform metadata I/O on this `QcowRawFile`, so all /// entries are materialized before the final positional write. pub(super) fn write_pointer_table<'a, T: Copy + 'a>( &mut self, offset: u64, entries: impl Iterator, mut f: impl FnMut(&mut QcowRawFile, T) -> io::Result, ) -> io::Result<()> { let mut buffer = Vec::with_capacity(entries.size_hint().0 * size_of::()); for addr in entries { let entry = f(self, *addr)?; buffer.extend_from_slice(&entry.to_be_bytes()); } self.file.write_all_at(&buffer, offset) } /// Writes a pointer table directly without transforming values. /// /// Uses the same materialize-then-write path as `write_pointer_table`. pub(super) fn write_pointer_table_direct<'a>( &mut self, offset: u64, entries: impl Iterator, ) -> io::Result<()> { let mut buffer = Vec::with_capacity(entries.size_hint().0 * size_of::()); for &entry in entries { buffer.extend_from_slice(&entry.to_be_bytes()); } self.file.write_all_at(&buffer, offset) } /// Read a refcount block from the file and returns a Vec containing the block. /// Always returns a cluster's worth of data. #[inline] pub(super) fn read_refcount_block(&mut self, offset: u64) -> io::Result> { (self.read_refcount_fn)(&mut self.file, offset, self.refcount_block_entries as usize) } /// Writes a refcount block to the file. #[inline] pub(super) fn write_refcount_block(&mut self, offset: u64, table: &[u64]) -> io::Result<()> { (self.write_refcount_fn)(&mut self.file, offset, table) } /// Allocates a new cluster at the end of the current file, return the address. pub(super) fn add_cluster_end( &mut self, max_valid_cluster_offset: u64, ) -> io::Result> { // Determine where the new end of the file should be and set_len, which // translates to truncate(2). let file_end: u64 = self.physical_size()?; let new_cluster_address: u64 = (file_end + self.cluster_size - 1) & !self.cluster_mask; if new_cluster_address > max_valid_cluster_offset { return Ok(None); } self.file.set_len(new_cluster_address + self.cluster_size)?; Ok(Some(new_cluster_address)) } /// Returns a reference to the underlying file. pub(super) fn file(&self) -> &AlignedFile { &self.file } /// Returns a mutable reference to the underlying file. pub(super) fn file_mut(&mut self) -> &mut AlignedFile { &mut self.file } /// Returns the size of the file's clusters. pub(super) fn cluster_size(&self) -> u64 { self.cluster_size } /// Returns the offset of `address` within a cluster. pub(super) fn cluster_offset(&self, address: u64) -> u64 { address & self.cluster_mask } /// Returns the base address of the cluster containing `address`. pub(super) fn cluster_address(&self, address: u64) -> u64 { address & !self.cluster_mask } /// Zeros out a cluster in the file. pub(super) fn zero_cluster(&mut self, address: u64) -> io::Result<()> { let cluster_size = self.cluster_size as usize; self.file.write_all_zeroes_at(address, cluster_size)?; Ok(()) } /// Writes pub(super) fn write_cluster(&mut self, address: u64, data: &[u8]) -> io::Result<()> { let cluster_size = self.cluster_size as usize; self.file.write_all_at(&data[0..cluster_size], address) } pub(super) fn physical_size(&self) -> io::Result { self.file.metadata().map(|m| m.len()) } } impl Clone for QcowRawFile { fn clone(&self) -> Self { QcowRawFile { file: self.file.try_clone().expect("QcowRawFile cloning failed"), cluster_size: self.cluster_size, cluster_mask: self.cluster_mask, refcount_block_entries: self.refcount_block_entries, read_refcount_fn: self.read_refcount_fn, write_refcount_fn: self.write_refcount_fn, } } } impl AsRawFd for QcowRawFile { fn as_raw_fd(&self) -> RawFd { self.file.as_raw_fd() } } impl AsFd for QcowRawFile { fn as_fd(&self) -> BorrowedFd<'_> { self.file.as_fd() } } #[cfg(test)] mod unit_tests { use std::io::Read; use std::os::unix::fs::FileExt; use vmm_sys_util::tempfile::TempFile; use super::*; fn be_bytes(entries: &[u64]) -> Vec { let mut v = Vec::with_capacity(size_of_val(entries)); for e in entries { v.extend_from_slice(&e.to_be_bytes()); } v } fn find_all(haystack: &[u8], needle: &[u8]) -> Vec { haystack .windows(needle.len()) .enumerate() .filter(|(_, w)| *w == needle) .map(|(i, _)| i) .collect() } const CLUSTER_SIZE: u64 = 0x10000; // 64 KiB const TARGET_OFFSET: u64 = 0x1000; // where the table must be written const FAR_OFFSET: u64 = 0x9000; // where the callback reads (refcount block) const FILE_LEN: u64 = 0x40000; // 256 KiB filler so all offsets are valid fn make_qcow_raw() -> (TempFile, QcowRawFile) { make_qcow_raw_bits(16) } fn make_qcow_raw_bits(refcount_bits: u64) -> (TempFile, QcowRawFile) { let temp_file = TempFile::new().unwrap(); temp_file.as_file().set_len(FILE_LEN).unwrap(); let file = temp_file.as_file().try_clone().unwrap(); let raw = AlignedFile::new(file, false); let qcow_raw = QcowRawFile::from(raw, CLUSTER_SIZE, refcount_bits).expect("QcowRawFile::from"); (temp_file, qcow_raw) } #[test] fn write_pointer_table_lands_at_offset_despite_callback_seek() { let (temp_file, mut qcow) = make_qcow_raw(); let entries: Vec = vec![0x1111_2222_3333_4444u64; 8]; // 64 bytes qcow.write_pointer_table(TARGET_OFFSET, entries.iter(), |q, addr| { let _ = q.read_refcount_block(FAR_OFFSET)?; Ok(addr) }) .expect("write_pointer_table"); let expected = be_bytes(&entries); let mut verify = temp_file.as_file().try_clone().unwrap(); let mut whole = Vec::new(); verify.read_to_end(&mut whole).unwrap(); let found_at = find_all(&whole, &expected); let mut at_target = vec![0u8; expected.len()]; verify.read_exact_at(&mut at_target, TARGET_OFFSET).unwrap(); assert_eq!( at_target, expected, "pointer table did NOT land at TARGET_OFFSET {TARGET_OFFSET:#x}; \ found matching bytes at {found_at:x?}" ); } #[test] fn write_pointer_table_direct_lands_at_offset() { let (temp_file, mut qcow) = make_qcow_raw(); let entries: Vec = vec![0xAAAA_BBBB_CCCC_DDDDu64; 8]; qcow.write_pointer_table_direct(TARGET_OFFSET, entries.iter()) .expect("write_pointer_table_direct"); let expected = be_bytes(&entries); let verify = temp_file.as_file().try_clone().unwrap(); let mut at_target = vec![0u8; expected.len()]; verify.read_exact_at(&mut at_target, TARGET_OFFSET).unwrap(); assert_eq!( at_target, expected, "write_pointer_table_direct did not land at {TARGET_OFFSET:#x}" ); } #[test] fn read_pointer_table_round_trips() { let (_temp_file, mut qcow) = make_qcow_raw(); let entries: Vec = vec![ 0x0000_0000_0000_0000, 0x0011_2233_4455_6677, 0x8899_aabb_ccdd_eeff, 0xffff_ffff_ffff_ffff, ]; qcow.write_pointer_table_direct(TARGET_OFFSET, entries.iter()) .expect("write_pointer_table_direct"); let read_back = qcow .read_pointer_table(TARGET_OFFSET, entries.len() as u64, None) .expect("read_pointer_table"); assert_eq!(read_back, entries); } #[test] fn read_pointer_table_applies_mask() { let (_temp_file, mut qcow) = make_qcow_raw(); let entries: Vec = vec![0xffff_ffff_ffff_ffffu64; 4]; let mask = 0x00ff_ffff_ffff_fe00u64; qcow.write_pointer_table_direct(TARGET_OFFSET, entries.iter()) .expect("write_pointer_table_direct"); let read_back = qcow .read_pointer_table(TARGET_OFFSET, entries.len() as u64, Some(mask)) .expect("read_pointer_table"); assert!(read_back.iter().all(|&e| e == mask)); } #[test] fn write_cluster_then_zero_cluster_round_trips() { let (temp_file, mut qcow) = make_qcow_raw(); let cluster_size = CLUSTER_SIZE as usize; let data: Vec = (0..cluster_size).map(|i| (i % 251) as u8).collect(); qcow.write_cluster(CLUSTER_SIZE, &data) .expect("write_cluster"); let verify = temp_file.as_file().try_clone().unwrap(); let mut buf = vec![0u8; cluster_size]; verify.read_exact_at(&mut buf, CLUSTER_SIZE).unwrap(); assert_eq!(buf, data); qcow.zero_cluster(CLUSTER_SIZE).expect("zero_cluster"); verify.read_exact_at(&mut buf, CLUSTER_SIZE).unwrap(); assert!(buf.iter().all(|&b| b == 0)); } #[test] fn refcount_block_round_trips() { let (_temp_file, mut qcow) = make_qcow_raw_bits(16); let count = qcow.refcount_block_entries as usize; let table: Vec = (0..count).map(|i| (i % 251) as u64).collect(); qcow.write_refcount_block(TARGET_OFFSET, &table) .expect("write_refcount_block"); let read_back = qcow .read_refcount_block(TARGET_OFFSET) .expect("read_refcount_block"); assert_eq!(read_back, table); } #[test] fn refcount_block_subbyte_round_trips() { let (_temp_file, mut qcow) = make_qcow_raw_bits(4); let count = qcow.refcount_block_entries as usize; let table: Vec = (0..count).map(|i| (i % 16) as u64).collect(); qcow.write_refcount_block(TARGET_OFFSET, &table) .expect("write_refcount_block"); let read_back = qcow .read_refcount_block(TARGET_OFFSET) .expect("read_refcount_block"); assert_eq!(read_back, table); } #[test] fn add_cluster_end_appends_aligned_cluster() { let (_temp_file, mut qcow) = make_qcow_raw(); let before = qcow.physical_size().unwrap(); let addr = qcow .add_cluster_end(u64::MAX) .expect("add_cluster_end") .expect("a cluster was allocated"); assert_eq!(addr % CLUSTER_SIZE, 0); assert!(addr >= before); assert_eq!(qcow.physical_size().unwrap(), addr + CLUSTER_SIZE); } #[test] fn add_cluster_end_respects_max_offset() { let (_temp_file, mut qcow) = make_qcow_raw(); assert!(qcow.add_cluster_end(0).unwrap().is_none()); } } cloud-hypervisor-53.0/block/src/formats/qcow/refcount.rs000066400000000000000000000226231522476750100234750ustar00rootroot00000000000000// Copyright 2018 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::{io, result}; use libc::EINVAL; use thiserror::Error; use super::qcow_raw_file::QcowRawFile; use super::vec_cache::{CacheMap, Cacheable, VecCache}; #[derive(Debug, Error)] pub enum Error { /// `EvictingCache` - Error writing a refblock from the cache to disk. #[error("Failed to write a refblock from the cache to disk")] EvictingRefCounts(#[source] io::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), /// `NeedNewCluster` - Handle this error by allocating a cluster and calling the function again. #[error("New cluster needs to be allocated for refcounts")] NeedNewCluster, /// `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(super) type Result = result::Result; /// Represents the refcount entries for an open qcow file. #[derive(Clone, Debug)] pub(super) struct RefCount { ref_table: VecCache, refcount_table_offset: u64, 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(super) fn new( raw_file: &mut QcowRawFile, refcount_table_offset: u64, 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, refcount_table_entries, None, )?); 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, refblock_cache: CacheMap::new(50), refcount_block_entries, cluster_size, max_valid_cluster_offset, max_refcount, refcount_bits, }) } /// Returns the number of refcounts per block. pub(super) fn refcounts_per_block(&self) -> u64 { self.refcount_block_entries } /// Returns the maximum valid cluster offset in the raw file for this refcount table. pub(super) fn max_valid_cluster_offset(&self) -> u64 { self.max_valid_cluster_offset } /// Returns `NeedNewCluster` if a new cluster needs to be allocated for refcounts. If an /// existing cluster needs to be read, `NeedCluster(addr)` is returned. The Caller should /// allocate a cluster or read the required one and call this function again with the cluster. /// On success, an optional address of a dropped cluster is returned. The dropped cluster can /// be reused for other purposes. pub(super) fn set_cluster_refcount( &mut self, raw_file: &mut QcowRawFile, cluster_address: u64, 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)?; // Fill the cache if this block isn't yet there. if !self.refblock_cache.contains_key(table_index) { // Need a new cluster if let Some((addr, table)) = new_cluster.take() { self.ref_table[table_index] = addr; let ref_table = &self.ref_table; self.refblock_cache .insert(table_index, table, |index, evicted| { raw_file.write_refcount_block(ref_table[index], evicted.get_values()) }) .map_err(Error::EvictingRefCounts)?; } else { if block_addr_disk == 0 { return Err(Error::NeedNewCluster); } return Err(Error::NeedCluster(block_addr_disk)); } } // Unwrap is safe here as the entry was filled directly above. 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() { self.ref_table[table_index] = addr; Some(block_addr_disk) } else { return Err(Error::NeedNewCluster); } }; self.refblock_cache.get_mut(table_index).unwrap()[block_index] = refcount; Ok(dropped_cluster) } /// Flush the dirty refcount blocks. This must be done before flushing the table that points to /// the blocks. pub(super) fn flush_blocks(&mut self, raw_file: &mut QcowRawFile) -> io::Result<()> { // Write out all dirty L2 tables. for (table_index, block) in self.refblock_cache.iter_mut().filter(|(_k, v)| v.dirty()) { let addr = self.ref_table[*table_index]; if addr != 0 { raw_file.write_refcount_block(addr, block.get_values())?; } else { return Err(io::Error::from_raw_os_error(EINVAL)); } block.mark_clean(); } Ok(()) } /// Flush the refcount table that keeps the address of the refcounts blocks. /// Returns true if the table changed since the previous `flush_table()` call. pub(super) fn flush_table(&mut self, raw_file: &mut QcowRawFile) -> io::Result { if self.ref_table.dirty() { raw_file .write_pointer_table_direct(self.refcount_table_offset, self.ref_table.iter())?; self.ref_table.mark_clean(); Ok(true) } else { Ok(false) } } /// Gets the refcount for a cluster with the given address. pub(super) fn get_cluster_refcount( &mut self, raw_file: &mut QcowRawFile, address: u64, ) -> 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 .read_refcount_block(block_addr_disk) .map_err(Error::ReadingRefCounts)?, ); let ref_table = &self.ref_table; self.refblock_cache .insert(table_index, table, |index, evicted| { raw_file.write_refcount_block(ref_table[index], evicted.get_values()) }) .map_err(Error::EvictingRefCounts)?; } Ok(self.refblock_cache.get(table_index).unwrap()[block_index]) } // Gets the address of the refcount block and the index into the block for the given address. fn get_refcount_index(&self, address: u64) -> (usize, usize) { let block_index = (address / self.cluster_size) % self.refcount_block_entries; let refcount_table_index = (address / self.cluster_size) / self.refcount_block_entries; (refcount_table_index as usize, block_index as usize) } } cloud-hypervisor-53.0/block/src/formats/qcow/util.rs000066400000000000000000000063461522476750100226310ustar00rootroot00000000000000// Copyright 2018 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. // // Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! Pure helper functions and constants for QCOW2 L1/L2 table entry //! manipulation and integer arithmetic. Shared across the `qcow` submodules. /// Nesting depth limit for disk formats that can open other disk files. pub(crate) const MAX_NESTING_DEPTH: u32 = 10; // bits 0-8 and 56-63 are reserved. pub(super) const L1_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00; pub(super) const L2_TABLE_OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00; // Flags pub(super) const ZERO_FLAG: u64 = 1 << 0; pub(super) const COMPRESSED_FLAG: u64 = 1 << 62; pub(super) const COMPRESSED_SECTOR_SIZE: u64 = 512; pub(super) const CLUSTER_USED_FLAG: u64 = 1 << 63; /// Check if L2 entry is empty (unallocated). pub(super) fn l2_entry_is_empty(l2_entry: u64) -> bool { l2_entry == 0 } /// Check bit 0 - only valid for standard clusters. pub(super) fn l2_entry_is_zero(l2_entry: u64) -> bool { l2_entry & ZERO_FLAG != 0 } /// Check if L2 entry refers to a compressed cluster. pub(super) fn l2_entry_is_compressed(l2_entry: u64) -> bool { l2_entry & COMPRESSED_FLAG != 0 } /// Get file offset and size of compressed cluster data. pub(super) fn l2_entry_compressed_cluster_layout(l2_entry: u64, cluster_bits: u32) -> (u64, usize) { let compressed_size_shift = 62 - (cluster_bits - 8); let compressed_size_mask = (1 << (cluster_bits - 8)) - 1; let compressed_cluster_addr = l2_entry & ((1 << compressed_size_shift) - 1); let nsectors = (l2_entry >> compressed_size_shift & compressed_size_mask) + 1; let compressed_cluster_size = ((nsectors * COMPRESSED_SECTOR_SIZE) - (compressed_cluster_addr & (COMPRESSED_SECTOR_SIZE - 1))) as usize; (compressed_cluster_addr, compressed_cluster_size) } /// Get file offset of standard (non-compressed) cluster. pub(super) fn l2_entry_std_cluster_addr(l2_entry: u64) -> u64 { l2_entry & L2_TABLE_OFFSET_MASK } /// Make L2 entry for standard (non-compressed) cluster. pub(super) fn l2_entry_make_std(cluster_addr: u64) -> u64 { (cluster_addr & L2_TABLE_OFFSET_MASK) | CLUSTER_USED_FLAG } /// Make L2 entry for preallocated zero cluster. pub(super) fn l2_entry_make_zero(cluster_addr: u64) -> u64 { (cluster_addr & L2_TABLE_OFFSET_MASK) | CLUSTER_USED_FLAG | ZERO_FLAG } /// Make L2 entry for an unallocated cluster that reads as logical zeros. pub(super) fn l2_entry_make_zero_plain() -> u64 { ZERO_FLAG } /// Make L1 entry with optional flags. pub(super) fn l1_entry_make(cluster_addr: u64, refcount_is_one: bool) -> u64 { (cluster_addr & L1_TABLE_OFFSET_MASK) | (refcount_is_one as u64 * CLUSTER_USED_FLAG) } /// Ceiling of the division of `dividend`/`divisor`. pub(super) fn div_round_up_u32(dividend: u32, divisor: u32) -> u32 { dividend / divisor + u32::from(!dividend.is_multiple_of(divisor)) } /// Ceiling of the division of `dividend`/`divisor`. pub(super) fn div_round_up_u64(dividend: u64, divisor: u64) -> u64 { dividend / divisor + u64::from(!dividend.is_multiple_of(divisor)) } cloud-hypervisor-53.0/block/src/formats/qcow/vec_cache.rs000066400000000000000000000134031522476750100235440ustar00rootroot00000000000000// Copyright 2018 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::collections::HashMap; use std::collections::hash_map::IterMut; use std::io; 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 /// it can be checked if they need to be committed to disk. pub(super) trait Cacheable { /// Used to check if the item needs to be written out or if it can be discarded. fn dirty(&self) -> bool; } #[derive(Clone, Debug)] /// Represents a vector that implements the `Cacheable` trait so it can be held in a cache. pub(super) struct VecCache { vec: Box<[T]>, dirty: bool, } impl VecCache { /// Creates a `VecCache` that can hold `count` elements. pub(super) fn new(count: usize) -> VecCache { VecCache { vec: vec![Default::default(); count].into_boxed_slice(), dirty: true, } } /// Creates a `VecCache` from the passed in `vec`. pub(super) fn from_vec(vec: Vec) -> VecCache { VecCache { vec: vec.into_boxed_slice(), dirty: false, } } pub(super) fn get(&self, index: I) -> Option<&>::Output> where I: SliceIndex<[T]>, { self.vec.get(index) } /// Gets a reference to the underlying vector. pub(super) fn get_values(&self) -> &[T] { &self.vec } /// Mark this cache element as clean. pub(super) fn mark_clean(&mut self) { self.dirty = false; } /// Returns the number of elements in the vector. pub(super) 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(super) 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 { fn dirty(&self) -> bool { self.dirty } } impl Index for VecCache { type Output = T; fn index(&self, index: usize) -> &T { self.vec.index(index) } } impl IndexMut for VecCache { fn index_mut(&mut self, index: usize) -> &mut T { self.dirty = true; self.vec.index_mut(index) } } impl Deref for VecCache { type Target = [T]; fn deref(&self) -> &[T] { &self.vec } } #[derive(Clone, Debug)] pub(super) struct CacheMap { capacity: usize, map: HashMap, } impl CacheMap { pub(super) fn new(capacity: usize) -> Self { CacheMap { capacity, map: HashMap::with_capacity(capacity), } } pub(super) fn contains_key(&self, key: usize) -> bool { self.map.contains_key(&key) } pub(super) fn get(&self, index: usize) -> Option<&T> { self.map.get(&index) } pub(super) fn get_mut(&mut self, index: usize) -> Option<&mut T> { self.map.get_mut(&index) } pub(super) fn iter_mut(&mut self) -> IterMut<'_, usize, T> { self.map.iter_mut() } // Check if the refblock cache is full and we need to evict. pub(super) fn insert(&mut self, index: usize, block: T, write_callback: F) -> io::Result<()> where F: FnOnce(usize, T) -> io::Result<()>, { 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) && evicted.dirty() { write_callback(to_evict, evicted)?; } } self.map.insert(index, block); Ok(()) } } #[cfg(test)] mod unit_tests { use super::*; struct NumCache(()); impl Cacheable for NumCache { fn dirty(&self) -> bool { true } } #[test] fn evicts_when_full() { let mut cache = CacheMap::::new(3); let mut evicted = None; cache .insert(0, NumCache(()), |index, _| { evicted = Some(index); Ok(()) }) .unwrap(); assert_eq!(evicted, None); cache .insert(1, NumCache(()), |index, _| { evicted = Some(index); Ok(()) }) .unwrap(); assert_eq!(evicted, None); cache .insert(2, NumCache(()), |index, _| { evicted = Some(index); Ok(()) }) .unwrap(); assert_eq!(evicted, None); cache .insert(3, NumCache(()), |index, _| { evicted = Some(index); Ok(()) }) .unwrap(); assert!(evicted.is_some()); // Check that three of the four items inserted are still there and that the most recently // inserted is one of them. let num_items = (0..=3).filter(|k| cache.contains_key(*k)).count(); assert_eq!(num_items, 3); assert!(cache.contains_key(3)); } } cloud-hypervisor-53.0/block/src/formats/raw/000077500000000000000000000000001522476750100211155ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/formats/raw/engine_aio.rs000066400000000000000000000112721522476750100235630ustar00rootroot00000000000000// Copyright © 2023 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause // // Copyright © 2023 Crusoe Energy Systems LLC // use std::os::unix::io::AsRawFd; use vmm_sys_util::eventfd::EventFd; use super::{operation_is_aligned, run_unaligned_operation}; use crate::async_io::{ AioDataIo, AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, }; use crate::error::{BlockError, BlockErrorKind, BlockResult}; use crate::sparse::{punch_hole, write_zeroes}; use crate::{AlignedFile, is_block_device}; pub(super) struct RawAio { raw_file: AlignedFile, data_io: AioDataIo, alignment: u64, is_block_device: bool, } impl RawAio { pub(super) fn new(raw_file: AlignedFile, queue_depth: u32) -> BlockResult { let data_io = AioDataIo::new(queue_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; let is_block_device = is_block_device(raw_file.as_raw_fd()); let alignment = raw_file.alignment() as u64; Ok(RawAio { raw_file, data_io, alignment, is_block_device, }) } } impl AsyncIo for RawAio { fn notifier(&self) -> &EventFd { self.data_io.notifier() } fn alignment(&self) -> u64 { self.alignment } fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> { let is_read = op.is_read(); if operation_is_aligned(&op, self.alignment) { let fd = self.raw_file.as_raw_fd(); return self.data_io.submit_operation(fd, op).map_err(|e| { if is_read { AsyncIoError::ReadVectored(e) } else { AsyncIoError::WriteVectored(e) } }); } let result = run_unaligned_operation(&self.raw_file, &mut op)?; self.data_io .inject_completion(AsyncIoCompletion::from_operation(op, result)); Ok(()) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { let fd = self.raw_file.as_raw_fd(); if let Some(user_data) = user_data { self.data_io .submit_fsync(fd, user_data) .map_err(AsyncIoError::Fsync)?; } else { // SAFETY: FFI call with a valid fd unsafe { libc::fsync(fd) }; } Ok(()) } fn next_completed_request(&mut self) -> Option { self.data_io.next_completion() } fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { // Linux AIO has no IOCB command for fallocate, so perform the // operation synchronously and signal completion via the completion // list, matching the pattern used by the sync backend (RawSync). punch_hole( self.raw_file.as_raw_fd(), self.is_block_device, offset, length, ) .map_err(AsyncIoError::PunchHole)?; self.data_io .inject_completion(AsyncIoCompletion::new(user_data, 0, None)); Ok(()) } fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { // Same as punch_hole(). write_zeroes( self.raw_file.as_raw_fd(), self.is_block_device, offset, length, ) .map_err(AsyncIoError::WriteZeroes)?; self.data_io .inject_completion(AsyncIoCompletion::new(user_data, 0, None)); Ok(()) } } #[cfg(test)] mod unit_tests { use vmm_sys_util::tempfile::TempFile; use super::*; use crate::formats::raw::tests; #[test] fn test_punch_hole() { let temp_file = TempFile::new().unwrap(); let mut file = temp_file.into_file(); let mut async_io = RawAio::new(AlignedFile::new(file.try_clone().unwrap(), false), 128).unwrap(); tests::test_punch_hole(&mut async_io, &mut file); } #[test] fn test_write_zeroes() { let temp_file = TempFile::new().unwrap(); let mut file = temp_file.into_file(); let mut async_io = RawAio::new(AlignedFile::new(file.try_clone().unwrap(), false), 128).unwrap(); tests::test_write_zeroes(&mut async_io, &mut file); } #[test] fn test_punch_hole_multiple_operations() { let temp_file = TempFile::new().unwrap(); let mut file = temp_file.into_file(); let mut async_io = RawAio::new(AlignedFile::new(file.try_clone().unwrap(), false), 128).unwrap(); tests::test_punch_hole_multiple_operations(&mut async_io, &mut file); } } cloud-hypervisor-53.0/block/src/formats/raw/engine_sync.rs000066400000000000000000000132241522476750100237660ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::collections::VecDeque; use std::io; use std::os::unix::io::AsRawFd; use vmm_sys_util::eventfd::EventFd; use super::{operation_is_aligned, run_unaligned_operation}; use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult}; use crate::sparse::{punch_hole, write_zeroes}; use crate::{AlignedFile, is_block_device}; pub(crate) struct RawSync { raw_file: AlignedFile, eventfd: EventFd, completion_list: VecDeque, alignment: u64, is_block_device: bool, } impl RawSync { pub(crate) fn new(raw_file: AlignedFile) -> Self { let is_block_device = is_block_device(raw_file.as_raw_fd()); let alignment = raw_file.alignment() as u64; RawSync { raw_file, eventfd: EventFd::new(libc::EFD_NONBLOCK).expect("Failed creating EventFd for RawFile"), completion_list: VecDeque::new(), alignment, is_block_device, } } } impl AsyncIo for RawSync { fn notifier(&self) -> &EventFd { &self.eventfd } fn alignment(&self) -> u64 { self.alignment } fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> { let is_read = op.is_read(); let result = if operation_is_aligned(&op, self.alignment) { let fd = self.raw_file.as_raw_fd(); let offset = op.offset(); let iovecs = op.iovecs(); let result = if is_read { // SAFETY: the memory pointed to by `iovecs` is backed by the op, // and valid for the kernel to write to by construction of // AsyncIoOperation. unsafe { libc::preadv( fd as libc::c_int, iovecs.as_ptr(), iovecs.len() as libc::c_int, offset, ) } } else { // SAFETY: the memory pointed to by `iovecs` is backed by the op, // and valid for the kernel to read from by construction of // AsyncIoOperation. unsafe { libc::pwritev( fd as libc::c_int, iovecs.as_ptr(), iovecs.len() as libc::c_int, offset, ) } }; if result < 0 { let error = io::Error::last_os_error(); return Err(if is_read { AsyncIoError::ReadVectored(error) } else { AsyncIoError::WriteVectored(error) }); } result as i32 } else { run_unaligned_operation(&self.raw_file, &mut op)? }; self.completion_list .push_back(AsyncIoCompletion::from_operation(op, result)); self.eventfd.write(1).unwrap(); Ok(()) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { // SAFETY: FFI call let result = unsafe { libc::fsync(self.raw_file.as_raw_fd() as libc::c_int) }; if result < 0 { return Err(AsyncIoError::Fsync(io::Error::last_os_error())); } if let Some(user_data) = user_data { self.completion_list .push_back(AsyncIoCompletion::new(user_data, result, None)); self.eventfd.write(1).unwrap(); } Ok(()) } fn next_completed_request(&mut self) -> Option { self.completion_list.pop_front() } fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { punch_hole( self.raw_file.as_raw_fd(), self.is_block_device, offset, length, ) .map_err(AsyncIoError::PunchHole)?; self.completion_list .push_back(AsyncIoCompletion::new(user_data, 0, None)); self.eventfd.write(1).unwrap(); Ok(()) } fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { write_zeroes( self.raw_file.as_raw_fd(), self.is_block_device, offset, length, ) .map_err(AsyncIoError::WriteZeroes)?; self.completion_list .push_back(AsyncIoCompletion::new(user_data, 0, None)); self.eventfd.write(1).unwrap(); Ok(()) } } #[cfg(test)] mod unit_tests { use vmm_sys_util::tempfile::TempFile; use super::*; use crate::formats::raw::tests; #[test] fn test_punch_hole() { let temp_file = TempFile::new().unwrap(); let mut file = temp_file.into_file(); let mut async_io = RawSync::new(AlignedFile::new(file.try_clone().unwrap(), false)); tests::test_punch_hole(&mut async_io, &mut file); } #[test] fn test_write_zeroes() { let temp_file = TempFile::new().unwrap(); let mut file = temp_file.into_file(); let mut async_io = RawSync::new(AlignedFile::new(file.try_clone().unwrap(), false)); tests::test_write_zeroes(&mut async_io, &mut file); } #[test] fn test_punch_hole_multiple_operations() { let temp_file = TempFile::new().unwrap(); let mut file = temp_file.into_file(); let mut async_io = RawSync::new(AlignedFile::new(file.try_clone().unwrap(), false)); tests::test_punch_hole_multiple_operations(&mut async_io, &mut file); } } cloud-hypervisor-53.0/block/src/formats/raw/engine_uring.rs000066400000000000000000000127501522476750100241410ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::os::unix::io::AsRawFd; use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE}; use vmm_sys_util::eventfd::EventFd; use super::{operation_is_aligned, run_unaligned_operation}; use crate::async_io::{ AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, UringDataIo, }; use crate::error::{BlockError, BlockErrorKind, BlockResult}; use crate::sparse::{blkdiscard, blkzeroout}; use crate::{AlignedFile, is_block_device}; pub(crate) struct RawAsync { raw_file: AlignedFile, data_io: UringDataIo, alignment: u64, is_block_device: bool, } impl RawAsync { pub(crate) fn new(raw_file: AlignedFile, ring_depth: u32) -> BlockResult { let data_io = UringDataIo::new(ring_depth).map_err(|e| BlockError::new(BlockErrorKind::Io, e))?; let is_block_device = is_block_device(raw_file.as_raw_fd()); let alignment = raw_file.alignment() as u64; Ok(RawAsync { raw_file, data_io, alignment, is_block_device, }) } } impl AsyncIo for RawAsync { fn notifier(&self) -> &EventFd { self.data_io.notifier() } fn alignment(&self) -> u64 { self.alignment } fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> { let is_read = op.is_read(); if operation_is_aligned(&op, self.alignment) { let fd = self.raw_file.as_raw_fd(); return self.data_io.submit_operation(fd, op).map_err(|e| { if is_read { AsyncIoError::ReadVectored(e) } else { AsyncIoError::WriteVectored(e) } }); } let result = run_unaligned_operation(&self.raw_file, &mut op)?; self.data_io .inject_completion(AsyncIoCompletion::from_operation(op, result)); Ok(()) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { let fd = self.raw_file.as_raw_fd(); if let Some(user_data) = user_data { self.data_io .submit_fsync(fd, user_data) .map_err(AsyncIoError::Fsync)?; } else { // SAFETY: FFI call with a valid fd unsafe { libc::fsync(fd) }; } Ok(()) } fn next_completed_request(&mut self) -> Option { self.data_io.next_completion() } fn batch_requests_enabled(&self) -> bool { true } fn submit_batch_requests(&mut self, batch_request: Vec) -> AsyncIoResult<()> { if self.alignment != 0 { let mut aligned_batch = Vec::with_capacity(batch_request.len()); for mut op in batch_request { if operation_is_aligned(&op, self.alignment) { aligned_batch.push(op); } else { let result = run_unaligned_operation(&self.raw_file, &mut op)?; self.data_io .inject_completion(AsyncIoCompletion::from_operation(op, result)); } } if aligned_batch.is_empty() { return Ok(()); } return self .data_io .submit_batch(self.raw_file.as_raw_fd(), aligned_batch) .map_err(AsyncIoError::SubmitBatchRequests); } self.data_io .submit_batch(self.raw_file.as_raw_fd(), batch_request) .map_err(AsyncIoError::SubmitBatchRequests) } fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { // Some block devices don't support fallocate(). Use ioctl instead. The assumption is that // this happens rarely and we don't need to introduce unnecessary complexity by submitting // a fallocate request, reaping ENOTSUPP in the completion routine, and reissuing the // request with an ioctl. if self.is_block_device { blkdiscard(self.raw_file.as_raw_fd(), offset, length) .map_err(AsyncIoError::PunchHole)?; // Deliver the completion through the normal io_uring path by // queuing a NOP carrying `user_data`. The registered eventfd will // fire when it completes, just like any other request. return self .data_io .submit_nop(user_data) .map_err(AsyncIoError::PunchHole); } let mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE; self.data_io .submit_fallocate(self.raw_file.as_raw_fd(), offset, length, mode, user_data) .map_err(AsyncIoError::PunchHole) } fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> { // Same rationale as punch_hole(). if self.is_block_device { blkzeroout(self.raw_file.as_raw_fd(), offset, length) .map_err(AsyncIoError::WriteZeroes)?; return self .data_io .submit_nop(user_data) .map_err(AsyncIoError::WriteZeroes); } let mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE; self.data_io .submit_fallocate(self.raw_file.as_raw_fd(), offset, length, mode, user_data) .map_err(AsyncIoError::WriteZeroes) } } cloud-hypervisor-53.0/block/src/formats/raw/mod.rs000066400000000000000000000237731522476750100222560ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! Raw disk image format. //! //! Provides [`RawDisk`], the `DiskFile` wrapper for flat disk images //! with no metadata or copy on write layer. use std::fs::File; use std::io; use std::os::unix::fs::FileTypeExt; use std::os::unix::io::AsRawFd; use log::warn; use self::engine_aio::RawAio; use self::engine_sync::RawSync; #[cfg(feature = "io_uring")] use self::engine_uring::RawAsync; use crate::async_io::{ AsyncIo, AsyncIoError, AsyncIoOperation, AsyncIoResult, BorrowedDiskFd, DiskFileError, }; use crate::error::{BlockError, BlockErrorKind, BlockResult}; use crate::{AlignedFile, DiskTopology, disk_file, probe_sparse_support, query_device_size}; mod engine_aio; pub(crate) mod engine_sync; #[cfg(feature = "io_uring")] pub(crate) mod engine_uring; #[cfg(test)] mod tests; /// Selects which async I/O backend a `RawDisk` uses. #[derive(Clone, Copy, Debug, PartialEq)] pub enum RawBackend { /// Blocking I/O where the caller waits for completion. Sync, /// Modern asynchronous I/O using shared submission and completion /// rings for lower overhead operation dispatch and completion handling. #[cfg(feature = "io_uring")] IoUring, /// Legacy asynchronous I/O where requests are handed to the kernel /// and completions are collected later. Aio, } /// Unified DiskFile wrapper for raw disk images. /// /// Owns the underlying file and delegates async I/O creation to the /// backend selected at construction time via [`RawBackend`]. #[derive(Debug)] pub struct RawDisk { file: File, backend: RawBackend, direct: bool, } impl RawDisk { pub fn new(file: File, backend: RawBackend, direct: bool) -> Self { Self { file, backend, direct, } } } impl disk_file::DiskSize for RawDisk { fn logical_size(&self) -> BlockResult { 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, direct: self.direct, })) } fn create_async_io(&self, ring_depth: u32) -> BlockResult> { let file = self .file .try_clone() .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?; let raw_file = AlignedFile::new(file, self.direct); match self.backend { RawBackend::Sync => Ok(Box::new(RawSync::new(raw_file))), #[cfg(feature = "io_uring")] RawBackend::IoUring => Ok(Box::new(RawAsync::new(raw_file, ring_depth)?)), RawBackend::Aio => Ok(Box::new(RawAio::new(raw_file, ring_depth)?)), } } } /// True when `op` satisfies `alignment` and can go straight to the kernel. fn operation_is_aligned(op: &AsyncIoOperation, alignment: u64) -> bool { if alignment == 0 { return true; } if !(op.offset() as u64).is_multiple_of(alignment) { return false; } op.iovecs().iter().all(|iov| { (iov.iov_base as u64).is_multiple_of(alignment) && (iov.iov_len as u64).is_multiple_of(alignment) }) } /// Runs an unaligned O_DIRECT operation synchronously through `aligned_file`. fn run_unaligned_operation( aligned_file: &AlignedFile, op: &mut AsyncIoOperation, ) -> AsyncIoResult { let offset = op.offset() as u64; let total_len = op.total_len(); if op.is_read() { let n = aligned_file .read_unaligned(offset, total_len, |data| op.write_bytes_at(0, data)) .map_err(AsyncIoError::ReadVectored)?; Ok(n as i32) } else { let n = aligned_file .write_unaligned(offset, total_len, |data| op.read_bytes_at(0, data)) .map_err(AsyncIoError::WriteVectored)?; Ok(n as i32) } } #[cfg(test)] mod unit_tests { use std::fs::File; use vmm_sys_util::tempfile::TempFile; use super::*; use crate::async_io::AsyncIo; use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize, Resizable}; const TEST_SIZE: u64 = 0x1122_3344; fn make_raw_file() -> File { let file: File = TempFile::new().unwrap().into_file(); file.set_len(TEST_SIZE).unwrap(); file } #[test] fn new_sync_returns_correct_size() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::Sync, false); assert_eq!(disk.logical_size().unwrap(), TEST_SIZE); } fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_backend: RawBackend) { let io: Box = disk.create_async_io(128).unwrap(); cfg_if::cfg_if! { if #[cfg(feature = "io_uring")] { let expected_batch_requests = expect_backend == RawBackend::IoUring; } else { let _ = expect_backend; let expected_batch_requests = false; } } assert_eq!(io.batch_requests_enabled(), expected_batch_requests); } fn assert_sync_backend(disk: &RawDisk) { assert_eq!(disk.backend, RawBackend::Sync); assert_async_io_from_dyn(disk, RawBackend::Sync); } fn assert_aio_backend(disk: &RawDisk) { assert_eq!(disk.backend, RawBackend::Aio); assert_async_io_from_dyn(disk, RawBackend::Aio); } #[cfg(feature = "io_uring")] fn assert_io_uring_backend(disk: &RawDisk) { assert_eq!(disk.backend, RawBackend::IoUring); assert_async_io_from_dyn(disk, RawBackend::IoUring); } #[test] fn sync_backend_disables_batch_requests() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::Sync, false); assert_sync_backend(&disk); } #[test] fn aio_backend_disables_batch_requests() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::Aio, false); assert_aio_backend(&disk); } #[cfg(feature = "io_uring")] #[test] fn io_uring_backend_enables_batch_requests() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::IoUring, false); assert_io_uring_backend(&disk); } fn assert_try_clone(disk: &RawDisk, expect_backend: RawBackend) { let cloned = disk.try_clone().unwrap(); assert_async_io_from_dyn(cloned.as_ref(), expect_backend); } #[test] fn try_clone_preserves_sync_backend() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::Sync, false); assert_try_clone(&disk, RawBackend::Sync); } #[test] fn try_clone_preserves_aio_backend() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::Aio, false); assert_try_clone(&disk, RawBackend::Aio); } #[cfg(feature = "io_uring")] #[test] fn try_clone_preserves_io_uring_backend() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::IoUring, false); assert_try_clone(&disk, RawBackend::IoUring); } #[test] fn resize_changes_file_size() { let file = make_raw_file(); let mut disk = RawDisk::new(file, RawBackend::Aio, false); let new_size = TEST_SIZE * 2; disk.resize(new_size).unwrap(); assert_eq!(disk.logical_size().unwrap(), new_size); } #[test] fn physical_size_reports_allocated_blocks() { let file = make_raw_file(); let disk = RawDisk::new(file, RawBackend::Aio, false); // Sparse file: physical size is less than logical size. assert!(disk.physical_size().unwrap() < disk.logical_size().unwrap()); } } cloud-hypervisor-53.0/block/src/formats/raw/tests.rs000066400000000000000000000133021522476750100226240ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! Shared test helpers for [`AsyncIo`] backends. //! //! Each helper takes a `&mut dyn AsyncIo` together with the [`File`] handle //! that backs the I/O object, so the same logic exercises every backend with //! only the constructor differing. use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; use crate::async_io::{AsyncIo, AsyncIoError}; fn next_completion(async_io: &mut dyn AsyncIo) -> (u64, i32) { let completion = async_io.next_completed_request().expect("No completion"); (completion.user_data, completion.result) } /// Tests punching a hole in the middle of a 4 MB file and verifying data /// integrity around the hole. pub fn test_punch_hole(async_io: &mut dyn AsyncIo, file: &mut File) { // Write 4MB of data let data = vec![0xAA; 4 * 1024 * 1024]; file.write_all(&data).unwrap(); file.sync_all().unwrap(); // Punch hole in the middle (1MB at offset 1MB) let offset = 1024 * 1024; let length = 1024 * 1024; async_io.punch_hole(offset, length, 1).unwrap(); // Check completion let (user_data, result) = next_completion(async_io); assert_eq!(user_data, 1); assert_eq!(result, 0); // Verify the hole reads as zeros file.seek(SeekFrom::Start(offset)).unwrap(); let mut read_buf = vec![0; length as usize]; file.read_exact(&mut read_buf).unwrap(); assert!( read_buf.iter().all(|&b| b == 0), "Punched hole should read as zeros" ); // Verify data before hole is intact file.seek(SeekFrom::Start(0)).unwrap(); let mut read_buf = vec![0; 1024]; file.read_exact(&mut read_buf).unwrap(); assert!( read_buf.iter().all(|&b| b == 0xAA), "Data before hole should be intact" ); // Verify data after hole is intact file.seek(SeekFrom::Start(offset + length)).unwrap(); let mut read_buf = vec![0; 1024]; file.read_exact(&mut read_buf).unwrap(); assert!( read_buf.iter().all(|&b| b == 0xAA), "Data after hole should be intact" ); } /// Tests writing zeroes to a 512 KB region inside a 4 MB file and verifying /// surrounding data is preserved. Gracefully skips when the filesystem does /// not support `FALLOC_FL_ZERO_RANGE`. pub fn test_write_zeroes(async_io: &mut dyn AsyncIo, file: &mut File) { // Write 4MB of data let data = vec![0xBB; 4 * 1024 * 1024]; file.write_all(&data).unwrap(); file.sync_all().unwrap(); // Write zeros in the middle (512KB at offset 2MB) let offset = 2 * 1024 * 1024; let length = 512 * 1024; let write_zeroes_result = async_io.write_zeroes(offset, length, 2); // FALLOC_FL_ZERO_RANGE might not be supported on all filesystems (e.g., tmpfs) // If it fails with ENOTSUP, skip the test if let Err(AsyncIoError::WriteZeroes(ref e)) = write_zeroes_result && (e.raw_os_error() == Some(libc::EOPNOTSUPP) || e.raw_os_error() == Some(libc::ENOTSUP)) { eprintln!("Skipping test_write_zeroes: filesystem doesn't support FALLOC_FL_ZERO_RANGE"); return; } write_zeroes_result.unwrap(); // Check completion let (user_data, result) = next_completion(async_io); assert_eq!(user_data, 2); assert_eq!(result, 0); // Verify the zeroed region reads as zeros file.seek(SeekFrom::Start(offset)).unwrap(); let mut read_buf = vec![0; length as usize]; file.read_exact(&mut read_buf).unwrap(); assert!( read_buf.iter().all(|&b| b == 0), "Zeroed region should read as zeros" ); // Verify data before zeroed region is intact file.seek(SeekFrom::Start(offset - 1024)).unwrap(); let mut read_buf = vec![0; 1024]; file.read_exact(&mut read_buf).unwrap(); assert!( read_buf.iter().all(|&b| b == 0xBB), "Data before zeroed region should be intact" ); // Verify data after zeroed region is intact file.seek(SeekFrom::Start(offset + length)).unwrap(); let mut read_buf = vec![0; 1024]; file.read_exact(&mut read_buf).unwrap(); assert!( read_buf.iter().all(|&b| b == 0xBB), "Data after zeroed region should be intact" ); } /// Tests punching multiple holes in an 8 MB file and verifying each hole /// independently reads as zeroes. pub fn test_punch_hole_multiple_operations(async_io: &mut dyn AsyncIo, file: &mut File) { // Write 8MB of data let data = vec![0xCC; 8 * 1024 * 1024]; file.write_all(&data).unwrap(); file.sync_all().unwrap(); // Punch multiple holes async_io.punch_hole(1024 * 1024, 512 * 1024, 10).unwrap(); async_io .punch_hole(3 * 1024 * 1024, 512 * 1024, 11) .unwrap(); async_io .punch_hole(5 * 1024 * 1024, 512 * 1024, 12) .unwrap(); // Check all completions let (user_data, result) = next_completion(async_io); assert_eq!(user_data, 10); assert_eq!(result, 0); let (user_data, result) = next_completion(async_io); assert_eq!(user_data, 11); assert_eq!(result, 0); let (user_data, result) = next_completion(async_io); assert_eq!(user_data, 12); assert_eq!(result, 0); // Verify all holes read as zeros file.seek(SeekFrom::Start(1024 * 1024)).unwrap(); let mut read_buf = vec![0; 512 * 1024]; file.read_exact(&mut read_buf).unwrap(); assert!(read_buf.iter().all(|&b| b == 0)); file.seek(SeekFrom::Start(3 * 1024 * 1024)).unwrap(); file.read_exact(&mut read_buf).unwrap(); assert!(read_buf.iter().all(|&b| b == 0)); file.seek(SeekFrom::Start(5 * 1024 * 1024)).unwrap(); file.read_exact(&mut read_buf).unwrap(); assert!(read_buf.iter().all(|&b| b == 0)); } cloud-hypervisor-53.0/block/src/formats/vhd/000077500000000000000000000000001522476750100211055ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/formats/vhd/engine_sync.rs000066400000000000000000000031531522476750100237560ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 use std::io; use vmm_sys_util::eventfd::EventFd; use crate::AlignedFile; use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult}; use crate::formats::raw::engine_sync::RawSync; pub(super) struct FixedVhdSync { raw_file_sync: RawSync, size: u64, } impl FixedVhdSync { pub(super) fn new(raw_file: AlignedFile, size: u64) -> Self { FixedVhdSync { raw_file_sync: RawSync::new(raw_file), size, } } } impl AsyncIo for FixedVhdSync { fn notifier(&self) -> &EventFd { self.raw_file_sync.notifier() } fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> { op.validate_bounds(self.size)?; self.raw_file_sync.submit_data_operation(op) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { self.raw_file_sync.fsync(user_data) } fn next_completed_request(&mut self) -> Option { self.raw_file_sync.next_completed_request() } fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { Err(AsyncIoError::PunchHole(io::Error::other( "punch_hole not supported for fixed VHD", ))) } fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { Err(AsyncIoError::WriteZeroes(io::Error::other( "write_zeroes not supported for fixed VHD", ))) } } cloud-hypervisor-53.0/block/src/formats/vhd/engine_uring.rs000066400000000000000000000040571522476750100241320ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 use std::io; use vmm_sys_util::eventfd::EventFd; use crate::AlignedFile; use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult}; use crate::error::BlockResult; use crate::formats::raw::engine_uring::RawAsync; pub(super) struct FixedVhdAsync { raw_file_async: RawAsync, size: u64, } impl FixedVhdAsync { pub(super) fn new(raw_file: AlignedFile, ring_depth: u32, size: u64) -> BlockResult { let raw_file_async = RawAsync::new(raw_file, ring_depth)?; Ok(FixedVhdAsync { raw_file_async, size, }) } } impl AsyncIo for FixedVhdAsync { fn notifier(&self) -> &EventFd { self.raw_file_async.notifier() } fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> { op.validate_bounds(self.size)?; self.raw_file_async.submit_data_operation(op) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { self.raw_file_async.fsync(user_data) } fn next_completed_request(&mut self) -> Option { self.raw_file_async.next_completed_request() } fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { Err(AsyncIoError::PunchHole(io::Error::other( "punch_hole not supported for fixed VHD", ))) } fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { Err(AsyncIoError::WriteZeroes(io::Error::other( "write_zeroes not supported for fixed VHD", ))) } fn batch_requests_enabled(&self) -> bool { true } fn submit_batch_requests(&mut self, batch_request: Vec) -> AsyncIoResult<()> { for op in &batch_request { op.validate_bounds(self.size)?; } self.raw_file_async.submit_batch_requests(batch_request) } } cloud-hypervisor-53.0/block/src/formats/vhd/fixed.rs000066400000000000000000000023131522476750100225510ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::fs::File; use std::io; use std::os::unix::io::{AsRawFd, RawFd}; use super::footer::VhdFooter; #[derive(Debug)] pub(super) struct FixedVhd { file: File, size: u64, } impl FixedVhd { pub(super) fn new(mut file: File) -> io::Result { let footer = VhdFooter::new(&mut file)?; Ok(Self { file, size: footer.current_size(), }) } pub(crate) fn file(&self) -> &File { &self.file } } impl AsRawFd for FixedVhd { fn as_raw_fd(&self) -> RawFd { self.file.as_raw_fd() } } impl FixedVhd { pub(crate) fn logical_size(&self) -> Result { Ok(self.size) } /// Returns the physical size of the underlying file. pub(crate) fn physical_size(&self) -> Result { self.file .metadata() .map(|m| m.len()) .map_err(crate::Error::GetFileMetadata) } } impl Clone for FixedVhd { fn clone(&self) -> Self { Self { file: self.file.try_clone().expect("FixedVhd cloning failed"), size: self.size, } } } cloud-hypervisor-53.0/block/src/formats/vhd/footer.rs000066400000000000000000000204421522476750100227530ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::fs::File; use std::io; use std::os::unix::fs::FileExt; use crate::{AlignedFile, query_device_size}; // Production code uses: cookie, file_format_version, data_offset, // current_size, disk_type. The remaining fields are parsed for VHD // spec completeness and exercised only by unit tests. #[derive(Clone, Copy)] #[cfg_attr(not(test), expect(dead_code))] pub(super) struct VhdFooter { cookie: u64, features: u32, file_format_version: u32, data_offset: u64, time_stamp: u32, creator_application: u32, creator_version: u32, creator_host_os: u32, original_size: u64, current_size: u64, disk_geometry: u32, disk_type: u32, checksum: u32, unique_id: u128, saved_state: u8, } impl VhdFooter { pub(super) fn new(file: &mut File) -> io::Result { let aligned = AlignedFile::new(file.try_clone()?, true); let size = query_device_size(file)?.0; let footer_offset = size.checked_sub(512).ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "file too small for VHD footer") })?; let mut sector = [0u8; 512]; aligned.read_exact_at(&mut sector, footer_offset)?; Ok(VhdFooter { cookie: u64::from_be_bytes(sector[0..8].try_into().unwrap()), features: u32::from_be_bytes(sector[8..12].try_into().unwrap()), file_format_version: u32::from_be_bytes(sector[12..16].try_into().unwrap()), data_offset: u64::from_be_bytes(sector[16..24].try_into().unwrap()), time_stamp: u32::from_be_bytes(sector[24..28].try_into().unwrap()), creator_application: u32::from_be_bytes(sector[28..32].try_into().unwrap()), creator_version: u32::from_be_bytes(sector[32..36].try_into().unwrap()), creator_host_os: u32::from_be_bytes(sector[36..40].try_into().unwrap()), original_size: u64::from_be_bytes(sector[40..48].try_into().unwrap()), current_size: u64::from_be_bytes(sector[48..56].try_into().unwrap()), disk_geometry: u32::from_be_bytes(sector[56..60].try_into().unwrap()), disk_type: u32::from_be_bytes(sector[60..64].try_into().unwrap()), checksum: u32::from_be_bytes(sector[64..68].try_into().unwrap()), unique_id: u128::from_be_bytes(sector[68..84].try_into().unwrap()), saved_state: u8::from_be_bytes(sector[84..85].try_into().unwrap()), }) } pub(super) fn cookie(&self) -> u64 { self.cookie } #[cfg(test)] pub fn features(&self) -> u32 { self.features } pub(super) fn file_format_version(&self) -> u32 { self.file_format_version } pub(super) fn data_offset(&self) -> u64 { self.data_offset } #[cfg(test)] pub fn time_stamp(&self) -> u32 { self.time_stamp } #[cfg(test)] pub fn creator_application(&self) -> u32 { self.creator_application } #[cfg(test)] pub fn creator_version(&self) -> u32 { self.creator_version } #[cfg(test)] pub fn creator_host_os(&self) -> u32 { self.creator_host_os } #[cfg(test)] pub fn original_size(&self) -> u64 { self.original_size } pub(super) fn current_size(&self) -> u64 { self.current_size } #[cfg(test)] pub fn disk_geometry(&self) -> u32 { self.disk_geometry } pub(super) fn disk_type(&self) -> u32 { self.disk_type } #[cfg(test)] pub fn checksum(&self) -> u32 { self.checksum } #[cfg(test)] pub fn unique_id(&self) -> u128 { self.unique_id } #[cfg(test)] pub fn saved_state(&self) -> u8 { self.saved_state } } /// Determine image type through file parsing. pub fn is_fixed_vhd(f: &mut File) -> io::Result { let footer = VhdFooter::new(f)?; // "conectix" => 0x636f6e6563746978 Ok(footer.cookie() == 0x636f6e6563746978 && footer.file_format_version() == 0x0001_0000 && footer.data_offset() == 0xffff_ffff_ffff_ffff && footer.disk_type() == 0x2) } #[cfg(test)] mod unit_tests { use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use vmm_sys_util::tempfile::TempFile; use super::{VhdFooter, is_fixed_vhd}; fn valid_fixed_vhd_footer() -> Vec { vec![ 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, 0x10, 0x00, 0x00, 0x00, // original size 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, // 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, 0xa4, 0xab, 0xd0, 0x69, 0x45, 0x2b, 0xf2, 0x23, // unique id 0x00, // saved state ] } fn valid_dynamic_vhd_footer() -> Vec { vec![ 0x63, 0x6f, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x78, // cookie 0x00, 0x00, 0x00, 0x02, // features 0x00, 0x01, 0x00, 0x00, // file format version 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 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, 0x10, 0x00, 0x00, 0x00, // original size 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, // current size 0x11, 0xe0, 0x10, 0x3f, // disk geometry 0x00, 0x00, 0x00, 0x03, // disk type 0x00, 0x00, 0x00, 0x00, // checksum 0x98, 0x7b, 0xb1, 0xcd, 0x84, 0x14, 0x41, 0xfc, 0xa4, 0xab, 0xd0, 0x69, 0x45, 0x2b, 0xf2, 0x23, // unique id 0x00, // saved state ] } fn with_file(footer: &[u8], mut testfn: F) where F: FnMut(File), { let mut disk_file: File = TempFile::new().unwrap().into_file(); disk_file.set_len(0x1000_0200).unwrap(); disk_file.seek(SeekFrom::Start(0x1000_0000)).unwrap(); disk_file.write_all(footer).unwrap(); testfn(disk_file); // File closed when the function exits. } #[test] fn test_check_vhd_footer() { with_file(&valid_fixed_vhd_footer(), |mut file: File| { let vhd_footer = VhdFooter::new(&mut file).expect("Failed to create VHD footer"); assert_eq!(vhd_footer.cookie(), 0x636f_6e65_6374_6978); assert_eq!(vhd_footer.features(), 0x0000_0002); assert_eq!(vhd_footer.file_format_version(), 0x0001_0000); assert_eq!(vhd_footer.data_offset(), 0xffff_ffff_ffff_ffff); assert_eq!(vhd_footer.time_stamp(), 0x27a6_a65d); assert_eq!(vhd_footer.creator_application(), 0x7165_6d75); assert_eq!(vhd_footer.creator_version(), 0x0005_0003); assert_eq!(vhd_footer.creator_host_os(), 0x5769_326b); assert_eq!(vhd_footer.original_size(), 0x0000_0000_1000_0000); assert_eq!(vhd_footer.current_size(), 0x0000_0000_1000_0000); assert_eq!(vhd_footer.disk_geometry(), 0x11e0_103f); assert_eq!(vhd_footer.disk_type(), 0x0000_0002); assert_eq!(vhd_footer.checksum(), 0x0000_0000); assert_eq!( vhd_footer.unique_id(), 0x987b_b1cd_8414_41fc_a4ab_d069_452b_f223 ); assert_eq!(vhd_footer.saved_state(), 0x00); }); } #[test] fn test_is_fixed_vhd() { with_file(&valid_fixed_vhd_footer(), |mut file: File| { assert!(is_fixed_vhd(&mut file).unwrap()); }); } #[test] fn test_is_not_fixed_vhd() { with_file(&valid_dynamic_vhd_footer(), |mut file: File| { assert!(!(is_fixed_vhd(&mut file).unwrap())); }); } } cloud-hypervisor-53.0/block/src/formats/vhd/mod.rs000066400000000000000000000256071522476750100222440ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 //! Fixed VHD disk image format. //! //! Provides [`VhdDisk`], the `DiskFile` wrapper for fixed size VHD //! images. mod engine_sync; #[cfg(feature = "io_uring")] mod engine_uring; mod fixed; mod footer; use std::fs::File; use std::io; use std::os::unix::io::AsRawFd; pub use footer::is_fixed_vhd; use log::warn; use self::engine_sync::FixedVhdSync; #[cfg(feature = "io_uring")] use self::engine_uring::FixedVhdAsync; use self::fixed::FixedVhd; use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; use crate::disk_file::DiskSize; use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; use crate::{AlignedFile, DiskTopology, Error, disk_file}; #[derive(Debug)] pub struct VhdDisk { inner: FixedVhd, use_io_uring: bool, direct: bool, } impl VhdDisk { pub fn new(file: File, use_io_uring: bool, direct: bool) -> BlockResult { #[cfg(not(feature = "io_uring"))] if use_io_uring { return Err(BlockError::new( BlockErrorKind::UnsupportedFeature, DiskFileError::NewAsyncIo(io::Error::other( "io_uring requested but feature is not enabled", )), )); } Ok(Self { inner: FixedVhd::new(file).map_err(|e| BlockError::from(e).with_op(ErrorOp::Open))?, use_io_uring, direct, }) } } impl disk_file::DiskSize for VhdDisk { fn logical_size(&self) -> BlockResult { self.inner .logical_size() .map_err(|e| BlockError::new(BlockErrorKind::Io, e)) } } impl disk_file::PhysicalSize for VhdDisk { 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 VhdDisk { fn fd(&self) -> BorrowedDiskFd<'_> { BorrowedDiskFd::new(self.inner.as_raw_fd()) } } impl disk_file::Geometry for VhdDisk { fn topology(&self) -> DiskTopology { DiskTopology::probe(self.inner.file()).unwrap_or_else(|_| { warn!("Unable to get device topology. Using default topology"); DiskTopology::default() }) } } impl disk_file::SparseCapable for VhdDisk {} impl disk_file::Resizable for VhdDisk { fn resize(&mut self, _size: u64) -> BlockResult<()> { Err(BlockError::new( BlockErrorKind::UnsupportedFeature, DiskFileError::ResizeError(io::Error::other("resize not supported for fixed VHD")), ) .with_op(ErrorOp::Resize)) } } impl disk_file::DiskFile for VhdDisk {} impl disk_file::AsyncDiskFile for VhdDisk { fn try_clone(&self) -> BlockResult> { Ok(Box::new(VhdDisk { inner: self.inner.clone(), use_io_uring: self.use_io_uring, direct: self.direct, })) } fn create_async_io(&self, ring_depth: u32) -> BlockResult> { let size = self.logical_size()?; let file = self.inner.file().try_clone().map_err(|e| { BlockError::new(BlockErrorKind::Io, DiskFileError::NewAsyncIo(e)).with_op(ErrorOp::Open) })?; let raw_file = AlignedFile::new(file, self.direct); if self.use_io_uring { #[cfg(feature = "io_uring")] { return Ok(Box::new(FixedVhdAsync::new(raw_file, ring_depth, size)?)); } #[cfg(not(feature = "io_uring"))] unreachable!("use_io_uring is set but io_uring feature is not enabled"); } let _ = ring_depth; Ok(Box::new(FixedVhdSync::new(raw_file, size))) } } #[cfg(test)] mod unit_tests { use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use vmm_sys_util::tempfile::TempFile; use super::*; use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoOperation, OwnedIoBuffer}; use crate::disk_file::{AsyncDiskFile, DiskSize, PhysicalSize, Resizable}; /// Minimal fixed VHD footer (disk type = 2, current_size = 0x11223344). fn fixed_vhd_footer() -> &'static [u8] { &[ 0x63, 0x6f, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x78, // cookie 0x00, 0x00, 0x00, 0x02, // features 0x00, 0x01, 0x00, 0x00, // file format version 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // data offset 0x27, 0xa6, 0xa6, 0x5d, // time stamp 0x71, 0x65, 0x6d, 0x75, // creator application 0x00, 0x05, 0x00, 0x03, // creator version 0x57, 0x69, 0x32, 0x6b, // creator host os 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, // original size 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, // current size 0x11, 0xe0, 0x10, 0x3f, // disk geometry 0x00, 0x00, 0x00, 0x02, // disk type 0x00, 0x00, 0x00, 0x00, // checksum 0x98, 0x7b, 0xb1, 0xcd, 0x84, 0x14, 0x41, 0xfc, // unique id 0xa4, 0xab, 0xd0, 0x69, 0x45, 0x2b, 0xf2, 0x23, 0x00, // saved state ] } fn make_vhd_file() -> File { let mut file: File = TempFile::new().unwrap().into_file(); let data_size: u64 = 0x1122_3344; file.set_len(data_size + 0x200).unwrap(); file.seek(SeekFrom::Start(data_size)).unwrap(); file.write_all(fixed_vhd_footer()).unwrap(); file } #[test] fn new_sync_returns_correct_size() { let file = make_vhd_file(); let disk = VhdDisk::new(file, false, false).unwrap(); assert_eq!(disk.logical_size().unwrap(), 0x1122_3344); } fn assert_async_io_from_dyn(disk: &dyn AsyncDiskFile, expect_batch: bool) { let io: Box = disk.create_async_io(128).unwrap(); assert_eq!(io.batch_requests_enabled(), expect_batch); } fn assert_async_io(disk: &VhdDisk, expect_batch: bool) { assert_async_io_from_dyn(disk, expect_batch); } #[test] fn sync_backend_disables_batch_requests() { let file = make_vhd_file(); let disk = VhdDisk::new(file, false, false).unwrap(); assert_async_io(&disk, false); } #[cfg(feature = "io_uring")] #[test] fn io_uring_backend_enables_batch_requests() { let file = make_vhd_file(); let disk = VhdDisk::new(file, true, false).unwrap(); assert_async_io(&disk, true); } #[test] fn sync_rejects_read_straddling_logical_size() { let file = TempFile::new().unwrap().into_file(); file.set_len(0x2000).unwrap(); let mut sync_io = FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000); let op = AsyncIoOperation::read_to_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1); assert!(matches!( sync_io.submit_data_operation(op), Err(AsyncIoError::ReadVectored(_)) )); } #[test] fn sync_rejects_write_straddling_logical_size() { let file = TempFile::new().unwrap().into_file(); file.set_len(0x2000).unwrap(); let mut sync_io = FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000); let op = AsyncIoOperation::write_from_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1); assert!(matches!( sync_io.submit_data_operation(op), Err(AsyncIoError::WriteVectored(_)) )); } #[test] fn sync_accepts_operation_exactly_filling_logical_size() { let file = TempFile::new().unwrap().into_file(); file.set_len(0x2000).unwrap(); let mut sync_io = FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000); // end == size: boundary must be accepted let op = AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 0x1000]), 1); sync_io.submit_data_operation(op).unwrap(); } #[test] fn sync_accepts_operation_at_last_byte() { let file = TempFile::new().unwrap().into_file(); file.set_len(0x2000).unwrap(); let mut sync_io = FixedVhdSync::new(AlignedFile::new(file.try_clone().unwrap(), false), 0x1000); // end = 0xFFF + 1 = 0x1000 == size: boundary must be accepted let op = AsyncIoOperation::read_to_vec(0xFFF, OwnedIoBuffer::from_vec(vec![0; 1]), 1); sync_io.submit_data_operation(op).unwrap(); } #[cfg(feature = "io_uring")] #[test] fn io_uring_batch_rejects_request_straddling_logical_size() { let file = TempFile::new().unwrap().into_file(); file.set_len(0x2000).unwrap(); let mut async_io = FixedVhdAsync::new( AlignedFile::new(file.try_clone().unwrap(), false), 8, 0x1000, ) .unwrap(); let op = AsyncIoOperation::read_to_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1); assert!(matches!( async_io.submit_batch_requests(vec![op]), Err(AsyncIoError::ReadVectored(_)) )); } #[cfg(feature = "io_uring")] #[test] fn io_uring_rejects_single_op_straddling_logical_size() { let file = TempFile::new().unwrap().into_file(); file.set_len(0x2000).unwrap(); let mut async_io = FixedVhdAsync::new( AlignedFile::new(file.try_clone().unwrap(), false), 8, 0x1000, ) .unwrap(); let op = AsyncIoOperation::read_to_vec(0x800, OwnedIoBuffer::from_vec(vec![0; 0x900]), 1); assert!(matches!( async_io.submit_data_operation(op), Err(AsyncIoError::ReadVectored(_)) )); } #[test] fn try_clone_preserves_sync_dispatch() { let file = make_vhd_file(); let disk = VhdDisk::new(file, false, false).unwrap(); let cloned = disk.try_clone().unwrap(); assert_async_io_from_dyn(cloned.as_ref(), false); } #[cfg(feature = "io_uring")] #[test] fn try_clone_preserves_io_uring_dispatch() { let file = make_vhd_file(); let disk = VhdDisk::new(file, true, false).unwrap(); let cloned = disk.try_clone().unwrap(); assert_async_io_from_dyn(cloned.as_ref(), true); } #[test] fn resize_returns_error() { let file = make_vhd_file(); let mut disk = VhdDisk::new(file, false, false).unwrap(); assert!(disk.resize(0x2000_0000).is_err()); } #[test] fn physical_size_includes_footer() { let file = make_vhd_file(); let disk = VhdDisk::new(file, false, false).unwrap(); // Data region (0x1122_3344) + VHD footer (0x200). assert_eq!(disk.physical_size().unwrap(), 0x1122_3344 + 0x200); } } cloud-hypervisor-53.0/block/src/formats/vhdx/000077500000000000000000000000001522476750100212755ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/formats/vhdx/bat.rs000066400000000000000000000065201522476750100224140ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::os::unix::fs::FileExt; use std::{io, result}; use byteorder::{ByteOrder, LittleEndian}; use remain::sorted; use thiserror::Error; use super::header::RegionTableEntry; use super::metadata::DiskSpec; use crate::aligned_file::AlignedFile; // Payload BAT Entry States pub(super) const PAYLOAD_BLOCK_NOT_PRESENT: u64 = 0; pub(super) const PAYLOAD_BLOCK_UNDEFINED: u64 = 1; pub(super) const PAYLOAD_BLOCK_ZERO: u64 = 2; pub(super) const PAYLOAD_BLOCK_UNMAPPED: u64 = 3; pub(super) const PAYLOAD_BLOCK_FULLY_PRESENT: u64 = 6; pub(super) const PAYLOAD_BLOCK_PARTIALLY_PRESENT: u64 = 7; // Mask for the BAT state pub(super) const BAT_STATE_BIT_MASK: u64 = 0x07; // Mask for the offset within the file in units of 1 MB pub(super) const BAT_FILE_OFF_MASK: u64 = 0xFFFFFFFFFFF00000; #[sorted] #[derive(Error, Debug)] pub enum VhdxBatError { #[error("Invalid BAT entry")] InvalidBatEntry, #[error("Invalid BAT entry count")] InvalidEntryCount, #[error("Failed to read BAT entry")] ReadBat(#[source] io::Error), #[error("Failed to write BAT entry")] WriteBat(#[source] io::Error), } pub(super) type Result = result::Result; #[derive(Default, Clone, Debug)] pub(super) struct BatEntry(pub u64); impl BatEntry { // Read all BAT entries presented on the disk and insert them to a vector pub(super) fn collect_bat_entries( f: &AlignedFile, disk_spec: &DiskSpec, bat_entry: &RegionTableEntry, ) -> Result> { let entry_count = BatEntry::calculate_entries( disk_spec.block_size, disk_spec.virtual_disk_size, disk_spec.chunk_ratio, ); if entry_count as usize > (bat_entry.length as usize / size_of::()) { return Err(VhdxBatError::InvalidEntryCount); } let mut bat: Vec = Vec::with_capacity(bat_entry.length as usize); let offset = bat_entry.file_offset; for i in 0..entry_count { let mut entry = [0u8; size_of::()]; f.read_exact_at(&mut entry, offset + i * size_of::() as u64) .map_err(VhdxBatError::ReadBat)?; bat.insert(i as usize, BatEntry(LittleEndian::read_u64(&entry))); } Ok(bat) } // Calculate the number of entries in the BAT fn calculate_entries(block_size: u32, virtual_disk_size: u64, chunk_ratio: u64) -> u64 { let data_blocks_count = virtual_disk_size.div_ceil(block_size as u64); data_blocks_count + (data_blocks_count - 1) / chunk_ratio } // Routine for writing BAT entries to the disk pub(super) fn write_bat_entries( f: &AlignedFile, bat_offset: u64, bat_entries: &[BatEntry], ) -> Result<()> { for i in 0..bat_entries.len() as u64 { let bat_entry = match bat_entries.get(i as usize) { Some(entry) => entry.0, None => { return Err(VhdxBatError::InvalidBatEntry); } }; let mut buf = [0u8; size_of::()]; LittleEndian::write_u64(&mut buf, bat_entry); f.write_all_at(&buf, bat_offset + i * size_of::() as u64) .map_err(VhdxBatError::WriteBat)?; } Ok(()) } } cloud-hypervisor-53.0/block/src/formats/vhdx/engine_sync.rs000066400000000000000000000144201522476750100241450ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 use std::collections::VecDeque; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::sync::{Arc, Mutex}; use vmm_sys_util::eventfd::EventFd; use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult}; use crate::formats::vhdx::Vhdx; pub(super) struct VhdxSync { vhdx_file: Arc>, eventfd: EventFd, completion_list: VecDeque, size: u64, } impl VhdxSync { pub(super) fn new(vhdx_file: Arc>, size: u64) -> Self { VhdxSync { vhdx_file, eventfd: EventFd::new(libc::EFD_NONBLOCK) .expect("Failed creating EventFd for VhdxSync"), completion_list: VecDeque::new(), size, } } fn read_operation(&mut self, op: &mut AsyncIoOperation) -> AsyncIoResult { let offset = op.offset(); let mut buf = vec![0u8; op.total_len()]; let mut vhdx = self.vhdx_file.lock().unwrap(); vhdx.seek(SeekFrom::Start(offset as u64)) .map_err(AsyncIoError::ReadVectored)?; let result = vhdx.read(&mut buf).map_err(AsyncIoError::ReadVectored)?; drop(vhdx); op.write_bytes_at(0, &buf[..result]) .map_err(AsyncIoError::ReadVectored)?; Ok(result) } fn write_operation(&mut self, op: &AsyncIoOperation) -> AsyncIoResult { let offset = op.offset(); let mut buf = vec![0u8; op.total_len()]; op.read_bytes_at(0, &mut buf) .map_err(AsyncIoError::WriteVectored)?; let mut vhdx = self.vhdx_file.lock().unwrap(); vhdx.seek(SeekFrom::Start(offset as u64)) .map_err(AsyncIoError::WriteVectored)?; let result = vhdx.write(&buf).map_err(AsyncIoError::WriteVectored)?; Ok(result) } } impl AsyncIo for VhdxSync { fn notifier(&self) -> &EventFd { &self.eventfd } fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()> { op.validate_bounds(self.size)?; let is_read = op.is_read(); let mut op = op; let result = if is_read { self.read_operation(&mut op)? } else { self.write_operation(&op)? }; self.completion_list .push_back(AsyncIoCompletion::from_operation(op, result as i32)); self.eventfd.write(1).unwrap(); Ok(()) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { self.vhdx_file .lock() .unwrap() .flush() .map_err(AsyncIoError::Fsync)?; if let Some(user_data) = user_data { self.completion_list .push_back(AsyncIoCompletion::new(user_data, 0, None)); self.eventfd.write(1).unwrap(); } Ok(()) } fn next_completed_request(&mut self) -> Option { self.completion_list.pop_front() } fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { Err(AsyncIoError::PunchHole(io::Error::other( "punch_hole not supported for VHDX", ))) } fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { Err(AsyncIoError::WriteZeroes(io::Error::other( "write_zeroes not supported for VHDX", ))) } } #[cfg(test)] mod tests { use std::fs; use std::sync::{Arc, Mutex}; use vmm_sys_util::tempfile::TempFile; use super::*; use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoOperation, OwnedIoBuffer}; use crate::formats::vhdx::Vhdx; use crate::formats::vhdx::test_util::create_dynamic_vhdx; fn make_vhdx_sync(tf: &TempFile) -> (VhdxSync, u64) { let file = fs::OpenOptions::new() .read(true) .write(true) .open(tf.as_path()) .unwrap(); let vhdx = Vhdx::new(file, false).unwrap(); let size = vhdx.virtual_disk_size(); let sync = VhdxSync::new(Arc::new(Mutex::new(vhdx)), size); (sync, size) } /// Builds a `VhdxSync` from a fresh 1 MiB dynamic VHDX, or `None` /// if `qemu-img` is unavailable to generate one. fn setup() -> Option<(VhdxSync, u64)> { let tf = create_dynamic_vhdx(1)?; Some(make_vhdx_sync(&tf)) } #[test] fn sync_rejects_read_straddling_logical_size() { let Some((mut sync, size)) = setup() else { eprintln!("skipping: qemu-img unavailable"); return; }; let op = AsyncIoOperation::read_to_vec( (size - 512) as i64, OwnedIoBuffer::from_vec(vec![0u8; 1024]), 1, ); assert!(matches!( sync.submit_data_operation(op), Err(AsyncIoError::ReadVectored(_)) )); } #[test] fn sync_rejects_write_straddling_logical_size() { let Some((mut sync, size)) = setup() else { eprintln!("skipping: qemu-img unavailable"); return; }; let op = AsyncIoOperation::write_from_vec( (size - 512) as i64, OwnedIoBuffer::from_vec(vec![0u8; 1024]), 1, ); assert!(matches!( sync.submit_data_operation(op), Err(AsyncIoError::WriteVectored(_)) )); } #[test] fn sync_accepts_operation_exactly_filling_logical_size() { let Some((mut sync, size)) = setup() else { eprintln!("skipping: qemu-img unavailable"); return; }; let op = AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0u8; size as usize]), 1); sync.submit_data_operation(op).unwrap(); } #[test] fn sync_accepts_operation_at_last_sector() { let Some((mut sync, size)) = setup() else { eprintln!("skipping: qemu-img unavailable"); return; }; // VHDX operates in 512-byte sectors; read exactly the last sector. let op = AsyncIoOperation::read_to_vec( (size - 512) as i64, OwnedIoBuffer::from_vec(vec![0u8; 512]), 1, ); sync.submit_data_operation(op).unwrap(); } } cloud-hypervisor-53.0/block/src/formats/vhdx/header.rs000066400000000000000000000517041522476750100231020ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::collections::btree_map::BTreeMap; use std::os::unix::fs::FileExt; use std::{io, result}; use byteorder::{ByteOrder, LittleEndian}; use remain::sorted; use thiserror::Error; use uuid::Uuid; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::aligned_file::AlignedFile; const VHDX_SIGN: u64 = 0x656C_6966_7864_6876; // "vhdxfile" const HEADER_SIGN: u32 = 0x6461_6568; // "head" const REGION_SIGN: u32 = 0x6967_6572; // "regi" const FILE_START: u64 = 0; // The first element const HEADER_1_START: u64 = 64 * 1024; // Header 1 start in Bytes const HEADER_2_START: u64 = 128 * 1024; // Header 2 start in Bytes pub(super) const REGION_TABLE_1_START: u64 = 192 * 1024; // Region 1 start in Bytes const REGION_TABLE_2_START: u64 = 256 * 1024; // Region 2 start in Bytes const HEADER_SIZE: u64 = 4 * 1024; // Each header is 64 KiB, but only first 4 kiB contains info const REGION_SIZE: u64 = 64 * 1024; // Each region size is 64 KiB const REGION_ENTRY_REQUIRED: u32 = 1; // VHDX stores GUIDs using little-endian GUID byte order. const BAT_GUID: [u8; 16] = [ 0x66, 0x77, 0xc2, 0x2d, 0x23, 0xf6, 0x00, 0x42, 0x9d, 0x64, 0x11, 0x5e, 0x9b, 0xfd, 0x4a, 0x08, ]; const MDR_GUID: [u8; 16] = [ 0x06, 0xa2, 0x7c, 0x8b, 0x90, 0x47, 0x9a, 0x4b, 0xb8, 0xfe, 0x57, 0x5f, 0x05, 0x0f, 0x88, 0x6e, ]; #[sorted] #[derive(Error, Debug)] pub enum VhdxHeaderError { #[error("Failed to calculate checksum")] CalculateChecksum, #[error("BAT entry is not unique")] DuplicateBATEntry, #[error("Metadata region entry is not unique")] DuplicateMDREntry, #[error("Checksum doesn't match for {0}")] InvalidChecksum(String), #[error("Invalid entry count")] InvalidEntryCount, #[error("Not a valid VHDx header")] InvalidHeaderSign, #[error("Not a valid VHDx region")] InvalidRegionSign, #[error("Not a VHDx file")] InvalidVHDXSign, #[error("No valid header found")] NoValidHeader, #[error("Cannot read checksum")] ReadChecksum, #[error("Failed to read File Type Identifier {0}")] ReadFileTypeIdentifier(#[source] io::Error), #[error("Failed to read headers {0}")] ReadHeader(#[source] io::Error), #[error("Failed to read metadata {0}")] ReadMetadata(#[source] io::Error), #[error("Failed to read region table entries {0}")] ReadRegionTableEntries(#[source] io::Error), #[error("Failed to read region table header {0}")] ReadRegionTableHeader(#[source] io::Error), #[error("Failed to read region entries")] RegionEntryCollectionFailed, #[error("Region entry file offset ({0}) and length ({1}) overflow u64")] RegionEntryOverflow(u64 /* start */, usize /* length */), #[error("Overlapping regions found")] RegionOverlap, #[error("Reserved region has non-zero value")] ReservedIsNonZero, #[error("We do not recognize this entry")] UnrecognizedRegionEntry, #[error("Failed to write header {0}")] WriteHeader(#[source] io::Error), } pub(super) type Result = result::Result; #[derive(Clone, Debug)] pub(super) struct FileTypeIdentifier { pub _signature: u64, } impl FileTypeIdentifier { /// Reads the File Type Identifier structure from a reference VHDx file pub(super) fn new(f: &AlignedFile) -> Result { let mut buf = [0u8; size_of::()]; f.read_exact_at(&mut buf, FILE_START) .map_err(VhdxHeaderError::ReadFileTypeIdentifier)?; let _signature = LittleEndian::read_u64(&buf); if _signature != VHDX_SIGN { return Err(VhdxHeaderError::InvalidVHDXSign); } Ok(FileTypeIdentifier { _signature }) } } #[repr(C, packed)] #[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] pub(super) struct Header { pub signature: u32, pub checksum: u32, pub sequence_number: u64, pub file_write_guid: u128, pub data_write_guid: u128, pub log_guid: u128, pub log_version: u16, pub version: u16, pub log_length: u32, pub log_offset: u64, } impl Header { /// Reads the Header structure from a reference VHDx file pub(super) fn new(f: &AlignedFile, start: u64) -> Result
{ // Read the whole header into a buffer. We will need it for // calculating checksum. let mut buffer = [0; HEADER_SIZE as usize]; f.read_exact_at(&mut buffer, start) .map_err(VhdxHeaderError::ReadHeader)?; let header = Header::read_from_prefix(&buffer).unwrap().0; if header.signature != HEADER_SIGN { return Err(VhdxHeaderError::InvalidHeaderSign); } let new_checksum = calculate_checksum(&mut buffer, size_of::()); if header.checksum != new_checksum { return Err(VhdxHeaderError::InvalidChecksum(String::from("Header"))); } Ok(header) } /// Creates and returns new updated header from the provided current header fn update_header( f: &AlignedFile, current_header: &Header, change_data_guid: bool, file_write_guid: u128, start: u64, ) -> Result
{ let mut buffer = [0u8; HEADER_SIZE as usize]; let data_write_guid = if change_data_guid { Uuid::new_v4().as_u128() } else { current_header.data_write_guid }; let file_write_guid = if file_write_guid == 0 { current_header.file_write_guid } else { file_write_guid }; let mut new_header = Header { signature: current_header.signature, checksum: 0, sequence_number: current_header.sequence_number + 1, file_write_guid, data_write_guid, log_guid: current_header.log_guid, log_version: current_header.log_version, version: current_header.version, log_length: current_header.log_length, log_offset: current_header.log_offset, }; new_header.write_to_prefix(&mut buffer).unwrap(); new_header.checksum = calculate_checksum(&mut buffer, size_of::()); new_header.write_to_prefix(&mut buffer).unwrap(); f.write_all_at(&buffer, start) .map_err(VhdxHeaderError::WriteHeader)?; Ok(new_header) } } #[repr(C, packed)] #[derive(Clone, Copy, Debug, FromBytes)] struct RegionTableHeader { pub signature: u32, pub checksum: u32, pub entry_count: u32, pub reserved: u32, } impl RegionTableHeader { /// Reads the Region Table Header structure from a reference VHDx file pub(crate) fn new(f: &AlignedFile, start: u64) -> Result { // Read the whole header into a buffer. We will need it for calculating // checksum. let mut buffer = [0u8; REGION_SIZE as usize]; f.read_exact_at(&mut buffer, start) .map_err(VhdxHeaderError::ReadRegionTableHeader)?; let region_table_header = RegionTableHeader::read_from_prefix(&buffer).unwrap().0; if region_table_header.signature != REGION_SIGN { return Err(VhdxHeaderError::InvalidRegionSign); } let new_checksum = calculate_checksum(&mut buffer, size_of::()); if region_table_header.checksum != new_checksum { return Err(VhdxHeaderError::InvalidChecksum(String::from("Region"))); } if region_table_header.entry_count > 2047 { return Err(VhdxHeaderError::InvalidEntryCount); } if region_table_header.reserved != 0 { return Err(VhdxHeaderError::ReservedIsNonZero); } Ok(region_table_header) } } /// Returns `true` if the half-open byte ranges `[a_start, a_end)` and /// `[b_start, b_end)` overlap. fn ranges_overlap(a_start: u64, a_end: u64, b_start: u64, b_end: u64) -> bool { a_start < b_end && b_start < a_end } pub(super) struct RegionInfo { pub bat_entry: RegionTableEntry, pub mdr_entry: RegionTableEntry, pub region_entries: BTreeMap, } impl RegionInfo { /// Collect all entries in a BTreeMap from the Region Table and identifies /// BAT and metadata regions pub(super) fn new(f: &AlignedFile, region_start: u64, entry_count: u32) -> Result { let mut bat_entry: Option = None; let mut mdr_entry: Option = None; let mut offset = 0; let mut region_entries = BTreeMap::new(); let mut buffer = [0; REGION_SIZE as usize]; // Read after the Region Table Header f.read_exact_at( &mut buffer, region_start + size_of::() as u64, ) .map_err(VhdxHeaderError::ReadRegionTableEntries)?; for _ in 0..entry_count { let entry = RegionTableEntry::read_from_bytes( &buffer[offset..offset + size_of::()], ) .unwrap(); offset += size_of::(); let start = entry.file_offset; let end = start.checked_add(entry.length as u64).ok_or( VhdxHeaderError::RegionEntryOverflow(start, entry.length as usize), )?; for (region_ent_start, region_ent_end) in region_entries.iter() { if ranges_overlap(start, end, *region_ent_start, *region_ent_end) { return Err(VhdxHeaderError::RegionOverlap); } } region_entries.insert(start, end); if entry.guid == BAT_GUID { if bat_entry.is_none() { bat_entry = Some(entry); continue; } return Err(VhdxHeaderError::DuplicateBATEntry); } if entry.guid == MDR_GUID { if mdr_entry.is_none() { mdr_entry = Some(entry); continue; } return Err(VhdxHeaderError::DuplicateMDREntry); } if (entry.required & REGION_ENTRY_REQUIRED) == 1 { // This implementation doesn't recognize this field. // Therefore, according to the spec, we are throwing an error. return Err(VhdxHeaderError::UnrecognizedRegionEntry); } } if bat_entry.is_none() || mdr_entry.is_none() { region_entries.clear(); return Err(VhdxHeaderError::RegionEntryCollectionFailed); } // It's safe to unwrap as we checked both entries have been filled. // Otherwise, an error is already returned. let bat_entry = bat_entry.unwrap(); let mdr_entry = mdr_entry.unwrap(); Ok(RegionInfo { bat_entry, mdr_entry, region_entries, }) } } #[repr(C, packed)] #[derive(Clone, Copy, Debug, FromBytes)] pub(super) struct RegionTableEntry { guid: [u8; 16], pub file_offset: u64, pub length: u32, pub required: u32, } enum HeaderNo { First, Second, } /// Contains the information from the header of a VHDx file #[derive(Clone, Debug)] pub(super) struct VhdxHeader { _file_type_identifier: FileTypeIdentifier, header_1: Header, header_2: Header, region_table_1: RegionTableHeader, _region_table_2: RegionTableHeader, } impl VhdxHeader { /// Creates a VhdxHeader from a reference to a file pub(super) fn new(f: &AlignedFile) -> Result { Ok(VhdxHeader { _file_type_identifier: FileTypeIdentifier::new(f)?, header_1: Header::new(f, HEADER_1_START)?, header_2: Header::new(f, HEADER_2_START)?, region_table_1: RegionTableHeader::new(f, REGION_TABLE_1_START)?, _region_table_2: RegionTableHeader::new(f, REGION_TABLE_2_START)?, }) } /// Identify the current header and return both headers along with an /// integer indicating the current header. fn current_header( header_1: Result
, header_2: Result
, ) -> Result<(HeaderNo, Header)> { let header_1 = header_1.ok(); let header_2 = header_2.ok(); match (header_1, header_2) { (None, None) => Err(VhdxHeaderError::NoValidHeader), (Some(header_1), None) => Ok((HeaderNo::First, header_1)), (None, Some(header_2)) => Ok((HeaderNo::Second, header_2)), (Some(header_1), Some(header_2)) => { if header_1.sequence_number >= header_2.sequence_number { Ok((HeaderNo::First, header_1)) } else { Ok((HeaderNo::Second, header_2)) } } } } /// This takes two headers and update the noncurrent header with the /// current one. Returns both headers as a tuple sequenced the way it was /// received from the parameter list. fn update_header( f: &AlignedFile, header_1: Result
, header_2: Result
, guid: u128, ) -> Result<(Header, Header)> { let (header_no, current_header) = VhdxHeader::current_header(header_1, header_2)?; match header_no { HeaderNo::First => { let other_header = Header::update_header(f, ¤t_header, true, guid, HEADER_2_START)?; Ok((current_header, other_header)) } HeaderNo::Second => { let other_header = Header::update_header(f, ¤t_header, true, guid, HEADER_1_START)?; Ok((other_header, current_header)) } } } // Update the provided headers according to the spec fn update_headers( f: &AlignedFile, header_1: Result
, header_2: Result
, guid: u128, ) -> Result<(Header, Header)> { // According to the spec, update twice let (header_1, header_2) = VhdxHeader::update_header(f, header_1, header_2, guid)?; VhdxHeader::update_header(f, Ok(header_1), Ok(header_2), guid) } pub(super) fn update(&mut self, f: &AlignedFile) -> Result<()> { let headers = VhdxHeader::update_headers(f, Ok(self.header_1), Ok(self.header_2), 0)?; self.header_1 = headers.0; self.header_2 = headers.1; Ok(()) } pub(super) fn region_entry_count(&self) -> u32 { self.region_table_1.entry_count } } /// Calculates the checksum of a buffer that itself contains its checksum /// Therefore, before calculating, the existing checksum is retrieved and the /// corresponding field is made zero. After the calculation, the existing checksum /// is put back to the buffer. fn calculate_checksum(buffer: &mut [u8], csum_offset: usize) -> u32 { // Read the original checksum from the buffer let orig_csum = LittleEndian::read_u32(&buffer[csum_offset..csum_offset + 4]); // Zero the checksum in the buffer LittleEndian::write_u32(&mut buffer[csum_offset..csum_offset + 4], 0); // Calculate the checksum on the resulting buffer let mut crc = crc_any::CRC::crc32c(); crc.digest(&buffer); let new_csum = crc.get_crc() as u32; // Put back the original checksum in the buffer LittleEndian::write_u32(&mut buffer[csum_offset..csum_offset + 4], orig_csum); new_csum } #[cfg(test)] mod tests { use std::os::unix::fs::FileExt; use vmm_sys_util::tempfile::TempFile; use zerocopy::{FromBytes, IntoBytes}; use super::{ BAT_GUID, HEADER_SIGN, Header, MDR_GUID, REGION_TABLE_1_START, RegionInfo, RegionTableHeader, VhdxHeaderError, ranges_overlap, }; use crate::aligned_file::AlignedFile; #[test] fn test_header_bytes_round_trip() { let header = Header { signature: HEADER_SIGN, checksum: 0x1122_3344, sequence_number: 0x0102_0304_0506_0708, file_write_guid: 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100, data_write_guid: 0x1f1e_1d1c_1b1a_1918_1716_1514_1312_1110, log_guid: 0x2f2e_2d2c_2b2a_2928_2726_2524_2322_2120, log_version: 0xabcd, version: 0x0001, log_length: 0x0010_0000, log_offset: 0x0000_0100_0000_0000, }; let bytes = header.as_bytes(); assert_eq!(&bytes[0..4], &header.signature.to_le_bytes()[..]); assert_eq!(&bytes[8..16], &header.sequence_number.to_le_bytes()[..]); assert_eq!(&bytes[16..32], &header.file_write_guid.to_le_bytes()[..]); assert_eq!(&bytes[64..66], &header.log_version.to_le_bytes()[..]); assert_eq!(&bytes[72..80], &header.log_offset.to_le_bytes()[..]); let parsed = Header::read_from_bytes(bytes).unwrap(); assert_eq!({ parsed.signature }, { header.signature }); assert_eq!({ parsed.checksum }, { header.checksum }); assert_eq!({ parsed.sequence_number }, { header.sequence_number }); assert_eq!({ parsed.file_write_guid }, { header.file_write_guid }); assert_eq!({ parsed.data_write_guid }, { header.data_write_guid }); assert_eq!({ parsed.log_guid }, { header.log_guid }); assert_eq!({ parsed.log_version }, { header.log_version }); assert_eq!({ parsed.version }, { header.version }); assert_eq!({ parsed.log_length }, { header.log_length }); assert_eq!({ parsed.log_offset }, { header.log_offset }); } #[test] fn test_ranges_overlap() { // (new [start,end), existing [s,e), expected overlap) let cases: &[(u64, u64, u64, u64, bool)] = &[ // Genuine overlaps — all of these must be detected. (0, 10, 0, 10, true), // identical (2, 8, 0, 10, true), // new fully inside existing (0, 20, 5, 10, true), // new fully contains existing (5, 15, 0, 10, true), // partial, new starts inside existing (0, 8, 5, 15, true), // partial, new starts before existing // Non-overlapping — must not be flagged. (0, 5, 10, 20, false), // disjoint, new before existing (30, 40, 10, 20, false), // disjoint, new after existing (0, 10, 10, 20, false), // touching at the boundary (half-open) ]; for &(a_start, a_end, b_start, b_end, expected) in cases { assert_eq!( ranges_overlap(a_start, a_end, b_start, b_end), expected, "[{a_start},{a_end}) vs [{b_start},{b_end})" ); // Overlap is symmetric. assert_eq!( ranges_overlap(b_start, b_end, a_start, a_end), expected, "symmetry: [{b_start},{b_end}) vs [{a_start},{a_end})" ); } } /// Builds the 32-byte on-disk region table entry for `guid` describing /// the region `[file_offset, file_offset + length)`. fn region_entry(guid: [u8; 16], file_offset: u64, length: u32) -> [u8; 32] { let mut e = [0u8; 32]; e[0..16].copy_from_slice(&guid); e[16..24].copy_from_slice(&file_offset.to_le_bytes()); e[24..28].copy_from_slice(&length.to_le_bytes()); // `required` (e[28..32]) left zero. e } #[test] fn test_region_info_rejects_overlapping_regions() { // BAT region [1 MiB, 3 MiB) and metadata region [2 MiB, 4 MiB) overlap // on [2 MiB, 3 MiB); per [MS-VHDX] all region objects must be // non-overlapping, so this image must be rejected. const MIB: u64 = 1024 * 1024; let region_start = REGION_TABLE_1_START; let entries_at = region_start + size_of::() as u64; let temp = TempFile::new().unwrap(); let f = temp.into_file(); f.set_len(entries_at + 64 * 1024).unwrap(); f.write_all_at(®ion_entry(BAT_GUID, MIB, (2 * MIB) as u32), entries_at) .unwrap(); f.write_all_at( ®ion_entry(MDR_GUID, 2 * MIB, (2 * MIB) as u32), entries_at + 32, ) .unwrap(); let af = AlignedFile::new(f, false); let res = RegionInfo::new(&af, region_start, 2); assert!( matches!(res, Err(VhdxHeaderError::RegionOverlap)), "expected RegionOverlap for an overlapping region table" ); } #[test] fn test_region_info_rejects_overflowing_region() { // A region whose file offset plus length wraps past u64::MAX must be // rejected rather than silently producing a small end offset that // could mask a genuine overlap. let region_start = REGION_TABLE_1_START; let entries_at = region_start + size_of::() as u64; let temp = TempFile::new().unwrap(); let f = temp.into_file(); f.set_len(entries_at + 64 * 1024).unwrap(); f.write_all_at(®ion_entry(BAT_GUID, u64::MAX, 0x1000), entries_at) .unwrap(); let af = AlignedFile::new(f, false); let res = RegionInfo::new(&af, region_start, 1); assert!( matches!(res, Err(VhdxHeaderError::RegionEntryOverflow(..))), "expected RegionEntryOverflow for a wrapping region entry" ); } } cloud-hypervisor-53.0/block/src/formats/vhdx/io.rs000066400000000000000000000156071522476750100222630ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::os::unix::fs::FileExt; use std::{io, result}; use remain::sorted; use thiserror::Error; use super::bat::{self, BatEntry, VhdxBatError}; use super::metadata::{self, DiskSpec}; use crate::aligned_file::AlignedFile; const SECTOR_SIZE: u64 = 512; #[sorted] #[derive(Error, Debug)] pub enum VhdxIoError { #[error("Invalid BAT entry state")] InvalidBatEntryState, #[error("Invalid BAT entry count")] InvalidBatIndex, #[error("Invalid disk size")] InvalidDiskSize, #[error("Failed reading sector blocks from file {0}")] ReadSectorBlock(#[source] io::Error), #[error("Failed changing file length {0}")] ResizeFile(#[source] io::Error), #[error("Differencing mode is not supported yet")] UnsupportedMode, #[error("Failed writing BAT to file {0}")] WriteBat(#[source] VhdxBatError), } pub(super) type Result = result::Result; macro_rules! align { ($n:expr, $align:expr) => {{ $n.div_ceil($align) * $align }}; } #[derive(Default)] struct Sector { bat_index: u64, free_sectors: u64, free_bytes: u64, file_offset: u64, block_offset: u64, } impl Sector { /// Translate sector index and count of data in file to actual offsets and /// BAT index. pub(crate) fn new( disk_spec: &DiskSpec, bat: &[BatEntry], sector_index: u64, sector_count: u64, ) -> Result { let mut sector = Sector::default(); sector.bat_index = sector_index / disk_spec.sectors_per_block as u64; sector.block_offset = sector_index % disk_spec.sectors_per_block as u64; sector.free_sectors = disk_spec.sectors_per_block as u64 - sector.block_offset; if sector.free_sectors > sector_count { sector.free_sectors = sector_count; } sector.free_bytes = sector.free_sectors * disk_spec.logical_sector_size as u64; sector.block_offset *= disk_spec.logical_sector_size as u64; let bat_entry = match bat.get(sector.bat_index as usize) { Some(entry) => entry.0, None => { return Err(VhdxIoError::InvalidBatIndex); } }; sector.file_offset = bat_entry & bat::BAT_FILE_OFF_MASK; if sector.file_offset != 0 { sector.file_offset += sector.block_offset; } Ok(sector) } } /// VHDx IO read routine: requires relative sector index and count for the /// requested data. pub(super) fn read( f: &AlignedFile, buf: &mut [u8], disk_spec: &DiskSpec, bat: &[BatEntry], mut sector_index: u64, mut sector_count: u64, ) -> Result { if disk_spec.has_parent { return Err(VhdxIoError::UnsupportedMode); } let mut read_count: usize = 0; while sector_count > 0 { let sector = Sector::new(disk_spec, bat, sector_index, sector_count)?; let bat_entry = match bat.get(sector.bat_index as usize) { Some(entry) => entry.0, None => { return Err(VhdxIoError::InvalidBatIndex); } }; match bat_entry & bat::BAT_STATE_BIT_MASK { bat::PAYLOAD_BLOCK_NOT_PRESENT | bat::PAYLOAD_BLOCK_UNDEFINED | bat::PAYLOAD_BLOCK_UNMAPPED | bat::PAYLOAD_BLOCK_ZERO => {} bat::PAYLOAD_BLOCK_FULLY_PRESENT => { f.read_exact_at( &mut buf [read_count..(read_count + (sector.free_sectors * SECTOR_SIZE) as usize)], sector.file_offset, ) .map_err(VhdxIoError::ReadSectorBlock)?; } bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => { return Err(VhdxIoError::UnsupportedMode); } _ => { return Err(VhdxIoError::InvalidBatEntryState); } } sector_count -= sector.free_sectors; sector_index += sector.free_sectors; read_count += sector.free_bytes as usize; } Ok(read_count) } /// VHDx IO write routine: requires relative sector index and count for the /// requested data. pub(super) fn write( f: &AlignedFile, buf: &[u8], disk_spec: &mut DiskSpec, bat_offset: u64, bat: &mut [BatEntry], mut sector_index: u64, mut sector_count: u64, ) -> Result { if disk_spec.has_parent { return Err(VhdxIoError::UnsupportedMode); } let mut write_count: usize = 0; while sector_count > 0 { let sector = Sector::new(disk_spec, bat, sector_index, sector_count)?; let bat_entry = match bat.get(sector.bat_index as usize) { Some(entry) => entry.0, None => { return Err(VhdxIoError::InvalidBatIndex); } }; match bat_entry & bat::BAT_STATE_BIT_MASK { bat::PAYLOAD_BLOCK_NOT_PRESENT | bat::PAYLOAD_BLOCK_UNDEFINED | bat::PAYLOAD_BLOCK_UNMAPPED | bat::PAYLOAD_BLOCK_ZERO => { let file_offset = align!(disk_spec.image_size, metadata::BLOCK_SIZE_MIN as u64); let new_size = file_offset .checked_add(disk_spec.block_size as u64) .ok_or(VhdxIoError::InvalidDiskSize)?; f.file() .set_len(new_size) .map_err(VhdxIoError::ResizeFile)?; disk_spec.image_size = new_size; let new_bat_entry = file_offset | (bat::PAYLOAD_BLOCK_FULLY_PRESENT & bat::BAT_STATE_BIT_MASK); bat[sector.bat_index as usize] = BatEntry(new_bat_entry); BatEntry::write_bat_entries(f, bat_offset, bat).map_err(VhdxIoError::WriteBat)?; if file_offset < metadata::BLOCK_SIZE_MIN as u64 { break; } f.write_all_at( &buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)], file_offset, ) .map_err(VhdxIoError::ReadSectorBlock)?; } bat::PAYLOAD_BLOCK_FULLY_PRESENT => { if sector.file_offset < metadata::BLOCK_SIZE_MIN as u64 { break; } f.write_all_at( &buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)], sector.file_offset, ) .map_err(VhdxIoError::ReadSectorBlock)?; } bat::PAYLOAD_BLOCK_PARTIALLY_PRESENT => { return Err(VhdxIoError::UnsupportedMode); } _ => { return Err(VhdxIoError::InvalidBatEntryState); } } sector_count -= sector.free_sectors; sector_index += sector.free_sectors; write_count += sector.free_bytes as usize; } Ok(write_count) } cloud-hypervisor-53.0/block/src/formats/vhdx/metadata.rs000066400000000000000000000275241522476750100234350ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::os::unix::fs::FileExt; use std::{io, result}; use byteorder::{ByteOrder, LittleEndian}; use remain::sorted; use thiserror::Error; use zerocopy::FromBytes; use super::header::RegionTableEntry; use crate::aligned_file::AlignedFile; const METADATA_SIGN: u64 = 0x6174_6164_6174_656D; const METADATA_ENTRY_SIZE: usize = 32; const METADATA_MAX_ENTRIES: u16 = 2047; // The size including the table header and entries const METADATA_TABLE_MAX_SIZE: usize = METADATA_ENTRY_SIZE * (METADATA_MAX_ENTRIES as usize + 1); const METADATA_FLAGS_IS_REQUIRED: u32 = 0x04; pub(super) const BLOCK_SIZE_MIN: u32 = 1 << 20; // 1 MiB const BLOCK_SIZE_MAX: u32 = 256 << 20; // 256 MiB const MAX_SECTORS_PER_BLOCK: u64 = 1 << 23; const BLOCK_HAS_PARENT: u32 = 0x02; // Has a parent or a backing file // GUID for known metadata items const METADATA_FILE_PARAMETER: [u8; 16] = [ 0x37, 0x67, 0xa1, 0xca, 0x36, 0xfa, 0x43, 0x4d, 0xb3, 0xb6, 0x33, 0xf0, 0xaa, 0x44, 0xe7, 0x6b, ]; const METADATA_VIRTUAL_DISK_SIZE: [u8; 16] = [ 0x24, 0x42, 0xa5, 0x2f, 0x1b, 0xcd, 0x76, 0x48, 0xb2, 0x11, 0x5d, 0xbe, 0xd8, 0x3b, 0xf4, 0xb8, ]; const METADATA_VIRTUAL_DISK_ID: [u8; 16] = [ 0xab, 0x12, 0xca, 0xbe, 0xe6, 0xb2, 0x23, 0x45, 0x93, 0xef, 0xc3, 0x09, 0xe0, 0x00, 0xc7, 0x46, ]; const METADATA_LOGICAL_SECTOR_SIZE: [u8; 16] = [ 0x1d, 0xbf, 0x41, 0x81, 0x6f, 0xa9, 0x09, 0x47, 0xba, 0x47, 0xf2, 0x33, 0xa8, 0xfa, 0xab, 0x5f, ]; const METADATA_PHYSICAL_SECTOR_SIZE: [u8; 16] = [ 0xc7, 0x48, 0xa3, 0xcd, 0x5d, 0x44, 0x71, 0x44, 0x9c, 0xc9, 0xe9, 0x88, 0x52, 0x51, 0xc5, 0x56, ]; const METADATA_PARENT_LOCATOR: [u8; 16] = [ 0x2d, 0x5f, 0xd3, 0xa8, 0x0b, 0xb3, 0x4d, 0x45, 0xab, 0xf7, 0xd3, 0xd8, 0x48, 0x34, 0xab, 0x0c, ]; const METADATA_FILE_PARAMETER_PRESENT: u16 = 0x01; const METADATA_VIRTUAL_DISK_SIZE_PRESENT: u16 = 0x02; const METADATA_VIRTUAL_DISK_ID_PRESENT: u16 = 0x04; const METADATA_LOGICAL_SECTOR_SIZE_PRESENT: u16 = 0x08; const METADATA_PHYSICAL_SECTOR_SIZE_PRESENT: u16 = 0x10; const METADATA_PARENT_LOCATOR_PRESENT: u16 = 0x20; const METADATA_ALL_PRESENT: u16 = METADATA_FILE_PARAMETER_PRESENT | METADATA_VIRTUAL_DISK_SIZE_PRESENT | METADATA_VIRTUAL_DISK_ID_PRESENT | METADATA_LOGICAL_SECTOR_SIZE_PRESENT | METADATA_PHYSICAL_SECTOR_SIZE_PRESENT; const METADATA_LENGTH_MAX: u32 = 1 << 20; // 1 MiB #[sorted] #[derive(Error, Debug)] pub enum VhdxMetadataError { #[error("Invalid block size count")] InvalidBlockSize, #[error("Invalid disk size {0}")] InvalidDiskSize(u64), #[error("Invalid metadata entry count")] InvalidEntryCount, #[error("Invalid logical sector size")] InvalidLogicalSectorSize, #[error("Invalid metadata ID")] InvalidMetadataItem, #[error("Invalid metadata length")] InvalidMetadataLength, #[error("Metadata sign doesn't match")] InvalidMetadataSign, #[error("Invalid physical sector size")] InvalidPhysicalSectorSize, #[error("Invalid value")] InvalidValue, #[error("Not all required metadata found")] MissingMetadata, #[error("Failed to read metadata headers {0}")] ReadMetadata(#[source] io::Error), #[error("Reserved region has non-zero value")] ReservedIsNonZero, #[error("This implementation doesn't support this metadata flag")] UnsupportedFlag, } pub(super) type Result = result::Result; #[derive(Default, Clone, Debug)] pub(super) struct DiskSpec { pub disk_id: u128, pub image_size: u64, pub block_size: u32, pub has_parent: bool, pub sectors_per_block: u32, pub virtual_disk_size: u64, pub logical_sector_size: u32, pub physical_sector_size: u32, pub chunk_ratio: u64, pub total_sectors: u64, } impl DiskSpec { /// Parse all metadata from the provided file and store info in DiskSpec /// structure. pub(super) fn new(f: &AlignedFile, metadata_region: &RegionTableEntry) -> Result { let mut disk_spec = DiskSpec::default(); let mut metadata_presence: u16 = 0; let mut offset = 0; let metadata = f .file() .metadata() .map_err(VhdxMetadataError::ReadMetadata)?; disk_spec.image_size = metadata.len(); let mut buffer = [0u8; METADATA_TABLE_MAX_SIZE]; f.read_exact_at(&mut buffer, metadata_region.file_offset) .map_err(VhdxMetadataError::ReadMetadata)?; let metadata_header = MetadataTableHeader::new(&buffer[0..size_of::()])?; offset += size_of::(); for _ in 0..metadata_header.entry_count { let metadata_entry = MetadataTableEntry::new(&buffer[offset..offset + size_of::()])?; let item_offset = metadata_region.file_offset + metadata_entry.offset as u64; if metadata_entry.item_id == METADATA_FILE_PARAMETER { let mut item = [0u8; 2 * size_of::()]; f.read_exact_at(&mut item, item_offset) .map_err(VhdxMetadataError::ReadMetadata)?; disk_spec.block_size = LittleEndian::read_u32(&item[0..4]); // MUST be at least 1 MiB and not greater than 256 MiB if disk_spec.block_size < BLOCK_SIZE_MIN || disk_spec.block_size > BLOCK_SIZE_MAX { return Err(VhdxMetadataError::InvalidBlockSize); } // MUST be power of 2 if !disk_spec.block_size.is_power_of_two() { return Err(VhdxMetadataError::InvalidBlockSize); } let bits = LittleEndian::read_u32(&item[4..8]); disk_spec.has_parent = bits & BLOCK_HAS_PARENT != 0; metadata_presence |= METADATA_FILE_PARAMETER_PRESENT; } else if metadata_entry.item_id == METADATA_VIRTUAL_DISK_SIZE { let mut item = [0u8; size_of::()]; f.read_exact_at(&mut item, item_offset) .map_err(VhdxMetadataError::ReadMetadata)?; disk_spec.virtual_disk_size = LittleEndian::read_u64(&item); metadata_presence |= METADATA_VIRTUAL_DISK_SIZE_PRESENT; } else if metadata_entry.item_id == METADATA_VIRTUAL_DISK_ID { let mut item = [0u8; size_of::()]; f.read_exact_at(&mut item, item_offset) .map_err(VhdxMetadataError::ReadMetadata)?; disk_spec.disk_id = LittleEndian::read_u128(&item); metadata_presence |= METADATA_VIRTUAL_DISK_ID_PRESENT; } else if metadata_entry.item_id == METADATA_LOGICAL_SECTOR_SIZE { let mut item = [0u8; size_of::()]; f.read_exact_at(&mut item, item_offset) .map_err(VhdxMetadataError::ReadMetadata)?; disk_spec.logical_sector_size = LittleEndian::read_u32(&item); if !(disk_spec.logical_sector_size == 512 || disk_spec.logical_sector_size == 4096) { return Err(VhdxMetadataError::InvalidLogicalSectorSize); } metadata_presence |= METADATA_LOGICAL_SECTOR_SIZE_PRESENT; } else if metadata_entry.item_id == METADATA_PHYSICAL_SECTOR_SIZE { let mut item = [0u8; size_of::()]; f.read_exact_at(&mut item, item_offset) .map_err(VhdxMetadataError::ReadMetadata)?; disk_spec.physical_sector_size = LittleEndian::read_u32(&item); if !(disk_spec.physical_sector_size == 512 || disk_spec.physical_sector_size == 4096) { return Err(VhdxMetadataError::InvalidPhysicalSectorSize); } metadata_presence |= METADATA_PHYSICAL_SECTOR_SIZE_PRESENT; } else if metadata_entry.item_id == METADATA_PARENT_LOCATOR { metadata_presence |= METADATA_PARENT_LOCATOR_PRESENT; } else { return Err(VhdxMetadataError::InvalidMetadataItem); } if (metadata_entry.flag_bits & METADATA_FLAGS_IS_REQUIRED) == 0 { return Err(VhdxMetadataError::UnsupportedFlag); } offset += size_of::(); } // Check if all required metadata are present if metadata_presence != METADATA_ALL_PRESENT { return Err(VhdxMetadataError::MissingMetadata); } // Make sure virtual disk size is not zero if (metadata_presence & METADATA_VIRTUAL_DISK_SIZE_PRESENT != 0) && disk_spec.virtual_disk_size == 0 { return Err(VhdxMetadataError::InvalidDiskSize( disk_spec.virtual_disk_size, )); } // Check if the virtual disk size is a multiple of the logical sector // size. if ((metadata_presence & METADATA_LOGICAL_SECTOR_SIZE_PRESENT) != 0) && (disk_spec.virtual_disk_size % disk_spec.logical_sector_size as u64 != 0) { return Err(VhdxMetadataError::InvalidBlockSize); } disk_spec.sectors_per_block = DiskSpec::sectors_per_block(disk_spec.block_size, disk_spec.logical_sector_size)?; disk_spec.chunk_ratio = DiskSpec::chunk_ratio(disk_spec.block_size, disk_spec.logical_sector_size)?; disk_spec.total_sectors = disk_spec.virtual_disk_size / disk_spec.logical_sector_size as u64; Ok(disk_spec) } /// Calculates the number of sectors per block fn sectors_per_block(block_size: u32, logical_sector_size: u32) -> Result { let sectors_per_block = block_size / logical_sector_size; if !sectors_per_block.is_power_of_two() { return Err(VhdxMetadataError::InvalidValue); } Ok(sectors_per_block) } /// Calculate the chunk ratio fn chunk_ratio(block_size: u32, logical_sector_size: u32) -> Result { let chunk_ratio = (MAX_SECTORS_PER_BLOCK * logical_sector_size as u64) / block_size as u64; if !chunk_ratio.is_power_of_two() { return Err(VhdxMetadataError::InvalidValue); } Ok(chunk_ratio) } } #[repr(C, packed)] #[derive(Default, Debug, Clone, Copy, FromBytes)] struct MetadataTableHeader { signature: u64, reserved: u16, entry_count: u16, _reserved2: [u8; 20], } impl MetadataTableHeader { pub(crate) fn new(buffer: &[u8]) -> Result { let metadata_table_header = MetadataTableHeader::read_from_bytes(buffer).unwrap(); if metadata_table_header.signature != METADATA_SIGN { return Err(VhdxMetadataError::InvalidMetadataSign); } if metadata_table_header.entry_count > METADATA_MAX_ENTRIES { return Err(VhdxMetadataError::InvalidEntryCount); } if metadata_table_header.reserved != 0 { return Err(VhdxMetadataError::ReservedIsNonZero); } Ok(metadata_table_header) } } #[repr(C, packed)] #[derive(Default, Debug, Clone, Copy, FromBytes)] pub(super) struct MetadataTableEntry { item_id: [u8; 16], offset: u32, length: u32, flag_bits: u32, reserved: u32, } impl MetadataTableEntry { /// Parse one metadata entry from the buffer fn new(buffer: &[u8]) -> Result { let metadata_table_entry = MetadataTableEntry::read_from_bytes(buffer).unwrap(); if metadata_table_entry.length > METADATA_LENGTH_MAX { return Err(VhdxMetadataError::InvalidMetadataLength); } if metadata_table_entry.length == 0 && metadata_table_entry.offset != 0 { return Err(VhdxMetadataError::InvalidMetadataLength); } if metadata_table_entry.reserved != 0 { return Err(VhdxMetadataError::ReservedIsNonZero); } Ok(metadata_table_entry) } } cloud-hypervisor-53.0/block/src/formats/vhdx/mod.rs000066400000000000000000000072501522476750100224260ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 //! VHDX disk format support. //! //! Provides [`VhdxDisk`], the `DiskFile` wrapper for dynamic VHDX //! images. mod bat; mod engine_sync; mod header; mod io; mod metadata; mod parser; #[cfg(test)] mod test_util; use std::fs::File; use std::io::Error as IoError; use std::os::fd::AsRawFd; use std::sync::{Arc, Mutex}; pub use parser::{Vhdx, VhdxError}; use self::engine_sync::VhdxSync; use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; use crate::{Error, disk_file}; #[derive(Debug)] pub struct VhdxDisk { // FIXME: The Mutex serializes all VHDX I/O operations across queues, which // is necessary for correctness but eliminates any parallelism benefit from // multiqueue. Vhdx::clone() shares the underlying file description across // threads, so concurrent I/O from multiple queues races on the file offset // causing data corruption. // // A proper fix would require restructuring the VHDX I/O path so that data // operations can proceed in parallel with independent file descriptors. vhdx_file: Arc>, } impl VhdxDisk { pub fn new(f: File, direct_io: bool) -> BlockResult { Ok(VhdxDisk { vhdx_file: Arc::new(Mutex::new(Vhdx::new(f, direct_io).map_err(|e| { let kind = match &e { VhdxError::NotVhdx(_) | VhdxError::ParseVhdxHeader(_) | VhdxError::ParseVhdxMetadata(_) | VhdxError::ParseVhdxRegionEntry(_) => BlockErrorKind::InvalidFormat, VhdxError::ReadBatEntry(_) => BlockErrorKind::CorruptImage, VhdxError::ReadFailed(_) | VhdxError::WriteFailed(_) => BlockErrorKind::Io, }; BlockError::new(kind, e).with_op(ErrorOp::Open) })?)), }) } } impl disk_file::DiskSize for VhdxDisk { fn logical_size(&self) -> BlockResult { Ok(self.vhdx_file.lock().unwrap().virtual_disk_size()) } } impl disk_file::PhysicalSize for VhdxDisk { 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 VhdxDisk { fn fd(&self) -> BorrowedDiskFd<'_> { BorrowedDiskFd::new(self.vhdx_file.lock().unwrap().as_raw_fd()) } } impl disk_file::Geometry for VhdxDisk {} impl disk_file::SparseCapable for VhdxDisk {} impl disk_file::Resizable for VhdxDisk { fn resize(&mut self, _size: u64) -> BlockResult<()> { Err(BlockError::new( BlockErrorKind::UnsupportedFeature, DiskFileError::ResizeError(IoError::other("resize not supported for VHDX")), ) .with_op(ErrorOp::Resize)) } } impl disk_file::DiskFile for VhdxDisk {} impl disk_file::AsyncDiskFile for VhdxDisk { fn try_clone(&self) -> BlockResult> { Ok(Box::new(VhdxDisk { vhdx_file: Arc::clone(&self.vhdx_file), })) } fn create_async_io(&self, _ring_depth: u32) -> BlockResult> { let size = self.vhdx_file.lock().unwrap().virtual_disk_size(); Ok(Box::new(VhdxSync::new(Arc::clone(&self.vhdx_file), size))) } } cloud-hypervisor-53.0/block/src/formats/vhdx/parser.rs000066400000000000000000000235421522476750100231450ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 use std::collections::btree_map::BTreeMap; use std::fs::File; use std::io::{ Error as IoError, ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, Write, }; use std::os::fd::{AsRawFd, RawFd}; use std::result; use remain::sorted; use thiserror::Error; use super::bat::{BatEntry, VhdxBatError}; use super::header::{self, RegionInfo, RegionTableEntry, VhdxHeader, VhdxHeaderError}; use super::io::{self, VhdxIoError}; use super::metadata::{DiskSpec, VhdxMetadataError}; use crate::aligned_file::AlignedFile; #[sorted] #[derive(Error, Debug)] pub enum VhdxError { #[error("Not a VHDx file")] NotVhdx(#[source] VhdxHeaderError), #[error("Failed to parse VHDx header")] ParseVhdxHeader(#[source] VhdxHeaderError), #[error("Failed to parse VHDx metadata")] ParseVhdxMetadata(#[source] VhdxMetadataError), #[error("Failed to parse VHDx region entries")] ParseVhdxRegionEntry(#[source] VhdxHeaderError), #[error("Failed reading metadata")] ReadBatEntry(#[source] VhdxBatError), #[error("Failed reading sector from disk")] ReadFailed(#[source] VhdxIoError), #[error("Failed writing to sector on disk")] WriteFailed(#[source] VhdxIoError), } pub(super) type Result = result::Result; #[derive(Debug)] pub struct Vhdx { aligned: AlignedFile, vhdx_header: VhdxHeader, region_entries: BTreeMap, bat_entry: RegionTableEntry, mdr_entry: RegionTableEntry, disk_spec: DiskSpec, bat_entries: Vec, current_offset: u64, first_write: bool, } impl Vhdx { /// Parse the Vhdx header, BAT, and metadata from a file and store info // in Vhdx structure. pub fn new(file: File, direct_io: bool) -> Result { let aligned = AlignedFile::new(file, direct_io); let vhdx_header = VhdxHeader::new(&aligned).map_err(VhdxError::ParseVhdxHeader)?; let collected_entries = RegionInfo::new( &aligned, header::REGION_TABLE_1_START, vhdx_header.region_entry_count(), ) .map_err(VhdxError::ParseVhdxRegionEntry)?; let bat_entry = collected_entries.bat_entry; let mdr_entry = collected_entries.mdr_entry; let disk_spec = DiskSpec::new(&aligned, &mdr_entry).map_err(VhdxError::ParseVhdxMetadata)?; let bat_entries = BatEntry::collect_bat_entries(&aligned, &disk_spec, &bat_entry) .map_err(VhdxError::ReadBatEntry)?; Ok(Vhdx { aligned, vhdx_header, region_entries: collected_entries.region_entries, bat_entry, mdr_entry, disk_spec, bat_entries, current_offset: 0, first_write: true, }) } pub fn virtual_disk_size(&self) -> u64 { self.disk_spec.virtual_disk_size } } impl Read for Vhdx { /// Wrapper function to satisfy Read trait implementation for VHDx disk. /// Convert the offset to sector index and buffer length to sector count. fn read(&mut self, buf: &mut [u8]) -> IoResult { let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64); let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64; let result = io::read( &self.aligned, buf, &self.disk_spec, &self.bat_entries, sector_index, sector_count, ) .map_err(|e| { IoError::other(format!( "Failed reading {sector_count} sectors from VHDx at index {sector_index}: {e}" )) })?; self.current_offset = self.current_offset.checked_add(result as u64).unwrap(); Ok(result) } } impl Write for Vhdx { fn flush(&mut self) -> IoResult<()> { self.aligned.file_mut().flush() } /// Wrapper function to satisfy Write trait implementation for VHDx disk. /// Convert the offset to sector index and buffer length to sector count. fn write(&mut self, buf: &[u8]) -> IoResult { let sector_count = (buf.len() as u64).div_ceil(self.disk_spec.logical_sector_size as u64); let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64; if self.first_write { self.first_write = false; self.vhdx_header .update(&self.aligned) .map_err(|e| IoError::other(format!("Failed to update VHDx header: {e}")))?; } let result = io::write( &self.aligned, buf, &mut self.disk_spec, self.bat_entry.file_offset, &mut self.bat_entries, sector_index, sector_count, ) .map_err(|e| { IoError::other(format!( "Failed writing {sector_count} sectors on VHDx at index {sector_index}: {e}" )) })?; self.current_offset = self.current_offset.checked_add(result as u64).unwrap(); Ok(result) } } impl Seek for Vhdx { /// Wrapper function to satisfy Seek trait implementation for VHDx disk. /// Updates the offset field in the Vhdx struct. fn seek(&mut self, pos: SeekFrom) -> IoResult { let new_offset: Option = match pos { SeekFrom::Start(off) => Some(off), SeekFrom::End(off) => { if off < 0 { 0i64.checked_sub(off).and_then(|increment| { self.virtual_disk_size().checked_sub(increment as u64) }) } else { self.virtual_disk_size().checked_add(off as u64) } } SeekFrom::Current(off) => { if off < 0 { 0i64.checked_sub(off) .and_then(|increment| self.current_offset.checked_sub(increment as u64)) } else { self.current_offset.checked_add(off as u64) } } }; if let Some(o) = new_offset && o <= self.virtual_disk_size() { self.current_offset = o; return Ok(o); } Err(IoError::new( IoErrorKind::InvalidData, "Failed seek operation", )) } } impl Vhdx { pub(crate) fn physical_size(&self) -> result::Result { self.aligned .file() .metadata() .map(|m| m.len()) .map_err(crate::Error::GetFileMetadata) } } impl Clone for Vhdx { fn clone(&self) -> Self { Vhdx { aligned: self.aligned.try_clone().unwrap(), vhdx_header: self.vhdx_header.clone(), region_entries: self.region_entries.clone(), bat_entry: self.bat_entry, mdr_entry: self.mdr_entry, disk_spec: self.disk_spec.clone(), bat_entries: self.bat_entries.clone(), current_offset: self.current_offset, first_write: self.first_write, } } } impl AsRawFd for Vhdx { fn as_raw_fd(&self) -> RawFd { self.aligned.file().as_raw_fd() } } #[cfg(test)] mod tests { use std::fs; use super::*; use crate::formats::vhdx::test_util::create_dynamic_vhdx; /// An unaligned sector write under a forced O_DIRECT alignment must go /// through `AlignedFile`'s read-modify-write bounce (the data block and the /// BAT update both land at unaligned host offsets) and read back intact. #[test] fn unaligned_write_is_rmw() { let Some(tf) = create_dynamic_vhdx(16) else { eprintln!("skipping unaligned_write_is_rmw: qemu-img unavailable"); return; }; let file = fs::OpenOptions::new() .read(true) .write(true) .open(tf.as_path()) .unwrap(); let mut vhdx = Vhdx::new(file, false).unwrap(); // Force a non-zero alignment so all of vhdx's positioned I/O exercises // the bounce/RMW path even though the tempfile is not really O_DIRECT. vhdx.aligned = AlignedFile::with_alignment(vhdx.aligned.file().try_clone().unwrap(), 512); let sector = vhdx.disk_spec.logical_sector_size as usize; let data: Vec = (0..sector).map(|i| ((i + 1) % 251) as u8).collect(); // Write at virtual offset 0 (allocates a new data block + rewrites BAT). vhdx.seek(SeekFrom::Start(0)).unwrap(); assert_eq!(vhdx.write(&data).unwrap(), data.len()); vhdx.flush().unwrap(); // Read it back through a fresh, forced-alignment handle. let mut readback = vec![0u8; sector]; vhdx.seek(SeekFrom::Start(0)).unwrap(); assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len()); assert_eq!(readback, data); } #[test] fn header_update_survives_reopen() { let Some(tf) = create_dynamic_vhdx(16) else { eprintln!("skipping header_update_survives_reopen: qemu-img unavailable"); return; }; let data = [0xa5u8; 512]; { let file = fs::OpenOptions::new() .read(true) .write(true) .open(tf.as_path()) .unwrap(); let mut vhdx = Vhdx::new(file, false).unwrap(); vhdx.seek(SeekFrom::Start(0)).unwrap(); assert_eq!(vhdx.write(&data).unwrap(), data.len()); vhdx.flush().unwrap(); } let file = fs::OpenOptions::new() .read(true) .write(true) .open(tf.as_path()) .unwrap(); let mut vhdx = Vhdx::new(file, false).unwrap(); let mut readback = [0u8; 512]; vhdx.seek(SeekFrom::Start(0)).unwrap(); assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len()); assert_eq!(readback, data); } } cloud-hypervisor-53.0/block/src/formats/vhdx/test_util.rs000066400000000000000000000014211522476750100236550ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! Shared test helpers for VHDX image tests. use std::process::Command; use vmm_sys_util::tempfile::TempFile; /// Generate a small dynamic VHDX with `qemu-img`. Returns `None` (and the /// test is skipped) when `qemu-img` is unavailable, e.g. in minimal CI. pub(crate) fn create_dynamic_vhdx(size_mib: u64) -> Option { let tf = TempFile::new().unwrap(); let path = tf.as_path(); let status = Command::new("qemu-img") .args(["create", "-f", "vhdx", "-o", "subformat=dynamic"]) .arg(path) .arg(format!("{size_mib}M")) .status(); match status { Ok(s) if s.success() => Some(tf), _ => None, } } cloud-hypervisor-53.0/block/src/io/000077500000000000000000000000001522476750100172605ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/io/async_io.rs000066400000000000000000000130731522476750100214360ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::marker::PhantomData; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; mod aio_data_io; mod common; mod completion; mod guest_memory_target; mod operation; mod owned_io_buffer; #[cfg(feature = "io_uring")] mod uring_data_io; use std::{io, result}; pub use aio_data_io::AioDataIo; pub use completion::AsyncIoCompletion; pub use guest_memory_target::GuestMemoryTarget; pub use operation::AsyncIoOperation; pub use owned_io_buffer::OwnedIoBuffer; use thiserror::Error; #[cfg(feature = "io_uring")] pub use uring_data_io::UringDataIo; use vmm_sys_util::eventfd::EventFd; use crate::SECTOR_SIZE; #[derive(Error, Debug)] pub enum DiskFileError { /// Failed getting disk file size. #[error("Failed getting disk file size")] Size(#[source] io::Error), /// Failed creating a new AsyncIo. #[error("Failed creating a new AsyncIo")] NewAsyncIo(#[source] io::Error), /// Unsupported operation. #[error("Unsupported operation")] Unsupported, /// Resize failed #[error("Resize failed")] ResizeError(#[source] io::Error), #[error("Failed cloning disk file")] Clone(#[source] io::Error), } pub type DiskFileResult = result::Result; /// A wrapper for [`RawFd`] capturing the lifetime of a corresponding disk file. /// /// This fulfills the same role as [`BorrowedFd`] but is tailored to the limitations /// by some disk implementations, which wrap the effective [`File`] /// in an `Arc>`, making the use of [`BorrowedFd`] impossible. /// /// [`BorrowedFd`]: std::os::fd::BorrowedFd #[derive(Copy, Clone, Debug)] pub struct BorrowedDiskFd<'fd> { raw_fd: RawFd, _lifetime: PhantomData<&'fd OwnedFd>, } impl BorrowedDiskFd<'_> { pub(crate) fn new(raw_fd: RawFd) -> Self { Self { raw_fd, _lifetime: PhantomData, } } } impl AsRawFd for BorrowedDiskFd<'_> { fn as_raw_fd(&self) -> RawFd { self.raw_fd } } #[derive(Error, Debug)] pub enum AsyncIoError { /// Failed vectored reading from file. #[error("Failed vectored reading from file")] ReadVectored(#[source] io::Error), /// Failed vectored writing to file. #[error("Failed vectored writing to file")] WriteVectored(#[source] io::Error), /// Failed synchronizing file. #[error("Failed synchronizing file")] Fsync(#[source] io::Error), /// Failed punching hole. #[error("Failed punching hole")] PunchHole(#[source] io::Error), /// Failed writing zeroes. #[error("Failed writing zeroes")] WriteZeroes(#[source] io::Error), /// Failed submitting batch requests. #[error("Failed submitting batch requests")] SubmitBatchRequests(#[source] io::Error), } pub type AsyncIoResult = result::Result; pub trait AsyncIo: Send { fn notifier(&self) -> &EventFd; /// Submits one owned data operation. /// /// Takes ownership of `op`. /// Implementations that complete asynchronously must retain it until its /// completion is returned. fn submit_data_operation(&mut self, op: AsyncIoOperation) -> AsyncIoResult<()>; /// Submits a read from `offset` into guest memory. fn read_to_memory( &mut self, offset: libc::off_t, target: GuestMemoryTarget, user_data: u64, ) -> AsyncIoResult<()> { self.submit_data_operation(AsyncIoOperation::read_to_memory(offset, target, user_data)) } /// Submits a write to `offset` from guest memory. fn write_from_memory( &mut self, offset: libc::off_t, target: GuestMemoryTarget, user_data: u64, ) -> AsyncIoResult<()> { self.submit_data_operation(AsyncIoOperation::write_from_memory( offset, target, user_data, )) } /// Submits a read from `offset` into an owned host-memory buffer. fn read_to_vec( &mut self, offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64, ) -> AsyncIoResult<()> { self.submit_data_operation(AsyncIoOperation::read_to_vec(offset, buffer, user_data)) } /// Submits a write to `offset` from an owned host-memory buffer. fn write_from_vec( &mut self, offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64, ) -> AsyncIoResult<()> { self.submit_data_operation(AsyncIoOperation::write_from_vec(offset, buffer, user_data)) } fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()>; fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>; fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()>; /// Returns the next owned completion, if one is available. /// /// Read completions from owned host-memory buffers return that buffer here. fn next_completed_request(&mut self) -> Option; fn batch_requests_enabled(&self) -> bool { false } /// Submits a batch of owned data operations. /// /// Backends either accept the whole batch for eventual completion or return /// an error before taking ownership of any operation. fn submit_batch_requests(&mut self, batch_request: Vec) -> AsyncIoResult<()> { if batch_request.is_empty() { Ok(()) } else { Err(AsyncIoError::SubmitBatchRequests(io::Error::other( "batch requests are not supported by this backend", ))) } } fn alignment(&self) -> u64 { SECTOR_SIZE } } cloud-hypervisor-53.0/block/src/io/async_io/000077500000000000000000000000001522476750100210645ustar00rootroot00000000000000cloud-hypervisor-53.0/block/src/io/async_io/aio_data_io.rs000066400000000000000000000202511522476750100236620ustar00rootroot00000000000000// Copyright © 2023 Intel Corporation // // Copyright © 2023 Crusoe Energy Systems LLC // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::collections::{HashMap, VecDeque}; use std::os::fd::{AsRawFd, RawFd}; use std::{io, slice}; use log::warn; use vmm_sys_util::aio; use vmm_sys_util::eventfd::EventFd; use super::common::{duplicate_user_data_error, errno_result, validate_batch}; use super::{AsyncIoCompletion, AsyncIoOperation}; /// Retained Linux AIO queue for owned async data I/O operations. pub struct AioDataIo { // Keep this before `in_flight`: Rust drops fields in declaration order, so // dropping the context destroys kernel AIO state before retained // operations release the buffers referenced by their iovecs. ctx: aio::IoContext, // The `EventFd` for completion signals. eventfd: EventFd, // `in_flight` tracks every user_data value accepted by the kernel. Owned // data operations store `Some(op)` so their iovecs and backing buffers // remain valid until completion; metadata operations store `None`. in_flight: HashMap>, // `completions` holds locally produced completions and kernel events that // have been fetched but not yet returned to the caller. completions: VecDeque, } impl AioDataIo { /// Creates a Linux AIO context and its completion eventfd. pub fn new(queue_depth: u32) -> io::Result { Ok(Self { ctx: aio::IoContext::new(queue_depth)?, eventfd: EventFd::new(libc::EFD_NONBLOCK)?, in_flight: HashMap::new(), completions: VecDeque::new(), }) } /// Returns the eventfd signaled when completions are available. pub fn notifier(&self) -> &EventFd { &self.eventfd } #[allow(unused_unsafe)] fn submit_iocbs(ctx: &aio::IoContext, iocbs: &[&mut aio::IoControlBlock]) -> io::Result { // SAFETY: vmm_sys_util currently marks IoContext::submit safe, but // io_submit consumes raw pointers asynchronously. Callers must ensure // all iovec and buffer memory referenced by each iocb remains valid // until completion or failed submission. unsafe { ctx.submit(iocbs) } } /// Submits one owned read or write operation to the queue. /// /// Submission failures are converted into injected completions so callers /// can observe every accepted request through the normal completion path. pub fn submit_operation(&mut self, fd: RawFd, op: AsyncIoOperation) -> io::Result<()> { validate_batch( |user_data| self.in_flight.contains_key(&user_data), slice::from_ref(&op), )?; let user_data = op.user_data(); let iovecs = op.iovecs(); let opcode = if op.is_read() { aio::IOCB_CMD_PREADV } else { aio::IOCB_CMD_PWRITEV }; let mut iocb = aio::IoControlBlock { aio_fildes: fd.as_raw_fd() as u32, aio_lio_opcode: opcode as u16, aio_buf: iovecs.as_ptr() as u64, aio_nbytes: iovecs.len() as u64, aio_offset: op.offset(), aio_data: user_data, aio_flags: aio::IOCB_FLAG_RESFD, aio_resfd: self.eventfd.as_raw_fd() as u32, ..Default::default() }; self.in_flight.insert(user_data, Some(op)); let result = match Self::submit_iocbs(&self.ctx, &[&mut iocb]) { Ok(1) => return Ok(()), Ok(_) => -libc::EAGAIN, Err(e) => errno_result(&e), }; let buffer = self .in_flight .remove(&user_data) .flatten() .and_then(AsyncIoOperation::into_completion_buffer); self.inject_completion(AsyncIoCompletion::new(user_data, result, buffer)); Ok(()) } /// Submits an fsync operation carrying `user_data`. pub fn submit_fsync(&mut self, fd: RawFd, user_data: u64) -> io::Result<()> { if self.in_flight.contains_key(&user_data) { return Err(duplicate_user_data_error(user_data)); } let mut iocb = aio::IoControlBlock { aio_fildes: fd.as_raw_fd() as u32, aio_lio_opcode: aio::IOCB_CMD_FSYNC as u16, aio_data: user_data, aio_flags: aio::IOCB_FLAG_RESFD, aio_resfd: self.eventfd.as_raw_fd() as u32, ..Default::default() }; self.in_flight.insert(user_data, None); let result = match Self::submit_iocbs(&self.ctx, &[&mut iocb]) { Ok(1) => return Ok(()), Ok(_) => -libc::EAGAIN, Err(e) => errno_result(&e), }; self.in_flight.remove(&user_data); self.inject_completion(AsyncIoCompletion::new(user_data, result, None)); Ok(()) } /// Injects a completion that did not come from a kernel AIO event. /// /// The notifier is signaled so callers can drain it with /// [`Self::next_completion`]. pub fn inject_completion(&mut self, completion: AsyncIoCompletion) { self.completions.push_back(completion); self.eventfd.write(1).unwrap(); } /// Returns the next kernel or injected completion if one is available. /// /// Consuming a kernel completion returns ownership of any buffer retained /// by the corresponding operation. pub fn next_completion(&mut self) -> Option { if self.completions.is_empty() { let mut events = [aio::IoEvent::default(); 32]; let rc = match self.ctx.get_events(0, &mut events, None) { Ok(rc) => rc, Err(e) => { warn!("Linux AIO get_events failed: {e}"); return None; } }; for event in &events[..rc] { self.completions.push_back(AsyncIoCompletion::new( event.data, event.res as i32, self.in_flight .remove(&event.data) .flatten() .and_then(AsyncIoOperation::into_completion_buffer), )); } } self.completions.pop_front() } } #[cfg(test)] mod tests { use std::io::{self, Write}; use std::os::fd::AsRawFd; use std::thread::sleep; use std::time::Duration; use vmm_sys_util::tempfile::TempFile; use super::AioDataIo; use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, OwnedIoBuffer}; fn wait_for_completion(data_io: &mut AioDataIo) -> AsyncIoCompletion { for _ in 0..1000 { if let Some(completion) = data_io.next_completion() { return completion; } sleep(Duration::from_millis(1)); } panic!("timed out waiting for Linux AIO completion"); } #[test] fn aio_rejects_duplicate_user_data_for_metadata_ops() { let mut file = TempFile::new().unwrap().into_file(); file.write_all(&[0xa5; 512]).unwrap(); let fd = file.as_raw_fd(); let mut data_io = AioDataIo::new(8).unwrap(); data_io .submit_operation( fd, AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), 7), ) .unwrap(); assert_eq!( data_io.submit_fsync(fd, 7).unwrap_err().kind(), io::ErrorKind::AlreadyExists ); let completion = wait_for_completion(&mut data_io); assert_eq!(completion.user_data, 7); assert_eq!(completion.result, 512); assert_eq!( completion.buffer.unwrap().as_slice(), [0xa5; 512].as_slice() ); } #[test] fn aio_injected_completion_uses_completion_path() { let mut data_io = AioDataIo::new(8).unwrap(); data_io.inject_completion(AsyncIoCompletion::new(9, -libc::EIO, None)); let completion = data_io.next_completion().unwrap(); assert_eq!(completion.user_data, 9); assert_eq!(completion.result, -libc::EIO); assert!(completion.buffer.is_none()); assert!(data_io.next_completion().is_none()); } } cloud-hypervisor-53.0/block/src/io/async_io/common.rs000066400000000000000000000022351522476750100227240ustar00rootroot00000000000000// Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause //! Helpers used by both aio and uring async io. use std::collections::HashSet; use std::io; use super::AsyncIoOperation; /// Converts an I/O error into the negative errno form used in completions. pub(super) fn errno_result(error: &io::Error) -> i32 { -error.raw_os_error().unwrap_or(libc::EIO) } /// Builds the error returned when a new request reuses in-flight `user_data`. pub(super) fn duplicate_user_data_error(user_data: u64) -> io::Error { io::Error::new( io::ErrorKind::AlreadyExists, format!("duplicate async I/O user_data {user_data}"), ) } /// Validates that a batch has unique `user_data` not already in flight. pub(super) fn validate_batch(mut is_in_flight: F, batch: &[AsyncIoOperation]) -> io::Result<()> where F: FnMut(u64) -> bool, { let mut seen = HashSet::with_capacity(batch.len()); for op in batch { let user_data = op.user_data(); if is_in_flight(user_data) || !seen.insert(user_data) { return Err(duplicate_user_data_error(user_data)); } } Ok(()) } cloud-hypervisor-53.0/block/src/io/async_io/completion.rs000066400000000000000000000025671522476750100236150ustar00rootroot00000000000000// Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use super::{AsyncIoOperation, OwnedIoBuffer}; /// Completion returned by an owned async I/O backend. /// /// The completion carries the caller provided `user_data`, the result, /// and any owned buffer that can now be dropped. #[derive(Debug)] pub struct AsyncIoCompletion { /// Caller provided identifier associated with the submitted operation. pub user_data: u64, /// I/O result reported by the backend. /// /// Successful operations report a non-negative byte count. Failed /// operations report a negative errno value. pub result: i32, /// The backing buffer that can now be dropped or re-used. pub buffer: Option, } impl AsyncIoCompletion { /// Creates a completion from its parts. pub fn new(user_data: u64, result: i32, buffer: Option) -> Self { Self { user_data, result, buffer, } } /// Creates a completion by consuming the operation that just completed. /// /// This returns ownership of any completion buffer carried by the /// operation. pub fn from_operation(op: AsyncIoOperation, result: i32) -> Self { let user_data = op.user_data(); Self::new(user_data, result, op.into_completion_buffer()) } } cloud-hypervisor-53.0/block/src/io/async_io/guest_memory_target.rs000066400000000000000000000176061522476750100255310ustar00rootroot00000000000000// Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::cmp::min; use std::fmt; use std::sync::Arc; use smallvec::SmallVec; use vm_memory::bitmap::Bitmap; use vm_memory::{Address, Bytes, GuestAddress, GuestMemory, GuestMemoryError, GuestMemoryMmap}; trait GuestMemoryTargetOwner: Send + Sync { fn iovec_for_range( &self, addr: GuestAddress, len: usize, ) -> Result; fn write_guest_slice(&self, buf: &[u8], addr: GuestAddress) -> Result<(), GuestMemoryError>; fn read_guest_slice(&self, buf: &mut [u8], addr: GuestAddress) -> Result<(), GuestMemoryError>; } impl GuestMemoryTargetOwner for GuestMemoryMmap where B: Bitmap + Send + Sync + 'static, { fn iovec_for_range( &self, addr: GuestAddress, len: usize, ) -> Result { let slice = self.get_slice(addr, len)?; let guard = slice.ptr_guard_mut(); Ok(libc::iovec { iov_base: guard.as_ptr().cast(), iov_len: len, }) } fn write_guest_slice(&self, buf: &[u8], addr: GuestAddress) -> Result<(), GuestMemoryError> { >::write_slice(self, buf, addr) } fn read_guest_slice(&self, buf: &mut [u8], addr: GuestAddress) -> Result<(), GuestMemoryError> { >::read_slice(self, buf, addr) } } /// Retains a guest-memory Arc and the validated ranges used for I/O. /// /// Keeping the guest memory arc with the ranges guarantees that the iovecs /// remain valid for as long as Self is alive. The iovecs are also shared with /// the kernel and must be stable. pub struct GuestMemoryTarget { owner: Arc, ranges: SmallVec<[(GuestAddress, usize); 1]>, iovecs: Vec, } // SAFETY: GuestMemoryTarget owns an Arc to the guest memory backing and // holds its iovecs in a heap allocation, so moving the target leaves the // iovec addresses (and the host pointers they reference) stable. unsafe impl Send for GuestMemoryTarget {} impl GuestMemoryTarget { /// Creates a new `GuestMemoryTarget`. /// /// The memory Arc is retained for the life of `Self`, making this /// appropriate for asynchronous I/O operations on the specified ranges. pub fn new( mem: Arc>, ranges: &[(GuestAddress, u32)], ) -> Result where B: Bitmap + Send + Sync + 'static, { let retained_ranges: SmallVec<[(GuestAddress, usize); 1]> = ranges .iter() .copied() .filter(|&(_, len)| len != 0) .map(|(addr, len)| { let len = len as usize; mem.get_slice(addr, len)?; Ok((addr, len)) }) .collect::, GuestMemoryError>>()?; // iovec_for_range cannot fail: each range was just validated by // get_slice above and the Arc keeps the mapping alive. let iovecs: Vec = retained_ranges .iter() .map(|&(addr, len)| { mem.iovec_for_range(addr, len) .expect("range validated above and retained by owner Arc") }) .collect(); Ok(Self { owner: mem, ranges: retained_ranges, iovecs, }) } /// Returns the raw iovecs to be passed to the kernel for asynchronous I/O. pub(super) fn iovecs(&self) -> &[libc::iovec] { &self.iovecs } /// Returns the total length of the ranges specified at creation. pub fn total_len(&self) -> usize { self.ranges.iter().map(|(_, len)| len).sum() } pub(crate) fn write_bytes_at(&self, start: usize, data: &[u8]) -> Result<(), GuestMemoryError> { self.for_each_range(start, data.len(), |addr, offset, len| { self.owner .write_guest_slice(&data[offset..offset + len], addr) }) } pub(crate) fn read_bytes_at( &self, start: usize, data: &mut [u8], ) -> Result<(), GuestMemoryError> { self.for_each_range(start, data.len(), |addr, offset, len| { self.owner .read_guest_slice(&mut data[offset..offset + len], addr) }) } pub(crate) fn fill_zeroes_at(&self, start: usize, len: usize) -> Result<(), GuestMemoryError> { let zeroes = [0u8; 4096]; self.for_each_range(start, len, |addr, _, mut len| { let mut offset = 0usize; while len > 0 { let count = min(len, zeroes.len()); let addr = addr .checked_add(offset as u64) .ok_or(GuestMemoryError::InvalidGuestAddress(addr))?; self.owner.write_guest_slice(&zeroes[..count], addr)?; offset += count; len -= count; } Ok(()) }) } fn for_each_range(&self, start: usize, len: usize, mut f: F) -> Result<(), GuestMemoryError> where F: FnMut(GuestAddress, usize, usize) -> Result<(), GuestMemoryError>, { self.validate_range(start, len)?; let mut copied = 0usize; let mut pos = 0usize; for &(addr, range_len) in self.ranges.iter() { let range_end = pos + range_len; if range_end <= start || copied == len { pos = range_end; continue; } let range_start = start.saturating_sub(pos); let count = min(range_len - range_start, len - copied); let addr = addr .checked_add(range_start as u64) .ok_or(GuestMemoryError::InvalidGuestAddress(addr))?; f(addr, copied, count)?; copied += count; if copied == len { break; } pos = range_end; } if copied != len { return Err(GuestMemoryError::PartialBuffer { expected: len, completed: copied, }); } Ok(()) } fn validate_range(&self, start: usize, len: usize) -> Result<(), GuestMemoryError> { let total_len = self.total_len(); if start <= total_len && let Some(end) = start.checked_add(len) && end <= total_len { return Ok(()); } Err(GuestMemoryError::PartialBuffer { expected: len, completed: total_len.saturating_sub(start).min(len), }) } } impl fmt::Debug for GuestMemoryTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug = f.debug_struct("GuestMemoryTarget"); debug.field("ranges", &self.ranges.len()); debug .field("iovecs", &self.iovecs.len()) .finish_non_exhaustive() } } #[cfg(test)] mod tests { use std::sync::Arc; use vm_memory::{GuestAddress, GuestMemoryMmap}; use super::GuestMemoryTarget; #[test] fn iovecs_survive_move() { // The iovec array must live on the heap so its address stays valid // after the GuestMemoryTarget (and the AsyncIoOperation that owns it) // is moved into an in-flight map. Capture the addresses before the // move and confirm they still match afterwards. let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 4096)]).unwrap()); let target = GuestMemoryTarget::new(mem, &[(GuestAddress(0), 512)]).unwrap(); let iovec_ptr_before = target.iovecs().as_ptr() as usize; let iov_base_before = target.iovecs()[0].iov_base as usize; let moved = Box::new(target); assert_eq!(moved.iovecs().as_ptr() as usize, iovec_ptr_before); assert_eq!(moved.iovecs()[0].iov_base as usize, iov_base_before); assert_eq!(moved.iovecs().len(), 1); } } cloud-hypervisor-53.0/block/src/io/async_io/operation.rs000066400000000000000000000241331522476750100234350ustar00rootroot00000000000000// Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::io; use std::ops::Range; use super::{AsyncIoError, AsyncIoResult, GuestMemoryTarget, OwnedIoBuffer}; /// A single async IO operation. /// /// Each operation owns or retains the memory target for the duration of the /// operation so backends can submit it to the kernel or copy through safe helper /// methods. #[derive(Debug)] pub enum AsyncIoOperation { /// Read from disk into guest memory. ReadToMemory { /// Disk offset for the operation. offset: libc::off_t, /// Guest-memory destination. target: GuestMemoryTarget, /// Caller-provided completion identifier. user_data: u64, }, /// Write from guest memory to disk. WriteFromMemory { /// Disk offset for the operation. offset: libc::off_t, /// Guest-memory source. target: GuestMemoryTarget, /// Caller-provided completion identifier. user_data: u64, }, /// Read from disk into an owned host-memory buffer. ReadToVec { /// Disk offset for the operation. offset: libc::off_t, /// Owned destination buffer. buffer: OwnedIoBuffer, /// Caller-provided completion identifier. user_data: u64, }, /// Write from an owned host-memory buffer to disk. WriteFromVec { /// Disk offset for the operation. offset: libc::off_t, /// Owned source buffer. buffer: OwnedIoBuffer, /// Caller-provided completion identifier. user_data: u64, }, } impl AsyncIoOperation { /// Creates an operation that reads from disk into guest memory. pub fn read_to_memory(offset: libc::off_t, target: GuestMemoryTarget, user_data: u64) -> Self { Self::ReadToMemory { offset, target, user_data, } } /// Creates an operation that writes from guest memory to disk. pub fn write_from_memory( offset: libc::off_t, target: GuestMemoryTarget, user_data: u64, ) -> Self { Self::WriteFromMemory { offset, target, user_data, } } /// Creates an operation that reads from disk into an owned buffer. pub fn read_to_vec(offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64) -> Self { Self::ReadToVec { offset, buffer, user_data, } } /// Creates an operation that writes from an owned buffer to disk. pub fn write_from_vec(offset: libc::off_t, buffer: OwnedIoBuffer, user_data: u64) -> Self { Self::WriteFromVec { offset, buffer, user_data, } } /// Returns the value provided at construction. pub fn user_data(&self) -> u64 { match self { Self::ReadToMemory { user_data, .. } | Self::WriteFromMemory { user_data, .. } | Self::ReadToVec { user_data, .. } | Self::WriteFromVec { user_data, .. } => *user_data, } } /// Returns the disk offset for this operation. pub fn offset(&self) -> libc::off_t { match self { Self::ReadToMemory { offset, .. } | Self::WriteFromMemory { offset, .. } | Self::ReadToVec { offset, .. } | Self::WriteFromVec { offset, .. } => *offset, } } /// Updates the disk offset for this operation. pub fn set_offset(&mut self, new_offset: libc::off_t) { match self { Self::ReadToMemory { offset, .. } | Self::WriteFromMemory { offset, .. } | Self::ReadToVec { offset, .. } | Self::WriteFromVec { offset, .. } => *offset = new_offset, } } /// Returns whether this operation reads from disk. pub fn is_read(&self) -> bool { matches!(self, Self::ReadToMemory { .. } | Self::ReadToVec { .. }) } /// Rejects an operation whose byte range falls outside a disk of `size` bytes. /// /// Returns the read/write-specific `AsyncIoError` variant, carrying an /// `InvalidData` error, when the offset overflows or `offset + len` /// exceeds `size`. pub(crate) fn validate_bounds(&self, size: u64) -> AsyncIoResult<()> { let bounds_error = || { let error = io::Error::new( io::ErrorKind::InvalidData, format!( "Invalid request offset {} and length {}, can't exceed file size {}", self.offset(), self.total_len(), size ), ); if self.is_read() { AsyncIoError::ReadVectored(error) } else { AsyncIoError::WriteVectored(error) } }; let offset = u64::try_from(self.offset()).map_err(|_| bounds_error())?; let len = u64::try_from(self.total_len()).map_err(|_| bounds_error())?; let end = offset.checked_add(len).ok_or_else(bounds_error)?; if end > size { return Err(bounds_error()); } Ok(()) } /// Returns the retained iovec array for kernel submission. /// /// The iovec pointers are valid while this operation is alive. pub fn iovecs(&self) -> &[libc::iovec] { match self { Self::ReadToMemory { target, .. } | Self::WriteFromMemory { target, .. } => { target.iovecs() } Self::ReadToVec { buffer, .. } | Self::WriteFromVec { buffer, .. } => buffer.iovecs(), } } /// Returns the total number of bytes described by the operation iovecs. pub fn total_len(&self) -> usize { match self { Self::ReadToMemory { target, .. } | Self::WriteFromMemory { target, .. } => { target.total_len() } Self::ReadToVec { buffer, .. } | Self::WriteFromVec { buffer, .. } => { buffer.total_len() } } } fn checked_range(total_len: usize, start: usize, len: usize) -> io::Result> { if start <= total_len && let Some(end) = start.checked_add(len) && end <= total_len { return Ok(start..end); } Err(io::Error::new( io::ErrorKind::InvalidInput, "async I/O buffer range out of bounds", )) } /// Copies bytes into a read operation at `start`. pub(crate) fn write_bytes_at(&mut self, start: usize, data: &[u8]) -> io::Result<()> { match self { Self::ReadToMemory { target, .. } => { target.write_bytes_at(start, data).map_err(io::Error::other) } Self::ReadToVec { buffer, .. } => { let range = Self::checked_range(buffer.total_len(), start, data.len())?; buffer.as_mut_slice()[range].copy_from_slice(data); Ok(()) } Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => Err(io::Error::new( io::ErrorKind::InvalidInput, "cannot write into a write operation", )), } } /// Fills a read operation with zeroes at `start`. pub(crate) fn fill_zeroes_at(&mut self, start: usize, len: usize) -> io::Result<()> { match self { Self::ReadToMemory { target, .. } => { target.fill_zeroes_at(start, len).map_err(io::Error::other) } Self::ReadToVec { buffer, .. } => { let range = Self::checked_range(buffer.total_len(), start, len)?; buffer.as_mut_slice()[range].fill(0); Ok(()) } Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => Err(io::Error::new( io::ErrorKind::InvalidInput, "cannot write into a write operation", )), } } /// Copies bytes out of a write operation at `start`. pub(crate) fn read_bytes_at(&self, start: usize, data: &mut [u8]) -> io::Result<()> { match self { Self::WriteFromMemory { target, .. } => { target.read_bytes_at(start, data).map_err(io::Error::other) } Self::WriteFromVec { buffer, .. } => { let range = Self::checked_range(buffer.total_len(), start, data.len())?; data.copy_from_slice(&buffer.as_slice()[range]); Ok(()) } Self::ReadToMemory { .. } | Self::ReadToVec { .. } => Err(io::Error::new( io::ErrorKind::InvalidInput, "cannot read from a read operation", )), } } /// Consumes the operation and returns the buffer needed by its completion. /// /// Only `ReadToVec` operations return a buffer because callers need the /// data they read. pub fn into_completion_buffer(self) -> Option { match self { Self::ReadToVec { buffer, .. } => Some(buffer), Self::ReadToMemory { .. } | Self::WriteFromMemory { .. } | Self::WriteFromVec { .. } => None, } } } #[cfg(test)] mod tests { use super::*; fn read_op(offset: libc::off_t, len: usize) -> AsyncIoOperation { AsyncIoOperation::read_to_vec(offset, OwnedIoBuffer::from_vec(vec![0u8; len]), 0) } fn write_op(offset: libc::off_t, len: usize) -> AsyncIoOperation { AsyncIoOperation::write_from_vec(offset, OwnedIoBuffer::from_vec(vec![0u8; len]), 0) } #[test] fn accepts_operation_exactly_filling_size() { read_op(0, 512).validate_bounds(512).unwrap(); } #[test] fn rejects_read_straddling_size() { assert!(matches!( read_op(256, 512).validate_bounds(512), Err(AsyncIoError::ReadVectored(_)) )); } #[test] fn rejects_write_straddling_size() { assert!(matches!( write_op(256, 512).validate_bounds(512), Err(AsyncIoError::WriteVectored(_)) )); } #[test] fn rejects_offset_at_size() { assert!(read_op(512, 1).validate_bounds(512).is_err()); } } cloud-hypervisor-53.0/block/src/io/async_io/owned_io_buffer.rs000066400000000000000000000132461522476750100245740ustar00rootroot00000000000000// Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::alloc::{Layout, alloc_zeroed, dealloc}; use std::{fmt, io, slice}; // Storage owned by an async I/O request for host-memory buffers. // // `Vec` is used when ordinary vector storage is sufficient. `Aligned` is used // when the backend requires an alignment that a normal `Vec` cannot // guarantee. enum OwnedIoBufferStorage { // Buffer backed by a standard `Vec`. Vec(Vec), // Buffer backed by an explicitly aligned allocation. Aligned { // Pointer returned by `alloc_zeroed` for `layout`. ptr: *mut u8, // Layout used to allocate and deallocate `ptr`. layout: Layout, // Logical buffer length exposed to I/O. len: usize, }, } // SAFETY: OwnedIoBufferStorage owns its allocation exclusively. Moving it to // another thread transfers that ownership. unsafe impl Send for OwnedIoBufferStorage {} impl OwnedIoBufferStorage { fn new(len: usize, alignment: usize) -> io::Result { if alignment <= 1 { return Ok(Self::Vec(vec![0; len])); } let alloc_len = len.max(1).next_multiple_of(alignment); let layout = Layout::from_size_align(alloc_len, alignment) .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; // SAFETY: layout has non-zero size because alloc_len is at least 1. let ptr = unsafe { alloc_zeroed(layout) }; if ptr.is_null() { return Err(io::Error::new( io::ErrorKind::OutOfMemory, "alloc_zeroed returned null", )); } Ok(Self::Aligned { ptr, layout, len }) } fn as_mut_ptr(&mut self) -> *mut u8 { match self { Self::Vec(buf) => buf.as_mut_ptr(), Self::Aligned { ptr, .. } => *ptr, } } fn as_slice(&self) -> &[u8] { match self { Self::Vec(buf) => buf.as_slice(), Self::Aligned { ptr, len, .. } => { // SAFETY: alloc_zeroed initialized `len` bytes at `ptr` and the // allocation is owned by Self. unsafe { slice::from_raw_parts(*ptr, *len) } } } } fn as_mut_slice(&mut self) -> &mut [u8] { match self { Self::Vec(buf) => buf.as_mut_slice(), Self::Aligned { ptr, len, .. } => { // SAFETY: alloc_zeroed initialized `len` bytes at `ptr`, // &mut self ensures unique access, and the allocation is // owned by Self. unsafe { slice::from_raw_parts_mut(*ptr, *len) } } } } } impl Drop for OwnedIoBufferStorage { fn drop(&mut self) { if let Self::Aligned { ptr, layout, .. } = self { // SAFETY: ptr was allocated by alloc_zeroed with this layout and is // solely owned by Self. unsafe { dealloc(*ptr, *layout) }; } } } impl fmt::Debug for OwnedIoBufferStorage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Vec(buf) => f.debug_tuple("Vec").field(&buf.len()).finish(), Self::Aligned { len, layout, .. } => f .debug_struct("Aligned") .field("len", len) .field("layout", layout) .finish(), } } } /// Owns host-memory buffer storage and the iovec array that points into it. /// /// The retained iovec is valid for as long as this value is alive. /// When used for Async I/O this struct must remain valid for the duration of the op. #[derive(Debug)] pub struct OwnedIoBuffer { storage: OwnedIoBufferStorage, iovecs: Vec, } // SAFETY: OwnedIoBuffer owns the storage referenced by its single iovec, and moving the buffer // keeps the allocation address stable. unsafe impl Send for OwnedIoBuffer {} impl OwnedIoBuffer { /// Creates a zeroed buffer with the requested logical length and alignment. /// /// An alignment of 0 or 1 uses ordinary `Vec` storage. Larger alignments use an explicitly /// aligned allocation whose allocated size may be rounded up while the exposed slice length /// remains `len`. pub fn new(len: usize, alignment: usize) -> io::Result { let mut storage = OwnedIoBufferStorage::new(len, alignment)?; let iovec = libc::iovec { iov_base: storage.as_mut_ptr().cast(), iov_len: len, }; Ok(Self { storage, iovecs: vec![iovec], }) } /// Creates an owned I/O buffer from an existing `Vec`. /// /// The generated iovec covers the full vector length and remains valid /// until the OwnedIoBuffer is dropped. pub fn from_vec(mut buf: Vec) -> Self { let iovec = libc::iovec { iov_base: buf.as_mut_ptr().cast(), iov_len: buf.len(), }; Self { storage: OwnedIoBufferStorage::Vec(buf), iovecs: vec![iovec], } } /// Returns the logical buffer contents. pub fn as_slice(&self) -> &[u8] { self.storage.as_slice() } /// Returns the logical buffer contents mutably. pub fn as_mut_slice(&mut self) -> &mut [u8] { self.storage.as_mut_slice() } /// Returns the retained iovec array for kernel submission. /// /// The iovec pointers remain valid while this buffer is alive. pub fn iovecs(&self) -> &[libc::iovec] { &self.iovecs } /// Returns the total number of bytes described by the retained iovecs. pub fn total_len(&self) -> usize { self.iovecs.iter().map(|iov| iov.iov_len).sum() } } cloud-hypervisor-53.0/block/src/io/async_io/uring_data_io.rs000066400000000000000000000352001522476750100242360ustar00rootroot00000000000000// Copyright © 2021 Intel Corporation // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::collections::{HashMap, VecDeque}; use std::os::fd::{AsRawFd, RawFd}; use std::{io, mem}; use io_uring::{IoUring, opcode, squeue, types}; use log::{error, warn}; use vmm_sys_util::eventfd::EventFd; use super::common::{duplicate_user_data_error, validate_batch}; use super::{AsyncIoCompletion, AsyncIoOperation}; /// `io_uring` wrapper for async I/O. /// /// Holds the `IoUring` and its `EventFd`. Tracks ops that are pending. pub struct UringDataIo { io_uring: IoUring, // The `EventFd` for completion signals. eventfd: EventFd, // `in_flight` tracks every user_data value accepted by the kernel. Owned // data operations store `Some(op)` so their iovecs and backing buffers // remain valid until completion; metadata operations store `None`. in_flight: HashMap>, // `injected` holds locally produced completions so synchronous failures // and short-circuited requests use the same drain path as kernel CQEs. injected: VecDeque, // `needs_submit_retry` is set when SQEs have been published to the ring, // but the submit syscall failed before confirming kernel ownership. needs_submit_retry: bool, } impl UringDataIo { /// Creates an io_uring queue and registers its completion eventfd. pub fn new(ring_depth: u32) -> io::Result { let io_uring = IoUring::new(ring_depth)?; let eventfd = EventFd::new(libc::EFD_NONBLOCK)?; io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?; Ok(Self { io_uring, eventfd, in_flight: HashMap::new(), injected: VecDeque::new(), needs_submit_retry: false, }) } /// Returns the eventfd signaled when completions are available. pub fn notifier(&self) -> &EventFd { &self.eventfd } /// Submits one owned read or write operation to the queue. pub fn submit_operation(&mut self, fd: RawFd, op: AsyncIoOperation) -> io::Result<()> { self.submit_batch(fd, vec![op]) } fn reserve_user_data(&mut self, user_data: u64) -> io::Result<()> { if self.in_flight.contains_key(&user_data) { return Err(duplicate_user_data_error(user_data)); } self.in_flight.insert(user_data, None); Ok(()) } fn submit_kernel_entry(&mut self, user_data: u64, entry: &squeue::Entry) -> io::Result<()> { self.reserve_user_data(user_data)?; let (submitter, mut sq, _) = self.io_uring.split(); // SAFETY: the entry has no caller-owned buffer. `user_data` is retained // in `in_flight` until the CQE is consumed. if let Err(e) = unsafe { sq.push(entry) } { self.in_flight.remove(&user_data); return Err(io::Error::other(format!("Submission queue is full: {e:?}"))); } sq.sync(); match submitter.submit() { Ok(_) => self.needs_submit_retry = false, Err(e) => { self.needs_submit_retry = true; warn!("io_uring submit failed after SQE was published: {e}"); self.eventfd.write(1).unwrap(); } } Ok(()) } /// Submits a batch of owned read and write operations. /// /// If the io_uring submission queue cannot accept the whole batch, each /// operation is completed locally with `-EAGAIN` so callers can observe /// every request through the normal completion path. pub fn submit_batch(&mut self, fd: RawFd, batch: Vec) -> io::Result<()> { if batch.is_empty() { return Ok(()); } validate_batch(|user_data| self.in_flight.contains_key(&user_data), &batch)?; let (submitter, mut sq, _) = self.io_uring.split(); let available = sq.capacity() - sq.len(); if batch.len() > available { // Not enough space for the batch. // Drop sq, which will re-publish an unmodified tail pointer drop(sq); for op in batch { self.injected .push_back(AsyncIoCompletion::from_operation(op, -libc::EAGAIN)); } self.eventfd.write(1).unwrap(); return Ok(()); } let mut signal_completion = false; let mut batch = batch.into_iter(); while let Some(op) = batch.next() { let user_data = op.user_data(); let entry = Self::build_entry(fd, &op); self.in_flight.insert(user_data, Some(op)); // SAFETY: the SQ capacity was just checked. Every iovec's pointer is retained in // self.in_flight before the SQ tail is advanced by sync or drop. in_flight only // drops the memory after a completion. if let Err(e) = unsafe { sq.push(&entry) } { Self::handle_push_failure( &mut self.in_flight, &mut self.injected, user_data, batch.by_ref(), &e, ); signal_completion = true; break; } } sq.sync(); match submitter.submit() { Ok(_) => self.needs_submit_retry = false, Err(e) => { self.needs_submit_retry = true; warn!("io_uring submit failed after SQEs were published: {e}"); signal_completion = true; } } if signal_completion { self.eventfd.write(1).unwrap(); } Ok(()) } #[cold] fn handle_push_failure( in_flight: &mut HashMap>, injected: &mut VecDeque, user_data: u64, remaining: impl Iterator, error: &squeue::PushError, ) { // Since capacity was just checked, this should only happen if the ring // state changed unexpectedly. Keep all affected operations memory safe // by returning local completions through the normal path. let op = in_flight .remove(&user_data) .flatten() .expect("pending operation missing after failed push"); injected.push_back(AsyncIoCompletion::from_operation(op, -libc::EAGAIN)); for op in remaining { injected.push_back(AsyncIoCompletion::from_operation(op, -libc::EAGAIN)); } warn!("io_uring submission queue became full after capacity check: {error:?}"); } fn build_entry(fd: RawFd, op: &AsyncIoOperation) -> squeue::Entry { let iovecs = op.iovecs(); let fd = types::Fd(fd); if op.is_read() { opcode::Readv::new(fd, iovecs.as_ptr(), iovecs.len() as u32) .offset(op.offset() as u64) .build() .user_data(op.user_data()) } else { opcode::Writev::new(fd, iovecs.as_ptr(), iovecs.len() as u32) .offset(op.offset() as u64) .build() .user_data(op.user_data()) } } /// Submits an io_uring NOP carrying `user_data`. pub fn submit_nop(&mut self, user_data: u64) -> io::Result<()> { self.submit_kernel_entry(user_data, &opcode::Nop::new().build().user_data(user_data)) } /// Submits an fsync operation carrying `user_data`. pub fn submit_fsync(&mut self, fd: RawFd, user_data: u64) -> io::Result<()> { self.submit_kernel_entry( user_data, &opcode::Fsync::new(types::Fd(fd)) .build() .user_data(user_data), ) } /// Submits a fallocate operation carrying `user_data`. pub fn submit_fallocate( &mut self, fd: RawFd, offset: u64, length: u64, mode: i32, user_data: u64, ) -> io::Result<()> { self.submit_kernel_entry( user_data, &opcode::Fallocate::new(types::Fd(fd), length) .offset(offset) .mode(mode) .build() .user_data(user_data), ) } /// Injects a completion that did not come from a kernel CQE. /// /// The notifier is signaled so callers can drain it with /// [`Self::next_completion`]. pub fn inject_completion(&mut self, completion: AsyncIoCompletion) { self.injected.push_back(completion); self.eventfd.write(1).unwrap(); } /// Returns the next kernel or injected completion if one is available. /// /// Consuming a kernel completion returns ownership of any buffer retained /// by the corresponding operation. pub fn next_completion(&mut self) -> Option { if self.needs_submit_retry { match self.io_uring.submitter().submit() { Ok(_) => self.needs_submit_retry = false, Err(e) => warn!("io_uring retry submit failed for retained SQEs: {e}"), } } if let Some(entry) = self.io_uring.completion().next() { let user_data = entry.user_data(); return Some(AsyncIoCompletion::new( user_data, entry.result(), self.in_flight .remove(&user_data) .flatten() .and_then(AsyncIoOperation::into_completion_buffer), )); } self.injected.pop_front() } } impl Drop for UringDataIo { fn drop(&mut self) { // Closing the ring fd does not cancel io_uring ops that have started. // Wait for CQEs before releasing retained iovecs. if self.needs_submit_retry { if let Err(e) = self.io_uring.submitter().submit() { warn!("io_uring drain submit failed for retained SQEs: {e}"); } self.needs_submit_retry = false; } let max_drain_iterations = self.in_flight.len().saturating_mul(2); let mut drain_iterations = 0; while !self.in_flight.is_empty() { if drain_iterations == max_drain_iterations { error!( "io_uring drain abandoned with {} operations still in flight after {} drain iterations", self.in_flight.len(), drain_iterations ); // Keep retained buffers mapped if the ring cannot be drained. mem::forget(mem::take(&mut self.in_flight)); break; } drain_iterations += 1; if let Some(entry) = self.io_uring.completion().next() { self.in_flight.remove(&entry.user_data()); continue; } // No completion ready: block in the kernel until at least one is. if let Err(e) = self.io_uring.submitter().submit_and_wait(1) { if e.kind() == io::ErrorKind::Interrupted { continue; } error!( "io_uring drain abandoned with {} operations still in flight: {e}", self.in_flight.len() ); // Keep retained buffers mapped if the ring cannot be drained. mem::forget(mem::take(&mut self.in_flight)); break; } } } } #[cfg(test)] mod tests { use std::io; use std::os::fd::AsRawFd; use std::thread::sleep; use std::time::Duration; use vmm_sys_util::tempfile::TempFile; use super::UringDataIo; use crate::async_io::{AsyncIoCompletion, AsyncIoOperation, OwnedIoBuffer}; fn wait_for_completion(data_io: &mut UringDataIo) -> AsyncIoCompletion { for _ in 0..1000 { if let Some(completion) = data_io.next_completion() { return completion; } sleep(Duration::from_millis(1)); } panic!("timed out waiting for io_uring completion"); } #[test] fn uring_rejects_duplicate_user_data_for_metadata_ops() { let file = TempFile::new().unwrap().into_file(); file.set_len(512).unwrap(); let fd = file.as_raw_fd(); let mut data_io = UringDataIo::new(8).unwrap(); data_io .submit_operation( fd, AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), 7), ) .unwrap(); assert_eq!( data_io.submit_fsync(fd, 7).unwrap_err().kind(), io::ErrorKind::AlreadyExists ); assert_eq!( data_io.submit_nop(7).unwrap_err().kind(), io::ErrorKind::AlreadyExists ); assert_eq!( data_io .submit_fallocate(fd, 0, 512, 0, 7) .unwrap_err() .kind(), io::ErrorKind::AlreadyExists ); let completion = wait_for_completion(&mut data_io); assert_eq!(completion.user_data, 7); assert_eq!(completion.result, 512); } #[test] fn uring_drop_drains_in_flight_operations() { let file = TempFile::new().unwrap().into_file(); file.set_len(8192).unwrap(); let fd = file.as_raw_fd(); let mut data_io = UringDataIo::new(8).unwrap(); for user_data in 0..4 { data_io .submit_operation( fd, AsyncIoOperation::read_to_vec( 0, OwnedIoBuffer::from_vec(vec![0; 512]), user_data, ), ) .unwrap(); } drop(data_io); } #[test] fn uring_queue_full_batch_completes_each_operation() { let file = TempFile::new().unwrap().into_file(); let fd = file.as_raw_fd(); let mut data_io = UringDataIo::new(1).unwrap(); let available = { let (_, sq, _) = data_io.io_uring.split(); sq.capacity() - sq.len() }; let batch_len = available + 1; let batch: Vec<_> = (0..batch_len as u64) .map(|user_data| { AsyncIoOperation::read_to_vec(0, OwnedIoBuffer::from_vec(vec![0; 512]), user_data) }) .collect(); data_io.submit_batch(fd, batch).unwrap(); let mut completed = Vec::new(); while let Some(completion) = data_io.next_completion() { assert_eq!(completion.result, -libc::EAGAIN); assert!(completion.buffer.is_some()); completed.push(completion.user_data); } completed.sort_unstable(); assert_eq!(completed, (0..batch_len as u64).collect::>()); } } cloud-hypervisor-53.0/block/src/io/fcntl.rs000066400000000000000000000212071522476750100207360ustar00rootroot00000000000000// Copyright © 2025 Cyberus Technology GmbH // // SPDX-License-Identifier: Apache-2.0 // //! Helpers for advisory file locking. //! //! Under the hood, the implementation uses OFD locks for the entire file, //! as described in [[0]]. The advantage over `F_SETLKW` (currently used by //! Rust std: `File::try_lock()`) is that only the very last `close()` on a //! file descriptor releases the lock. This prevents mistakes and unexpected //! behavior. //! //! [0]: . use std::fmt::Debug; use std::io; use std::os::fd::{AsRawFd, RawFd}; use std::str::FromStr; use thiserror::Error; /// Errors that can happen when working with file locks. #[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. #[error("The lock state could not be checked or set")] Io(#[source] io::Error), } /// Commands for use with [`fcntl`]. #[expect(non_camel_case_types)] enum FcntlArg<'a> { /// Set an OFD lock from the given lock description. F_OFD_SETLK(&'a libc::flock), /// Get the first OFD lock for the given lock description. F_OFD_GETLK(&'a mut libc::flock), } /// 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), } } } /// Describes the type of lock you want to set. #[derive(Clone, Copy, Debug)] pub enum LockType { /// Clear a lock. Unlock, /// Set a write lock (exclusive). Write, /// Set a read lock (shared). Read, } impl LockType { pub const fn to_libc_val(self) -> libc::c_int { match self { Self::Unlock => libc::F_UNLCK as libc::c_int, Self::Write => libc::F_WRLCK as libc::c_int, Self::Read => libc::F_RDLCK as libc::c_int, } } } /// 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, } 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}"), } } } /// 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 */), } impl LockGranularity { const fn l_start(self) -> u64 { match self { LockGranularity::WholeFile => 0, LockGranularity::ByteRange(start, _) => start, } } const fn l_len(self) -> u64 { match self { LockGranularity::WholeFile => 0, /* EOF */ LockGranularity::ByteRange(_, len) => len, } } } /// User-facing choice for the lock granularity. /// /// This allows external management software to create snapshots of the disk /// image. Without a byte-range lock, some NFS implementations may treat the /// entire file as exclusively locked and prevent such operations (e.g. NetApp). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub enum LockGranularityChoice { /// Byte-range lock covering [0, size). #[default] ByteRange, /// Whole-file lock (l_start=0, l_len=0) - original OFD whole-file lock behavior. Full, } /// Error returned when parsing a [`LockGranularityChoice`] from a string. #[derive(Error, Debug)] #[error("Invalid lock granularity value: {0}, expected 'byte-range' or 'full'")] pub struct LockGranularityParseError(String); impl FromStr for LockGranularityChoice { type Err = LockGranularityParseError; fn from_str(s: &str) -> Result { match s { "byte-range" => Ok(LockGranularityChoice::ByteRange), "full" => Ok(LockGranularityChoice::Full), _ => Err(LockGranularityParseError(s.to_owned())), } } } /// Returns a [`struct@libc::flock`] structure for the whole file. const fn get_flock(lock_type: LockType, granularity: LockGranularity) -> libc::flock { libc::flock { l_type: lock_type.to_libc_val() as libc::c_short, l_whence: libc::SEEK_SET as libc::c_short, l_start: granularity.l_start() as libc::c_long, l_len: granularity.l_len() as libc::c_long, l_pid: 0, /* filled by callee */ } } /// 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`] /// - `granularity`: The [`LockGranularity`]. pub fn try_acquire_lock( file: &Fd, lock_type: LockType, granularity: LockGranularity, ) -> Result<(), LockError> { let flock = get_flock(lock_type, granularity); loop { let res = fcntl(file.as_raw_fd(), FcntlArg::F_OFD_SETLK(&flock)); match res { 0 => return Ok(()), -1 => { let io_error = io::Error::last_os_error(); let errno = io_error.raw_os_error().unwrap(); match errno { // See man page for error code: // 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}"), } } } /// Clears a lock. /// /// # Parameters /// - `file`: The file to clear all locks for [`LockType`]. /// - `granularity`: The [`LockGranularity`]. pub fn clear_lock(file: &Fd, granularity: LockGranularity) -> Result<(), LockError> { try_acquire_lock(file, LockType::Unlock, granularity) } /// Returns the current lock state using [`fcntl`] with respect to the given /// parameters. /// /// # Parameters /// - `file`: The file for which to get the lock state. /// - `granularity`: The [`LockGranularity`]. pub fn get_lock_state( file: &Fd, granularity: LockGranularity, ) -> Result { let mut flock = get_flock(LockType::Write, granularity); 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)) } val => panic!("Unexpected return value from fcntl(): {val}"), } } cloud-hypervisor-53.0/block/src/io/mod.rs000066400000000000000000000004651522476750100204120ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! Shared I/O infrastructure for all disk format backends. //! //! Contains the async I/O trait, request handling, and file locking //! helpers. pub mod async_io; pub mod fcntl; pub mod request; cloud-hypervisor-53.0/block/src/io/request.rs000066400000000000000000000664721522476750100213350ustar00rootroot00000000000000// 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 // // Copyright (c) Meta Platforms, Inc. and affiliates. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::mem; use std::os::unix::fs::FileExt; use std::sync::Arc; 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; use vm_virtio::checked_descriptor::DescriptorChainExt; use vmm_sys_util::file_traits::FileSync; use crate::async_io::{ AsyncIo, AsyncIoCompletion, AsyncIoOperation, GuestMemoryTarget, OwnedIoBuffer, }; 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 = 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 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, start: Instant, } impl Request { pub fn parse( desc_chain: &mut DescriptorChain>>, access_platform: Option<&dyn AccessPlatform>, ) -> Result { let hdr_desc = desc_chain .next_checked(access_platform) .map_err(|addr| Error::GuestMemory(GuestMemoryError::InvalidGuestAddress(addr)))? .ok_or_else(|| { error!("Missing head descriptor"); Error::DescriptorChainTooShort })?; // The head contains the request type which MUST be readable. if hdr_desc.is_write_only() { return Err(Error::UnexpectedWriteOnlyDescriptor); } let hdr_desc_addr = hdr_desc.addr(); 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, start: Instant::now(), }; let status_desc; let mut desc = desc_chain .next_checked(access_platform) .map_err(|addr| Error::GuestMemory(GuestMemoryError::InvalidGuestAddress(addr)))? .ok_or_else(|| { error!("Only head descriptor present: request = {req:?}"); Error::DescriptorChainTooShort })?; if desc.has_next() { 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(), desc.len())); desc = desc_chain .next_checked(access_platform) .map_err(|addr| { Error::GuestMemory(GuestMemoryError::InvalidGuestAddress(addr)) })? .ok_or_else(|| { error!("DescriptorChain corrupted: request = {req:?}"); Error::DescriptorChainTooShort })?; } status_desc = desc; } 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.is_empty() { return Err(Error::DescriptorLengthTooSmall); } req.status_addr = status_desc.addr(); 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)?; let mut offset = self.sector << SECTOR_SHIFT; let mut len = 0; for (data_addr, data_len) in &self.data_descriptors { match self.request_type { RequestType::In => { let mut buf = vec![0u8; *data_len as usize]; disk.read_exact_at(&mut buf, offset) .map_err(ExecuteError::ReadExact)?; mem.read_exact_volatile_from( *data_addr, &mut buf.as_slice(), *data_len as usize, ) .map_err(ExecuteError::Read)?; offset += u64::from(*data_len); len += data_len; } RequestType::Out => { let mut buf: Vec = Vec::new(); mem.write_all_volatile_to(*data_addr, &mut buf, *data_len as usize) .map_err(ExecuteError::Write)?; disk.write_all_at(&buf, offset) .map_err(ExecuteError::WriteAll)?; if !self.writeback { disk.fsync().map_err(ExecuteError::Flush)?; } offset += u64::from(*data_len); } RequestType::Flush => disk.fsync().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: Arc>, 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 ret = ExecuteAsync { async_complete: true, batch_request: None, }; // Queue operations expected to be submitted. match request_type { RequestType::In => { self.mark_read_dirty(&mem)?; let op = self.build_data_operation(mem, offset, alignment, user_data)?; if disk_image.batch_requests_enabled() { ret.batch_request = Some(op); } else { match op { AsyncIoOperation::ReadToMemory { offset, target, user_data, } => disk_image .read_to_memory(offset, target, user_data) .map_err(ExecuteError::AsyncRead)?, AsyncIoOperation::ReadToVec { offset, buffer, user_data, } => disk_image .read_to_vec(offset, buffer, user_data) .map_err(ExecuteError::AsyncRead)?, _ => unreachable!("unexpected read operation"), } } } RequestType::Out => { let op = self.build_data_operation(mem, offset, alignment, user_data)?; if disk_image.batch_requests_enabled() { ret.batch_request = Some(op); } else { match op { AsyncIoOperation::WriteFromMemory { offset, target, user_data, } => disk_image .write_from_memory(offset, target, user_data) .map_err(ExecuteError::AsyncWrite)?, AsyncIoOperation::WriteFromVec { offset, buffer, user_data, } => disk_image .write_from_vec(offset, buffer, user_data) .map_err(ExecuteError::AsyncWrite)?, _ => unreachable!("unexpected write operation"), } } } RequestType::Flush => { 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 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_offset = wz_sector * SECTOR_SIZE; if wz_offset == 0 && disable_sector0_writes { return Err(ExecuteError::BadRequest(Error::InvalidOffset)); } let wz_length = (wz_num_sectors as u64) * SECTOR_SIZE; if wz_flags & VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP != 0 { 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) } // Builds a read or write operation for IO to or from `mem`. fn build_data_operation( &self, mem: Arc>, offset: libc::off_t, alignment: u64, user_data: u64, ) -> Result { if self.guest_memory_is_aligned(&mem, alignment)? { let target = GuestMemoryTarget::new(mem, &self.data_descriptors) .map_err(ExecuteError::GetHostAddress)?; return Ok(match self.request_type { RequestType::In => AsyncIoOperation::read_to_memory(offset, target, user_data), RequestType::Out => AsyncIoOperation::write_from_memory(offset, target, user_data), _ => unreachable!("unexpected data operation type"), }); } // The guest-memory buffers are unaligned, so use an aligned bounce buffer. let mut buffer = OwnedIoBuffer::new(self.data_len(), alignment as usize) .map_err(ExecuteError::TemporaryBufferAllocation)?; if self.request_type == RequestType::Out { self.copy_guest_to_buffer(&mem, buffer.as_mut_slice())?; } Ok(match self.request_type { RequestType::In => AsyncIoOperation::read_to_vec(offset, buffer, user_data), RequestType::Out => AsyncIoOperation::write_from_vec(offset, buffer, user_data), _ => unreachable!("unexpected data operation type"), }) } // Checks whether `self.data_descriptors` are aligned to `alignment`. fn guest_memory_is_aligned( &self, mem: &vm_memory::GuestMemoryMmap, alignment: u64, ) -> Result { if alignment <= 1 { return Ok(true); } for &(data_addr, data_len) in &self.data_descriptors { let _: u32 = data_len; const _: () = assert!( size_of::() <= 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)?; let origin_ptr = origin_ptr.ptr_guard_mut(); if !(origin_ptr.as_ptr() as u64).is_multiple_of(alignment) || !(origin_ptr.len() as u64).is_multiple_of(alignment) { return Ok(false); } } Ok(true) } // Returns the sum of the lengths of `self.data_descriptors`. fn data_len(&self) -> usize { self.data_descriptors .iter() .map(|(_, len)| *len as usize) .sum() } // Marks guest-memory read destinations dirty before submitting async IO. fn mark_read_dirty( &self, mem: &vm_memory::GuestMemoryMmap, ) -> Result<(), ExecuteError> { for (data_addr, data_len) in &self.data_descriptors { mem.get_slice(*data_addr, *data_len as usize) .map_err(ExecuteError::GetHostAddress)? .bitmap() .mark_dirty(0, *data_len as usize); } Ok(()) } // Copies guest descriptor contents into a contiguous host buffer. fn copy_guest_to_buffer( &self, mem: &vm_memory::GuestMemoryMmap, buffer: &mut [u8], ) -> Result<(), ExecuteError> { let mut offset = 0usize; for (data_addr, data_len) in &self.data_descriptors { let data_len = *data_len as usize; mem.read_slice(&mut buffer[offset..offset + data_len], *data_addr) .map_err(ExecuteError::Read)?; offset += data_len; } Ok(()) } // Copies a host completion buffer back into guest descriptors. fn copy_buffer_to_guest( &self, mem: &vm_memory::GuestMemoryMmap, buffer: &[u8], ) -> Result<(), Error> { let mut buffer_offset = 0usize; for (data_addr, data_len) in &self.data_descriptors { if buffer_offset >= buffer.len() { break; } let data_len = (*data_len as usize).min(buffer.len() - buffer_offset); mem.write_slice(&buffer[buffer_offset..buffer_offset + data_len], *data_addr) .map_err(Error::GuestMemory)?; buffer_offset += data_len; } Ok(()) } pub fn complete_async( &mut self, mem: &vm_memory::GuestMemoryMmap, completion: &mut AsyncIoCompletion, ) -> Result<(), Error> { if self.request_type == RequestType::In && completion.result > 0 && let Some(buffer) = completion.buffer.take() { let len = (completion.result as usize).min(buffer.as_slice().len()); self.copy_buffer_to_guest(mem, &buffer.as_slice()[..len])?; } Ok(()) } #[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(()); } if !total_bytes.is_multiple_of(SECTOR_SIZE) { return Err(ExecuteError::BadRequest(Error::InvalidDataLength)); } let total_sectors = total_bytes / SECTOR_SIZE; let end_sector = self .sector .checked_add(total_sectors) .ok_or(ExecuteError::BadRequest(Error::InvalidOffset))?; if end_sector > disk_nsectors { return Err(ExecuteError::BadRequest(Error::InvalidOffset)); } Ok(()) } } #[cfg(test)] mod unit_tests { use std::sync::Arc; use vm_memory::GuestMemoryMmap; use vmm_sys_util::eventfd::EventFd; use super::*; use crate::async_io::{AsyncIo, AsyncIoCompletion, AsyncIoOperation, AsyncIoResult}; struct PanicAsyncIo(EventFd); impl AsyncIo for PanicAsyncIo { fn notifier(&self) -> &EventFd { &self.0 } fn submit_data_operation(&mut self, _: AsyncIoOperation) -> AsyncIoResult<()> { unreachable!() } fn fsync(&mut self, _: Option) -> AsyncIoResult<()> { unreachable!() } fn punch_hole(&mut self, _: u64, _: u64, _: u64) -> AsyncIoResult<()> { unreachable!() } fn write_zeroes(&mut self, _: u64, _: u64, _: u64) -> AsyncIoResult<()> { unreachable!() } fn next_completed_request(&mut self) -> Option { None } } #[test] fn write_zeroes_rejects_sector_arithmetic_overflow() { let mem = Arc::new(GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 4096)]).unwrap()); mem.write_slice(&(u64::MAX - 100).to_le_bytes(), GuestAddress(0)) .unwrap(); mem.write_slice(&1000u32.to_le_bytes(), GuestAddress(8)) .unwrap(); let mut request = Request { request_type: RequestType::WriteZeroes, sector: 0, data_descriptors: SmallVec::from_slice(&[(GuestAddress(0), DISCARD_WZ_SEG_SIZE)]), status_addr: GuestAddress(0), writeback: true, start: Instant::now(), }; let mut disk = PanicAsyncIo(EventFd::new(0).unwrap()); let Err(ExecuteError::BadRequest(Error::InvalidOffset)) = request.execute_async(mem, 1024, &mut disk, &[], false, 0) else { panic!("expected BadRequest(InvalidOffset)"); }; } } cloud-hypervisor-53.0/block/src/lib.rs000066400000000000000000000760401522476750100177740ustar00rootroot00000000000000// 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 pub mod disk_file; pub mod error; pub mod factory; #[path = "io/mod.rs"] mod io_impl; pub use io_impl::{async_io, fcntl, request}; pub(crate) mod aligned_buffer; pub mod aligned_file; pub mod formats; mod sparse; use std::fmt::{self, Debug}; use std::fs::{File, OpenOptions}; use std::os::linux::fs::MetadataExt; use std::os::unix::fs::{FileExt, FileTypeExt}; use std::os::unix::io::{AsRawFd, RawFd}; use std::path::Path; use std::str::FromStr; use std::{cmp, io, mem, result}; pub use aligned_file::AlignedFile; use formats::qcow; #[cfg(feature = "io_uring")] use io_uring::{IoUring, Probe, opcode}; use libc::{ FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE, S_IFBLK, S_IFMT, ioctl, }; use log::{debug, info, warn}; pub use request::{ExecuteAsync, MAX_DISCARD_WRITE_ZEROES_SEG, Request, RequestType}; use serde::{Deserialize, Serialize}; pub use sparse::{BLKDISCARD, BLKZEROOUT}; use thiserror::Error; use virtio_bindings::virtio_blk::*; use vm_memory::bitmap::Bitmap; use vm_memory::{ByteValued, Bytes, GuestAddress, GuestMemory, GuestMemoryError}; use vmm_sys_util::{aio, ioctl_io_nr, ioctl_ior_nr}; use crate::async_io::AsyncIoError; use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; use crate::formats::vhdx::VhdxError; use crate::request::SECTOR_SIZE; #[derive(Error, Debug)] pub enum Error { #[error("Guest gave us bad memory addresses")] GuestMemory(#[source] GuestMemoryError), #[error("Guest address {0:?} with sector offset {1} would overflow a usize")] CheckedOffset(GuestAddress, usize /* sector offset */), #[error("Guest gave us a write only descriptor that protocol says to read from")] UnexpectedWriteOnlyDescriptor, #[error("Guest gave us a read only descriptor that protocol says to write to")] UnexpectedReadOnlyDescriptor, #[error("Guest gave us too few descriptors in a descriptor chain")] DescriptorChainTooShort, #[error("Guest gave us a descriptor that was too short to use")] DescriptorLengthTooSmall, #[error("Failed to detect image type")] DetectImageType(#[source] io::Error), #[error("Failure in fixed vhd")] FixedVhdError(#[source] io::Error), #[error("Getting a block's metadata failed")] GetFileMetadata(#[source] io::Error), #[error("The requested operation would cause a seek beyond disk end")] InvalidOffset, #[error("Request data length is not a multiple of the 512-byte sector size")] InvalidDataLength, #[error("Failure in qcow")] QcowError(#[source] qcow::Error), #[error("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(e) => return Err(Error::GetFileMetadata(e)), Ok(m) => m, }; // This is how kvmtool does it. let device_id = format!( "{}{}{}", blk_metadata.st_dev(), blk_metadata.st_rdev(), blk_metadata.st_ino() ); Ok(device_id) } pub fn build_serial(disk_path: &Path) -> Vec { let mut default_serial = vec![0; VIRTIO_BLK_ID_BYTES as usize]; match build_device_id(disk_path) { Err(_) => { warn!("Could not generate device id. We'll use a default."); } Ok(m) => { // The kernel only knows to read a maximum of VIRTIO_BLK_ID_BYTES. // 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 } #[derive(Error, Debug)] pub enum ExecuteError { #[error("Bad request")] BadRequest(#[source] Error), #[error("Failed to flush")] Flush(#[source] io::Error), #[error("Failed to read")] 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 write")] Write(#[source] GuestMemoryError), #[error("Failed to write_all")] 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")] GetHostAddress(#[source] GuestMemoryError), #[error("Failed to async read")] AsyncRead(#[source] AsyncIoError), #[error("Failed to async write")] 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), } impl ExecuteError { pub fn status(&self) -> u8 { let status = match *self { ExecuteError::BadRequest(_) => VIRTIO_BLK_S_IOERR, 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::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 } } pub fn request_type( mem: &vm_memory::GuestMemoryMmap, desc_addr: GuestAddress, ) -> result::Result { let type_ = mem.read_obj(desc_addr).map_err(Error::GuestMemory)?; match type_ { VIRTIO_BLK_T_IN => Ok(RequestType::In), 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)), } } fn sector( mem: &vm_memory::GuestMemoryMmap, desc_addr: GuestAddress, ) -> result::Result { const SECTOR_OFFSET: usize = 8; let addr = match mem.checked_offset(desc_addr, SECTOR_OFFSET) { Some(v) => v, None => return Err(Error::CheckedOffset(desc_addr, SECTOR_OFFSET)), }; mem.read_obj(addr).map_err(Error::GuestMemory) } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] #[repr(C, packed)] pub struct VirtioBlockConfig { pub capacity: u64, pub size_max: u32, pub seg_max: u32, pub geometry: VirtioBlockGeometry, pub blk_size: u32, pub physical_block_exp: u8, pub alignment_offset: u8, pub min_io_size: u16, pub opt_io_size: u32, pub writeback: u8, pub unused: u8, pub num_queues: u16, pub max_discard_sectors: u32, pub max_discard_seg: u32, pub discard_sector_alignment: u32, pub max_write_zeroes_sectors: u32, pub max_write_zeroes_seg: u32, pub write_zeroes_may_unmap: u8, pub unused1: [u8; 3], } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] #[repr(C, packed)] pub struct VirtioBlockGeometry { pub cylinders: u16, pub heads: u8, pub sectors: u8, } // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for VirtioBlockConfig {} // SAFETY: data structure only contain a series of integers unsafe impl ByteValued for VirtioBlockGeometry {} /// Check if aio can be used on the current system. pub fn block_aio_is_supported() -> bool { aio::IoContext::new(1).is_ok() } /// Check if io_uring for block device can be used on the current system, as /// it correctly supports the expected io_uring features. pub fn block_io_uring_is_supported() -> bool { #[cfg(not(feature = "io_uring"))] { info!("io_uring is disabled by crate features"); false } #[cfg(feature = "io_uring")] { let error_msg = "io_uring not supported:"; // Check we can create an io_uring instance, which effectively verifies // that io_uring_setup() syscall is supported. let io_uring = match IoUring::new(1) { Ok(io_uring) => io_uring, Err(e) => { info!("{error_msg} failed to create io_uring instance: {e}"); return false; } }; let submitter = io_uring.submitter(); let mut probe = Probe::new(); // Check we can register a probe to validate supported operations. match submitter.register_probe(&mut probe) { Ok(_) => {} Err(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!("{error_msg} IORING_OP_FSYNC operation not supported"); return false; } // Check IORING_OP_READV is supported if !probe.is_supported(opcode::Readv::CODE) { 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!("{error_msg} IORING_OP_WRITEV operation not supported"); return false; } true } } /// Returns `true` iff `fd` refers to a block device. /// /// Returns `false` if the `fstat()` probe itself fails. Callers that need to /// distinguish "not a block device" from "couldn't tell" should fall back to /// regular-file behaviour, which is what every current caller already does. pub(crate) fn is_block_device(fd: RawFd) -> bool { // SAFETY: `libc::stat` is POD; zero-initialization is a valid bit pattern // and `fstat` overwrites every field it cares about on success. let mut stat: libc::stat = unsafe { mem::zeroed() }; // SAFETY: FFI call with a valid fd and a valid out-pointer. let ret = unsafe { libc::fstat(fd, &mut stat) }; ret == 0 && stat.st_mode & S_IFMT == S_IFBLK } /// Returns the kernel reported direct I/O alignment for `fd`, or `None` /// when `fd` was not opened with O_DIRECT. /// /// When O_DIRECT is set, uses `statx(STATX_DIOALIGN)` (Linux >= 6.1) to obtain /// the exact memory and offset alignment the kernel requires for direct I/O on /// this specific fd. Unlike `fstatvfs().f_bsize`, which only returns the /// filesystem's preferred I/O block size, `STATX_DIOALIGN` reports the true per /// fd direct I/O constraint accounting for the filesystem, underlying block /// device, and any stacking such as loop or device mapper. Falls back to /// [`SECTOR_SIZE`] when the kernel does not report a value. pub(crate) fn probe_direct_alignment(fd: RawFd) -> Option { // SAFETY: fcntl(F_GETFL) is always safe on a valid fd. let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; if flags < 0 || (flags & libc::O_DIRECT) == 0 { return None; } // The libc crate does not expose statx / STATX_DIOALIGN on all targets, // for example musl, so define the constant and a minimal repr(C) struct // locally and invoke the syscall directly. const STATX_DIOALIGN: u32 = 0x2000; // Minimal statx layout, only the needed fields, everything else is // padding. #[repr(C)] struct Statx { stx_mask: u32, _pad: [u8; 148], stx_dio_mem_align: u32, stx_dio_offset_align: u32, _pad2: [u8; 96], } let mut stx = mem::MaybeUninit::::zeroed(); // SAFETY: FFI syscall with valid fd and correctly sized buffer. let ret = unsafe { libc::syscall( libc::SYS_statx, fd, c"".as_ptr(), libc::AT_EMPTY_PATH, STATX_DIOALIGN, stx.as_mut_ptr(), ) }; if ret == 0 { // SAFETY: statx succeeded, the struct is fully initialized. let stx = unsafe { stx.assume_init() }; if stx.stx_mask & STATX_DIOALIGN != 0 && stx.stx_dio_mem_align > 0 { return Some(cmp::max(stx.stx_dio_mem_align, stx.stx_dio_offset_align) as u64); } } debug!("O_DIRECT alignment query failed, falling back to default {SECTOR_SIZE}"); Some(SECTOR_SIZE) } /// Probe whether the file/device supports punch hole and zero range pub fn probe_sparse_support(file: &File) -> bool { let fd = file.as_raw_fd(); if is_block_device(fd) { 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() ); } } #[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; const VHDX_SIGN: u64 = 0x656C_6966_7864_6876; /// 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) -> BlockResult { let aligned = AlignedFile::new(f.try_clone()?, true); let mut block = vec![0u8; aligned.alignment()]; aligned .read_exact_at(&mut block, 0) .map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?; // Check 4 first bytes to get the header value and determine the image type let image_type = if u32::from_be_bytes(block[0..4].try_into().unwrap()) == QCOW_MAGIC { ImageType::Qcow2 } else if formats::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 } else { ImageType::Raw }; Ok(image_type) } #[derive(Debug)] pub struct DiskTopology { pub logical_block_size: u64, pub physical_block_size: u64, pub minimum_io_size: u64, pub optimal_io_size: u64, } impl Default for DiskTopology { fn default() -> Self { Self { logical_block_size: 512, physical_block_size: 512, minimum_io_size: 512, optimal_io_size: 0, } } } 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, MinimumIo, OptimalIo, } impl DiskTopology { // libc::ioctl() takes different types on different architectures fn query_block_size(f: &File, block_size_type: BlockSize) -> io::Result { let mut block_size = 0; // SAFETY: FFI call with correct arguments let ret = unsafe { ioctl( f.as_raw_fd(), match block_size_type { BlockSize::LogicalBlock => BLKSSZGET(), BlockSize::PhysicalBlock => BLKPBSZGET(), BlockSize::MinimumIo => BLKIOMIN(), BlockSize::OptimalIo => BLKIOOPT(), } as _, &mut block_size, ) }; if ret != 0 { return Err(io::Error::last_os_error()); } Ok(block_size) } pub fn probe(f: &File) -> io::Result { if !is_block_device(f.as_raw_fd()) { // For regular files opened with O_DIRECT, the logical block size // must reflect the filesystem DIO alignment so the guest issues // correctly sized I/O. if let Some(alignment) = probe_direct_alignment(f.as_raw_fd()) { return Ok(DiskTopology { logical_block_size: alignment, physical_block_size: alignment, minimum_io_size: alignment, optimal_io_size: 0, }); } return Ok(DiskTopology::default()); } Ok(DiskTopology { logical_block_size: Self::query_block_size(f, BlockSize::LogicalBlock)?, physical_block_size: Self::query_block_size(f, BlockSize::PhysicalBlock)?, minimum_io_size: Self::query_block_size(f, BlockSize::MinimumIo)?, optimal_io_size: Self::query_block_size(f, BlockSize::OptimalIo)?, }) } } #[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::{mem, 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); } // A mode-0 fallocate() is not eagerly accounted in st_blocks on every // filesystem: zfs reserves the range but accounts blocks lazily at // transaction-group commit, and FUSE-based filesystems such as virtiofs // report preallocated files as sparse. Identify those by filesystem type // so a skipped physical-size check always names a proven platform // limitation instead of being inferred from the value under test. fn fs_defers_fallocate_block_accounting(f: &File) -> bool { // SAFETY: a zeroed statfs is a valid output buffer for fstatfs and it // is only read after the call succeeds. let mut sfs: libc::statfs = unsafe { mem::zeroed() }; // SAFETY: the fd is valid and sfs outlives the call. let ret = unsafe { libc::fstatfs(f.as_raw_fd(), &mut sfs) }; assert_eq!(ret, 0, "fstatfs failed: {}", io::Error::last_os_error()); // ZFS_SUPER_MAGIC and FUSE_SUPER_MAGIC (statfs(2)), as untyped // literals because the width and signedness of f_type differ // between libc targets. matches!(sfs.f_type, 0x2fc1_2fc1 | 0x6573_5546) } #[test] fn test_query_device_size_sparse_file_punch_hole() { let temp_file = TempFile::new().unwrap(); 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, ) }; if ret != 0 { let err = io::Error::last_os_error(); if err.raw_os_error() == Some(libc::EOPNOTSUPP) { eprintln!("Skipping test: fallocate() is not supported: {err}"); return; } panic!("fallocate failed: {err}"); } f.sync_all().unwrap(); let (log_before, phys_before) = query_device_size(f).unwrap(); assert_eq!(log_before, size as u64); if fs_defers_fallocate_block_accounting(f) { eprintln!( "Skipping physical size checks: the filesystem defers \ fallocate() block accounting" ); return; } assert_eq!(phys_before, size as u64); // Punch a hole in the middle 512 KiB // 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, ) }; if ret != 0 { let err = io::Error::last_os_error(); if err.raw_os_error() == Some(libc::EOPNOTSUPP) { eprintln!( "Skipping punch-hole checks: FALLOC_FL_PUNCH_HOLE is not supported: {err}" ); return; } panic!("punch hole failed: {err}"); } f.sync_all().unwrap(); let (logical, physical) = query_device_size(f).unwrap(); 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 = File::open("/dev/zero").unwrap(); let err = query_device_size(&f).unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } } cloud-hypervisor-53.0/block/src/sparse.rs000066400000000000000000000075441522476750100205260ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause // Helpers for issuing `BLKDISCARD` / `BLKZEROOUT` ioctls on block devices, // and the `punch_hole` / `write_zeroes` dispatchers used by the raw I/O // backends. // // The kernel ioctl numbers and argument layout are stable userspace ABI // (see `include/uapi/linux/fs.h`): // // ```c // #define BLKDISCARD _IO(0x12, 119) /* arg: const __u64 range[2] = { start, len } */ // #define BLKZEROOUT _IO(0x12, 127) /* arg: const __u64 range[2] = { start, len } */ // ``` // // The kernel does `copy_from_user(range, arg, sizeof(range))`, i.e. it reads // 16 bytes through the single pointer it is given, so we must pass a single // `__u64[2]` array rather than two separate `*const u64` pointers. use std::io; use std::os::unix::io::RawFd; use libc::{FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE}; // `_IO(0x12, 119)` — issue a discard request to a block device. pub const BLKDISCARD: libc::c_ulong = 0x1277; // `_IO(0x12, 127)` — write zeroes to a range of a block device, with a // kernel-side fallback to writing zero pages when the hardware has no native // `WRITE_ZEROES`. pub const BLKZEROOUT: libc::c_ulong = 0x127f; // Issue a `BLK*` range ioctl with proper `[start, len]` argument. fn blk_range_ioctl(fd: RawFd, request: libc::c_ulong, offset: u64, length: u64) -> io::Result<()> { let range: [u64; 2] = [offset, length]; // SAFETY: `fd` is a valid block-device fd owned by the caller; `&range` // is a 16-byte array matching the kernel's expected `__u64[2]` layout // and lives for the duration of the call. let ret = unsafe { libc::ioctl(fd, request as _, &range) }; if ret == 0 { Ok(()) } else { Err(io::Error::last_os_error()) } } // Discard (TRIM/UNMAP) the byte range `[offset, offset + length)` on the // block device referenced by `fd`. pub(crate) fn blkdiscard(fd: RawFd, offset: u64, length: u64) -> io::Result<()> { blk_range_ioctl(fd, BLKDISCARD, offset, length) } // Zero the byte range `[offset, offset + length)` on the block device // referenced by `fd`. The kernel falls back to writing explicit zero pages // when the device has no hardware `WRITE_ZEROES`. pub(crate) fn blkzeroout(fd: RawFd, offset: u64, length: u64) -> io::Result<()> { blk_range_ioctl(fd, BLKZEROOUT, offset, length) } // Punch a hole in `fd` over the byte range `[offset, offset + length)`. // // On block devices the kernel rejects `fallocate(PUNCH_HOLE)` (notably ZFS // zvols), so route through `BLKDISCARD` instead. On regular files use // `fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)`. pub(crate) fn punch_hole(fd: RawFd, is_blkdev: bool, offset: u64, length: u64) -> io::Result<()> { if is_blkdev { blkdiscard(fd, offset, length) } else { fallocate( fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset, length, ) } } // Zero the byte range `[offset, offset + length)` in `fd`. // // Uses `BLKZEROOUT` on block devices (see [`punch_hole`] for the rationale) // and `fallocate(FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE)` on regular // files. pub(crate) fn write_zeroes(fd: RawFd, is_blkdev: bool, offset: u64, length: u64) -> io::Result<()> { if is_blkdev { blkzeroout(fd, offset, length) } else { fallocate( fd, FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE, offset, length, ) } } fn fallocate(fd: RawFd, mode: libc::c_int, offset: u64, length: u64) -> io::Result<()> { // SAFETY: FFI call with a valid fd; fallocate touches no userspace memory. let ret = unsafe { libc::fallocate(fd, mode, offset as libc::off_t, length as libc::off_t) }; if ret == 0 { Ok(()) } else { Err(io::Error::last_os_error()) } } cloud-hypervisor-53.0/clippy.toml000066400000000000000000000000511522476750100171610ustar00rootroot00000000000000absolute-paths-allowed-crates = ["arch"] cloud-hypervisor-53.0/cloud-hypervisor/000077500000000000000000000000001522476750100203065ustar00rootroot00000000000000cloud-hypervisor-53.0/cloud-hypervisor/Cargo.toml000066400000000000000000000036531522476750100222450ustar00rootroot00000000000000[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 = "53.0.0" [dependencies] api_client = { path = "../api_client" } clap = { workspace = true, features = ["string"] } dhat = { workspace = true, optional = true } env_logger = { 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 } signal-hook-registry = { workspace = true } thiserror = { workspace = true } tracer = { path = "../tracer" } 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 = ["vmm/igvm"] io_uring = ["vmm/io_uring"] ivshmem = ["vmm/ivshmem"] kvm = ["vmm/kvm"] mshv = ["vmm/mshv"] pvmemcontrol = ["vmm/pvmemcontrol"] sev_snp = ["igvm", "vmm/sev_snp"] tdx = ["vmm/tdx"] tracing = ["tracer/tracing", "vmm/tracing"] [lints] workspace = true cloud-hypervisor-53.0/cloud-hypervisor/build.rs000066400000000000000000000020211522476750100217460ustar00rootroot00000000000000// Copyright © 2020 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::env; 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() && 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. if let Ok(extra_version) = env::var("CH_EXTRA_VERSION") { println!("cargo:rerun-if-env-changed=CH_EXTRA_VERSION"); version.push_str(&format!("-{extra_version}")); } // This println!() has a special behavior, as it will set the environment // variable BUILD_VERSION, so that it can be reused from the binary. // Particularly, this is used from src/main.rs to display the exact // version. println!("cargo:rustc-env=BUILD_VERSION={version}"); } cloud-hypervisor-53.0/cloud-hypervisor/src/000077500000000000000000000000001522476750100210755ustar00rootroot00000000000000cloud-hypervisor-53.0/cloud-hypervisor/src/bin/000077500000000000000000000000001522476750100216455ustar00rootroot00000000000000cloud-hypervisor-53.0/cloud-hypervisor/src/bin/ch-remote.rs000066400000000000000000001367721522476750100241160ustar00rootroot00000000000000// Copyright © 2020 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // #[cfg(test)] #[path = "../test_util.rs"] mod test_util; use std::io::{self, Read}; use std::marker::PhantomData; use std::os::unix::net::UnixStream; use std::{error, fs, iter, num, process}; use api_client::{ Error as ApiClientError, simple_api_command, simple_api_command_with_fds, simple_api_full_command, }; #[cfg(feature = "dbus_api")] use clap::ArgAction; use clap::{Arg, ArgMatches, Command}; use log::error; use option_parser::{ByteSized, ByteSizedParseError}; use thiserror::Error; use vmm::api; use vmm::config::{self, RestoreConfig}; use vmm::vm_config::{ DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig, VsockConfig, }; #[cfg(feature = "dbus_api")] use zbus::{blocking::Connection, proxy, zvariant::Optional}; type ApiResult = Result<(), Error>; #[derive(Error, Debug)] enum Error { #[error("http client error")] HttpApiClient(#[source] ApiClientError), #[cfg(feature = "dbus_api")] #[error("dbus api client error")] DBusApiClient(#[source] zbus::Error), #[error("Error parsing CPU count")] InvalidCpuCount(#[source] num::ParseIntError), #[error("Error parsing memory size")] InvalidMemorySize(#[source] ByteSizedParseError), #[error("Error parsing balloon size")] InvalidBalloonSize(#[source] ByteSizedParseError), #[error("Error parsing device syntax")] AddDeviceConfig(#[source] config::Error), #[error("Error parsing disk syntax")] AddDiskConfig(#[source] config::Error), #[error("Error parsing filesystem syntax")] AddFsConfig(#[source] config::Error), #[error("Error parsing generic vhost-user syntax")] AddGenericVhostUserConfig(#[source] config::Error), #[error("Error parsing persistent memory syntax")] AddPmemConfig(#[source] config::Error), #[error("Error parsing network syntax")] AddNetConfig(#[source] config::Error), #[error("Error parsing user device syntax")] AddUserDeviceConfig(#[source] config::Error), #[error("Error parsing vDPA device syntax")] AddVdpaConfig(#[source] config::Error), #[error("Error parsing vsock syntax")] AddVsockConfig(#[source] config::Error), #[error("Error parsing restore syntax")] Restore(#[source] config::Error), #[error("Error reading from stdin")] ReadingStdin(#[source] io::Error), #[error("Error reading from file")] ReadingFile(#[source] io::Error), #[error("Invalid disk size")] InvalidDiskSize(#[source] ByteSizedParseError), #[error("Error parsing receive migration configuration")] ReceiveMigrationConfig(#[from] api::VmReceiveMigrationConfigError), #[error("Error parsing send migration configuration")] SendMigrationConfig(#[from] api::VmSendMigrationConfigError), } enum TargetApi<'a> { HttpApi(UnixStream, PhantomData<&'a ()>), #[cfg(feature = "dbus_api")] DBusApi(DBusApi1ProxyBlocking<'a>), } #[cfg(feature = "dbus_api")] #[proxy(name = "org.cloudhypervisor.DBusApi1", assume_defaults = false)] trait DBusApi1 { fn vmm_ping(&self) -> zbus::Result; fn vmm_shutdown(&self) -> zbus::Result<()>; 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>; fn vm_add_vdpa(&self, vdpa_config: &str) -> zbus::Result>; fn vm_add_vsock(&self, vsock_config: &str) -> zbus::Result>; fn vm_boot(&self) -> zbus::Result<()>; fn vm_coredump(&self, vm_coredump_data: &str) -> zbus::Result<()>; fn vm_counters(&self) -> zbus::Result>; fn vm_create(&self, vm_config: &str) -> zbus::Result<()>; fn vm_delete(&self) -> zbus::Result<()>; fn vm_info(&self) -> zbus::Result; fn vm_pause(&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<()>; fn vm_resize(&self, vm_resize: &str) -> zbus::Result<()>; fn vm_resize_zone(&self, vm_resize_zone: &str) -> zbus::Result<()>; fn vm_restore(&self, restore_config: &str) -> zbus::Result<()>; fn vm_receive_migration(&self, receive_migration_data: &str) -> zbus::Result<()>; fn vm_send_migration(&self, receive_migration_data: &str) -> zbus::Result<()>; fn vm_resume(&self) -> zbus::Result<()>; fn vm_shutdown(&self) -> zbus::Result<()>; fn vm_snapshot(&self, vm_snapshot_config: &str) -> zbus::Result<()>; } #[cfg(feature = "dbus_api")] impl<'a> DBusApi1ProxyBlocking<'a> { fn new_connection(name: &'a str, path: &'a str, system_bus: bool) -> Result { let connection = if system_bus { Connection::system()? } else { Connection::session()? }; Self::builder(&connection) .destination(name)? .path(path)? .build() } fn print_response(&self, result: zbus::Result>) -> ApiResult { result .map(|ret| { if let Some(ref output) = *ret { println!("{output}"); } }) .map_err(Error::DBusApiClient) } fn api_vmm_ping(&self) -> ApiResult { self.vmm_ping() .map(|ping| println!("{ping}")) .map_err(Error::DBusApiClient) } fn api_vmm_shutdown(&self) -> ApiResult { self.vmm_shutdown().map_err(Error::DBusApiClient) } fn api_vm_add_device(&self, device_config: &str) -> ApiResult { self.print_response(self.vm_add_device(device_config)) } fn api_vm_add_disk(&self, disk_config: &str) -> ApiResult { self.print_response(self.vm_add_disk(disk_config)) } fn api_vm_add_fs(&self, fs_config: &str) -> ApiResult { 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)) } fn api_vm_add_pmem(&self, pmem_config: &str) -> ApiResult { self.print_response(self.vm_add_pmem(pmem_config)) } fn api_vm_add_user_device(&self, vm_add_user_device: &str) -> ApiResult { self.print_response(self.vm_add_user_device(vm_add_user_device)) } fn api_vm_add_vdpa(&self, vdpa_config: &str) -> ApiResult { self.print_response(self.vm_add_vdpa(vdpa_config)) } fn api_vm_add_vsock(&self, vsock_config: &str) -> ApiResult { self.print_response(self.vm_add_vsock(vsock_config)) } fn api_vm_boot(&self) -> ApiResult { self.vm_boot().map_err(Error::DBusApiClient) } fn api_vm_coredump(&self, vm_coredump_data: &str) -> ApiResult { self.vm_coredump(vm_coredump_data) .map_err(Error::DBusApiClient) } fn api_vm_counters(&self) -> ApiResult { self.print_response(self.vm_counters()) } fn api_vm_create(&self, vm_config: &str) -> ApiResult { self.vm_create(vm_config).map_err(Error::DBusApiClient) } fn api_vm_delete(&self) -> ApiResult { self.vm_delete().map_err(Error::DBusApiClient) } fn api_vm_info(&self) -> ApiResult { self.vm_info() .map(|info| println!("{info}")) .map_err(Error::DBusApiClient) } fn api_vm_pause(&self) -> ApiResult { self.vm_pause().map_err(Error::DBusApiClient) } fn api_vm_power_button(&self) -> ApiResult { self.vm_power_button().map_err(Error::DBusApiClient) } fn api_vm_reboot(&self) -> ApiResult { self.vm_reboot().map_err(Error::DBusApiClient) } fn api_vm_remove_device(&self, vm_remove_device: &str) -> ApiResult { self.vm_remove_device(vm_remove_device) .map_err(Error::DBusApiClient) } fn api_vm_resize(&self, vm_resize: &str) -> ApiResult { self.vm_resize(vm_resize).map_err(Error::DBusApiClient) } fn api_vm_resize_zone(&self, vm_resize_zone: &str) -> ApiResult { self.vm_resize_zone(vm_resize_zone) .map_err(Error::DBusApiClient) } fn api_vm_restore(&self, restore_config: &str) -> ApiResult { self.vm_restore(restore_config) .map_err(Error::DBusApiClient) } fn api_vm_receive_migration(&self, receive_migration_data: &str) -> ApiResult { self.vm_receive_migration(receive_migration_data) .map_err(Error::DBusApiClient) } fn api_vm_send_migration(&self, send_migration_data: &str) -> ApiResult { self.vm_send_migration(send_migration_data) .map_err(Error::DBusApiClient) } fn api_vm_resume(&self) -> ApiResult { self.vm_resume().map_err(Error::DBusApiClient) } fn api_vm_shutdown(&self) -> ApiResult { self.vm_shutdown().map_err(Error::DBusApiClient) } fn api_vm_snapshot(&self, vm_snapshot_config: &str) -> ApiResult { self.vm_snapshot(vm_snapshot_config) .map_err(Error::DBusApiClient) } } impl TargetApi<'_> { fn do_command(&mut self, matches: &ArgMatches) -> ApiResult { match self { Self::HttpApi(api_socket, _) => rest_api_do_command(matches, api_socket), #[cfg(feature = "dbus_api")] Self::DBusApi(proxy) => dbus_api_do_command(matches, proxy), } } } fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResult { match matches.subcommand_name() { Some("boot") => { simple_api_command(socket, "PUT", "boot", None).map_err(Error::HttpApiClient) } Some("delete") => { simple_api_command(socket, "PUT", "delete", None).map_err(Error::HttpApiClient) } Some("shutdown-vmm") => simple_api_full_command(socket, "PUT", "vmm.shutdown", None) .map_err(Error::HttpApiClient), Some("resume") => { simple_api_command(socket, "PUT", "resume", None).map_err(Error::HttpApiClient) } Some("power-button") => { simple_api_command(socket, "PUT", "power-button", None).map_err(Error::HttpApiClient) } Some("reboot") => { simple_api_command(socket, "PUT", "reboot", None).map_err(Error::HttpApiClient) } Some("pause") => { simple_api_command(socket, "PUT", "pause", None).map_err(Error::HttpApiClient) } Some("info") => { simple_api_command(socket, "GET", "info", None).map_err(Error::HttpApiClient) } Some("counters") => { simple_api_command(socket, "GET", "counters", None).map_err(Error::HttpApiClient) } Some("ping") => { simple_api_full_command(socket, "GET", "vmm.ping", None).map_err(Error::HttpApiClient) } Some("shutdown") => { simple_api_command(socket, "PUT", "shutdown", None).map_err(Error::HttpApiClient) } Some("nmi") => simple_api_command(socket, "PUT", "nmi", None).map_err(Error::HttpApiClient), Some("resize") => { let resize = resize_config( matches .subcommand_matches("resize") .unwrap() .get_one::("cpus") .map(|x| x as &str), matches .subcommand_matches("resize") .unwrap() .get_one::("memory") .map(|x| x as &str), matches .subcommand_matches("resize") .unwrap() .get_one::("balloon") .map(|x| x as &str), )?; 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 .subcommand_matches("resize-zone") .unwrap() .get_one::("id") .unwrap(), matches .subcommand_matches("resize-zone") .unwrap() .get_one::("size") .unwrap(), )?; simple_api_command(socket, "PUT", "resize-zone", Some(&resize_zone)) .map_err(Error::HttpApiClient) } Some("add-device") => { let (device_config, fds) = add_device_config( matches .subcommand_matches("add-device") .unwrap() .get_one::("device_config") .unwrap(), )?; simple_api_command_with_fds(socket, "PUT", "add-device", Some(&device_config), &fds) .map_err(Error::HttpApiClient) } Some("remove-device") => { let remove_device_data = remove_device_config( matches .subcommand_matches("remove-device") .unwrap() .get_one::("id") .unwrap(), ); simple_api_command(socket, "PUT", "remove-device", Some(&remove_device_data)) .map_err(Error::HttpApiClient) } Some("add-disk") => { let disk_config = add_disk_config( matches .subcommand_matches("add-disk") .unwrap() .get_one::("disk_config") .unwrap(), )?; simple_api_command(socket, "PUT", "add-disk", Some(&disk_config)) .map_err(Error::HttpApiClient) } Some("add-fs") => { let fs_config = add_fs_config( matches .subcommand_matches("add-fs") .unwrap() .get_one::("fs_config") .unwrap(), )?; 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 .subcommand_matches("add-pmem") .unwrap() .get_one::("pmem_config") .unwrap(), )?; simple_api_command(socket, "PUT", "add-pmem", Some(&pmem_config)) .map_err(Error::HttpApiClient) } Some("add-net") => { let (net_config, fds) = add_net_config( matches .subcommand_matches("add-net") .unwrap() .get_one::("net_config") .unwrap(), )?; simple_api_command_with_fds(socket, "PUT", "add-net", Some(&net_config), &fds) .map_err(Error::HttpApiClient) } Some("add-user-device") => { let device_config = add_user_device_config( matches .subcommand_matches("add-user-device") .unwrap() .get_one::("device_config") .unwrap(), )?; simple_api_command(socket, "PUT", "add-user-device", Some(&device_config)) .map_err(Error::HttpApiClient) } Some("add-vdpa") => { let vdpa_config = add_vdpa_config( matches .subcommand_matches("add-vdpa") .unwrap() .get_one::("vdpa_config") .unwrap(), )?; simple_api_command(socket, "PUT", "add-vdpa", Some(&vdpa_config)) .map_err(Error::HttpApiClient) } Some("add-vsock") => { let vsock_config = add_vsock_config( matches .subcommand_matches("add-vsock") .unwrap() .get_one::("vsock_config") .unwrap(), )?; simple_api_command(socket, "PUT", "add-vsock", Some(&vsock_config)) .map_err(Error::HttpApiClient) } Some("snapshot") => { let snapshot_config = snapshot_config( matches .subcommand_matches("snapshot") .unwrap() .get_one::("snapshot_config") .unwrap(), ); simple_api_command(socket, "PUT", "snapshot", Some(&snapshot_config)) .map_err(Error::HttpApiClient) } Some("restore") => { let (restore_config, fds) = restore_config( matches .subcommand_matches("restore") .unwrap() .get_one::("restore_config") .unwrap(), )?; simple_api_command_with_fds(socket, "PUT", "restore", Some(&restore_config), &fds) .map_err(Error::HttpApiClient) } Some("coredump") => { let coredump_config = coredump_config( matches .subcommand_matches("coredump") .unwrap() .get_one::("coredump_config") .unwrap(), ); simple_api_command(socket, "PUT", "coredump", Some(&coredump_config)) .map_err(Error::HttpApiClient) } Some("send-migration") => { let send_migration_data = send_migration_data( matches .subcommand_matches("send-migration") .unwrap() .get_one::("send_migration_config") .unwrap(), )?; simple_api_command(socket, "PUT", "send-migration", Some(&send_migration_data)) .map_err(Error::HttpApiClient) } Some("receive-migration") => { let receive_migration_data = receive_migration_data( matches .subcommand_matches("receive-migration") .unwrap() .get_one::("receive_migration_config") .unwrap(), )?; simple_api_command( socket, "PUT", "receive-migration", Some(&receive_migration_data), ) .map_err(Error::HttpApiClient) } Some("create") => { let data = create_data( matches .subcommand_matches("create") .unwrap() .get_one::("path") .unwrap(), )?; simple_api_command(socket, "PUT", "create", Some(&data)).map_err(Error::HttpApiClient) } _ => unreachable!(), } } #[cfg(feature = "dbus_api")] fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) -> ApiResult { match matches.subcommand_name() { Some("boot") => proxy.api_vm_boot(), Some("delete") => proxy.api_vm_delete(), Some("shutdown-vmm") => proxy.api_vmm_shutdown(), Some("resume") => proxy.api_vm_resume(), Some("power-button") => proxy.api_vm_power_button(), Some("reboot") => proxy.api_vm_reboot(), Some("pause") => proxy.api_vm_pause(), Some("info") => proxy.api_vm_info(), Some("counters") => proxy.api_vm_counters(), Some("ping") => proxy.api_vmm_ping(), Some("shutdown") => proxy.api_vm_shutdown(), Some("resize") => { let resize = resize_config( matches .subcommand_matches("resize") .unwrap() .get_one::("cpus") .map(|x| x as &str), matches .subcommand_matches("resize") .unwrap() .get_one::("memory") .map(|x| x as &str), matches .subcommand_matches("resize") .unwrap() .get_one::("balloon") .map(|x| x as &str), )?; proxy.api_vm_resize(&resize) } Some("resize-zone") => { let resize_zone = resize_zone_config( matches .subcommand_matches("resize-zone") .unwrap() .get_one::("id") .unwrap(), matches .subcommand_matches("resize-zone") .unwrap() .get_one::("size") .unwrap(), )?; proxy.api_vm_resize_zone(&resize_zone) } Some("add-device") => { let (device_config, _fds) = add_device_config( matches .subcommand_matches("add-device") .unwrap() .get_one::("device_config") .unwrap(), )?; proxy.api_vm_add_device(&device_config) } Some("remove-device") => { let remove_device_data = remove_device_config( matches .subcommand_matches("remove-device") .unwrap() .get_one::("id") .unwrap(), ); proxy.api_vm_remove_device(&remove_device_data) } Some("add-disk") => { let disk_config = add_disk_config( matches .subcommand_matches("add-disk") .unwrap() .get_one::("disk_config") .unwrap(), )?; proxy.api_vm_add_disk(&disk_config) } Some("add-fs") => { let fs_config = add_fs_config( matches .subcommand_matches("add-fs") .unwrap() .get_one::("fs_config") .unwrap(), )?; 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 .subcommand_matches("add-pmem") .unwrap() .get_one::("pmem_config") .unwrap(), )?; proxy.api_vm_add_pmem(&pmem_config) } Some("add-net") => { let (net_config, _fds) = add_net_config( matches .subcommand_matches("add-net") .unwrap() .get_one::("net_config") .unwrap(), )?; proxy.api_vm_add_net(&net_config) } Some("add-user-device") => { let device_config = add_user_device_config( matches .subcommand_matches("add-user-device") .unwrap() .get_one::("device_config") .unwrap(), )?; proxy.api_vm_add_user_device(&device_config) } Some("add-vdpa") => { let vdpa_config = add_vdpa_config( matches .subcommand_matches("add-vdpa") .unwrap() .get_one::("vdpa_config") .unwrap(), )?; proxy.api_vm_add_vdpa(&vdpa_config) } Some("add-vsock") => { let vsock_config = add_vsock_config( matches .subcommand_matches("add-vsock") .unwrap() .get_one::("vsock_config") .unwrap(), )?; proxy.api_vm_add_vsock(&vsock_config) } Some("snapshot") => { let snapshot_config = snapshot_config( matches .subcommand_matches("snapshot") .unwrap() .get_one::("snapshot_config") .unwrap(), ); proxy.api_vm_snapshot(&snapshot_config) } Some("restore") => { let (restore_config, _fds) = restore_config( matches .subcommand_matches("restore") .unwrap() .get_one::("restore_config") .unwrap(), )?; proxy.api_vm_restore(&restore_config) } Some("coredump") => { let coredump_config = coredump_config( matches .subcommand_matches("coredump") .unwrap() .get_one::("coredump_config") .unwrap(), ); proxy.api_vm_coredump(&coredump_config) } Some("send-migration") => { let send_migration_data = send_migration_data( matches .subcommand_matches("send-migration") .unwrap() .get_one::("send_migration_config") .unwrap(), )?; proxy.api_vm_send_migration(&send_migration_data) } Some("receive-migration") => { let receive_migration_data = receive_migration_data( matches .subcommand_matches("receive-migration") .unwrap() .get_one::("receive_migration_config") .unwrap(), )?; proxy.api_vm_receive_migration(&receive_migration_data) } Some("create") => { let data = create_data( matches .subcommand_matches("create") .unwrap() .get_one::("path") .unwrap(), )?; proxy.api_vm_create(&data) } _ => unreachable!(), } } fn resize_config( cpus: Option<&str>, memory: Option<&str>, balloon: Option<&str>, ) -> Result { let desired_vcpus: Option = if let Some(cpus) = cpus { Some(cpus.parse().map_err(Error::InvalidCpuCount)?) } else { None }; let desired_ram: Option = if let Some(memory) = memory { Some( memory .parse::() .map_err(Error::InvalidMemorySize)? .0, ) } else { None }; let desired_balloon: Option = if let Some(balloon) = balloon { Some( balloon .parse::() .map_err(Error::InvalidBalloonSize)? .0, ) } else { None }; let resize = api::VmResizeData { desired_vcpus, desired_ram, desired_balloon, }; Ok(serde_json::to_string(&resize).unwrap()) } fn resize_disk_config(id: &str, size: &str) -> Result { let resize_disk = 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 = api::VmResizeZoneData { id: id.to_owned(), desired_ram: size .parse::() .map_err(Error::InvalidMemorySize)? .0, }; Ok(serde_json::to_string(&resize_zone).unwrap()) } fn add_device_config(config: &str) -> Result<(String, Vec), Error> { let mut device_config = DeviceConfig::parse(config).map_err(Error::AddDeviceConfig)?; // DeviceConfig is modified on purpose here by taking the file // descriptor out. Keeping it and sending it over to the server side // process would not make any sense since the file descriptor may be // represented with different values. let fds = device_config .fd .take() .map(|fd| vec![fd]) .unwrap_or_default(); let device_config = serde_json::to_string(&device_config).unwrap(); Ok((device_config, fds)) } fn add_user_device_config(config: &str) -> Result { let device_config = UserDeviceConfig::parse(config).map_err(Error::AddUserDeviceConfig)?; let device_config = serde_json::to_string(&device_config).unwrap(); Ok(device_config) } fn remove_device_config(id: &str) -> String { let remove_device_data = api::VmRemoveDeviceData { id: id.to_owned() }; serde_json::to_string(&remove_device_data).unwrap() } fn add_disk_config(config: &str) -> Result { let disk_config = DiskConfig::parse(config).map_err(Error::AddDiskConfig)?; let disk_config = serde_json::to_string(&disk_config).unwrap(); Ok(disk_config) } fn add_fs_config(config: &str) -> Result { let fs_config = FsConfig::parse(config).map_err(Error::AddFsConfig)?; let fs_config = serde_json::to_string(&fs_config).unwrap(); 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(); Ok(pmem_config) } fn add_net_config(config: &str) -> Result<(String, Vec), Error> { let mut net_config = NetConfig::parse(config).map_err(Error::AddNetConfig)?; // NetConfig is modified on purpose here by taking the list of file // descriptors out. Keeping the list and send it to the server side // process would not make any sense since the file descriptor may be // represented with different values. let fds = net_config.fds.take().unwrap_or_default(); let net_config = serde_json::to_string(&net_config).unwrap(); Ok((net_config, fds)) } fn add_vdpa_config(config: &str) -> Result { let vdpa_config = VdpaConfig::parse(config).map_err(Error::AddVdpaConfig)?; let vdpa_config = serde_json::to_string(&vdpa_config).unwrap(); Ok(vdpa_config) } fn add_vsock_config(config: &str) -> Result { let vsock_config = VsockConfig::parse(config).map_err(Error::AddVsockConfig)?; let vsock_config = serde_json::to_string(&vsock_config).unwrap(); Ok(vsock_config) } fn snapshot_config(url: &str) -> String { let snapshot_config = api::VmSnapshotConfig { destination_url: String::from(url), }; serde_json::to_string(&snapshot_config).unwrap() } fn restore_config(config: &str) -> Result<(String, Vec), Error> { let mut restore_config = RestoreConfig::parse(config).map_err(Error::Restore)?; // RestoreConfig is modified on purpose to take out the file descriptors. // These fds are passed to the server side process via SCM_RIGHTS let fds = match &mut restore_config.net_fds { Some(net_fds) => net_fds .iter_mut() .flat_map(|net| net.fds.take().unwrap_or_default()) .collect(), None => Vec::new(), }; let restore_config = serde_json::to_string(&restore_config).unwrap(); Ok((restore_config, fds)) } fn coredump_config(destination_url: &str) -> String { let coredump_config = api::VmCoredumpData { destination_url: String::from(destination_url), }; serde_json::to_string(&coredump_config).unwrap() } fn receive_migration_data(config: &str) -> Result { let receive_migration_data = api::VmReceiveMigrationData::parse(config).map_err(Error::ReceiveMigrationConfig)?; Ok(serde_json::to_string(&receive_migration_data).unwrap()) } fn send_migration_data(config: &str) -> Result { let send_migration_data = 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 { let mut data = String::default(); if path == "-" { io::stdin() .read_to_string(&mut data) .map_err(Error::ReadingStdin)?; } else { data = fs::read_to_string(path).map_err(Error::ReadingFile)?; } Ok(data) } /// Returns all [`Arg`]s in alphabetical order. /// /// This is the order used in the `--help` output. fn get_cli_args() -> Box<[Arg]> { [ Arg::new("api-socket") .long("api-socket") .help("HTTP API socket path (UNIX domain socket).") .num_args(1), #[cfg(feature = "dbus_api")] Arg::new("dbus-object-path") .long("dbus-object-path") .help("Object path which the interface is being served at") .num_args(1), #[cfg(feature = "dbus_api")] Arg::new("dbus-service-name") .long("dbus-service-name") .help("Well known name of the dbus service") .num_args(1), #[cfg(feature = "dbus_api")] Arg::new("dbus-system-bus") .long("dbus-system-bus") .action(ArgAction::SetTrue) .num_args(0) .help("Use the system bus instead of a session bus"), ] .to_vec() .into_boxed_slice() } /// Returns all [`Command`]s in alphabetical order. /// /// This is the order used in the `--help` output. fn get_cli_commands_sorted() -> Box<[Command]> { [ Command::new("add-device").about("Add VFIO device").arg( Arg::new("device_config") .index(1) .help(DeviceConfig::SYNTAX), ), Command::new("add-disk") .about("Add block device") .arg(Arg::new("disk_config").index(1).help(DiskConfig::SYNTAX)), Command::new("add-fs") .about("Add virtio-fs backed fs device") .arg(Arg::new("fs_config").index(1).help(FsConfig::SYNTAX)), Command::new("add-generic-vhost-user") .about("Add generic vhost-user device") .arg( Arg::new("generic_vhost_user_config") .index(1) .help(GenericVhostUserConfig::SYNTAX), ), Command::new("add-net") .about("Add network device") .arg(Arg::new("net_config").index(1).help(NetConfig::SYNTAX)), Command::new("add-pmem") .about("Add persistent memory device") .arg(Arg::new("pmem_config").index(1).help(PmemConfig::SYNTAX)), Command::new("add-user-device") .about("Add userspace device") .arg( Arg::new("device_config") .index(1) .help(UserDeviceConfig::SYNTAX), ), Command::new("add-vdpa") .about("Add vDPA device") .arg(Arg::new("vdpa_config").index(1).help(VdpaConfig::SYNTAX)), Command::new("add-vsock") .about("Add vsock device") .arg(Arg::new("vsock_config").index(1).help(VsockConfig::SYNTAX)), Command::new("boot").about("Boot a created VM"), Command::new("coredump") .about("Create a coredump from VM") .arg(Arg::new("coredump_config").index(1).help("")), Command::new("counters").about("Counters from the VM"), Command::new("create") .about("Create VM from a JSON configuration") .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("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("power-button").about("Trigger a power button in the VM"), Command::new("reboot").about("Reboot the VM"), Command::new("receive-migration") .about("Receive a VM migration") .arg( Arg::new("receive_migration_config") .index(1) .help(api::VmReceiveMigrationData::SYNTAX), ), Command::new("remove-device") .about("Remove VFIO and PCI device") .arg(Arg::new("id").index(1).help("")), Command::new("resize") .about("Resize the VM") .arg( Arg::new("balloon") .long("balloon") .help("New balloon size in bytes (supports K/M/G suffix)") .num_args(1), ) .arg( Arg::new("cpus") .long("cpus") .help("New vCPUs count") .num_args(1), ) .arg( Arg::new("memory") .long("memory") .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( Arg::new("id") .long("id") .help("Memory zone identifier") .num_args(1), ) .arg( Arg::new("size") .long("size") .help("New memory zone size in bytes (supports K/M/G suffix)") .num_args(1), ), Command::new("restore") .about("Restore VM from a snapshot") .arg( Arg::new("restore_config") .index(1) .required(true) .help(RestoreConfig::SYNTAX), ), Command::new("resume").about("Resume the VM"), Command::new("send-migration") .about("Initiate a VM migration") .arg( Arg::new("send_migration_config") .index(1) .help(api::VmSendMigrationData::SYNTAX), ), Command::new("shutdown").about("Shutdown the VM"), Command::new("shutdown-vmm").about("Shutdown the VMM"), Command::new("snapshot") .about("Create a snapshot from VM") .arg( Arg::new("snapshot_config") .index(1) .required(true) .help(""), ), ] .to_vec() .into_boxed_slice() } fn main() { env_logger::init(); let app = Command::new("ch-remote") .author(env!("CARGO_PKG_AUTHORS")) .version(env!("BUILD_VERSION")) .about("Remotely control a cloud-hypervisor VMM.") .arg_required_else_help(true) .subcommand_required(true) .args(get_cli_args()) .subcommands(get_cli_commands_sorted()); let matches = app.get_matches(); let mut target_api = match ( matches.get_one::("api-socket"), #[cfg(feature = "dbus_api")] matches.get_one::("dbus-service-name"), #[cfg(feature = "dbus_api")] matches.get_one::("dbus-object-path"), ) { #[cfg(not(feature = "dbus_api"))] (Some(api_sock),) => TargetApi::HttpApi( UnixStream::connect(api_sock).unwrap_or_else(|e| { error!("Error opening HTTP socket: {e}"); process::exit(1) }), PhantomData, ), #[cfg(feature = "dbus_api")] (Some(api_sock), None, None) => TargetApi::HttpApi( UnixStream::connect(api_sock).unwrap_or_else(|e| { error!("Error opening HTTP socket: {e}"); process::exit(1) }), PhantomData, ), #[cfg(feature = "dbus_api")] (None, Some(dbus_name), Some(dbus_path)) => TargetApi::DBusApi( DBusApi1ProxyBlocking::new_connection( dbus_name, dbus_path, matches.get_flag("dbus-system-bus"), ) .map_err(Error::DBusApiClient) .unwrap_or_else(|e| { error!("Error creating D-Bus proxy: {e}"); process::exit(1) }), ), #[cfg(feature = "dbus_api")] (Some(_), Some(_) | None, Some(_) | None) => { error!( "`api-socket` and (dbus-service-name or dbus-object-path) are mutually exclusive" ); process::exit(1); } _ => { 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. #[expect(clippy::needless_pass_by_value)] fn join_strs(mut acc: String, next: String) -> String { if !acc.is_empty() { acc.push('\n'); } acc.push_str(&next); acc } // This function helps to modify the Display representation of remote // API failures so that it aligns with the regular output of error // messages. As we transfer a deep/rich chain of errors as String via // the HTTP API, the nested error chain is lost. We retrieve it from // the error response. // // In case the repose itself is broken, the error is printed directly // by using the `X` level. fn server_api_error_display_modifier( level: usize, indention: usize, error: &(dyn error::Error + 'static), ) -> Option { if let Some(api_client::Error::ServerResponse(status_code, body)) = error.downcast_ref::() { let body = body.as_ref().map_or("", |body| body.as_str()); // Retrieve the list of error messages back. let lines: Vec = match serde_json::from_str(body) { Ok(json) => json, Err(e) => { return Some(format!( "{idention}X: Can't get remote's error messages from JSON response: {e}: body='{body}'", idention = " ".repeat(indention) )); } }; let error_status = format!("Server responded with {status_code:?}"); // Prepend the error status line to the lines iter. let lines = iter::once(error_status.as_str()).chain(lines.iter().map(|s| s.as_str())); let error_msg_multiline = lines .enumerate() .map(|(index, error_msg)| (index + level, error_msg)) .map(|(level, error_msg)| { format!( "{idention}{level}: {error_msg}", idention = " ".repeat(indention) ) }) .fold(String::new(), join_strs); return Some(error_msg_multiline); } None } let top_error: &dyn error::Error = &top_error; cloud_hypervisor::cli_print_error_chain( top_error, "ch-remote", server_api_error_display_modifier, ); process::exit(1) } } #[cfg(test)] mod unit_tests { use std::cmp::Ordering; use super::*; use crate::test_util::assert_args_sorted; #[test] fn test_cli_args_sorted() { let args = get_cli_args(); assert_args_sorted(|| args.iter()); } #[test] fn test_cli_commands_sorted() { let commands = get_cli_commands_sorted(); // check commands itself are sorted let iter = commands.iter().zip(commands.iter().skip(1)); for (command, next) in iter { assert_ne!( command.get_name().cmp(next.get_name()), Ordering::Greater, "commands not alphabetically sorted: command={}, next={}", command.get_name(), next.get_name() ); } // check args of commands sorted for command in commands { assert_args_sorted(|| command.get_arguments()); } } #[test] fn test_error_deserialization() { let body = r#"["Error from API","The VM could not be snapshotted","Cannot send VM snapshot","Failed to send migratable component snapshot","Destination is not a directory: \"/tmp/ch.dump\""]"#; let lines: Result, _> = serde_json::from_str(body); assert!(lines.is_ok()); let lines = lines.unwrap(); assert_eq!(lines.len(), 5); assert_eq!( lines[4], r#"Destination is not a directory: "/tmp/ch.dump""# ); } } cloud-hypervisor-53.0/cloud-hypervisor/src/lib.rs000066400000000000000000000027321522476750100222150ustar00rootroot00000000000000// Copyright © 2025 Cyberus Technology GmbH // // SPDX-License-Identifier: Apache-2.0 use std::error::Error; use std::iter; 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. pub fn cli_print_error_chain<'a>( top_error: &'a (dyn Error + 'static), component: &str, // Function optionally returning the display representation of an error. display_modifier: impl Fn( /* level */ usize, /*indention */ usize, &'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:"); eprintln!(" {top_error}"); } else { eprintln!("chain of errors:"); iter::successors(Some(top_error), |sub_error| { // Dereference necessary to mitigate rustc compiler bug. // See (*sub_error).source() }) .enumerate() .for_each(|(level, error)| { // Special case: handling of HTTP Server responses in ch-remote if let Some(message) = display_modifier(level, 2, error) { eprintln!("{message}"); } else { eprintln!(" {level}: {error}"); } }); } } cloud-hypervisor-53.0/cloud-hypervisor/src/logger.rs000066400000000000000000000552721522476750100227350ustar00rootroot00000000000000// Copyright © 2026 Cloud Hypervisor Contributors // // SPDX-License-Identifier: Apache-2.0 // use std::io::{self, Write}; use std::str::FromStr; use std::sync::Mutex; use std::time::Instant; use std::{mem, process, thread}; use jiff::tz::TimeZone; use thiserror::Error; #[derive(Debug, Error)] 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, ) -> 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(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: 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(TimeZone::UTC)); write!(&mut *out, "{:.6}", zoned.timestamp()) } Token::Glog => { let zoned = zoned_utc .get_or_insert_with(|| jiff::Timestamp::now().to_zoned(TimeZone::UTC)); write!(&mut *out, "{}", zoned.strftime("%m%d %H:%M:%S%.6f")) } Token::LocalGlog => { let zoned = zoned_local.get_or_insert_with(|| { jiff::Timestamp::now().to_zoned(self.local_tz.clone()) }); write!(&mut *out, "{}", zoned.strftime("%m%d %H:%M:%S%.6f")) } Token::Pid => write!(&mut *out, "{}", self.pid), // SAFETY: gettid(2) always succeeds Token::Tid => write!(&mut *out, "{}", unsafe { libc::gettid() }), Token::Thread => write!( &mut *out, "{}", thread::current().name().unwrap_or("anonymous") ), Token::Level => write!(&mut *out, "{}", record.level()), Token::LevelChar => write!(&mut *out, "{}", level_char(record.level())), Token::Location => match (record.file(), record.line()) { (Some(file), Some(line)) => write!(&mut *out, "{file}:{line}"), _ => write!(&mut *out, "{}", record.target()), }, Token::Msg => write!(&mut *out, "{}", record.args()), Token::Time(field, zone) => { let zoned = match zone { Zone::Utc => zoned_utc .get_or_insert_with(|| jiff::Timestamp::now().to_zoned(TimeZone::UTC)), Zone::Local => zoned_local.get_or_insert_with(|| { jiff::Timestamp::now().to_zoned(self.local_tz.clone()) }), }; write_time_field(&mut *out, *field, zoned) } }; } let _ = out.write_all(b"\r\n"); } 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, 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}"); } } cloud-hypervisor-53.0/cloud-hypervisor/src/main.rs000066400000000000000000002203661522476750100224000ustar00rootroot00000000000000// Copyright © 2019 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // mod logger; #[cfg(test)] mod test_util; use std::fs::{self, File}; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::path::Path; #[cfg(feature = "guest_debug")] use std::path::PathBuf; use std::sync::mpsc::channel; use std::{any, cmp, env, io, num, process, str, thread}; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command}; use event_monitor::event; use libc::EFD_NONBLOCK; use log::{LevelFilter, error, info, warn}; use option_parser::OptionParser; use seccompiler::SeccompAction; use signal_hook::consts::SIGSYS; use signal_hook::low_level; use thiserror::Error; use vm_migration::protocol; #[cfg(feature = "dbus_api")] use vmm::api::dbus::{DBusApiOptions, dbus_api_graceful_shutdown}; use vmm::api::http::http_api_graceful_shutdown; use vmm::api::{self, ApiAction}; use vmm::config::{self, RestoreConfig, VmParams}; use vmm::landlock::{Landlock, LandlockError}; use vmm::vm::Vm; use vmm::vm_config; #[cfg(feature = "fw_cfg")] use vmm::vm_config::FwCfgConfig; #[cfg(feature = "ivshmem")] use vmm::vm_config::IvshmemConfig; use vmm::vm_config::{ BalloonConfig, ConsoleConfig, DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, LandlockConfig, NetConfig, NumaConfig, PciSegmentConfig, PlatformConfig, PmemConfig, RateLimiterGroupConfig, RngConfig, RtcConfig, SerialConfig, TpmConfig, UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, }; use vmm_sys_util::eventfd::EventFd; use vmm_sys_util::signal::block_signal; use crate::logger::Logger; // Linux exposes seccomp's SIGSYS payload via the siginfo_t layout; this struct mirrors the // fields we need so the handler can read the syscall and arch. #[repr(C)] struct SeccompSiginfo { si_signo: libc::c_int, si_errno: libc::c_int, si_code: libc::c_int, _pad0: libc::c_int, si_call_addr: *mut libc::c_void, si_syscall: libc::c_int, si_arch: libc::c_uint, } fn handle_sigsys(info: &libc::siginfo_t) { // SAFETY: The handler only reads the provided siginfo pointer, writes a // diagnostic message, and then delegates to the default SIGSYS handler. unsafe { let current_thread = thread::current(); let thread_name = current_thread.name().unwrap_or(""); let tid = libc::syscall(libc::SYS_gettid) as i64; let info = &*(info as *const libc::siginfo_t as *const SeccompSiginfo); eprintln!( concat!( "\n==== Possible seccomp violation ====\n", "Syscall number: {} (arch: {:#x}, tid: {}, thread: {})\n", "Try running with `strace -ff` to identify the cause and open an issue: ", "https://github.com/cloud-hypervisor/cloud-hypervisor/issues/new", ), info.si_syscall, info.si_arch, tid, thread_name, ); low_level::emulate_default_handler(SIGSYS).unwrap(); } } #[cfg(feature = "dhat-heap")] #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; #[derive(Error, Debug)] enum Error { #[error("Failed to create API EventFd")] CreateApiEventFd(#[source] io::Error), #[cfg(feature = "guest_debug")] #[error("Failed to create Debug EventFd")] CreateDebugEventFd(#[source] io::Error), #[error("Failed to create exit EventFd")] CreateExitEventFd(#[source] io::Error), #[error("Failed to open hypervisor interface (is hypervisor interface available?)")] CreateHypervisor(#[source] hypervisor::HypervisorError), #[error("Failed to start the VMM thread")] StartVmmThread(#[source] vmm::Error), #[error("Error parsing config")] ParsingConfig(#[source] config::Error), #[error("Error creating VM")] VmCreate(#[source] api::ApiError), #[error("Error booting VM")] VmBoot(#[source] api::ApiError), #[error("Error restoring VM")] VmRestore(#[source] api::ApiError), #[error("Error parsing restore")] ParsingRestore(#[source] config::Error), #[error("Failed to join on VMM thread: {0:?}")] ThreadJoin(Box), #[error("VMM thread exited with error")] VmmThread(#[source] vmm::Error), #[error("Error parsing --api-socket")] ParsingApiSocket(#[source] num::ParseIntError), #[error("Error parsing --event-monitor")] ParsingEventMonitor(#[source] option_parser::OptionParserError), #[cfg(feature = "dbus_api")] #[error("`--dbus-object-path` option isn't provided")] MissingDBusObjectPath, #[cfg(feature = "dbus_api")] #[error("`--dbus-service-name` option isn't provided")] MissingDBusServiceName, #[error("Error parsing --event-monitor: path or fd required")] BareEventMonitor, #[error("Error doing event monitor I/O")] EventMonitorIo(#[source] io::Error), #[error("Event monitor thread failed")] EventMonitorThread(#[source] vmm::Error), #[cfg(feature = "guest_debug")] #[error("Error parsing --gdb")] ParsingGdb(#[source] option_parser::OptionParserError), #[cfg(feature = "guest_debug")] #[error("Error parsing --gdb: path required")] BareGdb, #[error("Error creating log file")] LogFileCreation(#[source] 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")] HttpApiShutdown(#[source] vmm::Error), #[error("Failed to create Landlock object")] CreateLandlock(#[source] LandlockError), #[error("Failed to apply Landlock")] ApplyLandlock(#[source] LandlockError), } #[derive(Error, Debug)] enum FdTableError { #[error("Failed to create event fd")] CreateEventFd(#[source] io::Error), #[error("Failed to obtain file limit")] GetRLimit(#[source] io::Error), #[error("Error calling fcntl with F_GETFD")] GetFd(#[source] io::Error), #[error("Failed to duplicate file handle")] Dup2(#[source] io::Error), } fn prepare_default_values() -> (String, String, String) { (default_vcpus(), default_memory(), default_rng()) } fn default_vcpus() -> String { format!( "boot={},max_phys_bits={}", vm_config::DEFAULT_VCPUS, vm_config::DEFAULT_MAX_PHYS_BITS ) } fn default_memory() -> String { format!("size={}M", vm_config::DEFAULT_MEMORY_MB) } fn default_rng() -> String { format!("src={}", RngConfig::DEFAULT_RNG_SOURCE) } /// Returns all [`Arg`]s in alphabetical order. This is the order used in the /// `--help` output. fn get_cli_options_sorted( default_vcpus: String, default_memory: String, default_rng: String, ) -> Box<[Arg]> { [ Arg::new("api-socket") .long("api-socket") .help("HTTP API socket (UNIX domain socket): path= or fd=.") .num_args(1) .group("vmm-config"), Arg::new("balloon") .long("balloon") .help(BalloonConfig::SYNTAX) .num_args(1) .group("vm-config"), Arg::new("cmdline") .long("cmdline") .help("Kernel command line") .num_args(1) .group("vm-config"), Arg::new("console") .long("console") .help(ConsoleConfig::SYNTAX) .default_value("tty") .group("vm-config"), Arg::new("cpus") .long("cpus") .help( "boot=,max=,\ topology=:::,\ kvm_hyperv=on|off,max_phys_bits=,\ affinity=,\ features=,\ nested=on|off,core_scheduling=vm|vcpu|off", ) .default_value(default_vcpus) .group("vm-config"), #[cfg(feature = "dbus_api")] Arg::new("dbus-object-path") .long("dbus-object-path") .help("Object path to serve the dbus interface") .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("Path to report events on: path= or fd=") .num_args(1) .group("vmm-config"), Arg::new("firmware") .long("firmware") .help("Path to firmware that is loaded in an architectural specific way") .num_args(1) .group("vm-payload"), Arg::new("fs") .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=") .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") .help("Path to IGVM file to load.") .num_args(1) .group("vm-payload"), #[cfg(feature = "sev_snp")] Arg::new("host-data") .long("host-data") .help("Host specific data to SEV SNP guest") .num_args(1) .group("vm-config"), Arg::new("initramfs") .long("initramfs") .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( "Path to kernel to load. This may be a kernel or firmware that supports a PVH \ entry point (e.g. vmlinux) or architecture equivalent", ) .num_args(1) .group("vm-payload"), Arg::new("landlock") .long("landlock") .num_args(0) .help("enable/disable Landlock.") .action(ArgAction::SetTrue) .default_value("false") .group("vm-config"), Arg::new("landlock-rules") .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( "Memory parameters \ \"size=,mergeable=on|off,shared=on|off,\ hugepages=on|off,hugepage_size=,\ hotplug_method=acpi|virtio-mem,\ hotplug_size=,\ hotplugged_size=,\ prefault=on|off,reserve=on|off,thp=on|off\"", ) .default_value(default_memory) .group("vm-config"), Arg::new("memory-zone") .long("memory-zone") .help( "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,reserve=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(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") .long("pvmemcontrol") .help("Pvmemcontrol device") .num_args(0) .action(ArgAction::SetTrue) .group("vm-config"), Arg::new("pvpanic") .long("pvpanic") .help("Enable pvpanic device") .num_args(0) .action(ArgAction::SetTrue) .group("vm-config"), Arg::new("rate-limit-group") .long("rate-limit-group") .help(RateLimiterGroupConfig::SYNTAX) .num_args(1..) .action(ArgAction::Append) .group("vm-config"), Arg::new("restore") .long("restore") .help(RestoreConfig::SYNTAX) .num_args(1) .group("vmm-config"), Arg::new("rng") .long("rng") .help(RngConfig::SYNTAX) .default_value(default_rng) .group("vm-config"), Arg::new("rtc") .long("rtc") .help(RtcConfig::SYNTAX) .num_args(0..=1) .default_missing_value("") .group("vm-config"), Arg::new("seccomp") .long("seccomp") .num_args(1) .value_parser(["true", "false", "log", "errno"]) .default_value("true"), Arg::new("serial") .long("serial") .help(SerialConfig::SYNTAX) .default_value("null") .group("vm-config"), Arg::new("tpm") .long("tpm") .num_args(1) .help(TpmConfig::SYNTAX) .group("vm-config"), Arg::new("user-device") .long("user-device") .help(UserDeviceConfig::SYNTAX) .num_args(1..) .action(ArgAction::Append) .group("vm-config"), Arg::new("v") .short('v') .action(ArgAction::Count) .help("Sets the level of debugging output") .group("logging"), Arg::new("vdpa") .long("vdpa") .help(VdpaConfig::SYNTAX) .num_args(1..) .action(ArgAction::Append) .group("vm-config"), Arg::new("version") .short('V') .long("version") .action(ArgAction::SetTrue) .help("Print version") .num_args(0), Arg::new("vsock") .long("vsock") .help(VsockConfig::SYNTAX) .num_args(1) .group("vm-config"), Arg::new("watchdog") .long("watchdog") .help("Enable virtio-watchdog") .num_args(0) .action(ArgAction::SetTrue) .group("vm-config"), ] .to_vec() .into_boxed_slice() } /// Creates the CLI definition of Cloud Hypervisor. fn create_app(default_vcpus: String, default_memory: String, default_rng: String) -> Command { let groups = [ ArgGroup::new("vm-config") .multiple(true) .requires("vm-payload"), ArgGroup::new("vmm-config").multiple(true), ArgGroup::new("logging").multiple(true), ArgGroup::new("vm-payload").multiple(true), ]; let args = get_cli_options_sorted(default_vcpus, default_memory, default_rng); Command::new("cloud-hypervisor") // 'BUILD_VERSION' is set by the build script 'build.rs' at // compile time .author(env!("CARGO_PKG_AUTHORS")) .about("Launch a cloud-hypervisor VMM.") .arg_required_else_help(true) .groups(groups) .args(args) } 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, 2 => LevelFilter::Debug, _ => LevelFilter::Trace, }; let log_file: Box = if let Some(ref file) = cmd_arguments.get_one::("log-file") { Box::new(File::create(Path::new(file)).map_err(Error::LogFileCreation)?) } else { Box::new(io::stderr()) }; 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)?; let api_request_sender_clone = api_request_sender.clone(); let seccomp_action = if let Some(seccomp_value) = cmd_arguments.get_one::("seccomp") { match seccomp_value as &str { "true" => SeccompAction::Trap, "false" => SeccompAction::Allow, "log" => SeccompAction::Log, "errno" => SeccompAction::Errno(libc::EPERM as u32), val => { // The user providing an invalid value will be rejected panic!("Invalid parameter {val} for \"--seccomp\" flag"); } } } else { SeccompAction::Trap }; if seccomp_action == SeccompAction::Trap { // SAFETY: We only use signal_hook for managing signals and only execute signal // handler safe functions (writing to stderr) and manipulating signals. unsafe { signal_hook_registry::register_sigaction(SIGSYS, handle_sigsys) .map_err(|e| error!("Error adding SIGSYS signal handler: {e}")) .ok(); } } // SAFETY: Trivially safe. unsafe { libc::signal(libc::SIGCHLD, libc::SIG_IGN); } // Before we start any threads, mask the signals we'll be // installing handlers for, to make sure they only ever run on the // dedicated signal handling thread we'll start in a bit. for sig in &Vm::HANDLED_SIGNALS { if let Err(e) = block_signal(*sig) { error!("Error blocking signals: {e}"); } } for sig in &vmm::Vmm::HANDLED_SIGNALS { if let Err(e) = block_signal(*sig) { error!("Error blocking signals: {e}"); } } info!("{} starting", env!("BUILD_VERSION")); let hypervisor = hypervisor::new().map_err(Error::CreateHypervisor)?; #[cfg(feature = "guest_debug")] let gdb_socket_path = if let Some(gdb_config) = cmd_arguments.get_one::("gdb") { let mut parser = OptionParser::new(); parser.add("path"); parser.parse(gdb_config).map_err(Error::ParsingGdb)?; if parser.is_set("path") { Some(PathBuf::from(parser.get("path").unwrap())) } else { return Err(Error::BareGdb); } } else { None }; #[cfg(feature = "guest_debug")] let debug_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateDebugEventFd)?; #[cfg(feature = "guest_debug")] let vm_debug_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateDebugEventFd)?; 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 .get_one::("event-monitor") .as_ref() .map(|monitor_config| { let mut parser = OptionParser::new(); parser.add("path").add("fd"); parser .parse(monitor_config) .map_err(Error::ParsingEventMonitor)?; if parser.is_set("fd") { let fd = parser .convert("fd") .map_err(Error::ParsingEventMonitor)? .unwrap(); // SAFETY: fd is valid Ok(Some(unsafe { File::from_raw_fd(fd) })) } else if parser.is_set("path") { Ok(Some( fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(parser.get("path").unwrap()) .map_err(Error::EventMonitorIo)?, )) } else { Err(Error::BareEventMonitor) } }) .transpose()? .map(|event_monitor_file| { event_monitor::set_monitor(event_monitor_file).map_err(Error::EventMonitorIo) }) .transpose()?; #[cfg(feature = "dbus_api")] let dbus_options = match ( cmd_arguments.get_one::("dbus-service-name"), cmd_arguments.get_one::("dbus-object-path"), ) { (Some(name), Some(path)) => { // monitor is either set (file based) or not. // if it's not set, create one without file support. let mut monitor = match event_monitor.take() { Some(monitor) => monitor, None => event_monitor::set_monitor(None).map_err(Error::EventMonitorIo)?, }; let options = DBusApiOptions { service_name: name.to_string(), object_path: path.to_string(), system_bus: cmd_arguments.get_flag("dbus-system-bus"), event_monitor_rx: monitor.subscribe(), }; event_monitor = Some(monitor); Ok(Some(options)) } (Some(_), None) => Err(Error::MissingDBusObjectPath), (None, Some(_)) => Err(Error::MissingDBusServiceName), (None, None) => Ok(None), }?; if let Some(monitor) = event_monitor { vmm::start_event_monitor_thread( monitor, &seccomp_action, landlock_enable, exit_evt.try_clone().unwrap(), ) .map_err(Error::EventMonitorThread)?; } 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_fd, #[cfg(feature = "dbus_api")] dbus_options, api_evt.try_clone().unwrap(), api_request_sender_clone, api_request_receiver, #[cfg(feature = "guest_debug")] gdb_socket_path, #[cfg(feature = "guest_debug")] debug_evt.try_clone().unwrap(), #[cfg(feature = "guest_debug")] vm_debug_evt.try_clone().unwrap(), exit_evt.try_clone().unwrap(), &seccomp_action, hypervisor, no_shutdown, landlock_enable, ) .map_err(Error::StartVmmThread)?; let r: Result<(), Error> = (|| { #[cfg(feature = "igvm")] let payload_present = cmd_arguments.contains_id("kernel") || cmd_arguments.contains_id("firmware") || cmd_arguments.contains_id("igvm"); #[cfg(not(feature = "igvm"))] let payload_present = cmd_arguments.contains_id("kernel") || cmd_arguments.contains_id("firmware"); if payload_present { 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. let sender = api_request_sender.clone(); api::VmCreate .send( api_evt.try_clone().unwrap(), api_request_sender, Box::new(vm_config), ) .map_err(Error::VmCreate)?; api::VmBoot .send(api_evt.try_clone().unwrap(), sender, ()) .map_err(Error::VmBoot)?; } else if let Some(restore_params) = cmd_arguments.get_one::("restore") { api::VmRestore .send( api_evt.try_clone().unwrap(), api_request_sender, RestoreConfig::parse(restore_params).map_err(Error::ParsingRestore)?, ) .map_err(Error::VmRestore)?; } Ok(()) })(); if r.is_err() && let Err(e) = exit_evt.write(1) { warn!("writing to exit EventFd: {e}"); } if landlock_enable { Landlock::new() .map_err(Error::CreateLandlock)? .restrict_self() .map_err(Error::ApplyLandlock)?; } vmm_thread_handle .thread_handle .join() .map_err(Error::ThreadJoin)? .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)?; } #[cfg(feature = "dbus_api")] if let Some(chs) = vmm_thread_handle.dbus_shutdown_chs { dbus_api_graceful_shutdown(chs); } r } // This is a best-effort solution to the latency induced by the RCU // synchronization that happens in the kernel whenever the file descriptor table // fills up. // The table has initially 64 entries on amd64 and every time it fills up, a new // table is created, double the size of the current one, and the entries are // copied to the new table. The filesystem code that does this uses // synchronize_rcu() to ensure all preexisting RCU read-side critical sections // have completed: // // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/file.c?h=v6.9.1#n162 // // Rust programs that create lots of file handles or use // {File,EventFd}::try_clone() to share them are impacted by this issue. This // behavior is quite noticeable in the snapshot restore scenario, the latency is // a big chunk of the total time required to start cloud-hypervisor and restore // the snapshot. // // The kernel has an optimization in code, where it doesn't call // synchronize_rcu() if there is only one thread in the process. We can take // advantage of this optimization by expanding the descriptor table at // application start, when it has only one thread. // // The code tries to resize the table to an adequate size for most use cases, // 4096, this way we avoid any expansion that might take place later. fn expand_fdtable() -> Result<(), FdTableError> { let mut limits = libc::rlimit { rlim_cur: 0, rlim_max: 0, }; // SAFETY: FFI call with valid arguments if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) } < 0 { return Err(FdTableError::GetRLimit(io::Error::last_os_error())); } let table_size = if limits.rlim_cur == libc::RLIM_INFINITY { 4096 } else { cmp::min(limits.rlim_cur, 4096) as libc::c_int }; // The first 3 handles are stdin, stdout, stderr. We don't want to touch // any of them. if table_size <= 3 { return Ok(()); } let dummy_evt = EventFd::new(0).map_err(FdTableError::CreateEventFd)?; // Test if the file descriptor is empty // SAFETY: FFI call with valid arguments let flags: i32 = unsafe { libc::fcntl(table_size - 1, libc::F_GETFD) }; if flags >= 0 { // Nothing to do, the table is already big enough return Ok(()); } let err = io::Error::last_os_error(); if err.raw_os_error() != Some(libc::EBADF) { return Err(FdTableError::GetFd(err)); } // SAFETY: FFI call with valid arguments if unsafe { libc::dup2(dummy_evt.as_raw_fd(), table_size - 1) } < 0 { return Err(FdTableError::Dup2(io::Error::last_os_error())); } // SAFETY: FFI call, trivially unsafe { libc::close(table_size - 1) }; Ok(()) } fn main() { #[cfg(feature = "tdx")] compile_error!("Feature 'tdx' is broken."); #[cfg(all(feature = "tdx", feature = "sev_snp"))] compile_error!("Feature 'tdx' and 'sev_snp' are mutually exclusive."); #[cfg(all(feature = "sev_snp", not(target_arch = "x86_64")))] compile_error!("Feature 'sev_snp' needs target 'x86_64'"); #[cfg(all(feature = "fw_cfg", target_arch = "riscv64"))] compile_error!("Feature 'fw_cfg' needs targets 'x86_64' or 'aarch64'"); #[cfg(all(feature = "igvm", not(any(feature = "kvm", feature = "mshv"))))] compile_error!("Feature 'igvm' needs feature 'kvm' or 'mshv'"); #[cfg(all(feature = "sev_snp", not(any(feature = "kvm", feature = "mshv"))))] compile_error!("Feature 'sev_snp' needs feature 'kvm' or 'mshv'"); #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); // Ensure all created files (.e.g sockets) are only accessible by this user // SAFETY: trivially safe let _ = unsafe { libc::umask(0o077) }; let (default_vcpus, default_memory, default_rng) = prepare_default_values(); let cmd_arguments = create_app(default_vcpus, default_memory, default_rng).get_matches(); if cmd_arguments.get_flag("version") { println!("{} {}", env!("CARGO_BIN_NAME"), env!("BUILD_VERSION")); let migration_protocol_versions = protocol::supported_protocol_versions() .map(|version| version.to_string()) .collect::>() .join(", "); println!("Migration Protocol Versions: {migration_protocol_versions}"); if cmd_arguments.get_count("v") != 0 { println!("Enabled features: {:?}", vmm::feature_list()); } return; } if let Err(e) = expand_fdtable() { warn!("Error expanding FD table: {e}"); } 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); process::exit(1); } }; let vmm_result = start_vmm(&cmd_arguments, &api_socket_path, api_socket_fd); // Remove the socket only when we actually ran (Ok): a failed start may mean // another instance already holds the path, and removing it would clobber // that live socket. A stale socket left by a crash is cleared under the lock // on the next start. if vmm_result.is_ok() && let Some(ref api_socket_path) = api_socket_path { let _ = fs::remove_file(api_socket_path); } let exit_code = match vmm_result { Ok(()) => { info!("Cloud Hypervisor exited successfully"); 0 } Err(top_error) => { cloud_hypervisor::cli_print_error_chain(&top_error, "Cloud Hypervisor", |_, _, _| None); 1 } }; #[cfg(feature = "dhat-heap")] drop(_profiler); process::exit(exit_code); } #[cfg(test)] mod unit_tests { use std::path::PathBuf; use vmm::config::VmParams; #[cfg(target_arch = "x86_64")] use vmm::vm_config::DebugConsoleConfig; use vmm::vm_config::{ CommonConsoleConfig, ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, PciDeviceCommonConfig, RngConfig, SerialConfig, VmConfig, }; use crate::test_util::assert_args_sorted; use crate::{create_app, get_cli_options_sorted, prepare_default_values}; fn get_vm_config_from_vec(args: &[&str]) -> VmConfig { let (default_vcpus, default_memory, default_rng) = prepare_default_values(); let cmd_arguments = create_app(default_vcpus, default_memory, default_rng).get_matches_from(args); let vm_params = VmParams::from_arg_matches(&cmd_arguments); VmConfig::parse(vm_params).unwrap() } fn compare_vm_config_cli_vs_json( cli: &[&str], openapi: &str, equal: bool, ) -> (VmConfig, VmConfig) { let cli_vm_config = get_vm_config_from_vec(cli); let openapi_vm_config: VmConfig = serde_json::from_str(openapi).unwrap(); if equal { assert_eq!(cli_vm_config, openapi_vm_config); } else { assert_ne!(cli_vm_config, openapi_vm_config); } (cli_vm_config, openapi_vm_config) } #[test] fn test_valid_vm_config_default() { let cli = vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"]; let openapi = r#"{ "payload": {"kernel": "/path/to/kernel"} }"#; // First we check we get identical VmConfig structures. let (result_vm_config, _) = compare_vm_config_cli_vs_json(&cli, openapi, true); // As a second step, we validate all the default values. let expected_vm_config = VmConfig { cpus: CpusConfig { boot_vcpus: 1, max_vcpus: 1, topology: None, kvm_hyperv: false, max_phys_bits: 46, affinity: None, features: CpuFeatures::default(), nested: true, core_scheduling: CoreScheduling::Vm, profile: Default::default(), }, memory: MemoryConfig { size: 536_870_912, mergeable: false, hotplug_method: HotplugMethod::Acpi, hotplug_size: None, hotplugged_size: None, shared: false, hugepages: false, hugepage_size: None, prefault: false, reserve: false, zones: None, thp: true, }, payload: Some(PayloadConfig { kernel: Some(PathBuf::from("/path/to/kernel")), firmware: None, cmdline: None, initramfs: None, #[cfg(feature = "igvm")] 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"), pci_common: PciDeviceCommonConfig::default(), }, balloon: None, fs: None, generic_vhost_user: None, pmem: None, serial: SerialConfig { common: CommonConsoleConfig { file: None, mode: ConsoleOutputMode::Null, socket: None, }, }, console: ConsoleConfig { common: CommonConsoleConfig { file: None, mode: ConsoleOutputMode::Tty, socket: None, }, pci_common: PciDeviceCommonConfig::default(), }, #[cfg(target_arch = "x86_64")] debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, vsock: None, pvpanic: false, #[cfg(feature = "pvmemcontrol")] pvmemcontrol: None, iommu: false, numa: None, watchdog: false, rtc: None, #[cfg(feature = "guest_debug")] gdb: false, pci_segments: None, platform: None, tpm: None, preserved_fds: None, landlock_enable: false, landlock_rules: None, #[cfg(feature = "ivshmem")] ivshmem: None, }; assert_eq!(expected_vm_config, result_vm_config); } #[test] fn test_valid_vm_config_cpus() { [ ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--cpus", "boot=1", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "cpus": {"boot_vcpus": 1, "max_vcpus": 1} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--cpus", "boot=1,max=3", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "cpus": {"boot_vcpus": 1, "max_vcpus": 3} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--cpus", "boot=2,max=4", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "cpus": {"boot_vcpus": 1, "max_vcpus": 3} }"#, false, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_memory() { [( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1073741824"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory": {"size": 1073741824} }"#, true, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory": {"size": 1073741824} }"#, true, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=on"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory": {"size": 1073741824, "mergeable": true} }"#, true, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=off"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory": {"size": 1073741824, "mergeable": false} }"#, true, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,mergeable=on"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory": {"size": 1073741824, "mergeable": false} }"#, false, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,hotplug_size=1G"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory": {"size": 1073741824, "hotplug_method": "Acpi", "hotplug_size": 1073741824} }"#, true, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "size=1G,hotplug_method=virtio-mem,hotplug_size=1G"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "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); }); } #[test] fn test_valid_vm_config_kernel() { [( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], r#"{ "payload": {"kernel": "/path/to/kernel"} }"#, true, )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_cmdline() { [( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--cmdline", "arg1=foo arg2=bar", ], r#"{ "payload": {"kernel": "/path/to/kernel", "cmdline": "arg1=foo arg2=bar"} }"#, true, )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_disks() { [ ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--disk", "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", "image_type": "Raw"}, {"path": "/path/to/disk/2", "image_type": "Unknown"} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--disk", "path=/path/to/disk/1,image_type=raw", "path=/path/to/disk/2,image_type=qcow2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "disks": [ {"path": "/path/to/disk/1"} ] }"#, false, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--disk", "vhost_user=true,socket=/tmp/sock1", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "disks": [ {"vhost_user":true, "vhost_socket":"/tmp/sock1"} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--disk", "vhost_user=true,socket=/tmp/sock1", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "disks": [ {"vhost_user":true, "vhost_socket":"/tmp/sock1"} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--disk", "path=/path/to/disk/1,rate_limit_group=group0", "path=/path/to/disk/2,rate_limit_group=group0", "--rate-limit-group", "id=group0,bw_size=1000,bw_refill_time=100", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "disks": [ {"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}}} ] }"#, true, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_net() { [ // 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. ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--net", "mac="], r#"{ "payload": {"kernel": "/path/to/kernel"}, "net": [] }"#, false, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "net": [ {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd"} ] }"#, 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", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "net": [ {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0"} ] }"#, 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,mask=5.6.7.8", ], 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", "mask": "5.6.7.8"} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--cpus", "boot=2", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=4", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "cpus": {"boot_vcpus": 2, "max_vcpus": 2}, "net": [ {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 4} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--cpus", "boot=2", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,tap=tap0,ip=1.2.3.4,mask=5.6.7.8,num_queues=4,queue_size=128", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "cpus": {"boot_vcpus": 2, "max_vcpus": 2}, "net": [ {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "tap": "tap0", "ip": "1.2.3.4", "mask": "5.6.7.8", "num_queues": 4, "queue_size": 128} ] }"#, 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,mask=5.6.7.8,num_queues=2,queue_size=256", ], 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", "mask": "5.6.7.8"} ] }"#, 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,mask=5.6.7.8", ], 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", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256} ] }"#, true, ), #[cfg(target_arch = "x86_64")] ( 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,mask=5.6.7.8,num_queues=2,queue_size=256,iommu=on", ], 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", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256, "iommu": true} ] }"#, false, ), #[cfg(target_arch = "x86_64")] ( 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,mask=5.6.7.8,num_queues=2,queue_size=256,iommu=on", ], 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", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256, "iommu": true} ], "iommu": true }"#, 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,mask=5.6.7.8,num_queues=2,queue_size=256,iommu=off", ], 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", "mask": "5.6.7.8", "num_queues": 2, "queue_size": 256, "iommu": false} ] }"#, true, ), ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--net", "mac=12:34:56:78:90:ab,host_mac=34:56:78:90:ab:cd,vhost_user=true,socket=/tmp/sock"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "net": [ {"mac": "12:34:56:78:90:ab", "host_mac": "34:56:78:90:ab:cd", "vhost_user": true, "vhost_socket": "/tmp/sock"} ] }"#, true, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_rng() { [( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--rng", "src=/path/to/entropy/source", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "rng": {"src": "/path/to/entropy/source"} }"#, true, )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_fs() { [( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--fs", "tag=virtiofs1,socket=/path/to/sock1", "tag=virtiofs2,socket=/path/to/sock2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "fs": [ {"tag": "virtiofs1", "socket": "/path/to/sock1"}, {"tag": "virtiofs2", "socket": "/path/to/sock2"} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--fs", "tag=virtiofs1,socket=/path/to/sock1", "tag=virtiofs2,socket=/path/to/sock2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "fs": [ {"tag": "virtiofs1", "socket": "/path/to/sock1"} ] }"#, false, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--cpus", "boot=4", "--fs", "tag=virtiofs1,socket=/path/to/sock1,num_queues=4", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "cpus": {"boot_vcpus": 4, "max_vcpus": 4}, "fs": [ {"tag": "virtiofs1", "socket": "/path/to/sock1", "num_queues": 4} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--memory", "shared=true", "--cpus", "boot=4", "--fs", "tag=virtiofs1,socket=/path/to/sock1,num_queues=4,queue_size=128" ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "memory" : { "shared": true, "size": 536870912 }, "cpus": {"boot_vcpus": 4, "max_vcpus": 4}, "fs": [ {"tag": "virtiofs1", "socket": "/path/to/sock1", "num_queues": 4, "queue_size": 128} ] }"#, true, )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_pmem() { [ ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--pmem", "file=/path/to/img/1,size=1G", "file=/path/to/img/2,size=2G", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "pmem": [ {"file": "/path/to/img/1", "size": 1073741824}, {"file": "/path/to/img/2", "size": 2147483648} ] }"#, true, ), #[cfg(target_arch = "x86_64")] ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--pmem", "file=/path/to/img/1,size=1G,iommu=on", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "pmem": [ {"file": "/path/to/img/1", "size": 1073741824, "iommu": true} ], "iommu": true }"#, true, ), #[cfg(target_arch = "x86_64")] ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--pmem", "file=/path/to/img/1,size=1G,iommu=on", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "pmem": [ {"file": "/path/to/img/1", "size": 1073741824, "iommu": true} ] }"#, false, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[cfg(target_arch = "x86_64")] #[test] fn test_valid_vm_config_debug_console() { [( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--debug-console", "tty,iobase=0xe9", ], // 233 == 0xe9 r#"{ "payload": {"kernel": "/path/to/kernel" }, "debug_console": {"mode": "Tty", "iobase": 233 } }"#, true, )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_serial_console() { [ ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "serial": {"mode": "Null"}, "console": {"mode": "Tty"} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--serial", "null", "--console", "tty,pci_segment=0,pci_device_id=7", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "serial": {"mode": "Null"}, "console": {"mode": "Tty", "iommu": false, "pci_segment": 0, "pci_device_id": 7} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--serial", "tty", "--console", "off", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "serial": {"mode": "Tty"}, "console": {"mode": "Off"} }"#, true, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_serial_pty_console_pty() { [ ( vec!["cloud-hypervisor", "--kernel", "/path/to/kernel"], r#"{ "payload": {"kernel": "/path/to/kernel"}, "serial": {"mode": "Null"}, "console": {"mode": "Tty"} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--serial", "null", "--console", "tty", ], r#"{ "payload": {"kernel": "/path/to/kernel"} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--serial", "pty", "--console", "pty", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "serial": {"mode": "Pty"}, "console": {"mode": "Pty"} }"#, true, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] #[cfg(target_arch = "x86_64")] fn test_valid_vm_config_devices() { [ ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--device", "path=/path/to/device/1", "path=/path/to/device/2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "devices": [ {"path": "/path/to/device/1"}, {"path": "/path/to/device/2"} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--device", "path=/path/to/device/1", "path=/path/to/device/2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "devices": [ {"path": "/path/to/device/1"} ] }"#, false, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--device", "path=/path/to/device,iommu=on", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "devices": [ {"path": "/path/to/device", "iommu": true} ], "iommu": true }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--device", "path=/path/to/device,iommu=on", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "devices": [ {"path": "/path/to/device", "iommu": true} ] }"#, false, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--device", "path=/path/to/device,iommu=off", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "devices": [ {"path": "/path/to/device", "iommu": false} ] }"#, true, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_vdpa() { [ ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vdpa", "path=/path/to/device/1", "path=/path/to/device/2,num_queues=2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vdpa": [ {"path": "/path/to/device/1", "num_queues": 1}, {"path": "/path/to/device/2", "num_queues": 2} ] }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vdpa", "path=/path/to/device/1", "path=/path/to/device/2", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vdpa": [ {"path": "/path/to/device/1"} ] }"#, false, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_vsock() { [ ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vsock", "cid=123,socket=/path/to/sock/1", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vsock": {"cid": 123, "socket": "/path/to/sock/1"} }"#, true, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vsock", "cid=124,socket=/path/to/sock/1", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vsock": {"cid": 123, "socket": "/path/to/sock/1"} }"#, false, ), #[cfg(target_arch = "x86_64")] ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vsock", "cid=123,socket=/path/to/sock/1,iommu=on", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": true}, "iommu": true }"#, true, ), #[cfg(target_arch = "x86_64")] ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vsock", "cid=123,socket=/path/to/sock/1,iommu=on", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": true} }"#, false, ), ( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--vsock", "cid=123,socket=/path/to/sock/1,iommu=off", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "vsock": {"cid": 123, "socket": "/path/to/sock/1", "iommu": false} }"#, true, ), ] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } #[test] fn test_valid_vm_config_tpm_socket() { [( vec![ "cloud-hypervisor", "--kernel", "/path/to/kernel", "--tpm", "socket=/path/to/tpm/sock", ], r#"{ "payload": {"kernel": "/path/to/kernel"}, "tpm": {"socket": "/path/to/tpm/sock"} }"#, true, )] .iter() .for_each(|(cli, openapi, equal)| { compare_vm_config_cli_vs_json(cli, openapi, *equal); }); } // TODO the check for the option list being sorted could be moved into the // getter itself, when the getter becomes a const function. This however // needs more support by Rust (as of March 2025). #[test] fn test_cli_options_sorted() { 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()); } } cloud-hypervisor-53.0/cloud-hypervisor/src/test_util.rs000066400000000000000000000011601522476750100234550ustar00rootroot00000000000000// Copyright © 2025 Cyberus Technology GmbH // // SPDX-License-Identifier: Apache-2.0 // //! Test utilities. use std::cmp::Ordering; use clap::Arg; /// Ensures that all [`Arg`]s are sorted alphabetically. pub fn assert_args_sorted<'a, F: Fn() -> R, R: Iterator>(get_base_iter: F) { let iter = get_base_iter().zip(get_base_iter().skip(1)); for (arg, next) in iter { assert_ne!( arg.get_id().cmp(next.get_id()), Ordering::Greater, "args not alphabetically sorted: arg={}, next={}", arg.get_id(), next.get_id() ); } } cloud-hypervisor-53.0/cloud-hypervisor/tests/000077500000000000000000000000001522476750100214505ustar00rootroot00000000000000cloud-hypervisor-53.0/cloud-hypervisor/tests/common/000077500000000000000000000000001522476750100227405ustar00rootroot00000000000000cloud-hypervisor-53.0/cloud-hypervisor/tests/common/mod.rs000066400000000000000000000002451522476750100240660ustar00rootroot00000000000000// Copyright 2025 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 pub(crate) mod tests_wrappers; pub(crate) mod utils; cloud-hypervisor-53.0/cloud-hypervisor/tests/common/tests_wrappers.rs000066400000000000000000003333361522476750100264060ustar00rootroot00000000000000// 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::net::{IpAddr, Ipv4Addr}; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::string::String; use std::sync::mpsc; use std::time::Duration; use std::{panic, thread}; 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"); 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 = 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"); fs::write(&temp_config_path, request_body).unwrap(); let create_config = temp_config_path.as_os_str().to_str().unwrap(); let r = 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"); fs::write(&temp_config_path, request_body).unwrap(); let create_config = temp_config_path.as_os_str().to_str().unwrap(); let r = 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"); 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 = 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(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(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 = fs::OpenOptions::new() .write(true) .read(true) .open(pty_path) .unwrap(); // Read concurrently so we drain the replayed backlog instead of leaving // it stranded behind a non-reading client (which would back-pressure the // sender and never let the login keystrokes through). let ptyc = pty_read(cf.try_clone().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(Duration::new(5, 0)); assert_eq!(cf.write(b"cloud\n").unwrap(), 6); thread::sleep(Duration::new(2, 0)); assert_eq!(cf.write(b"cloud123\n").unwrap(), 9); thread::sleep(Duration::new(2, 0)); assert_eq!(cf.write(b"echo test_pty_console\n").unwrap(), 22); thread::sleep(Duration::new(2, 0)); let mut prev = String::new(); // The console can stream continuously (e.g. journald forwarded to it), so // bound the wait: a missing marker must not loop until the harness timeout. for _ in 0..20 { thread::sleep(Duration::new(2, 0)); // Drain everything available this round so a large replayed backlog // does not take one 2s tick per chunk to get through. loop { match ptyc.try_recv() { Ok(line) => { prev = prev + &line; if prev.contains("test_pty_console") { return; } } Err(mpsc::TryRecvError::Empty) => break, Err(_) => panic!("No login on pty"), } } } // Bounded out without ever seeing the marker: the login never completed. 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 = 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 = 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(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 = 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: Child; let mut child: 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 = 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) -> (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 = 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 = 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) -> (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],device_type=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 = 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 _ = fs::remove_file(&virtiofsd_socket_path); let (daemon_child, virtiofsd_socket_path) = prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); let r = 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],device_type=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(); 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 = 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 = 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(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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 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 = 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 { 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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(Duration::from_secs(20)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); let r = 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 = 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); guest.validate_memory(None); }); 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 = 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 max_overhead = if on_kvm_sev_snp() { MAXIMUM_VMM_OVERHEAD_KB_SEV_SNP } else { MAXIMUM_VMM_OVERHEAD_KB }; let r = panic::catch_unwind(|| { let overhead = get_vmm_overhead(child.id(), guest_memory_size_kb); eprintln!("Guest memory overhead: {overhead} vs {max_overhead}"); assert!(overhead <= max_overhead); }); 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 = 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 = 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 = 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 = 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 = 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 = 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(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 = 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(IpAddr::V4( 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 = 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 = 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 = 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); } cloud-hypervisor-53.0/cloud-hypervisor/tests/common/utils.rs000066400000000000000000001232501522476750100244510ustar00rootroot00000000000000// 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, Output}; use std::string::String; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::time::{Duration, Instant}; use std::{cmp, fs, io, panic, thread}; use block::formats::qcow::ImageType as QcowImageType; 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; // The KVM SEV-SNP build (igvm+sev_snp+fw_cfg) has a larger size pub(crate) const MAXIMUM_VMM_OVERHEAD_KB_SEV_SNP: u32 = 12 * 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) -> (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, ) -> (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(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()); 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, ) -> (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) -> (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(), ); 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(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 = 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 = 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) -> (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: fs::File) -> Receiver { let (tx, rx) = mpsc::channel::(); thread::spawn(move || { loop { let mut buf = [0; 4096]; match pty.read(&mut buf) { Ok(0) => break, Ok(bytes) => { let output = String::from_utf8_lossy(&buf[..bytes]).into_owned(); if tx.send(output).is_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<(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 Path::new(backing_file).is_absolute() { PathBuf::from(backing_file) } else { path.parent() .unwrap_or_else(|| 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<(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(QcowImageType::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: &Path, args: &[&str], trailing_args: Option<&[&str]>) -> Output { let mut cmd = 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: &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) -> 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 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!("receiver_url=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(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(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(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}") } cloud-hypervisor-53.0/cloud-hypervisor/tests/integration.rs000066400000000000000000016443011522476750100243520ustar00rootroot00000000000000// Copyright © 2020 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // #![cfg(any(devcli_testenv, clippy))] #![expect(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)] 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::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::string::String; use std::sync::Mutex; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{fs, panic, thread}; use block::ImageType; use test_infra::*; use vmm_sys_util::tempdir::TempDir; use vmm_sys_util::tempfile::TempFile; use wait_timeout::ChildExt; mod common; use common::tests_wrappers::*; use common::utils::*; 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)) }}; } mod common_parallel { use std::cell::Cell; use std::io::{self, SeekFrom}; #[cfg(not(feature = "mshv"))] use std::num::NonZeroU32; use std::process::Command; #[cfg(not(feature = "mshv"))] use std::sync::mpsc; use test_infra::GuestFactory; #[cfg(not(feature = "mshv"))] use vmm::api::TimeoutStrategy; use crate::*; #[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); } #[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); } #[test] fn test_multi_cpu() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_multi_cpu(&guest); } #[test] #[cfg_attr(target_arch = "x86_64", should_panic)] 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); } #[test] fn test_cpu_topology_262() { test_cpu_topology(2, 6, 2, false); } #[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(); let r = panic::catch_unwind(|| { 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, ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); let expected = if nested { "yes" } else { "no" }; assert_eq!( guest .ssh_command("test -c /dev/kvm && echo yes || echo no") .unwrap() .trim(), expected ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] #[cfg(target_arch = "x86_64")] fn test_nested_virtualization_on() { _test_nested_virtualization(true); } #[test] #[cfg(target_arch = "x86_64")] fn test_nested_virtualization_off() { _test_nested_virtualization(false); } #[test] fn test_cpu_affinity() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); _test_cpu_affinity(&guest); } #[test] fn test_virtio_queue_affinity() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); _test_virtio_queue_affinity(&guest); } #[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(); let mut child = cmd.spawn().unwrap(); guest.wait_vm_boot().unwrap(); let r = 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" ); assert!(guest.get_total_memory().unwrap_or_default() > 5_000_000); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[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(); let mut child = cmd.spawn().unwrap(); guest.wait_vm_boot().unwrap(); let r = panic::catch_unwind(|| { assert!(guest.get_total_memory().unwrap_or_default() > 128_000_000); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_power_button() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_power_button(&guest); } #[test] 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); let kernel_path = direct_kernel_boot_path(); 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); guest.enable_memory_hotplug(); 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)); guest.reboot_linux(0); // 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); // 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)); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_guest_numa_nodes() { _test_guest_numa_nodes(false); } #[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)); // 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.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(); let mut child = cmd.spawn().unwrap(); guest.wait_vm_boot().unwrap(); let r = 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!( guest .ssh_command("ls /sys/kernel/iommu_groups/*/devices") .unwrap() .contains("0001:00:01.0") ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_pci_msi() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_pci_msi(&guest); } #[test] fn test_virtio_net_ctrl_queue() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_virtio_net_ctrl_queue(&guest); } #[test] fn test_pci_multiple_segments() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_pci_multiple_segments(&guest, MAX_NUM_PCI_SEGMENTS, 15u16); } #[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(); // 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; 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(); let cmd = "cat /sys/block/vdc/device/../numa_node"; let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!( guest .ssh_command(cmd) .unwrap() .trim() .parse::() .unwrap_or_default(), TEST_DISK_NODE ); // 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]); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_direct_kernel_boot() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_direct_kernel_boot(&guest); } #[test] #[cfg(target_arch = "x86_64")] fn test_direct_kernel_boot_bzimage() { let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); 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 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 r = 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); let grep_cmd = "grep -c PCI-MSI /proc/interrupts"; 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); } #[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); } #[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); } #[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); } // RAII wrapper around a `losetup`'d loop device. The backing file lives // in the guest's tmp_dir so it is cleaned up automatically when the // guest is dropped. struct LoopDev { path: String, } impl LoopDev { fn new(tmp: &Path, size_mb: u64) -> Self { let backing = tmp.join("blkdev.img"); let backing_str = backing.to_str().unwrap(); assert!( exec_host_command_status(&format!("truncate -s {size_mb}M {backing_str}")) .success(), "truncate failed" ); let out = exec_host_command_output(&format!("losetup -f --show -- {backing_str}")); assert!(out.status.success(), "losetup failed: {out:?}"); let path = String::from_utf8(out.stdout).unwrap().trim().to_string(); Self { path } } } impl Drop for LoopDev { fn drop(&mut self) { let _ = exec_host_command_status(&format!("losetup -d {}", self.path)); } } // Exercise the new BLKDISCARD / BLKZEROOUT ioctl paths in `block/` by // attaching a real block device (a loop device) as a writable virtio-blk // disk and issuing `blkdiscard` / `fstrim` from inside the guest. // // `is_block_device()` returns true for `/dev/loopN`, so cloud-hypervisor now // routes punch_hole / write_zeroes through the BLK* ioctls regardless of // whether `fallocate()` would have worked. This test therefore covers: // - is_block_device() detection at backend construction, // - blkdiscard() / blkzeroout() helpers in block::sparse, // - the io_uring `submit_nop()` completion plumbing (io_uring case), // - the BLKDISCARD / BLKZEROOUT additions to the VirtioBlock seccomp // whitelist (any of these would otherwise SIGSYS the device thread). fn _test_virtio_block_blkdev(disable_io_uring: bool, disable_aio: bool) { let focal = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(focal)); let loopdev = LoopDev::new(guest.tmp_dir.as_path(), 64); let kernel_path = direct_kernel_boot_path(); let mut cloud_child = GuestCommand::new(&guest) .args(["--cpus", "boot=2"]) .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::OperatingSystem).unwrap() ) .as_str(), format!( "path={}", guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), format!( "path={},image_type=raw,num_queues=2,\ _disable_io_uring={},_disable_aio={}", loopdev.path, disable_io_uring, disable_aio, ) .as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // The loop device shows up as vdc. assert_eq!( guest .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); // 1) BLKDISCARD path: fill the first 16 MiB with random data, // then discard the first 8 MiB. We only assert that the // ioctl path succeeds end-to-end; whether the discarded // range reads back as zeros is not guaranteed by the // BLKDISCARD contract (see BLKZEROOUT below for the // read-as-zero assertion). guest .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=16 conv=fsync") .expect("initial write failed"); guest .ssh_command("sudo blkdiscard -o 0 -l 8388608 /dev/vdc") .expect("blkdiscard (BLKDISCARD ioctl path) failed"); // 2) BLKZEROOUT path: same idea, but via `blkdiscard -z` which // issues BLKZEROOUT instead of BLKDISCARD. guest .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=16 conv=fsync") .expect("second write failed"); guest .ssh_command("sudo blkdiscard -z -o 0 -l 8388608 /dev/vdc") .expect("blkdiscard -z (BLKZEROOUT ioctl path) failed"); let nonzero = guest .ssh_command( "sudo dd if=/dev/vdc bs=1M count=8 status=none | \ tr -d '\\000' | wc -c", ) .unwrap(); assert_eq!( nonzero.trim(), "0", "BLKZEROOUT did not zero the requested range" ); // 3) End-to-end fstrim through a real filesystem on the block // device. This is the closest CI-friendly approximation of // the original ZFS-zvol scenario in #8127. guest .ssh_command("sudo mkfs.ext4 -F /dev/vdc") .expect("mkfs.ext4 failed"); guest .ssh_command( "sudo mkdir -p /mnt/blkdev && \ sudo mount -o discard /dev/vdc /mnt/blkdev", ) .expect("mount -o discard failed"); guest .ssh_command( "sudo dd if=/dev/urandom of=/mnt/blkdev/f bs=1M count=16 \ conv=fsync && sudo rm /mnt/blkdev/f && sync", ) .expect("populate-then-delete failed"); // `fstrim -v` prints e.g. "/mnt/blkdev: 12 MiB (12582912 bytes) // trimmed on /dev/vdc". Extract the byte count and assert that // it is non-zero: `fstrim` exits 0 even when nothing is trimmed // (e.g. if virtio-blk DISCARD was not negotiated), which would // silently mask exactly the regression this test exists to // catch. let fstrim_out = guest .ssh_command("sudo fstrim -v /mnt/blkdev") .expect("fstrim invocation failed"); let trimmed_bytes: u64 = fstrim_out .split_once('(') .and_then(|(_, rest)| rest.split_once(" bytes")) .and_then(|(n, _)| n.trim().parse().ok()) .unwrap_or_else(|| panic!("could not parse fstrim output: {fstrim_out:?}")); assert!( trimmed_bytes > 0, "fstrim trimmed 0 bytes -- virtio-blk DISCARD likely \ not advertised or BLK* path not wired up: {fstrim_out:?}" ); guest .ssh_command("sudo umount /mnt/blkdev") .expect("umount failed"); }); let _ = cloud_child.kill(); let output = cloud_child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_virtio_block_blkdev_io_uring() { _test_virtio_block_blkdev(false, false); } #[test] fn test_virtio_block_blkdev_aio() { _test_virtio_block_blkdev(true, false); } #[test] fn test_virtio_block_blkdev_sync() { _test_virtio_block_blkdev(true, true); } #[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); } #[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); } #[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); } #[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); } #[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); // Verify only 16MB was read let position = reader.position(); assert_eq!(position, 16 * 1024 * 1024); } #[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); } #[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); } #[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); } #[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); } #[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); } #[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); } /// 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, } /// 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) + 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(); 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) } }; 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); test_fn(&guest); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); disk_check_consistency( guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), None, ); disk_check_consistency(&test_image_path, initial_backing_checksum); } #[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" ); 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 .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"); assert_eq!( guest .ssh_command("ls /mnt/test/file* | wc -l") .unwrap() .trim() .parse::() .unwrap_or_default(), 8, "Expected 8 files to be created" ); guest .ssh_command("sudo rm -f /mnt/test/file*") .expect("Failed to remove files"); // 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"); assert_eq!( guest .ssh_command("ls /mnt/test/file* | wc -l") .unwrap() .trim() .parse::() .unwrap_or_default(), 16, "Expected 16 files after second round" ); guest .ssh_command("sudo umount /mnt/test") .expect("Failed to unmount"); }); } #[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"); guest .ssh_command( "sudo dd if=/dev/urandom of=/mnt/test/readfile bs=1M count=64 conv=fsync", ) .expect("Failed to create initial file"); 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"); assert_eq!( guest .ssh_command("ls /mnt/test/writefile* | wc -l") .unwrap() .trim() .parse::() .unwrap_or_default(), 4, "Expected 4 write files" ); 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"); guest .ssh_command("sudo umount /mnt/test") .expect("Failed to unmount"); }); } #[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"); 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"); 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"); assert_eq!( guest .ssh_command("ls /mnt/test/new* | wc -l") .unwrap() .trim() .parse::() .unwrap_or_default(), 8, "Expected 8 new files" ); guest .ssh_command("sudo umount /mnt/test") .expect("Failed to unmount"); }); } #[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"); 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"); 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"); }); } #[test] fn test_virtio_block_qcow2_multiqueue_fsync() { 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 .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"); 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)" ); 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"); guest .ssh_command("sudo umount /mnt/test") .expect("Failed to unmount"); }); } #[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 .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"); 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}" ); 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"); 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}" ); 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"); 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"); guest .ssh_command("sync && sudo umount /mnt/test") .expect("Failed to unmount"); }); } #[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"); // 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"); 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"); assert_eq!( guest .ssh_command("ls /mnt/test/file* | wc -l") .unwrap() .trim() .parse::() .unwrap_or_default(), 4, "Expected 4 files to be created" ); guest .ssh_command("sudo rm -f /mnt/test/file*") .expect("Failed to remove files"); guest .ssh_command("sudo fstrim -v /mnt/test") .expect("fstrim failed - DISCARD not working"); guest .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"); assert_eq!( guest .ssh_command("ls /mnt/test/file* | wc -l") .unwrap() .trim() .parse::() .unwrap_or_default(), 8, "Expected 8 files after second round" ); guest .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( "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"); // Write known patterns to the same sparse locations guest .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"); // Even more aggressive sparse writes with smaller chunks but more of them guest .ssh_command( "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", ) .expect("Failed third sparse write pattern"); guest .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M count=64") .expect("Failed to read back data after sparse writes"); }); } #[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"); // 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"); 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"); guest .ssh_command("wait") .expect("Failed to wait for background writes"); assert_eq!( 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" ); // 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"); // 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"); guest .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M count=128") .expect("Failed to read back data after discard stress"); }); } #[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(); 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 4G {}", 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 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" ); 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 r = panic::catch_unwind(|| { guest.wait_vm_boot_custom_timeout(180).unwrap(); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap())); let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}")); handle_child_output(r, &output); } // Direct=on data disk on a 4k sector loop FS, aligned O_DIRECT round trip. fn _test_virtio_block_direct_io_data_disk_4k(image_type: ImageType) { let (qemu_fmt, qemu_extra): (&str, &[&str]) = match image_type { ImageType::Raw => ("raw", &[]), ImageType::Qcow2 => ("qcow2", &[]), ImageType::FixedVhd => ("vpc", &["-o", "subformat=fixed"]), ImageType::Vhdx => ("vhdx", &[]), ImageType::Unknown => panic!("unsupported image_type {image_type}"), }; let image_type_str = image_type.to_string(); 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 loop backing file needs a real O_DIRECT capable FS, not tmpfs. 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 1G {}", 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 = mnt_dir.join(format!("data.{image_type_str}")); let mut create_args: Vec<&str> = vec!["create", "-f", qemu_fmt]; create_args.extend_from_slice(qemu_extra); let res = run_qemu_img(&test_disk, &create_args, Some(&["64M"])); assert!(res.status.success(), "qemu-img create failed: {res:?}"); let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=4"]) .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::OperatingSystem).unwrap() ) .as_str(), format!( "path={}", guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), format!( "path={},direct=on,image_type={image_type_str}", test_disk.to_str().unwrap() ) .as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); // qcow2 and vhdx report 512 LBS from the on disk format. Raw and // fixed VHD pass through the host 4096. let expected_log_sec: u32 = match image_type { ImageType::Qcow2 | ImageType::Vhdx => 512, _ => 4096, }; let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); let log_sec: u32 = guest .ssh_command("lsblk -t | grep vdc | awk '{print $6}'") .unwrap() .trim() .parse() .unwrap_or_default(); assert_eq!(log_sec, expected_log_sec, "unexpected logical sector size"); 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 conv=fsync && \ sudo dd if=/dev/vdc of=/tmp/readback bs=4096 count=8 skip=1 \ iflag=direct && \ cmp /tmp/pattern /tmp/readback", ) .expect("aligned 4k direct IO round trip failed"); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); let _ = exec_host_command_output(&format!("umount {}", mnt_dir.to_str().unwrap())); let _ = exec_host_command_output(&format!("losetup -d {loop_dev_path}")); handle_child_output(r, &output); } #[test] fn test_virtio_block_direct_io_data_disk_4k_raw() { _test_virtio_block_direct_io_data_disk_4k(ImageType::Raw); } #[test] fn test_virtio_block_direct_io_data_disk_4k_qcow2() { _test_virtio_block_direct_io_data_disk_4k(ImageType::Qcow2); } #[test] fn test_virtio_block_direct_io_data_disk_4k_vhd() { _test_virtio_block_direct_io_data_disk_4k(ImageType::FixedVhd); } #[test] fn test_virtio_block_direct_io_data_disk_4k_vhdx() { _test_virtio_block_direct_io_data_disk_4k(ImageType::Vhdx); } #[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 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"); assert_eq!( check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), Some(false), "Image should start with dirty bit cleared" ); 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 = 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; } // Simulate unclean shutdown with SIGKILL let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) }; let _ = child.wait(); assert_eq!( check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), Some(true), "Dirty bit should remain set after unclean shutdown" ); } #[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"); assert_eq!( check_dirty_flag(&test_image_path).expect("Failed to check dirty flag"), Some(false), "Image should start with dirty bit cleared" ); 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 = 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" ); }); // Clean shutdown using SIGTERM kill_child(&mut child); if r.is_err() { let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); return; } let _ = child.wait(); disk_check_consistency(&test_image_path, None); } #[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(); let test_image_path = guest.tmp_dir.as_path().join("test-corrupt.qcow2"); let original_image = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); copy(original_image, &test_image_path).expect("Failed to copy qcow2 image"); assert_eq!( check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), Some(false), "Image should start with corrupt bit cleared" ); set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag"); assert_eq!( check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), Some(true), "Corrupt bit should be set" ); 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}" ); } #[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(); 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"); set_corrupt_flag(&test_image_path, true).expect("Failed to set corrupt flag"); assert_eq!( check_corrupt_flag(&test_image_path).expect("Failed to check corrupt flag"), Some(true), "Corrupt bit should be set" ); 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(); thread::sleep(Duration::from_secs(5)); 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}"); } } let _ = unsafe { libc::kill(child.id() as i32, libc::SIGKILL) }; let output = child.wait_with_output().unwrap(); 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}" ); 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" ); } #[test] fn test_virtio_block_vhd() { let mut workload_path = dirs::home_dir().unwrap(); workload_path.push("workloads"); 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); // Generate VHD file from RAW file 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); } #[test] fn test_virtio_block_vhdx() { let mut workload_path = dirs::home_dir().unwrap(); workload_path.push("workloads"); 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); // Generate dynamic VHDX file from RAW file 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); } #[test] fn test_virtio_block_dynamic_vhdx_expand() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_virtio_block_dynamic_vhdx_expand(&guest); } #[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)); // 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"); 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot_custom_timeout(180).unwrap(); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[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); } #[test] fn test_vhost_user_net_tap_multiple_queues() { test_vhost_user_net( Some("vunet-tap1"), 4, &prepare_vhost_user_net_daemon, false, false, ); } #[test] fn test_vhost_user_net_host_mac() { test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, true, false); } #[test] fn test_vhost_user_net_client_mode() { test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, true); } #[test] #[cfg(not(target_arch = "aarch64"))] fn test_vhost_user_blk_default() { test_vhost_user_blk(2, false, false, Some(&prepare_vubd)); } #[test] #[cfg(not(target_arch = "aarch64"))] fn test_vhost_user_blk_readonly() { test_vhost_user_blk(1, true, false, Some(&prepare_vubd)); } #[test] #[cfg(not(target_arch = "aarch64"))] fn test_vhost_user_blk_direct() { test_vhost_user_blk(1, false, true, Some(&prepare_vubd)); } #[test] fn test_boot_from_vhost_user_blk_default() { test_boot_from_vhost_user_blk(1, false, false, Some(&prepare_vubd)); } #[test] #[cfg(target_arch = "x86_64")] fn test_split_irqchip() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_split_irqchip(&guest); } #[test] #[cfg(target_arch = "x86_64")] fn test_dmi_serial_number() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_dmi_serial_number(&guest); } #[test] #[cfg(target_arch = "x86_64")] fn test_dmi_uuid() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_dmi_uuid(&guest); } #[test] #[cfg(target_arch = "x86_64")] fn test_dmi_oem_strings() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_dmi_oem_strings(&guest); } #[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); } #[test] fn test_virtio_fs() { _test_virtio_fs(&prepare_virtiofsd, false, false, None); } #[test] fn test_virtio_fs_hotplug() { _test_virtio_fs(&prepare_virtiofsd, true, false, None); } #[test] fn test_virtio_fs_multi_segment_hotplug() { _test_virtio_fs(&prepare_virtiofsd, true, false, Some(15)); } #[test] fn test_virtio_fs_multi_segment() { _test_virtio_fs(&prepare_virtiofsd, false, false, Some(15)); } #[test] fn test_generic_vhost_user() { _test_virtio_fs(&prepare_virtiofsd, false, true, None); } #[test] fn test_generic_vhost_user_hotplug() { _test_virtio_fs(&prepare_virtiofsd, true, true, None); } #[test] fn test_generic_vhost_user_multi_segment_hotplug() { _test_virtio_fs(&prepare_virtiofsd, true, true, Some(15)); } #[test] fn test_generic_vhost_user_multi_segment() { _test_virtio_fs(&prepare_virtiofsd, false, true, Some(15)); } #[test] fn test_virtio_rtc() { 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(); // virtio-rtc is disabled by default and needs to be explicitly enabled. let mut child = GuestCommand::new(&guest) .default_cpus() .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args(["--rtc"]) .default_disks() .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Fail fast if the virtio-rtc driver is not loaded. assert_eq!( guest .ssh_command( "test -d /sys/bus/virtio/drivers/virtio_rtc && echo ok || echo missing" ) .unwrap() .trim(), "ok" ); // Find the PTP device backed by virtio-rtc (clock_name starts with // "Virtio PTP"), which may coexist with KVM's "KVM virtual PTP". let ptp_dev = guest .ssh_command( "for d in /sys/class/ptp/ptp*; do \ if grep -q '^Virtio PTP' \"$d/clock_name\" 2>/dev/null; then \ basename \"$d\"; break; \ fi; \ done", ) .unwrap(); let ptp_dev = ptp_dev.trim(); assert!(ptp_dev.starts_with("ptp"), "No virtio-rtc PTP device found"); // Verify the device node exists assert_eq!( guest .ssh_command(&format!("ls /dev/{ptp_dev} 2>/dev/null || echo missing")) .unwrap() .trim(), format!("/dev/{ptp_dev}") ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_virtio_pmem_discard_writes() { test_virtio_pmem(true, false); } #[test] fn test_virtio_pmem_with_size() { test_virtio_pmem(true, true); } #[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) .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([ "--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 = panic::catch_unwind(|| { 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); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_multiple_network_interfaces() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_multiple_network_interfaces(&guest); } #[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]) .default_disks() .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Test that PMU exists. assert_eq!( guest .ssh_command(GREP_PMU_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); } #[test] fn test_serial_off() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_serial_off(&guest); } #[test] 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"; cmd.default_cpus() .default_memory() .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(); let mut child = cmd.spawn().unwrap(); let r = panic::catch_unwind(|| { 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 ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = panic::catch_unwind(|| { assert!(!String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); }); handle_child_output(r, &output); } #[test] fn test_serial_tty() { 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(); #[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) .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 = panic::catch_unwind(|| { 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(Duration::new(2, 0)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); }); handle_child_output(r, &output); } #[test] fn test_serial_file() { let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); 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) .default_cpus() .default_memory() .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", format!("file={}", serial_path.to_str().unwrap()).as_str(), ]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { 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 ); guest.ssh_command("sudo shutdown -h now").unwrap(); }); let _ = child.wait_timeout(Duration::from_secs(20)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = 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 = 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] 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", &cmdline]) .default_disks() .default_net() .args(["--serial", "null"]) .args(["--console", "pty"]) .args(["--api-socket", &api_socket]) .spawn() .unwrap(); let r = panic::catch_unwind(|| { 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(); }); let _ = child.wait_timeout(Duration::from_secs(20)); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = panic::catch_unwind(|| { // Check that the cloud-hypervisor binary actually terminated assert!(output.status.success()); }); handle_child_output(r, &output); } #[test] 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) .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 _ = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); }); let mut socat_command = Command::new("socat"); let socat_args = [ &format!("pty,link={},raw,echo=0", serial_socket_pty.display()), &format!("UNIX-CONNECT:{}", serial_socket.display()), ]; socat_command.args(socat_args); let mut socat_child = socat_command.spawn().unwrap(); thread::sleep(Duration::new(1, 0)); let _ = panic::catch_unwind(|| { _test_pty_interaction(serial_socket_pty); }); let _ = socat_child.kill(); let _ = socat_child.wait(); let r = panic::catch_unwind(|| { guest.ssh_command("sudo shutdown -h now").unwrap(); }); let _ = child.wait_timeout(Duration::from_secs(20)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = 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] fn test_virtio_console() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_virtio_console(&guest); } #[test] fn test_console_file() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_console_file(&guest); } #[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 disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new_from_ip_range(Box::new(disk_config), "172.18", 0); let mut workload_path = dirs::home_dir().unwrap(); workload_path.push("workloads"); let kernel_path = direct_kernel_boot_path(); let mut vfio_path = workload_path.clone(); vfio_path.push("vfio"); let mut cloud_init_vfio_base_path = vfio_path.clone(); cloud_init_vfio_base_path.push("cloudinit.img"); // 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"); let mut vfio_disk_path = workload_path.clone(); vfio_disk_path.push("vfio.img"); // 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 mut blk_file_path = workload_path; blk_file_path.push("blk.img"); let vfio_tap0 = "vfio-tap0"; let vfio_tap1 = "vfio-tap1"; let vfio_tap2 = "vfio-tap2"; let vfio_tap3 = "vfio-tap3"; 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!( "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", 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() .spawn() .unwrap(); guest.wait_for_ssh(Duration::from_secs(30)).unwrap(); let r = panic::catch_unwind(|| { 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(); let auth = PasswordAuth { username: String::from("cloud"), password: String::from("cloud123"), }; // 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 )); // 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 amount of PCI devices appearing in L2 VM. assert!(check_lines_count( guest .ssh_command_l2_1("ls /sys/bus/pci/devices") .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(), &["vdc", "16M"], 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(), &["{\"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'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 )); // 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(); 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(), 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(), 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_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); } #[test] fn test_virtio_vsock() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_virtio_vsock(&guest, false); } #[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); } #[test] fn test_api_http_shutdown() { let guest = basic_regular_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_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(4); let target_api = TargetApi::new_http_api(&guest.tmp_dir); _test_api_delete(&target_api, &guest); } #[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_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 kernel_path = direct_kernel_boot_path(); let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=2,max=4"]) .default_memory() .args(["--kernel", kernel_path.to_str().unwrap()]) .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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); 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); assert_eq!( 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); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[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 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(["--balloon", "size=0"]) .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().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); assert!(wait_until(Duration::from_secs(10), || { guest.get_total_memory().unwrap_or_default() > 960_000 })); // Use balloon to remove RAM from the VM let desired_balloon = 512 << 20; resize_command(&api_socket, None, None, Some(desired_balloon), 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 })); guest.reboot_linux(0); assert!(guest.get_total_memory().unwrap_or_default() < 960_000); // Use balloon add RAM to the VM let desired_balloon = 0; resize_command(&api_socket, None, None, Some(desired_balloon), None); assert!(wait_until(Duration::from_secs(10), || { guest.get_total_memory().unwrap_or_default() > 960_000 })); guest.enable_memory_hotplug(); // Add RAM to the VM let desired_ram = 2048 << 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() > 1_920_000 })); // Remove RAM to the VM (only applies after reboot) let desired_ram = 1024 << 20; resize_command(&api_socket, None, Some(desired_ram), None, None); guest.reboot_linux(1); assert!(guest.get_total_memory().unwrap_or_default() > 960_000); assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] 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 kernel_path = direct_kernel_boot_path(); 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().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); assert!(wait_until(Duration::from_secs(10), || { guest.get_total_memory().unwrap_or_default() > 960_000 })); // Add RAM to the VM let desired_ram = 2048 << 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() > 1_920_000 })); // Remove RAM from the VM let desired_ram = 1024 << 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 > 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=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 = panic::catch_unwind(|| { 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); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] 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] #[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_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] #[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] fn test_disk_resize() { 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); // 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() ); 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() .default_net() .capture_output(); let mut child = cmd.spawn().unwrap(); let r = panic::catch_unwind(|| { 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("lsblk | grep vdc | grep -c 16M") .unwrap() .trim() .parse::() .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("lsblk | grep vdc | grep -c 32M") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); // And check all blocks can be written to guest .ssh_command("sudo dd if=/dev/zero of=/dev/vdc bs=1M count=32") .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("lsblk | grep vdc | grep -c 16M") .unwrap() .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); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_disk_resize_qcow2() { 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 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]) .default_disks() .default_net() .capture_output(); let mut child = cmd.spawn().unwrap(); let r = panic::catch_unwind(|| { 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("lsblk | grep vdc | grep -c 16M") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); // 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(); // 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("lsblk | grep vdc | grep -c 32M") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); // 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("lsblk | grep vdc | grep -c 64M") .unwrap() .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); } 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"; #[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], } 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], } } } #[derive(Default)] #[repr(C)] struct LoopConfig { fd: u32, block_size: u32, info: LoopInfo64, _reserved: [u64; 8], } // 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_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_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::OperatingSystem).unwrap() ) .as_str(), format!( "path={}", guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), format!("path={loop_dev},direct=on,image_type=raw").as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); 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_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]) .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={},direct=on,image_type=raw", test_disk_path.to_str().unwrap() ) .as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); let log_sec: u32 = guest .ssh_command("lsblk -t | grep vdc | awk '{print $6}'") .unwrap() .trim() .parse() .unwrap_or_default(); assert_eq!( 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}")); } // 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 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})" ); } // 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" ); } } /// 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(); assert!( result.trim() == "ZEROS", "Expected {} region at offset {} length {} to read as zeros, but got: {}", device, offset, length, result.trim() ); } // 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=4"]) .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={},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(); 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 = Cell::new(0u64); let r = panic::catch_unwind(panic::AssertUnwindSafe(|| { guest.wait_vm_boot().unwrap(); assert_eq!( guest .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); // 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(); // 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}" ); } // Force sync to ensure async DISCARD operations complete guest.ssh_command("sync").unwrap(); // 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); } 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(); } 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 ); 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); if verify_disk { disk_check_consistency(&test_disk_path, None); } } #[test] fn test_virtio_block_discard_qcow2() { _test_virtio_block_discard("qcow2", "qcow2", &[], true, true); } #[test] fn test_virtio_block_discard_raw() { _test_virtio_block_discard("raw", "raw", &[], true, false); } #[test] fn test_virtio_block_discard_raw_aio() { _test_virtio_block_discard_with_backend("raw", "raw", &[], true, false, true); } #[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) .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={},image_type=raw", test_disk_path.to_str().unwrap()).as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = 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 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); } #[test] fn test_virtio_block_discard_unsupported_vhd() { _test_virtio_block_discard("vhd", "vpc", &["-o", "subformat=fixed"], false, false); } #[test] fn test_virtio_block_discard_unsupported_vhdx() { _test_virtio_block_discard("vhdx", "vhdx", &[], false, false); } #[test] 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 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", 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},image_type=raw").as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!( guest .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); assert_eq!( guest .ssh_command("lsblk -t | grep vdc | awk '{print $6}'") .unwrap() .trim() .parse::() .unwrap_or_default(), 4096 ); 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(); 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}" ); guest.ssh_command("sync").unwrap(); assert_guest_disk_region_is_zero(&guest, "/dev/vdc", 0, 4194304); }); 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_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 origin_path = guest.tmp_dir.as_path().join("dm_origin.raw"); let cow_path = guest.tmp_dir.as_path().join("dm_cow.raw"); 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 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 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 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 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) ); Command::new("dmsetup") .arg("mknodes") .output() .expect("dmsetup mknodes failed"); // 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) ); Command::new("dmsetup") .arg("mknodes") .output() .expect("dmsetup mknodes failed"); 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]) .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() .capture_output() .spawn() .unwrap(); let r = 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 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}"); let uptime = guest.ssh_command("uptime").unwrap(); assert!( !uptime.is_empty(), "Guest unresponsive after blkdiscard attempt {attempt}" ); } 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 _ = 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, ); } 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 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]) .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() .capture_output() .spawn() .unwrap(); let max_size_during_writes = Cell::new(0u64); let r = panic::catch_unwind(panic::AssertUnwindSafe(|| { guest.wait_vm_boot().unwrap(); assert_eq!( guest .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); guest.ssh_command("sudo mkfs.ext4 -F /dev/vdc").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(); // 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); } } handle_child_output(r, &output); if verify_disk { disk_check_consistency(&test_disk_path, None); } } #[test] fn test_virtio_block_fstrim_qcow2() { _test_virtio_block_fstrim("qcow2", "qcow2", &[], true, true); } #[test] fn test_virtio_block_fstrim_raw() { _test_virtio_block_fstrim("raw", "raw", &[], true, false); } #[test] fn test_virtio_block_fstrim_raw_aio() { _test_virtio_block_fstrim_with_backend("raw", "raw", &[], true, false, true); } #[test] fn test_virtio_block_fstrim_unsupported_vhd() { _test_virtio_block_fstrim("vhd", "vpc", &["-o", "subformat=fixed"], false, false); } #[test] fn test_virtio_block_fstrim_unsupported_vhdx() { _test_virtio_block_fstrim("vhdx", "vhdx", &[], false, false); } #[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 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.raw"); let test_disk_path = test_disk_path.to_str().unwrap(); 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 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) .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={test_disk_path},sparse=off").as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = 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([ "--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},sparse=off,num_queues=4").as_str(), ]) .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!( guest .ssh_command("lsblk | grep -c vdc") .unwrap() .trim() .parse::() .unwrap_or_default(), 1 ); // 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)" ); // WRITE_ZEROES should still work via blkdiscard --zeroout guest .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("sudo blkdiscard -z -o 1048576 -l 65536 /dev/vdc") .unwrap(); 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(); 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_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(["--api-socket", &api_socket]) .default_cpus() .args(["--memory", "size=4G"]) .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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // 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); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] 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)); //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(); let pid = child.id(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().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_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(); }); // 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); // 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(); handle_child_output(r, &output); } #[test] #[cfg_attr(target_arch = "aarch64", ignore = "See #8187")] fn test_pmem_hotplug() { _test_pmem_hotplug(None); } #[test] #[cfg_attr(target_arch = "aarch64", ignore = "See #8187")] fn test_pmem_multi_segment_hotplug() { _test_pmem_hotplug(Some(15)); } 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 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() .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 = panic::catch_unwind(|| { guest.wait_vm_boot().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( &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\"}") ); } // 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); // 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"))); // 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) })); 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); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] 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] 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)); } #[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"); #[cfg(target_arch = "x86_64")] let mut kernels = vec![direct_kernel_boot_path()]; #[cfg(target_arch = "aarch64")] let kernels = [direct_kernel_boot_path()]; #[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); } let mut initramfs_path = workload_path; initramfs_path.push("alpine_initramfs.img"); let test_string = String::from("axz34i9rylotd8n50wbv6kcj7f2qushme1pg"); let cmdline = format!("console=hvc0 quiet TEST_STRING={test_string}"); 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(); thread::sleep(Duration::new(20, 0)); kill_child(&mut child); let output = child.wait_with_output().unwrap(); let r = panic::catch_unwind(|| { let s = String::from_utf8_lossy(&output.stdout); assert_ne!(s.lines().position(|line| line == test_string), None); }); handle_child_output(r, &output); }); } #[test] fn test_counters() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_counters(&guest); } #[test] #[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); 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(); let mut child = cmd.spawn().unwrap(); let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert!(remote_command(&api_socket, "pause", None)); assert!(remote_command( &api_socket, "coredump", Some(format!("file://{vmcore_file}").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"); // 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"); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[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); 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(); let mut child = cmd.spawn().unwrap(); let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert!(remote_command( &api_socket, "coredump", Some(format!("file://{vmcore_file}").as_str()), )); assert_eq!(vm_state(&api_socket), "Running"); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_pvpanic() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_pvpanic(&guest); } #[test] fn test_tap_from_fd() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); _test_tap_from_fd(&guest); } #[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"); } #[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"); } #[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)); 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)); 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 = 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_ip0.clone(); thread::spawn(move || { ssh_command_ip( "nc -l 12345", &guest_ip, DEFAULT_SSH_RETRIES, DEFAULT_SSH_TIMEOUT, ) .unwrap(); }); guest1 .wait_for_ssh_command( "ss -ltnH | awk '{print $4}' | grep -q ':12345$'", Duration::from_secs(20), ) .unwrap(); // Check the connection fails this time guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap_err(); // 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()); // And finally check the connection is functional again guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); // 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()), )); // Wait for the source VM snapshot artifacts to be ready. assert!(wait_until(Duration::from_secs(10), || { Path::new(&snapshot_dir).exists() })); }); // Shutdown the source VM kill_child(&mut child2); let output = child2.wait_with_output().unwrap(); handle_child_output(r, &output); // Remove the vhost-user socket file. Command::new("rm") .arg("-f") .arg("/tmp/dpdkvhostclient2") .output() .unwrap(); 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(); // Wait for the restored VM to accept SSH again after resume. let r = 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(); // 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(); }); guest1 .wait_for_ssh_command( "ss -ltnH | awk '{print $4}' | grep -q ':12345$'", Duration::from_secs(20), ) .unwrap(); // And check the connection is still functional after restore guest2.ssh_command("nc -vz 172.100.0.1 12345").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); } fn setup_spdk_nvme(nvme_dir: &Path) -> Child { cleanup_spdk_nvme(); 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() ); // 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(Duration::new(2, 0)); assert!(exec_host_command_with_retries( "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_transport -t VFIOUSER", 3, 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()); child } fn cleanup_spdk_nvme() { exec_host_command_status("pkill -f nvmf_tgt"); } #[test] 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)); 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]) .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() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // 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\"}") ); // 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 _ = spdk_child.kill(); let _ = spdk_child.wait(); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[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 guest = basic_regular_guest!(JAMMY_IMAGE_NAME).with_cpu(2); _test_vdpa_block(&guest); } #[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 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=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-2,num_queues=3"]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().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 .ssh_command("sudo ip link set dev ens6 address 00:e8:ca:33:ba:06") .unwrap(); guest .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(); // 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 ); // Send 6 packets with ping command guest.ssh_command("ping 172.16.1.10 -c 6 || true").unwrap(); // Check we can find 6 packets on both TX/RX of the network interface assert_eq!( guest .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 6'") .unwrap() .trim() .parse::() .unwrap_or(0), 2 ); // No need to check for hotplug as we already tested it through // test_vdpa_block() }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] #[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)); let (mut swtpm_command, swtpm_socket_path) = prepare_swtpm_daemon(&guest.tmp_dir); 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(); // Start swtpm daemon let mut swtpm_child = swtpm_command.spawn().unwrap(); assert!(wait_until(Duration::from_secs(10), || { Path::new(&swtpm_socket_path).exists() })); let mut child = guest_cmd.spawn().unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); let exercise_tpm = || { assert_eq!( guest.ssh_command("ls /dev/tpm0").unwrap().trim(), "/dev/tpm0" ); guest.ssh_command("sudo tpm2_selftest -f").unwrap(); guest .ssh_command( "sudo tpm2_getrandom 32 >/tmp/tpm_random \ && test $(wc -c /tmp/tpm_pcrread") .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(); }; exercise_tpm(); guest.reboot_linux(0); exercise_tpm(); }); 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] #[cfg(target_arch = "x86_64")] 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"; let kernel_path = direct_kernel_boot_path(); 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]); let mut child = cmd.spawn().unwrap(); let mut r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); if r.is_ok() { r = 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 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 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"]) .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(); let mut child = cmd.spawn().unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert!(remote_command(&api_socket, "nmi", None)); 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 )); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); 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_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); // 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(["--console", "tty,pci_device_id=7"]) .args(["--balloon", "size=0,pci_device_id=8"]) .default_net() .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 = panic::catch_unwind(|| { // 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() })); // Make sure an explicit BDF for virtio-balloon is set. assert!(wait_until(Duration::from_secs(10), || { ssh_command_ip_with_auth( "lspci -n | grep \"00:08.0\"", &default_guest_auth(), &guest.network.guest_ip0, Some(Duration::from_secs(1)), ) .is_ok() })); 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); // 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(), ), ); 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); } #[test] // 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); // 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]) .default_net() .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 = panic::catch_unwind(|| { 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); // 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); 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(), ), ); // 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}" ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] // 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(); #[cfg(target_arch = "aarch64")] let kernel_path = edk2_path(); 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() .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_net() .default_disks() .capture_output(); let mut child = cmd.spawn().unwrap(); guest.wait_vm_boot().unwrap(); let r = panic::catch_unwind(|| { // 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,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id=188", guest.network.guest_mac1, guest.network.host_ip1, ) .as_str(), ), ); // 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}", ); // Use the reserved device ID 0 (root device) let (cmd_success, _, cmd_stderr) = remote_command_w_output( &api_socket, "add-net", Some( format!( "id=test0,tap=,mac={},ip={},mask=255.255.255.128,pci_device_id=0", guest.network.guest_mac1, guest.network.host_ip1, ) .as_str(), ), ); // 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}" ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); 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. #[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 ); let memory_param: &[&str] = if local { &["--memory", "size=1500M,shared=on"] } else { &["--memory", "size=1500M"] }; 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(); 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() .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 r = 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() > 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() })); // 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(); } // Start the live-migration let migration_socket = String::from( 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, paused ), "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", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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); guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); }); // 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 the destination VM has the expected 'console_text' from its output let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); handle_child_output(r, &dest_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. #[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 ); let boot_vcpus = 2; let max_vcpus = 4; let mut blk_file_path = dirs::home_dir().unwrap(); blk_file_path.push("workloads"); blk_file_path.push("blk.img"); 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(["--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(); // 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Make sure the source VM is functaionl // 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 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()), )); // Start the live-migration let migration_socket = String::from( guest .tmp_dir .as_path() .join("live-migration.sock") .to_str() .unwrap(), ); assert!( start_live_migration( &migration_socket, &src_api_socket, &dest_api_socket, true, 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", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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); } // 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() } #[cfg(not(feature = "mshv"))] fn start_live_migration_tcp( src_api_socket: &str, dest_api_socket: &str, dest_event_path: &str, connections: NonZeroU32, ) -> bool { start_live_migration_tcp_with_flags( src_api_socket, dest_api_socket, dest_event_path, connections, false, ) } #[cfg(not(feature = "mshv"))] fn start_live_migration_tcp_with_flags( src_api_socket: &str, dest_api_socket: &str, dest_event_path: &str, connections: NonZeroU32, postcopy: bool, ) -> bool { // Get an available TCP port let migration_port = get_available_port(); let host_ip = "127.0.0.1"; let receive_arg = if postcopy { format!("receiver_url=tcp:0.0.0.0:{migration_port},memory_mode=postcopy") } else { format!("receiver_url=tcp:0.0.0.0:{migration_port}") }; // 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", &receive_arg, ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); let expected_events = [&MetaEvent { event: "migration-receive-ready".to_string(), device_id: None, }]; assert!(wait_for_sequential_events( Duration::from_secs(30), &expected_events, dest_event_path )); // Start the 'send-migration' command on the source let connections = connections.get(); let extra = if postcopy { ",memory_mode=postcopy" } else { "" }; 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}{extra}" ), ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .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 }; 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) ); } // 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 }; 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 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 dest_event_path = temp_event_monitor_path(&guest.tmp_dir); let pmem_temp_file = TempFile::new().unwrap(); pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 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 hotplug_blk_file_path = dirs::home_dir().unwrap(); hotplug_blk_file_path.push("workloads"); hotplug_blk_file_path.push("blk.img"); let hotplug_disk_id = "test0"; let hotplug_disk_params = format!( "path={},id={hotplug_disk_id},readonly=true", hotplug_blk_file_path.to_str().unwrap() ); // The hotplugged disk is expected to appear as /dev/vdc and blk.img is // the 16 MiB workload image used by the disk hotplug tests. let hotplug_disk_count_is = |expected| { guest .ssh_command("lsblk | grep -c 'vdc.*16M' || true") .is_ok_and(|s| s.trim().parse::().is_ok_and(|count| count == expected)) }; let hotplug_disk_exists = || hotplug_disk_count_is(1); let hotplug_disk_absent = || hotplug_disk_count_is(0); // 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", 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]) .args([ "--event-monitor", format!("path={dest_event_path}").as_str(), ]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { 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)); // Test hot(re)plugging works before a migration. // // This currently excludes ARM, because on ARM we boot without OVMF, // using direct kernel boot, where ACPI support is missing. #[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() })); // 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(); assert!(hotplug_disk_absent()); assert!(remote_command( &src_api_socket, "add-disk", Some(hotplug_disk_params.as_str()), )); assert!(wait_until(Duration::from_secs(10), hotplug_disk_exists)); } // Start TCP live migration assert!( start_live_migration_tcp( &src_api_socket, &dest_api_socket, &dest_event_path, connections ), "Unsuccessful command: 'send-migration' or 'receive-migration'." ); }); // 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 src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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() > 1_400_000); guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); // Test hot(re)plugging works after a migration. // // This currently excludes ARM, because on ARM we boot without OVMF, // using direct kernel boot, where ACPI support is missing. #[cfg(target_arch = "x86_64")] { assert!(hotplug_disk_exists()); assert!(remote_command( &dest_api_socket, "remove-device", Some(hotplug_disk_id), )); assert!(wait_until(Duration::from_secs(10), hotplug_disk_absent)); assert!(remote_command( &dest_api_socket, "add-disk", Some(hotplug_disk_params.as_str()), )); assert!(wait_until(Duration::from_secs(10), hotplug_disk_exists)); } }); // 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); // Check if the expected `console_text` is present in the destination VM's output let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); handle_child_output(r, &dest_output); #[cfg(not(target_arch = "x86_64"))] { let _ = hotplug_disk_params; let _ = hotplug_disk_exists; let _ = hotplug_disk_absent; } } // Postcopy live migration. Verifies the destination boots a guest // that touches all of its memory, which forces every page to be // demand-faulted across the network. #[cfg(not(feature = "mshv"))] fn _test_live_migration_tcp_postcopy() { 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 = "netpc1"; 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=512M"]; let boot_vcpus = 2; let src_vm_path = clh_command("cloud-hypervisor"); let src_api_socket = temp_api_path(&guest.tmp_dir); let mut src_child = GuestCommand::new_with_binary_path(&guest, &src_vm_path) .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", net_params.as_str()]) .args(["--api-socket", &src_api_socket]) .capture_output() .spawn() .unwrap(); let dest_event_path = temp_event_monitor_path(&guest.tmp_dir); 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]) .args([ "--event-monitor", format!("path={dest_event_path}").as_str(), ]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); assert!(guest.get_total_memory().unwrap_or_default() > 400_000); guest.check_devices_common(None, Some(&console_text), None); assert!( start_live_migration_tcp_with_flags( &src_api_socket, &dest_api_socket, &dest_event_path, NonZeroU32::new(1).unwrap(), /* postcopy */ true, ), "Postcopy live migration command failed." ); }); if r.is_err() { print_and_panic( src_child, dest_child, None, "Error occurred during postcopy live-migration", ); } let src_exited_ok = wait_until(Duration::from_secs(60), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { print_and_panic( src_child, dest_child, None, "Source VM (postcopy) was not terminated successfully.", ); } let r = panic::catch_unwind(|| { // Probing the destination forces page faults across most of // guest memory. If the source serve loop drops bytes, these // checks fail. assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); assert!(guest.get_total_memory().unwrap_or_default() > 400_000); guest.check_devices_common(None, Some(&console_text), None); }); let _ = dest_child.kill(); let dest_output = dest_child.wait_with_output().unwrap(); handle_child_output(r, &dest_output); } #[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; let src_vm_path = clh_command("cloud-hypervisor"); let src_api_socket = temp_api_path(&guest.tmp_dir); let event_path = temp_event_monitor_path(&guest.tmp_dir); let src_event_path = format!("{event_path}.src"); let dest_event_path = temp_event_monitor_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", net_params.as_str()]) .args(["--api-socket", &src_api_socket]) .args(["--event-monitor", format!("path={src_event_path}").as_str()]) .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]) .args([ "--event-monitor", format!("path={dest_event_path}").as_str(), ]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(panic::AssertUnwindSafe(|| { guest.wait_vm_boot().unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); // 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)); let migration_port = get_available_port(); let host_ip = "127.0.0.1"; let mut receive_migration = Command::new(clh_command("ch-remote")) .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", &format!("receiver_url=tcp:0.0.0.0:{migration_port}"), ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); let expected_events = [&MetaEvent { event: "migration-receive-ready".to_string(), device_id: None, }]; assert!(wait_for_sequential_events( Duration::from_secs(30), &expected_events, &dest_event_path )); // 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(); 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 send_dispatched = match send_status { Some(status) => status.success(), None => { let _ = send_migration.kill(); false } }; assert!( send_dispatched, "send-migration should have dispatched successfully" ); // Clean up receive-migration regardless of its outcome if receive_status.is_none() { let _ = receive_migration.kill(); } // 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 => { let expected_events = [ &MetaEvent { event: "migration-started".to_string(), device_id: None, }, &MetaEvent { event: "migration-failed".to_string(), device_id: None, }, ]; assert!(wait_for_sequential_events( Duration::from_secs(30), &expected_events, &src_event_path )); thread::sleep(Duration::from_secs(2)); assert!( src_child.try_wait().unwrap().is_none(), "Source VM should still be running after a cancelled migration" ); // Confirm the source VM is still responsive over SSH assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); } TimeoutStrategy::Ignore => { let expected_events = [ &MetaEvent { event: "migration-started".to_string(), device_id: None, }, &MetaEvent { event: "migration-finished".to_string(), device_id: None, }, ]; assert!(wait_for_sequential_events( Duration::from_secs(30), &expected_events, &src_event_path )); thread::sleep(Duration::from_secs(3)); assert!( src_child.try_wait().unwrap().is_some(), "Source VM should have terminated after a forced migration" ); // Confirm the VM is still responsive over SSH on the new host assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); } } })); 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(); handle_child_output(r, &src_output); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_basic() { _test_live_migration(false, false, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_local() { _test_live_migration(false, true, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_basic_paused() { _test_live_migration(false, false, true); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_local_paused() { _test_live_migration(false, true, true); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_tcp() { _test_live_migration_tcp(NonZeroU32::new(1).unwrap()); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_tcp_parallel_connections() { _test_live_migration_tcp(NonZeroU32::new(8).unwrap()); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_tcp_postcopy() { _test_live_migration_tcp_postcopy(); } #[test] #[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_live_migration_tcp_timeout_ignore() { _test_live_migration_tcp_timeout(TimeoutStrategy::Ignore); } // 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); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_upgrade_local() { _test_live_migration(true, true, false); } #[test] #[cfg(not(feature = "mshv"))] #[cfg(target_arch = "x86_64")] fn test_live_migration_with_landlock() { _test_live_migration_with_landlock(); } #[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(); let shared_dir = guest.tmp_dir.as_path().join("virtiofs_shared"); fs::create_dir(&shared_dir).unwrap(); let (daemon_child, virtiofsd_socket_path) = prepare_virtiofsd(&guest.tmp_dir, shared_dir.to_str().unwrap()); let src_api_socket = temp_api_path(&guest.tmp_dir); // 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([ "--fs", format!("socket={virtiofsd_socket_path},tag=myfs,num_queues=1,queue_size=1024") .as_str(), ]) .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(); // 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) = 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // 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(); // Verify the file is accessible assert_eq!( guest .ssh_command("cat mount_dir/migration_test_file") .unwrap() .trim(), "pre_migration_data" ); let migration_socket = String::from( guest .tmp_dir .as_path() .join("live-migration.sock") .to_str() .unwrap(), ); // 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 _ = fs::remove_file(&virtiofsd_socket_path); 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 with virtio-fs", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { print_and_panic( src_child, dest_child, None, "source VM was not terminated successfully.", ); } // Post live-migration checks let r = 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" ); // Write a new file after migration guest .ssh_command( "sudo bash -c 'echo post_migration_data > mount_dir/post_migration_file'", ) .unwrap(); // Verify the new file exists on the host let post_content = fs::read_to_string(shared_dir.join("post_migration_file")).unwrap(); assert_eq!(post_content.trim(), "post_migration_data"); }); // 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 _ = fs::remove_file(shared_dir.join("migration_test_file")); let _ = fs::remove_file(shared_dir.join("post_migration_file")); handle_child_output(r, &dest_output); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_virtio_fs() { _test_live_migration_virtio_fs(false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_virtio_fs_local() { _test_live_migration_virtio_fs(true); } } 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 mut child = GuestCommand::new(&guest) .args(dbus_api.guest_args()) .args(http_api.guest_args()) .capture_output() .spawn() .unwrap(); thread::sleep(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"); fs::write(&temp_config_path, request_body).unwrap(); let create_config = temp_config_path.as_os_str().to_str().unwrap(); let r = 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().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 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().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); } #[test] 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 target_api = TargetApi::new_dbus_api(&guest.tmp_dir); _test_api_create_boot(&target_api, &guest); } #[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); let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); _test_api_shutdown(&target_api, &guest); } #[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); let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); _test_api_delete(&target_api, &guest); } #[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); let target_api = TargetApi::new_dbus_api(&guest.tmp_dir); _test_api_pause_resume(&target_api, &guest); } } mod ivshmem { #[cfg(not(feature = "mshv"))] use std::fs::remove_dir_all; use std::process::Command; use test_infra::{Guest, GuestCommand, UbuntuDiskConfig, handle_child_output, kill_child}; use crate::*; #[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 ); let memory_param: &[&str] = if local { &["--memory", "size=4G,shared=on"] } else { &["--memory", "size=4G"] }; 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(); 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 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(); // 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", 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 = 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); // 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)); // 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)); } // 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(Duration::new(40, 0)); // Start the live-migration let migration_socket = String::from( 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, 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", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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); guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); // Check ivshmem device _test_ivshmem(&guest, &ivshmem_file_path, file_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); // Check the destination VM has the expected 'console_text' from its output let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); handle_child_output(r, &dest_output); } #[test] 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 kernel_path = direct_kernel_boot_path(); 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 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([ "--ivshmem", format!("path={ivshmem_file_path},size={file_size}").as_str(), ]) .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = 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(); handle_child_output(r, &output); } #[test] #[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 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()]) .default_disks() .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 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Check the number of vCPUs assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); 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); // Remove the vsock socket file. 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}").as_str(), ]) .capture_output() .spawn() .unwrap(); 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 )); // Remove the snapshot dir let _ = remove_dir_all(snapshot_dir.as_str()); let r = 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)); 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 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 = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); handle_child_output(r, &output); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_ivshmem() { _test_live_migration_ivshmem(false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_ivshmem_local() { _test_live_migration_ivshmem(true); } #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_hotplug_virtiomem() { snapshot_restore_common::_test_snapshot_restore( snapshot_restore_common::SnapshotRestoreTest { use_hotplug: true, ..Default::default() }, ); } #[test] #[cfg(not(feature = "mshv"))] // See issue #7437 fn test_snapshot_restore_basic() { snapshot_restore_common::_test_snapshot_restore( snapshot_restore_common::SnapshotRestoreTest::default(), ); } #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_with_resume() { snapshot_restore_common::_test_snapshot_restore( snapshot_restore_common::SnapshotRestoreTest { use_resume_option: true, ..Default::default() }, ); } #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_check_guest_time() { snapshot_restore_common::_test_snapshot_restore( snapshot_restore_common::SnapshotRestoreTest { use_resume_option: true, check_clock: true, ..Default::default() }, ); } // aarch64 same-host pause/resume must keep the guest clock correct: the // architected counter free-runs across the pause, so the resume clock path // must not perturb it. The test network is isolated, so the guest cannot // NTP-correct itself -- any drift would be the pause path's doing. x86_64 // has its own kvmclock path and is covered separately. #[test] #[cfg(all(not(feature = "mshv"), target_arch = "aarch64"))] fn test_pause_resume_guest_time() { 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(["--api-socket", &api_socket]) .args(["--cpus", "boot=2"]) .args(["--memory", "size=1G"]) .args(["--kernel", kernel_path.to_str().unwrap()]) .default_disks() .args(["--net", guest.default_net_string().as_str()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Pause for an off-host interval, then resume on the same host. assert!(remote_command(&api_socket, "pause", None)); thread::sleep(Duration::from_secs( snapshot_restore_common::CLOCK_DOWNTIME_SECS, )); assert!(remote_command(&api_socket, "resume", None)); // The counter advanced across the pause, so the guest wall clock must // still match the host. let host_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() as i64; let guest_secs = guest .ssh_command("date -u +%s") .unwrap() .trim() .parse::() .unwrap(); let skew = (host_secs - guest_secs).abs(); assert!( skew <= snapshot_restore_common::CLOCK_SKEW_TOLERANCE_SECS, "guest clock is {skew}s from host after pause/resume \ (host={host_secs}, guest={guest_secs})" ); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_uffd() { snapshot_restore_common::_test_snapshot_restore_uffd("size=2G", &[], 1_920_000); } #[test] #[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] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_offload() { snapshot_restore_common::_test_snapshot_restore_offload(false, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_offload_virtio_mem() { snapshot_restore_common::_test_snapshot_restore_offload(true, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_offload_ondemand() { snapshot_restore_common::_test_snapshot_restore_offload(false, true); } #[test] fn test_virtio_pmem_persist_writes() { test_virtio_pmem(false, false); } } #[cfg(not(feature = "mshv"))] mod snapshot_restore_common { use std::fs::remove_dir_all; use std::process::Command; use crate::*; // Off-host interval simulated between snapshot and restore, and the maximum // guest-vs-host clock skew tolerated afterwards. The interval must exceed the // tolerance so a guest that fails to advance on restore is caught. pub(crate) const CLOCK_DOWNTIME_SECS: u64 = 30; pub(crate) const CLOCK_SKEW_TOLERANCE_SECS: i64 = 15; 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] = [ &MetaEvent { event: "pausing".to_string(), device_id: None, }, &MetaEvent { event: "paused".to_string(), device_id: None, }, ]; assert!(wait_for_latest_events_exact( Duration::from_secs(30), &latest_events, event_path )); // Take a snapshot from the VM assert!(remote_command( api_socket, "snapshot", Some(format!("file://{snapshot_dir}").as_str()), )); let latest_events = [ &MetaEvent { event: "snapshotting".to_string(), device_id: None, }, &MetaEvent { event: "snapshotted".to_string(), device_id: None, }, ]; assert!(wait_for_latest_events_exact( Duration::from_secs(30), &latest_events, event_path )); } /// Easy disambiguation between snapshot/restore variants. #[derive(Clone, Copy, Default)] pub(crate) struct SnapshotRestoreTest { pub use_hotplug: bool, pub use_resume_option: bool, pub check_clock: bool, } pub(crate) fn _test_snapshot_restore(cfg: SnapshotRestoreTest) { let SnapshotRestoreTest { use_hotplug, use_resume_option, check_clock, } = cfg; 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.128", net_id, guest.network.guest_mac0, guest.network.host_ip0 ); let mut mem_params = "size=1G"; if use_hotplug { mem_params = "size=2G,hotplug_method=virtio-mem,hotplug_size=32G"; } let cloudinit_params = format!( "path={},iommu=on", guest.disk_config.disk(DiskType::CloudInit).unwrap() ); let socket = temp_vsock_path(&guest.tmp_dir); let event_path = temp_event_monitor_path(&guest.tmp_dir); // x86_64: force kvm-clock — the restore catch-up moves kvmclock (KVM_SET_CLOCK), // not the tsc clocksource, so a tsc guest wouldn't catch up. aarch64 ignores this // (CNTVCT is advanced directly). let boot_cmdline = if check_clock && cfg!(target_arch = "x86_64") { format!("{DIRECT_KERNEL_BOOT_CMDLINE} clocksource=kvm-clock") } else { DIRECT_KERNEL_BOOT_CMDLINE.to_string() }; let mut child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket_source]) .args(["--event-monitor", format!("path={event_path}").as_str()]) .args(["--cpus", "boot=4"]) .args(["--memory", mem_params]) .args(["--balloon", "size=0"]) .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(["--vsock", format!("cid=3,socket={socket}").as_str()]) .args(["--cmdline", &boot_cmdline]) .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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Check the number of vCPUs assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); // Check the guest RAM 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( &api_socket_source, None, Some(6 << 30), None, Some(&event_path), ); thread::sleep(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( &api_socket_source, None, None, Some(1 << 30), Some(&event_path), ); thread::sleep(Duration::new(5, 0)); let total_memory = guest.get_total_memory().unwrap_or_default(); 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); // x86_64: We check that removing and adding back the virtio-net device // does not break the snapshot/restore support for virtio-pci. // This is an important thing to test as the hotplug will // trigger a PCI BAR reprogramming, which is a good way of // checking if the stored resources are correctly restored. // Unplug the virtio-net device // AArch64: Device hotplug is currently not supported, skipping here. #[cfg(target_arch = "x86_64")] { assert!(remote_command( &api_socket_source, "remove-device", Some(net_id), )); let latest_events = [&MetaEvent { event: "device-removed".to_string(), device_id: Some(net_id.to_string()), }]; assert!(wait_for_latest_events_exact( Duration::from_secs(30), &latest_events, &event_path )); // Plug the virtio-net device again assert!(remote_command( &api_socket_source, "add-net", Some(net_params.as_str()), )); thread::sleep(Duration::new(10, 0)); } 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); let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); handle_child_output(r, &output); // Remove the vsock socket file. Command::new("rm") .arg("-f") .arg(socket.as_str()) .output() .unwrap(); // Simulate an off-host interval between snapshot and restore so the guest // clock must visibly catch up on restore (asserted after resume below). if check_clock { thread::sleep(Duration::from_secs(CLOCK_DOWNTIME_SECS)); } 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},resume={use_resume_option}").as_str(), ]) .capture_output() .spawn() .unwrap(); 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!(wait_for_sequential_events( Duration::from_secs(30), &expected_events, &event_path_restored )); if use_resume_option { let latest_events = [ &MetaEvent { event: "restored".to_string(), device_id: None, }, &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 )); } 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 = panic::catch_unwind(|| { if use_resume_option { // VM was automatically resumed via restore option, just wait for events thread::sleep(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 > 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(Duration::new(5, 0)); assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); // Decrease guest RAM with virtio-mem resize_command(&api_socket_restored, None, Some(5 << 30), None, None); thread::sleep(Duration::new(5, 0)); let total_memory = guest.get_total_memory().unwrap_or_default(); 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); if check_clock { // Across the off-host interval the restored guest's wall clock // must catch up to real time: x86_64 via kvmclock // (KVM_CLOCK_REALTIME), aarch64 via the CNTVCT advance. The test // network is isolated, so the guest cannot NTP-correct itself -- // any catch-up is the restore path's doing. let host_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() as i64; let guest_secs = guest .ssh_command("date -u +%s") .unwrap() .trim() .parse::() .unwrap(); let skew = (host_secs - guest_secs).abs(); assert!( skew <= CLOCK_SKEW_TOLERANCE_SECS, "guest clock is {skew}s from host after restore \ (host={host_secs}, guest={guest_secs}); the \ {CLOCK_DOWNTIME_SECS}s off-host interval was not applied" ); } }); // 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 = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); handle_child_output(r, &output); } 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 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 source_cmd = GuestCommand::new(&guest); source_cmd .args(["--api-socket", &api_socket_source]) .args(["--event-monitor", format!("path={event_path}").as_str()]) .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(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() .default_net() .args(["--vsock", format!("cid=3,socket={socket}").as_str()]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); 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(Some(&socket), Some(&console_text), None); snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = 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)); let snapshot_dir2 = String::from(guest.tmp_dir.as_path().join("snapshot2").to_str().unwrap()); fs::create_dir(&snapshot_dir2).unwrap(); 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(); 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 )); let r = panic::catch_unwind(|| { assert!(wait_until(Duration::from_secs(30), || remote_command( &api_socket_restored, "info", None ))); // Snapshot the still-restoring VM: refused until prefault completes. assert!(wait_until(Duration::from_secs(120), || remote_command( &api_socket_restored, "snapshot", Some(format!("file://{snapshot_dir2}").as_str()), ))); 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 )); 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(Some(&socket), Some(&console_text), None); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = 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); // Restore the 2nd snapshot into a fresh VM to prove no guest RAM was dropped. Command::new("rm") .arg("-f") .arg(socket.as_str()) .output() .unwrap(); let api_socket_restored2 = format!("{}.3", temp_api_path(&guest.tmp_dir)); let event_path_restored2 = format!("{}.3", temp_event_monitor_path(&guest.tmp_dir)); let mut child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket_restored2]) .args([ "--event-monitor", format!("path={event_path_restored2}").as_str(), ]) .args([ "--restore", format!("source_url=file://{snapshot_dir2}").as_str(), ]) .capture_output() .spawn() .unwrap(); 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_restored2 )); let r = panic::catch_unwind(|| { assert!(wait_until(Duration::from_secs(30), || remote_command( &api_socket_restored2, "info", None ))); assert!(remote_command(&api_socket_restored2, "resume", None)); 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(Some(&socket), Some(&console_text), None); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let _ = remove_dir_all(snapshot_dir.as_str()); let _ = remove_dir_all(snapshot_dir2.as_str()); } 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)); let device_params = { let mut data = vec![]; if pvpanic { data.push(String::from("--pvpanic")); } data }; 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()]) .default_disks() .default_net() .args(["--vsock", format!("cid=3,socket={socket}").as_str()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .args(device_params) .capture_output() .spawn() .unwrap(); 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 r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); 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)); 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(); 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 )); let _ = remove_dir_all(snapshot_dir.as_str()); let r = panic::catch_unwind(|| { 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 )); assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); guest.check_devices_common(Some(&socket), Some(&console_text), None); if pvpanic { make_guest_panic(&guest); thread::sleep(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_restored )); } }); kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); handle_child_output(r, &output); } // Round-trip via the reference offload daemon over the existing // `vm.send-migration local=on` / `vm.receive-migration` endpoints, // proving parity with `vm.snapshot`/`vm.restore`. pub(crate) fn _test_snapshot_restore_offload(virtio_mem: bool, ondemand: 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 console_text = String::from("On a branch floating down river a cricket, singing."); let offload_dir = String::from( guest .tmp_dir .as_path() .join("offload-store") .to_str() .unwrap(), ); fs::create_dir(&offload_dir).unwrap(); let snapshot_socket = String::from( guest .tmp_dir .as_path() .join("snapshot-offload.sock") .to_str() .unwrap(), ); // Shared memory required: offload runs over local live migration. // With virtio_mem, expose a hotpluggable zone so the test can plug // RAM before the snapshot and check it survives the round-trip. let mem_params = if virtio_mem { "size=512M,hotplug_method=virtio-mem,hotplug_size=2G,shared=on" } else { "size=512M,shared=on" }; let mut child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket_source]) .args(["--cpus", "boot=2"]) .args(["--memory", mem_params]) .args(["--kernel", kernel_path.to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() .default_net() .capture_output() .spawn() .unwrap(); let snap_daemon: Mutex> = Mutex::new(None); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); assert!(guest.get_total_memory().unwrap_or_default() > 400_000); guest.check_devices_common(None, Some(&console_text), None); if virtio_mem { // Plug additional RAM via virtio-mem before snapshotting so the // offload round-trip has plugged blocks to preserve. resize_command(&api_socket_source, None, Some(1 << 30), None, None); thread::sleep(Duration::from_secs(5)); assert!( guest.get_total_memory().unwrap_or_default() > 900_000, "virtio-mem plug before snapshot did not take effect" ); } // Daemon binds the socket and listens. let daemon = Command::new(clh_command("offload_daemon")) .args([ "snapshot", "--socket", &snapshot_socket, "--output-dir", &offload_dir, ]) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); *snap_daemon.lock().unwrap() = Some(daemon); // Give the daemon a moment to bind. assert!(wait_until(Duration::from_secs(5), || Path::new( &snapshot_socket ) .exists())); // Pause explicitly to mirror typical operator usage. assert!(remote_command(&api_socket_source, "pause", None)); // Source exits on success, as with any local live migration. assert!(remote_command( &api_socket_source, "send-migration", Some(format!("destination_url=unix:{snapshot_socket},local=on").as_str(),), )); // The daemon should exit cleanly after persisting the snapshot. let daemon_output = snap_daemon .lock() .unwrap() .take() .unwrap() .wait_with_output() .unwrap(); assert!( daemon_output.status.success(), "offload daemon (snapshot) failed: stderr={}", String::from_utf8_lossy(&daemon_output.stderr) ); }); if let Some(mut daemon) = snap_daemon.into_inner().unwrap() { let _ = daemon.kill(); let _ = daemon.wait(); } let _ = fs::remove_file(&snapshot_socket); // Source VM should have exited cleanly on its own. let source_exited_ok = wait_until(Duration::from_secs(30), || { matches!(child.try_wait(), Ok(Some(_))) }) && child.try_wait().unwrap().is_some_and(|s| s.success()); if !source_exited_ok { kill_child(&mut child); } let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); // Now bring up a fresh VMM and restore through the offload daemon. let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); let restore_socket = String::from( guest .tmp_dir .as_path() .join("restore-offload.sock") .to_str() .unwrap(), ); let mut dest_child = GuestCommand::new(&guest) .args(["--api-socket", &api_socket_restored]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { // Wait for the destination VMM to be up and responsive. assert!(wait_until(Duration::from_secs(30), || remote_command( &api_socket_restored, "ping", None ))); // receive-migration blocks until done. Run it in a thread so // we can start the daemon as the sender in parallel. On demand // mode adds `memory_mode=postcopy`. let api_socket_restored_clone = api_socket_restored.clone(); let restore_socket_clone = restore_socket.clone(); let ondemand_for_thread = ondemand; let restore_thread = thread::spawn(move || { let arg = if ondemand_for_thread { format!("receiver_url=unix:{restore_socket_clone},memory_mode=postcopy") } else { format!("receiver_url=unix:{restore_socket_clone}") }; remote_command(&api_socket_restored_clone, "receive-migration", Some(&arg)) }); // Wait for CH to bind the socket before starting the daemon. assert!(wait_until(Duration::from_secs(10), || { Path::new(&restore_socket).exists() })); // Daemon in restore (sender) mode with --resume so the // guest is live when we probe it. On demand mode adds --ondemand and // keeps the daemon connected to serve PageFault requests. let mut restore_args = vec![ "restore".to_string(), "--socket".to_string(), restore_socket.clone(), "--input-dir".to_string(), offload_dir.clone(), "--resume".to_string(), ]; if ondemand { restore_args.push("--ondemand".to_string()); } let daemon = Command::new(clh_command("offload_daemon")) .args(&restore_args) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); if ondemand { drop(daemon); } else { let daemon_output = daemon.wait_with_output().unwrap(); assert!( daemon_output.status.success(), "offload daemon (restore) failed: stderr={}", String::from_utf8_lossy(&daemon_output.stderr) ); } assert!( restore_thread.join().unwrap(), "ch-remote receive-migration command failed" ); // Restored VM should be functional. guest.wait_for_ssh(Duration::from_secs(30)).unwrap(); assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); guest.check_devices_common(None, Some(&console_text), None); if virtio_mem { // The plugged virtio-mem blocks must survive the offload // snapshot/restore round-trip. assert!( guest.get_total_memory().unwrap_or_default() > 900_000, "virtio-mem plugged RAM was lost across offload restore" ); } }); kill_child(&mut dest_child); let output = dest_child.wait_with_output().unwrap(); handle_child_output(r, &output); let _ = remove_dir_all(offload_dir.as_str()); } } mod common_sequential { #[cfg(not(feature = "mshv"))] use std::fs::remove_dir_all; #[cfg(not(feature = "mshv"))] use std::net::{IpAddr, Ipv4Addr}; use crate::*; #[test] #[cfg(not(feature = "mshv"))] fn test_memory_mergeable_on() { test_memory_mergeable(true); } #[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; } // reserve=on opts the hugepage-backed zone out of MAP_NORESERVE, so // the 2MiB pages are reserved from the pool at mmap time. Hugepages // are the most likely place to want this, and exercising it here keeps // the reserve path covered across both the source boot and the // demand-paged UFFD restore. The skip guard above already requires the // 256 free pages this zone needs, and the source VM is killed before // restore, so the pool only ever has to back one VM at a time. snapshot_restore_common::_test_snapshot_restore_uffd( "size=0", &["id=mem0,size=512M,hugepages=on,hugepage_size=2M,reserve=on"], 480_000, ); } #[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(); 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(IpAddr::V4( 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 ); let cloudinit_params = format!( "path={},iommu=on", guest.disk_config.disk(DiskType::CloudInit).unwrap() ); let n_cpu = 2; 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", 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(); 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 = panic::catch_unwind(|| { guest.wait_vm_boot().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()) }; } // 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); // Check the guest virtio-devices, e.g. block, rng, vsock, console, and net guest.check_devices_common(None, Some(&console_text), None); 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); let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); handle_child_output(r, &output); 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(), ]) .capture_output() .spawn() .unwrap(); thread::sleep(Duration::new(2, 0)); let taps = net_util::open_tap( Some(tap_name), Some(IpAddr::V4( 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()) )); // Wait for the VM to be restored assert!(wait_until(Duration::from_secs(20), || { remote_command(&api_socket_restored, "info", None) })); // 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, }, ]; // 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 )); // Remove the snapshot dir let _ = remove_dir_all(snapshot_dir.as_str()); let r = 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)); 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(), n_cpu); assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 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); let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); }); handle_child_output(r, &output); } #[test] #[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 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 snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Mount virtiofs and write a test file guest .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") .unwrap(); // Verify the shared directory is accessible assert_eq!( guest.ssh_command("cat mount_dir/file1").unwrap().trim(), "foo" ); // 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, ); }); // Shutdown the source VM kill_child(&mut child); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); // 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 = 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(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 = 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 _ = fs::remove_file(shared_dir.join("snapshot_test_file")); let _ = fs::remove_file(shared_dir.join("post_restore_file")); } #[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 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 boot_vcpus = 2; let max_vcpus = 4; let pmem_temp_file = TempFile::new().unwrap(); pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 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() .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 r = 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(); } // Start the live-migration let migration_socket = String::from( 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, 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", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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); guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); // 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(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(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); }); // 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 the destination VM has the expected 'console_text' from its output let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); handle_child_output(r, &dest_output); } #[cfg(not(feature = "mshv"))] 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 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 boot_vcpus = 6; let max_vcpus = 12; let pmem_temp_file = TempFile::new().unwrap(); pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 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() .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 r = 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() > 2_880_000); // Check the guest virtio-devices, e.g. block, rng, console, and net guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); // 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"]), ); // 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(&src_api_socket, "mem0", "2G"); resize_zone_command(&src_api_socket, "mem1", "2G"); resize_zone_command(&src_api_socket, "mem2", "3G"); thread::sleep(Duration::new(5, 0)); guest.check_numa_common(Some(&[1_920_000, 1_920_000, 1_920_000]), None, None); } } // 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(); } // Start the live-migration let migration_socket = String::from( 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, 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", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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); guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); // 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"]), ); } // 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"]), ); 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(&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(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, ); } } }); // 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 the destination VM has the expected 'console_text' from its output let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); handle_child_output(r, &dest_output); } #[cfg(not(feature = "mshv"))] 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 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); // 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); // 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 r = panic::catch_unwind(|| { // Give it '1s' to make sure the 'dest_api_socket' file is properly created thread::sleep(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, 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, Some(ovs_child), "Error occurred during live-migration", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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(); }); // Wait for the server to be listening thread::sleep(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(); }); // 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(); cleanup_ovs_dpdk(); 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_live_migration_balloon() { _test_live_migration_balloon(false, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_balloon_local() { _test_live_migration_balloon(false, true); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_upgrade_balloon() { _test_live_migration_balloon(true, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_upgrade_balloon_local() { _test_live_migration_balloon(true, true); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_numa() { _test_live_migration_numa(false, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_numa_local() { _test_live_migration_numa(false, true); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_upgrade_numa() { _test_live_migration_numa(true, false); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_upgrade_numa_local() { _test_live_migration_numa(true, true); } // 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); } #[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); } #[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 #5532"] #[cfg(target_arch = "x86_64")] #[cfg(not(feature = "mshv"))] fn test_live_upgrade_ovs_dpdk_local() { _test_live_migration_ovs_dpdk(true, true); } #[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 memory_param: &[&str] = if local { &["--memory", "size=1500M,shared=on"] } else { &["--memory", "size=1500M"] }; 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(); 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() .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 r = 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() > 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() })); // 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(); } // Enable watchdog and ensure its functional let 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(Duration::new(40, 0)); // Check no reboot assert_eq!(get_reboot_count(&guest), expected_reboot_count); // Start the live-migration let migration_socket = String::from( 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, 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", ); } let src_exited_ok = wait_until(Duration::from_secs(30), || { matches!(src_child.try_wait(), Ok(Some(_))) }) && src_child.try_wait().unwrap().is_some_and(|s| s.success()); if !src_exited_ok { 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 = 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); guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); // Perform checks on watchdog let mut expected_reboot_count = 1; // Allow some normal time to elapse to check we don't get spurious reboots thread::sleep(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(&dest_api_socket, "pause", None)); thread::sleep(Duration::new(30, 0)); assert!(remote_command(&dest_api_socket, "resume", None)); // Check no reboot assert_eq!(get_reboot_count(&guest), expected_reboot_count); } }); // 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 the destination VM has the expected 'console_text' from its output let r = panic::catch_unwind(|| { assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); }); handle_child_output(r, &dest_output); } #[test] fn test_watchdog() { let guest = basic_regular_guest!(JAMMY_IMAGE_NAME); _test_watchdog(&guest); } #[test] #[cfg(not(feature = "mshv"))] fn test_live_migration_watchdog() { _test_live_migration_watchdog(false, false); } } mod windows { use std::sync::LazyLock; 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; } 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_retry( cmd, &self.auth, &self.guest.network.guest_ip0, DEFAULT_SSH_RETRIES, DEFAULT_SSH_TIMEOUT, ) .unwrap() } fn cpu_count(&self) -> u8 { self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).NumberOfLogicalProcessors\"") .trim() .parse::() .unwrap_or(0) } fn ram_size(&self) -> usize { self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).TotalPhysicalMemory\"") .trim() .parse::() .unwrap_or(0) } 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) } fn disk_count(&self) -> u8 { self.ssh_cmd("powershell -Command \"Get-Disk | Measure-Object -Line | Format-Table -HideTableHeaders\"") .trim() .parse::() .unwrap_or(0) } fn tpm_status(&self) -> String { self.ssh_cmd( "powershell -NoProfile -Command \"(Get-PnpDevice -Class SecurityDevices | \ Where-Object { $_.FriendlyName -match 'Trusted Platform Module|TPM' } | \ Select-Object -First 1 -ExpandProperty Status)\"", ) .trim() .to_string() } fn reboot(&self) { let _ = self.ssh_cmd("shutdown /r /t 0"); } fn shutdown(&self) { let _ = self.ssh_cmd("shutdown /s /t 0"); } fn run_dnsmasq(&self) -> 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 ); 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() } // A second dnsmasq for the dedicated KDNET debug NIC so the KDNET // target can obtain an address and reach the debugger host address. fn run_dnsmasq_debug(&self) -> Child { let listen_address = format!("--listen-address={}", self.guest.network.host_ip1); let dhcp_host = format!( "--dhcp-host={},{}", self.guest.network.guest_mac1, self.guest.network.guest_ip1 ); let dhcp_range = format!( "--dhcp-range=eth,{},{}", self.guest.network.guest_ip1, self.guest.network.guest_ip1 ); Command::new("dnsmasq") .arg("--no-daemon") .arg("--log-queries") .arg(listen_address.as_str()) .arg("--except-interface=lo") .arg("--bind-dynamic") .arg("--conf-file=/dev/null") .arg(dhcp_host.as_str()) .arg(dhcp_range.as_str()) .spawn() .unwrap() } // Return the "bus.device.function" of the NIC with the given MAC. KDNET // selects the debug adapter with this value (bcdedit busparams). Windows // reports the MAC uppercased with dashes and the PCI location as e.g. // "PCI bus 0, device 3, function 0". fn nic_busparams(&self, mac: &str) -> String { let win_mac = mac.to_uppercase().replace(':', "-"); let location = self.ssh_cmd(&format!( "powershell -Command \"Get-NetAdapter | Where-Object {{ $_.MacAddress -eq '{win_mac}' }} | ForEach-Object {{ (Get-PnpDeviceProperty -InstanceId $_.PnpDeviceID -KeyName DEVPKEY_Device_LocationInfo).Data }}\"" )); let nums: Vec<&str> = location .split(|c: char| !c.is_ascii_digit()) .filter(|s| !s.is_empty()) .collect(); assert!( nums.len() >= 3, "unexpected NIC location '{location}' for mac {mac}" ); format!("{}.{}.{}", nums[0], nums[1], nums[2]) } // 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 img = PathBuf::from(format!("/tmp/test-hotplug-{id}.raw")); let _ = fs::remove_file(&img); // 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:?}"); // 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:?}"); // 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:?}"); // Disengage the loop device let out = Command::new("losetup") .args(["-d", loop_dev]) .output() .expect("loop device not found") .stdout; println!("{out:?}"); // 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:?}"); // 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:?}"); // 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:?}"); img.to_str().unwrap().to_string() } fn disks_set_rw(&self) { let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsReadOnly $False\""); } fn disks_online(&self) { let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsOffline $False\""); } fn disk_file_put(&self, fname: &str, data: &str) { let _ = self.ssh_cmd(&format!( "powershell -Command \"'{data}' | Set-Content -Path {fname}\"" )); } fn disk_file_read(&self, fname: &str) -> String { self.ssh_cmd(&format!( "powershell -Command \"Get-Content -Path {fname}\"" )) } 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 } 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 } 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_windows_guest() { let windows_guest = WindowsGuest::new(); 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"]) .default_disks() .default_net() .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(); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); windows_guest.shutdown(); }); let _ = child.wait_timeout(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); } // Verify Windows kernel network debugging (KDNET) works over a Cloud // Hypervisor virtio-net device. Configure KDNET on a dedicated virtio-net // NIC and confirm the debuggee actually transmits KDNET packets to the // debugger host address. Receiving such a packet exercises the whole device // data path: discovery, feature negotiation, virtqueue setup and the TX // doorbell. A real debugger is not needed because KDNET connections are // initiated by the target, which polls the debugger host address on boot. #[test] #[cfg(not(target_arch = "aarch64"))] fn test_windows_guest_kdnet_virtio_net() { use std::net::UdpSocket; use std::time::Instant; let windows_guest = WindowsGuest::new(); let mut ovmf_path = dirs::home_dir().unwrap(); ovmf_path.push("workloads"); ovmf_path.push(OVMF_NAME); let port = 50000u16; // Dedicated debug NIC. KDNET takes exclusive ownership of the NIC it // uses, so keep it separate from the management NIC used for SSH. Its // host tap address doubles as the KDNET debugger host address. let debug_net = format!( "tap=,mac={},ip={},mask=255.255.255.128", windows_guest.guest().network.guest_mac1, windows_guest.guest().network.host_ip1 ); let mut child = GuestCommand::new(windows_guest.guest()) .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() .args(["--net", debug_net.as_str()]) .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(); let mut child_dnsmasq_debug = windows_guest.run_dnsmasq_debug(); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up. windows_guest.wait_for_boot().unwrap(); // Enable KDNET on the dedicated virtio-net NIC, selected by its PCI // bus params, pointing at the host tap address as the debugger. let busparams = windows_guest.nic_busparams(&windows_guest.guest().network.guest_mac1); windows_guest.ssh_cmd(&format!( "bcdedit /dbgsettings net hostip:{} port:{} key:1.2.3.4", windows_guest.guest().network.host_ip1, port )); windows_guest.ssh_cmd(&format!( "bcdedit /set \"{{dbgsettings}}\" busparams {busparams}" )); windows_guest.ssh_cmd("bcdedit /debug on"); // Reboot so the KDNET transport takes over the debug NIC, then wait // for Windows to come back (over the unaffected management NIC). windows_guest.reboot(); windows_guest.wait_for_boot().unwrap(); // Listen on the debugger host address and wait for the KDNET target // to poll it. Any datagram from the debuggee proves the virtio-net // TX path works end to end. let socket = UdpSocket::bind((windows_guest.guest().network.host_ip1.as_str(), port)).unwrap(); socket .set_read_timeout(Some(Duration::from_secs(5))) .unwrap(); let mut buf = [0u8; 1500]; let deadline = Instant::now() + Duration::from_secs(120); let mut received = false; while Instant::now() < deadline { match socket.recv_from(&mut buf) { Ok((n, src)) if n > 0 && src.ip().to_string() == windows_guest.guest().network.guest_ip1 => { received = true; break; } _ => {} } } assert!( received, "no KDNET packet received from the debuggee over virtio-net" ); windows_guest.shutdown(); }); let _ = child.wait_timeout(Duration::from_secs(60)); let _ = child.kill(); let output = child.wait_with_output().unwrap(); let _ = child_dnsmasq.kill(); let _ = child_dnsmasq.wait(); let _ = child_dnsmasq_debug.kill(); let _ = child_dnsmasq_debug.wait(); handle_child_output(r, &output); } #[test] #[cfg(target_arch = "x86_64")] fn test_windows_guest_tpm() { let windows_guest = WindowsGuest::new(); let (mut swtpm_command, swtpm_socket_path) = prepare_swtpm_daemon(&windows_guest.guest().tmp_dir); let mut swtpm_child = swtpm_command.spawn().unwrap(); assert!(wait_until(Duration::from_secs(10), || { Path::new(&swtpm_socket_path).exists() })); 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(["--tpm", &format!("socket={swtpm_socket_path}")]) .default_disks() .default_net() .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(); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); assert_eq!(windows_guest.tpm_status(), "OK"); windows_guest.shutdown(); }); let _ = child.wait_timeout(Duration::from_secs(60)); let _ = child.kill(); let output = child.wait_with_output().unwrap(); let _ = child_dnsmasq.kill(); let _ = child_dnsmasq.wait(); let _ = swtpm_child.kill(); let _ = swtpm_child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_windows_guest_multiple_queues() { let windows_guest = WindowsGuest::new(); 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", ovmf_path.to_str().unwrap()]) .args(["--serial", "tty"]) .args(["--console", "off"]) .args([ "--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(), ]) .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(); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); windows_guest.shutdown(); }); let _ = child.wait_timeout(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] #[cfg(not(feature = "mshv"))] #[cfg_attr(target_arch = "aarch64", ignore = "See #4327")] fn test_windows_guest_snapshot_restore() { 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_source = format!("{}.1", temp_api_path(&tmp_dir)); 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() .default_net() .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 windows_guest.wait_for_boot().unwrap(); 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 snapshot_state_path = Path::new(&snapshot_dir).join("state.json"); let snapshot_config_path = Path::new(&snapshot_dir).join("config.json"); assert!(wait_until(Duration::from_secs(30), || { snapshot_state_path.exists() && snapshot_config_path.exists() })); let _ = child.kill(); child.wait().unwrap(); 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) })); let r = 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)); windows_guest.shutdown(); }); let _ = child.wait_timeout(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] #[cfg(not(feature = "mshv"))] #[cfg(not(target_arch = "aarch64"))] fn test_windows_guest_cpu_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); 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(); let mut child_dnsmasq = windows_guest.run_dnsmasq(); let r = 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(Duration::new(10, 0)); // 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); windows_guest.shutdown(); }); let _ = child.wait_timeout(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] #[cfg(not(feature = "mshv"))] #[cfg(not(target_arch = "aarch64"))] fn test_windows_guest_ram_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); 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(); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); 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); 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)); 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); windows_guest.shutdown(); }); let _ = child.wait_timeout(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] #[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); 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 = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); // 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); // 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); // 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); windows_guest.shutdown(); }); let _ = child.wait_timeout(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] #[ignore = "See #6037"] #[cfg(not(feature = "mshv"))] #[cfg(not(target_arch = "aarch64"))] fn test_windows_guest_disk_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); 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 disk = windows_guest.disk_new(WindowsGuest::FS_FAT, 100); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); // 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\"")); // 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); 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); // 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); // 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()); // Intentionally no unmount, it'll happen at shutdown. windows_guest.shutdown(); }); let _ = child.wait_timeout(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] #[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 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=2G"]) .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(); // 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 = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); // 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); 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); // 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); } // 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); } // 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); // 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(); }); let _ = child.wait_timeout(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] #[cfg(not(feature = "mshv"))] #[cfg(not(target_arch = "aarch64"))] fn test_windows_guest_netdev_multi() { 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); 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(); let r = panic::catch_unwind(|| { // Wait to make sure Windows boots up windows_guest.wait_for_boot().unwrap(); let netdev_num = 3; assert_eq!(windows_guest.netdev_count(), netdev_num); assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); let tap_count = exec_host_command_output("ip link | grep -c mytap42"); assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); windows_guest.shutdown(); }); let _ = child.wait_timeout(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_windows_guest_qcow2_backing_direct() { let windows_guest = WindowsGuest::new(); let qcow2_path = windows_guest.guest().disk_config.qcow2_disk().unwrap(); 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 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(); let r = panic::catch_unwind(|| { 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()); } windows_guest.shutdown(); }); let _ = child.wait_timeout(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); } } #[cfg(target_arch = "x86_64")] mod vfio { use std::io; use crate::*; 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; fn nvidia_vfio_device_ready() -> bool { if !Path::new(NVIDIA_VFIO_DEVICE).exists() { println!("SKIPPED: VFIO device {NVIDIA_VFIO_DEVICE} not found"); return false; } let driver_path = format!("{NVIDIA_VFIO_DEVICE}/driver"); if let Ok(driver) = 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; } true } fn largest_nvidia_prefetchable_memory_bar() -> Option { let resource_path = format!("{NVIDIA_VFIO_DEVICE}/resource"); let resource = match fs::read_to_string(&resource_path) { Ok(resource) => resource, Err(e) => { println!("SKIPPED: failed to read {resource_path}: {e}"); return None; } }; 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; }; 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; }; if flags & IORESOURCE_MEM == 0 || end < start || (start == 0 && end == 0) { continue; } if flags & IORESOURCE_PREFETCH == 0 { continue; } let size = end - start + 1; if size > selected_size { selected_bar = Some(index as u8); selected_size = size; } } 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() } } 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(); let r = panic::catch_unwind(|| { 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())); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_nvidia_card_memory_hotplug_acpi() { test_nvidia_card_memory_hotplug("acpi", false); } #[test] fn test_nvidia_card_memory_hotplug_virtio_mem() { test_nvidia_card_memory_hotplug("virtio-mem", false); } #[test] fn test_iommufd_nvidia_card_memory_hotplug_acpi() { test_nvidia_card_memory_hotplug("acpi", true); } #[test] fn test_iommufd_nvidia_card_memory_hotplug_virtio_mem() { test_nvidia_card_memory_hotplug("virtio-mem", true); } 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // 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 the VFIO device works after hotplug assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu())); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_nvidia_card_pci_hotplug() { test_nvidia_card_pci_hotplug_common(false); } #[test] #[ignore = "See #8548"] 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([ "--device", format!("path={NVIDIA_VFIO_DEVICE},iommu=on").as_str(), ]) .args(["--api-socket", &api_socket]) .default_disks() .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // 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()); }); 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); } // Pass the NVIDIA card to the guest via an externally-opened // /dev/vfio/devices/ FD instead of a sysfs path, and verify it // survives a guest reboot. Mirrors `_test_tap_from_fd` for VFIO. #[test] fn test_iommufd_nvidia_card_reboot_from_fd() { 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); // Discover the cdev for the NVIDIA card. The directory has a // single entry whose name (e.g. "vfio0") is also the basename // of the corresponding /dev/vfio/devices/ node. let vfio_dev_dir = format!("{NVIDIA_VFIO_DEVICE}/vfio-dev"); let cdev_name = fs::read_dir(&vfio_dev_dir) .unwrap_or_else(|e| panic!("read_dir({vfio_dev_dir}) failed: {e}")) .next() .expect("no vfio-dev entry") .unwrap() .file_name(); let cdev_path = PathBuf::from("/dev/vfio/devices").join(&cdev_name); let cdev_file = OpenOptions::new() .read(true) .write(true) .open(&cdev_path) .unwrap_or_else(|e| panic!("open({cdev_path:?}) failed: {e}")); let iommufd_file = OpenOptions::new() .read(true) .write(true) .open("/dev/iommu") .unwrap_or_else(|e| panic!("open(/dev/iommu) failed: {e}")); // OpenOptions sets FD_CLOEXEC by default; clear it for both fds // so they're inherited by the VMM child via spawn(). for f in [&cdev_file, &iommufd_file] { // SAFETY: FFI call to fcntl on a valid fd we own. let ret = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_SETFD, 0) }; assert!( ret >= 0, "fcntl(F_SETFD, 0) failed: {}", io::Error::last_os_error() ); } let platform = format!( "{},iommufd_fd={}", platform_cfg(true), iommufd_file.as_raw_fd() ); let mut child = GuestCommand::new(&guest) .args(["--cpus", "boot=4"]) .args(["--memory", "size=1G"]) .args(["--platform", &platform]) .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) .args([ "--device", format!("fd={},iommu=on", cdev_file.as_raw_fd()).as_str(), ]) .args(["--api-socket", &api_socket]) .default_disks() .default_net() .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // VFIO device works after boot from an externally-opened FD. assert!(guest.check_nvidia_gpu()); guest.reboot_linux(0); // Both FDs survive a VM reboot assert!(guest.check_nvidia_gpu()); }); drop(cdev_file); drop(iommufd_file); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } 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) ); 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert!( guest .ssh_command("sudo dmesg") .unwrap() .contains("input address: 42 bits") ); // Check the VFIO device works after boot guest.check_nvidia_gpu(); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_nvidia_card_iommu_address_width() { test_nvidia_card_iommu_address_width_common(false); } #[test] fn test_iommufd_nvidia_card_iommu_address_width() { test_nvidia_card_iommu_address_width_common(true); } fn test_nvidia_card_x_exclude_mmap_bars_common(iommufd: bool) { if !nvidia_vfio_device_ready() { return; } let Some(bar) = largest_nvidia_prefetchable_memory_bar() else { return; }; let disk_config = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); assert!(wait_until(Duration::from_secs(10), || guest.check_nvidia_gpu())); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); 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); } #[test] fn test_nvidia_card_x_exclude_mmap_bars() { test_nvidia_card_x_exclude_mmap_bars_common(false); } #[test] #[ignore = "See #8549"] fn test_iommufd_nvidia_card_x_exclude_mmap_bars() { test_nvidia_card_x_exclude_mmap_bars_common(true); } fn test_nvidia_guest_numa_generic_initiator_common(iommufd: bool) { if !nvidia_vfio_device_ready() { return; } 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); // 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(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // 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"]), ); // 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(); assert_eq!( has_gi, "2", "Generic Initiator support should be detected by kernel" ); // 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(); assert_eq!( srat_check, "exists", "SRAT table should exist in guest firmware" ); // 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(); assert_eq!( srat_has_type5, "found", "SRAT table should contain Generic Initiator Affinity Structure (Type 5, Length 0x20/32)" ); }); 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); } } #[cfg(target_arch = "aarch64")] mod aarch64_acpi { use crate::*; #[test] fn test_simple_launch_acpi() { let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); vec![Box::new(jammy)].drain(..).for_each(|disk_config| { let guest = Guest::new(disk_config); let mut child = GuestCommand::new(&guest) .default_cpus() .default_memory() .args(["--kernel", edk2_path().to_str().unwrap()]) .default_disks() .default_net() .args(["--serial", "tty", "--console", "off"]) .capture_output() .spawn() .unwrap(); let r = 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() > 400_000); assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000"); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); }); } #[test] fn test_guest_numa_nodes_acpi() { _test_guest_numa_nodes(true); } #[test] fn test_cpu_topology_421_acpi() { test_cpu_topology(4, 2, 1, true); } #[test] fn test_cpu_topology_142_acpi() { test_cpu_topology(1, 4, 2, true); } #[test] fn test_cpu_topology_262_acpi() { test_cpu_topology(2, 6, 2, true); } #[test] fn test_power_button_acpi() { 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] fn test_cache_topology() { let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); vec![Box::new(jammy)].drain(..).for_each(|disk_config| { let guest = Guest::new(disk_config); let mut child = GuestCommand::new(&guest) .default_cpus() .default_memory() .args(["--kernel", edk2_path().to_str().unwrap()]) .default_disks() .default_net() .args(["--serial", "tty", "--console", "off"]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); let cache_levels = ["L1d", "L1i", "L2", "L3"]; for level in cache_levels { let host_sz = exec_host_command_output(&format!( "lscpu -C=NAME,ONE-SIZE | grep \"{level}\" | awk '{{print $2}}'" )); let guest_sz = guest .ssh_command(&format!( "lscpu -C=NAME,ONE-SIZE | grep \"{level}\" | awk '{{print $2}}'" )) .unwrap(); assert_eq!( String::from_utf8_lossy(&host_sz.stdout).trim(), guest_sz.trim(), "Cache size mismatch for {level}" ); } }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); }); } } 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 { let upper_limit = limit * (1_f64 + difference); let lower_limit = limit * (1_f64 - difference); if measured > lower_limit && measured < upper_limit { return true; } eprintln!( "\n\n==== Start 'check_rate_limit' failed ==== \ \n\nmeasured={measured}, , lower_limit={lower_limit}, upper_limit={upper_limit} \ \n\n==== End 'check_rate_limit' failed ====\n\n" ); false } fn _test_rate_limiter_net(rx: bool) { let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); let num_queues = 2; let queue_size = 256; 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.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, bw_refill_time, ); let mut child = GuestCommand::new(&guest) .args(["--cpus", &format!("boot={}", num_queues / 2)]) .args(["--memory", "size=1G"]) .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) .default_disks() .args(["--net", net_params.as_str()]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { 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)); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_rate_limiter_net_rx() { _test_rate_limiter_net(true); } #[test] fn test_rate_limiter_net_tx() { _test_rate_limiter_net(false); } fn _test_rate_limiter_block(bandwidth: bool, num_queues: u32) { let fio_ops = FioOps::RandRW; let bw_size = if bandwidth { 104857600_u64 // bytes } else { 1000_u64 // I/O }; let bw_refill_time = 1000; // ms let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64; 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() ); let test_blk_params = if bandwidth { format!( "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_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=1G"]) .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={}", guest.disk_config.disk(DiskType::CloudInit).unwrap() ) .as_str(), test_blk_params.as_str(), ]) .default_net() .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { 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={BLOCK_RATE_LIMITER_RUNTIME} \ --ramp_time={BLOCK_RATE_LIMITER_RAMP_TIME} --numjobs={num_queues}", ); let output = guest.ssh_command(&fio_command).unwrap(); // Parse fio output let measured_rate = if bandwidth { parse_fio_output(&output, &fio_ops, num_queues).unwrap() } else { parse_fio_output_iops(&output, &fio_ops, num_queues).unwrap() }; assert!(check_rate_limit(measured_rate, limit_rate, 0.1)); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } fn _test_rate_limiter_group_block(bandwidth: bool, num_queues: u32, num_disks: u32) { let fio_ops = FioOps::RandRW; let bw_size = if bandwidth { 104857600_u64 // bytes } else { 1000_u64 // I/O }; let bw_refill_time = 1000; // ms let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64; 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_one_time_burst=0,bw_refill_time={bw_refill_time}" ) } else { format!( "id=group0,ops_size={bw_size},ops_one_time_burst=0,ops_refill_time={bw_refill_time}" ) }; let mut disk_args = vec![ "--disk".to_string(), format!( "path={}", guest.disk_config.disk(DiskType::OperatingSystem).unwrap() ), format!( "path={}", guest.disk_config.disk(DiskType::CloudInit).unwrap() ), ]; for i in 0..num_disks { let test_img_path = String::from( test_img_dir .as_path() .join(format!("blk{i}.img")) .to_str() .unwrap(), ); 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,image_type=raw" )); } let mut child = GuestCommand::new(&guest) .args(["--cpus", &format!("boot={}", num_queues * num_disks)]) .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]) .args(disk_args) .default_net() .args(["--api-socket", &api_socket]) .capture_output() .spawn() .unwrap(); let r = panic::catch_unwind(|| { 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={BLOCK_RATE_LIMITER_RUNTIME} \ --ramp_time={BLOCK_RATE_LIMITER_RAMP_TIME} --numjobs={num_queues}", ); // Generate additional argument for each disk: // --name=job0 --filename=/dev/vdc \ // --name=job1 --filename=/dev/vdd \ // --name=job2 --filename=/dev/vde \ // ... for i in 0..num_disks { let c: char = 'c'; let arg = format!( " --name=job{i} --filename=/dev/vd{}", char::from_u32((c as u32) + i).unwrap() ); fio_command += &arg; } let output = guest.ssh_command(&fio_command).unwrap(); // Parse fio output let measured_rate = if bandwidth { parse_fio_output(&output, &fio_ops, num_queues * num_disks).unwrap() } else { parse_fio_output_iops(&output, &fio_ops, num_queues * num_disks).unwrap() }; assert!(check_rate_limit(measured_rate, limit_rate, 0.2)); }); let _ = child.kill(); let output = child.wait_with_output().unwrap(); handle_child_output(r, &output); } #[test] fn test_rate_limiter_block_bandwidth() { _test_rate_limiter_block(true, 1); _test_rate_limiter_block(true, 2); } #[test] fn test_rate_limiter_group_block_bandwidth() { _test_rate_limiter_group_block(true, 1, 1); _test_rate_limiter_group_block(true, 2, 1); _test_rate_limiter_group_block(true, 1, 2); _test_rate_limiter_group_block(true, 2, 2); } #[test] fn test_rate_limiter_block_iops() { _test_rate_limiter_block(false, 1); _test_rate_limiter_block(false, 2); } #[test] fn test_rate_limiter_group_block_iops() { _test_rate_limiter_group_block(false, 1, 1); _test_rate_limiter_group_block(false, 2, 1); _test_rate_limiter_group_block(false, 1, 2); _test_rate_limiter_group_block(false, 2, 2); } } #[cfg(not(target_arch = "riscv64"))] mod fw_cfg { use crate::*; #[test] 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"); 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); // Wait a while for guest thread::sleep(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] 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 = panic::catch_unwind(|| { guest.wait_vm_boot().unwrap(); thread::sleep(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); } } cloud-hypervisor-53.0/cloud-hypervisor/tests/integration_cvm.rs000066400000000000000000000230331522476750100252070ustar00rootroot00000000000000// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 // #![cfg(any(devcli_testenv, clippy))] #![expect(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(feature = "sev_snp")] 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] #[cfg(not(feature = "kvm"))] 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] #[cfg(not(feature = "kvm"))] fn test_dmi_uuid() { let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); _test_dmi_uuid(&guest); } #[test] #[cfg(not(feature = "kvm"))] fn test_dmi_oem_strings() { let guest = basic_cvm_guest!(JAMMY_IMAGE_NAME); _test_dmi_oem_strings(&guest); } #[test] #[cfg(not(feature = "kvm"))] 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] #[cfg(not(feature = "kvm"))] 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); } } cloud-hypervisor-53.0/devices/000077500000000000000000000000001522476750100164125ustar00rootroot00000000000000cloud-hypervisor-53.0/devices/Cargo.toml000066400000000000000000000026021522476750100203420ustar00rootroot00000000000000[package] authors = ["The Chromium OS Authors"] edition.workspace = true name = "devices" rust-version.workspace = true version = "0.1.0" [dependencies] acpi_tables = { workspace = true } anyhow = { workspace = true } arch = { path = "../arch" } bitfield-struct = { version = "0.13.0", optional = true } bitflags = { workspace = true } byteorder = { workspace = true } event_monitor = { path = "../event_monitor" } hypervisor = { path = "../hypervisor" } 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 = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tpm = { path = "../tpm" } vm-allocator = { path = "../vm-allocator" } vm-device = { path = "../vm-device" } vm-memory = { workspace = true, features = [ "backend-atomic", "backend-bitmap", "backend-mmap", ] } vm-migration = { path = "../vm-migration" } vmm-sys-util = { workspace = true } zerocopy = { version = "0.8.50", 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 cloud-hypervisor-53.0/devices/src/000077500000000000000000000000001522476750100172015ustar00rootroot00000000000000cloud-hypervisor-53.0/devices/src/acpi.rs000066400000000000000000000222321522476750100204640ustar00rootroot00000000000000// Copyright © 2019 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Barrier}; use std::time::{Duration, Instant}; use std::{io, thread}; 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; use super::AcpiNotificationFlags; pub const GED_DEVICE_ACPI_SIZE: usize = 0x1; /// A device for handling ACPI shutdown and reboot pub struct AcpiShutdownDevice { guest_exit_evt: EventFd, reset_evt: EventFd, vcpus_kill_signalled: Arc, } impl AcpiShutdownDevice { /// Constructs a device that will signal the given event when the guest requests it. pub fn new( guest_exit_evt: EventFd, reset_evt: EventFd, vcpus_kill_signalled: Arc, ) -> AcpiShutdownDevice { AcpiShutdownDevice { guest_exit_evt, reset_evt, vcpus_kill_signalled, } } } // Same I/O port used for shutdown and reboot impl BusDevice for AcpiShutdownDevice { // Spec has all fields as zero fn read(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { if data.len() != 1 { warn!("Invalid sized read of ACPI shutdown device: {}", data.len()); return; } data.fill(0); } fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { if data.len() != 1 { warn!( "Invalid sized write of ACPI shutdown device: {}", data.len() ); return None; } if data[0] == 1 { info!("ACPI Reboot signalled"); if let Err(e) = self.reset_evt.write(1) { 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) { // This is more effective than thread::yield_now() at // avoiding a priority inversion with the VMM thread thread::sleep(Duration::from_millis(1)); } } // The ACPI DSDT table specifies the S5 sleep state (shutdown) as value 5 const S5_SLEEP_VALUE: u8 = 5; const SLEEP_STATUS_EN_BIT: u8 = 5; 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.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) { // This is more effective than thread::yield_now() at // avoiding a priority inversion with the VMM thread thread::sleep(Duration::from_millis(1)); } } None } } /// A device for handling ACPI GED event generation pub struct AcpiGedDevice { interrupt: Arc, notification_type: AcpiNotificationFlags, ged_irq: u32, address: GuestAddress, } impl AcpiGedDevice { pub fn new( interrupt: Arc, ged_irq: u32, address: GuestAddress, ) -> AcpiGedDevice { AcpiGedDevice { interrupt, notification_type: AcpiNotificationFlags::NO_DEVICES_CHANGED, ged_irq, address, } } pub fn notify(&mut self, notification_type: AcpiNotificationFlags) -> io::Result<()> { self.notification_type |= notification_type; self.interrupt.trigger(0) } pub fn irq(&self) -> u32 { self.ged_irq } } // I/O port reports what type of notification was made impl BusDevice for AcpiGedDevice { // Spec has all fields as zero fn read(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { if data.len() != 1 { warn!("Invalid sized read of ACPI GED device: {}", data.len()); return; } data[0] = self.notification_type.bits(); self.notification_type = AcpiNotificationFlags::NO_DEVICES_CHANGED; } } impl Aml for AcpiGedDevice { fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { aml::Device::new( "_SB_.GEC_".into(), vec![ &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0A06")), &aml::Name::new("_UID".into(), &"Generic Event Controller"), &aml::Name::new( "_CRS".into(), &aml::ResourceTemplate::new(vec![&aml::AddressSpace::new_memory( aml::AddressSpaceCacheable::NotCacheable, true, self.address.0, self.address.0 + GED_DEVICE_ACPI_SIZE as u64 - 1, None, )]), ), &aml::OpRegion::new( "GDST".into(), aml::OpRegionSpace::SystemMemory, &(self.address.0 as usize), &GED_DEVICE_ACPI_SIZE, ), &aml::Field::new( "GDST".into(), aml::FieldAccessType::Byte, aml::FieldLockRule::NoLock, aml::FieldUpdateRule::WriteAsZeroes, vec![aml::FieldEntry::Named(*b"GDAT", 8)], ), &aml::Method::new( "ESCN".into(), 0, true, vec![ &aml::Store::new(&aml::Local(0), &aml::Path::new("GDAT")), &aml::And::new(&aml::Local(1), &aml::Local(0), &aml::ONE), &aml::If::new( &aml::Equal::new(&aml::Local(1), &aml::ONE), vec![&aml::MethodCall::new("\\_SB_.CPUS.CSCN".into(), vec![])], ), &aml::And::new(&aml::Local(1), &aml::Local(0), &2usize), &aml::If::new( &aml::Equal::new(&aml::Local(1), &2usize), vec![&aml::MethodCall::new("\\_SB_.MHPC.MSCN".into(), vec![])], ), &aml::And::new(&aml::Local(1), &aml::Local(0), &4usize), &aml::If::new( &aml::Equal::new(&aml::Local(1), &4usize), vec![&aml::MethodCall::new("\\_SB_.PHPR.PSCN".into(), vec![])], ), &aml::And::new(&aml::Local(1), &aml::Local(0), &8usize), &aml::If::new( &aml::Equal::new(&aml::Local(1), &8usize), vec![&aml::Notify::new( &aml::Path::new("\\_SB_.PWRB"), &0x80usize, )], ), ], ), ], ) .to_aml_bytes(sink); aml::Device::new( "_SB_.GED_".into(), vec![ &aml::Name::new("_HID".into(), &"ACPI0013"), &aml::Name::new("_UID".into(), &aml::ZERO), &aml::Name::new( "_CRS".into(), &aml::ResourceTemplate::new(vec![&aml::Interrupt::new( true, true, false, false, self.ged_irq, )]), ), &aml::Method::new( "_EVT".into(), 1, true, vec![&aml::MethodCall::new("\\_SB_.GEC_.ESCN".into(), vec![])], ), ], ) .to_aml_bytes(sink); } } pub struct AcpiPmTimerDevice { start: Instant, } impl AcpiPmTimerDevice { pub fn new() -> Self { Self { start: Instant::now(), } } } impl Default for AcpiPmTimerDevice { fn default() -> Self { Self::new() } } impl BusDevice for AcpiPmTimerDevice { fn read(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { if data.len() != size_of::() { warn!("Invalid sized read of PM timer: {}", data.len()); return; } let now = Instant::now(); let since = now.duration_since(self.start); let nanos = since.as_nanos(); const PM_TIMER_FREQUENCY_HZ: u128 = 3_579_545; const NANOS_PER_SECOND: u128 = 1_000_000_000; let counter = (nanos * PM_TIMER_FREQUENCY_HZ) / NANOS_PER_SECOND; let counter: u32 = (counter & 0xffff_ffff) as u32; data.copy_from_slice(&counter.to_le_bytes()); } } cloud-hypervisor-53.0/devices/src/aia.rs000066400000000000000000000106721522476750100203070ustar00rootroot00000000000000// Copyright © 2024 Institute of Software, CAS. All rights reserved. // Copyright 2020, ARM Limited. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::result; use std::sync::{Arc, Mutex}; use arch::layout; use hypervisor::arch::riscv64::aia::{Vaia, VaiaConfig}; use hypervisor::{AiaState, CpuState}; use vm_device::interrupt::{ InterruptIndex, InterruptManager, InterruptSourceConfig, InterruptSourceGroup, LegacyIrqSourceConfig, MsiIrqGroupConfig, }; 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. pub const IRQ_LEGACY_BASE: usize = layout::IRQ_BASE as usize; pub const IRQ_LEGACY_COUNT: usize = 32; // TODO: AIA snapshotting is not yet completed. pub const _AIA_SNAPSHOT_ID: &str = ""; // Aia (Advance Interrupt Architecture) struct provides all the functionality of a // AIA device. It wraps a hypervisor-emulated AIA device (Vaia) provided by the // `hypervisor` crate. // Aia struct also implements InterruptController to provide interrupt delivery // service. pub struct Aia { interrupt_source_group: Arc, // The hypervisor agnostic virtual AIA vaia: Arc>, } impl Aia { #[expect(clippy::needless_pass_by_value)] pub fn new( vcpu_count: u32, interrupt_manager: Arc>, vm: Arc, ) -> Result { let interrupt_source_group = interrupt_manager .create_group(MsiIrqGroupConfig { base: IRQ_LEGACY_BASE as InterruptIndex, count: IRQ_LEGACY_COUNT as InterruptIndex, }) .map_err(Error::CreateInterruptSourceGroup)?; 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, vaia, }; aia.enable()?; Ok(aia) } pub fn restore_vaia( &mut self, state: Option, _saved_vcpu_states: &[CpuState], ) -> Result<()> { self.vaia .clone() .lock() .unwrap() .set_state(&state.unwrap()) .map_err(Error::RestoreAia) } fn enable(&self) -> Result<()> { // Set irqfd for legacy interrupts self.interrupt_source_group .enable() .map_err(Error::EnableInterrupt)?; // Set irq_routing for legacy interrupts. // irqchip: Hardcode to 0 as we support only 1 APLIC // pin: Use irq number as pin for i in IRQ_LEGACY_BASE..(IRQ_LEGACY_BASE + IRQ_LEGACY_COUNT) { let config = LegacyIrqSourceConfig { irqchip: 0, pin: (i - IRQ_LEGACY_BASE) as u32, }; self.interrupt_source_group .update( i as InterruptIndex, InterruptSourceConfig::LegacyIrq(config), false, false, ) .map_err(Error::EnableInterrupt)?; } self.interrupt_source_group .set_gsi() .map_err(Error::EnableInterrupt)?; Ok(()) } /// Default config implied by arch::layout pub fn create_default_config(vcpu_count: u64) -> VaiaConfig { VaiaConfig { vcpu_count: vcpu_count as u32, aplic_addr: layout::APLIC_START.raw_value(), imsic_addr: layout::IMSIC_START.raw_value(), nr_irqs: layout::IRQ_NUM, } } pub fn get_vaia(&mut self) -> Result>> { Ok(self.vaia.clone()) } } impl InterruptController for Aia { // This should be called anytime an interrupt needs to be injected into the // running guest. fn service_irq(&mut self, irq: usize) -> Result<()> { self.interrupt_source_group .trigger(irq as InterruptIndex) .map_err(Error::TriggerInterrupt)?; Ok(()) } fn notifier(&self, irq: usize) -> Option { self.interrupt_source_group.notifier(irq as InterruptIndex) } } impl Snapshottable for Aia {} impl Pausable for Aia {} impl Transportable for Aia {} impl Migratable for Aia {} cloud-hypervisor-53.0/devices/src/debug_console.rs000066400000000000000000000031611522476750100223600ustar00rootroot00000000000000// Copyright © 2023 Cyberus Technology // // SPDX-License-Identifier: Apache-2.0 // //! Module for [`DebugConsole`]. 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}; /// I/O-port. pub const DEFAULT_PORT: u64 = 0xe9; /// Emulates a debug console similar to the QEMU debugcon device. This device /// is stateless and only prints the bytes (usually text) that are written to /// it. /// /// This device is only available on x86. /// /// Reference: /// - https://github.com/qemu/qemu/blob/master/hw/char/debugcon.c /// - https://phip1611.de/blog/how-to-use-qemus-debugcon-feature-and-write-to-a-file/ pub struct DebugConsole { id: String, out: Box, } impl DebugConsole { pub fn new(id: String, out: Box) -> Self { Self { id, out } } } impl BusDevice for DebugConsole { fn read(&mut self, _base: u64, _offset: u64, _data: &mut [u8]) {} fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { if let Err(e) = self.out.write_all(data) { // unlikely error!("debug-console: failed writing data: {e:?}"); } None } } impl Snapshottable for DebugConsole { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> Result { Snapshot::new_from_state(&()) } } impl Pausable for DebugConsole {} impl Transportable for DebugConsole {} impl Migratable for DebugConsole {} cloud-hypervisor-53.0/devices/src/gic.rs000066400000000000000000000131131522476750100203100ustar00rootroot00000000000000// Copyright 2020, ARM Limited. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::result; use std::sync::{Arc, Mutex}; use anyhow::anyhow; use arch::layout; use hypervisor::CpuState; use hypervisor::arch::aarch64::gic::{GicState, Vgic, VgicConfig}; use vm_device::interrupt::{ InterruptIndex, InterruptManager, InterruptSourceConfig, InterruptSourceGroup, LegacyIrqSourceConfig, MsiIrqGroupConfig, }; 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. pub const IRQ_LEGACY_BASE: usize = layout::IRQ_BASE as usize; pub const IRQ_LEGACY_COUNT: usize = 32; pub const GIC_SNAPSHOT_ID: &str = "gic-v3-its"; // Gic (Generic Interrupt Controller) struct provides all the functionality of a // GIC device. It wraps a hypervisor-emulated GIC device (Vgic) provided by the // `hypervisor` crate. // Gic struct also implements InterruptController to provide interrupt delivery // service. pub struct Gic { interrupt_source_group: Arc, // The hypervisor agnostic virtual GIC vgic: Option>>, } impl Gic { #[expect(clippy::needless_pass_by_value)] pub fn new( vcpu_count: u32, interrupt_manager: Arc>, vm: Arc, ) -> Result { let interrupt_source_group = interrupt_manager .create_group(MsiIrqGroupConfig { base: IRQ_LEGACY_BASE as InterruptIndex, count: IRQ_LEGACY_COUNT as InterruptIndex, }) .map_err(Error::CreateInterruptSourceGroup)?; 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, vgic: Some(vgic), }; gic.enable()?; Ok(gic) } pub fn restore_vgic( &mut self, state: Option, saved_vcpu_states: &[CpuState], ) -> Result<()> { self.set_gicr_typers(saved_vcpu_states); self.vgic .clone() .unwrap() .lock() .unwrap() .set_state(&state.unwrap()) .map_err(Error::RestoreGic) } fn enable(&self) -> Result<()> { // Set irqfd for legacy interrupts self.interrupt_source_group .enable() .map_err(Error::EnableInterrupt)?; // Set irq_routing for legacy interrupts. // irqchip: Hardcode to 0 as we support only 1 GIC // pin: Use irq number as pin for i in IRQ_LEGACY_BASE..(IRQ_LEGACY_BASE + IRQ_LEGACY_COUNT) { let config = LegacyIrqSourceConfig { irqchip: 0, pin: (i - IRQ_LEGACY_BASE) as u32, }; self.interrupt_source_group .update( i as InterruptIndex, InterruptSourceConfig::LegacyIrq(config), false, false, ) .map_err(Error::EnableInterrupt)?; } self.interrupt_source_group .set_gsi() .map_err(Error::EnableInterrupt)?; Ok(()) } /// Default config implied by arch::layout pub fn create_default_config(vcpu_count: u64) -> VgicConfig { let redists_size = layout::GIC_V3_REDIST_SIZE * vcpu_count; let redists_addr = layout::GIC_V3_DIST_START.raw_value() - redists_size; VgicConfig { vcpu_count, dist_addr: layout::GIC_V3_DIST_START.raw_value(), dist_size: layout::GIC_V3_DIST_SIZE, redists_addr, redists_size, msi_addr: redists_addr - layout::GIC_V3_ITS_SIZE, msi_size: layout::GIC_V3_ITS_SIZE, nr_irqs: layout::IRQ_NUM, } } pub fn get_vgic(&mut self) -> Result>> { Ok(self.vgic.clone().unwrap()) } pub fn set_gicr_typers(&mut self, vcpu_states: &[CpuState]) { let vgic = self.vgic.as_ref().unwrap().clone(); vgic.lock().unwrap().set_gicr_typers(vcpu_states); } } impl InterruptController for Gic { // This should be called anytime an interrupt needs to be injected into the // running guest. fn service_irq(&mut self, irq: usize) -> Result<()> { self.interrupt_source_group .trigger(irq as InterruptIndex) .map_err(Error::TriggerInterrupt)?; Ok(()) } fn notifier(&self, irq: usize) -> Option { self.interrupt_source_group.notifier(irq as InterruptIndex) } } impl Snapshottable for Gic { fn id(&self) -> String { GIC_SNAPSHOT_ID.to_string() } fn snapshot(&mut self) -> result::Result { let vgic = self.vgic.as_ref().unwrap().clone(); let state = vgic.lock().unwrap().state().unwrap(); Snapshot::new_from_state(&state) } } impl Pausable for Gic { fn pause(&mut self) -> result::Result<(), MigratableError> { // 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:?}",)) })?; Ok(()) } } impl Transportable for Gic {} impl Migratable for Gic {} cloud-hypervisor-53.0/devices/src/interrupt_controller.rs000066400000000000000000000047111522476750100240510ustar00rootroot00000000000000// Copyright © 2024 Institute of Software, CAS. All rights reserved. // Copyright 2020, ARM Limited. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::{io, result}; #[cfg(target_arch = "aarch64")] use hypervisor::arch::aarch64::gic; use thiserror::Error; use vmm_sys_util::eventfd::EventFd; #[derive(Debug, Error)] pub enum Error { /// Invalid trigger mode. #[error("Invalid trigger mode")] InvalidTriggerMode, /// Invalid delivery mode. #[error("Invalid delivery mode")] InvalidDeliveryMode, /// Failed creating the interrupt source group. #[error("Failed creating the interrupt source group")] CreateInterruptSourceGroup(#[source] io::Error), /// Failed triggering the interrupt. #[error("Failed triggering the interrupt")] TriggerInterrupt(#[source] io::Error), /// Failed masking the interrupt. #[error("Failed masking the interrupt")] MaskInterrupt(#[source] io::Error), /// Failed unmasking the interrupt. #[error("Failed unmasking the interrupt")] UnmaskInterrupt(#[source] io::Error), /// Failed updating the interrupt. #[error("Failed updating the interrupt")] UpdateInterrupt(#[source] io::Error), /// Failed enabling the interrupt. #[error("Failed enabling the interrupt")] EnableInterrupt(#[source] io::Error), #[cfg(target_arch = "aarch64")] /// Failed creating GIC device. #[error("Failed creating GIC device")] CreateGic(#[source] hypervisor::HypervisorVmError), #[cfg(target_arch = "aarch64")] /// Failed restoring GIC device. #[error("Failed restoring GIC device")] RestoreGic(#[source] gic::Error), #[cfg(target_arch = "riscv64")] /// Failed creating AIA device. #[error("Failed creating AIA device")] CreateAia(#[source] hypervisor::HypervisorVmError), #[cfg(target_arch = "riscv64")] /// Failed restoring AIA device. #[error("Failed restoring AIA device")] RestoreAia(#[source] hypervisor::arch::riscv64::aia::Error), } type Result = result::Result; // Introduce trait InterruptController to uniform the interrupt // service provided for devices. // Device manager uses this trait without caring whether it is a // IOAPIC (X86), GIC (Arm) or AIA (RISC-V). pub trait InterruptController: Send { fn service_irq(&mut self, irq: usize) -> Result<()>; #[cfg(target_arch = "x86_64")] fn end_of_interrupt(&mut self, vec: u8); fn notifier(&self, irq: usize) -> Option; } cloud-hypervisor-53.0/devices/src/ioapic.rs000066400000000000000000000347631522476750100210300ustar00rootroot00000000000000// Copyright 2019 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 file. // // Copyright © 2019 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause // // Implementation of an intel 82093AA Input/Output Advanced Programmable Interrupt Controller // See https://pdos.csail.mit.edu/6.828/2016/readings/ia32/ioapic.pdf for a specification. 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_memory::GuestAddress; 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; // I/O REDIRECTION TABLE REGISTER // // There are 24 I/O Redirection Table entry registers. Each register is a // dedicated entry for each interrupt input signal. Each register is 64 bits // split between two 32 bits registers as follow: // // 63-56: Destination Field - R/W // 55-17: Reserved // 16: Interrupt Mask - R/W // 15: Trigger Mode - R/W // 14: Remote IRR - RO // 13: Interrupt Input Pin Polarity - R/W // 12: Delivery Status - RO // 11: Destination Mode - R/W // 10-8: Delivery Mode - R/W // 7-0: Interrupt Vector - R/W pub type RedirectionTableEntry = u64; fn vector(entry: RedirectionTableEntry) -> u8 { (entry & 0xffu64) as u8 } fn delivery_mode(entry: RedirectionTableEntry) -> u8 { ((entry >> 8) & 0x7u64) as u8 } fn destination_mode(entry: RedirectionTableEntry) -> u8 { ((entry >> 11) & 0x1u64) as u8 } fn remote_irr(entry: RedirectionTableEntry) -> u8 { ((entry >> 14) & 0x1u64) as u8 } fn trigger_mode(entry: RedirectionTableEntry) -> u8 { ((entry >> 15) & 0x1u64) as u8 } fn interrupt_mask(entry: RedirectionTableEntry) -> u8 { ((entry >> 16) & 0x1u64) as u8 } fn destination_field(entry: RedirectionTableEntry) -> u8 { // When the destination mode is physical, the destination field should only // be defined through bits 56-59, as defined in the IOAPIC specification. // But from the APIC specification, the APIC ID is always defined on 8 bits // no matter which destination mode is selected. That's why we always // retrieve the destination field based on bits 56-63. ((entry >> 56) & 0xffu64) as u8 } fn set_delivery_status(entry: &mut RedirectionTableEntry, val: u8) { // Clear bit 12 *entry &= 0xffff_ffff_ffff_efff; // Set it with the expected value *entry |= u64::from(val & 0x1) << 12; } fn set_remote_irr(entry: &mut RedirectionTableEntry, val: u8) { // Clear bit 14 *entry &= 0xffff_ffff_ffff_bfff; // Set it with the expected value *entry |= u64::from(val & 0x1) << 14; } pub const NUM_IOAPIC_PINS: usize = 24; const IOAPIC_VERSION_ID: u32 = 0x0017_0011; // Constants for IOAPIC direct register offset const IOAPIC_REG_ID: u8 = 0x00; const IOAPIC_REG_VERSION: u8 = 0x01; const IOAPIC_REG_ARBITRATION_ID: u8 = 0x02; // Register offsets const IOREGSEL_OFF: u8 = 0x0; const IOWIN_OFF: u8 = 0x10; const IOWIN_SCALE: u8 = 0x2; const REG_MAX_OFFSET: u8 = IOWIN_OFF + (NUM_IOAPIC_PINS as u8 * 2) - 1; #[repr(u8)] enum TriggerMode { Edge = 0, Level = 1, } #[repr(u8)] enum DeliveryMode { Fixed = 0b000, Lowest = 0b001, Smi = 0b010, // System management interrupt RemoteRead = 0b011, // This is no longer supported by intel. Nmi = 0b100, // Non maskable interrupt Init = 0b101, Startup = 0b110, External = 0b111, } /// Given an offset that was read from/written to, return a tuple of the relevant IRQ and whether /// the offset refers to the high bits of that register. fn decode_irq_from_selector(selector: u8) -> (usize, bool) { ( ((selector - IOWIN_OFF) / IOWIN_SCALE) as usize, selector & 1 != 0, ) } pub struct Ioapic { id: String, id_reg: u32, reg_sel: u32, reg_entries: [RedirectionTableEntry; NUM_IOAPIC_PINS], used_entries: [bool; NUM_IOAPIC_PINS], apic_address: GuestAddress, interrupt_source_group: Arc, } #[derive(Serialize, Deserialize)] pub struct IoapicState { id_reg: u32, reg_sel: u32, reg_entries: [RedirectionTableEntry; NUM_IOAPIC_PINS], used_entries: [bool; NUM_IOAPIC_PINS], apic_address: u64, } impl BusDevice for Ioapic { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { if data.len() != size_of::() { warn!("Invalid read size on IOAPIC: {}", data.len()); return; } 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}"); return; } }; LittleEndian::write_u32(data, value); } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { if data.len() != size_of::() { warn!("Invalid write size on IOAPIC: {}", data.len()); return None; } debug!("IOAPIC_W @ offset 0x{offset:x}"); let value = LittleEndian::read_u32(data); match offset as u8 { IOREGSEL_OFF => self.reg_sel = value, IOWIN_OFF => self.ioapic_write(value), _ => { error!("IOAPIC: failed writing at offset {offset}"); } } None } } impl Ioapic { pub fn new( id: String, apic_address: GuestAddress, interrupt_manager: &dyn InterruptManager, state: Option<&IoapicState>, ) -> Result { let interrupt_source_group = interrupt_manager .create_group(MsiIrqGroupConfig { base: 0, count: NUM_IOAPIC_PINS as InterruptIndex, }) .map_err(Error::CreateInterruptSourceGroup)?; let (id_reg, reg_sel, reg_entries, used_entries, apic_address) = if let Some(state) = &state { ( state.id_reg, state.reg_sel, state.reg_entries, state.used_entries, GuestAddress(state.apic_address), ) } else { ( 0, 0, [0x10000; NUM_IOAPIC_PINS], [false; NUM_IOAPIC_PINS], apic_address, ) }; // The IOAPIC is created with entries already masked. The guest will be // in charge of unmasking them if/when necessary. let ioapic = Ioapic { id, id_reg, reg_sel, reg_entries, used_entries, apic_address, interrupt_source_group, }; // When restoring the Ioapic, we must enable used entries. if state.is_some() { for (irq, entry) in ioapic.used_entries.iter().enumerate() { if *entry { ioapic.update_entry(irq, false)?; } } ioapic .interrupt_source_group .set_gsi() .map_err(Error::UpdateInterrupt)?; } Ok(ioapic) } fn ioapic_write(&mut self, val: u32) { debug!("IOAPIC_W reg 0x{:x}, val 0x{:x}", self.reg_sel, val); match self.reg_sel as u8 { IOAPIC_REG_VERSION => { if val == 0 { // Windows writes zero here (see #1791) } else { error!( "IOAPIC: invalid write to version register (0x{:x}): 0x{:x}", self.reg_sel, val ); } } IOAPIC_REG_ID => self.id_reg = (val >> 24) & 0xf, 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}"); return; } if is_high_bits { self.reg_entries[index] &= 0xffff_ffff; self.reg_entries[index] |= u64::from(val) << 32; } else { // Ensure not to override read-only bits: // - Delivery Status (bit 12) // - Remote IRR (bit 14) self.reg_entries[index] &= 0xffff_ffff_0000_5000; self.reg_entries[index] |= u64::from(val) & 0xffff_afff; } // 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:?}"); } // Store the information this IRQ is now being used. self.used_entries[index] = true; } _ => error!( "IOAPIC: invalid write to register offset 0x{:x}", self.reg_sel ), } } fn ioapic_read(&self) -> u32 { debug!("IOAPIC_R reg 0x{:x}", self.reg_sel); match self.reg_sel as u8 { IOAPIC_REG_VERSION => IOAPIC_VERSION_ID, IOAPIC_REG_ID | IOAPIC_REG_ARBITRATION_ID => (self.id_reg & 0xf) << 24, 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}"); return 0; } if is_high_bits { (self.reg_entries[index] >> 32) as u32 } else { (self.reg_entries[index] & 0xffff_ffff) as u32 } } _ => { error!( "IOAPIC: invalid read from register offset 0x{:x}", self.reg_sel ); 0 } } } fn state(&self) -> IoapicState { IoapicState { id_reg: self.id_reg, reg_sel: self.reg_sel, reg_entries: self.reg_entries, used_entries: self.used_entries, apic_address: self.apic_address.0, } } fn update_entry(&self, irq: usize, set_gsi: bool) -> Result<()> { let entry = self.reg_entries[irq]; // Validate Destination Mode value, and retrieve Destination ID let destination_mode = destination_mode(entry); let destination_id = destination_field(entry); // When this bit is set, the message is directed to the processor with // the lowest interrupt priority among processors that can receive the // interrupt. let redirection_hint: u8 = 1; // Generate MSI message address let low_addr: u32 = self.apic_address.0 as u32 | (u32::from(destination_id) << 12) | (u32::from(redirection_hint) << 3) | (u32::from(destination_mode) << 2); // Validate Trigger Mode value let trigger_mode = trigger_mode(entry); match trigger_mode { x if (x == TriggerMode::Edge as u8) || (x == TriggerMode::Level as u8) => {} _ => return Err(Error::InvalidTriggerMode), } // Validate Delivery Mode value let delivery_mode = delivery_mode(entry); match delivery_mode { x if (x == DeliveryMode::Fixed as u8) || (x == DeliveryMode::Lowest as u8) || (x == DeliveryMode::Smi as u8) || (x == DeliveryMode::RemoteRead as u8) || (x == DeliveryMode::Nmi as u8) || (x == DeliveryMode::Init as u8) || (x == DeliveryMode::Startup as u8) || (x == DeliveryMode::External as u8) => {} _ => return Err(Error::InvalidDeliveryMode), } // Generate MSI message data let data: u32 = (u32::from(trigger_mode) << 15) | (u32::from(remote_irr(entry)) << 14) | (u32::from(delivery_mode) << 8) | u32::from(vector(entry)); let config = MsiIrqSourceConfig { high_addr: 0x0, low_addr, data, devid: 0, }; self.interrupt_source_group .update( irq as InterruptIndex, InterruptSourceConfig::MsiIrq(config), interrupt_mask(entry) == 1, set_gsi, ) .map_err(Error::UpdateInterrupt)?; Ok(()) } } impl InterruptController for Ioapic { // The ioapic must be informed about EOIs in order to deassert interrupts // already sent. fn end_of_interrupt(&mut self, vec: u8) { for i in 0..NUM_IOAPIC_PINS { let entry = &mut self.reg_entries[i]; // Clear Remote IRR bit if vector(*entry) == vec && trigger_mode(*entry) == 1 { set_remote_irr(entry, 0); } } } // This should be called anytime an interrupt needs to be injected into the // running guest. fn service_irq(&mut self, irq: usize) -> Result<()> { let entry = &mut self.reg_entries[irq]; self.interrupt_source_group .trigger(irq as InterruptIndex) .map_err(Error::TriggerInterrupt)?; trace!("Interrupt {irq} successfully delivered"); // If trigger mode is level sensitive, set the Remote IRR bit. // It will be cleared when the EOI is received. if trigger_mode(*entry) == 1 { set_remote_irr(entry, 1); } // Clear the Delivery Status bit set_delivery_status(entry, 0); Ok(()) } fn notifier(&self, irq: usize) -> Option { self.interrupt_source_group.notifier(irq as InterruptIndex) } } impl Snapshottable for Ioapic { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> result::Result { Snapshot::new_from_state(&self.state()) } } impl Pausable for Ioapic {} impl Transportable for Ioapic {} impl Migratable for Ioapic {} cloud-hypervisor-53.0/devices/src/ivshmem.rs000066400000000000000000000322501522476750100212210ustar00rootroot00000000000000// Copyright © 2024 Tencent Corporation. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 // use std::any::Any; use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Barrier, Mutex}; use std::{io, result}; use anyhow::anyhow; 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(#[source] anyhow::Error), #[error("Failed to create userspace mapping.")] CreateUserspaceMapping(#[source] anyhow::Error), #[error("Failed to remove old userspace mapping.")] RemoveUserspaceMapping(#[source] anyhow::Error), } #[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>, ) -> 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, ) -> 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 doesn't use an interrupt, we return zero now. data.fill(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) -> io::Result<()> { 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(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(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) -> 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 {} cloud-hypervisor-53.0/devices/src/legacy/000077500000000000000000000000001522476750100204455ustar00rootroot00000000000000cloud-hypervisor-53.0/devices/src/legacy/cmos.rs000066400000000000000000000147671522476750100217730ustar00rootroot00000000000000// 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 file. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::cmp::min; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Barrier}; use std::time::Duration; 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_REALTIME, clock_gettime, gmtime_r, timespec, tm}; use log::{info, warn}; use vm_device::BusDevice; use vmm_sys_util::eventfd::EventFd; const INDEX_MASK: u8 = 0x7f; const INDEX_OFFSET: u64 = 0x0; const DATA_OFFSET: u64 = 0x1; const DATA_LEN: usize = 128; /// A CMOS/RTC device commonly seen on x86 I/O port 0x70/0x71. pub struct Cmos { index: u8, data: [u8; DATA_LEN], reset_evt: EventFd, vcpus_kill_signalled: Option>, } impl Cmos { /// Constructs a CMOS/RTC device with initial data. /// `mem_below_4g` is the size of memory in bytes below the 32-bit gap. /// `mem_above_4g` is the size of memory in bytes above the 32-bit gap. pub fn new( mem_below_4g: u64, mem_above_4g: u64, reset_evt: EventFd, vcpus_kill_signalled: Option>, ) -> Cmos { let mut data = [0u8; DATA_LEN]; // Extended memory from 16 MB to 4 GB in units of 64 KB let ext_mem = min( 0xFFFF, mem_below_4g.saturating_sub(16 * 1024 * 1024) / (64 * 1024), ); data[0x34] = ext_mem as u8; data[0x35] = (ext_mem >> 8) as u8; // High memory (> 4GB) in units of 64 KB let high_mem = min(0x00FF_FFFF, mem_above_4g / (64 * 1024)); data[0x5b] = high_mem as u8; data[0x5c] = (high_mem >> 8) as u8; data[0x5d] = (high_mem >> 16) as u8; Cmos { index: 0, data, reset_evt, vcpus_kill_signalled, } } } impl BusDevice for Cmos { fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { if data.len() != 1 { warn!("Invalid write size on CMOS device: {}", data.len()); return None; } match offset { INDEX_OFFSET => self.index = data[0], DATA_OFFSET => { 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(Duration::from_millis(1)); } } } else { self.data[(self.index & INDEX_MASK) as usize] = data[0]; } } o => warn!("bad write offset on CMOS device: {o}"), } None } fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { fn to_bcd(v: u8) -> u8 { assert!(v < 100); ((v / 10) << 4) | (v % 10) } if data.len() != 1 { warn!("Invalid read size on CMOS device: {}", data.len()); return; } data[0] = match offset { INDEX_OFFSET => self.index, DATA_OFFSET => { let seconds; let minutes; let hours; let week_day; let day; let month; let year; // SAFETY: The clock_gettime and gmtime_r calls are safe as long as the structs they are // given are large enough, and neither of them fail. It is safe to zero initialize // 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, &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, &raw mut tm); // The following lines of code are safe but depend on tm being in scope. seconds = tm.tm_sec; minutes = tm.tm_min; hours = tm.tm_hour; week_day = tm.tm_wday + 1; day = tm.tm_mday; month = tm.tm_mon + 1; year = tm.tm_year; // Update in Progress bit held for last 224us of each second const NANOSECONDS_PER_SECOND: i64 = 1_000_000_000; const UIP_HOLD_LENGTH: i64 = 8 * NANOSECONDS_PER_SECOND / 32768; timespec.tv_nsec >= (NANOSECONDS_PER_SECOND - UIP_HOLD_LENGTH) }; match self.index { 0x00 => to_bcd(seconds as u8), 0x02 => to_bcd(minutes as u8), 0x04 => to_bcd(hours as u8), 0x06 => to_bcd(week_day as u8), 0x07 => to_bcd(day as u8), 0x08 => to_bcd(month as u8), 0x09 => to_bcd((year % 100) as u8), // Bit 5 for 32kHz clock. Bit 7 for Update in Progress 0x0a => (1 << 5) | ((update_in_progress as u8) << 7), // Bit 0-6 are reserved and must be 0. // Bit 7 must be 1 (CMOS has power) 0x0d => 1 << 7, 0x32 => to_bcd(((year + 1900) / 100) as u8), _ => { // self.index is always guaranteed to be in range via INDEX_MASK. self.data[(self.index & INDEX_MASK) as usize] } } } o => { warn!("bad read offset on CMOS device: {o}"); 0 } } } } cloud-hypervisor-53.0/devices/src/legacy/debug_port.rs000066400000000000000000000044571522476750100231570ustar00rootroot00000000000000// Copyright © 2022 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::fmt; use std::sync::{Arc, Barrier}; use std::time::Instant; use log::{error, warn}; use vm_device::BusDevice; /// Debug I/O port, see: /// https://www.intel.com/content/www/us/en/support/articles/000005500/boards-and-kits.html /// /// Since we're not a physical platform, we can freely assign code ranges for /// debugging specific parts of our virtual platform. pub enum DebugIoPortRange { Firmware, Bootloader, Kernel, Userspace, Custom, } #[cfg(target_arch = "x86_64")] const DEBUG_IOPORT_PREFIX: &str = "Debug I/O port"; #[cfg(target_arch = "x86_64")] impl DebugIoPortRange { fn from_u8(value: u8) -> DebugIoPortRange { match value { 0x00..=0x1f => DebugIoPortRange::Firmware, 0x20..=0x3f => DebugIoPortRange::Bootloader, 0x40..=0x5f => DebugIoPortRange::Kernel, 0x60..=0x7f => DebugIoPortRange::Userspace, _ => DebugIoPortRange::Custom, } } } #[cfg(target_arch = "x86_64")] impl fmt::Display for DebugIoPortRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { DebugIoPortRange::Firmware => write!(f, "{DEBUG_IOPORT_PREFIX}: Firmware"), DebugIoPortRange::Bootloader => write!(f, "{DEBUG_IOPORT_PREFIX}: Bootloader"), DebugIoPortRange::Kernel => write!(f, "{DEBUG_IOPORT_PREFIX}: Kernel"), DebugIoPortRange::Userspace => write!(f, "{DEBUG_IOPORT_PREFIX}: Userspace"), DebugIoPortRange::Custom => write!(f, "{DEBUG_IOPORT_PREFIX}: Custom"), } } } pub struct DebugPort { timestamp: Instant, } impl DebugPort { pub fn new(timestamp: Instant) -> Self { Self { timestamp } } } impl BusDevice for DebugPort { fn read(&mut self, _base: u64, _offset: u64, _data: &mut [u8]) { error!("Invalid read to debug port"); } fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { let elapsed = self.timestamp.elapsed(); let code = data[0]; warn!( "[{} code 0x{:x}] {}.{:>06} seconds", DebugIoPortRange::from_u8(code), code, elapsed.as_secs(), elapsed.as_micros() ); None } } cloud-hypervisor-53.0/devices/src/legacy/fw_cfg.rs000066400000000000000000001014521522476750100222510ustar00rootroot00000000000000// 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::{ cmp, ffi::CString, 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(|_| 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 = cmp::min(FILE_NAME_SIZE - 1, name.len()); c_name[0..c_len].copy_from_slice(&name.as_bytes()[0..c_len]); c_name } #[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 = 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: 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 { // Zero fill rather than the usual 0xff fill for QEMU compatibility data.fill(0); let start = offset as usize; let end = start + size as usize; match content { FwCfgContent::Bytes(b) => { if end <= b.len() { data.copy_from_slice(&b[start..end]); } } FwCfgContent::Slice(s) => { if end <= s.len() { 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(); if end <= bytes.len() { 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); } } cloud-hypervisor-53.0/devices/src/legacy/fwdebug.rs000066400000000000000000000023441522476750100224410ustar00rootroot00000000000000// 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::sync::{Arc, Barrier}; use log::error; use vm_device::BusDevice; /// Provides firmware debug output via I/O port controls #[derive(Default)] pub struct FwDebugDevice {} impl FwDebugDevice { pub fn new() -> Self { Self {} } } /// FwDebugDevice sits on the I/O bus as 0x402 and receives ASCII characters 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; } else { error!("Invalid read size on debug port: {}", data.len()); } } fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { if data.len() == 1 { print!("{}", data[0] as char); } else { error!("Invalid write size on debug port: {}", data.len()); } None } } cloud-hypervisor-53.0/devices/src/legacy/gpio_pl061.rs000066400000000000000000000354401522476750100227010ustar00rootroot00000000000000// Copyright 2021 Arm Limited (or its affiliates). All rights reserved. // // SPDX-License-Identifier: Apache-2.0 //! ARM PrimeCell General Purpose Input/Output(PL061) //! //! This module implements an ARM PrimeCell General Purpose Input/Output(PL061) to support gracefully poweroff microvm from external. //! use std::sync::{Arc, Barrier}; use std::{io, result}; use log::warn; use serde::{Deserialize, Serialize}; use thiserror::Error; 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}; const OFS_DATA: u64 = 0x400; // Data Register const GPIODIR: u64 = 0x400; // Direction Register const GPIOIS: u64 = 0x404; // Interrupt Sense Register const GPIOIBE: u64 = 0x408; // Interrupt Both Edges Register const GPIOIEV: u64 = 0x40c; // Interrupt Event Register const GPIOIE: u64 = 0x410; // Interrupt Mask Register 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. const GPIO_ID: [u8; 8] = [0x61, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; // ID Margins const GPIO_ID_LOW: u64 = 0xfe0; const GPIO_ID_HIGH: u64 = 0x1000; const N_GPIOS: u32 = 8; #[derive(Debug, Error)] pub enum Error { #[error("Bad Write Offset: {0}")] BadWriteOffset(u64), #[error("GPIO interrupt disabled by guest driver")] GpioInterruptDisabled, #[error("Could not trigger GPIO interrupt")] GpioInterruptFailure(#[source] io::Error), #[error("Invalid GPIO Input key triggered: {0}")] GpioTriggerKeyFailure(u32), } type Result = result::Result; /// A GPIO device following the PL061 specification. pub struct Gpio { id: String, // Data Register data: u32, old_in_data: u32, // Direction Register dir: u32, // Interrupt Sense Register isense: u32, // Interrupt Both Edges Register ibe: u32, // Interrupt Event Register iev: u32, // Interrupt Mask Register im: u32, // Raw Interrupt Status Register istate: u32, // Mode Control Select Register afsel: u32, // GPIO irq_field interrupt: Arc, } #[derive(Serialize, Deserialize)] pub struct GpioState { data: u32, old_in_data: u32, dir: u32, isense: u32, ibe: u32, iev: u32, im: u32, istate: u32, afsel: u32, } impl Gpio { /// Constructs an PL061 GPIO device. pub fn new( id: String, interrupt: Arc, state: Option, ) -> Self { let (data, old_in_data, dir, isense, ibe, iev, im, istate, afsel) = if let Some(state) = state { ( state.data, state.old_in_data, state.dir, state.isense, state.ibe, state.iev, state.im, state.istate, state.afsel, ) } else { (0, 0, 0, 0, 0, 0, 0, 0, 0) }; Self { id, data, old_in_data, dir, isense, ibe, iev, im, istate, afsel, interrupt, } } fn state(&self) -> GpioState { GpioState { data: self.data, old_in_data: self.old_in_data, dir: self.dir, isense: self.isense, ibe: self.ibe, iev: self.iev, im: self.im, istate: self.istate, afsel: self.afsel, } } fn pl061_internal_update(&mut self) { // FIXME: // Missing Output Interrupt Emulation. // Input Edging Interrupt Emulation. let changed = (self.old_in_data ^ self.data) & !self.dir; if changed > 0 { self.old_in_data = self.data; for i in 0..N_GPIOS { let mask = (1 << i) as u32; if (changed & mask) > 0 { // Bits set high in GPIOIS(Interrupt sense register) configure the corresponding // pins to detect levels, otherwise, detect edges. if (self.isense & mask) == 0 { if (self.ibe & mask) > 0 { // Bits set high in GPIOIBE(Interrupt both-edges register) configure the corresponding // pins to detect both falling and rising edges. // Clearing a bit configures the pin to be controlled by GPIOIEV. self.istate |= mask; } else { // Bits set to high in GPIOIEV(Interrupt event register) configure the // corresponding pin to detect rising edges, otherwise, detect falling edges. self.istate |= !(self.data ^ self.iev) & mask; } } } } } // Input Level Interrupt Emulation. self.istate |= !(self.data ^ self.iev) & self.isense; } fn handle_write(&mut self, offset: u64, val: u32) -> Result<()> { if offset < OFS_DATA { // In order to write to data register, the corresponding bits in the mask, resulting // from the offsite[9:2], must be HIGH. otherwise the bit values remain unchanged. let mask = (offset >> 2) as u32 & self.dir; self.data = (self.data & !mask) | (val & mask); } else { match offset { GPIODIR => { /* Direction Register */ self.dir = val & 0xff; } GPIOIS => { /* Interrupt Sense Register */ self.isense = val & 0xff; } GPIOIBE => { /* Interrupt Both Edges Register */ self.ibe = val & 0xff; } GPIOIEV => { /* Interrupt Event Register */ self.iev = val & 0xff; } GPIOIE => { /* Interrupt Mask Register */ self.im = val & 0xff; } GPIOIC => { /* Interrupt Clear Register */ self.istate &= !val; } GPIOAFSEL => { /* Mode Control Select Register */ self.afsel = val & 0xff; } o => { return Err(Error::BadWriteOffset(o)); } } } Ok(()) } pub fn trigger_key(&mut self, key: u32) -> Result<()> { let mask = (1 << key) as u32; if (!self.dir & mask) > 0 { // emulate key event // By default, Input Pin is configured to detect both rising and falling edges. // So reverse the input pin data to generate a pulse. self.data |= !(self.data & mask) & mask; self.pl061_internal_update(); match self.trigger_gpio_interrupt() { Ok(_) | Err(Error::GpioInterruptDisabled) => return Ok(()), Err(e) => return Err(e), } } Err(Error::GpioTriggerKeyFailure(key)) } fn trigger_gpio_interrupt(&self) -> Result<()> { // Bits set to high in GPIOIE(Interrupt mask register) allow the corresponding pins to // trigger their individual interrupts and then the combined GPIOINTR line. if (self.istate & self.im) == 0 { warn!("Failed to trigger GPIO input interrupt (disabled by guest OS)"); return Err(Error::GpioInterruptDisabled); } self.interrupt .trigger(0) .map_err(Error::GpioInterruptFailure)?; Ok(()) } } impl BusDevice for Gpio { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { let mut read_ok = true; let value = if (GPIO_ID_LOW..GPIO_ID_HIGH).contains(&offset) { let index = ((offset - GPIO_ID_LOW) >> 2) as usize; u32::from(GPIO_ID[index]) } else if offset < OFS_DATA { self.data & ((offset >> 2) as u32) } else { match offset { GPIODIR => self.dir, GPIOIS => self.isense, GPIOIBE => self.ibe, GPIOIEV => self.iev, GPIOIE => self.im, GPIORIE => self.istate, GPIOMIS => self.istate & self.im, GPIOAFSEL => self.afsel, _ => { read_ok = false; 0 } } }; if read_ok && data.len() <= 4 { write_le_u32(data, value); } else { warn!( "Invalid GPIO PL061 read: offset {}, data length {}", offset, data.len() ); } } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { 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}"); } } else { warn!( "Invalid GPIO PL061 write: offset {}, data length {}", offset, data.len() ); } None } } impl Snapshottable for Gpio { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> result::Result { Snapshot::new_from_state(&self.state()) } } impl Pausable for Gpio {} impl Transportable for Gpio {} impl Migratable for Gpio {} #[cfg(test)] mod unit_tests { use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; use vmm_sys_util::eventfd::EventFd; use super::*; const GPIO_NAME: &str = "gpio"; const LEGACY_GPIO_MAPPED_IO_START: u64 = 0x0902_0000; struct TestInterrupt { event_fd: EventFd, } impl InterruptSourceGroup for TestInterrupt { fn trigger(&self, _index: InterruptIndex) -> io::Result<()> { self.event_fd.write(1) } fn update( &self, _index: InterruptIndex, _config: InterruptSourceConfig, _masked: bool, _set_gsi: bool, ) -> io::Result<()> { Ok(()) } fn set_gsi(&self) -> io::Result<()> { 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_gpio_read_write_and_event() { let intr_evt = EventFd::new(libc::EFD_NONBLOCK).unwrap(); let mut gpio = Gpio::new( String::from(GPIO_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), None, ); let mut data = [0; 4]; // Read and write to the GPIODIR register. // Set pin 0 output pin. write_le_u32(&mut data, 1); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIODIR, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIODIR, &mut data); let v = read_le_u32(&data); assert_eq!(v, 1); // Read and write to the GPIODATA register. write_le_u32(&mut data, 1); // Set pin 0 high. let offset = 0x00000004_u64; gpio.write(LEGACY_GPIO_MAPPED_IO_START, offset, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, offset, &mut data); let v = read_le_u32(&data); assert_eq!(v, 1); // Read and write to the GPIOIS register. // Configure pin 0 detecting level interrupt. write_le_u32(&mut data, 1); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIOIS, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIOIS, &mut data); let v = read_le_u32(&data); assert_eq!(v, 1); // Read and write to the GPIOIBE register. // Configure pin 1 detecting both falling and rising edges. write_le_u32(&mut data, 2); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIOIBE, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIOIBE, &mut data); let v = read_le_u32(&data); assert_eq!(v, 2); // Read and write to the GPIOIEV register. // Configure pin 2 detecting both falling and rising edges. write_le_u32(&mut data, 4); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIOIEV, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIOIEV, &mut data); let v = read_le_u32(&data); assert_eq!(v, 4); // Read and write to the GPIOIE register. // Configure pin 0...2 capable of triggering their individual interrupts // and then the combined GPIOINTR line. write_le_u32(&mut data, 7); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIOIE, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIOIE, &mut data); let v = read_le_u32(&data); assert_eq!(v, 7); let mask = 0x00000002_u32; // emulate an rising pulse in pin 1. gpio.data |= !(gpio.data & mask) & mask; gpio.pl061_internal_update(); // The interrupt line on pin 1 should be on. // Read the GPIOMIS register. gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIOMIS, &mut data); let v = read_le_u32(&data); assert_eq!(v, 2); // Read and Write to the GPIOIC register. // clear interrupt in pin 1. write_le_u32(&mut data, 2); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIOIC, &data); gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIOIC, &mut data); let v = read_le_u32(&data); assert_eq!(v, 2); // Attempts to write beyond the writable space. write_le_u32(&mut data, 0); gpio.write(LEGACY_GPIO_MAPPED_IO_START, GPIO_ID_LOW, &data); let mut data = [0; 4]; gpio.read(LEGACY_GPIO_MAPPED_IO_START, GPIO_ID_LOW, &mut data); let index = GPIO_ID_LOW + 3; assert_eq!(data[0], GPIO_ID[((index - GPIO_ID_LOW) >> 2) as usize]); } } cloud-hypervisor-53.0/devices/src/legacy/i8042.rs000066400000000000000000000044671522476750100215740ustar00rootroot00000000000000// 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. // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Barrier}; use std::thread; use std::time::Duration; use log::{error, info}; use vm_device::BusDevice; use vmm_sys_util::eventfd::EventFd; /// A i8042 PS/2 controller that emulates just enough to shutdown the machine. pub struct I8042Device { reset_evt: EventFd, vcpus_kill_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 { I8042Device { reset_evt, vcpus_kill_signalled, } } } // i8042 device is located at I/O port 0x61. We partially implement two 8-bit // registers: port 0x61 (I8042_PORT_B_REG, offset 0 from base of 0x61), and // port 0x64 (I8042_COMMAND_REG, offset 3 from base of 0x61). impl BusDevice for I8042Device { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { if data.len() == 1 && offset == 3 { data[0] = 0x0; } else if data.len() == 1 && offset == 0 { // Like kvmtool, we return bit 5 set in I8042_PORT_B_REG to // avoid hang in pit_calibrate_tsc() in Linux kernel. data[0] = 0x20; } } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { 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}"); } // 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) { // This is more effective than thread::yield_now() at // avoiding a priority inversion with the VMM thread thread::sleep(Duration::from_millis(1)); } } None } } cloud-hypervisor-53.0/devices/src/legacy/mod.rs000066400000000000000000000022311522476750100215700ustar00rootroot00000000000000// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. 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")] mod gpio_pl061; mod i8042; #[cfg(target_arch = "aarch64")] mod rtc_pl031; mod serial; #[cfg(target_arch = "aarch64")] 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")] pub use self::gpio_pl061::Error as GpioDeviceError; #[cfg(target_arch = "aarch64")] pub use self::gpio_pl061::Gpio; pub use self::i8042::I8042Device; #[cfg(target_arch = "aarch64")] pub use self::rtc_pl031::Rtc; pub use self::serial::Serial; #[cfg(target_arch = "aarch64")] pub use self::uart_pl011::Pl011; cloud-hypervisor-53.0/devices/src/legacy/rtc_pl031.rs000066400000000000000000000403021522476750100225210ustar00rootroot00000000000000// Copyright 2020 Arm Limited (or its affiliates). All rights reserved. // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //! ARM PL031 Real Time Clock //! //! 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 log::warn; use thiserror::Error; use vm_device::BusDevice; use crate::{read_le_u32, write_le_u32}; // As you can see in https://developer.arm.com/documentation/ddi0224/c/ // at section 3.2 Summary of RTC registers, the total size occupied by this device is 0x000 -> 0xFFC + 4 = 0x1000. // From 0x0 to 0x1C we have following registers: const RTCDR: u64 = 0x0; // Data Register. const RTCMR: u64 = 0x4; // Match Register. const RTCLR: u64 = 0x8; // Load Register. const RTCCR: u64 = 0xc; // Control Register. 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. 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; const AMBA_ID_HIGH: u64 = 0x1000; /// Constant to convert seconds to nanoseconds. pub const NANOS_PER_SECOND: u64 = 1_000_000_000; #[derive(Debug, Error)] pub enum Error { #[error("Bad Write Offset: {0}")] BadWriteOffset(u64), } type Result = result::Result; /// Wrapper over `libc::clockid_t` to specify Linux Kernel clock source. pub enum ClockType { /// Equivalent to `libc::CLOCK_MONOTONIC`. Monotonic, /// Equivalent to `libc::CLOCK_REALTIME`. Real, /// Equivalent to `libc::CLOCK_PROCESS_CPUTIME_ID`. ProcessCpu, /// Equivalent to `libc::CLOCK_THREAD_CPUTIME_ID`. ThreadCpu, } impl From for libc::clockid_t { fn from(ct: ClockType) -> libc::clockid_t { match ct { ClockType::Monotonic => libc::CLOCK_MONOTONIC, ClockType::Real => libc::CLOCK_REALTIME, ClockType::ProcessCpu => libc::CLOCK_PROCESS_CPUTIME_ID, ClockType::ThreadCpu => libc::CLOCK_THREAD_CPUTIME_ID, } } } /// Returns a timestamp in nanoseconds based on the provided clock type. /// /// # Arguments /// /// * `clock_type` - Identifier of the Linux Kernel clock on which to act. pub fn get_time(clock_type: ClockType) -> u64 { let mut time_struct = libc::timespec { tv_sec: 0, tv_nsec: 0, }; // SAFETY: the parameters are valid. unsafe { libc::clock_gettime(clock_type.into(), &mut time_struct) }; seconds_to_nanoseconds(time_struct.tv_sec).unwrap() as u64 + (time_struct.tv_nsec as u64) } /// Converts a timestamp in seconds to an equivalent one in nanoseconds. /// Returns `None` if the conversion overflows. /// /// # Arguments /// /// * `value` - Timestamp in seconds. pub fn seconds_to_nanoseconds(value: i64) -> Option { value.checked_mul(NANOS_PER_SECOND as i64) } /// A RTC device following the PL031 specification.. pub struct Rtc { previous_now: Instant, tick_offset: i64, // This is used for implementing the RTC alarm. However, in Firecracker we do not need it. match_value: u32, // Writes to this register load an update value into the RTC. load: u32, } impl Rtc { /// Constructs an AMBA PL031 RTC device. 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, } } fn get_time(&self) -> u32 { let ts = (self.tick_offset as i128) + (Instant::now().duration_since(self.previous_now).as_nanos() as i128); (ts / NANOS_PER_SECOND as i128) as u32 } fn handle_write(&mut self, offset: u64, val: u32) -> Result<()> { match offset { RTCMR => { // The MR register is used for implementing the RTC alarm. A real time clock alarm is // a feature that can be used to allow a computer to 'wake up' after shut down to execute // tasks every day or on a certain day. It can sometimes be found in the 'Power Management' // section of a motherboard's BIOS setup. This is functionality that extends beyond // Firecracker intended use. However, we increment a metric just in case. self.match_value = val; } RTCLR => { self.load = val; self.previous_now = Instant::now(); // If the unwrap fails, then the internal value of the clock has been corrupted and // we want to terminate the execution of the process. self.tick_offset = seconds_to_nanoseconds(i64::from(val)).unwrap(); } RTCIMSC => (), RTCICR => (), RTCCR => (), // ignore attempts to turn off the timer. o => { return Err(Error::BadWriteOffset(o)); } } Ok(()) } } 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; let v = if (AMBA_ID_LOW..AMBA_ID_HIGH).contains(&offset) { let index = ((offset - AMBA_ID_LOW) >> 2) as usize; u32::from(PL031_ID[index]) } else { match offset { RTCDR => self.get_time(), RTCMR => { // Even though we are not implementing RTC alarm we return the last value self.match_value } RTCLR => self.load, RTCCR => 1, // RTC is always enabled. RTCIMSC => 0, // Interrupt is always disabled. RTCRIS => 0, RTCMIS => 0, _ => { read_ok = false; 0 } } }; if read_ok && data.len() <= 4 { write_le_u32(data, v); } else { warn!( "Invalid RTC PL031 read: offset {}, data length {}", offset, data.len() ); } } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { 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}"); } } else { warn!( "Invalid RTC PL031 write: offset {offset}, data length {}", data.len() ); } None } } #[cfg(test)] mod unit_tests { use std::{fmt, ptr}; use super::*; use crate::{ read_be_u16, read_be_u32, read_le_i32, read_le_u16, read_le_u64, write_be_u16, write_be_u32, write_le_i32, write_le_u16, write_le_u64, }; const LEGACY_RTC_MAPPED_IO_START: u64 = 0x0901_0000; struct LocalTime { sec: i32, min: i32, hour: i32, mday: i32, mon: i32, year: i32, nsec: i64, } impl LocalTime { fn now() -> LocalTime { let mut timespec = libc::timespec { tv_sec: 0, tv_nsec: 0, }; let mut tm: libc::tm = libc::tm { tm_sec: 0, tm_min: 0, tm_hour: 0, tm_mday: 0, tm_mon: 0, tm_year: 0, tm_wday: 0, tm_yday: 0, tm_isdst: 0, tm_gmtoff: 0, tm_zone: ptr::null(), }; // SAFETY: the parameters are valid. unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut timespec); libc::localtime_r(×pec.tv_sec, &mut tm); } LocalTime { sec: tm.tm_sec, min: tm.tm_min, hour: tm.tm_hour, mday: tm.tm_mday, mon: tm.tm_mon, year: tm.tm_year, nsec: timespec.tv_nsec, } } } impl fmt::Display for LocalTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}", self.year + 1900, self.mon + 1, self.mday, self.hour, self.min, self.sec, self.nsec ) } } #[test] fn test_get_time() { for _ in 0..1000 { assert!(get_time(ClockType::Monotonic) <= get_time(ClockType::Monotonic)); } for _ in 0..1000 { assert!(get_time(ClockType::ProcessCpu) <= get_time(ClockType::ProcessCpu)); } for _ in 0..1000 { assert!(get_time(ClockType::ThreadCpu) <= get_time(ClockType::ThreadCpu)); } assert_ne!(get_time(ClockType::Real), 0); } #[test] fn test_local_time_display() { let local_time = LocalTime { sec: 30, min: 15, hour: 10, mday: 4, mon: 6, year: 119, nsec: 123_456_789, }; assert_eq!( String::from("2019-07-04T10:15:30.123456789"), local_time.to_string() ); let local_time = LocalTime { sec: 5, min: 5, hour: 5, mday: 23, mon: 7, year: 44, nsec: 123, }; assert_eq!( String::from("1944-08-23T05:05:05.000000123"), local_time.to_string() ); let local_time = LocalTime::now(); assert!(local_time.mon >= 0 && local_time.mon <= 11); } #[test] fn test_seconds_to_nanoseconds() { assert_eq!( seconds_to_nanoseconds(100).unwrap() as u64, 100 * NANOS_PER_SECOND ); assert!(seconds_to_nanoseconds(9_223_372_037).is_none()); } #[test] fn test_rtc_read_write_and_event() { let mut rtc = Rtc::new(); let mut data = [0; 4]; // Read and write to the MR register. write_le_u32(&mut data, 123); rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCMR, &data); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCMR, &mut data); let v = read_le_u32(&data); assert_eq!(v, 123); // Read and write to the LR register. let v = get_time(ClockType::Real); write_le_u32(&mut data, (v / NANOS_PER_SECOND) as u32); let previous_now_before = rtc.previous_now; rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCLR, &data); assert!(rtc.previous_now > previous_now_before); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCLR, &mut data); let v_read = read_le_u32(&data); assert_eq!((v / NANOS_PER_SECOND) as u32, v_read); // Read and write to IMSC register. // 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); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCIMSC, &mut data); let v = read_le_u32(&data); assert_eq!(0, v); // Now test with 0. write_le_u32(&mut data, 0); rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCIMSC, &data); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCIMSC, &mut data); let v = read_le_u32(&data); assert_eq!(0, v); // Read and write to the ICR register. write_le_u32(&mut data, 1); rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCICR, &data); let v_before = read_le_u32(&data); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCICR, &mut data); let v = read_le_u32(&data); // ICR is a write only register. Data received should stay equal to data sent. assert_eq!(v, v_before); // Attempts to turn off the RTC should not go through. write_le_u32(&mut data, 0); rtc.write(LEGACY_RTC_MAPPED_IO_START, RTCCR, &data); rtc.read(LEGACY_RTC_MAPPED_IO_START, RTCCR, &mut data); let v = read_le_u32(&data); assert_eq!(v, 1); // Attempts to write beyond the writable space. Using here the space used to read // the CID and PID from. write_le_u32(&mut data, 0); rtc.write(LEGACY_RTC_MAPPED_IO_START, AMBA_ID_LOW, &data); // However, reading from the AMBA_ID_LOW should succeed upon read. let mut data = [0; 4]; rtc.read(LEGACY_RTC_MAPPED_IO_START, AMBA_ID_LOW, &mut data); let index = AMBA_ID_LOW + 3; assert_eq!(data[0], PL031_ID[((index - AMBA_ID_LOW) >> 2) as usize]); } macro_rules! byte_order_test_read_write { ($test_name: ident, $write_fn_name: ident, $read_fn_name: ident, $is_be: expr, $data_type: ty) => { #[test] fn $test_name() { let test_cases = [ ( 0x0123_4567_89AB_CDEF as u64, [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef], ), ( 0x0000_0000_0000_0000 as u64, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], ), ( 0x1923_2345_ABF3_CCD4 as u64, [0x19, 0x23, 0x23, 0x45, 0xAB, 0xF3, 0xCC, 0xD4], ), ( 0x0FF0_0FF0_0FF0_0FF0 as u64, [0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0], ), ( 0xFFFF_FFFF_FFFF_FFFF as u64, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], ), ( 0x89AB_12D4_C2D2_09BB as u64, [0x89, 0xAB, 0x12, 0xD4, 0xC2, 0xD2, 0x09, 0xBB], ), ]; let type_size = size_of::<$data_type>(); for (test_val, v_arr) in &test_cases { let v = *test_val as $data_type; let cmp_iter: Box> = if $is_be { Box::new(v_arr[(8 - type_size)..].iter()) } else { Box::new(v_arr.iter().rev()) }; // test write let mut write_arr = vec![Default::default(); type_size]; $write_fn_name(&mut write_arr, v); for (cmp, cur) in cmp_iter.zip(write_arr.iter()) { assert_eq!(*cmp, *cur as u8) } // test read let read_val = $read_fn_name(&write_arr); assert_eq!(v, read_val); } } }; } byte_order_test_read_write!(test_le_u16, write_le_u16, read_le_u16, false, u16); byte_order_test_read_write!(test_le_u32, write_le_u32, read_le_u32, false, u32); byte_order_test_read_write!(test_le_u64, write_le_u64, read_le_u64, false, u64); byte_order_test_read_write!(test_le_i32, write_le_i32, read_le_i32, false, i32); byte_order_test_read_write!(test_be_u16, write_be_u16, read_be_u16, true, u16); byte_order_test_read_write!(test_be_u32, write_be_u32, read_be_u32, true, u32); } cloud-hypervisor-53.0/devices/src/legacy/serial.rs000066400000000000000000000400271522476750100222750ustar00rootroot00000000000000// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. use std::collections::VecDeque; use std::sync::{Arc, Barrier}; use std::{io, result}; use serde::{Deserialize, Serialize}; use vm_device::BusDevice; use vm_device::interrupt::InterruptSourceGroup; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::errno::Result; const LOOP_SIZE: usize = 0x40; const DATA: u8 = 0; const IER: u8 = 1; const IIR: u8 = 2; const LCR: u8 = 3; const MCR: u8 = 4; const LSR: u8 = 5; const MSR: u8 = 6; const SCR: u8 = 7; const DLAB_LOW: u8 = 0; const DLAB_HIGH: u8 = 1; const IER_RECV_BIT: u8 = 0x1; const IER_THR_BIT: u8 = 0x2; const IER_FIFO_BITS: u8 = 0x0f; const IIR_FIFO_BITS: u8 = 0xc0; const IIR_NONE_BIT: u8 = 0x1; const IIR_THR_BIT: u8 = 0x2; const IIR_RECV_BIT: u8 = 0x4; const LCR_DLAB_BIT: u8 = 0x80; const LSR_DATA_BIT: u8 = 0x1; const LSR_EMPTY_BIT: u8 = 0x20; const LSR_IDLE_BIT: u8 = 0x40; const MCR_LOOP_BIT: u8 = 0x10; const DEFAULT_INTERRUPT_IDENTIFICATION: u8 = IIR_NONE_BIT; // no pending interrupt const DEFAULT_LINE_STATUS: u8 = LSR_EMPTY_BIT | LSR_IDLE_BIT; // THR empty and line is idle const DEFAULT_LINE_CONTROL: u8 = 0x3; // 8-bits per character const DEFAULT_MODEM_CONTROL: u8 = 0x8; // Auxiliary output 2 const DEFAULT_MODEM_STATUS: u8 = 0x20 | 0x10 | 0x80; // data ready, clear to send, carrier detect const DEFAULT_BAUD_DIVISOR: u16 = 12; // 9600 bps /// Emulates serial COM ports commonly seen on x86 I/O ports 0x3f8/0x2f8/0x3e8/0x2e8. /// /// This can optionally write the guest's output to a Write trait object. To send input to the /// guest, use `queue_input_bytes`. pub struct Serial { id: String, interrupt_enable: u8, interrupt_identification: u8, line_control: u8, line_status: u8, modem_control: u8, modem_status: u8, scratch: u8, baud_divisor: u16, in_buffer: VecDeque, interrupt: Arc, out: Option>, } #[derive(Serialize, Deserialize)] pub struct SerialState { interrupt_enable: u8, interrupt_identification: u8, line_control: u8, line_status: u8, modem_control: u8, modem_status: u8, scratch: u8, baud_divisor: u16, in_buffer: Vec, } impl Serial { pub fn new( id: String, interrupt: Arc, out: Option>, state: Option, ) -> Serial { let ( interrupt_enable, interrupt_identification, line_control, line_status, modem_control, modem_status, scratch, baud_divisor, in_buffer, ) = if let Some(state) = state { ( state.interrupt_enable, state.interrupt_identification, state.line_control, state.line_status, state.modem_control, state.modem_status, state.scratch, state.baud_divisor, state.in_buffer.into(), ) } else { ( 0, DEFAULT_INTERRUPT_IDENTIFICATION, DEFAULT_LINE_CONTROL, DEFAULT_LINE_STATUS, DEFAULT_MODEM_CONTROL, DEFAULT_MODEM_STATUS, 0, DEFAULT_BAUD_DIVISOR, VecDeque::new(), ) }; Serial { id, interrupt_enable, interrupt_identification, line_control, line_status, modem_control, modem_status, scratch, baud_divisor, in_buffer, interrupt, out, } } /// Constructs a Serial port ready for output. pub fn new_out( id: String, interrupt: Arc, out: Box, state: Option, ) -> Serial { Self::new(id, interrupt, Some(out), state) } /// Constructs a Serial port with no connected output. pub fn new_sink( id: String, interrupt: Arc, state: Option, ) -> Serial { Self::new(id, interrupt, None, state) } pub fn set_out(&mut self, out: Option>) { self.out = out; } /// Queues raw bytes for the guest to read and signals the interrupt if the line status would /// change. pub fn queue_input_bytes(&mut self, c: &[u8]) -> Result<()> { if !self.is_loop() { self.in_buffer.extend(c); self.recv_data()?; } Ok(()) } pub fn flush_output(&mut self) -> io::Result<()> { if let Some(out) = self.out.as_mut() { out.flush()?; } Ok(()) } fn is_dlab_set(&self) -> bool { (self.line_control & LCR_DLAB_BIT) != 0 } fn is_recv_intr_enabled(&self) -> bool { (self.interrupt_enable & IER_RECV_BIT) != 0 } fn is_thr_intr_enabled(&self) -> bool { (self.interrupt_enable & IER_THR_BIT) != 0 } fn is_loop(&self) -> bool { (self.modem_control & MCR_LOOP_BIT) != 0 } fn add_intr_bit(&mut self, bit: u8) { self.interrupt_identification &= !IIR_NONE_BIT; self.interrupt_identification |= bit; } fn del_intr_bit(&mut self, bit: u8) { self.interrupt_identification &= !bit; if self.interrupt_identification == 0x0 { self.interrupt_identification = IIR_NONE_BIT; } } fn thr_empty(&mut self) -> Result<()> { if self.is_thr_intr_enabled() { self.add_intr_bit(IIR_THR_BIT); self.trigger_interrupt()?; } Ok(()) } fn recv_data(&mut self) -> Result<()> { if self.is_recv_intr_enabled() { self.add_intr_bit(IIR_RECV_BIT); self.trigger_interrupt()?; } self.line_status |= LSR_DATA_BIT; Ok(()) } fn trigger_interrupt(&mut self) -> io::Result<()> { self.interrupt.trigger(0) } fn iir_reset(&mut self) { self.interrupt_identification = DEFAULT_INTERRUPT_IDENTIFICATION; } 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); } DLAB_HIGH if self.is_dlab_set() => { self.baud_divisor = (self.baud_divisor & 0x00ff) | ((u16::from(v)) << 8); } DATA => { if self.is_loop() { if self.in_buffer.len() < LOOP_SIZE { self.in_buffer.push_back(v); self.recv_data()?; } } else { if let Some(out) = self.out.as_mut() { out.write_all(&[v])?; out.flush()?; } self.thr_empty()?; } } IER => self.interrupt_enable = v & IER_FIFO_BITS, LCR => self.line_control = v, MCR => self.modem_control = v, SCR => self.scratch = v, _ => {} } Ok(()) } fn state(&self) -> SerialState { SerialState { interrupt_enable: self.interrupt_enable, interrupt_identification: self.interrupt_identification, line_control: self.line_control, line_status: self.line_status, modem_control: self.modem_control, modem_status: self.modem_status, scratch: self.scratch, baud_divisor: self.baud_divisor, in_buffer: self.in_buffer.clone().into(), } } } impl BusDevice for Serial { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { if data.len() != 1 { return; } data[0] = match offset as u8 { DLAB_LOW if self.is_dlab_set() => self.baud_divisor as u8, DLAB_HIGH if self.is_dlab_set() => (self.baud_divisor >> 8) as u8, DATA => { self.del_intr_bit(IIR_RECV_BIT); if self.in_buffer.len() <= 1 { self.line_status &= !LSR_DATA_BIT; } self.in_buffer.pop_front().unwrap_or_default() } IER => self.interrupt_enable, IIR => { let v = self.interrupt_identification | IIR_FIFO_BITS; self.iir_reset(); v } LCR => self.line_control, MCR => self.modem_control, LSR => self.line_status, MSR => self.modem_status, SCR => self.scratch, _ => 0, }; } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { if data.len() != 1 { return None; } self.handle_write(offset as u8, data[0]).ok(); None } } impl Snapshottable for Serial { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> result::Result { Snapshot::new_from_state(&self.state()) } } impl Pausable for Serial {} impl Transportable for Serial {} impl Migratable for Serial {} #[cfg(test)] mod unit_tests { use std::sync::Mutex; use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; use vmm_sys_util::eventfd::EventFd; use super::*; const SERIAL_NAME: &str = "serial"; struct TestInterrupt { event_fd: EventFd, } impl InterruptSourceGroup for TestInterrupt { fn trigger(&self, _index: InterruptIndex) -> io::Result<()> { self.event_fd.write(1) } fn update( &self, _index: InterruptIndex, _config: InterruptSourceConfig, _masked: bool, _set_gsi: bool, ) -> io::Result<()> { Ok(()) } fn set_gsi(&self) -> io::Result<()> { 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 } } } #[derive(Clone)] struct SharedBuffer { buf: Arc>>, } impl SharedBuffer { fn new() -> SharedBuffer { SharedBuffer { buf: Arc::new(Mutex::new(Vec::new())), } } } impl io::Write for SharedBuffer { fn write(&mut self, buf: &[u8]) -> io::Result { self.buf.lock().unwrap().write(buf) } fn flush(&mut self) -> io::Result<()> { self.buf.lock().unwrap().flush() } } #[test] fn serial_output() { let intr_evt = EventFd::new(0).unwrap(); let serial_out = SharedBuffer::new(); let mut serial = Serial::new_out( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), Box::new(serial_out.clone()), None, ); serial.write(0, DATA as u64, b"xy"); serial.write(0, DATA as u64, b"a"); serial.write(0, DATA as u64, b"b"); serial.write(0, DATA as u64, b"c"); assert_eq!(serial_out.buf.lock().unwrap().as_slice(), b"abc"); } #[test] fn serial_input() { let intr_evt = EventFd::new(0).unwrap(); let serial_out = SharedBuffer::new(); let mut serial = Serial::new_out( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), Box::new(serial_out), None, ); // write 1 to the interrupt event fd, so that read doesn't block in case the event fd // counter doesn't change (for 0 it blocks) intr_evt.write(1).unwrap(); serial.write(0, IER as u64, &[IER_RECV_BIT]); serial.queue_input_bytes(b"abc").unwrap(); assert_eq!(intr_evt.read().unwrap(), 2); // check if reading in a 2-length array doesn't have side effects let mut data = [0u8, 0u8]; serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data, [0u8, 0u8]); let mut data = [0u8]; serial.read(0, LSR as u64, &mut data[..]); assert_ne!(data[0] & LSR_DATA_BIT, 0); serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data[0], b'a'); serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data[0], b'b'); serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data[0], b'c'); // check if reading from the largest u8 offset returns 0 serial.read(0, 0xff, &mut data[..]); assert_eq!(data[0], 0); } #[test] fn serial_thr() { let intr_evt = EventFd::new(0).unwrap(); let mut serial = Serial::new_sink( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), None, ); // write 1 to the interrupt event fd, so that read doesn't block in case the event fd // counter doesn't change (for 0 it blocks) intr_evt.write(1).unwrap(); serial.write(0, IER as u64, &[IER_THR_BIT]); serial.write(0, DATA as u64, b"a"); assert_eq!(intr_evt.read().unwrap(), 2); let mut data = [0u8]; serial.read(0, IER as u64, &mut data[..]); assert_eq!(data[0] & IER_FIFO_BITS, IER_THR_BIT); serial.read(0, IIR as u64, &mut data[..]); assert_ne!(data[0] & IIR_THR_BIT, 0); } #[test] fn serial_dlab() { let intr_evt = EventFd::new(0).unwrap(); let mut serial = Serial::new_sink( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), None, ); serial.write(0, LCR as u64, &[LCR_DLAB_BIT]); serial.write(0, DLAB_LOW as u64, &[0x12]); serial.write(0, DLAB_HIGH as u64, &[0x34]); let mut data = [0u8]; serial.read(0, LCR as u64, &mut data[..]); assert_eq!(data[0], LCR_DLAB_BIT); serial.read(0, DLAB_LOW as u64, &mut data[..]); assert_eq!(data[0], 0x12); serial.read(0, DLAB_HIGH as u64, &mut data[..]); assert_eq!(data[0], 0x34); } #[test] fn serial_modem() { let intr_evt = EventFd::new(0).unwrap(); let mut serial = Serial::new_sink( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), None, ); serial.write(0, MCR as u64, &[MCR_LOOP_BIT]); serial.write(0, DATA as u64, b"a"); serial.write(0, DATA as u64, b"b"); serial.write(0, DATA as u64, b"c"); let mut data = [0u8]; serial.read(0, MSR as u64, &mut data[..]); assert_eq!(data[0], DEFAULT_MODEM_STATUS); serial.read(0, MCR as u64, &mut data[..]); assert_eq!(data[0], MCR_LOOP_BIT); serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data[0], b'a'); serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data[0], b'b'); serial.read(0, DATA as u64, &mut data[..]); assert_eq!(data[0], b'c'); } #[test] fn serial_scratch() { let intr_evt = EventFd::new(0).unwrap(); let mut serial = Serial::new_sink( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), None, ); serial.write(0, SCR as u64, &[0x12]); let mut data = [0u8]; serial.read(0, SCR as u64, &mut data[..]); assert_eq!(data[0], 0x12); } } cloud-hypervisor-53.0/devices/src/legacy/uart_pl011.rs000066400000000000000000000364201522476750100227100ustar00rootroot00000000000000// Copyright 2021 Arm Limited (or its affiliates). All rights reserved. // SPDX-License-Identifier: Apache-2.0 //! ARM PrimeCell UART(PL011) //! //! This module implements an ARM PrimeCell UART(PL011). //! use std::collections::VecDeque; 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::BusDevice; use vm_device::interrupt::InterruptSourceGroup; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::errno; use crate::{read_le_u32, write_le_u32}; /* Registers */ const UARTDR: u64 = 0; const UARTRSR_UARTECR: u64 = 1; const UARTFR: u64 = 6; const UARTILPR: u64 = 8; const UARTIBRD: u64 = 9; const UARTFBRD: u64 = 10; const UARTLCR_H: u64 = 11; const UARTCR: u64 = 12; const UARTIFLS: u64 = 13; const UARTIMSC: u64 = 14; const UARTRIS: u64 = 15; const UARTMIS: u64 = 16; const UARTICR: u64 = 17; const UARTDMACR: u64 = 18; const UARTDEBUG: u64 = 0x3c0; const PL011_INT_TX: u32 = 0x20; const PL011_INT_RX: u32 = 0x10; const PL011_FLAG_RXFF: u32 = 0x40; const PL011_FLAG_RXFE: u32 = 0x10; const PL011_ID: [u8; 8] = [0x11, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1]; // We are only interested in the margins. const AMBA_ID_LOW: u64 = 0x3f8; const AMBA_ID_HIGH: u64 = 0x401; #[derive(Debug, Error)] pub enum Error { #[error("pl011_write: Bad Write Offset: {0}")] BadWriteOffset(u64), #[error("pl011: DMA not implemented")] DmaNotImplemented, #[error("Failed to trigger interrupt")] InterruptFailure(#[source] io::Error), #[error("Failed to write")] WriteAllFailure(#[source] io::Error), #[error("Failed to flush")] FlushFailure(#[source] io::Error), } type Result = result::Result; /// A PL011 device following the PL011 specification. pub struct Pl011 { id: String, flags: u32, lcr: u32, rsr: u32, cr: u32, dmacr: u32, debug: u32, int_enabled: u32, int_level: u32, read_fifo: VecDeque, ilpr: u32, ibrd: u32, fbrd: u32, ifl: u32, read_count: u32, read_trigger: u32, irq: Arc, out: Option>, timestamp: Instant, } #[derive(Serialize, Deserialize)] pub struct Pl011State { flags: u32, lcr: u32, rsr: u32, cr: u32, dmacr: u32, debug: u32, int_enabled: u32, int_level: u32, read_fifo: Vec, ilpr: u32, ibrd: u32, fbrd: u32, ifl: u32, read_count: u32, read_trigger: u32, } impl Pl011 { /// Constructs an AMBA PL011 UART device. pub fn new( id: String, irq: Arc, out: Option>, timestamp: Instant, state: Option, ) -> Self { let ( flags, lcr, rsr, cr, dmacr, debug, int_enabled, int_level, read_fifo, ilpr, ibrd, fbrd, ifl, read_count, read_trigger, ) = if let Some(state) = state { ( state.flags, state.lcr, state.rsr, state.cr, state.dmacr, state.debug, state.int_enabled, state.int_level, state.read_fifo.into(), state.ilpr, state.ibrd, state.fbrd, state.ifl, state.read_count, state.read_trigger, ) } else { ( 0x90, 0, 0, 0x300, 0, 0, 0, 0, VecDeque::new(), 0, 0, 0, 0x12, 0, 1, ) }; Self { id, flags, lcr, rsr, cr, dmacr, debug, int_enabled, int_level, read_fifo, ilpr, ibrd, fbrd, ifl, read_count, read_trigger, irq, out, timestamp, } } pub fn set_out(&mut self, out: Option>) { self.out = out; } fn state(&self) -> Pl011State { Pl011State { flags: self.flags, lcr: self.lcr, rsr: self.rsr, cr: self.cr, dmacr: self.dmacr, debug: self.debug, int_enabled: self.int_enabled, int_level: self.int_level, read_fifo: self.read_fifo.clone().into(), ilpr: self.ilpr, ibrd: self.ibrd, fbrd: self.fbrd, ifl: self.ifl, read_count: self.read_count, read_trigger: self.read_trigger, } } /// Queues raw bytes for the guest to read and signals the interrupt pub fn queue_input_bytes(&mut self, c: &[u8]) -> errno::Result<()> { self.read_fifo.extend(c); self.read_count += c.len() as u32; self.flags &= !PL011_FLAG_RXFE; if ((self.lcr & 0x10) == 0) || (self.read_count == 16) { self.flags |= PL011_FLAG_RXFF; } if self.read_count >= self.read_trigger { self.int_level |= PL011_INT_RX; self.trigger_interrupt()?; } Ok(()) } pub fn flush_output(&mut self) -> io::Result<()> { if let Some(out) = self.out.as_mut() { out.flush()?; } Ok(()) } fn pl011_get_baudrate(&self) -> u32 { if self.fbrd == 0 { return 0; } let clk = 24_000_000; // We set the APB_PLCK to 24M in device tree (clk / ((self.ibrd << 6) + self.fbrd)) << 2 } fn pl011_trace_baudrate_change(&self) { debug!( "=== New baudrate: {:#?} (clk: {:#?}Hz, ibrd: {:#?}, fbrd: {:#?}) ===", self.pl011_get_baudrate(), 24_000_000, // We set the APB_PLCK to 24M in device tree self.ibrd, self.fbrd ); } fn pl011_set_read_trigger(&mut self) { self.read_trigger = 1; } fn handle_write(&mut self, offset: u64, val: u32) -> Result<()> { match offset >> 2 { UARTDR => { self.int_level |= PL011_INT_TX; if let Some(out) = self.out.as_mut() { out.write_all(&[val.to_le_bytes()[0]]) .map_err(Error::WriteAllFailure)?; out.flush().map_err(Error::FlushFailure)?; } } UARTRSR_UARTECR => { self.rsr = 0; } UARTFR => { /* Writes to Flag register are ignored.*/ } UARTILPR => { self.ilpr = val; } UARTIBRD => { self.ibrd = val; self.pl011_trace_baudrate_change(); } UARTFBRD => { self.fbrd = val; self.pl011_trace_baudrate_change(); } UARTLCR_H => { /* Reset the FIFO state on FIFO enable or disable */ if ((self.lcr ^ val) & 0x10) != 0 { self.read_count = 0; } self.lcr = val; self.pl011_set_read_trigger(); } UARTCR => { self.cr = val; } UARTIFLS => { self.ifl = val; self.pl011_set_read_trigger(); } UARTIMSC => { self.int_enabled = val; self.trigger_interrupt().map_err(Error::InterruptFailure)?; } UARTICR => { self.int_level &= !val; self.trigger_interrupt().map_err(Error::InterruptFailure)?; } UARTDMACR => { self.dmacr = val; if (val & 3) != 0 { return Err(Error::DmaNotImplemented); } } UARTDEBUG => { self.debug = val; self.handle_debug(); } off => { debug!("PL011: Bad write offset, offset: {off}"); return Err(Error::BadWriteOffset(off)); } } Ok(()) } fn handle_debug(&self) { let elapsed = self.timestamp.elapsed(); match self.debug { 0x00..=0x1f => warn!( "[Debug I/O port: Firmware code: 0x{:x}] {}.{:>06} seconds", self.debug, elapsed.as_secs(), elapsed.as_micros() ), 0x20..=0x3f => warn!( "[Debug I/O port: Bootloader code: 0x{:x}] {}.{:>06} seconds", self.debug, elapsed.as_secs(), elapsed.as_micros() ), 0x40..=0x5f => warn!( "[Debug I/O port: Kernel code: 0x{:x}] {}.{:>06} seconds", self.debug, elapsed.as_secs(), elapsed.as_micros() ), 0x60..=0x7f => warn!( "[Debug I/O port: Userspace code: 0x{:x}] {}.{:>06} seconds", self.debug, elapsed.as_secs(), elapsed.as_micros() ), _ => {} } } fn trigger_interrupt(&mut self) -> io::Result<()> { self.irq.trigger(0) } } impl BusDevice for Pl011 { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { let mut read_ok = true; let v = if (AMBA_ID_LOW..AMBA_ID_HIGH).contains(&(offset >> 2)) { let index = ((offset - 0xfe0) >> 2) as usize; u32::from(PL011_ID[index]) } else { match offset >> 2 { UARTDR => { self.flags &= !PL011_FLAG_RXFF; let c: u32 = self.read_fifo.pop_front().unwrap_or_default().into(); if self.read_count > 0 { self.read_count -= 1; } if self.read_count == 0 { self.flags |= PL011_FLAG_RXFE; } if self.read_count == (self.read_trigger - 1) { self.int_level &= !PL011_INT_RX; } self.rsr = c >> 8; c } UARTRSR_UARTECR => self.rsr, UARTFR => self.flags, UARTILPR => self.ilpr, UARTIBRD => self.ibrd, UARTFBRD => self.fbrd, UARTLCR_H => self.lcr, UARTCR => self.cr, UARTIFLS => self.ifl, UARTIMSC => self.int_enabled, UARTRIS => self.int_level, UARTMIS => self.int_level & self.int_enabled, UARTDMACR => self.dmacr, UARTDEBUG => self.debug, _ => { read_ok = false; 0 } } }; if read_ok && data.len() <= 4 { write_le_u32(data, v); } else { warn!( "Invalid PL011 read: offset {}, data length {}", offset, data.len() ); } } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { 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}"); } } else { warn!( "Invalid PL011 write: offset {offset}, data length {}", data.len() ); } None } } impl Snapshottable for Pl011 { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> result::Result { Snapshot::new_from_state(&self.state()) } } impl Pausable for Pl011 {} impl Transportable for Pl011 {} impl Migratable for Pl011 {} #[cfg(test)] mod unit_tests { use std::sync::Mutex; use vm_device::interrupt::{InterruptIndex, InterruptSourceConfig}; use vmm_sys_util::eventfd::EventFd; use super::*; const SERIAL_NAME: &str = "serial"; struct TestInterrupt { event_fd: EventFd, } impl InterruptSourceGroup for TestInterrupt { fn trigger(&self, _index: InterruptIndex) -> io::Result<()> { self.event_fd.write(1) } fn update( &self, _index: InterruptIndex, _config: InterruptSourceConfig, _masked: bool, _set_gsi: bool, ) -> io::Result<()> { Ok(()) } fn set_gsi(&self) -> io::Result<()> { 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 } } } #[derive(Clone)] struct SharedBuffer { buf: Arc>>, } impl SharedBuffer { fn new() -> SharedBuffer { SharedBuffer { buf: Arc::new(Mutex::new(Vec::new())), } } } impl io::Write for SharedBuffer { fn write(&mut self, buf: &[u8]) -> io::Result { self.buf.lock().unwrap().write(buf) } fn flush(&mut self) -> io::Result<()> { self.buf.lock().unwrap().flush() } } #[test] fn pl011_output() { let intr_evt = EventFd::new(0).unwrap(); let pl011_out = SharedBuffer::new(); let mut pl011 = Pl011::new( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), Some(Box::new(pl011_out.clone())), Instant::now(), None, ); pl011.write(0, UARTDR, b"xy"); pl011.write(0, UARTDR, b"a"); pl011.write(0, UARTDR, b"b"); pl011.write(0, UARTDR, b"c"); assert_eq!(pl011_out.buf.lock().unwrap().as_slice(), b"xabc"); } #[test] fn pl011_input() { let intr_evt = EventFd::new(0).unwrap(); let pl011_out = SharedBuffer::new(); let mut pl011 = Pl011::new( String::from(SERIAL_NAME), Arc::new(TestInterrupt::new(intr_evt.try_clone().unwrap())), Some(Box::new(pl011_out)), Instant::now(), None, ); // write 1 to the interrupt event fd, so that read doesn't block in case the event fd // counter doesn't change (for 0 it blocks) intr_evt.write(1).unwrap(); pl011.queue_input_bytes(b"abc").unwrap(); assert_eq!(intr_evt.read().unwrap(), 2); let mut data = [0u8]; pl011.read(0, UARTDR, &mut data); assert_eq!(data[0], b'a'); pl011.read(0, UARTDR, &mut data); assert_eq!(data[0], b'b'); pl011.read(0, UARTDR, &mut data); assert_eq!(data[0], b'c'); } } cloud-hypervisor-53.0/devices/src/lib.rs000066400000000000000000000064271522476750100203260ustar00rootroot00000000000000// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // // Portions Copyright 2017 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE-BSD-3-Clause file. //! Emulates virtual and hardware devices. pub mod acpi; #[cfg(target_arch = "riscv64")] pub mod aia; #[cfg(target_arch = "x86_64")] pub mod debug_console; #[cfg(target_arch = "aarch64")] 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; pub mod pvpanic; // TODO: TPM is not yet supported #[cfg(not(target_arch = "riscv64"))] pub mod tpm; use bitflags::bitflags; pub use self::acpi::{AcpiGedDevice, AcpiPmTimerDevice, AcpiShutdownDevice}; #[cfg(feature = "ivshmem")] pub use self::ivshmem::IvshmemDevice; pub use self::pvpanic::{PVPANIC_DEVICE_MMIO_SIZE, PvPanicDevice}; bitflags! { pub struct AcpiNotificationFlags: u8 { const NO_DEVICES_CHANGED = 0; const CPU_DEVICES_CHANGED = 0b1; const MEMORY_DEVICES_CHANGED = 0b10; const PCI_DEVICES_CHANGED = 0b100; const POWER_BUTTON_CHANGED = 0b1000; } } #[cfg(target_arch = "aarch64")] macro_rules! generate_read_fn { ($fn_name: ident, $data_type: ty, $byte_type: ty, $type_size: expr, $endian_type: ident) => { pub fn $fn_name(input: &[$byte_type]) -> $data_type { assert!($type_size == size_of::<$data_type>()); let mut array = [0u8; $type_size]; for (byte, read) in array.iter_mut().zip(input.iter().cloned()) { *byte = read as u8; } <$data_type>::$endian_type(array) } }; } #[cfg(target_arch = "aarch64")] macro_rules! generate_write_fn { ($fn_name: ident, $data_type: ty, $byte_type: ty, $endian_type: ident) => { pub fn $fn_name(buf: &mut [$byte_type], n: $data_type) { for (byte, read) in buf .iter_mut() .zip(<$data_type>::$endian_type(n).iter().cloned()) { *byte = read as $byte_type; } } }; } #[cfg(target_arch = "aarch64")] generate_read_fn!(read_le_u16, u16, u8, 2, from_le_bytes); #[cfg(target_arch = "aarch64")] generate_read_fn!(read_le_u32, u32, u8, 4, from_le_bytes); #[cfg(target_arch = "aarch64")] generate_read_fn!(read_le_u64, u64, u8, 8, from_le_bytes); #[cfg(target_arch = "aarch64")] generate_read_fn!(read_le_i32, i32, i8, 4, from_le_bytes); #[cfg(target_arch = "aarch64")] generate_read_fn!(read_be_u16, u16, u8, 2, from_be_bytes); #[cfg(target_arch = "aarch64")] generate_read_fn!(read_be_u32, u32, u8, 4, from_be_bytes); #[cfg(target_arch = "aarch64")] generate_write_fn!(write_le_u16, u16, u8, to_le_bytes); #[cfg(target_arch = "aarch64")] generate_write_fn!(write_le_u32, u32, u8, to_le_bytes); #[cfg(target_arch = "aarch64")] generate_write_fn!(write_le_u64, u64, u8, to_le_bytes); #[cfg(target_arch = "aarch64")] generate_write_fn!(write_le_i32, i32, i8, to_le_bytes); #[cfg(target_arch = "aarch64")] generate_write_fn!(write_be_u16, u16, u8, to_be_bytes); #[cfg(target_arch = "aarch64")] generate_write_fn!(write_be_u32, u32, u8, to_be_bytes); cloud-hypervisor-53.0/devices/src/pvmemcontrol.rs000066400000000000000000000630511522476750100223010ustar00rootroot00000000000000// Copyright © 2024 Google LLC // // SPDX-License-Identifier: Apache-2.0 // use std::any::Any; use std::collections::HashMap; use std::ffi::CString; use std::sync::{Arc, Barrier, RwLock}; use std::{fmt, io, mem, ptr, result}; use log::{debug, warn}; use num_enum::TryFromPrimitive; use pci::{ BarReprogrammingParams, PciBarConfiguration, PciBarPrefetchable, PciBarRegionType, PciClassCode, PciConfiguration, PciDevice, PciDeviceError, PciHeaderType, PciSubclass, }; use thiserror::Error; use vm_allocator::page_size::get_page_size; use vm_allocator::{AddressAllocator, SystemAllocator}; use vm_device::{BusDeviceSync, Resource}; use vm_memory::bitmap::AtomicBitmap; use vm_memory::{ Address, ByteValued, Bytes, GuestAddress, GuestAddressSpace, GuestMemory, GuestMemoryAtomic, GuestMemoryError, GuestMemoryMmap, Le32, Le64, }; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; const PVMEMCONTROL_VENDOR_ID: u16 = 0x1ae0; const PVMEMCONTROL_DEVICE_ID: u16 = 0x0087; const PVMEMCONTROL_SUBSYSTEM_VENDOR_ID: u16 = 0x1ae0; const PVMEMCONTROL_SUBSYSTEM_ID: u16 = 0x011F; const MAJOR_VERSION: u64 = 1; const MINOR_VERSION: u64 = 0; #[derive(Error, Debug)] pub enum Error { // device errors #[error("Guest gave us bad memory addresses")] GuestMemory(#[source] GuestMemoryError), #[error("Guest sent us invalid request")] InvalidRequest, #[error("Guest sent us invalid command: {0}")] InvalidCommand(u32), #[error("Guest sent us invalid connection: {0}")] InvalidConnection(u32), // pvmemcontrol errors #[error("Request contains invalid arguments: {0}")] InvalidArgument(u64), #[error("Unknown function code: {0}")] UnknownFunctionCode(u64), #[error("Libc call fail")] LibcFail(#[source] io::Error), } #[derive(Copy, Clone)] enum PvmemcontrolSubclass { Other = 0x80, } impl PciSubclass for PvmemcontrolSubclass { fn get_register_value(&self) -> u8 { *self as u8 } } /// commands have 0 as the most significant byte #[repr(u32)] #[derive(PartialEq, Eq, Copy, Clone, TryFromPrimitive)] enum PvmemcontrolTransportCommand { Reset = 0x060f_e6d2, Register = 0x0e35_9539, Ready = 0x0ca8_d227, Disconnect = 0x030f_5da0, Ack = 0x03cf_5196, Error = 0x01fb_a249, } #[repr(C)] #[derive(Copy, Clone)] struct PvmemcontrolTransportRegister { buf_phys_addr: Le64, } #[repr(C)] #[derive(Copy, Clone)] struct PvmemcontrolTransportRegisterResponse { command: Le32, _padding: u32, } #[repr(C)] #[derive(Copy, Clone)] union PvmemcontrolTransportUnion { register: PvmemcontrolTransportRegister, register_response: PvmemcontrolTransportRegisterResponse, unit: (), } #[repr(C)] #[derive(Copy, Clone)] struct PvmemcontrolTransport { payload: PvmemcontrolTransportUnion, command: PvmemcontrolTransportCommand, } const PVMEMCONTROL_DEVICE_MMIO_SIZE: u64 = size_of::() as u64; const PVMEMCONTROL_DEVICE_MMIO_ALIGN: u64 = align_of::() as u64; impl PvmemcontrolTransport { fn ack() -> Self { PvmemcontrolTransport { payload: PvmemcontrolTransportUnion { unit: () }, command: PvmemcontrolTransportCommand::Ack, } } fn error() -> Self { PvmemcontrolTransport { payload: PvmemcontrolTransportUnion { unit: () }, command: PvmemcontrolTransportCommand::Error, } } fn register_response(command: u32) -> Self { PvmemcontrolTransport { payload: PvmemcontrolTransportUnion { register_response: PvmemcontrolTransportRegisterResponse { command: command.into(), _padding: 0, }, }, command: PvmemcontrolTransportCommand::Ack, } } unsafe fn as_register(self) -> PvmemcontrolTransportRegister { // SAFETY: We access initialized data. unsafe { self.payload.register } } } // SAFETY: Contains no references and does not have compiler-inserted padding unsafe impl ByteValued for PvmemcontrolTransportUnion {} // SAFETY: Contains no references and does not have compiler-inserted padding unsafe impl ByteValued for PvmemcontrolTransport {} #[repr(u64)] #[derive(Copy, Clone, TryFromPrimitive, Debug)] enum FunctionCode { Info = 0, Dontneed = 1, Remove = 2, Free = 3, Pageout = 4, Dontdump = 5, SetVMAAnonName = 6, Mlock = 7, Munlock = 8, MprotectNone = 9, MprotectR = 10, MprotectW = 11, MprotectRW = 12, Mergeable = 13, Unmergeable = 14, } #[repr(C)] #[derive(Copy, Clone, Debug, Default)] struct PvmemcontrolReq { func_code: Le64, addr: Le64, length: Le64, arg: Le64, } // SAFETY: it only has data and has no implicit padding. unsafe impl ByteValued for PvmemcontrolReq {} #[repr(C)] #[derive(Copy, Clone, Default)] struct PvmemcontrolResp { ret_errno: Le32, ret_code: Le32, ret_value: Le64, arg0: Le64, arg1: Le64, } impl fmt::Debug for PvmemcontrolResp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let PvmemcontrolResp { ret_errno, ret_code, .. } = self; write!( f, "PvmemcontrolResp {{ ret_errno: {}, ret_code: {}, .. }}", ret_errno.to_native(), ret_code.to_native() ) } } // SAFETY: it only has data and has no implicit padding. unsafe impl ByteValued for PvmemcontrolResp {} /// The guest connections start at 0x8000_0000, which has a leading 1 in /// the most significant byte, this ensures it does not conflict with /// any of the transport commands #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug)] pub struct GuestConnection { command: u32, } impl Default for GuestConnection { fn default() -> Self { GuestConnection::new(0x8000_0000) } } impl GuestConnection { fn new(command: u32) -> Self { Self { command } } fn next(&self) -> Self { let GuestConnection { command } = *self; if command == u32::MAX { GuestConnection::default() } else { GuestConnection::new(command + 1) } } } impl TryFrom for GuestConnection { type Error = Error; fn try_from(value: u32) -> Result { if (value & 0x8000_0000) != 0 { Ok(GuestConnection::new(value)) } else { Err(Error::InvalidConnection(value)) } } } struct PercpuInitState { port_buf_map: HashMap, next_conn: GuestConnection, } impl PercpuInitState { fn new() -> Self { PercpuInitState { port_buf_map: HashMap::new(), next_conn: GuestConnection::default(), } } } enum PvmemcontrolState { PercpuInit(PercpuInitState), Ready(HashMap), Broken, } pub struct PvmemcontrolDevice { transport: PvmemcontrolTransport, state: PvmemcontrolState, } impl PvmemcontrolDevice { fn new(transport: PvmemcontrolTransport, state: PvmemcontrolState) -> Self { PvmemcontrolDevice { transport, state } } } impl PvmemcontrolDevice { fn register_percpu_buf( guest_memory: &GuestMemoryAtomic>, mut state: PercpuInitState, PvmemcontrolTransportRegister { buf_phys_addr }: PvmemcontrolTransportRegister, ) -> Self { // access to this address is checked let buf_phys_addr = GuestAddress(buf_phys_addr.into()); if !guest_memory.memory().check_range( buf_phys_addr, size_of::().max(size_of::()), ) { warn!("guest sent invalid phys addr {:#x}", buf_phys_addr.0); return PvmemcontrolDevice::new( PvmemcontrolTransport::error(), PvmemcontrolState::Broken, ); } let conn = { // find an available port+byte combination, and fail if full let mut next_conn = state.next_conn; while state.port_buf_map.contains_key(&next_conn) { next_conn = next_conn.next(); if next_conn == state.next_conn { warn!("connections exhausted"); return PvmemcontrolDevice::new( PvmemcontrolTransport::error(), PvmemcontrolState::Broken, ); } } next_conn }; state.next_conn = conn.next(); state.port_buf_map.insert(conn, buf_phys_addr); // inform guest of the connection let response = PvmemcontrolTransport::register_response(conn.command); PvmemcontrolDevice::new(response, PvmemcontrolState::PercpuInit(state)) } fn reset() -> Self { PvmemcontrolDevice::new( PvmemcontrolTransport::ack(), PvmemcontrolState::PercpuInit(PercpuInitState::new()), ) } fn error() -> Self { PvmemcontrolDevice::new(PvmemcontrolTransport::error(), PvmemcontrolState::Broken) } fn ready(PercpuInitState { port_buf_map, .. }: PercpuInitState) -> Self { PvmemcontrolDevice::new( PvmemcontrolTransport::ack(), PvmemcontrolState::Ready(port_buf_map), ) } fn run_command( &mut self, guest_memory: &GuestMemoryAtomic>, command: PvmemcontrolTransportCommand, ) { let state = mem::replace(&mut self.state, PvmemcontrolState::Broken); *self = match command { PvmemcontrolTransportCommand::Reset => Self::reset(), PvmemcontrolTransportCommand::Register => { if let PvmemcontrolState::PercpuInit(state) = state { // SAFETY: By device protocol. If driver is wrong the device // can enter a Broken state, but the behavior is still sound. Self::register_percpu_buf(guest_memory, state, unsafe { self.transport.as_register() }) } else { debug!("received register without reset"); Self::error() } } PvmemcontrolTransportCommand::Ready => { if let PvmemcontrolState::PercpuInit(state) = state { Self::ready(state) } else { debug!("received ready without reset"); Self::error() } } PvmemcontrolTransportCommand::Disconnect => Self::error(), PvmemcontrolTransportCommand::Ack => { debug!("received ack as command"); Self::error() } PvmemcontrolTransportCommand::Error => { debug!("received error as command"); Self::error() } } } /// read from the transport fn read_transport(&self, offset: u64, data: &mut [u8]) { self.transport .as_slice() .iter() .skip(offset as usize) .zip(data.iter_mut()) .for_each(|(src, dest)| *dest = *src); } /// can only write to transport payload /// command is a special register that needs separate dispatching fn write_transport(&mut self, offset: u64, data: &[u8]) { self.transport .payload .as_mut_slice() .iter_mut() .skip(offset as usize) .zip(data.iter()) .for_each(|(dest, src)| *dest = *src); } fn find_connection(&self, conn: GuestConnection) -> Option { match &self.state { PvmemcontrolState::Ready(map) => map.get(&conn).copied(), _ => None, } } } pub struct PvmemcontrolBusDevice { mem: GuestMemoryAtomic>, dev: RwLock, } pub struct PvmemcontrolPciDevice { id: String, configuration: PciConfiguration, bar_regions: Vec, } impl PvmemcontrolBusDevice { /// f is called with the host address of `range_base` and only when /// [`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, 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. let Ok(slice) = memory.get_slice(range_base, range_len) else { return Err(Error::GuestMemory(GuestMemoryError::InvalidGuestAddress( range_base, ))); }; 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())); } Ok(()) } fn madvise(&self, addr: u64, length: u64, advice: libc::c_int) -> result::Result<(), Error> { // SAFETY: [`base`, `base` + `len`) is guest memory self.operate_on_memory_range(addr, length, |base, len| unsafe { libc::madvise(base, len, advice) }) } fn mlock(&self, addr: u64, length: u64, on_default: bool) -> result::Result<(), Error> { // SAFETY: [`base`, `base` + `len`) is guest memory self.operate_on_memory_range(addr, length, |base, len| unsafe { libc::mlock2(base, len, if on_default { libc::MLOCK_ONFAULT } else { 0 }) }) } fn munlock(&self, addr: u64, length: u64) -> result::Result<(), Error> { // SAFETY: [`base`, `base` + `len`) is guest memory self.operate_on_memory_range(addr, length, |base, len| unsafe { libc::munlock(base, len) }) } fn mprotect( &self, addr: u64, length: u64, protection: libc::c_int, ) -> result::Result<(), Error> { // SAFETY: [`base`, `base` + `len`) is guest memory self.operate_on_memory_range(addr, length, |base, len| unsafe { libc::mprotect(base, len, protection) }) } fn set_vma_anon_name(&self, addr: u64, length: u64, name: u64) -> result::Result<(), Error> { let name = (name != 0).then(|| CString::new(format!("pvmemcontrol-{name}")).unwrap()); let name_ptr = if let Some(name) = &name { name.as_ptr() } else { ptr::null() }; 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 { libc::prctl( libc::PR_SET_VMA, libc::PR_SET_VMA_ANON_NAME, base, len, name_ptr, ) }) } fn process_request( &self, func_code: FunctionCode, addr: u64, length: u64, arg: u64, ) -> Result { let result = match func_code { FunctionCode::Info => { return Ok(PvmemcontrolResp { ret_errno: 0.into(), ret_code: 0.into(), 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), FunctionCode::Free => self.madvise(addr, length, libc::MADV_FREE), FunctionCode::Pageout => self.madvise(addr, length, libc::MADV_PAGEOUT), FunctionCode::Dontdump => self.madvise(addr, length, libc::MADV_DONTDUMP), FunctionCode::SetVMAAnonName => self.set_vma_anon_name(addr, length, arg), FunctionCode::Mlock => self.mlock(addr, length, false), FunctionCode::Munlock => self.munlock(addr, length), FunctionCode::MprotectNone => self.mprotect(addr, length, libc::PROT_NONE), FunctionCode::MprotectR => self.mprotect(addr, length, libc::PROT_READ), FunctionCode::MprotectW => self.mprotect(addr, length, libc::PROT_WRITE), FunctionCode::MprotectRW => { self.mprotect(addr, length, libc::PROT_READ | libc::PROT_WRITE) } FunctionCode::Mergeable => self.madvise(addr, length, libc::MADV_MERGEABLE), FunctionCode::Unmergeable => self.madvise(addr, length, libc::MADV_UNMERGEABLE), }; result.map(|_| PvmemcontrolResp::default()) } fn handle_request( &self, PvmemcontrolReq { func_code, addr, length, arg, }: PvmemcontrolReq, ) -> Result { let (func_code, addr, length, arg) = ( func_code.to_native(), addr.to_native(), length.to_native(), arg.to_native(), ); let resp_or_err = FunctionCode::try_from(func_code) .map_err(|_| Error::UnknownFunctionCode(func_code)) .and_then(|func_code| self.process_request(func_code, addr, length, arg)); let resp = match resp_or_err { Ok(resp) => resp, Err(e) => match e { Error::InvalidArgument(arg) => PvmemcontrolResp { ret_errno: (libc::EINVAL as u32).into(), ret_code: (arg as u32).into(), ..Default::default() }, Error::LibcFail(err) => PvmemcontrolResp { ret_errno: (err.raw_os_error().unwrap_or(libc::EFAULT) as u32).into(), ret_code: 0u32.into(), ..Default::default() }, Error::UnknownFunctionCode(func_code) => PvmemcontrolResp { ret_errno: (libc::EOPNOTSUPP as u32).into(), ret_code: (func_code as u32).into(), ..Default::default() }, Error::GuestMemory(err) => { warn!("{err}"); PvmemcontrolResp { ret_errno: (libc::EINVAL as u32).into(), ret_code: (func_code as u32).into(), ..Default::default() } } // device error, stop responding other => return Err(other), }, }; Ok(resp) } fn handle_pvmemcontrol_request(&self, guest_addr: GuestAddress) { let request: PvmemcontrolReq = if let Ok(x) = self.mem.memory().read_obj(guest_addr) { x } else { warn!("cannot read from guest address {:#x}", guest_addr.0); return; }; let response: PvmemcontrolResp = match self.handle_request(request) { Ok(x) => x, Err(e) => { warn!("cannot process request {request:?} with error {e}"); return; } }; if self.mem.memory().write_obj(response, guest_addr).is_err() { warn!("cannot write to guest address {:#x}", guest_addr.0); } } fn handle_guest_write(&self, offset: u64, data: &[u8]) { if offset as usize != mem::offset_of!(PvmemcontrolTransport, command) { if data.len() != 4 && data.len() != 8 { warn!("guest write is not 4 or 8 bytes long"); return; } self.dev.write().unwrap().write_transport(offset, data); return; } let data = if data.len() == 4 { let mut d = [0u8; 4]; d.iter_mut() .zip(data.iter()) .for_each(|(d, data)| *d = *data); d } else { warn!("guest write with non u32 at command register"); return; }; let data_cmd = u32::from_le_bytes(data); let command = PvmemcontrolTransportCommand::try_from(data_cmd); match command { Ok(command) => self.dev.write().unwrap().run_command(&self.mem, command), Err(_) => { GuestConnection::try_from(data_cmd) .and_then(|conn| { self.dev .read() .unwrap() .find_connection(conn) .ok_or(Error::InvalidConnection(conn.command)) }) .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); } } impl PvmemcontrolDevice { pub fn make_device( id: String, mem: GuestMemoryAtomic>, ) -> (PvmemcontrolPciDevice, PvmemcontrolBusDevice) { let dev = RwLock::new(PvmemcontrolDevice::error()); let mut configuration = PciConfiguration::new( PVMEMCONTROL_VENDOR_ID, PVMEMCONTROL_DEVICE_ID, 0x1, PciClassCode::BaseSystemPeripheral, &PvmemcontrolSubclass::Other, None, PciHeaderType::Device, PVMEMCONTROL_SUBSYSTEM_VENDOR_ID, PVMEMCONTROL_SUBSYSTEM_ID, None, None, ); let command: [u8; 2] = [0x03, 0x01]; // memory, io, SERR# configuration.write_config_register(1, 0, &command); ( PvmemcontrolPciDevice { id, configuration, bar_regions: Vec::new(), }, PvmemcontrolBusDevice { mem, dev }, ) } } impl PciDevice for PvmemcontrolPciDevice { 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_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 Any { self } fn id(&self) -> Option { Some(self.id.clone()) } fn allocate_bars( &mut self, _allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, _mmio64_allocator: &mut AddressAllocator, resources: Option>, ) -> Result, PciDeviceError> { let mut bars = Vec::new(); let region_type = PciBarRegionType::Memory32BitRegion; let bar_id = 0; let region_size = PVMEMCONTROL_DEVICE_MMIO_SIZE; let restoring = resources.is_some(); let bar_addr = mmio32_allocator .allocate(None, region_size, Some(PVMEMCONTROL_DEVICE_MMIO_ALIGN)) .ok_or(PciDeviceError::IoAllocationFailed(region_size))?; let bar = PciBarConfiguration::default() .set_index(bar_id as usize) .set_address(bar_addr.raw_value()) .set_size(region_size) .set_region_type(region_type) .set_prefetchable(PciBarPrefetchable::NotPrefetchable); if !restoring { self.configuration .add_pci_bar(&bar) .map_err(|e| PciDeviceError::IoRegistrationFailed(bar_addr.raw_value(), e))?; } bars.push(bar); self.bar_regions.clone_from(&bars); Ok(bars) } fn free_bars( &mut self, _allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, _mmio64_allocator: &mut AddressAllocator, ) -> Result<(), PciDeviceError> { for bar in self.bar_regions.drain(..) { mmio32_allocator.free(GuestAddress(bar.addr()), bar.size()); } Ok(()) } fn move_bar(&mut self, old_base: u64, new_base: u64) -> io::Result<()> { for bar in self.bar_regions.iter_mut() { if bar.addr() == old_base { *bar = bar.set_address(new_base); } } Ok(()) } } impl Pausable for PvmemcontrolPciDevice { fn pause(&mut self) -> result::Result<(), MigratableError> { Ok(()) } fn resume(&mut self) -> result::Result<(), MigratableError> { Ok(()) } } impl Snapshottable for PvmemcontrolPciDevice { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> result::Result { let mut snapshot = Snapshot::new_from_state(&())?; // Snapshot PciConfiguration snapshot.add_snapshot(self.configuration.id(), self.configuration.snapshot()?); Ok(snapshot) } } impl Transportable for PvmemcontrolPciDevice {} impl Migratable for PvmemcontrolPciDevice {} impl BusDeviceSync for PvmemcontrolBusDevice { fn read(&self, _base: u64, offset: u64, data: &mut [u8]) { self.handle_guest_read(offset, data); } fn write(&self, _base: u64, offset: u64, data: &[u8]) -> Option> { self.handle_guest_write(offset, data); None } } cloud-hypervisor-53.0/devices/src/pvpanic.rs000066400000000000000000000171051522476750100212130ustar00rootroot00000000000000// Copyright © 2023 Tencent Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::any::Any; use std::sync::{Arc, Barrier}; use std::{io, result}; use anyhow::anyhow; use event_monitor::event; use log::{debug, info}; 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}; use vm_memory::{Address, GuestAddress}; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; const PVPANIC_VENDOR_ID: u16 = 0x1b36; const PVPANIC_DEVICE_ID: u16 = 0x0011; pub const PVPANIC_DEVICE_MMIO_SIZE: u64 = 0x2; pub const PVPANIC_DEVICE_MMIO_ALIGNMENT: u64 = 0x10; const PVPANIC_PANICKED: u8 = 1 << 0; const PVPANIC_CRASH_LOADED: u8 = 1 << 1; #[derive(Debug, Error)] pub enum PvPanicError { #[error("Failed creating PvPanicDevice")] CreatePvPanicDevice(#[source] anyhow::Error), #[error("Failed to retrieve PciConfigurationState")] RetrievePciConfigurationState(#[source] anyhow::Error), } #[derive(Copy, Clone)] enum PvPanicSubclass { Other = 0x80, } impl PciSubclass for PvPanicSubclass { fn get_register_value(&self) -> u8 { *self as u8 } } /// A device for handling guest panic event pub struct PvPanicDevice { id: String, events: u8, // PCI configuration registers. configuration: PciConfiguration, bar_regions: Vec, } #[derive(Serialize, Deserialize)] pub struct PvPanicDeviceState { events: u8, } impl PvPanicDevice { 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, PVPANIC_DEVICE_ID, 0x1, // modern pci devices PciClassCode::BaseSystemPeripheral, &PvPanicSubclass::Other, None, PciHeaderType::Device, 0, 0, None, pci_configuration_state, ); let command: [u8; 2] = [0x03, 0x01]; let bar_reprogram = configuration.write_config_register(1, 0, &command); assert!( bar_reprogram.is_empty(), "No bar reprogrammig is expected from writing to the COMMAND register" ); let state: Option = snapshot .as_ref() .map(|s| s.to_state()) .transpose() .map_err(|e| { PvPanicError::CreatePvPanicDevice(anyhow!( "Failed to get PvPanicDeviceState from Snapshot: {e}" )) })?; let events = if let Some(state) = state { state.events } else { PVPANIC_PANICKED | PVPANIC_CRASH_LOADED }; let pvpanic_device = PvPanicDevice { id, events, configuration, bar_regions: vec![], }; Ok(pvpanic_device) } pub fn event_to_string(&self, event: u8) -> String { if event == PVPANIC_PANICKED { "panic".to_string() } else if event == PVPANIC_CRASH_LOADED { "crash_loaded".to_string() } else { "unknown_event".to_string() } } fn state(&self) -> PvPanicDeviceState { PvPanicDeviceState { events: self.events, } } pub fn config_bar_addr(&self) -> u64 { self.configuration.get_bar_addr(0) } } impl BusDevice for PvPanicDevice { 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> { let event = self.event_to_string(data[0]); info!("pvpanic got guest event {event}"); event!("guest", "panic", "event", &event); None } } impl PciDevice for PvPanicDevice { 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 allocate_bars( &mut self, _allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, _mmio64_allocator: &mut AddressAllocator, resources: Option>, ) -> result::Result, PciDeviceError> { let mut bars = Vec::new(); let region_type = PciBarRegionType::Memory32BitRegion; let bar_id = 0; let region_size = PVPANIC_DEVICE_MMIO_SIZE; let restoring = resources.is_some(); let bar_addr = mmio32_allocator .allocate(None, region_size, Some(PVPANIC_DEVICE_MMIO_ALIGNMENT)) .ok_or(PciDeviceError::IoAllocationFailed(region_size))?; let bar = PciBarConfiguration::default() .set_index(bar_id as usize) .set_address(bar_addr.raw_value()) .set_size(region_size) .set_region_type(region_type) .set_prefetchable(PciBarPrefetchable::NotPrefetchable); debug!("pvpanic bar address 0x{:x}", bar_addr.0); if !restoring { self.configuration .add_pci_bar(&bar) .map_err(|e| PciDeviceError::IoRegistrationFailed(bar_addr.raw_value(), e))?; } bars.push(bar); self.bar_regions.clone_from(&bars); Ok(bars) } fn free_bars( &mut self, _allocator: &mut SystemAllocator, mmio32_allocator: &mut AddressAllocator, _mmio64_allocator: &mut AddressAllocator, ) -> result::Result<(), PciDeviceError> { for bar in self.bar_regions.drain(..) { mmio32_allocator.free(GuestAddress(bar.addr()), bar.size()); } Ok(()) } fn move_bar(&mut self, old_base: u64, new_base: u64) -> io::Result<()> { 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 read_bar(&mut self, _base: u64, _offset: u64, data: &mut [u8]) { data[0] = self.events; } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn id(&self) -> Option { Some(self.id.clone()) } } impl Pausable for PvPanicDevice {} impl Snapshottable for PvPanicDevice { fn id(&self) -> String { self.id.clone() } fn snapshot(&mut self) -> 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 PvPanicDevice {} impl Migratable for PvPanicDevice {} cloud-hypervisor-53.0/devices/src/tpm.rs000066400000000000000000000532171522476750100203570ustar00rootroot00000000000000// Copyright © 2022, Microsoft Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::cmp; use std::ops::Range; use std::path::Path; use std::sync::{Arc, Barrier}; use anyhow::anyhow; #[cfg(target_arch = "aarch64")] 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}; use thiserror::Error; use tpm::TPM_CRB_BUFFER_MAX; use tpm::emulator::{BackendCmd, Emulator}; use vm_device::BusDevice; #[derive(Error, Debug)] pub enum Error { #[error("Emulator doesn't implement min required capabilities")] CheckCaps(#[source] anyhow::Error), #[error("Failed to initialize tpm")] Init(#[source] anyhow::Error), } type Result = anyhow::Result; #[expect(dead_code)] #[derive(Copy, Clone)] enum LocStateFields { TpmEstablished, LocAssigned, ActiveLocality, Reserved, TpmRegValidSts, } #[derive(Copy, Clone)] enum LocStsFields { Granted, BeenSeized, } #[expect(dead_code)] #[derive(Copy, Clone)] enum IntfIdFields { InterfaceType, InterfaceVersion, CapLocality, CapCRBIdleBypass, Reserved1, CapDataXferSizeSupport, CapFIFO, CapCRB, CapIFRes, InterfaceSelector, IntfSelLock, Reserved2, Rid, } #[expect(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), IntfId(IntfIdFields), IntfId2(IntfId2Fields), CtrlSts(CtrlStsFields), } /* crb 32-bit registers */ const CRB_LOC_STATE: u32 = 0x0; //Register Fields // Field => (base, offset, length) // base: starting position of the register // offset: lowest bit in the bit field numbered from 0 // length: length of the bit field const fn get_crb_loc_state_field(f: LocStateFields) -> (u32, u32, u32) { let (offset, len) = match f { LocStateFields::TpmEstablished => (0, 1), LocStateFields::LocAssigned => (1, 1), LocStateFields::ActiveLocality => (2, 3), LocStateFields::Reserved => (5, 2), LocStateFields::TpmRegValidSts => (7, 1), }; (CRB_LOC_STATE, offset, len) } const CRB_LOC_CTRL: u32 = 0x08; 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), LocStsFields::BeenSeized => (1, 1), }; (CRB_LOC_STS, offset, len) } const CRB_INTF_ID: u32 = 0x30; const fn get_crb_intf_id_field(f: IntfIdFields) -> (u32, u32, u32) { let (offset, len) = match f { IntfIdFields::InterfaceType => (0, 4), IntfIdFields::InterfaceVersion => (4, 4), IntfIdFields::CapLocality => (8, 1), IntfIdFields::CapCRBIdleBypass => (9, 1), IntfIdFields::Reserved1 => (10, 1), IntfIdFields::CapDataXferSizeSupport => (11, 2), IntfIdFields::CapFIFO => (13, 1), IntfIdFields::CapCRB => (14, 1), IntfIdFields::CapIFRes => (15, 2), IntfIdFields::InterfaceSelector => (17, 2), IntfIdFields::IntfSelLock => (19, 1), IntfIdFields::Reserved2 => (20, 4), IntfIdFields::Rid => (24, 8), }; (CRB_INTF_ID, offset, len) } const CRB_INTF_ID2: u32 = 0x34; const fn get_crb_intf_id2_field(f: IntfId2Fields) -> (u32, u32, u32) { let (offset, len) = match f { IntfId2Fields::Vid => (0, 16), IntfId2Fields::Did => (16, 16), }; (CRB_INTF_ID2, offset, len) } const CRB_CTRL_REQ: u32 = 0x40; const CRB_CTRL_REQ_CMD_READY: u32 = 1 << 0; const CRB_CTRL_REQ_GO_IDLE: u32 = 1 << 1; const CRB_CTRL_STS: u32 = 0x44; const fn get_crb_ctrl_sts_field(f: CtrlStsFields) -> (u32, u32, u32) { let (offset, len) = match f { CtrlStsFields::TpmSts => (0, 1), CtrlStsFields::TpmIdle => (1, 1), }; (CRB_CTRL_STS, offset, len) } const CRB_CTRL_CANCEL: u32 = 0x48; const CRB_CANCEL_INVOKE: u32 = 1 << 0; const CRB_CTRL_START: u32 = 0x4C; const CRB_START_INVOKE: u32 = 1 << 0; const CRB_CTRL_CMD_LADDR: u32 = 0x5C; const CRB_CTRL_CMD_HADDR: u32 = 0x60; const CRB_CTRL_RSP_SIZE: u32 = 0x64; const CRB_CTRL_RSP_ADDR: u32 = 0x68; const CRB_DATA_BUFFER: u32 = 0x80; const TPM_CRB_NO_LOCALITY: u32 = 0xff; const TPM_CRB_ADDR_BASE: u32 = TPM_START.0 as u32; const TPM_CRB_ADDR_SIZE: usize = TPM_SIZE as usize; const TPM_CRB_R_MAX: usize = CRB_DATA_BUFFER as usize; // CRB Protocol details const CRB_INTF_TYPE_CRB_ACTIVE: u32 = 0b1; const CRB_INTF_VERSION_CRB: u32 = 0b1; const CRB_INTF_CAP_LOCALITY_0_ONLY: u32 = 0b0; const CRB_INTF_CAP_IDLE_FAST: u32 = 0b0; const CRB_INTF_CAP_XFER_SIZE_64: u32 = 0b11; const CRB_INTF_CAP_FIFO_NOT_SUPPORTED: u32 = 0b0; const CRB_INTF_CAP_CRB_SUPPORTED: u32 = 0b1; const CRB_INTF_IF_SELECTOR_CRB: u32 = 0b1; const PCI_VENDOR_ID_IBM: u32 = 0x1014; const CRB_CTRL_CMD_SIZE_REG: u32 = 0x58; const CRB_CTRL_CMD_SIZE: usize = TPM_CRB_ADDR_SIZE - CRB_DATA_BUFFER as usize; // Returns (register base, offset, len) const fn get_field(reg: CrbRegister) -> (u32, u32, u32) { match reg { CrbRegister::LocState(f) => get_crb_loc_state_field(f), CrbRegister::LocSts(f) => get_crb_loc_sts_field(f), CrbRegister::IntfId(f) => get_crb_intf_id_field(f), CrbRegister::IntfId2(f) => get_crb_intf_id2_field(f), CrbRegister::CtrlSts(f) => get_crb_ctrl_sts_field(f), } } // Set a particular field in a Register fn set_reg_field(regs: &mut [u32; TPM_CRB_R_MAX], reg: CrbRegister, value: u32) { let (base, offset, len) = get_field(reg); let mask = (!(0_u32) >> (32 - len)) << offset; regs[base as usize] = (regs[base as usize] & !mask) | ((value << offset) & mask); } // Get the value of a particular field in a Register const fn get_reg_field(regs: &[u32; TPM_CRB_R_MAX], reg: CrbRegister) -> u32 { let (base, offset, len) = get_field(reg); let mask = (!(0_u32) >> (32 - len)) << offset; (regs[base as usize] & mask) >> offset } fn locality_from_addr(addr: u32) -> u8 { (addr >> 12) as u8 } fn update_reg(regs: &mut [u32; TPM_CRB_R_MAX], offset: u32, data: &[u8]) -> u32 { let reg_offset = (offset & !0x3) as usize; let byte_offset = (offset & 0x3) as usize; let mut input = [0; 4]; input[..data.len()].copy_from_slice(data); let value = u32::from_le_bytes(input); let mask = (u32::MAX >> (32 - data.len() * 8)) << (byte_offset * 8); regs[reg_offset] = (regs[reg_offset] & !mask) | ((value << (byte_offset * 8)) & mask); regs[reg_offset] } fn read_reg_value(value: u32, offset: u32) -> u32 { let byte_offset = (offset & 0x3) as usize; value >> (byte_offset * 8) } #[cfg(test)] fn read_reg(regs: &[u32; TPM_CRB_R_MAX], offset: u32) -> u32 { let reg_offset = (offset & !0x3) as usize; read_reg_value(regs[reg_offset], offset) } fn complete_request(regs: &mut [u32; TPM_CRB_R_MAX], success: bool) { regs[CRB_CTRL_START as usize] &= !CRB_START_INVOKE; if !success { set_reg_field(regs, CrbRegister::CtrlSts(CtrlStsFields::TpmSts), 1); } } fn data_buffer_range(offset: u32, len: usize, buffer_len: usize) -> Option> { let end_offset = offset.checked_add(len as u32)?; let buffer_end = CRB_DATA_BUFFER.checked_add(buffer_len as u32)?; if offset >= CRB_DATA_BUFFER && end_offset <= buffer_end { let start = (offset - CRB_DATA_BUFFER) as usize; Some(start..start + len) } else { None } } pub struct Tpm { emulator: Emulator, regs: [u32; TPM_CRB_R_MAX], backend_buff_size: usize, data_buff: [u8; TPM_CRB_BUFFER_MAX], data_buff_len: usize, } impl Tpm { pub fn new(path: impl AsRef) -> Result { let emulator = Emulator::new(path) .map_err(|e| Error::Init(anyhow!("Failed while initializing tpm Emulator: {e:?}")))?; let mut tpm = Tpm { emulator, regs: [0; TPM_CRB_R_MAX], backend_buff_size: TPM_CRB_BUFFER_MAX, data_buff: [0; TPM_CRB_BUFFER_MAX], data_buff_len: 0, }; tpm.reset()?; Ok(tpm) } fn get_active_locality(&mut self) -> u32 { if get_reg_field( &self.regs, CrbRegister::LocState(LocStateFields::LocAssigned), ) == 0 { return TPM_CRB_NO_LOCALITY; } get_reg_field( &self.regs, CrbRegister::LocState(LocStateFields::ActiveLocality), ) } fn request_completed(&mut self, success: bool) { complete_request(&mut self.regs, success); } fn reset(&mut self) -> Result<()> { let cur_buff_size = self.emulator.get_buffer_size(); self.regs = [0; TPM_CRB_R_MAX]; set_reg_field( &mut self.regs, CrbRegister::LocState(LocStateFields::TpmRegValidSts), 1, ); set_reg_field( &mut self.regs, CrbRegister::CtrlSts(CtrlStsFields::TpmIdle), 1, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::InterfaceType), CRB_INTF_TYPE_CRB_ACTIVE, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::InterfaceVersion), CRB_INTF_VERSION_CRB, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::CapLocality), CRB_INTF_CAP_LOCALITY_0_ONLY, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::CapCRBIdleBypass), CRB_INTF_CAP_IDLE_FAST, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::CapDataXferSizeSupport), CRB_INTF_CAP_XFER_SIZE_64, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::CapFIFO), CRB_INTF_CAP_FIFO_NOT_SUPPORTED, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::CapCRB), CRB_INTF_CAP_CRB_SUPPORTED, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::InterfaceSelector), CRB_INTF_IF_SELECTOR_CRB, ); set_reg_field( &mut self.regs, CrbRegister::IntfId(IntfIdFields::Rid), 0b0000, ); set_reg_field( &mut self.regs, CrbRegister::IntfId2(IntfId2Fields::Vid), PCI_VENDOR_ID_IBM, ); self.regs[CRB_CTRL_CMD_SIZE_REG as usize] = CRB_CTRL_CMD_SIZE as u32; self.regs[CRB_CTRL_CMD_LADDR as usize] = TPM_CRB_ADDR_BASE + CRB_DATA_BUFFER; self.regs[CRB_CTRL_RSP_SIZE as usize] = CRB_CTRL_CMD_SIZE as u32; self.regs[CRB_CTRL_RSP_ADDR as usize] = TPM_CRB_ADDR_BASE + CRB_DATA_BUFFER; self.backend_buff_size = cmp::min(cur_buff_size, TPM_CRB_BUFFER_MAX); if let Err(e) = self.emulator.startup_tpm(self.backend_buff_size) { return Err(Error::Init(anyhow!( "Failed while running Startup TPM. Error: {e:?}" ))); } Ok(()) } } impl BusDevice for Tpm { fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) { let mut offset: u32 = offset as u32; let read_len: usize = data.len(); if let Some(range) = data_buffer_range(offset, read_len, self.data_buff.len()) { // Read from Data Buffer data[..].clone_from_slice(&self.data_buff[range]); } else { offset &= 0xff; let reg_offset = offset & !0x3; let mut val = self.regs[reg_offset as usize]; // Per the TCG PC Client Platform TPM Profile (PTP) spec, bit 0 // of TPM_LOC_STATE_x is `tpmEstablished`. Reflect the live value // from the backend rather than the (zero-initialised) shadow. if reg_offset == CRB_LOC_STATE && self.emulator.get_established_bit() { val |= 0x1; } val = read_reg_value(val, offset); if data.len() <= 4 { data.clone_from_slice(val.to_ne_bytes()[0..read_len].as_ref()); } else { error!( "Invalid tpm read: offset {:#X}, data length {:?}", offset, data.len() ); } } debug!( "MMIO Read: offset {:#X} len {:?} val = {:02X?} ", offset, data.len(), data ); } fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option> { debug!( "MMIO Write: offset {:#X} len {:?} input data {:02X?}", offset, data.len(), data ); let mut offset: u32 = offset as u32; if offset < CRB_DATA_BUFFER { offset &= 0xff; } let locality = locality_from_addr(offset) as u32; let write_len = data.len(); if let Some(range) = data_buffer_range(offset, write_len, self.data_buff.len()) { let start = range.start; if start == 0 { // If filling data_buff at index 0, reset length to 0 self.data_buff_len = 0; self.data_buff.fill(0); } self.data_buff[range].clone_from_slice(data); self.data_buff_len += data.len(); } else { // Ctrl Commands that take more than 4 bytes as input are not yet supported // CTRL_RSP_ADDR usually gets 8 byte write request. Last 4 bytes are zeros. if write_len > 4 && offset != CRB_CTRL_RSP_ADDR { error!( "Invalid tpm write: offset {:#X}, data length {}", offset, data.len() ); return None; } let write_offset = offset; let reg_data_len = cmp::min(write_len, 4 - (offset as usize & 0x3)); let mut input = [0; 4]; input[..reg_data_len].copy_from_slice(&data[..reg_data_len]); let v = u32::from_le_bytes(input) << ((offset & 0x3) * 8); offset &= !0x3; match offset { CRB_CTRL_CMD_SIZE_REG => { update_reg(&mut self.regs, write_offset, &data[..reg_data_len]); } CRB_CTRL_CMD_LADDR => { update_reg(&mut self.regs, write_offset, &data[..reg_data_len]); } CRB_CTRL_CMD_HADDR => { update_reg(&mut self.regs, write_offset, &data[..reg_data_len]); } CRB_CTRL_RSP_SIZE => { update_reg(&mut self.regs, write_offset, &data[..reg_data_len]); } CRB_CTRL_RSP_ADDR => { update_reg(&mut self.regs, write_offset, &data[..reg_data_len]); } CRB_CTRL_REQ => match v { CRB_CTRL_REQ_CMD_READY => { set_reg_field( &mut self.regs, CrbRegister::CtrlSts(CtrlStsFields::TpmIdle), 0, ); } CRB_CTRL_REQ_GO_IDLE => { set_reg_field( &mut self.regs, CrbRegister::CtrlSts(CtrlStsFields::TpmIdle), 1, ); } _ => { error!("Invalid value passed to CTRL_REQ register"); return None; } }, 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() { error!("Failed to run cancel command. Error: {e:?}"); } } CRB_CTRL_START => { if v == CRB_START_INVOKE && ((self.regs[CRB_CTRL_START as usize] & CRB_START_INVOKE) == 0) && self.get_active_locality() == locality { self.regs[CRB_CTRL_START as usize] |= CRB_START_INVOKE; let mut cmd = BackendCmd { buffer: &mut self.data_buff, input_len: cmp::min(self.data_buff_len, TPM_CRB_BUFFER_MAX), }; let status = self.emulator.deliver_request(&mut cmd).is_ok(); self.request_completed(status); } } CRB_LOC_CTRL => { debug!("CRB_LOC_CTRL locality to write = {locality:?} val = {v:?}"); match v { CRB_LOC_CTRL_RESET_ESTABLISHMENT_BIT => {} CRB_LOC_CTRL_RELINQUISH => { set_reg_field( &mut self.regs, CrbRegister::LocState(LocStateFields::LocAssigned), 0, ); set_reg_field( &mut self.regs, CrbRegister::LocSts(LocStsFields::Granted), 0, ); } CRB_LOC_CTRL_REQUEST_ACCESS => { set_reg_field( &mut self.regs, CrbRegister::LocSts(LocStsFields::Granted), 1, ); set_reg_field( &mut self.regs, CrbRegister::LocSts(LocStsFields::BeenSeized), 0, ); set_reg_field( &mut self.regs, CrbRegister::LocState(LocStateFields::LocAssigned), 1, ); } _ => { error!("Invalid value to write in CRB_LOC_CTRL {v:#X} "); } } } _ => { error!( "Invalid tpm write: offset {:#X}, data length {:?}", offset, data.len() ); } } } None } } #[cfg(test)] mod unit_tests { use super::*; #[test] fn test_set_get_reg_field() { let mut regs: [u32; TPM_CRB_R_MAX] = [0; TPM_CRB_R_MAX]; set_reg_field(&mut regs, CrbRegister::IntfId(IntfIdFields::Rid), 0xAC); assert_eq!( get_reg_field(®s, CrbRegister::IntfId(IntfIdFields::Rid)), 0xAC, concat!("Test: ", stringify!(set_get_reg_field)) ); } #[test] fn test_update_reg_partial_write() { let mut regs: [u32; TPM_CRB_R_MAX] = [0; TPM_CRB_R_MAX]; update_reg(&mut regs, CRB_CTRL_CMD_SIZE_REG, &[0x78, 0x56, 0x34, 0x12]); update_reg(&mut regs, CRB_CTRL_CMD_SIZE_REG + 1, &[0xaa]); assert_eq!(regs[CRB_CTRL_CMD_SIZE_REG as usize], 0x1234_aa78); } #[test] fn test_update_reg_preserves_untouched_byte_lanes() { let mut regs: [u32; TPM_CRB_R_MAX] = [0; TPM_CRB_R_MAX]; regs[CRB_CTRL_CMD_LADDR as usize] = 0x1122_3344; update_reg(&mut regs, CRB_CTRL_CMD_LADDR + 1, &[0xaa, 0xbb]); assert_eq!(regs[CRB_CTRL_CMD_LADDR as usize], 0x11bb_aa44); } #[test] fn test_read_reg_partial_read_uses_byte_offset() { let mut regs: [u32; TPM_CRB_R_MAX] = [0; TPM_CRB_R_MAX]; regs[CRB_CTRL_CMD_SIZE_REG as usize] = 0x1234_5678; assert_eq!(read_reg(®s, CRB_CTRL_CMD_SIZE_REG + 1) as u8, 0x56); assert_eq!(read_reg(®s, CRB_CTRL_CMD_SIZE_REG + 2) as u16, 0x1234); assert_eq!(read_reg_value(0x1, CRB_LOC_STATE + 1) as u8, 0); } #[test] fn test_complete_request_preserves_reserved_bits() { let mut regs: [u32; TPM_CRB_R_MAX] = [0; TPM_CRB_R_MAX]; regs[CRB_CTRL_START as usize] = 0xffff_fffe | CRB_START_INVOKE; complete_request(&mut regs, true); assert_eq!(regs[CRB_CTRL_START as usize], 0xffff_fffe); } #[test] fn test_complete_request_sets_error_status_on_failure() { let mut regs: [u32; TPM_CRB_R_MAX] = [0; TPM_CRB_R_MAX]; regs[CRB_CTRL_START as usize] = CRB_START_INVOKE; complete_request(&mut regs, false); assert_eq!(regs[CRB_CTRL_START as usize], 0); assert_eq!( get_reg_field(®s, CrbRegister::CtrlSts(CtrlStsFields::TpmSts)), 1 ); } #[test] fn test_data_buffer_range_accepts_exact_end_access() { let start = (TPM_CRB_BUFFER_MAX - 4)..TPM_CRB_BUFFER_MAX; assert_eq!( data_buffer_range( CRB_DATA_BUFFER + TPM_CRB_BUFFER_MAX as u32 - 4, 4, TPM_CRB_BUFFER_MAX ), Some(start) ); assert_eq!( data_buffer_range( CRB_DATA_BUFFER + TPM_CRB_BUFFER_MAX as u32 - 3, 4, TPM_CRB_BUFFER_MAX ), None ); } } cloud-hypervisor-53.0/docs/000077500000000000000000000000001522476750100157205ustar00rootroot00000000000000cloud-hypervisor-53.0/docs/amd_sev_snp.md000066400000000000000000000027711522476750100205470ustar00rootroot00000000000000# AMD SEV-SNP AMD Secure Encrypted Virtualization & Secure Nested Paging (SEV-SNP) is an AMD technology designed to add strong memory integrity protection to help prevent 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://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 A machine with AMD SEV-SNP support which is enabled in the BIOS is required. On the Cloud Hypervisor side, build the project with the `sev_snp` and `igvm` and the hypervisor backend you want to use. For the MSHV SEV-SNP build: ```bash cargo build --no-default-features --features "mshv,sev_snp,igvm" ``` Change `mshv` to `kvm` for the KVM backend. You can enable both at the same time. **Note** Please note that `sev_snp` cannot be enabled in conjunction with the `tdx` feature flag. SEV-SNP is also supported on KVM with an IGVM stage0 image and a guest kernel provided through `fw_cfg`. Build that configuration with: ```bash cargo build --no-default-features --features "kvm,igvm,sev_snp,fw_cfg" ``` You can run a SEV-SNP VM using the following command: ```bash ./cloud-hypervisor \ --platform sev_snp=on \ --cpus boot=1 \ --memory size=1G \ --disk path=ubuntu.img ``` For more information related to Microsoft Hypervisor, please see [mshv.md](mshv.md). cloud-hypervisor-53.0/docs/api.md000066400000000000000000000564231522476750100170250ustar00rootroot00000000000000- [Cloud Hypervisor API](#cloud-hypervisor-api) - [External API](#external-api) - [REST API](#rest-api) - [REST API Location and availability](#rest-api-location-and-availability) - [REST API Endpoints](#rest-api-endpoints) - [Virtual Machine Manager (VMM) Actions](#virtual-machine-manager-vmm-actions) - [Virtual Machine (VM) Actions](#virtual-machine-vm-actions) - [REST API Examples](#rest-api-examples) - [Create a Virtual Machine](#create-a-virtual-machine) - [Boot a Virtual Machine](#boot-a-virtual-machine) - [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-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) # Cloud Hypervisor API The Cloud Hypervisor API is made of 2 distinct interfaces: 1. **The External API** This is the user facing API. Users and operators can control and manage the Cloud Hypervisor through various options including a REST API, a Command Line Interface (CLI) or a D-Bus based API, which is not compiled into Cloud Hypervisor by default. 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 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. ## External API ### REST API The Cloud Hypervisor [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API triggers VM and VMM specific actions, and as such it is designed as a collection of RPC-style, static methods. The API is [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md) compliant. Please consult the [Cloud Hypervisor OpenAPI Document](https://raw.githubusercontent.com/cloud-hypervisor/cloud-hypervisor/master/vmm/src/api/openapi/cloud-hypervisor.yaml) for more details about the API payloads and responses. #### REST API Location and availability The REST API, if enabled, is available as soon as the Cloud Hypervisor binary is started, through either a local UNIX socket as given in the Cloud Hypervisor option `--api-socket path=...` or a fd with `--api-socket fd=...`. ``` $ ./target/debug/cloud-hypervisor --api-socket path=/tmp/cloud-hypervisor.sock ``` #### REST API Endpoints The Cloud Hypervisor API exposes the following actions through its endpoints: ##### Virtual Machine Manager (VMM) Actions | Action | Endpoint | Request Body | Response Body | Prerequisites | | ----------------------------------- | --------------- | ------------ | -------------------------- | ------------------ | | Check for the REST API availability | `/vmm.ping` | N/A | `/schemas/VmmPingResponse` | N/A | | Shut the VMM down | `/vmm.shutdown` | N/A | N/A | The VMM is running | ##### 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 | | 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 enabled. Without this feature, the corresponding [REST API](#rest-api) or [D-Bus API](#d-bus-api) endpoints are not available. #### REST API Examples For the following set of examples, we assume Cloud Hypervisor is started with the REST API available at `/tmp/cloud-hypervisor.sock`: ``` $ ./target/debug/cloud-hypervisor --api-socket /tmp/cloud-hypervisor.sock ``` ##### Create a Virtual Machine We want to create a virtual machine with the following characteristics: * 4 vCPUs * 1 GB of RAM * 1 virtio based networking interface * Direct kernel boot from a custom 5.6.0-rc4 Linux kernel located at `/opt/clh/kernel/vmlinux-virtio-fs-virtio-iommu` * Using a Ubuntu image as its root filesystem, located at `/opt/clh/images/focal-server-cloudimg-amd64.raw` ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i \ -X PUT 'http://localhost/api/v1/vm.create' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "cpus":{"boot_vcpus": 4, "max_vcpus": 4}, "payload":{"kernel":"/opt/clh/kernel/vmlinux-virtio-fs-virtio-iommu", "cmdline":"console=ttyS0 console=hvc0 root=/dev/vda1 rw"}, "disks":[{"path":"/opt/clh/images/focal-server-cloudimg-amd64.raw"}], "rng":{"src":"/dev/urandom"}, "net":[{"ip":"192.168.10.10", "mask":"255.255.255.0", "mac":"12:34:56:78:90:01"}] }' ``` ##### Boot a Virtual Machine Once the VM is created, we can boot it: ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i -X PUT 'http://localhost/api/v1/vm.boot' ``` ##### Dump Virtual Machine Information We can fetch information about any VM as soon as it's created: ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i \ -X GET 'http://localhost/api/v1/vm.info' \ -H 'Accept: application/json' ``` ##### Reboot a Virtual Machine We can reboot a VM that's already booted: ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i -X PUT 'http://localhost/api/v1/vm.reboot' ``` ##### Shut a Virtual Machine Down Once booted, we can shut a VM down from the REST API: ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i -X PUT 'http://localhost/api/v1/vm.shutdown' ``` ### D-Bus API Cloud Hypervisor offers a D-Bus API as an alternative to its REST API. This D-Bus API fully reflects the functionality of the REST API, exposing the same group of endpoints. It can be a drop-in replacement since it also consumes/produces JSON. In addition, the D-Bus API also exposes events from `event-monitor` in the form of a D-Bus signal to which users can subscribe. For more information, 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` feature flag when compiling Cloud Hypervisor. ```sh $ ./scripts/dev_cli.sh build --release --libc musl -- --features dbus_api ``` Once this feature is enabled, it can be configured with the following CLI options: ``` --dbus-service-name well known name of the service --dbus-object-path object path to serve the dbus interface --dbus-system-bus use the system bus instead of a session bus ``` Example invocation: ```sh $ ./cloud-hypervisor --dbus-service-name "org.cloudhypervisor.DBusApi" \ --dbus-object-path "/org/cloudhypervisor/DBusApi" ``` This will start serving a service with the name `org.cloudhypervisor.DBusApi` and the interface `org.cloudhypervisor.DBusApi1`, which in turn can be used to control and manage Cloud Hypervisor. #### D-Bus API Interface Please refer to the [REST API](#rest-api) documentation for everything that is in common with the REST API. As previously mentioned, the D-Bus API can be used as a drop-in replacement for the [REST API](#rest-api). The D-Bus interface also exposes a signal, named `Event`, which is emitted whenever a new event is published from the `event-monitor` crate. Here is its definition in XML format: ```xml ``` ### Command Line Interface The Cloud Hypervisor Command Line Interface (CLI) can only be used for launching the Cloud Hypervisor binary, i.e. it cannot be used for controlling the VMM or the launched VM once they're up and running. If you want to inspect the VMM, or control the VM after launching Cloud Hypervisor from the CLI, you must use either the [REST API](#rest-api) or the [D-Bus API](#d-bus-api). From the CLI, one can: 1. Create and boot a complete virtual machine by using the CLI options to build the VM config. Run `cloud-hypervisor --help` for a complete list of CLI options. As soon as the `cloud-hypervisor` binary is launched, contrary to the [D-Bus API](#d-bus-api), the [REST API](#rest-api) is available for controlling and managing the VM. The [D-Bus API](#d-bus-api) doesn't start automatically and needs to be explicitly configured in order to be run. 1. Start either the REST API, D-Bus API or both simultaneously without passing any VM configuration options. The VM can then be asynchronously created and booted by calling API methods of choice. It should be noted that one external API does not exclude another; it is possible to have both the REST and D-Bus APIs running simultaneously. ### REST API, D-Bus API and CLI Architectural Relationship 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.5.53/clap/) and then translated into [internal API](#internal-api) commands. The REST API is processed by an HTTP thread using the [Firecracker's `micro_http`](https://github.com/firecracker-microvm/micro-http) crate. As with the CLI, the HTTP requests eventually get translated into [internal API](#internal-api) commands. 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 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): ``` +------------------+ REST API | | +--------->+ micro_http +--------+ | | | | | +------------------+ | | | +------------------------+ | | | | +------------+ | +----------+ | | | | | | D-Bus API | | | | +--------------+ | | User +---------+----------->+ zbus +--------------+------> | Internal API | | | | | | | | | +--------------+ | +------------+ | +----------+ | | | | | | | | | +------------------------+ | +----------+ | VMM | CLI | | | +----------->+ clap +--------------+ | | +----------+ ``` ## Internal API 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 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 control loop. API producers are the HTTP thread handling the [REST API](#rest-api), the D-Bus thread handling the [D-Bus API](#d-bus-api) and the main thread that initially parses the [CLI](#command-line-interface). ### Goals and Design The internal API is designed for controlling, managing and inspecting a Cloud Hypervisor VMM and its guest. It is a backend for handling external, user visible requests through the [REST API](#rest-api), the [D-Bus API](#d-bus-api) or the [CLI](#command-line-interface) interfaces. The API follows a command-response scheme that closely maps the [REST API](#rest-api). Any command must be replied to with a response. Commands are [MPSC](https://doc.rust-lang.org/std/sync/mpsc/) based messages and are received and processed by the VMM control loop. In order for the VMM control loop to respond to any internal API command, it must be able to send a response back to the MPSC sender. For that purpose, all internal API command payload carry the [Sender](https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html) end of an [MPSC](https://doc.rust-lang.org/std/sync/mpsc/) channel. The sender of any internal API command is therefore responsible for: 1. Creating an [MPSC](https://doc.rust-lang.org/std/sync/mpsc/) response channel. 1. Passing the [Sender](https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html) end of the response channel as part of the internal API command payload. 1. Waiting for the internal API command's response on the [Receiver](https://doc.rust-lang.org/std/sync/mpsc/struct.Receiver.html) end of the response channel. ## End to End Example In order to further understand how the external and internal Cloud Hypervisor 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 create a virtual machine: ```shell #!/usr/bin/env bash curl --unix-socket /tmp/cloud-hypervisor.sock -i \ -X PUT 'http://localhost/api/v1/vm.create' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "cpus":{"boot_vcpus": 4, "max_vcpus": 4}, "payload":{"kernel":"/opt/clh/kernel/vmlinux-virtio-fs-virtio-iommu", "cmdline":"console=ttyS0 console=hvc0 root=/dev/vda1 rw"}, "disks":[{"path":"/opt/clh/images/focal-server-cloudimg-amd64.raw"}], "rng":{"src":"/dev/urandom"}, "net":[{"ip":"192.168.10.10", "mask":"255.255.255.0", "mac":"12:34:56:78:90:01"}] }' ``` 1. The Cloud Hypervisor HTTP thread processes the request and de-serializes the HTTP request JSON body into an internal `VmConfig` structure. 1. The Cloud Hypervisor HTTP thread creates an [MPSC](https://doc.rust-lang.org/std/sync/mpsc/) channel for the internal API server to send its response back. 1. The Cloud Hypervisor HTTP thread prepares an internal API command for creating a virtual machine. The command's payload is made of the de-serialized `VmConfig` structure and the response channel: ```Rust VmCreate(Arc>, Sender) ``` 1. The Cloud Hypervisor HTTP thread sends the internal API command, and waits for the response: ```Rust // Send the VM creation request. api_sender .send(ApiRequest::VmCreate(config, response_sender)) .map_err(ApiError::RequestSend)?; api_evt.write(1).map_err(ApiError::EventFdWrite)?; response_receiver.recv().map_err(ApiError::ResponseRecv)??; ``` 1. The Cloud Hypervisor control loop receives the command, as it listens on the internal API [MPSC](https://doc.rust-lang.org/std/sync/mpsc/) channel: ```Rust // Read from the API receiver channel let api_request = api_receiver.recv().map_err(Error::ApiRequestRecv)?; ``` 1. The Cloud Hypervisor control loop matches the received internal API against 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): ```Rust match api_request { ApiRequest::VmCreate(config, sender) => { // We only store the passed VM config. // The VM will be created when being asked to boot it. let response = if self.vm_config.is_none() { self.vm_config = Some(config); Ok(ApiResponsePayload::Empty) } else { Err(ApiError::VmAlreadyCreated) }; sender.send(response).map_err(Error::ApiResponseSend)?; } ``` 1. The Cloud Hypervisor HTTP thread receives the internal API command response as the return value from its `VmCreate` HTTP handler. Depending on the control loop internal API response, it generates the appropriate HTTP response: ```Rust // Call vm_create() match vm_create(api_notifier, api_sender, Arc::new(Mutex::new(vm_config))) .map_err(HttpError::VmCreate) { Ok(_) => Response::new(Version::Http11, StatusCode::NoContent), Err(e) => error_response(e, StatusCode::InternalServerError), } ``` 1. The Cloud Hypervisor HTTP thread sends the formed HTTP response back to the user. This is abstracted by the [micro_http](https://github.com/firecracker-microvm/micro-http) crate. cloud-hypervisor-53.0/docs/balloon.md000066400000000000000000000041111522476750100176650ustar00rootroot00000000000000# Balloon Cloud Hypervisor implements a balloon device based on the VIRTIO specification. Its main purpose is to provide the host a way to reclaim memory by controlling the amount of memory visible to the guest. But it also provides some interesting features related to guest memory management. ## Parameters `BalloonConfig` (known as `--balloon` from the CLI perspective) contains the list of parameters available for the balloon device. ```rust struct BalloonConfig { pub size: u64, pub deflate_on_oom: bool, pub free_page_reporting: bool, } ``` ``` --balloon Balloon parameters "size=,deflate_on_oom=on|off,free_page_reporting=on|off" ``` ### `size` 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, it is entitled to all of it. This parameter is mandatory. Value is an unsigned integer of 64 bits corresponding to the balloon size in bytes. _Example_ ``` --balloon size=1G ``` ### `deflate_on_oom` 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. This parameter is optional. Value is a boolean set to `off` by default. _Example_ ``` --balloon size=2G,deflate_on_oom=on ``` ### `free_page_reporting` Allow the guest to report lists of free pages. This feature doesn't require the balloon to be of any specific size as it doesn't impact the balloon size. The guest can let the VMM know about pages that are free after they have been used. Based on this information, the VMM can advise the host that it doesn't need these pages anymore. This parameter is optional. Value is a boolean set to `off` by default. _Example_ ``` --balloon size=0,free_page_reporting=on ``` cloud-hypervisor-53.0/docs/building.md000066400000000000000000000060161522476750100200420ustar00rootroot00000000000000- [Building Cloud Hypervisor](#building-cloud-hypervisor) - [Preparation](#preparation) - [Install prerequisites](#install-prerequisites) - [Clone and build](#clone-and-build) - [Containerized builds and tests](#containerized-builds-and-tests) # Building Cloud Hypervisor We recommend users use the pre-built binaries that are mentioned in the README.md file in the root of the repository. Building from source is only necessary if you wish to make modifications. ## Preparation We create a folder to build and run `cloud-hypervisor` at `$HOME/cloud-hypervisor` ```shell $ export CLOUDH=$HOME/cloud-hypervisor $ mkdir $CLOUDH ``` ## Install prerequisites You need to install some prerequisite packages in order to build and test Cloud Hypervisor. Here, all the steps are based on Ubuntu, for other Linux distributions please replace the package manager and package name. ```shell # 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 $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # If you want to build statically linked binary please add musl target $ rustup target add x86_64-unknown-linux-musl # x86-64 $ rustup target add aarch64-unknown-linux-musl # AArch64 ``` ## Clone and build First you need to clone and build the Cloud Hypervisor repository: ```shell $ pushd $CLOUDH $ git clone https://github.com/cloud-hypervisor/cloud-hypervisor.git $ cd cloud-hypervisor $ cargo build --release # We need to give the cloud-hypervisor binary the NET_ADMIN capabilities for it to set TAP interfaces up on the host. $ sudo setcap cap_net_admin+ep ./target/release/cloud-hypervisor # If you want to build statically linked binary $ cargo build --release --target=x86_64-unknown-linux-musl --all # x86-64 $ cargo build --release --target=aarch64-unknown-linux-musl --all # AArch64 $ popd ``` This will build a `cloud-hypervisor` binary under `$CLOUDH/cloud-hypervisor/target/release/cloud-hypervisor`. ### Containerized builds and tests If you want to build and test Cloud Hypervisor without having to install all the required dependencies (The rust toolchain, cargo tools, etc), you can also use Cloud Hypervisor's development script: `dev_cli.sh`. Please note that upon its first invocation, this script will pull a fairly large container image. For example, to build the Cloud Hypervisor release binary: ```shell $ pushd $CLOUDH $ cd cloud-hypervisor $ ./scripts/dev_cli.sh build --release ``` With `dev_cli.sh`, one can also run the Cloud Hypervisor CI locally. This can be very convenient for debugging CI errors without having to fully rely on the Cloud Hypervisor CI infrastructure. For example, to run the Cloud Hypervisor unit tests: ```shell $ ./scripts/dev_cli.sh tests --unit ``` Run the `./scripts/dev_cli.sh --help` command to view all the supported development script commands and their related options. cloud-hypervisor-53.0/docs/coverage.md000066400000000000000000000046351522476750100200450ustar00rootroot00000000000000# Code coverage LLVM provides a set of tools to collect code coverage data and present the data in human-consumable forms. ## Building a suitable binary The compiler flag to generate code coverage data has been stabilized since Rust 1.60. An instrumented binary can be built with the following command: ```shell cargo clean && RUSTFLAGS='-C instrument-coverage' cargo build ``` Using either `debug` or `release` profile is fine. You will need to adjust the path for some commands. ## Running the binary Run the binary as you normally would. When the process exits, you will see files with the prefix `profraw`. Multiple runs of the same binary will produce multiple `profraw` files. The more diverse the runs are, the better. Try to exercise different features as much as possible. ## Combining raw data Raw data files can be combined with `llvm-profdata`. ```shell rustup component add llvm-tools-preview # Assuming profraw files reside in the current directory and its children directories find . -name '*.profraw' -exec llvm-profdata merge -sparse {} -o coverage.profdata \; ``` A file named `coverage.profdata` will be generated. ## Generating HTML files for human consumption This can be done either with LLVM or `grcov`. Here is an example using grcov. ```shell cargo install grcov # Assuming the profdata file is in the top level directory of the Cloud Hypervisor repository grcov . --binary-path ./target/x86_64-unknown-linux-gnu/release -s . -t html --branch --ignore-not-existing -o coverage-html-output/ ``` You can then open the `index.html` file under coverage-html-output to see the results. ## Notes on running the in-tree integration tests and unit tests Please set RUSTFLAGS the same way while invoking `dev_cli.sh`. The script will pass RUSTFLAGS to the container. Since the `profraw` files are generated from within the container, the file paths embedded in the data files are going to be different. It is easier to do the data processing from within the container if you don't want to fight the tool chain. ```shell # Set env to enable code coverage export RUSTFLAGS="-Cinstrument-coverage" export LLVM_PROFILE_FILE="ch-%p-%m.profraw" # Run unit tests scripts/dev_cli.sh tests --unit --libc gnu # Run integration tests scripts/dev_cli.sh tests --integration --libc gnu scripts/dev_cli.sh tests --integration-live-migration --libc gnu # Export code coverage report scripts/dev_cli.sh tests --coverage -- -- html ``` cloud-hypervisor-53.0/docs/cpu.md000066400000000000000000000163321522476750100170360ustar00rootroot00000000000000# CPU Cloud Hypervisor has many options when it comes to the creation of virtual CPUs. This document aims to explain what Cloud Hypervisor is capable of and how it can be used to meet the needs of very different use cases. ## Options `CpusConfig` or what is known as `--cpus` from the CLI perspective is the way to set vCPUs options for Cloud Hypervisor. ```rust struct CpusConfig { 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=,nested=on|off,core_scheduling=vm|vcpu|off ``` ### `boot` Number of vCPUs present at boot time. 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 32 bits. _Example_ ``` --cpus boot=2 ``` ### `max` 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 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 32 bits. By default this option takes the value of `boot`, meaning vCPU hotplug is not expected and can't be performed. _Example_ ``` --cpus max=3 ``` ### `topology` 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 performance. The topology is described through the following structure: ```rust struct CpuTopology { threads_per_core: u16, cores_per_die: u16, dies_per_package: u16, packages: u16, } ``` or the following syntax through the CLI: ``` topology=::: ``` By default the topology will be `1:1:1:1`. _Example_ ``` --cpus boot=2,topology=1:1:2:1 ``` ### `kvm_hyperv` Enable KVM Hyper-V emulation. When turned on, this option relies on KVM to emulate the synthetic interrupt controller (SynIC) along with synthetic timers expected by a Windows guest. A Windows guest usually runs on top of Microsoft Hyper-V, therefore expects these synthetic devices to be present. That's why KVM provides a way to emulate them and avoids failures running a Windows guest with Cloud Hypervisor. By default this option is turned off. _Example_ ``` --cpus kvm_hyperv=on ``` ### `max_phys_bits` 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 debugging purposes. The value is an unsigned integer of 8 bits. _Example_ ``` --cpus max_phys_bits=40 ``` ### `affinity` 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 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: u32, host_cpus: Vec, } ``` or the following syntax through the CLI: ``` affinity=[@[, ], @[, ]] ``` 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 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 same example could also be described with `affinity=0@[0,1,2,3,4]`. A combination of both `-` and `,` separators is useful when one might need to describe a list containing host CPUs from 0 to 99 and the host CPU 255, as it could simply be described with `affinity=0@[0-99,255]`. As soon as one tries to describe a list of values, `[` and `]` must be used to demarcate the list. By default each vCPU runs on the entire host CPU set. _Example_ ``` --cpus boot=3,affinity=[0@[2,3],1@[0,1]] ``` In this example, assuming the host has 4 CPUs, vCPU 0 will run exclusively on host CPUs 2 and 3, while vCPU 1 will run exclusively on host CPUs 0 and 1. Because nothing is defined for vCPU 2, it can run on any of the 4 host CPUs. ### `features` Set of CPU features to enable. This option allows the user to enable a set of CPU features that are disabled by default otherwise. The currently available feature set is: `amx`. The `amx` feature will enable the x86 extension adding hardware units for matrix operations (int and float dot products). The goal of the extension is to provide performance enhancements for these common operations. _Example_ ``` --cpus features=amx ``` 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. cloud-hypervisor-53.0/docs/custom-image.md000066400000000000000000000024271522476750100206410ustar00rootroot00000000000000# How to create a custom Ubuntu image In the context of adding more utilities to the Ubuntu cloud image being used for integration testing, this quick guide details how to achieve the proper modification of an official Ubuntu cloud image. ## Image generation script This [script](../scripts/build-custom-image.sh) can be used to generate a custom image (needs to be modified per architecture/distribution image) ## Switch CI to use the new image ### Upload to Azure storage A command like the following can be used to upload the image: `az storage blob upload --account-name cloudhypervisorstorages --container-name '$web' --name jammy-server-cloudimg-amd64-custom-20241017-0.qcow2 --file jammy-server-cloudimg-amd64-custom-20241017-0.qcow2 --sas-token ` ### Update integration tests Last step is about updating the integration tests to work with this new image. The key point is to identify where the Linux filesystem partition is located, as we might need to update the direct kernel boot command line, replacing `/dev/vda1` with the appropriate partition number. Update all references to the previous image name to the new one. ## NVIDIA image for VFIO bare-metal CI Uncomment "VFIO_CUSTOM_IMAGE" in the script listed above to generate the custom image used for the VFIO worker.cloud-hypervisor-53.0/docs/debug-port.md000066400000000000000000000074101522476750100203140ustar00rootroot00000000000000# `cloud-hypervisor` debug IO ports When running x86 guests, `cloud-hypervisor` provides different kinds of debug ports: - [`0x80` debug port](https://web.archive.org/web/20211028033025/https://www.intel.com/content/www/us/en/support/articles/000005500/boards-and-kits.html) - Debug console (by default at `0xe9`). - Firmware debug port at `0x402`. All of them can be used to trace user-defined guest events and all of them can be used simultaneously. ## Debug Ports Overview ### `0x80` I/O port 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. It is up to the guest stack to decide when and what to write to the 0x80 port in order to signal the host about specific events and have `cloud-hypervisor` log it. `cloud-hypervisor` defines several debug port code ranges that should be used for debugging specific components of the guest software stack. When logging a write of one of those codes to the debug port, `cloud-hypervisor` adds a pre-defined string to the logs. | Code Range | Component | Log string | | ---------------- | ---------- | ------------ | | `0x00` to `0x1f` | Firmware | `Firmware` | | `0x20` to `0x3f` | Bootloader | `Bootloader` | | `0x40` to `0x5f` | Kernel | `Kernel` | | `0x60` to `0x7f` | Userspace | `Userspace` | | `0x80` to `0xff` | Custom | `Custom` | One typical use case is guest boot time measurement and tracing. By writing different values to the debug I/O port at different boot process steps, the guest will have `cloud-hypervisor` generate timestamped logs of all those steps. That provides a basic but convenient way of measuring not only the overall guest boot time but all intermediate steps as well. #### Logging Assuming parts of the guest software stack have been instrumented to use the `cloud-hypervisor` debug I/O port, we may want to gather the related logs. To do so we need to start `cloud-hypervisor` with the right debug level (`-vvv`). It is also recommended to have it log into a dedicated file in order to easily grep for the tracing logs (e.g. `--log-file /tmp/cloud-hypervisor.log`): ``` ./target/debug/cloud-hypervisor \ --kernel ~/rust-hypervisor-firmware/target/release/hypervisor-fw \ --disk path=~/hypervisor/images/focal-server-cloudimg-amd64.raw \ --cpus boot=4 \ --memory size=1024M \ --rng \ --log-file /tmp/ch-fw.log \ -vvv ``` After booting the guest, we then have to grep for the debug I/O port traces in the log file: ```Shell $ grep "Debug I/O port" /tmp/ch-fw.log cloud-hypervisor: 19.762449ms: DEBUG:vmm/src/vm.rs:510 -- [Debug I/O port: Firmware code 0x0] 0.019004 seconds cloud-hypervisor: 403.499628ms: DEBUG:vmm/src/vm.rs:510 -- [Debug I/O port: Firmware code 0x1] 0.402744 seconds ``` ### Debug console port The debug console is inspired by QEMU and Bochs, which have a similar feature. By default, the I/O port `0xe9` is used. This port can be configured like a console. Thus, it can print to a tty, a file, or a pty, for example. ### Firmware debug port The firmware debug port is also a simple port that prints all bytes written to it. The firmware debug port only prints to stdout. ## When do I need these ports? The ports are on the one hand interesting for firmware or kernel developers, as they provide an easy way to print debug information from within a guest. Furthermore, you can patch "normal" software to measure certain events, such as the boot time of a guest. ## Which port should I choose? 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 aspects of your logging messages. cloud-hypervisor-53.0/docs/device_model.md000066400000000000000000000231441522476750100206650ustar00rootroot00000000000000# Device Model This document describes the device model supported by `cloud-hypervisor`. ## Summary | Device | Build configurable | Enabled by default | Runtime configurable | | :----: | :----: | :----: | :----: | | Serial port | :x: | :x: | :heavy_check_mark: | | RTC/CMOS | :heavy_check_mark: | :heavy_check_mark: | :x: | | I/O APIC | :x: | :x: | :heavy_check_mark: | | i8042 shutdown/reboot | :x: | :x: | :x: | | ACPI shutdown/reboot | :x: | :heavy_check_mark: | :x: | | virtio-blk | :x: | :x: | :heavy_check_mark: | | virtio-console | :x: | :x: | :heavy_check_mark: | | virtio-iommu | :x: | :x: | :heavy_check_mark: | | virtio-net | :x: | :x: | :heavy_check_mark: | | virtio-pmem | :x: | :x: | :heavy_check_mark: | | virtio-rng | :x: | :x: | :heavy_check_mark: | | virtio-vsock | :x: | :x: | :heavy_check_mark: | | vhost-user-blk | :x: | :x: | :heavy_check_mark: | | vhost-user-fs | :x: | :x: | :heavy_check_mark: | | vhost-user-net | :x: | :x: | :heavy_check_mark: | | VFIO | :heavy_check_mark: | :x: | :heavy_check_mark: | ## Legacy devices ### Serial port 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 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`. This device is always built-in, and it is disabled by default. It can be enabled with the `--serial` option, as long as its parameter is not `off`. ### RTC/CMOS For environments such as Windows or EFI which cannot rely on KVM clock, the emulation of this legacy device makes the platform usable. 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. This device is built-in by default for the AArch64 platform, and it is always enabled, and cannot be disabled from the command line. ### I/O APIC `cloud-hypervisor` supports a so-called split IRQ chip implementation by implementing support for the [IOAPIC](https://wiki.osdev.org/IOAPIC). By moving part of the IRQ chip implementation from kernel space to user space, the IRQ chip emulation does not always run in a fully privileged mode. The device is always built-in, and it is enabled depending on the presence of the serial port. If the serial port is disabled, and because no other device would require pin based interrupts (INTx), the I/O APIC is disabled. ### i8042 Simplified PS/2 port since it supports only one key to trigger a reboot or shutdown, depending on the ACPI support. This device is always built-in, but it is disabled by default. Because ACPI is enabled by default, the handling of reboot/shutdown goes through the dedicated ACPI device. In case ACPI is disabled, this device is enabled to bring to the VM some reboot/shutdown support. ### ARM PrimeCell General Purpose Input/Output (PL061) Simplified ARM PrimeCell GPIO (PL061) implementation. Only supports key 3 to trigger a graceful shutdown of the AArch64 guest. ### ACPI device This is a dedicated device for handling ACPI shutdown and reboot when ACPI is enabled. This device is always built-in, and it is enabled by default since the ACPI feature is enabled by default. ## Virtio devices 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=` 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 The `virtio-blk` device exposes a block device to the guest. This device is usually used to boot the operating system running in the VM. This device is always built-in, and it is enabled based on the presence of the flag `--disk`. ### virtio-console `cloud-hypervisor` exposes a `virtio-console` device to the guest. Although using this device as a guest console can potentially cut some early boot messages, it can reduce the guest boot time and provides a complete console implementation. This device is always built-in, and it is enabled by default to provide a guest console. It can be disabled, switching back to the legacy serial port by selecting `--serial tty --console off` from the command line. ### virtio-iommu As we want to improve our nested guests support, we added support for exposing a [paravirtualized IOMMU](iommu.md) device through virtio. This allows for a safer nested virtio and directly assigned devices support. This device is always built-in, and it is enabled based on the presence of the parameter `iommu=on` in any of the virtio or VFIO devices. If at least one of these devices needs to be connected to the paravirtualized IOMMU, the `virtio-iommu` device will be created. ### virtio-net The `virtio-net` device provides network connectivity for the guest, as it creates a network interface connected to a TAP interface automatically created by the `cloud-hypervisor` on the host. This device is always built-in, and it is enabled based on the presence of the flag `--net`. ### virtio-pmem 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 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`. ### virtio-rng A VM does not generate entropy like a real machine would, which is an issue when workloads running in the guest need random numbers to be generated. The `virtio-rng` device provides entropy to the guest by relying on the generator that can be found on the host. By default, the chosen source of entropy is `/dev/urandom`. This device is always built-in, and it is always enabled. The `--rng` flag can be used to change the source of entropy. ### virtio-vsock In order to more efficiently and securely communicate between host and guest, we added a hybrid implementation of the [VSOCK](http://man7.org/linux/man-pages/man7/vsock.7.html) socket address family over virtio. Credits go to the [Firecracker](https://github.com/firecracker-microvm/firecracker/blob/master/docs/vsock.md) project as our implementation is a copy of theirs. This device is always built-in, and it is enabled based on the presence of the flag `--vsock`. ## Vhost-user devices Vhost-user devices are virtio backends running outside of the VMM, as its own separate process. They are usually used to bring more flexibility and increased isolation. ### vhost-user-blk As part of the general effort to offload paravirtualized I/O to external processes, we added support for vhost-user-blk backends. This enables `cloud-hypervisor` users to plug a `vhost-user` based block device (e.g. SPDK) into the VMM as their virtio block backend. This device is always built-in, and it is enabled when `vhost_user=true` and `socket` are provided to the `--disk` parameter. ### vhost-user-fs `cloud-hypervisor` supports the [virtio-fs](https://virtio-fs.gitlab.io/) shared file system, allowing for an efficient and reliable way of sharing a filesystem between the host and the cloud-hypervisor guest. See our [filesystem sharing](fs.md) documentation for more details on how to use virtio-fs with cloud-hypervisor. This device is always built-in, and it is enabled based on the presence of the flag `--fs`. ### vhost-user-net As part of the general effort to offload paravirtualized I/O to external processes, we added support for [vhost-user-net](https://access.redhat.com/solutions/3394851) backends. This enables `cloud-hypervisor` users to plug a `vhost-user` based 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 access to userspace. `cloud-hypervisor` uses VFIO to directly assign host physical devices into its guest. See our [VFIO documentation](vfio.md) for more details on how to directly assign host devices to `cloud-hypervisor` guests. Because VFIO implies `vfio-pci` in the `cloud-hypervisor` context, the VFIO support is built-in when the `pci` feature is selected. And because the `pci` feature is built-in by default, VFIO support is also built-in by default. When VFIO support is built-in, a physical device can be passed through, using the flag `--device` in order to enable the VFIO code. cloud-hypervisor-53.0/docs/disk_locking.md000066400000000000000000000050241522476750100207030ustar00rootroot00000000000000# 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=/foo.img,lock_granularity=byte-range --disk path=/bar.img,lock_granularity=full ``` ### `byte-range` (default) 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. cloud-hypervisor-53.0/docs/fs.md000066400000000000000000000060221522476750100166520ustar00rootroot00000000000000# How to use virtio-fs In the context of virtualization, it is always convenient to be able to share a directory from the host with the guest. __virtio-fs__, also known as __vhost-user-fs__ is a virtual device defined by the VIRTIO specification which allows any VMM to perform filesystem sharing. ## Pre-requisites ### The daemon This virtual device relies on the _vhost-user_ protocol, which assumes the backend (device emulation) is handled by a dedicated process running on the host. This daemon is called __virtiofsd__ and needs to be present on the host. _Build virtiofsd_ ```bash git clone https://gitlab.com/virtio-fs/virtiofsd pushd virtiofsd cargo build --release sudo setcap cap_sys_admin+epi target/release/virtiofsd ``` _Create shared directory_ ```bash mkdir /tmp/shared_dir ``` _Run virtiofsd_ ```bash ./virtiofsd \ --log-level debug \ --socket-path=/tmp/virtiofs \ --shared-dir=/tmp/shared_dir \ --cache=never \ --thread-pool-size=$N ``` The `cache=never` option is the default when using `virtiofsd` with Cloud Hypervisor. This prevents from using the host page cache, reducing the overall footprint on host memory. This increases the maximum density of virtual machines that can be launched on a single host. The `cache=always` option will allow the host page cache to be used, which can result in better performance for the guest's workload at the cost of increasing the footprint on host memory. The `thread-pool-size` option controls how many IO threads are spawned. For very fast storage like NVMe spawning enough worker threads is critical to getting an acceptable performance compared to native. ### Kernel support Modern Linux kernels (at least v5.10) have support for virtio-fs. Use of older kernels, with additional patches, are not supported. ## How to share directories with cloud-hypervisor ### Start the VM Once the daemon is running, the option `--fs` from Cloud Hypervisor needs to be used. Both direct kernel boot and EFI firmware can be used to boot a VM with virtio-fs, given that the cloud image contains a recent enough kernel. Correct functioning of `--fs` requires `--memory shared=on` to facilitate interprocess memory sharing. Assuming you have `focal-server-cloudimg-amd64.raw` and `vmlinux` on your system, here is the Cloud Hypervisor command you need to run: ```bash ./cloud-hypervisor \ --cpus boot=1 \ --memory size=1G,shared=on \ --disk path=focal-server-cloudimg-amd64.raw \ --kernel vmlinux \ --cmdline "console=hvc0 root=/dev/vda1 rw" \ --fs tag=myfs,socket=/tmp/virtiofs,num_queues=1,queue_size=512 ``` ### Mount the shared directory The last step is to mount the shared directory inside the guest, using the `virtiofs` filesystem type. ```bash mkdir mount_dir mount -t virtiofs myfs mount_dir/ ``` The `tag` needs to be consistent with what has been provided through the Cloud Hypervisor command line, which happens to be `myfs` in this example. ## DAX feature Given the DAX feature is not stable yet from a daemon standpoint, it is not available in Cloud Hypervisor. cloud-hypervisor-53.0/docs/fuzzing.md000066400000000000000000000012141522476750100177340ustar00rootroot00000000000000# Fuzzing in Cloud Hypervisor Cloud Hypervisor uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) for fuzzing individual components. The fuzzers are in the `fuzz/fuzz_targets` directory ## Preparation Switch to nightly: ```` rustup override set nightly ```` Install `cargo fuzz`: ``` cargo install cargo-fuzz ``` ## Running the fuzzers e.g. To run the `block` fuzzer using all available CPUs: ``` cargo fuzz run block -j `nproc` ``` ## Adding a new fuzzer ``` cargo fuzz add ``` Inspiration for fuzzers can be found in [crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/+/refs/heads/master/fuzz/) cloud-hypervisor-53.0/docs/fw_cfg.md000066400000000000000000000100331522476750100174720ustar00rootroot00000000000000# 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`. cloud-hypervisor-53.0/docs/gdb.md000066400000000000000000000024151522476750100170000ustar00rootroot00000000000000# GDB Support 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: ```bash 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: ```bash ./cloud-hypervisor \ --kernel hypervisor-fw \ --disk path=bionic-server-cloudimg-amd64.raw \ --cpus boot=1 \ --memory size=1024M \ --net "tap=,mac=,ip=,mask=" \ --console off \ --serial tty \ --gdb path=/tmp/ch-gdb-sock ``` Cloud Hypervisor will listen for GDB on the host side before starting the guest. On the host side, connect to the GDB remote server as follows: ```bash gdb -q (gdb) target remote /tmp/ch-gdb-sock Remote debugging using /tmp/ch-gdb-sock warning: No executable has been specified, and target does not support determining executable automatically. Try using the "file" command. 0x000000000011217e in ?? () ``` You can set up to four hardware breakpoints using the x86 debug register: ```bash (gdb) hb *0x1121b7 Hardware assisted breakpoint 1 at 0x1121b7 (gdb) c Continuing. Breakpoint 1, 0x00000000001121b7 in ?? () (gdb) ``` cloud-hypervisor-53.0/docs/generic-vhost-user.md000066400000000000000000000055021522476750100217750ustar00rootroot00000000000000# 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//\"/\"\"}\",device_type=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 `device_type=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. cloud-hypervisor-53.0/docs/heap-profiling.md000066400000000000000000000024011522476750100211430ustar00rootroot00000000000000# Heap profiling Cloud Hypervisor supports generating a profile using [dhat](https://docs.rs/dhat/latest/dhat/) of the heap allocations made during the runtime of the process. ## Building a suitable binary This adds the symbol information to the release binary but does not otherwise affect the performance. ``` $ cargo build --profile profiling --features "dhat-heap" ``` ## Generating output Cloud Hypervisor can then be run as usual. However it is necessary to run with `--seccomp false` as the profiling requires extra syscalls. ``` $ target/profiling/cloud-hypervisor \ --kernel ~/src/linux/vmlinux \ --pmem file=~/workloads/focal.raw \ --cpus boot=1 --memory size=1G \ --cmdline "root=/dev/pmem0p1 console=ttyS0" \ --serial tty --console off \ --api-socket /tmp/api1 \ --seccomp false ``` When the VMM exits a message like the following will be shown: ``` dhat: Total: 384,582 bytes in 3,512 blocks dhat: At t-gmax: 133,885 bytes in 379 blocks dhat: At t-end: 12,160 bytes in 20 blocks dhat: The data has been saved to dhat-heap.json, and is viewable with dhat/dh_view.html ``` The JSON output can then be uploaded to [the dh_view tool](https://nnethercote.github.io/dh_view/dh_view.html) for analysis. cloud-hypervisor-53.0/docs/hotplug.md000066400000000000000000000223541522476750100177320ustar00rootroot00000000000000# Cloud Hypervisor Hot Plug Currently Cloud Hypervisor supports hot plugging of CPUs devices (x86 only), PCI devices and memory resizing. ## Kernel support For hotplug on Cloud Hypervisor ACPI GED support is needed. This can either be achieved by turning on `CONFIG_ACPI_REDUCED_HARDWARE_ONLY` or by using this kernel patch (available in 5.5-rc1 and later): https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/patch/drivers/acpi/Makefile?id=ac36d37e943635fc072e9d4f47e40a48fbcdb3f0 ## CPU Hot Plug Extra vCPUs can be added and removed from a running `cloud-hypervisor` instance. This is controlled by two mechanisms: 1. Specifying a number of maximum potential vCPUs that is greater than the number of default (boot) vCPUs. 2. Making a HTTP API request to the VMM to ask for the additional vCPUs to be added. To use CPU hotplug start the VM with the number of max vCPUs greater than the number of boot vCPUs, e.g. ```shell $ pushd $CLOUDH $ sudo setcap cap_net_admin+ep ./cloud-hypervisor/target/release/cloud-hypervisor $ ./cloud-hypervisor/target/release/cloud-hypervisor \ --kernel custom-vmlinux.bin \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --disk path=focal-server-cloudimg-amd64.raw \ --cpus boot=4,max=8 \ --memory size=1024M \ --net "tap=,mac=,ip=,mask=" \ --rng \ --api-socket=/tmp/ch-socket $ popd ``` Notice the addition of `--api-socket=/tmp/ch-socket` and a `max` parameter on `--cpus boot=4,max=8`. To ask the VMM to add additional vCPUs then use the resize API: ```shell ./ch-remote --api-socket=/tmp/ch-socket resize --cpus 8 ``` The extra vCPU threads will be created and advertised to the running kernel. The kernel does not bring up the CPUs immediately and instead the user must "online" them from inside the VM: ```shell root@ch-guest ~ # lscpu | grep list: On-line CPU(s) list: 0-3 Off-line CPU(s) list: 4-7 root@ch-guest ~ # echo 1 | tee /sys/devices/system/cpu/cpu[4,5,6,7]/online 1 root@ch-guest ~ # lscpu | grep list: On-line CPU(s) list: 0-7 ``` After a reboot the added CPUs will remain. Removing CPUs works similarly by reducing the number in the "desired_vcpus" field of the resize API. The CPUs will be automatically offlined inside the guest so there is no need to run any commands inside the guest: ```shell ./ch-remote --api-socket=/tmp/ch-socket resize --cpus 2 ``` As per adding CPUs to the guest, after a reboot the VM will be running with the reduced number of vCPUs. ## Memory Hot Plug ### ACPI method Extra memory can be added from a running `cloud-hypervisor` instance. This is controlled by two mechanisms: 1. Allocating some of the guest physical address space for hotplug memory. 2. Making a HTTP API request to the VMM to ask for a new amount of RAM to be assigned to the VM. In the case of expanding the memory for the VM the new memory will be hotplugged into the running VM, if reducing the size of the memory then change will take effect after the next reboot. To use memory hotplug start the VM specifying some size RAM in the `hotplug_size` parameter to the memory configuration. Not all the memory specified in this parameter will be available to hotplug as there are spacing and alignment requirements so it is recommended to make it larger than the hotplug RAM needed. Because the ACPI method is the default, there is no need to add the extra option `hotplug_method=acpi`. ```shell $ pushd $CLOUDH $ sudo setcap cap_net_admin+ep ./cloud-hypervisor/target/release/cloud-hypervisor $ ./cloud-hypervisor/target/release/cloud-hypervisor \ --kernel custom-vmlinux.bin \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --disk path=focal-server-cloudimg-amd64.raw \ --cpus boot=4,max=8 \ --memory size=1024M,hotplug_size=8192M \ --net "tap=,mac=,ip=,mask=" \ --rng \ --api-socket=/tmp/ch-socket $ popd ``` Before issuing the API request it is necessary to run the following command inside the VM to make it automatically online the added memory: ```shell root@ch-guest ~ # echo online | sudo tee /sys/devices/system/memory/auto_online_blocks ``` To ask the VMM to expand the RAM for the VM: ```shell ./ch-remote --api-socket=/tmp/ch-socket resize --memory 3G ``` The new memory is now available to use inside the VM: ```shell free -h total used free shared buff/cache available Mem: 3.0Gi 71Mi 2.8Gi 0.0Ki 47Mi 2.8Gi Swap: 32Mi 0B 32Mi ``` 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. Memory and CPU resizing can be combined together into the same HTTP API request. ### virtio-mem method Extra memory can be added and removed from a running Cloud Hypervisor instance. This is controlled by two mechanisms: 1. Allocating some of the guest physical address space for hotplug memory. 2. Making a HTTP API request to the VMM to ask for a new amount of RAM to be assigned to the VM. To use memory hotplug start the VM specifying some size RAM in the `hotplug_size` parameter along with `hotplug_method=virtio-mem` to the memory configuration. ```shell $ pushd $CLOUDH $ sudo setcap cap_net_admin+ep ./cloud-hypervisor/target/release/cloud-hypervisor $ ./cloud-hypervisor/target/release/cloud-hypervisor \ --kernel custom-vmlinux.bin \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --disk path=focal-server-cloudimg-amd64.raw \ --memory size=1024M,hotplug_size=8192M,hotplug_method=virtio-mem \ --net "tap=,mac=,ip=,mask=" \ --api-socket=/tmp/ch-socket $ popd ``` To ask the VMM to expand the RAM for the VM (request is in bytes): ```shell ./ch-remote --api-socket=/tmp/ch-socket resize --memory 3G ``` The new memory is now available to use inside the VM: ```shell free -h total used free shared buff/cache available Mem: 3.0Gi 71Mi 2.8Gi 0.0Ki 47Mi 2.8Gi Swap: 32Mi 0B 32Mi ``` The same API can also be used to reduce the desired RAM for a VM. It is important to note that reducing RAM size might only partially work, as the guest might be using some of it. ## PCI Device Hot Plug Extra PCI devices can be added and removed from a running `cloud-hypervisor` instance. This is controlled by making a HTTP API request to the VMM to ask for the additional device to be added, or for the existing device to be removed. Note: On AArch64 platform, PCI device hotplug can only be achieved using ACPI. Please refer to the [documentation](uefi.md#building-uefi-firmware-for-aarch64) for more information. To use PCI device hotplug start the VM with the HTTP server. ```shell $ sudo setcap cap_net_admin+ep ./cloud-hypervisor/target/release/cloud-hypervisor $ ./cloud-hypervisor/target/release/cloud-hypervisor \ --kernel custom-vmlinux.bin \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --disk path=focal-server-cloudimg-amd64.raw \ --cpus boot=4 \ --memory size=1024M \ --net "tap=,mac=,ip=,mask=" \ --api-socket=/tmp/ch-socket ``` Notice the addition of `--api-socket=/tmp/ch-socket`. ### Add VFIO Device 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/ ``` ### Add Disk Device 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 ``` ### Add Fs Device 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 ``` ### Add Net Device 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 ``` ### Add Pmem Device 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 ``` ### Add Vsock Device 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 ``` ### Common Across All PCI Devices The extra PCI device will be created and advertised to the running kernel. The new device can be found by checking the list of PCI devices. ```shell root@ch-guest ~ # lspci 00:00.0 Host bridge: Intel Corporation Device 0d57 00:01.0 Unassigned class [ffff]: Red Hat, Inc. Virtio console (rev 01) 00:02.0 Mass storage controller: Red Hat, Inc. Virtio block device (rev 01) 00:03.0 Unassigned class [ffff]: Red Hat, Inc. Virtio RNG (rev 01) ``` After a reboot the added PCI device will remain. ### Remove PCI device 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 ``` As per adding a PCI device to the guest, after a reboot the VM will be running without the removed PCI device. cloud-hypervisor-53.0/docs/intel_tdx.md000066400000000000000000000135351522476750100202430ustar00rootroot00000000000000# Intel TDX Intel® Trust Domain Extensions (Intel® TDX) is an Intel technology designed to isolate virtual machines from the VMM, hypervisor and any other software on the host platform. Here are some useful links: - [TDX Homepage](https://www.intel.com/content/www/us/en/developer/tools/trust-domain-extensions/overview.html): more information about TDX technical aspects, design and specification - [KVM TDX tree](https://github.com/intel/tdx/tree/kvm): the required Linux kernel changes for the host side - [Guest TDX tree](https://github.com/intel/tdx/tree/guest): the Linux kernel changes for the guest side - [EDK2 project](https://github.com/tianocore/edk2): the TDVF firmware - [Confidential Containers project](https://github.com/confidential-containers/td-shim): the TDShim firmware - [TDX Linux](https://github.com/intel/tdx-linux): a collection of tools and scripts to setup TDX environment for testing purpose (such as installing required packages on the host, creating guest images, and building the custom Linux kernel for TDX host and guest) ## Cloud Hypervisor support It is required to use a machine with TDX enabled in hardware and with the host OS compiled from the [KVM TDX tree](https://github.com/intel/tdx/tree/kvm). The host environment can also be setup with the [TDX Linux](https://github.com/intel/tdx-linux). Cloud Hypervisor can run TDX VM (Trust Domain) by loading a TD firmware ([TDVF](https://github.com/tianocore/edk2)), which will then load the guest kernel from the image. The image must be custom as it must include a kernel built from the [Guest TDX tree](https://github.com/intel/tdx/tree/guest). Cloud Hypervisor can also boot a TDX VM with direct kernel boot using [TDshim](https://github.com/confidential-containers/td-shim). The custom Linux kernel for the guest can be built with the [TDX Linux](https://github.com/intel/tdx-linux). ### TDVF > **Note** > The latest version of TDVF being tested is [_13b9773_](https://github.com/tianocore/edk2/commit/13b97736c876919b9786055829caaa4fa46984b7). The firmware can be built as follows: ```bash sudo apt-get update sudo apt-get install uuid-dev nasm iasl build-essential python3-distutils git git clone https://github.com/tianocore/edk2.git cd edk2 git checkout 13b97736c876919b9786055829caaa4fa46984b7 source ./edksetup.sh git submodule update --init --recursive make -C BaseTools -j `nproc` build -p OvmfPkg/IntelTdx/IntelTdxX64.dsc -a X64 -t GCC5 -b RELEASE ``` If debug logs are needed, here is the alternative command: ```bash build -p OvmfPkg/IntelTdx/IntelTdxX64.dsc -a X64 -t GCC5 -D DEBUG_ON_SERIAL_PORT=TRUE ``` On the Cloud Hypervisor side, all you need is to build the project with the `tdx` feature enabled: ```bash cargo build --features tdx ``` And run a TDX VM by providing the firmware previously built, along with the guest image containing the TDX enlightened kernel. The latest image `td-guest-rhel8.5.raw` contains `console=hvc0` on the kernel boot parameters, meaning it will be printing guest kernel logs to the `virtio-console` device. ```bash ./cloud-hypervisor \ --platform tdx=on \ --firmware edk2/Build/IntelTdx/RELEASE_GCC5/FV/OVMF.fd \ --cpus boot=1 \ --memory size=1G \ --disk path=tdx_guest_img ``` And here is the alternative command when looking for debug logs from the firmware: ```bash ./cloud-hypervisor \ --platform tdx=on \ --firmware edk2/Build/IntelTdx/DEBUG_GCC5/FV/OVMF.fd \ --cpus boot=1 \ --memory size=1G \ --disk path=tdx_guest_img \ --serial file=/tmp/ch_serial \ --console tty ``` ### TDShim > **Note** > The latest version of TDShim being tested is [_v0.8.0_](https://github.com/confidential-containers/td-shim/releases/tag/v0.8.0). 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 built as follows: ```bash git clone https://github.com/confidential-containers/td-shim cd td-shim git checkout v0.8.0 cargo install cargo-xbuild export CC=clang export AR=llvm-ar export CC_x86_64_unknown_none=clang export AR_x86_64_unknown_none=llvm-ar git submodule update --init --recursive ./sh_script/preparation.sh cargo image --release ``` If debug logs from the TDShim is needed, here are the alternative commands: ```bash cargo image ``` And run a TDX VM by providing the firmware previously built, along with a guest kernel built from the [Guest TDX tree](https://github.com/intel/tdx/tree/guest) or the [TDX Linux](https://github.com/intel/tdx-linux). The appropriate kernel boot options must be provided through the `--cmdline` option as well. ```bash ./cloud-hypervisor \ --platform tdx=on \ --firmware td-shim/target/release/final.bin \ --kernel bzImage \ --cmdline "root=/dev/vda3 console=hvc0 rw" \ --cpus boot=1 \ --memory size=1G \ --disk path=tdx_guest_img ``` And here is the alternative command when looking for debug logs from the TDShim: ```bash ./cloud-hypervisor \ --platform tdx=on \ --firmware td-shim/target/debug/final.bin \ --kernel bzImage \ --cmdline "root=/dev/vda3 console=hvc0 rw" \ --cpus boot=1 \ --memory size=1G \ --disk path=tdx_guest_img ``` ### Guest kernel limitations #### Serial ports disabled The latest guest kernel that can be found in the latest image `td-guest-rhel8.5.raw` disabled the support for serial ports. This means adding `console=ttyS0` will have no effect and will not print any log from the guest. #### PCI hotplug through ACPI Unless you run the guest kernel with the parameter `tdx_disable_filter`, ACPI devices responsible for handling PCI hotplug (PCI hotplug controller, PCI Express Bus and Generic Event Device) will not be allowed, therefore the corresponding drivers will not be loaded and the PCI hotplug feature will not be supported. cloud-hypervisor-53.0/docs/io_throttling.md000066400000000000000000000062351522476750100211350ustar00rootroot00000000000000# I/O Throttling Cloud Hypervisor now supports I/O throttling on virtio-block and virtio-net devices. This support is based on the [`rate-limiter` module](https://github.com/firecracker-microvm/firecracker/tree/7a1231b141e958d15d5b2c079dd5e0880528b4b0/src/rate_limiter) from Firecracker. This document explains the user interface of this feature, and highlights some internal implementations that can help users better understand the expected behavior of I/O throttling in practice. Cloud Hypervisor allows to limit both the I/O bandwidth (e.g. bytes/s) and I/O operations (ops/s) independently. For virtio-net devices, while sharing the same "rate limit" from user inputs (on both bandwidth and operations), the RX and TX queues are throttled independently. To limit the I/O bandwidth, Cloud Hypervisor provides three user options, i.e., `bw_size` (bytes), `bw_one_time_burst` (bytes), and `bw_refill_time` (ms). Both `bw_size` and `bw_refill_time` are required, while `bw_one_time_burst` is optional. Internally, these options define a TokenBucket with a maximum capacity (`bw_size` bytes), an initial burst size (`bw_one_time_burst`) and an interval for refilling purposes (`bw_refill_time`). The "refill-rate" is `bw_size` bytes per `bw_refill_time` ms, and it is the constant rate at which the tokens replenish. The refill process only starts happening after the initial burst budget is consumed. Consumption from the token bucket is unbounded in speed which allows for bursts bound in size by the amount of tokens available. Once the token bucket is empty, consumption speed is bound by the "refill-rate". Similarly, Cloud 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 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 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 `ops_size=10,ops_refill_time=10`. However, the actual IOPS limits are likely to be ~1000 IOPS and ~100 IOPS respectively. The reason is the actual rate limit users get can be as low as `ops_size/(ops_refill_time+cool_down_time)`. As a result, it is generally advisable to keep `bw/ops_refill_time` larger than `100 ms` (`cool_down_time`) to make sure the actual rate limit is close to users' expectation ("refill-rate"). ## Rate Limit Groups It is possible to throttle the aggregate bandwidth or operations of multiple virtio-blk devices using a `rate_limit_group`. virtio-blk devices may be dynamically added and removed from a `rate_limit_group`. The following example 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=100 ``` cloud-hypervisor-53.0/docs/iommu.md000066400000000000000000000242061522476750100173740ustar00rootroot00000000000000# Virtual IOMMU ## Rationales Having the possibility to expose a virtual IOMMU to the guest can be interesting to support specific use cases. That being said, it is always important to keep in mind a virtual IOMMU can impact the performance of the attached devices, which is the reason why one should be careful when enabling this feature. ### Protect nested virtual machines The first reason why one might want to expose a virtual IOMMU to the guest is 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 trying accessing the guest memory. This is standard interposition that is performed here by the VMM. The increased security does not apply for a simple case where we have one VM per VMM. Because the guest cannot be trusted, as we always consider it could be malicious and gain unauthorized privileges inside the VM, preventing some devices from accessing the entire guest memory is pointless. But let's take the interesting case of nested virtualization, and let's assume we have a VMM running a first layer VM. This L1 guest is fully trusted as the user intends to run multiple VMs from this L1. We can end up with multiple L2 VMs running on a single L1 VM. In this particular case, and without exposing a virtual IOMMU to the L1 guest, it would be possible for any L2 guest to use the device implementation from the host VMM to access the entire guest L1 memory. The virtual IOMMU prevents from this kind of trouble as it will validate the addresses the device is authorized to access. ### Achieve VFIO nested Another reason for having a virtual IOMMU is to allow passing physical devices from the host through multiple layers of virtualization. Let's take as example a system with a physical IOMMU running a VM with a virtual IOMMU. The implementation of the virtual IOMMU is responsible for updating the physical DMA Remapping table (DMAR) every time the DMA mapping changes. This must happen through the VFIO framework on the host as this is the only userspace interface to interact with a physical IOMMU. Relying on this update mechanism, it is possible to attach physical devices to the virtual IOMMU, which allows these devices to be passed from L1 to another layer of virtualization. ## Why virtio-iommu? The Cloud Hypervisor project decided to implement the brand new virtio-iommu device in order to provide a virtual IOMMU to its users. The reason being the simplicity brought by the paravirtualization solution. By having one side handled from the guest itself, it removes the complexity of trapping memory page accesses and shadowing them. This is why the project will not try to implement a full emulation of a physical IOMMU. ## Pre-requisites ### Kernel As of Kernel 5.14, virtio-iommu is available for both X86-64 and Aarch64. ## Usage In order to expose a virtual IOMMU to the guest, it is required to create a virtio-iommu device and expose it through the ACPI IORT table. This can be simply achieved by attaching at least one device to the virtual IOMMU. The way to expose to the guest a specific device as sitting behind this IOMMU is to explicitly tag it from the command line with the option `iommu=on`. 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 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: ```bash ./cloud-hypervisor \ --cpus boot=1 \ --memory size=512M \ --disk path=focal-server-cloudimg-amd64.raw,iommu=on \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ ``` From a guest perspective, it is easy to verify if the device is protected by the virtual IOMMU. Check the directories listed under `/sys/kernel/iommu_groups`: ```bash ls /sys/kernel/iommu_groups 0 ``` In this case, only one IOMMU group should be created. Under this group, it is possible to find out the b/d/f of the device(s) part of this group. ```bash ls /sys/kernel/iommu_groups/0/devices/ 0000:00:03.0 ``` And you can validate the device is the one we expect running `lspci`: ```bash lspci 00:00.0 Host bridge: Intel Corporation Device 0d57 00:01.0 Unassigned class [ffff]: Red Hat, Inc. Device 1057 00:02.0 Unassigned class [ffff]: Red Hat, Inc. Virtio console 00:03.0 Mass storage controller: Red Hat, Inc. Virtio block device 00:04.0 Unassigned class [ffff]: Red Hat, Inc. Virtio RNG ``` ### Work with FDT on AArch64 On AArch64 architecture, the virtual IOMMU can still be used even if ACPI is not enabled. But the effect is different with what the aforementioned test showed. When ACPI is disabled, virtual IOMMU is supported through Flattened Device Tree (FDT). In this case, the guest kernel cannot tell which device should be 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 an IOMMU group. As a result, the directory content of `/sys/kernel/iommu_groups` would be: ```bash ls /sys/kernel/iommu_groups/0/devices/ 0000:00:02.0 ls /sys/kernel/iommu_groups/1/devices/ 0000:00:03.0 ls /sys/kernel/iommu_groups/2/devices/ 0000:00:04.0 ``` ## Faster mappings By default, the guest memory is mapped with 4k pages and no huge pages, which causes the virtual IOMMU device to be asked for 4k mappings only. This configuration slows down the setup of the physical IOMMU as an important number 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 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 of mappings, which might exceed the VFIO limit set on the host. The default value is 65536, which can simply be reached with a 256MiB sized RAM. The way to solve both problems, the slowdown and the limit being exceeded, is to reduce the amount of requests to describe those same large mappings. This can be achieved by using 2MiB pages, known as huge pages. By seeing the guest RAM as larger pages, and because the virtual IOMMU device supports it, the guest will require less mappings, which will prevent the limit from being exceeded, but also will take less time to process them on the host. That's how using huge pages as much as possible can speed up VM boot time. ### Basic usage Let's look at an example of how to run a guest with huge pages. First, make sure your system has enough pages to cover the entire guest RAM: ```bash # This example creates 4096 hugepages echo 4096 > /proc/sys/vm/nr_hugepages ``` Next step is simply to create the VM. Two things are important, first we want the VM RAM to be mapped on huge pages by backing it with `/dev/hugepages`. And second thing, we need to create some huge pages in the guest itself so they can be consumed. ```bash ./cloud-hypervisor \ --cpus boot=1 \ --memory size=8G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw hugepagesz=2M hugepages=2048" \ --net tap=,mac=,iommu=on ``` ### Nested usage Let's now look at the specific example of nested virtualization. In order to 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`. ```bash ./cloud-hypervisor \ --cpus boot=1 \ --memory size=8G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw kvm-intel.nested=1 vfio_iommu_type1.allow_unsafe_interrupts rw hugepagesz=2M hugepages=2048" \ --device path=/sys/bus/pci/devices/0000:00:01.0,iommu=on ``` Once the L1 VM is running, unbind the device from the default driver in the guest, and bind it to VFIO (it should appear as `0000:00:04.0`). ```bash echo 0000:00:04.0 > /sys/bus/pci/devices/0000\:00\:04.0/driver/unbind echo 8086 1502 > /sys/bus/pci/drivers/vfio-pci/new_id echo 0000:00:04.0 > /sys/bus/pci/drivers/vfio-pci/bind ``` Last thing is to start the L2 guest with the huge pages memory backend. ```bash ./cloud-hypervisor \ --cpus boot=1 \ --memory size=4G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --device path=/sys/bus/pci/devices/0000:00:04.0 ``` ### Dedicated IOMMU PCI segments To facilitate hotplug of devices that require being behind an IOMMU it is possible to mark entire PCI segments as behind the IOMMU. This is accomplished through `--platform num_pci_segments=,iommu_segments=` or via the equivalents in `PlatformConfig` for the API. e.g. ```bash ./cloud-hypervisor \ --api-socket=/tmp/api \ --cpus boot=1 \ --memory size=4G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --platform num_pci_segments=2,iommu_segments=1 ``` This adds a second PCI segment to the platform behind the IOMMU. A VFIO device requiring the IOMMU then may be hotplugged: e.g. ```bash ./ch-remote --api-socket=/tmp/api add-device path=/sys/bus/pci/devices/0000:00:04.0,iommu=on,pci_segment=1 ``` Devices that cannot be placed behind an IOMMU (e.g. lacking an `iommu=` option) cannot be placed on the IOMMU segments. cloud-hypervisor-53.0/docs/ivshmem.md000066400000000000000000000032401522476750100177110ustar00rootroot00000000000000# 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=,id=,pci_segment=,pci_device_id=" ``` ## 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`. cloud-hypervisor-53.0/docs/landlock.md000066400000000000000000000057521522476750100200420ustar00rootroot00000000000000# Sandboxing using Landlock Landlock is a lightweight mechanism to allow unprivileged applications to sandbox themselves. During initial stages of running, applications can define the set of resources (mostly files) they need to access during their lifetime. All such rules are used to create a ruleset. Once the ruleset is applied, the process cannot access any resources outside of the ruleset during its lifetime, even if it were compromised. Under the scope of `read` and `write` access, Landlock currently allows some additional accesses (eg: for now, access to extended file attributes is always allowed). Eventually, Landlock will only allow accesses similar to Unix permissions. ## Host Setup 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 the following command: ``` $ sudo dmesg | grep -w landlock [ 0.000000] landlock: Up and running. ``` 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 at the `vm_create` stage of guest boot. ### Command Line Append `--landlock` to Cloud-Hypervisor's command line to enable Landlock support. If you expect guest to access additional paths after it boots (ex: during hotplug), those paths can be passed using `--landlock-rules` command line parameter. ### API Landlock can also be enabled during `vm.create` request by passing a config like below: ``` { ... "landlock_enable": true, "landlock_rules": [ { "path": "/tmp/disk1", "access": "rw" }, { "path": "/tmp/disk2", "access": "rw" } ] ... } ``` ## Usage Examples To enable Landlock: ``` ./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=" \ --landlock ``` Hotplugging any new file-backed resources to above guest will result in **Permission Denied** error. To enable Landlock with hotplug support: ``` ./cloud-hypervisor \ --api-socket /tmpXXXX/ch.socket \ --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=" \ --landlock \ --landlock-rules path="/path/to/hotplug1",access="rw" path="/path/to/hotplug2",access="rw" ./ch-remote --api-socket /tmpXXXX/ch.socket \ add-disk "path=/path/to/hotplug/blk.raw" ``` `--landlock-rules` accepts file or directory paths among its options. # References * https://landlock.io/ cloud-hypervisor-53.0/docs/live_migration.md000066400000000000000000000327211522476750100212570ustar00rootroot00000000000000# Live Migration 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; also called UNIX socket migration. 1. **Remote Migration** (TCP Migration): migrating a VM between two TCP/IP hosts. > :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) Launch the source VM (on the host machine): ```console $ target/release/cloud-hypervisor --kernel ~/workloads/vmlinux \ --disk path=~/workloads/focal.raw \ --cpus boot=1 --memory size=1G,shared=on \ --cmdline "root=/dev/vda1 console=ttyS0" \ --serial tty --console off --api-socket=/tmp/api1 ``` Launch the destination VM from the same directory (on the host machine): ```console $ target/release/cloud-hypervisor --api-socket=/tmp/api2 ``` Get ready for receiving migration for the destination VM (on the host machine): ```console $ target/release/ch-remote --api-socket=/tmp/api2 receive-migration receiver_url=unix:/tmp/sock ``` Start to send migration for the source VM (on the host machine): ```console $ target/release/ch-remote --api-socket=/tmp/api1 send-migration destination_url=unix:/tmp/sock,local=on ``` 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 (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 minimal VM setup without storage. ### Preparation Make sure that `src` and `dst` can reach each other via the network. You should be able to ping each machine. Also each machine should have an open TCP port. You will need a kernel and initramfs for a minimal Linux system. For this example, we will use the Debian netboot image. Place the kernel and initramfs into the _same directory_ on both machines. This is important for the migration to succeed. We will use `/var/images`: ```console src $ export DEBIAN=https://ftp.debian.org/debian/dists/stable/main/installer-amd64/current/images/netboot/debian-installer/amd64 src $ mkdir -p /var/images src $ curl $DEBIAN/linux > /var/images/linux src $ curl $DEBIAN/initrd.gz > /var/images/initrd ``` Repeat the above steps on the destination host. ### Unix Socket Migration If Unix socket is selected for migration, we can tunnel traffic through "socat". #### Starting the Receiver VM On the receiver side, we prepare an empty VM: ```console dst $ cloud-hypervisor --api-socket /tmp/api ``` In a different terminal, configure the VM as a migration target: ```console dst $ ch-remote --api-socket=/tmp/api receive-migration receiver_url=unix:/tmp/sock ``` In yet another terminal, forward TCP connections to the Unix domain socket: ```console dst $ socat TCP-LISTEN:{port},reuseaddr UNIX-CLIENT:/tmp/sock ``` #### Starting the Sender VM Let's start the VM on the source machine: ```console src $ cloud-hypervisor \ --serial tty --console off \ --cpus boot=2 --memory size=4G \ --kernel /var/images/linux \ --initramfs /var/images/initrd \ --cmdline "console=ttyS0" \ --api-socket /tmp/api ``` After a few seconds the VM should be up and you can interact with it. #### Performing the Migration First, we start `socat`: ```console src $ socat UNIX-LISTEN:/tmp/sock,reuseaddr TCP:{dst}:{port} ``` > Replace {dst}:{port} with the actual IP address and port of your destination host. Then we kick-off the migration itself: ```console src $ ch-remote --api-socket=/tmp/api send-migration destination_url=unix:/tmp/sock ``` When the above commands completed, the VM should be successfully migrated to the destination machine without interrupting the workload. ### Network Announcements After Resume After a VM resumes from migration, snapshot restore, or any other path that restores a previously paused VM, Cloud Hypervisor asks supported network devices to announce the VM from its new host. For `virtio-net`, the current implementation sets `VIRTIO_NET_S_ANNOUNCE`, raises a config interrupt, retries that request a few times in the background, and also sends host-side RARP announcements on the TAP interfaces. A guest re-announcement therefore only happens when the guest negotiated `VIRTIO_NET_F_GUEST_ANNOUNCE`. For `vhost-user-net`, the current implementation only uses the guest announcement path. ### TCP Socket Migration If TCP socket is selected for migration, we need to consider migrating in a trusted network. #### Starting the Receiver VM On the receiver side, we prepare an empty VM: ```console dst $ cloud-hypervisor --api-socket /tmp/api ``` In a different terminal, prepare to receive the migration: ```console dst $ ch-remote --api-socket=/tmp/api receive-migration receiver_url=tcp:0.0.0.0:{port} ``` #### Starting the Sender VM Let's start the VM on the source machine: ```console src $ cloud-hypervisor \ --serial tty --console off \ --cpus boot=2 --memory size=4G \ --kernel /var/images/linux \ --initramfs /var/images/initrd \ --cmdline "console=ttyS0" \ --api-socket /tmp/api ``` After a few seconds the VM should be up and you can interact with it. #### Performing the Migration Initiate the Migration over TCP: ```console 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. 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. See [Network Announcements After Resume](#network-announcements-after-resume) for the announcement behavior after a VM resumes. #### 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: - `ca-cert.pem`: the CA certificate used to verify source certificates - `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 - `client-cert.pem`: the certificate presented by the source - `client-key.pem`: the private key for `client-cert.pem` Protect the private key files with file mode `0600` to reduce the risk of accidental disclosure: ```console $ chmod 600 server-key.pem client-key.pem ``` Current TCP migration uses mutual TLS (mTLS) authentication. The source verifies the destination certificate against `ca-cert.pem` and presents `client-cert.pem` and `client-key.pem`. The destination presents `server-cert.pem` and `server-key.pem`, and only accepts client certificates that chain to `ca-cert.pem`. 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. The commands below describe how to create the encryption material necessary to perform a same-host TCP migration with TLS. ##### Setup the Certificate Authority First, set up the CA with a private key and a self-signed certificate. ```console $ certtool --generate-privkey \ --outfile ca-key.pem ``` To generate the certificate, provide at least your organization name in a template file, for example `ca.info`: ```text cn = Name of your organization ca cert_signing_key ``` Then you can create the certificate: ```console $ certtool --generate-self-signed \ --load-privkey ca-key.pem \ --template ca.info \ --outfile ca-cert.pem ``` ##### Issuing host certificates Generate a private key and a certificate for each side. Cloud Hypervisor enforces mTLS, thus you also need a key for the migration sender. The certificates also require template data. You can use separate templates for sender and receiver, but this example uses a single template, `hosts.info`: ```text organization = Name of your organization cn = localhost tls_www_server tls_www_client signing_key dns_name = localhost ip_address = 127.0.0.1 ``` Then you can create the necessary files for the client (migration sender): ```console $ certtool --generate-privkey \ --outfile client-key.pem $ certtool --generate-certificate \ --load-ca-certificate ca-cert.pem \ --load-ca-privkey ca-key.pem \ --load-privkey client-key.pem \ --template hosts.info \ --outfile client-cert.pem ``` And the necessary files for the server (migration receiver): ```console $ certtool --generate-privkey \ --outfile server-key.pem $ certtool --generate-certificate \ --load-ca-certificate ca-cert.pem \ --load-ca-privkey ca-key.pem \ --load-privkey server-key.pem \ --template hosts.info \ --outfile server-cert.pem ``` For a same-host test, place these files in a single directory and use that directory as `tls_dir` for both commands. ##### Performing the Migration On the receiver side, we prepare an empty VM: ```console dst $ cloud-hypervisor --api-socket /tmp/api-rcv ``` In a different terminal, prepare to receive the migration: ```console dst $ ch-remote --api-socket=/tmp/api-rcv receive-migration receiver_url=tcp:0.0.0.0:9000,tls_dir=/path/to/certificates ``` On the sender side, we start a VM: ```console src $ cloud-hypervisor \ --serial tty --console off \ --cpus boot=2 --memory size=4G \ --kernel /var/images/linux \ --initramfs /var/images/initrd \ --cmdline "console=ttyS0" \ --api-socket /tmp/api-src ``` After a few seconds the VM should be up and you can interact with it. Then trigger the migration: ```console src $ ch-remote --api-socket=/tmp/api-src send-migration destination_url=tcp:localhost:9000,tls_dir=/path/to/certificates ``` #### 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. - `memory_mode `: \ Memory transfer mode. `postcopy` resumes the destination first and faults guest pages in on demand over a dedicated connection. Defaults to `precopy`. ## Version Compatibility Cloud Hypervisor live migration compatibility has two dimensions: the Cloud Hypervisor version and the migration protocol version. Cloud Hypervisor uses a versioned migration protocol for the messages exchanged between source and destination. Each Cloud Hypervisor release sends its current migration protocol version and accepts that version plus the immediately previous protocol version. This means migration is supported from an older protocol version to the same or next protocol version. If the source and destination are more than one migration protocol version apart, the VM must be migrated through an intermediate Cloud Hypervisor version first. ### Technical Details The source sends its migration protocol version in the initial `Start` request. The destination accepts the migration only if that protocol version is supported. A zeroed `Start` command header is handled as protocol `v0`, so older deployments that do not explicitly send a protocol version remain compatible. The migration protocol version covers the protocol spoken after a migration connection has been established. Examples include adding a mandatory migration command, changing the order of migration protocol messages, or changing the framing or encoding of protocol command payloads. The migration protocol version does not cover migration transport setup. For example, choosing TCP vs. UNIX sockets or opening the initial connection needs separate compatibility handling. Migration protocol versioning is separate from snapshot state compatibility. Device and VM state changes still need to be handled by the respective snapshot serialization/deserialization code. That compatibility is Cloud Hypervisor version dependent, but it is not the responsibility of migration protocol versioning. cloud-hypervisor-53.0/docs/logging.md000066400000000000000000000112571522476750100176760ustar00rootroot00000000000000# Logging The target audience of this document is both: - Developers who want to understand what log level to use and when, - Users who want to debug issues with running their workloads in Cloud Hypervisor ## Control The number of `-v` parameters passed to the `cloud-hypervisor` binary will determine the log level. Currently the default is log messages up to `WARN:` (`warn!`) are included by default. The `--log-file` allows the log to be sent to a location other than `stderr`. ## Levels ### `error!()` For any user-initiated action that cannot be carried out as expected, as well as serious or fatal conditions within the VMM itself. This covers two cases: - A requested operation fails with material impact, even if Cloud Hypervisor can continue running (e.g. a failed device hotplug, live migration, snapshot/restore, or resize). From the user's perspective the action they asked for did not happen, so it is an error to them. - An unrecoverable condition where Cloud Hypervisor cannot continue and exits with a non-zero code (e.g. conflicting command line options, or a required file that is not present). Users should react by checking their configuration or the requested operation. ### `warn!()` For abnormal conditions that neither prevent a user-initiated action nor seriously impact the VM. These are user-facing and developer-facing warnings. A typical example is an ineffectual out-of-bounds access that the VMM safely ignores. ### `info!()` Use `-v` to enable. Primarily targeted at operators and users. For important but infrequent normal conditions, events, and state changes that are meaningful in production. The same message should not "spam" the logs, and the VM should remain usable when this level is enabled (e.g. using stdin/stdout while the logs go to stderr). ### `debug!()` Use `-vv` to enable. Developer-facing diagnostic information. It is acceptable to repeat the same message here. ### `trace!()` Use `-vvv` to enable. The most verbose level, for very detailed developer-facing information. As with `debug!()`, repeated messages are acceptable. This level is typically 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}' ``` cloud-hypervisor-53.0/docs/macvtap-bridge.md000066400000000000000000000027121522476750100211310ustar00rootroot00000000000000# 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 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 mac="c2:67:4f:53:29:cb" # Host network adapter to bridge the guest onto host_net="eno1" # Create the macvtap0 as a new virtual MAC associated with the host network sudo ip link add link "$host_net" name macvtap0 type macvtap sudo ip link set macvtap0 address "$mac" up sudo ip link show macvtap0 # A new character device is created for this interface tapindex=$(< /sys/class/net/macvtap0/ifindex) tapdevice="/dev/tap$tapindex" # Ensure that we can access this device sudo chown "$UID:$UID" "$tapdevice" # Use --net fd=3 to point to fd 3 which the shell has opened to point to the /dev/tapN device target/debug/cloud-hypervisor \ --kernel ~/src/linux/vmlinux \ --disk path=~/workloads/focal.raw \ --cpus boot=1 --memory size=512M \ --cmdline "root=/dev/vda1 console=hvc0" \ --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) cloud-hypervisor-53.0/docs/memory.md000066400000000000000000000572561522476750100175710ustar00rootroot00000000000000# Memory Cloud Hypervisor has many ways to expose memory to the guest VM. This document aims to explain what Cloud Hypervisor is capable of and how it can be used to meet the needs of very different use cases. ## Basic Parameters `MemoryConfig` or what is known as `--memory` from the CLI perspective is the easiest way to get started with Cloud Hypervisor. ```rust struct MemoryConfig { size: u64, mergeable: bool, hotplug_method: HotplugMethod, hotplug_size: Option, hotplugged_size: Option, shared: bool, hugepages: bool, hugepage_size: Option, prefault: bool, reserve: bool, thp: bool, zones: Option>, } ``` ``` --memory Memory parameters "size=,mergeable=on|off,shared=on|off,hugepages=on|off,hugepage_size=,hotplug_method=acpi|virtio-mem,hotplug_size=,hotplugged_size=,prefault=on|off,reserve=on|off,thp=on|off" [default: size=512M,thp=on] ``` ### `size` Size of the RAM in the guest VM. This option is mandatory when using the `--memory` parameter. Value is an unsigned integer of 64 bits. _Example_ ``` --memory size=1G ``` ### `mergeable` Specifies if the pages from the guest RAM must be marked as _mergeable_. In case this option is `true` or `on`, the pages will be marked with `madvise(2)` to let the host kernel know which pages are eligible for being merged by the KSM daemon. This option can be used when trying to reach a higher density of VMs running on a single host, as it will reduce the amount of memory consumed by each VM. By default this option is turned off. _Example_ ``` --memory size=1G,mergeable=on ``` ### `hotplug_method` Selects the way of adding and/or removing memory to/from a booted VM. Possible values are `acpi` and `virtio-mem`. Default value is `acpi`. _Example_ ``` --memory size=1G,hotplug_method=acpi ``` ### `hotplug_size` Amount of memory that can be dynamically added to the VM. Value is an unsigned integer of 64 bits. A value of 0 is invalid. _Example_ ``` --memory size=1G,hotplug_size=1G ``` ### `hotplugged_size` Amount of memory that will be dynamically added to the VM at boot. This option allows for starting a VM with a certain amount of memory that can be reduced during runtime. This is only valid when the `hotplug_method` is `virtio-mem` as it does not make sense for the `acpi` use case. When using ACPI, the memory can't be resized after it has been extended. This option is only valid when `hotplug_size` is specified, and its value can't exceed the value of `hotplug_size`. Value is an unsigned integer of 64 bits. A value of 0 is invalid. _Example_ ``` --memory size=1G,hotplug_method=virtio-mem,hotplug_size=1G,hotplugged_size=512M ``` ### `shared` Specifies if the memory must be `mmap(2)` with `MAP_SHARED` flag. By sharing a memory mapping, one can share the guest RAM with other processes running on the host. One can use this option when running 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. 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 require `MAP_SHARED`. _Example_ ``` --memory size=1G,shared=on ``` ### `hugepages` and `hugepage_size` Specifies if the memory must be created and `mmap(2)` with `MAP_HUGETLB` and size flags. This performs a memory mapping relying on the specified huge page size. 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 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 behaviour, e.g. error with `ReadKernelImage` is common. If there is a strange error with `hugepages` enabled, just disable it or check whether there are enough huge pages. If `hugepages=on` then the value of `shared` is ignored as huge pages always requires `MAP_SHARED`. By default this option is turned off. _Example_ ``` --memory size=1G,hugepages=on,hugepage_size=2M ``` ### `prefault` Specifies whether guest memory should be prefaulted with `madvise(2)` using `MADV_POPULATE_WRITE` after it is `mmap(2)`-ed. With prefault enabled, Cloud Hypervisor allocates the required physical memory and creates its page tables before the VM starts. This reduces page faults at runtime and can surface insufficient backing memory during VM creation. If prefaulting fails, VM creation fails with an error. Note that boot of VM will be slower with `prefault` enabled because of allocating physical memory and creating page tables in advance, and physical memory of the specified size will be consumed quickly. This option only takes effect at boot of VM. There is also a `prefault` option in restore and its choice will overwrite `prefault` in memory. By default, this option is turned off. _Example_ ``` --memory size=1G,prefault=on ``` ### `reserve` Specifies if guest memory should be `mmap(2)`-ed _without_ the `MAP_NORESERVE` flag, asking the kernel to reserve the backing pages (swap space, or huge pages for hugepage-backed memory) for the whole region up front at `mmap` time. By default Cloud Hypervisor maps guest memory with `MAP_NORESERVE`, so VM creation succeeds even when the backing pool cannot satisfy the full guest size. The shortfall then surfaces only later, as a `SIGBUS` delivered to the guest when it faults a page the pool cannot back. With `reserve=on` the reservation is made when the memory is mapped, so an over-committed configuration fails cleanly at VM creation with an out-of-memory error instead of crashing the guest at run time. This is most useful for hugepage-backed memory, where the huge pages are reserved from the pool. Unlike `prefault`, this does not populate or fault in the memory, so it does not slow down boot; it only reserves it. By default this option is turned off. _Example_ ``` --memory size=1G,hugepages=on,reserve=on ``` ### `thp` Specifies if private anonymous memory for the guest (i.e. `shared=off` and no 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 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. By default this option is turned on. _Example_ ``` --memory size=1G,thp=on ``` ## Advanced Parameters `MemoryZoneConfig` or what is known as `--memory-zone` from the CLI perspective is a power user parameter. It allows for a full description of the guest RAM, describing how every memory region is backed and exposed to the guest. ```rust struct MemoryZoneConfig { id: String, size: u64, file: Option, shared: bool, hugepages: bool, hugepage_size: Option, host_numa_node: Option, hotplug_size: Option, hotplugged_size: Option, prefault: bool, reserve: 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,reserve=on|off,mergeable=on|off" ``` This parameter expects one or more occurrences, allowing for a list of memory zones to be defined. It must be used with `--memory size=0`, clearly indicating that the memory will be described through advanced parameters. Each zone is given a list of options which we detail through the following sections. ### `id` Memory zone identifier. This identifier must be unique, otherwise an error will be returned. This option is useful when referring to a memory zone previously created. In particular, the `--numa` parameter can associate a memory zone to a specific NUMA node based on the memory zone identifier. This option is mandatory when using the `--memory-zone` parameter. Value is a string. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G ``` ### `size` Size of the memory zone. This option is mandatory when using the `--memory-zone` parameter. Value is an unsigned integer of 64 bits. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G ``` ### `file` Path to the file backing the memory zone. The file will be opened and used as the backing file for the `mmap(2)` operation. This option can be particularly useful when trying to back a part of the guest RAM with a well known file. In the context of the snapshot/restore feature, and if the provided path is a file, the snapshot operation will not perform any copy of the guest RAM content for this specific memory zone since the user has access to it and it would duplicate data already stored on the current filesystem. Value is a string. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G,file=/foo/bar ``` ### `shared` Specifies if the memory zone must be `mmap(2)` with `MAP_SHARED` flag. By sharing a memory zone mapping, one can share part of the guest RAM with other processes running on the host. One can use this option when running 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 require `MAP_SHARED`. By default this option is turned off, which results in performing `mmap(2)` with `MAP_PRIVATE` flag. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G,shared=on ``` ### `hugepages` and `hugepage_size` Specifies if the memory must be created and `mmap(2)` with `MAP_HUGETLB` and size flags. This performs a memory mapping relying on the specified huge page size. 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 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 behaviour, e.g. error with `ReadKernelImage` is common. If there is a strange error with `hugepages` enabled, just disable it or check whether there are enough huge pages. If `hugepages=on` then the value of `shared` is ignored as huge pages always require `MAP_SHARED`. By default this option is turned off. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G,hugepages=on,hugepage_size=2M ``` ### `host_numa_node` Node identifier of a node present on the host. This option will let the user pick a specific NUMA node from which the memory must be allocated. After the memory zone is `mmap(2)`, the NUMA policy for this memory mapping will be applied through `mbind(2)`, relying on the provided node identifier. If the node does not exist on the host, the call to `mbind(2)` will fail. This option is useful when trying to back a VM memory with a specific type of memory from the host. Assuming a host has two types of memory, with one slower than the other, each related to a distinct NUMA node, one could create a VM with slower memory accesses by backing the entire guest RAM from the furthest NUMA node on the host. This option also gives the opportunity to create a VM with non uniform memory accesses as one could define a first memory zone backed by fast memory, and a second memory zone backed by slow memory. Value is an unsigned integer of 32 bits. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G,host_numa_node=0 ``` ### `hotplug_size` Amount of memory that can be dynamically added to the memory zone. Since `virtio-mem` is the only way of resizing a memory zone, one must specify the `hotplug_method=virtio-mem` to the `--memory` parameter. Value is an unsigned integer of 64 bits. A value of 0 is invalid. _Example_ ``` --memory size=0,hotplug_method=virtio-mem --memory-zone id=mem0,size=1G,hotplug_size=1G ``` ### `hotplugged_size` Amount of memory that will be dynamically added to a memory zone at VM's boot. This option allows for starting a VM with a certain amount of memory that can be reduced during runtime. This is only valid when the `hotplug_method` is `virtio-mem` as it does not make sense for the `acpi` use case. When using ACPI, the memory can't be resized after it has been extended. This option is only valid when `hotplug_size` is specified, and its value can't exceed the value of `hotplug_size`. Value is an unsigned integer of 64 bits. A value of 0 is invalid. _Example_ ``` --memory size=0,hotplug_method=virtio-mem --memory-zone id=mem0,size=1G,hotplug_size=1G,hotplugged_size=512M ``` ### `prefault` Specifies whether this memory zone should be prefaulted with `madvise(2)` using `MADV_POPULATE_WRITE` after it is `mmap(2)`-ed. With prefault enabled, Cloud Hypervisor allocates the required physical memory and creates its page tables before the VM starts. This reduces page faults at runtime and can surface insufficient backing memory during VM creation. If prefaulting fails, VM creation fails with an error. Note that boot of VM will be slower with `prefault` enabled because of allocating physical memory and creating page tables in advance, and physical memory of the specified size will be consumed quickly. This option only takes effect at boot of VM. There is also a `prefault` option in restore and its choice will overwrite `prefault` in memory. By default, this option is turned off. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G,prefault=on ``` ### `reserve` Specifies if the memory for this zone should be `mmap(2)`-ed _without_ the `MAP_NORESERVE` flag, asking the kernel to reserve the backing pages (swap space, or huge pages for hugepage-backed memory) for the whole zone up front at `mmap` time. By default Cloud Hypervisor maps guest memory with `MAP_NORESERVE`, so VM creation succeeds even when the backing pool cannot satisfy the full zone size. The shortfall then surfaces only later, as a `SIGBUS` delivered to the guest when it faults a page the pool cannot back. With `reserve=on` the reservation is made when the memory is mapped, so an over-committed configuration fails cleanly at VM creation with an out-of-memory error instead of crashing the guest at run time. This is most useful for hugepage-backed zones, where the huge pages are reserved from the pool. Unlike `prefault`, this does not populate or fault in the memory, so it does not slow down boot; it only reserves it. By default this option is turned off. _Example_ ``` --memory size=0 --memory-zone id=mem0,size=1G,hugepages=on,reserve=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 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>, distances: Option>, memory_zones: Option>, device_id: Option, } ``` ``` --numa Settings related to a given NUMA node "guest_numa_id=,cpus=,distances=,memory_zones=,device_id=" ``` ### `guest_numa_id` Node identifier of a guest NUMA node. This identifier must be unique, otherwise an error will be returned. This option is mandatory when using the `--numa` parameter. Value is an unsigned integer of 32 bits. _Example_ ``` --numa guest_numa_id=0,cpus=[0-1],memory_zones=mem0 ``` ### `cpus` List of virtual CPUs attached to the guest NUMA node identified by the `guest_numa_id` option. This allows for describing a list of CPUs which must be seen by the guest as belonging to the NUMA node `guest_numa_id`. One can use this option for a fine-grained description of the NUMA topology 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 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 same example could also be described with `cpus=[0,1,2,3,4]`. A combination of both `-` and `,` separators is useful when one might need to describe a list containing all CPUs from 0 to 99 and the CPU 255, as it could 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_ ``` --cpus boot=8 --numa guest_numa_id=0,cpus=[1-3,7] guest_numa_id=1,cpus=[0,4-6] ``` ### `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 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. One or more tuple of two values must be provided through this option. The first value is an unsigned integer of 32 bits as it represents the destination NUMA node. The second value is an unsigned integer of 8 bits as it represents the distance between the current NUMA node and the destination NUMA node. The two values are separated by `@` (`value1@value2`), meaning the destination NUMA node `value1` is located at a distance of `value2`. Each tuple is separated 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` List of memory zones attached to the guest NUMA node identified by the `guest_numa_id` option. This allows for describing a list of memory ranges which must be seen by the guest as belonging to the NUMA node `guest_numa_id`. This option can be very useful and powerful when combined with `host_numa_node` option from `--memory-zone` parameter as it allows for creating a VM with non uniform memory accesses, and let the guest know about it. It allows for exposing memory zones through different NUMA nodes, which can help the guest workload run more efficiently. Multiple values can be provided to define the list. Each value is a string referring to an existing memory zone identifier. Values are separated from each other with the `,` separator. As soon as one tries to describe a list of values, `[` and `]` must be used to demarcate the list. 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_ ``` --memory size=0 --memory-zone id=mem0,size=1G id=mem1,size=1G id=mem2,size=1G --numa guest_numa_id=0,memory_zones=[mem0,mem2] guest_numa_id=1,memory_zones=mem1 ``` ### `device_id` (Generic Initiator) Device identifier for creating a Generic Initiator NUMA node that is {CPU,memory}-less and associated with a specific VFIO-PCI device. 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. 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 ``` 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. 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_ ``` --platform num_pci_segments=2 --memory-zone size=16G,host_numa_node=0,id=mem0 --memory-zone size=16G,host_numa_node=1,id=mem1 --numa guest_numa_id=0,memory_zones=mem0,pci_segments=[0] --numa guest_numa_id=1,memory_zones=mem1,pci_segments=[1] ``` cloud-hypervisor-53.0/docs/mshv.md000066400000000000000000000070141522476750100172210ustar00rootroot00000000000000# Microsoft Hypervisor The Microsoft Hypervisor is a Type 1 hypervisor which runs on x64 and ARM64 architectures. As the foundation of the Hyper-V virtualization stack, it runs millions of Linux and Windows guests in Azure and on-premises deployments. It supports nested virtualization, and security features like AMD's SEV-SNP. It also supports various features in Windows such as [Device guard and confidential guard](https://techcommunity.microsoft.com/t5/iis-support-blog/windows-10-device-guard-and-credential-guard-demystified/ba-p/376419), and [WSL2](https://docs.microsoft.com/en-us/windows/wsl/wsl2-faq) Since 2020, Microsoft has been releasing open-source components to support Linux running as root partition on the Microsoft Hypervisor. 1. Kernel patches to support Linux booting as root partition 2. A Linux kernel driver exposing an IOCTL interface for managing guest partitions, via a device node - /dev/mshv 3. Rust bindings and IOCTL wrappers 4. IGVM related crates ## Components The following components are related to MSHV support with Cloud-Hypervisor: * [igvm-crates](https://github.com/microsoft/igvm) : Parsing IGVM file * [mshv-crates](https://github.com/rust-vmm/mshv) : Rust crates to interact with kernel module (/dev/mshv) * [igvm-tooling](https://github.com/microsoft/igvm-tooling) : Tool to generate IGVM file ## IGVM Independent Guest Virtual Machine (IGVM) file format.The format specification can be found in the igvm_defs crate, with a Rust implementation of the binary format in the igvm crate. The IGVM file format is designed to encapsulate all information required to launch a virtual machine on any given virtualization stack, with support for different isolation technologies such as AMD SEV-SNP and Intel TDX. At a conceptual level, this file format is a set of commands created by the tool that generated the file, used by the loader to construct the initial guest state. The file format also contains measurement information that the underlying platform will use to confirm that the file was loaded correctly and signed by the appropriate authorities. Cloud Hypervisor can be built using the `igvm` feature flag along with `mshv` and/or `sev_snp`. IGVM is used for MSHV guests and for KVM SEV-SNP guests that boot through an IGVM stage0 image. ## SEV-SNP AMD's [Secure Encrypted Virtualization (SEV)](https://www.amd.com/en/developer/sev.html) and extensions such as Secure Nested Paging (SEV-SNP) encrypt memory and restrict access to a guest VM's memory and registers, securing it against a compromised hypervisor or VMM. They utilize the Platform Security Processor (PSP) to store keys and encrypt/decrypt the data. Microsoft has been continuously adding/improving support for SEV-SNP on Microsoft Hyper-V. Cloud Hypervisor's default `sev_snp` feature enables the `mshv` and `igvm` features needed for the MSHV SEV-SNP path. ## Use Cases Cloud Hypervisor can be built to run on an MSHV root partition by enabling the mshv feature, e.g.: ```cargo build --locked --all --all-targets --no-default-features --tests --examples --features mshv``` Cloud Hypervisor on MSHV can boot Linux guests using an IGVM file. IGVM feature depends on mshv for running legacy VMs.e.g.: ```cargo build --locked --all --all-targets --no-default-features --tests --examples --features igvm``` For running confidential VMs on mshv, you will only need to enable sev_snp, it requires and enables mshv and igvm automatically, eg.: ```cargo build --locked --all --all-targets --no-default-features --tests --examples --features sev_snp``` cloud-hypervisor-53.0/docs/performance_metrics.md000066400000000000000000000127461522476750100223030ustar00rootroot00000000000000# Performance Metrics Cloud Hypervisor provides a [performance metrics](https://github.com/cloud-hypervisor/cloud-hypervisor/tree/main/performance-metrics) binary for users to generate metrics data from their own environment. This document describes how to generate metrics data quickly by using Cloud Hypervisor's development script, e.g. `dev_cli.sh`. The only prerequisite is [Docker installation](https://docs.docker.com/engine/install/). Please note that upon its first invocation, this script will pull a fairly large container image. ## Run the Performance Tests To generate metrics data for all available performance tests (including boot time, block I/O throughput, and network throughput & latency) and output the result into a json file: ``` $ ./scripts/dev_cli.sh tests --metrics -- -- --report-file /tmp/metrics.json ``` To get a list of available performance tests: ``` $ ./scripts/dev_cli.sh tests --metrics -- -- --list-tests ``` To generate metrics data for selected performance tests, e.g. boot time only: ``` $ ./scripts/dev_cli.sh tests --metrics -- -- --report-file /tmp/metrics.json --test-filter boot_time ``` To set custom timeout or test iterations for all performance tests: ``` $ ./scripts/dev_cli.sh tests --metrics -- -- --timeout 5 --iterations 10 ``` ## Performance Tests Details The following table lists the supported performance tests with default timeout and number of iterations. The `timeout` defines the maximum execution time of each test for each iteration. The `iteration` defines how many times a test needs to be executed to generate the final metrics data. | **Type** | **Metric** | **Timeout(s)** | **Iterations** | |------------|--------------------------------------------|----------------|----------------| | Boot Time | boot_time_ms | 2 | 10 | | | boot_time_pmem_ms | 2 | 10 | | | boot_time_16_vcpus_ms | 2 | 10 | | | boot_time_16_vcpus_pmem_ms | 2 | 10 | | Virtio Net | virtio_net_latency_us | 10 | 5 | | | virtio_net_throughput_single_queue_rx_gbps | 10 | 5 | | | virtio_net_throughput_single_queue_tx_gbps | 10 | 5 | | | virtio_net_throughput_multi_queue_rx_gbps | 10 | 5 | | | virtio_net_throughput_multi_queue_tx_gbps | 10 | 5 | | Block | block_read_MiBps | 10 | 5 | | | block_write_MiBps | 10 | 5 | | | block_random_read_MiBps | 10 | 5 | | | block_random_write_MiBps | 10 | 5 | | | block_multi_queue_read_MiBps | 10 | 5 | | | block_multi_queue_write_MiBps | 10 | 5 | | | block_multi_queue_random_read_MiBps | 10 | 5 | | | block_multi_queue_random_write_MiBps | 10 | 5 | | | block_read_IOPS | 10 | 5 | | | block_write_IOPS | 10 | 5 | | | block_random_read_IOPS | 10 | 5 | | | block_random_write_IOPS | 10 | 5 | | | block_multi_queue_read_IOPS | 10 | 5 | | | block_multi_queue_write_IOPS | 10 | 5 | | | block_multi_queue_random_read_IOPS | 10 | 5 | | | block_multi_queue_random_write_IOPS | 10 | 5 | | Other | restore_latency_time_ms | 2 | 10 | ## Output Format Performance-metrics output the result into a json file if `report-file` param is set. The fields included in JSON include: | Field Name | Content | |--------------------|------------------------------------------| | git_human_readable | Recent tag information of git repository | | git_revision | Commit id of HEAD | | git_commit_date | Commit date of HEAD | | date | Date for executing the program | | results | A list of metrics | ## Example Here is an example of generating metrics data for the boot time using `pmem`: ```bash $ ./scripts/dev_cli.sh tests --metrics -- -- --test-filter boot_time_pmem_ms ``` Here is a sample output: ```json { "git_human_readable": "v40.0", "git_revision": "e9b263975786abbf895469b93dfc00f21ce39a88", "git_commit_date": "Fri Jun 21 08:40:44 2024 +0000", "date": "Tue Jul 16 16:35:29 UTC 2024", "results": [ { "name": "boot_time_pmem_ms", "mean": 105.9461, "std_dev": 7.140993312558129, "max": 120.01499999999999, "min": 92.37600000000002 } ] } ``` Note that the metrics data above is for illustration purpose only and does not represent the actual performance of Cloud Hypervisor on your system. cloud-hypervisor-53.0/docs/profiling.md000066400000000000000000000034661522476750100202440ustar00rootroot00000000000000# Profiling `perf` can be used to profile the `cloud-hypervisor` binary but it is necessary to make some modifications to the build in order to produce a binary that gives useful results. ## Building a suitable binary This adds the symbol information to the release binary but does not otherwise affect the performance. The binary must also be built with frame pointers included so that the call graph can be captured by the profiler. ``` $ cargo clean && RUSTFLAGS='-C force-frame-pointers=y' cargo build --profile profiling ``` ## Profiling `perf` may then be used in the usual manner: e.g. ``` $ perf record -g target/profiling/cloud-hypervisor \ --kernel ~/src/linux/vmlinux \ --pmem file=~/workloads/focal.raw \ --cpus boot=1 --memory size=1G \ --cmdline "root=/dev/pmem0p1 console=ttyS0" \ --serial tty --console off \ --api-socket=/tmp/api1 ``` For analysing the samples: ``` $ perf report -g ``` If profiling with a network device attached either the TAP device must be already created and configured or the profiling must be done as root so that the TAP device can be created. ## Userspace only profiling with LBR The use of LBR (Last Branch Record; available since Haswell) offers lower overhead if only userspace profiling is required. This lower overhead can allow a higher frequency of sampling. This also removes the requirement to compile with custom `RUSTFLAGS` however debug symbols should still be included: e.g. ``` $ perf record --call-graph lbr --all-user --user-callchains -g target/release/cloud-hypervisor \ --kernel ~/src/linux/vmlinux \ --pmem file=~/workloads/focal.raw \ --cpus boot=1 --memory size=1G \ --cmdline "root=/dev/pmem0p1 console=ttyS0" \ --serial tty --console off \ --api-socket=/tmp/api1 ``` cloud-hypervisor-53.0/docs/releases.md000066400000000000000000000047031522476750100200510ustar00rootroot00000000000000# Release Documentation ## Abstract This document provides guidance to users, downstream maintainers and any other consumers of the Cloud Hypervisor project, this document describes the release process, release cadence, stability expectations and related topics. ## Basic Terms ### Stability For Cloud Hypervisor the following areas are subject to stability guarantees: - [REST API](api.md#rest-api) - [Command line options](api.md#command-line-interface) - [Device Model](device_model.md) - Device tree, device list, ACPI, Hyper-V enlightenments and any other features exposed to guest - KVM compatibility - Rust edition compatibility This list is incomplete but this document serves as a best effort guide to stability across releases. ### Experimental features Experimental features are under active development and no guarantees are made about their stability. List of experimental features: - TDX - vfio-user - vDPA ### Security Security fixes should be included in a new point release. For security issues an advisory will be published via the GitHub security advisory process along with the release. Watching the project on GitHub will notify you of those issues. ## Releases ### Versioning The versioning scheme uses `MAJOR.POINT` pattern: - `MAJOR` can introduce incompatible changes along with support for new features. Changes to the [API](api.md#rest-api), [CLI options](api.md#command-line-interface) and [device model](device_model.md) require a notice at least 2 releases in advance for the actual change to take place. - `POINT` contains bug fixes and/or security fixes. ### Major Release Cadence Cloud Hypervisor is under active development. A new major release is issued approximately every 6 weeks. Point releases are issued on demand, when important bug fixes are in the queue. A major release would receive bug fixes for the next two cycles (~12 weeks) and then be considered EOL. ``` + - Active release support E - EOL 2021 2022 2023 | | | | | | | | | 18.0 | | | ++++++++E 19.0 | | | |++++++++E 20.0 | | | | ++++++++E 21.0 | | | | | ++++++++E 22.0 | | | | | +++++++++E 23.0 | | | | | | +++++++++E ``` ### Major Release Stability Considerations Snapshot/restore support is not compatible across `MAJOR` versions. Live migration support is not compatible across `MAJOR` versions. cloud-hypervisor-53.0/docs/riscv.md000066400000000000000000000103521522476750100173710ustar00rootroot00000000000000# How to build and test Cloud Hypervisor on riscv64 This document introduces how to build and test Cloud Hypervisor on `riscv64`. All instructions here are tested with Ubuntu 24.04.2 as the host OS. ## Hardware requirements - riscv64 servers (recommended) or development boards equipped with the AIA (Advance Interrupt Architecture) interrupt controller. ## Getting started We create a folder to build and run Cloud Hypervisor at `$HOME/cloud-hypervisor` ```console export CLOUDH=$HOME/cloud-hypervisor mkdir $CLOUDH ``` ## Prerequisites You need to install some prerequisite packages to build and test Cloud Hypervisor. ### Tools ```console # Install rust tool chain curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install the tools used for building guest kernel, EDK2 and converting guest disk sudo apt-get update sudo apt-get install git build-essential m4 bison flex uuid-dev qemu-utils ``` ### Building Cloud Hypervisor ```console pushd $CLOUDH git clone https://github.com/cloud-hypervisor/cloud-hypervisor.git cd cloud-hypervisor cargo build popd ``` ### Disk image Download the Ubuntu cloud image and convert the image type. ```console pushd $CLOUDH wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-riscv64.img qemu-img convert -p -f qcow2 -O raw jammy-server-cloudimg-riscv64.img jammy-server-cloudimg-riscv64.raw popd ``` ## Direct-kernel booting ### Building kernel ```console pushd $CLOUDH git clone --depth 1 "https://github.com/cloud-hypervisor/linux.git" -b ch-6.12.8 cd linux make ch_defconfig make -j `nproc` popd ``` ### Booting the guest VM ```console pushd $CLOUDH sudo $CLOUDH/cloud-hypervisor/target/debug/cloud-hypervisor \ --kernel $CLOUDH/linux/arch/riscv64/boot/Image \ --disk path=jammy-server-cloudimg-riscv64.raw \ --cmdline "console=hvc0 root=/dev/vda rw" \ --cpus boot=1 \ --memory size=1024M \ --seccomp false \ --log-file boot.log -vv popd ``` ## Virtualized Development Setup Since there are few RISC-V development boards on the market and not many details about the AIA interrupt controller featured in product listings, QEMU is a popular and viable choice for creating a RISC-V development environment. Below are the steps used to create a QEMU virtual machine that can be used for cloud-hypervisor RISC-V development: ### Install Dependencies ```console sudo apt update sudo apt install opensbi qemu-system-misc u-boot-qemu ``` ### Download and Build QEMU (>=v9.2.0) Older versions of QEMU may not have support for the AIA interrupt controller. ```console wget https://download.qemu.org/qemu-10.0.0.tar.xz tar xvJf qemu-10.0.0.tar.xz cd qemu-10.0.0 ./configure --target-list=riscv64-softmmu make -j $(nproc) sudo make install ``` ### Download Ubuntu Server Image At the time of writing, the best results have been seen with the Ubuntu 24.10 (Oracular) server image. Ex: ```console wget https://cdimage.ubuntu.com/releases/oracular/release/ubuntu-24.10-preinstalled-server-riscv64.img.xz xz -dk ubuntu-24.10-preinstalled-server-riscv64.img.xz ``` ### (Optional) Resize Disk If you would like a larger disk, you can resize it now. ```console qemu-img resize -f raw +5G ``` ### Boot VM Note the inclusion of the AIA interrupt controller in the invocation. ```console qemu-system-riscv64 \ -machine virt,aia=aplic-imsic \ -nographic -m 1G -smp 8 \ -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \ -device virtio-rng-pci \ -device virtio-net-device,netdev=eth0 -netdev user,id=eth0 \ -drive file=,format=raw,if=virtio ``` ### Install KVM Kernel Module Within VM KVM is not enabled within the VM by default, so we must enable it manually. ```console sudo modprobe kvm ``` From this point, you can continue with the above steps from the beginning. ### Sources https://risc-v-getting-started-guide.readthedocs.io/en/latest/linux-qemu.html https://canonical-ubuntu-boards.readthedocs-hosted.com/en/latest/how-to/qemu-riscv/#using-the-live-server-image https://www.qemu.org/docs/master/specs/riscv-aia.html ## Known limitations - Direct kernel boot only - `64-bit Linux` guest OS only - For more details, see [here](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/6978). cloud-hypervisor-53.0/docs/seccomp.md000066400000000000000000000056751522476750100177100ustar00rootroot00000000000000# Seccomp filtering As a means to harden Cloud Hypervisor's security, the project leverages seccomp filtering. ## What is seccomp filtering A seccomp filter is a way for a process to tell the kernel which system calls are authorized. In case this process calls into a prohibited system call, the kernel will kill the process right away. ## How does it apply to Cloud Hypervisor Cloud Hypervisor is a multi threaded application. It spawns dedicated threads for virtual CPUs, virtio devices and HTTP server, along with the main thread representing the VMM. Each of these threads has a limited scope of what it is expected to perform, which is why different filters are applied to each of them. By default, Cloud Hypervisor enables seccomp filtering as the project believes that security should not be an option. For development and debugging purposes, one might want to disable this feature or log the faulty system call. ### Disabling seccomp filters Append `--seccomp false` to Cloud Hypervisor's command line to prevent seccomp filtering from being applied. ### Logging prohibited system calls In the context of debug, one alternative to disabling seccomp filtering is to log faulty system calls that would have caused the application to be killed by the kernel. Append `--seccomp log` to Cloud Hypervisor's command line to enable faulty system calls to be logged. The kernel running on the host machine must have the `audit` parameter enabled. If this is not the case, update kernel boot options by appending `audit=1`. Unauthorized system calls will be logged to the journal similarly to the following example ``` type=SECCOMP msg=audit(1423263412.694:7878): auid=1000 uid=1000 gid=1000 ses=3 subj=unconfined_u:unconfined_r:cloud_hypervisor:s0-s0:c0.c1023 pid=1193 comm="cloud-hypervisor" exe="/usr/bin/cloud-hypervisor" sig=0 arch=c000003e syscall=47 compat=0 ip=0x7f4f63982604 code=0x50000 ``` Provided `ausyscall` has been installed on the host, the system call can be identified with ``` $ ausyscall 47 recvmsg ``` ### Returning EPERM instead of killing the VMM Append `--seccomp errno` to make prohibited system calls return `EPERM` to the caller instead of terminating the process. This lets the VMM keep running so a regression caused by an unknown syscall can be observed without an immediate crash, at the cost of turning the violation into a soft failure visible only as a syscall error. By default the kernel does not log `errno` actions. To audit them, ensure `errno` is included in the host's `kernel.seccomp.actions_logged` sysctl, for example: ``` # sysctl -w kernel.seccomp.actions_logged='kill_process kill_thread trap errno log' ``` ### Further debug with `strace` One more way of debugging seccomp related issues is to use the `strace` tool as it will log every system call issued by the process. It is important to use `-f` option in order to trace each and every thread belonging to the process. ``` strace --decode-pids=comm -f ./cloud-hypervisor ... ``` cloud-hypervisor-53.0/docs/snapshot_restore.md000066400000000000000000000245201522476750100216470ustar00rootroot00000000000000# Snapshot and Restore The goal for the snapshot/restore feature is to provide the user with the ability to take a snapshot of a previously paused virtual machine. This snapshot can be used as the base for creating new identical virtual machines, without the need to boot them from scratch. The restore codepath takes the snapshot and creates the exact same virtual machine, restoring the previously saved states. The new virtual machine is restored in a paused state, as it was before the snapshot was performed. ## Snapshot a Cloud Hypervisor VM First thing, we must run a Cloud Hypervisor VM: ```bash ./cloud-hypervisor \ --api-socket /tmp/cloud-hypervisor.sock \ --cpus boot=4 \ --memory size=4G \ --kernel vmlinux \ --cmdline "root=/dev/vda1 console=hvc0 rw" \ --disk path=focal-server-cloudimg-amd64.raw ``` At any point in time when the VM is running, one might choose to pause it: ```bash ./ch-remote --api-socket=/tmp/cloud-hypervisor.sock pause ``` Once paused, the VM can be safely snapshot into the specified directory and using the following command: ```bash ./ch-remote --api-socket=/tmp/cloud-hypervisor.sock snapshot file:///home/foo/snapshot ``` Given the directory was present on the system, the snapshot will succeed and it should contain the following files: ```bash ll /home/foo/snapshot/ total 4194536 drwxrwxr-x 2 foo bar 4096 Jul 22 11:50 ./ drwxr-xr-x 47 foo bar 4096 Jul 22 11:47 ../ -rw------- 1 foo bar 1084 Jul 22 11:19 config.json -rw------- 1 foo bar 4294967296 Jul 22 11:19 memory-ranges -rw------- 1 foo bar 217853 Jul 22 11:19 state.json ``` `config.json` contains the virtual machine configuration. It is used to create a similar virtual machine with the correct amount of CPUs, RAM, and other expected devices. It is stored in a human readable format so that it could be modified between the snapshot and restore phases to achieve some very special use cases. But for most cases, manually modifying the configuration should not be needed. `memory-ranges` stores the content of the guest RAM. `state.json` contains the virtual machine state. It is used to restore each component in the state it was left before the snapshot occurred. ## Restore a Cloud Hypervisor VM Given that one has access to an existing snapshot in `/home/foo/snapshot`, it is possible to create a new VM based on this snapshot with the following command: ```bash ./cloud-hypervisor \ --api-socket /tmp/cloud-hypervisor.sock \ --restore source_url=file:///home/foo/snapshot ``` Or using two different commands from two terminals: ```bash # First terminal ./cloud-hypervisor --api-socket /tmp/cloud-hypervisor.sock # Second terminal ./ch-remote --api-socket=/tmp/cloud-hypervisor.sock restore source_url=file:///home/foo/snapshot ``` Remember the VM is restored in a `paused` state, which was the VM's state when it was snapshot. For this reason, one must explicitly `resume` the VM before to start using it. ```bash ./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. See [Network Announcements After Resume](live_migration.md#network-announcements-after-resume) for the announcement behavior after restore/resume. 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: ```bash # First terminal ./cloud-hypervisor --api-socket /tmp/cloud-hypervisor.sock # Second terminal ./ch-remote --api-socket=/tmp/cloud-hypervisor.sock restore source_url=file:///home/foo/snapshot net_fds=[net1@[23,24],net2@[25,26]] ``` In the example above, the net device with id `net1` will be backed by FDs '23' and '24', and the net device with id `net2` will be backed by FDs '25' and '26' from the restored VM. ## VFIO devices Snapshot and restore are supported for VFIO devices that implement the kernel VFIO migration v2 protocol (e.g. Mellanox NICs bound to the `mlx5_vfio_pci` driver). See [`vfio.md`](vfio.md) for details on requirements and behavior. ## Offload Snapshot and Restore Cloud Hypervisor can hand the snapshot payload off to a user-provided offload daemon instead of writing files to a `file://` directory. The daemon can transform the payload on the fly (encrypt, compress, stream to object storage, etc.) without ever touching local disk. There is no dedicated API surface for offload: the daemon talks to CH over the existing local live-migration protocol, playing the migration peer role: - On snapshot, CH acts as the migration sender and the daemon acts as the receiver. The source VM shuts down on success, exactly as it would for a local live migration. Memory is transferred via `SCM_RIGHTS`, CH handing off the daemon one memfd per guest-memory slot. - On restore, CH acts as the migration receiver and the daemon acts as the sender. The daemon provides one memfd per slot, populated from its storage, and CH uses those memfds directly as guest RAM backing. In practice, this means offload is driven through the existing `vm.send-migration` / `vm.receive-migration` endpoints (with `local=on` and a `unix:` URL). The daemon is just another peer of these endpoints. This requires the VM to be configured with shared-memory backing, which is the same precondition that applies to local live migration today. ### Snapshot offload usage ```bash # 1. Run a VM with shared memory. ./cloud-hypervisor \ --api-socket /tmp/cloud-hypervisor.sock \ --cpus boot=2 \ --memory size=1G,shared=on \ --kernel vmlinux \ --cmdline "root=/dev/vda1 console=hvc0 rw" \ --disk path=focal-server-cloudimg-amd64.raw # 2. Start your offload daemon. The reference implementation is shipped as # `offload_daemon` and persists snapshot data to a local directory. ./offload_daemon snapshot \ --socket /tmp/offload.sock \ --output-dir /var/snapshots/vm1 # 3. Issue a local live migration to the daemon's socket. CH connects to # /tmp/offload.sock, streams the snapshot, and exits on success. ./ch-remote --api-socket /tmp/cloud-hypervisor.sock pause ./ch-remote --api-socket /tmp/cloud-hypervisor.sock \ send-migration destination_url=unix:/tmp/offload.sock,local=on ``` ### Restore offload usage ```bash # 1. Start a CH process. ./cloud-hypervisor --api-socket /tmp/cloud-hypervisor.sock # 2. Tell CH to listen for an inbound migration from the offload daemon. ./ch-remote --api-socket /tmp/cloud-hypervisor.sock \ receive-migration receiver_url=unix:/tmp/restore.sock & # 3. Start the daemon in restore mode pointing at the same saved snapshot. # With --resume, the restored VM starts running on completion; # without it, the VM is left paused (issue `resume` to start it). ./offload_daemon restore \ --socket /tmp/restore.sock \ --input-dir /var/snapshots/vm1 \ --resume ``` ### On demand restore usage For speeding up a VM restore, the daemon's `--ondemand` mode hands CH empty memfds and serves page contents on demand via userfaultfd. This requires `memory_mode=postcopy` on the receive-migration call so CH registers userfaultfd on the memfds before resuming vCPUs and keeps the daemon's socket open for `PageFault` requests: ```bash ./ch-remote --api-socket /tmp/cloud-hypervisor.sock \ receive-migration receiver_url=unix:/tmp/restore.sock,memory_mode=postcopy & ./offload_daemon restore \ --socket /tmp/restore.sock \ --input-dir /var/snapshots/vm1 \ --resume --ondemand ``` ### The daemon protocol The daemon implements the local live-migration wire protocol defined in `vm-migration/src/protocol.rs`. Two state machines are involved: - Snapshot mode (migration receiver): walk `Start → MemoryFd (×N) → Config → State → CompletePaused`. For each `MemoryFd` command, receive a guest-memory fd via SCM_RIGHTS on the same UNIX socket. - Restore mode (migration sender): walk the same sequence in reverse, emitting one `MemoryFd` per slot (with the memfd attached via SCM_RIGHTS) before sending `Config` and `State`. Finish with either `CompletePaused` (restored VM remains paused) or `Complete` (restored VM resumes). ### Critical invariant on snapshot On the snapshot path, the daemon must finish reading from every memory fd before it ACKs `CompletePaused`. Cloud Hypervisor blocks at the `CompletePaused` handshake until the daemon ACKs. Once it ACKs, the source VM shuts down and the daemon's fds are the only remaining record of guest RAM. The reference daemon dumps each slot to disk and `fsync`s before ACKing. ### Reference daemon The in-tree `offload_daemon` binary is intentionally minimal: it just serialises the snapshot to a local directory and replays it back. Its purpose is to back the offload integration test and to serve as a working example for daemon authors. Use it as a template, not a production backend. ### Limitations - The VM must use shared-memory backing (`shared=on` or file-backed). Anonymous memory is rejected with the same error message that local live migration produces. - Orchestrator-supplied network FDs (today carried by `vm.restore`'s `net_fds` field) are not plumbed through `vm.receive-migration`, so VMs whose configuration relies on externally-provided net FDs cannot currently be restored via the offload path. - Confidential VMs (CVMs) inherit the live-migration restriction: offload is not supported for CVMs. cloud-hypervisor-53.0/docs/testing.md000066400000000000000000000357251522476750100177330ustar00rootroot00000000000000# 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) ## Overview Cloud Hypervisor's development script runs builds and tests 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` | auto | Hypervisor backend (`kvm`/`mshv`). Auto-detected from the host device node when omitted. | | `--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#...] \ [--