diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6442e420d..6006d0ce5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ ## Dockerfile for devcontainer -FROM mcr.microsoft.com/devcontainers/base:debian AS base +FROM mcr.microsoft.com/devcontainers/base:bookworm AS base ARG USER=vscode ARG GROUP=vscode @@ -22,18 +22,15 @@ RUN apt-get update \ make \ software-properties-common \ sudo \ - wget + wget \ + musl-tools -ARG LLVM_VERSION=17 +ARG LLVM_VERSION=18 # Install llvm RUN wget https://apt.llvm.org/llvm.sh \ && chmod +x ./llvm.sh \ - && sudo ./llvm.sh ${LLVM_VERSION} all \ - && sudo ln -s /usr/lib/llvm-${LLVM_VERSION}/bin/clang-cl /usr/bin/clang-cl \ - && sudo ln -s /usr/lib/llvm-${LLVM_VERSION}/bin/llvm-lib /usr/bin/llvm-lib \ - && sudo ln -s /usr/lib/llvm-${LLVM_VERSION}/bin/lld-link /usr/bin/lld-link \ - && sudo ln -s /usr/lib/llvm-${LLVM_VERSION}/bin/llvm-ml /usr/bin/llvm-ml \ + && sudo ./llvm.sh ${LLVM_VERSION} clang clang-tools-extra \ && sudo ln -s /usr/lib/llvm-${LLVM_VERSION}/bin/ld.lld /usr/bin/ld.lld \ && sudo ln -s /usr/lib/llvm-${LLVM_VERSION}/bin/clang /usr/bin/clang @@ -52,13 +49,14 @@ RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhisto USER $USER -ARG RUST_TOOLCHAIN=1.85 +ARG RUST_TOOLCHAIN=1.89 # Install rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ && rustup default ${RUST_TOOLCHAIN} \ && rustup target add x86_64-unknown-linux-gnu \ + && rustup target add x86_64-unknown-linux-musl \ && rustup target add x86_64-unknown-none \ && rustup toolchain add nightly-x86_64-unknown-linux-gnu \ - && cargo install just - + && cargo install just \ + && cargo install cross --locked --version 0.2.5 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8dd50eea7..51e8bb207 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,3 +21,30 @@ updates: - dependency-name: "mshv-bindings" versions: [ ">=0.2.1" ] open-pull-requests-limit: 20 + - package-ecosystem: "cargo" + directory: "/src/tests/rust_guests/dummyguest" + schedule: + interval: "daily" + time: "03:00" + labels: + - "kind/dependencies" + - "area/guest" + open-pull-requests-limit: 5 + - package-ecosystem: "cargo" + directory: "/src/tests/rust_guests/simpleguest" + schedule: + interval: "daily" + time: "03:00" + labels: + - "kind/dependencies" + - "area/guest" + open-pull-requests-limit: 5 + - package-ecosystem: "cargo" + directory: "/src/tests/rust_guests/witguest" + schedule: + interval: "daily" + time: "03:00" + labels: + - "kind/dependencies" + - "area/guest" + open-pull-requests-limit: 5 diff --git a/.github/workflows/Benchmarks.yml b/.github/workflows/Benchmarks.yml index 64df29bb2..25b62de49 100644 --- a/.github/workflows/Benchmarks.yml +++ b/.github/workflows/Benchmarks.yml @@ -14,37 +14,46 @@ jobs: # its execution. this dependency should be expressed in the dependent # workflow benchmark: + timeout-minutes: 60 strategy: fail-fast: true matrix: - hypervisor: [hyperv, mshv, mshv3, kvm] # hyperv is windows, mshv and kvm are linux + hypervisor: [hyperv, 'hyperv-ws2025', mshv, mshv3, kvm] # hyperv,hyperv-ws2025 are windows, mshv, mshv3 and kvm are linux cpu: [amd, intel] - config: [release] # don't want to benchmark debug-builds - - runs-on: ${{ fromJson(format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}"]', matrix.hypervisor == 'hyperv' && 'Windows' || 'Linux', matrix.hypervisor == 'hyperv' && 'win2022' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor, matrix.cpu)) }} - + config: [release] + runs-on: ${{ fromJson( + format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}"]', + (matrix.hypervisor == 'hyperv' || matrix.hypervisor == 'hyperv-ws2025') && 'Windows' || 'Linux', + matrix.hypervisor == 'hyperv' && 'win2022' || matrix.hypervisor == 'hyperv-ws2025' && 'win2025' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor, + matrix.cpu)) }} steps: ### Setup ### - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Download Guest Binaries - uses: actions/download-artifact@v4 + - name: Download Rust Guest Binaries + uses: actions/download-artifact@v5 + with: + name: rust-guest-binaries-release + path: ./downloaded-rust-guest-binaries-release + + - name: Download C Guest Binaries + uses: actions/download-artifact@v5 with: - name: guest-binaries-release - path: ./downloaded-guest-binaries-release + name: c-guest-binaries-release + path: ./downloaded-c-guest-binaries-release - name: Copy Guest Binaries run: | - cp ./downloaded-guest-binaries-release/callbackguest ./src/tests/rust_guests/bin/release/callbackguest - cp ./downloaded-guest-binaries-release/simpleguest ./src/tests/rust_guests/bin/release/simpleguest - cp ./downloaded-guest-binaries-release/dummyguest ./src/tests/rust_guests/bin/release/dummyguest + cp ./downloaded-rust-guest-binaries-release/simpleguest ./src/tests/rust_guests/bin/release/simpleguest + cp ./downloaded-rust-guest-binaries-release/dummyguest ./src/tests/rust_guests/bin/release/dummyguest + cp ./downloaded-c-guest-binaries-release/simpleguest ./src/tests/c_guests/bin/release/simpleguest ### Benchmarks ### - name: Fetch tags @@ -57,7 +66,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run Benchmarks - run: just bench-ci main release ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}} + run: just bench-ci main ${{ matrix.hypervisor == 'mshv' && 'mshv2' || ''}} - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/CargoAudit.yml b/.github/workflows/CargoAudit.yml index 00cf41515..b38b7507d 100644 --- a/.github/workflows/CargoAudit.yml +++ b/.github/workflows/CargoAudit.yml @@ -13,14 +13,14 @@ jobs: audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # We are not using the common workflow here because it installs a bunch of tools we don't need. # TODO: Once the runner image is updated to include the necessary tools (without downloading), we can switch to the common workflow. - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.85" - + toolchain: "1.89" + - uses: rustsec/audit-check@v2.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/CargoPublish.yml b/.github/workflows/CargoPublish.yml index 5d80a0523..ac24250b4 100644 --- a/.github/workflows/CargoPublish.yml +++ b/.github/workflows/CargoPublish.yml @@ -27,15 +27,14 @@ jobs: if: ${{ startsWith(github.ref, 'refs/heads/release/v') || inputs.dry_run }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" - + rust-toolchain: "1.89" - name: Check crate versions shell: bash run: | @@ -46,7 +45,7 @@ jobs: VERSION="${VERSION#refs/heads/release/v}" echo "VERSION=$VERSION" >> $GITHUB_ENV fi - ./dev/verify-version.sh "$VERSION" hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro + ./dev/verify-version.sh "$VERSION" hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-guest-tracing - name: Determine which crates need publishing run: | @@ -75,6 +74,7 @@ jobs: needs_publish hyperlight-component-util needs_publish hyperlight-component-macro needs_publish hyperlight-host + needs_publish hyperlight-guest-tracing - name: Publish hyperlight-common continue-on-error: ${{ inputs.dry_run }} @@ -82,6 +82,13 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_PUBLISH_TOKEN }} if: env.PUBLISH_HYPERLIGHT_COMMON != 'false' + + - name: Publish hyperlight-guest-tracing + continue-on-error: ${{ inputs.dry_run }} + run: cargo publish --manifest-path ./src/hyperlight_guest_tracing/Cargo.toml ${{ inputs.dry_run && '--dry-run' || '' }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_PUBLISH_TOKEN }} + if: env.PUBLISH_HYPERLIGHT_GUEST_TRACING != 'false' - name: Publish hyperlight-guest continue-on-error: ${{ inputs.dry_run }} diff --git a/.github/workflows/CleanUpReleasesAndPackages.yml b/.github/workflows/CleanUpReleasesAndPackages.yml index 64bce8650..8d2404f4c 100644 --- a/.github/workflows/CleanUpReleasesAndPackages.yml +++ b/.github/workflows/CleanUpReleasesAndPackages.yml @@ -28,7 +28,7 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Delete old Github Actions Artifacts env: diff --git a/.github/workflows/CreateDevcontainerImage.yml b/.github/workflows/CreateDevcontainerImage.yml index 81e0a9cb5..6716ef233 100644 --- a/.github/workflows/CreateDevcontainerImage.yml +++ b/.github/workflows/CreateDevcontainerImage.yml @@ -15,8 +15,8 @@ env: IMAGE_NAME: ${{ github.repository }}-devcontainer USER: vscode GROUP: vscode - LLVM_VERSION: 17 - RUST_TOOLCHAIN_DEFAULT: 1.85 + LLVM_VERSION: 18 + RUST_TOOLCHAIN_DEFAULT: 1.89 RUST_TOOLCHAIN_FILE: rust-toolchain.toml # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Read Rust toolchain version from ${{ env.RUST_TOOLCHAIN_FILE }} id: toolchain diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml index 02a3796fe..64791b3f5 100644 --- a/.github/workflows/CreateRelease.yml +++ b/.github/workflows/CreateRelease.yml @@ -10,20 +10,30 @@ on: permissions: contents: write id-token: write + issues: read jobs: + release-blocker-check: + # see https://github.com/orgs/community/discussions/26286#discussioncomment-3251208 for why we need to check the ref + if: ${{ contains(github.ref, 'refs/heads/release/') }} || ${{ github.ref=='refs/heads/main' }} + uses: ./.github/workflows/ReleaseBlockerCheck.yml + with: + repository: ${{ github.repository }} + secrets: inherit + build-rust-ubuntu: # see https://github.com/orgs/community/discussions/26286#discussioncomment-3251208 for why we need to check the ref if: ${{ contains(github.ref, 'refs/heads/release/') }} || ${{ github.ref=='refs/heads/main' }} runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] + needs: [release-blocker-check] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -37,13 +47,14 @@ jobs: # see https://github.com/orgs/community/discussions/26286#discussioncomment-3251208 for why we need to check the ref if: ${{ contains(github.ref, 'refs/heads/release/') }} || ${{ github.ref=='refs/heads/main' }} runs-on: windows-2022 + needs: [release-blocker-check] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -56,6 +67,7 @@ jobs: build-guest-binaries: uses: ./.github/workflows/dep_build_guest_binaries.yml secrets: inherit + needs: [release-blocker-check] benchmarks: needs: [build-guest-binaries] @@ -95,14 +107,14 @@ jobs: if: ${{ contains(github.ref, 'refs/heads/release/') }} run: echo "CONFIG=release" >> $GITHUB_ENV - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -122,7 +134,7 @@ jobs: just tar-static-lib - name: Download all benchmarks - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: benchmarks_* diff --git a/.github/workflows/CreateReleaseBranch.yml b/.github/workflows/CreateReleaseBranch.yml index 500efc637..275f90928 100644 --- a/.github/workflows/CreateReleaseBranch.yml +++ b/.github/workflows/CreateReleaseBranch.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Create Release Branch run: | diff --git a/.github/workflows/Fuzzing.yml b/.github/workflows/Fuzzing.yml index 8bf437e8b..f1b96e0bb 100644 --- a/.github/workflows/Fuzzing.yml +++ b/.github/workflows/Fuzzing.yml @@ -15,4 +15,19 @@ jobs: with: targets: '["fuzz_host_print", "fuzz_guest_call", "fuzz_host_call"]' # Pass as a JSON array max_total_time: 18000 # 5 hours in seconds - secrets: inherit \ No newline at end of file + secrets: inherit + + notify-failure: + runs-on: ubuntu-latest + needs: fuzzing + if: always() && needs.fuzzing.result == 'failure' + permissions: + issues: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Notify Fuzzing Failure + run: ./dev/notify-ci-failure.sh --labels="area/fuzzing,area/testing,lifecycle/needs-review,release-blocker" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/IssueLabelChecker.yml b/.github/workflows/IssueLabelChecker.yml index 53ed1f33f..4d1d0fcc4 100644 --- a/.github/workflows/IssueLabelChecker.yml +++ b/.github/workflows/IssueLabelChecker.yml @@ -11,7 +11,7 @@ jobs: labeler: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check and Add label run: | # The cryptic head -c -1 is because otherwise gh always terminates output with a newline diff --git a/.github/workflows/PRLabelChecker.yml b/.github/workflows/PRLabelChecker.yml index e824aa5a8..d5186324b 100644 --- a/.github/workflows/PRLabelChecker.yml +++ b/.github/workflows/PRLabelChecker.yml @@ -11,7 +11,7 @@ jobs: check-labels: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Ensure exactly one "kind/*" label is applied run: | # Count the number of "kind/*" labels directly from the PR labels diff --git a/.github/workflows/ReleaseBlockerCheck.yml b/.github/workflows/ReleaseBlockerCheck.yml new file mode 100644 index 000000000..d6857bc74 --- /dev/null +++ b/.github/workflows/ReleaseBlockerCheck.yml @@ -0,0 +1,44 @@ +name: Release Blocker Check + +on: + workflow_call: + inputs: + repository: + description: "Repository to check in format 'owner/repo'" + required: false + type: string + default: ${{ github.repository }} + workflow_dispatch: + inputs: + repository: + description: "Repository to check in format 'owner/repo'" + required: false + type: string + +permissions: + issues: read + contents: read + +jobs: + check-blockers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Check for Release Blocking Issues + run: | + REPO="${{ inputs.repository || github.repository }}" + echo "Checking repository: $REPO" + + # Only check for release blockers on release branches + if [[ "${{ github.ref }}" == *"refs/heads/release/"* ]]; then + echo "Release branch detected - checking for release blockers..." + if ! ./dev/check-release-blockers.sh "$REPO"; then + echo "::error::Release blocked by open issues with 'release-blocker' label" + exit 1 + fi + else + echo "Non-release branch - skipping release blocker check" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ReleaseBlockerLabelCleanUp.yml b/.github/workflows/ReleaseBlockerLabelCleanUp.yml new file mode 100644 index 000000000..7aa93e8a5 --- /dev/null +++ b/.github/workflows/ReleaseBlockerLabelCleanUp.yml @@ -0,0 +1,33 @@ +name: Release Blocker Cleanup + +on: + issues: + types: [closed] + +permissions: + issues: write + contents: read + +jobs: + remove-release-blocker: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Remove release-blocker label from closed issue + run: | + ISSUE_NUMBER=${{ github.event.issue.number }} + echo "Checking if issue #$ISSUE_NUMBER has release-blocker label..." + + # Check if the issue has the release-blocker label + HAS_LABEL=$(gh issue view "$ISSUE_NUMBER" --json labels -q '.labels[] | select(.name == "release-blocker") | .name') + + if [ -n "$HAS_LABEL" ]; then + echo "✅ Issue #$ISSUE_NUMBER has release-blocker label, removing it..." + gh issue edit "$ISSUE_NUMBER" --remove-label "release-blocker" + echo "✅ Successfully removed release-blocker label from issue #$ISSUE_NUMBER" + else + echo "â„šī¸ Issue #$ISSUE_NUMBER does not have release-blocker label, no action needed" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/RustNightly.yml b/.github/workflows/RustNightly.yml new file mode 100644 index 000000000..ff5b1880e --- /dev/null +++ b/.github/workflows/RustNightly.yml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Nightly + +on: + workflow_dispatch: + schedule: + # 12:00 AM, every 2 days + - cron: '0 0 */2 * *' + +permissions: + id-token: write + contents: read + +jobs: + musl: + strategy: + fail-fast: true + matrix: + hypervisor: [kvm, mshv3] + cpu: [amd, intel] + config: [debug, release] + uses: ./.github/workflows/dep_rust.yml + secrets: inherit + with: + hypervisor: ${{ matrix.hypervisor }} + cpu: ${{ matrix.cpu }} + config: ${{ matrix.config }} + target_triple: x86_64-unknown-linux-musl + + notify-failure: + runs-on: ubuntu-latest + needs: musl + if: always() && needs.musl.result == 'failure' + permissions: + issues: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Notify Nightly Failure + run: ./dev/notify-ci-failure.sh --title="Nightly musl Failure - ${{ github.run_number }}" --labels="area/ci-periodics,area/testing,lifecycle/needs-review,release-blocker" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 83a3cb2d4..f5e8b1157 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -32,7 +32,7 @@ jobs: - '**/*.txt' all: - '**/*' - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 id: docs-only with: script: | @@ -44,10 +44,19 @@ jobs: rust: needs: - docs-pr + strategy: + fail-fast: true + matrix: + hypervisor: [hyperv, 'hyperv-ws2025', mshv, mshv3, kvm] + cpu: [amd, intel] + config: [debug, release] uses: ./.github/workflows/dep_rust.yml secrets: inherit with: docs_only: ${{needs.docs-pr.outputs.docs-only}} + hypervisor: ${{ matrix.hypervisor }} + cpu: ${{ matrix.cpu }} + config: ${{ matrix.config }} fuzzing: needs: @@ -63,15 +72,15 @@ jobs: name: spell check with typos runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Spell Check Repo - uses: crate-ci/typos@v1.33.1 + uses: crate-ci/typos@v1.38.1 license-headers: name: check license headers runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check License Headers run: ./dev/check-license-headers.sh @@ -89,9 +98,8 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - name: Previous jobs succeeded - if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} - run: exit 0 - - name: Previous jobs failed - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} - run: exit 1 + # Calculate the exit status of the whole CI workflow. + # If all dependent jobs were successful, this exits with 0 (and the outcome job continues successfully). + # If a some dependent job has failed, this exits with 1. + - name: calculate the correct exit status + run: jq --exit-status 'all(.result == "success" or .result == "skipped")' <<< '${{ toJson(needs) }}' diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index 1248d16bb..0586963af 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -33,7 +33,7 @@ jobs: permission-contents: write - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ steps.get-app-token.outputs.token }} persist-credentials: false diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 21c544125..d1343bcc6 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -18,7 +18,7 @@ jobs: # If you do not check out your code, Copilot will do this for you. steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 # For rust-fmt - name: Set up nightly rust @@ -26,8 +26,8 @@ jobs: with: components: rustfmt - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dep_build_guest_binaries.yml b/.github/workflows/dep_build_guest_binaries.yml index 912a0078e..5b20336ea 100644 --- a/.github/workflows/dep_build_guest_binaries.yml +++ b/.github/workflows/dep_build_guest_binaries.yml @@ -29,23 +29,30 @@ jobs: config: release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build and move Rust guests - run: just build-and-move-rust-guests + - name: Build and move Rust and C guests + run: just guests - - name: Upload Binary Artifacts + - name: Upload Rust Guest Artifacts uses: actions/upload-artifact@v4 with: - name: guest-binaries-${{ matrix.config }} + name: rust-guest-binaries-${{ matrix.config }} path: | - src\tests\rust_guests\bin\${{ matrix.config }}\callbackguest src\tests\rust_guests\bin\${{ matrix.config }}\dummyguest src\tests\rust_guests\bin\${{ matrix.config }}\simpleguest if-no-files-found: error + + - name: Upload C Guest Artifacts + uses: actions/upload-artifact@v4 + with: + name: c-guest-binaries-${{ matrix.config }} + path: | + src\tests\c_guests\bin\${{ matrix.config }}\simpleguest + if-no-files-found: error diff --git a/.github/workflows/dep_fuzzing.yml b/.github/workflows/dep_fuzzing.yml index 0519bfcc4..d4c4f2609 100644 --- a/.github/workflows/dep_fuzzing.yml +++ b/.github/workflows/dep_fuzzing.yml @@ -30,11 +30,11 @@ jobs: target: ${{ fromJson(inputs.targets) }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dep_rust.yml b/.github/workflows/dep_rust.yml index 389f24faa..c9a94f7ea 100644 --- a/.github/workflows/dep_rust.yml +++ b/.github/workflows/dep_rust.yml @@ -11,6 +11,26 @@ on: required: false type: string default: "false" + target_triple: + description: Target triple for cross-compilation + required: false + type: string + default: "" + hypervisor: + description: Hypervisor for this run (passed from caller matrix) + required: false + type: string + default: "kvm" + config: + description: Build configuration for this run (passed from caller matrix) + required: false + type: string + default: "debug" + cpu: + description: CPU architecture for the build (passed from caller matrix) + required: false + type: string + default: "amd" env: CARGO_TERM_COLOR: always @@ -31,23 +51,58 @@ defaults: shell: bash jobs: + code-checks: + if: ${{ inputs.docs_only == 'false' && (inputs.hypervisor == 'hyperv-ws2025' || inputs.hypervisor == 'kvm') }} + timeout-minutes: 60 + runs-on: ${{ fromJson( + format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-amd"]', + (inputs.hypervisor == 'hyperv-ws2025') && 'Windows' || 'Linux', + inputs.hypervisor == 'hyperv-ws2025' && 'win2025' || 'kvm')) }} + steps: + - uses: actions/checkout@v5 + + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 + with: + rust-toolchain: "1.89" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Does not check for updated Cargo.lock files for test rust guests as this causes an issue with this checkwhen deoendabot updates dependencies in common crates + - name: Ensure up-to-date Cargo.lock + run: | + cargo fetch --locked + + - name: fmt + run: just fmt-check + + - name: clippy + if: ${{ (runner.os == 'Windows' )}} + run: | + just clippy ${{ inputs.config }} + just clippy-guests ${{ inputs.config }} + env: + TARGET_TRIPLE: ${{ inputs.target_triple }} + + - name: clippy exhaustive check + if: ${{ (runner.os == 'Linux' )}} + run: | + just clippy-exhaustive ${{ inputs.config }} + env: + TARGET_TRIPLE: ${{ inputs.target_triple }} + + - name: Verify MSRV + run: ./dev/verify-msrv.sh hyperlight-host hyperlight-guest hyperlight-guest-bin hyperlight-common + build: if: ${{ inputs.docs_only == 'false' }} timeout-minutes: 60 - strategy: - fail-fast: true - matrix: - hypervisor: [hyperv, 'hyperv-ws2025', mshv, mshv3, kvm] # hyperv is windows, mshv and kvm are linux - cpu: [amd, intel] - config: [debug, release] - runs-on: ${{ fromJson( format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}"]', - (matrix.hypervisor == 'hyperv' || matrix.hypervisor == 'hyperv-ws2025') && 'Windows' || 'Linux', - matrix.hypervisor == 'hyperv' && 'win2022' || matrix.hypervisor == 'hyperv-ws2025' && 'win2025' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor, - matrix.cpu)) }} + (inputs.hypervisor == 'hyperv' || inputs.hypervisor == 'hyperv-ws2025') && 'Windows' || 'Linux', + inputs.hypervisor == 'hyperv' && 'win2022' || inputs.hypervisor == 'hyperv-ws2025' && 'win2025' || inputs.hypervisor == 'mshv3' && 'azlinux3-mshv' || inputs.hypervisor, + inputs.cpu)) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # For rust-fmt - name: Set up nightly rust @@ -55,25 +110,12 @@ jobs: with: components: rustfmt - - uses: hyperlight-dev/ci-setup-workflow@v1.5.0 + - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 with: - rust-toolchain: "1.85" + rust-toolchain: "1.89" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: fmt - run: just fmt-check - - - name: clippy - run: | - just clippy ${{ matrix.config }} - just clippy-guests ${{ matrix.config }} - - # Does not check for updated Cargo.lock files for test rust guests as this causes an issue with this checkwhen deoendabot updates dependencies in common crates - - name: Ensure up-to-date Cargo.lock - run: | - cargo fetch --locked - - name: Get gh action service name if: ${{ (runner.os == 'Windows' )}} run: (Get-Service actions.runner.*) | Foreach { $_.Name, $_.UserName, $_.ServiceType } @@ -82,37 +124,36 @@ jobs: - name: Build and move Rust guests run: | # use these commands in favor of build-and-move-rust-guests to avoid building both configs - just build-rust-guests ${{ matrix.config }} - just move-rust-guests ${{ matrix.config }} + just build-rust-guests ${{ inputs.config }} + just move-rust-guests ${{ inputs.config }} - name: Build c guests run: | # use these commands in favor of build-and-move-c-guests to avoid building both configs - just build-c-guests ${{ matrix.config }} - just move-c-guests ${{ matrix.config }} + just build-c-guests ${{ inputs.config }} + just move-c-guests ${{ inputs.config }} + - name: Build - run: just build ${{ matrix.config }} - - - name: Verify MSRV - run: ./dev/verify-msrv.sh hyperlight-host hyperlight-guest hyperlight-guest-bin hyperlight-common + run: just build ${{ inputs.config }} + env: + TARGET_TRIPLE: ${{ inputs.target_triple }} - name: Run Rust tests env: CARGO_TERM_COLOR: always + TARGET_TRIPLE: ${{ inputs.target_triple }} run: | # with default features - just test ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}} + just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv' && 'mshv2' || '""'}} - # with only one driver enabled (driver mshv/kvm feature is ignored on windows) + seccomp - just test ${{ matrix.config }} seccomp,${{ matrix.hypervisor == 'mshv' && 'mshv2' || matrix.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }} + # with only one driver enabled (driver mshv/kvm feature is ignored on windows) + just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv' && 'mshv2' || inputs.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }} # make sure certain cargo features compile - cargo check -p hyperlight-host --features crashdump - cargo check -p hyperlight-host --features print_debug - cargo check -p hyperlight-host --features gdb + just check - # without any driver (shouldn't compile) - just test-compilation-fail ${{ matrix.config }} + # without any features + just test-compilation-no-default-features ${{ inputs.config }} # One of the examples is flaky on Windows GH runners, so this allows us to disable it for now - name: Run Rust examples - windows @@ -120,36 +161,48 @@ jobs: env: CARGO_TERM_COLOR: always RUST_LOG: debug - run: just run-rust-examples ${{ matrix.config }} + TARGET_TRIPLE: ${{ inputs.target_triple }} + run: just run-rust-examples ${{ inputs.config }} + - name: Run Rust examples - linux if: ${{ (runner.os != 'Windows') }} env: CARGO_TERM_COLOR: always RUST_LOG: debug - run: just run-rust-examples-linux ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}} + TARGET_TRIPLE: ${{ inputs.target_triple }} + run: just run-rust-examples-linux ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv' && 'mshv2' || '""'}} - - name: Run Rust Gdb tests - linux - if: runner.os == 'Linux' + - name: Run Rust Gdb tests env: CARGO_TERM_COLOR: always RUST_LOG: debug - run: just test-rust-gdb-debugging ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}} + TARGET_TRIPLE: ${{ inputs.target_triple }} + run: just test-rust-gdb-debugging ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv' && 'mshv2' || '""'}} - name: Run Rust Crashdump tests env: CARGO_TERM_COLOR: always RUST_LOG: debug - run: just test-rust-crashdump ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}} + TARGET_TRIPLE: ${{ inputs.target_triple }} + run: just test-rust-crashdump ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv' && 'mshv2' || '""'}} + + - name: Run Rust Tracing tests - linux + if: runner.os == 'Linux' + env: + CARGO_TERM_COLOR: always + RUST_LOG: debug + TARGET_TRIPLE: ${{ inputs.target_triple }} + run: just test-rust-tracing ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv' && 'mshv2' || '""'}} - name: Download benchmarks from "latest" - run: just bench-download ${{ runner.os }} ${{ matrix.hypervisor }} ${{ matrix.cpu}} dev-latest # compare to prerelease + run: just bench-download ${{ runner.os }} ${{ inputs.hypervisor }} ${{ inputs.cpu}} dev-latest # compare to prerelease env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true - if: ${{ matrix.config == 'release' }} + if: ${{ inputs.config == 'release' && inputs.target_triple == '' }} - name: Run benchmarks run: | - just bench-ci main ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}} - if: ${{ matrix.config == 'release' }} + just bench-ci main ${{ inputs.hypervisor == 'mshv' && 'mshv2' || ''}} + if: ${{ inputs.config == 'release' && inputs.target_triple == '' }} diff --git a/.gitignore b/.gitignore index dec76e560..dbd55529f 100644 --- a/.gitignore +++ b/.gitignore @@ -471,3 +471,5 @@ hyperlight_guest.h # gdb .gdbinit +trace/* +.gdbguest diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ec1edf0e..f8cea4474 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,10 @@ "Cargo.toml", // guest crates for testing, not part of the workspace "src/tests/rust_guests/simpleguest/Cargo.toml", - "src/tests/rust_guests/callbackguest/Cargo.toml" - ] + ], + // Enable features in rust analyzer here + "rust-analyzer.cargo.features": [ + // "gdb", + // "kvm" + ], } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f72db57d0..084090a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Prerelease] - Unreleased +## [v0.10.0] - 2025-10-02 + +### Fixed + +- Fix error code conversion for Exception enum TryFrom implementation by @vshailesh in https://github.com/hyperlight-dev/hyperlight/pull/869 +- Remove Allocations from Panic Handler by @adamperlin in https://github.com/hyperlight-dev/hyperlight/pull/818 + +### Changed + +- Update rust to 1.89 by @simongdavies in https://github.com/hyperlight-dev/hyperlight/pull/883 +- Update mshv crates for Azure Linux to v0.6.1 (from v0.3.2) by @simongdavies in https://github.com/hyperlight-dev/hyperlight/pull/891 +- Only clear io buffer after unsuccessful guest call by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/811 + +## [v0.9.0] - 2025-08-28 + +### Fixed + +- fix release blocker so it only blocks on release branches by @simongdavies in https://github.com/hyperlight-dev/hyperlight/pull/777 +- Enforce release builds for benchmarks and simplify command interface by @Copilot in https://github.com/hyperlight-dev/hyperlight/pull/741 +- fix(guest-bin): align user memory allocations by @andreiltd in https://github.com/hyperlight-dev/hyperlight/pull/753 +- Fix unbounded growth of panic hook after each new sandbox by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/827 +- Update the like-ci recipe by @simongdavies in https://github.com/hyperlight-dev/hyperlight/pull/837 +- Fixes to Host Call Fuzzing by @adamperlin in https://github.com/hyperlight-dev/hyperlight/pull/840 + +### Changed + +- Optimize function call serializing by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/778 +- Make the component macros support passing host resources to guests by @syntactically in https://github.com/hyperlight-dev/hyperlight/pull/839 +- Build c guests as required by benchmarks by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/822 + +### Removed +- Remove DbgMemAccessHandlerCaller trait and DbgMemAccessHandlerWrapper abstractions by @Copilot in https://github.com/hyperlight-dev/hyperlight/pull/824 + +## [v0.8.0] - 2025-08-08 + +:warning: `hyperlight_component_macro::host_bindgen` and `hyperlight_component_macro::guest_bindgen` used the `Callable` trait which no longer restores state after each function call and requires an explicit Snapshot Restore using the newly exposed Snapshot API. See https://github.com/hyperlight-dev/hyperlight/pull/697 and https://github.com/hyperlight-dev/hyperlight/pull/761 + +### Fixed +- gdb: fix issue "Debug not enabled" when `gdb` feature was enabled by @dblnz in https://github.com/hyperlight-dev/hyperlight/pull/678 +- Fix Windows build with `--no-default-features` by @danbugs in https://github.com/hyperlight-dev/hyperlight/pull/712 +- fix(guest-bin): move logger initialization by @andreiltd in https://github.com/hyperlight-dev/hyperlight/pull/755 +- Fix mem mgr not initialized by @dblnz in https://github.com/hyperlight-dev/hyperlight/pull/745 + +### Changed +- Remove some dev-dependencies and cargo features to speed up compilation by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/535 +- Introduce a separate KVM error variant of HyperlightError. by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/771API. by @jprendes in https://github.com/hyperlight-dev/hyperlight/pull/697 +- Evolving and Devolving apis replaced by Snapshot API + - Remove sandbox evolving and devolving and replace it with snapshotting API. by @jprendes in https://github.com/hyperlight-dev/hyperlight/pull/697 + - Bring back the previous behavior of `call_guest_function_by_name` by @jprendes in https://github.com/hyperlight-dev/hyperlight/pull/761 + +### Added +- Memory Mapping Support + - Support mapping host memory into the guest by @syntactically in https://github.com/hyperlight-dev/hyperlight/pull/696 + - Make MultiUseSandbox::map_file_cow public by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/725 + - Add memory mapping support with KVM by @jprendes in https://github.com/hyperlight-dev/hyperlight/pull/709 + - Make sure mmapped memory is not mapped writeable into sandbox in kvm by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/740 + - Make snapshots region aware by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/742 + - Restrict restoring sandboxes to snapshot taken on self by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/746 +- Enable guest tracing by @dblnz in https://github.com/hyperlight-dev/hyperlight/pull/695 + +### Removed +- Removed the OutBHandler and MemAccessHandler abstractions and related implementations. by @simongdavies in https://github.com/hyperlight-dev/hyperlight/pull/732 + ## [v0.7.0] - 2025-06-26 ### Fixed @@ -138,7 +201,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The Initial Hyperlight Release 🎉 -[Prerelease]: +[Prerelease]: +[v0.10.0]: +[v0.9.0]: +[v0.8.0]: +[v0.7.0]: +[v0.6.1]: +[v0.6.0]: +[v0.5.1]: +[v0.5.0]: [v0.4.0]: [v0.3.0]: [v0.2.0]: diff --git a/Cargo.lock b/Cargo.lock index 7983d278c..ea3d1c39c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,29 +4,36 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ + "cpp_demangle", + "fallible-iterator", "gimli", + "memmap2", + "object", + "rustc-demangle", + "smallvec", + "typed-arena", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy 0.8.26", ] [[package]] @@ -38,12 +45,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -61,9 +62,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -76,114 +77,94 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] -name = "async-trait" -version = "0.1.88" +name = "arrayref" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] -name = "atomic-waker" -version = "1.1.2" +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "autocfg" -version = "1.4.0" +name = "associative-cache" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "b993cd767a2bc7307dd87622311ca22c44329cc7a21366206bfa0896827b2bad" [[package]] -name = "aws-lc-rs" -version = "1.13.0" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "aws-lc-sys", - "zeroize", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "aws-lc-sys" -version = "0.28.0" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" -dependencies = [ - "bindgen 0.69.5", - "cc", - "cmake", - "dunce", - "fs_extra", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "backtrace" -version = "0.3.74" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -191,36 +172,13 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", - "which", -] - [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -257,9 +215,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] [[package]] name = "block-buffer" @@ -301,9 +272,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -317,6 +288,29 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cairo-rs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1158f326d7b755a9ae2b36c5b5391400e3431f3b77418cedb6d7130126628f10" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b963177900ec8e783927e5ed99e16c0ec1b723f1f125dff8992db28ef35c62c3" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "cast" version = "0.3.0" @@ -325,9 +319,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cbindgen" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684" +checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" dependencies = [ "clap", "heck", @@ -339,15 +333,16 @@ dependencies = [ "serde_json", "syn", "tempfile", - "toml", + "toml 0.9.8", ] [[package]] name = "cc" -version = "1.2.27" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -362,11 +357,21 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -376,16 +381,15 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -428,18 +432,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -449,30 +453,27 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] -name = "cmake" -version = "0.1.54" +name = "colorchoice" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "colorchoice" -version = "1.0.3" +name = "constant_time_eq" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -484,6 +485,51 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -493,11 +539,20 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" dependencies = [ "anes", "cast", @@ -518,25 +573,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools 0.10.5", -] - -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", + "itertools 0.13.0", ] [[package]] @@ -584,9 +626,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -600,9 +642,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -637,7 +679,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -652,10 +694,16 @@ dependencies = [ ] [[package]] -name = "dunce" -version = "1.0.5" +name = "dwrote" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "20c93d234bac0cdd0e2ac08bc8a5133f8df2169e95b262dfcea5e5cb7855672f" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] [[package]] name = "either" @@ -706,6 +754,15 @@ dependencies = [ "log", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -714,30 +771,70 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "flatbuffers" -version = "25.2.10" +version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "rustc_version", ] +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -750,20 +847,55 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] -name = "fs_extra" -version = "1.3.0" +name = "framehop" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +checksum = "78a7d65f75e837647bf8b1594ad3b559a929ee9a58d956d9f46999749957b6b9" +dependencies = [ + "arrayvec", + "cfg-if", + "fallible-iterator", + "gimli", + "macho-unwind-info", + "pe-unwind-info", +] [[package]] name = "futures" @@ -856,11 +988,11 @@ dependencies = [ [[package]] name = "gdbstub" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d66e32caf5dd59f561be0143e413e01d651bd8498eb9aa0be8c482c81c8d31" +checksum = "72742d2b395902caf8a5d520d0dd3334ba6d1138938429200e58d5174e275f3f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg-if", "log", "managed", @@ -880,15 +1012,16 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" dependencies = [ + "cc", "cfg-if", "libc", "log", "rustversion", - "windows 0.58.0", + "windows 0.61.3", ] [[package]] @@ -903,51 +1036,129 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6298e594375a7fead9efd5568f0a46e6a154fb6a9bdcbe3c06946ffd81a5f6" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + +[[package]] +name = "gio" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5e3f390d01b79e30da451dd00e27cd1ac2de81658e3abf6c1fc3229b24c5f" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "a03f2234671e5a588cfe1f59c2b22c103f5772ea351be9cc824a9ce0d06d99fd" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.60.2", +] [[package]] name = "git2" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", "url", ] +[[package]] +name = "glib" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c43cff6a7dc43821e45ebf172399437acd6716fa2186b6852d2b397bf622d" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -958,15 +1169,26 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gobject-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9a190eef2bce144a6aa8434e306974c6062c398e0a33a146d60238f9062d5c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", ] [[package]] name = "goblin" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e961b33649994dcf69303af6b3a332c1228549e604d455d61ec5d2ab5e68d3a" +checksum = "51876e3748c4a347fe65b906f2b1ae46a1e55a497b22c94c1f4f2c469ff7673a" dependencies = [ "log", "plain", @@ -975,9 +1197,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1002,16 +1224,42 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", "serde", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heapless" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -1024,15 +1272,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "http" version = "1.3.1" @@ -1073,51 +1312,28 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1133,17 +1349,21 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -1153,7 +1373,7 @@ dependencies = [ [[package]] name = "hyperlight-common" -version = "0.7.0" +version = "0.10.0" dependencies = [ "anyhow", "arbitrary", @@ -1166,7 +1386,7 @@ dependencies = [ [[package]] name = "hyperlight-component-macro" -version = "0.7.0" +version = "0.10.0" dependencies = [ "env_logger", "hyperlight-component-util", @@ -1180,7 +1400,7 @@ dependencies = [ [[package]] name = "hyperlight-component-util" -version = "0.7.0" +version = "0.10.0" dependencies = [ "itertools 0.14.0", "log", @@ -1202,49 +1422,70 @@ dependencies = [ [[package]] name = "hyperlight-guest" -version = "0.7.0" +version = "0.10.0" dependencies = [ "anyhow", + "flatbuffers", "hyperlight-common", + "hyperlight-guest-tracing", "serde_json", + "tracing", ] [[package]] name = "hyperlight-guest-bin" -version = "0.7.0" +version = "0.10.0" dependencies = [ "buddy_system_allocator", "cc", "cfg-if", + "flatbuffers", "glob", "hyperlight-common", "hyperlight-guest", + "hyperlight-guest-tracing", "log", "spin 0.10.0", + "tracing", +] + +[[package]] +name = "hyperlight-guest-tracing" +version = "0.10.0" +dependencies = [ + "heapless", + "hyperlight-common", + "spin 0.10.0", + "tracing", + "tracing-core", ] [[package]] name = "hyperlight-host" -version = "0.7.0" +version = "0.10.0" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags 2.10.0", + "blake3", "built", "cfg-if", "cfg_aliases", "chrono", "criterion", - "crossbeam", "crossbeam-channel", "crossbeam-queue", "elfcore", "env_logger", + "envy", + "fallible-iterator", "flatbuffers", + "framehop", "gdbstub", "gdbstub_arch", "goblin", "hyperlight-common", "hyperlight-component-macro", + "hyperlight-guest-tracing", "hyperlight-testing", "kvm-bindings", "kvm-ioctls", @@ -1255,9 +1496,9 @@ dependencies = [ "metrics-exporter-prometheus", "metrics-util", "mshv-bindings 0.2.1", - "mshv-bindings 0.3.2", + "mshv-bindings 0.6.1", "mshv-ioctls 0.2.1", - "mshv-ioctls 0.3.2", + "mshv-ioctls 0.6.1", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", @@ -1267,7 +1508,6 @@ dependencies = [ "proptest", "rand", "rust-embed", - "seccompiler", "serde", "serde_json", "serial_test", @@ -1275,7 +1515,7 @@ dependencies = [ "signal-hook-registry", "tempfile", "termcolor", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "tracing-chrome", @@ -1288,9 +1528,9 @@ dependencies = [ "tracing-tracy", "uuid", "vmm-sys-util", - "windows 0.61.3", - "windows-result 0.3.4", - "windows-sys 0.59.0", + "windows 0.62.2", + "windows-result 0.4.1", + "windows-sys 0.61.2", "windows-version", ] @@ -1311,9 +1551,10 @@ dependencies = [ [[package]] name = "hyperlight_guest_capi" -version = "0.7.0" +version = "0.10.0" dependencies = [ "cbindgen", + "flatbuffers", "hyperlight-common", "hyperlight-guest", "hyperlight-guest-bin", @@ -1346,21 +1587,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1369,31 +1611,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1401,72 +1623,59 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1475,9 +1684,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1485,13 +1694,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -1501,28 +1711,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itertools" -version = "0.10.5" +name = "iri-string" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ - "either", + "memchr", + "serde", ] [[package]] -name = "itertools" -version = "0.12.1" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -1550,9 +1752,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -1563,9 +1765,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", @@ -1574,11 +1776,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -1592,22 +1794,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "kvm-bindings" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3432d9f609fbede9f624d1dbefcce77985a9322de1d0e6d460ec05502b7fd0" +checksum = "4b3c06ff73c7ce03e780887ec2389d62d2a2a9ddf471ab05c2ff69207cd3f3b4" dependencies = [ "vmm-sys-util", ] [[package]] name = "kvm-ioctls" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e00243d27a20feb05cf001ae52ddc79831ac70c020f215ba1153ff9270b650a" +checksum = "333f77a20344a448f3f70664918135fddeb804e938f28a99d685bd92926e0b19" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "kvm-bindings", "libc", "vmm-sys-util", @@ -1619,23 +1832,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.174" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -1643,9 +1850,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.1+1.9.0" +version = "0.18.2+1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" dependencies = [ "cc", "libc", @@ -1655,12 +1862,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] @@ -1669,18 +1876,18 @@ version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb" dependencies = [ - "bindgen 0.70.1", + "bindgen", "errno", "libc", ] [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "libc", ] @@ -1696,12 +1903,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1710,15 +1911,15 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1726,9 +1927,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -1745,13 +1946,24 @@ dependencies = [ [[package]] name = "mach2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.17", + "zerocopy 0.8.26", + "zerocopy-derive 0.8.26", +] + [[package]] name = "managed" version = "0.8.0" @@ -1760,18 +1972,33 @@ checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] [[package]] name = "metrics" @@ -1790,18 +2017,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b166dea96003ee2531cf14833efedced545751d800f03535801d833313f8c15" dependencies = [ "base64", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", "indexmap", - "ipnet", "metrics", "metrics-util", "quanta", - "thiserror 2.0.12", - "tokio", - "tracing", + "thiserror 2.0.17", ] [[package]] @@ -1813,7 +2033,7 @@ dependencies = [ "aho-corasick", "crossbeam-epoch", "crossbeam-utils", - "hashbrown", + "hashbrown 0.15.5", "indexmap", "metrics", "ordered-float", @@ -1824,12 +2044,6 @@ dependencies = [ "sketches-ddsketch", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1838,22 +2052,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -1870,14 +2085,14 @@ dependencies = [ [[package]] name = "mshv-bindings" -version = "0.3.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0cb5031f3243a7459b7c13d960d25420980874eebda816db24ce6077e21d43" +checksum = "fef6d8b8215ff502f0f662d1fc5229d6c79628208a0d03277ce52fbbe7fe1872" dependencies = [ "libc", "num_enum", "vmm-sys-util", - "zerocopy 0.8.24", + "zerocopy 0.8.26", ] [[package]] @@ -1894,13 +2109,13 @@ dependencies = [ [[package]] name = "mshv-ioctls" -version = "0.3.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89abe853221fa6f14ad4066affb9abda241a03d65622887d5794e1422d0bd75a" +checksum = "fdbb879d6a9ca5359ae020c17ebf8587e0be309bf32beae636030e4408c2e481" dependencies = [ "libc", - "mshv-bindings 0.3.2", - "thiserror 2.0.12", + "mshv-bindings 0.6.1", + "thiserror 2.0.17", "vmm-sys-util", ] @@ -1936,12 +2151,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1955,18 +2169,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro2", "quote", @@ -1975,11 +2190,13 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ + "flate2", "memchr", + "ruzstd", ] [[package]] @@ -1989,36 +2206,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "oorandom" -version = "11.1.5" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "oorandom" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "opentelemetry" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", ] [[package]] name = "opentelemetry-http" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", @@ -2029,9 +2246,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ "http", "opentelemetry", @@ -2040,35 +2257,35 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tonic", - "tracing", ] [[package]] name = "opentelemetry-proto" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" [[package]] name = "opentelemetry_sdk" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", @@ -2076,8 +2293,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand", - "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-stream", ] @@ -2097,12 +2313,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "page_size" version = "0.6.0" @@ -2113,11 +2323,61 @@ dependencies = [ "winapi", ] +[[package]] +name = "pango" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47feb3403aa564edaeb68620c5b9159f8814733a7dd45f0b1a27d19de362fe" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f855bccb447644e149fae79086e1f81514c30fe5e9b8bd257d9d3c941116c86" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pangocairo" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb23cf0052917cbf75f160d4913a46ce741567f566b514fadc09d761f41eb2fb" +dependencies = [ + "cairo-rs", + "glib", + "libc", + "pango", + "pangocairo-sys", +] + +[[package]] +name = "pangocairo-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcda09c0b17007d7eb6c5eb1643c5b40b067073c15f0cc5a809a6fc68b5d9be7" +dependencies = [ + "cairo-sys-rs", + "glib-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2125,9 +2385,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -2142,11 +2402,111 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pe-unwind-info" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f6fccfd2d9d2df765ca23ff85fe5cc437fb0e6d3e164e4d3cbe09d14780c93" +dependencies = [ + "arrayvec", + "bitflags 2.10.0", + "thiserror 2.0.17", + "zerocopy 0.8.26", + "zerocopy-derive 0.8.26", +] + [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "piet" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31867aac026f5706ca679ee13e0089fe85be00646ff1de73dd5ca6a15ddb48e1" +dependencies = [ + "kurbo", + "unic-bidi", +] + +[[package]] +name = "piet-cairo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae84f3d22ce540ca5a598669c3fc95fe0cae45bdf733abc9796d20f4445017c1" +dependencies = [ + "cairo-rs", + "pango", + "pangocairo", + "piet", + "unicode-segmentation", + "xi-unicode", +] + +[[package]] +name = "piet-common" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34c5f8f1bf281d40b08da00b90e20b14d3a68035eee1c12ba260c62b040172f3" +dependencies = [ + "cairo-rs", + "cairo-sys-rs", + "cfg-if", + "core-graphics", + "piet", + "piet-cairo", + "piet-coregraphics", + "piet-direct2d", + "piet-web", + "png", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "piet-coregraphics" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75958f4ea4a36640178d2fb342fe29707e346dbc31b5983e31c8ef3bb78b7fce" +dependencies = [ + "associative-cache", + "core-foundation", + "core-foundation-sys", + "core-graphics", + "core-text", + "foreign-types", + "piet", +] + +[[package]] +name = "piet-direct2d" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "838ce5efd17a7911574e2047040dbae5d4b2633f50263673af6be4e65ae68f5c" +dependencies = [ + "associative-cache", + "dwrote", + "piet", + "utf16_lit", + "winapi", + "wio", +] + +[[package]] +name = "piet-web" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "838f32f0ea82a258c767662c1751e9e35f34b6e3977e2d9c0b7bc207d9547628" +dependencies = [ + "js-sys", + "piet", + "unicode-segmentation", + "wasm-bindgen", + "web-sys", + "xi-unicode", +] [[package]] name = "pin-project" @@ -2220,11 +2580,24 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -2235,30 +2608,48 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy 0.8.26", ] [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -2270,7 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3db44c5aa60e193a25fcd93bb9ed27423827e8f118897866f946e2cf936c44fb" dependencies = [ "anyhow", - "bindgen 0.70.1", + "bindgen", "libc", "libproc", "mach2", @@ -2279,19 +2670,19 @@ dependencies = [ [[package]] name = "proptest" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.1", + "bitflags 2.10.0", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -2299,9 +2690,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -2309,9 +2700,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools 0.14.0", @@ -2322,15 +2713,15 @@ dependencies = [ [[package]] name = "quanta" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -2343,18 +2734,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radix_trie" @@ -2368,9 +2759,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -2392,7 +2783,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -2419,14 +2810,14 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2434,9 +2825,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2444,73 +2835,58 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", @@ -2522,11 +2898,8 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "serde", @@ -2535,33 +2908,19 @@ dependencies = [ "sync_wrapper", "tokio", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", -] - -[[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.15", - "libc", - "untrusted", - "windows-sys 0.52.0", ] [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -2570,9 +2929,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" dependencies = [ "proc-macro2", "quote", @@ -2584,9 +2943,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" dependencies = [ "globset", "sha2", @@ -2595,9 +2954,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -2616,79 +2975,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" -dependencies = [ - "bitflags 2.9.1", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - -[[package]] -name = "rustls-webpki" -version = "0.103.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", + "linux-raw-sys", + "windows-sys 0.60.2", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" @@ -2702,6 +3004,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ruzstd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" +dependencies = [ + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.20" @@ -2719,22 +3030,13 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -2769,62 +3071,40 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" - -[[package]] -name = "seccompiler" -version = "0.5.0" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae55de56877481d112a559bbc12667635fdaf5e005712fd4e2b2fa50ffc884" -dependencies = [ - "libc", -] +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" [[package]] -name = "security-framework" -version = "3.2.0" +name = "semver" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] -name = "security-framework-sys" -version = "2.14.0" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "core-foundation-sys", - "libc", + "serde_core", + "serde_derive", ] [[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2833,25 +3113,35 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2891,9 +3181,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2939,13 +3229,19 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "sketches-ddsketch" version = "0.3.0" @@ -2954,27 +3250,24 @@ checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3007,17 +3300,11 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" -version = "2.0.104" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -3035,26 +3322,45 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "system-deps" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.23", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.5", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -3077,11 +3383,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -3097,9 +3403,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -3108,19 +3414,18 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3138,11 +3443,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -3151,30 +3455,20 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -3188,9 +3482,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -3201,43 +3495,82 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tonic" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "base64", @@ -3250,7 +3583,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "sync_wrapper", "tokio", "tokio-stream", "tower", @@ -3259,6 +3592,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" @@ -3278,6 +3622,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3290,6 +3652,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "trace_dump" +version = "0.10.0" +dependencies = [ + "addr2line", + "blake3", + "piet-common", +] + [[package]] name = "tracing" version = "0.1.41" @@ -3304,9 +3675,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -3336,14 +3707,14 @@ dependencies = [ [[package]] name = "tracing-forest" -version = "0.1.6" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee40835db14ddd1e3ba414292272eddde9dad04d3d4b65509656414d1c42592f" +checksum = "3298fe855716711a00474eceb89cc7dc254bbe67f6bc4afafdeec5f0c538771c" dependencies = [ "chrono", "serde", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.17", "tracing", "tracing-subscriber", "uuid", @@ -3362,15 +3733,16 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" dependencies = [ "js-sys", - "once_cell", "opentelemetry", "opentelemetry_sdk", + "rustversion", "smallvec", + "thiserror 2.0.17", "tracing", "tracing-core", "tracing-log", @@ -3390,14 +3762,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -3419,9 +3791,9 @@ dependencies = [ [[package]] name = "tracy-client" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d90a2c01305b02b76fdd89ac8608bae27e173c829a35f7d76a345ab5d33836db" +checksum = "ef54005d3d760186fd662dad4b7bb27ecd5531cdef54d1573ebd3f20a9205ed7" dependencies = [ "loom", "once_cell", @@ -3430,9 +3802,9 @@ dependencies = [ [[package]] name = "tracy-client-sys" -version = "0.24.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fff37da548239c3bf9e64a12193d261e8b22b660991c6fd2df057c168f435f" +checksum = "319c70195101a93f56db4c74733e272d720768e13471f400c78406a326b172b0" dependencies = [ "cc", "windows-targets 0.52.6", @@ -3444,6 +3816,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.18.0" @@ -3456,6 +3840,57 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unic-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356b759fb6a82050666f11dce4b6fe3571781f1449f3ef78074e408d468ec09" +dependencies = [ + "matches", + "unic-ucd-bidi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3463,27 +3898,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] -name = "untrusted" -version = "0.9.0" +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] -name = "utf16_iter" -version = "1.0.5" +name = "utf16_lit" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +checksum = "14706d2a800ee8ff38c1d3edb873cd616971ea59eb7c0d046bb44ef59b06a1ae" [[package]] name = "utf8_iter" @@ -3499,11 +3935,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "js-sys", "serde", "wasm-bindgen", @@ -3521,6 +3957,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -3529,9 +3971,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vmm-sys-util" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" +checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" dependencies = [ "bitflags 1.3.2", "libc", @@ -3567,17 +4009,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -3653,12 +4095,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.224.1" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" dependencies = [ - "bitflags 2.9.1", - "hashbrown", + "bitflags 2.10.0", + "hashbrown 0.15.5", "indexmap", "semver", "serde", @@ -3684,18 +4126,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3714,11 +4144,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3729,25 +4159,27 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.58.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", ] [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link", - "windows-numerics", + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -3760,16 +4192,12 @@ dependencies = [ ] [[package]] -name = "windows-core" -version = "0.58.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -3778,13 +4206,26 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", - "windows-link", + "windows-implement", + "windows-interface", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-future" version = "0.2.1" @@ -3792,26 +4233,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", - "windows-threading", + "windows-link 0.1.3", + "windows-threading 0.1.0", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -3820,9 +4261,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -3830,21 +4271,16 @@ dependencies = [ ] [[package]] -name = "windows-interface" -version = "0.59.1" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -3853,27 +4289,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", -] - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-link 0.1.3", ] [[package]] -name = "windows-result" -version = "0.2.0" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -3882,35 +4308,34 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] -name = "windows-strings" -version = "0.1.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3931,6 +4356,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3949,10 +4392,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3969,16 +4413,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", ] [[package]] name = "windows-version" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4079,39 +4532,45 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wio" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" dependencies = [ - "bitflags 2.9.1", + "winapi", ] [[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "xi-unicode" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4121,9 +4580,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -4143,11 +4602,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive 0.8.26", ] [[package]] @@ -4163,9 +4622,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", @@ -4194,16 +4653,21 @@ dependencies = [ ] [[package]] -name = "zeroize" -version = "1.8.1" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -4212,9 +4676,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2c16795c3..e509372a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,37 +10,39 @@ members = [ "src/hyperlight_guest", "src/hyperlight_host", "src/hyperlight_guest_capi", + "src/hyperlight_guest_tracing", "src/hyperlight_testing", "fuzz", "src/hyperlight_guest_bin", "src/hyperlight_component_util", "src/hyperlight_component_macro", + "src/trace_dump", ] # Guests have custom linker flags, so we need to exclude them from the workspace exclude = [ - "src/tests/rust_guests/callbackguest", "src/tests/rust_guests/dummyguest", "src/tests/rust_guests/simpleguest", "src/tests/rust_guests/witguest", ] [workspace.package] -version = "0.7.0" +version = "0.10.0" edition = "2024" -rust-version = "1.85" +rust-version = "1.88" license = "Apache-2.0" homepage = "https://github.com/hyperlight-dev/hyperlight" repository = "https://github.com/hyperlight-dev/hyperlight" readme = "README.md" [workspace.dependencies] -hyperlight-common = { path = "src/hyperlight_common", version = "0.7.0", default-features = false } -hyperlight-host = { path = "src/hyperlight_host", version = "0.7.0", default-features = false } -hyperlight-guest = { path = "src/hyperlight_guest", version = "0.7.0", default-features = false } -hyperlight-guest-bin = { path = "src/hyperlight_guest_bin", version = "0.7.0", default-features = false } +hyperlight-common = { path = "src/hyperlight_common", version = "0.10.0", default-features = false } +hyperlight-host = { path = "src/hyperlight_host", version = "0.10.0", default-features = false } +hyperlight-guest = { path = "src/hyperlight_guest", version = "0.10.0", default-features = false } +hyperlight-guest-bin = { path = "src/hyperlight_guest_bin", version = "0.10.0", default-features = false } hyperlight-testing = { path = "src/hyperlight_testing", default-features = false } -hyperlight-component-util = { path = "src/hyperlight_component_util", version = "0.7.0", default-features = false } -hyperlight-component-macro = { path = "src/hyperlight_component_macro", version = "0.7.0", default-features = false } +hyperlight-guest-tracing = { path = "src/hyperlight_guest_tracing", version = "0.10.0", default-features = false } +hyperlight-component-util = { path = "src/hyperlight_component_util", version = "0.10.0", default-features = false } +hyperlight-component-macro = { path = "src/hyperlight_component_macro", version = "0.10.0", default-features = false } [workspace.lints.rust] unsafe_op_in_unsafe_fn = "deny" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 000000000..da3c48089 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,14 @@ +[build] +pre-build = [ + "apt-get update && apt-get -y install wget build-essential libgmp-dev libmpfr-dev", + "cd /tmp && curl https://sourceware.org/pub/gdb/releases/gdb-16.3.tar.gz -o /tmp/gdb-16.3.tar.gz", + "cd /tmp && tar -xf gdb-16.3.tar.gz", + "cd /tmp/gdb-16.3 && ./configure --prefix=/usr/local", + "cd /tmp/gdb-16.3 && make -j$(nproc)", + "cd /tmp/gdb-16.3 && make install", +] + +[build.env] +passthrough = [ + "TARGET_TRIPLE", # Some tests invoke Cargo directly and need this to run correctly +] diff --git a/Justfile b/Justfile index 66b06a587..67925f662 100644 --- a/Justfile +++ b/Justfile @@ -6,12 +6,25 @@ set dotenv-load := true set-env-command := if os() == "windows" { "$env:" } else { "export " } bin-suffix := if os() == "windows" { ".bat" } else { ".sh" } +################ +### cross-rs ### +################ +target-triple := env('TARGET_TRIPLE', "") +docker := if target-triple != "" { require("docker") } else { "" } +# this command is only used host side not for guests +# include the --target-dir for the cross builds. This ensures that the builds are separated and avoid any conflicts with the guest builds +cargo-cmd := if target-triple != "" { require("cross") } else { "cargo" } +target-triple-flag := if target-triple != "" { "--target " + target-triple + " --target-dir ./target/host"} else { "" } +# set up cross to use the devices +kvm-gid := if path_exists("/dev/kvm") == "true" { `getent group kvm | cut -d: -f3` } else { "" } +export CROSS_CONTAINER_OPTS := if path_exists("/dev/kvm") == "true" { "--device=/dev/kvm" } else if path_exists("/dev/mshv") == "true" { "--device=/dev/mshv" } else { "" } +export CROSS_CONTAINER_GID := if path_exists("/dev/kvm") == "true" { kvm-gid } else {"1000"} # required to have ownership of the mapped in device on kvm + root := justfile_directory() default-target := "debug" simpleguest_source := "src/tests/rust_guests/simpleguest/target/x86_64-unknown-none" dummyguest_source := "src/tests/rust_guests/dummyguest/target/x86_64-unknown-none" -callbackguest_source := "src/tests/rust_guests/callbackguest/target/x86_64-unknown-none" witguest_source := "src/tests/rust_guests/witguest/target/x86_64-unknown-none" rust_guests_bin_dir := "src/tests/rust_guests/bin" @@ -24,12 +37,7 @@ alias cg := build-and-move-c-guests # build host library build target=default-target: - cargo build --profile={{ if target == "debug" { "dev" } else { target } }} - -# build host library -build-with-musl-libc target=default-target: - cargo build --profile={{ if target == "debug" { "dev" } else { target } }} --target x86_64-unknown-linux-musl - + {{ cargo-cmd }} build --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} # build testing guest binaries guests: build-and-move-rust-guests build-and-move-c-guests @@ -38,14 +46,12 @@ witguest-wit: cargo install --locked wasm-tools cd src/tests/rust_guests/witguest && wasm-tools component wit guest.wit -w -o interface.wasm -build-rust-guests target=default-target: (witguest-wit) - cd src/tests/rust_guests/callbackguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }} - cd src/tests/rust_guests/simpleguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }} - cd src/tests/rust_guests/dummyguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }} - cd src/tests/rust_guests/witguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }} +build-rust-guests target=default-target features="": (witguest-wit) + cd src/tests/rust_guests/simpleguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} + cd src/tests/rust_guests/dummyguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} + cd src/tests/rust_guests/witguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} @move-rust-guests target=default-target: - cp {{ callbackguest_source }}/{{ target }}/callbackguest* {{ rust_guests_bin_dir }}/{{ target }}/ cp {{ simpleguest_source }}/{{ target }}/simpleguest* {{ rust_guests_bin_dir }}/{{ target }}/ cp {{ dummyguest_source }}/{{ target }}/dummyguest* {{ rust_guests_bin_dir }}/{{ target }}/ cp {{ witguest_source }}/{{ target }}/witguest* {{ rust_guests_bin_dir }}/{{ target }}/ @@ -59,9 +65,8 @@ clean-rust: cargo clean cd src/tests/rust_guests/simpleguest && cargo clean cd src/tests/rust_guests/dummyguest && cargo clean - cd src/tests/rust_guests/callbackguest && cargo clean - cd src/tests/rust_guests/witguest && cargo clean - cd src/tests/rust_guests/witguest && rm -f interface.wasm + {{ if os() == "windows" { "cd src/tests/rust_guests/witguest -ErrorAction SilentlyContinue; cargo clean" } else { "[ -d src/tests/rust_guests/witguest ] && cd src/tests/rust_guests/witguest && cargo clean || true" } }} + {{ if os() == "windows" { "Remove-Item src/tests/rust_guests/witguest/interface.wasm -Force -ErrorAction SilentlyContinue" } else { "rm -f src/tests/rust_guests/witguest/interface.wasm" } }} git clean -fdx src/tests/c_guests/bin src/tests/rust_guests/bin ################ @@ -73,79 +78,148 @@ clean-rust: # convenience recipe to run all tests with the given target and features (similar to CI) test-like-ci config=default-target hypervisor="kvm": @# with default features - just test {{config}} {{ if hypervisor == "mshv3" {"mshv3"} else {""} }} + just test {{config}} {{ if hypervisor == "mshv" {"mshv2"} else {""} }} - @# with only one driver enabled + seccomp + build-metadata + init-paging - just test {{config}} seccomp,build-metadata,init-paging,{{ if hypervisor == "mshv" {"mshv2"} else if hypervisor == "mshv3" {"mshv3"} else {"kvm"} }} + @# with only one driver enabled + build-metadata + init-paging + just test {{config}} build-metadata,init-paging,{{ if hypervisor == "mshv" {"mshv2"} else if hypervisor == "mshv3" {"mshv3"} else {"kvm"} }} @# make sure certain cargo features compile - cargo check -p hyperlight-host --features crashdump - cargo check -p hyperlight-host --features print_debug - cargo check -p hyperlight-host --features gdb + just check @# without any driver (should fail to compile) - just test-compilation-fail {{config}} + just test-compilation-no-default-features {{config}} @# test the crashdump feature just test-rust-crashdump {{config}} + @# test the tracing related features + {{ if os() == "linux" { "just test-rust-tracing " + config + " " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }} + +like-ci config=default-target hypervisor="kvm": + @# Ensure up-to-date Cargo.lock + cargo fetch --locked + + @# fmt + just fmt-check + + @# clippy + {{ if os() == "windows" { "just clippy " + config } else { "" } }} + {{ if os() == "windows" { "just clippy-guests " + config } else { "" } }} + + @# clippy exhaustive check + {{ if os() == "linux" { "just clippy-exhaustive " + config } else { "" } }} + + @# Verify MSRV + ./dev/verify-msrv.sh hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-guest-tracing + + @# Build and move Rust guests + just build-rust-guests {{config}} + just move-rust-guests {{config}} + + @# Build c guests + just build-c-guests {{config}} + just move-c-guests {{config}} + + @# Build + just build {{config}} + + @# Run Rust tests + just test-like-ci {{config}} {{hypervisor}} + + @# Run Rust examples - Windows + {{ if os() == "windows" { "just run-rust-examples " + config } else { "" } }} + + @# Run Rust examples - linux + {{ if os() == "linux" { "just run-rust-examples-linux " + config + " " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }} + + @# Run Rust Gdb tests + just test-rust-gdb-debugging {{ config }} {{ if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } }} + + @# Run Rust Crashdump tests + just test-rust-crashdump {{config}} {{ if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } }} + + @# Run Rust Tracing tests - linux + {{ if os() == "linux" { "just test-rust-tracing " + config + " " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }} + + @# Run benchmarks + {{ if config == "release" { "just bench-ci main " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }} + # runs all tests -test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration "rust" target features) (test-integration "c" target features) (test-seccomp target features) +test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration "rust" target features) (test-integration "c" target features) (test-doc target features) # runs unit tests test-unit target=default-target features="": - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} --lib + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib # runs tests that requires being run separately, for example due to global state -test-isolated target=default-target features="": - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_trace_trace --exact --ignored - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_log_trace --exact --ignored - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --lib -- sandbox::initialized_multi_use::tests::create_1000_sandboxes --exact --ignored - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --lib -- sandbox::outb::tests::test_log_outb_log --exact --ignored - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --lib -- mem::shared_mem::tests::test_drop --exact --ignored - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --test integration_test -- log_message --exact --ignored +test-isolated target=default-target features="" : + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_trace_trace --exact --ignored + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_log_trace --exact --ignored + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::initialized_multi_use::tests::create_1000_sandboxes --exact --ignored + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::outb::tests::test_log_outb_log --exact --ignored + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- mem::shared_mem::tests::test_drop --exact --ignored + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --test integration_test -- log_message --exact --ignored @# metrics tests - cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics,init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact + {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics,init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact # runs integration tests. Guest can either be "rust" or "c" test-integration guest target=default-target features="": @# run execute_on_heap test with feature "executable_heap" on and off - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} cargo test --profile={{ if target == "debug" { "dev" } else { target } }} --test integration_test execute_on_heap {{ if features =="" {" --features executable_heap"} else {"--features executable_heap," + features} }} -- --ignored - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} cargo test --profile={{ if target == "debug" { "dev" } else { target } }} --test integration_test execute_on_heap {{ if features =="" {""} else {"--features " + features} }} -- --ignored + {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {" --features executable_heap"} else {"--features executable_heap," + features} }} -- --ignored + {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap {{ if features =="" {""} else {"--features " + features} }} -- --ignored @# run the rest of the integration tests - {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} cargo test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} --test '*' - -# runs seccomp tests -test-seccomp target=default-target features="": - @# run seccomp test with feature "seccomp" on and off - cargo test --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host test_violate_seccomp_filters --lib {{ if features =="" {''} else { "--features " + features } }} -- --ignored - cargo test --profile={{ if target == "debug" { "dev" } else { target } }} -p hyperlight-host test_violate_seccomp_filters --no-default-features {{ if features =~"mshv3" {"--features init-paging,mshv3"} else {"--features mshv2,init-paging,kvm" } }} --lib -- --ignored - -# runs tests that ensure compilation fails when it should -test-compilation-fail target=default-target: - @# the following should fail on linux because one of kvm, mshv, or mshv3 feature must be specified, which is why the exit code is inverted with an !. - {{ if os() == "linux" { "! cargo check -p hyperlight-host --no-default-features 2> /dev/null"} else { "" } }} + {{if os() == "windows" { "$env:" } else { "" } }}GUEST="{{guest}}"{{if os() == "windows" { ";" } else { "" } }} {{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F init-paging," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' + +# tests compilation with no default features on different platforms +test-compilation-no-default-features target=default-target: + @# Linux should fail without a hypervisor feature (kvm, mshv, or mshv3) + {{ if os() == "linux" { "! " + cargo-cmd + " check -p hyperlight-host --no-default-features "+target-triple-flag+" 2> /dev/null" } else { "" } }} + @# Windows should succeed even without default features + {{ if os() == "windows" { cargo-cmd + " check -p hyperlight-host --no-default-features" } else { "" } }} + @# Linux should succeed with a hypervisor driver but without init-paging + {{ if os() == "linux" { cargo-cmd + " check -p hyperlight-host --no-default-features --features kvm" } else { "" } }} {{ target-triple-flag }} + {{ if os() == "linux" { cargo-cmd + " check -p hyperlight-host --no-default-features --features mshv2" } else { "" } }} {{ target-triple-flag }} + {{ if os() == "linux" { cargo-cmd + " check -p hyperlight-host --no-default-features --features mshv3" } else { "" } }} {{ target-triple-flag }} # runs tests that exercise gdb debugging test-rust-gdb-debugging target=default-target features="": - cargo test --profile={{ if target == "debug" { "dev" } else { target } }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} - cargo test --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- test_gdb + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- test_gdb # rust test for crashdump test-rust-crashdump target=default-target features="": - cargo test --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {'--features crashdump'} else { "--features crashdump," + features } }} -- test_crashdump + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} {{ if features =="" {'--features crashdump'} else { "--features crashdump," + features } }} -- test_crashdump + +# rust test for tracing +test-rust-tracing target=default-target features="": + # Run tests for the tracing guest and macro + {{ cargo-cmd }} test -p hyperlight-guest-tracing --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} + # Build the tracing guest to ensure it builds with the tracing feature + just build-rust-guests {{ target }} trace_guest + just move-rust-guests {{ target }} + + # Rebuild the tracing guests without the tracing feature + # This is to ensure that the tracing feature does not affect the other tests + just build-rust-guests {{ target }} + just move-rust-guests {{ target }} + +test-doc target=default-target features="": + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} {{ if features =="" {''} else { "--features " + features } }} --doc ################ ### LINTING #### ################ check: - cargo check + {{ cargo-cmd }} check {{ target-triple-flag }} + {{ cargo-cmd }} check -p hyperlight-host --features crashdump {{ target-triple-flag }} + {{ cargo-cmd }} check -p hyperlight-host --features print_debug {{ target-triple-flag }} + {{ cargo-cmd }} check -p hyperlight-host --features gdb {{ target-triple-flag }} + {{ cargo-cmd }} check -p hyperlight-host --features trace_guest,mem_profile {{ target-triple-flag }} fmt-check: cargo +nightly fmt --all -- --check - cargo +nightly fmt --manifest-path src/tests/rust_guests/callbackguest/Cargo.toml -- --check cargo +nightly fmt --manifest-path src/tests/rust_guests/simpleguest/Cargo.toml -- --check cargo +nightly fmt --manifest-path src/tests/rust_guests/dummyguest/Cargo.toml -- --check cargo +nightly fmt --manifest-path src/tests/rust_guests/witguest/Cargo.toml -- --check @@ -156,18 +230,16 @@ check-license-headers: fmt-apply: cargo +nightly fmt --all - cargo +nightly fmt --manifest-path src/tests/rust_guests/callbackguest/Cargo.toml cargo +nightly fmt --manifest-path src/tests/rust_guests/simpleguest/Cargo.toml cargo +nightly fmt --manifest-path src/tests/rust_guests/dummyguest/Cargo.toml cargo +nightly fmt --manifest-path src/tests/rust_guests/witguest/Cargo.toml cargo +nightly fmt --manifest-path src/hyperlight_guest_capi/Cargo.toml clippy target=default-target: (witguest-wit) - cargo clippy --all-targets --all-features --profile={{ if target == "debug" { "dev" } else { target } }} -- -D warnings + {{ cargo-cmd }} clippy --all-targets --all-features --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -- -D warnings clippy-guests target=default-target: (witguest-wit) cd src/tests/rust_guests/simpleguest && cargo clippy --profile={{ if target == "debug" { "dev" } else { target } }} -- -D warnings - cd src/tests/rust_guests/callbackguest && cargo clippy --profile={{ if target == "debug" { "dev" } else { target } }} -- -D warnings cd src/tests/rust_guests/witguest && cargo clippy --profile={{ if target == "debug" { "dev" } else { target } }} -- -D warnings clippy-apply-fix-unix: @@ -176,23 +248,39 @@ clippy-apply-fix-unix: clippy-apply-fix-windows: cargo clippy --target x86_64-pc-windows-msvc --fix --all +# Run clippy with feature combinations for all packages +clippy-exhaustive target=default-target: (witguest-wit) + ./hack/clippy-package-features.sh hyperlight-host {{ target }} {{ target-triple }} + ./hack/clippy-package-features.sh hyperlight-guest {{ target }} + ./hack/clippy-package-features.sh hyperlight-guest-bin {{ target }} + ./hack/clippy-package-features.sh hyperlight-common {{ target }} {{ target-triple }} + ./hack/clippy-package-features.sh hyperlight-testing {{ target }} {{ target-triple }} + ./hack/clippy-package-features.sh hyperlight-component-macro {{ target }} {{ target-triple }} + ./hack/clippy-package-features.sh hyperlight-component-util {{ target }} {{ target-triple }} + ./hack/clippy-package-features.sh hyperlight-guest-tracing {{ target }} + just clippy-guests {{ target }} + +# Test a specific package with all feature combinations +clippy-package package target=default-target: (witguest-wit) + ./hack/clippy-package-features.sh {{ package }} {{ target }} + # Verify Minimum Supported Rust Version verify-msrv: - ./dev/verify-msrv.sh hyperlight-host hyperlight-guest hyperlight-guest-lib hyperlight-common + ./dev/verify-msrv.sh hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-guest-tracing ##################### ### RUST EXAMPLES ### ##################### run-rust-examples target=default-target features="": - cargo run --profile={{ if target == "debug" { "dev" } else { target } }} --example metrics {{ if features =="" {''} else { "--features " + features } }} - cargo run --profile={{ if target == "debug" { "dev" } else { target } }} --example metrics {{ if features =="" {"--features function_call_metrics"} else {"--features function_call_metrics," + features} }} - cargo run --profile={{ if target == "debug" { "dev" } else { target } }} --example logging {{ if features =="" {''} else { "--features " + features } }} + {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example metrics {{ if features =="" {''} else { "--features " + features } }} + {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example metrics {{ if features =="" {"--features function_call_metrics"} else {"--features function_call_metrics," + features} }} + {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example logging {{ if features =="" {''} else { "--features " + features } }} # The two tracing examples are flaky on windows so we run them on linux only for now, need to figure out why as they run fine locally on windows run-rust-examples-linux target=default-target features="": (run-rust-examples target features) - cargo run --profile={{ if target == "debug" { "dev" } else { target } }} --example tracing {{ if features =="" {''} else { "--features " + features } }} - cargo run --profile={{ if target == "debug" { "dev" } else { target } }} --example tracing {{ if features =="" {"--features function_call_metrics" } else {"--features function_call_metrics," + features} }} + {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example tracing {{ if features =="" {''} else { "--features " + features } }} + {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example tracing {{ if features =="" {"--features function_call_metrics" } else {"--features function_call_metrics," + features} }} ######################### @@ -220,7 +308,7 @@ tar-static-lib: (build-rust-capi "release") (build-rust-capi "debug") # Downloads the benchmarks result from the given release tag. # If tag is not given, defaults to latest release # Options for os: "Windows", or "Linux" -# Options for Linux hypervisor: "kvm", "mshv" +# Options for Linux hypervisor: "kvm", "mshv", "mshv3" # Options for Windows hypervisor: "hyperv" # Options for cpu: "amd", "intel" bench-download os hypervisor cpu tag="": @@ -229,11 +317,13 @@ bench-download os hypervisor cpu tag="": tar -zxvf target/benchmarks_{{ os }}_{{ hypervisor }}_{{ cpu }}.tar.gz -C target/criterion/ --strip-components=1 # Warning: compares to and then OVERWRITES the given baseline -bench-ci baseline target=default-target features="": - cargo bench --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {''} else { "--features " + features } }} -- --verbose --save-baseline {{ baseline }} +bench-ci baseline features="": + @# Benchmarks are always run with release builds for meaningful results + cargo bench --profile=release {{ if features =="" {''} else { "--features " + features } }} -- --verbose --save-baseline {{ baseline }} -bench target=default-target features="": - cargo bench --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {''} else { "--features " + features } }} -- --verbose +bench features="": + @# Benchmarks are always run with release builds for meaningful results + cargo bench --profile=release {{ if features =="" {''} else { "--features " + features } }} -- --verbose ############### ### FUZZING ### @@ -263,7 +353,7 @@ build-fuzzer fuzz-target: ################### gen-all-fbs-rust-code: - for fbs in `find src -name "*.fbs"`; do flatc -r --rust-module-root-file --gen-all -o ./src/hyperlight_common/src/flatbuffers/ $fbs; done + flatc --rust --rust-module-root-file --gen-all -o ./src/hyperlight_common/src/flatbuffers/ ./src/schema/all.fbs just fmt-apply install-vcpkg: diff --git a/README.md b/README.md index 45f04b70b..185cc5fdc 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,14 @@ It is followed by an example of a simple guest application using the Hyperlight ### Host ```rust -use std::{thread, sync::{Arc, Mutex}}; +use std::thread; -use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnType}; -use hyperlight_host::{UninitializedSandbox, MultiUseSandbox, func::HostFunction0, sandbox_state::transition::Noop, sandbox_state::sandbox::EvolvableSandbox}; +use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; fn main() -> hyperlight_host::Result<()> { // Create an uninitialized sandbox with a guest binary let mut uninitialized_sandbox = UninitializedSandbox::new( - hyperlight_host::GuestBinary::FilePath(hyperlight_testing::simple_guest_as_string().unwrap()), + hyperlight_host::GuestBinary::FilePath("path/to/your/guest/binary".to_string()), None // default configuration )?; @@ -53,18 +52,16 @@ fn main() -> hyperlight_host::Result<()> { // Note: This function is unused by the guest code below, it's just here for demonstration purposes // Initialize sandbox to be able to call host functions - let mut multi_use_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve(Noop::default())?; + let mut multi_use_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve()?; // Call a function in the guest let message = "Hello, World! I am executing inside of a VM :)\n".to_string(); // in order to call a function it first must be defined in the guest and exposed so that // the host can call it - let result: i32 = multi_use_sandbox.call_guest_function_by_name( + multi_use_sandbox.call::( "PrintOutput", message, - ); - - assert!(result.is_ok()); + )?; Ok(()) } @@ -84,22 +81,21 @@ use hyperlight_common::flatbuffer_wrappers::function_types::{ ParameterType, ParameterValue, ReturnType, }; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; -use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result_from_int; +use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result; use hyperlight_guest::error::{HyperlightGuestError, Result}; use hyperlight_guest_bin::guest_function::definition::GuestFunctionDefinition; use hyperlight_guest_bin::guest_function::register::register_function; -use hyperlight_guest_bin::host_comm::{call_host_function, call_host_function_without_returning_result}; +use hyperlight_guest_bin::host_comm::call_host_function; fn print_output(function_call: &FunctionCall) -> Result> { if let ParameterValue::String(message) = function_call.parameters.clone().unwrap()[0].clone() { - call_host_function( + let result = call_host_function::( "HostPrint", Some(Vec::from(&[ParameterValue::String(message.to_string())])), ReturnType::Int, )?; - let result = get_host_value_return_as_int()?; - Ok(get_flatbuffer_result_from_int(result)) + Ok(get_flatbuffer_result(result)) } else { Err(HyperlightGuestError::new( ErrorCode::GuestFunctionParameterTypeMismatch, @@ -114,7 +110,7 @@ pub extern "C" fn hyperlight_main() { "PrintOutput".to_string(), Vec::from(&[ParameterType::String]), ReturnType::Int, - print_output as i64, + print_output as usize, ); register_function(print_output_def); } @@ -129,6 +125,28 @@ pub fn guest_dispatch_function(function_call: FunctionCall) -> Result> { } ``` +**Note**: Guest applications require a specific build configuration. Create a `.cargo/config.toml` file in your guest project with the following content: + +```toml +[build] +target = "x86_64-unknown-none" + +[target.x86_64-unknown-none] +rustflags = [ + "-C", + "code-model=small", + "-C", + "link-args=-e entrypoint", +] +linker = "rust-lld" + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" +``` + For additional examples of using the Hyperlight host Rust library, see the [./src/hyperlight_host/examples](./src/hyperlight_host/examples) directory. @@ -172,7 +190,7 @@ After having an environment with a hypervisor setup, running the example has the 1. On Linux or WSL, you'll most likely need build essential. For Ubuntu, run `sudo apt install build-essential`. For Azure Linux, run `sudo dnf install build-essential`. -2. [Rust](https://www.rust-lang.org/tools/install). Install toolchain v1.85 or later. +2. [Rust](https://www.rust-lang.org/tools/install). Install toolchain v1.89 or later. 3. [just](https://github.com/casey/just). `cargo install just` On Windows you also need [pwsh](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4). 4. [clang and LLVM](https://clang.llvm.org/get_started.html). - On Ubuntu, run: @@ -180,13 +198,9 @@ After having an environment with a hypervisor setup, running the example has the ```sh wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 17 all - sudo ln -s /usr/lib/llvm-17/bin/clang-cl /usr/bin/clang-cl - sudo ln -s /usr/lib/llvm-17/bin/llvm-lib /usr/bin/llvm-lib - sudo ln -s /usr/lib/llvm-17/bin/lld-link /usr/bin/lld-link - sudo ln -s /usr/lib/llvm-17/bin/llvm-ml /usr/bin/llvm-ml - sudo ln -s /usr/lib/llvm-17/bin/ld.lld /usr/bin/ld.lld - sudo ln -s /usr/lib/llvm-17/bin/clang /usr/bin/clang + sudo ./llvm.sh 18 clang clang-tools-extra + sudo ln -s /usr/lib/llvm-18/bin/ld.lld /usr/bin/ld.lld + sudo ln -s /usr/lib/llvm-18/bin/clang /usr/bin/clang ``` - On Windows, see [this](https://learn.microsoft.com/en-us/cpp/build/clang-support-msbuild?view=msvc-170). @@ -194,9 +208,10 @@ After having an environment with a hypervisor setup, running the example has the - On Azure Linux, run: ```sh - sudo dnf remove clang -y || true - sudo dnf install clang17 -y - sudo dnf install clang17-tools-extra -y + if ! command -v clang > /dev/null 2>&1; then + sudo dnf install clang -y + sudo dnf install clang-tools-extra -y + fi ``` Then, we are ready to build and run the example: diff --git a/c.just b/c.just index d83776cc5..aeed36808 100644 --- a/c.just +++ b/c.just @@ -16,14 +16,11 @@ build-rust-capi target=default-target: compile-c-guest target=default-target: # elf cd src/tests/c_guests/c_simpleguest && {{ mkdir }} "./out/{{target}}" && clang -c {{ c-compile-options-elf }} {{ if target == "debug" { c-flags-debug-elf } else { c-flags-release-elf } }} main.c {{c-include-flags-elf}} -o "out/{{ target }}/main.o" - cd src/tests/c_guests/c_callbackguest && {{ mkdir }} "./out/{{target}}" && clang -c {{ c-compile-options-elf }} {{ if target == "debug" { c-flags-debug-elf } else { c-flags-release-elf } }} main.c {{c-include-flags-elf}} -o "out/{{ target }}/main.o" link-c-guest target=default-target: # elf cd src/tests/c_guests/c_simpleguest && ld.lld -o out/{{target}}/simpleguest {{c-linker-options-elf}} out/{{target}}/main.o -l hyperlight_guest_capi -L "{{justfile_directory()}}/target/x86_64-unknown-none/{{target}}" - cd src/tests/c_guests/c_callbackguest && ld.lld -o out/{{target}}/callbackguest {{c-linker-options-elf}} out/{{target}}/main.o -l hyperlight_guest_capi -L "{{justfile_directory()}}/target/x86_64-unknown-none/{{target}}" move-c-guests target=default-target: # elf cp src/tests/c_guests/c_simpleguest/out/{{target}}/simpleguest src/tests/c_guests/bin/{{target}}/ - cp src/tests/c_guests/c_callbackguest/out/{{target}}/callbackguest src/tests/c_guests/bin/{{target}}/ diff --git a/dev/check-release-blockers.sh b/dev/check-release-blockers.sh new file mode 100755 index 000000000..cf7475c65 --- /dev/null +++ b/dev/check-release-blockers.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -e +set -u +set -o pipefail + +## DESCRIPTION: +## +## This script checks for open issues with the 'release-blocker' label +## in a given GitHub repository. It exits with code 1 if any blocking +## issues are found, or 0 if none are found. +## +## PRE-REQS: +## +## This script assumes that the gh cli is installed and in the PATH +## and that there is a GitHub PAT in the GITHUB_TOKEN env var +## with the following permissions: +## - repo (read) +## - issues (read) +## or that the user is logged into the gh cli with an account with those permissions + + +# Check if repository argument is provided +if [ -z "${1:-}" ]; then + echo "Error: Repository name not provided." + echo "Usage: $0 " + echo "Example: $0 hyperlight-dev/hyperlight" + exit 1 +fi + +REPO="$1" +echo "Checking for open issues with 'release-blocker' label in $REPO..." + +# Extract owner and repo name from the argument +OWNER=$(echo "$REPO" | cut -d'/' -f1) +REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) + +# Get all open issues with release-blocker label +BLOCKING_ISSUES=$(gh api graphql -f query=' + query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + issues(first: 100, states: OPEN, labels: ["release-blocker"]) { + totalCount + nodes { + number + title + url + } + } + } + }' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository.issues') + +BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq '.totalCount') + +if [ "$BLOCKER_COUNT" -gt 0 ]; then + echo "❌ Found $BLOCKER_COUNT open release-blocking issue(s):" + echo "$BLOCKING_ISSUES" | jq -r '.nodes[] | " - #\(.number): \(.title) (\(.url))"' + echo "" + echo "Release blocked by open issue(s) with 'release-blocker' label" + exit 1 +else + echo "✅ No open release blocking issues found" + exit 0 +fi diff --git a/dev/notify-ci-failure.sh b/dev/notify-ci-failure.sh new file mode 100755 index 000000000..69be564c1 --- /dev/null +++ b/dev/notify-ci-failure.sh @@ -0,0 +1,169 @@ +#!/bin/bash +set -e +set -u +set -o pipefail + +## DESCRIPTION: +## +# Generic notifier for CI job failures that can create or update GitHub issues. +## This script creates or updates GitHub issues when a jobs fail. +## It checks for existing open failure issues and either creates +## a new one or adds a comment to an existing one. +## +## PRE-REQS: +## +## This script assumes that the gh cli is installed and in the PATH +## and that there is a GitHub PAT in the GITHUB_TOKEN env var +## with the following permissions: +## - issues (read/write) +## or that the user is logged into the gh cli with an account with those permissions +## +## Usage examples: +## ./dev/notify-fuzzing-failure.sh +## ./dev/notify-fuzzing-failure.sh --title="Nightly Failure" --labels="area/testing,kind/bug" +## ./dev/notify-fuzzing-failure.sh --test +## Run this script locally like: +## GITHUB_REPOSITORY="fork/hyperlight" GITHUB_RUN_ID=1 ./dev/notify-fuzzing-failure.sh --title="Nightly Failure" --labels="area/testing,kind/bug" + +REPO="${GITHUB_REPOSITORY:-hyperlight-dev/hyperlight}" +WORKFLOW_RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/actions/runs/${GITHUB_RUN_ID:-unknown}" +TEST_MODE=false +ISSUE_TITLE="" +LABELS="area/testing,kind/bug,area/fuzzing,lifecycle/needs-review" + +for arg in "$@"; do + case $arg in + --test) + TEST_MODE=true + shift + ;; + --title=*) + ISSUE_TITLE="${arg#*=}" + shift + ;; + --labels=*) + LABELS="${arg#*=}" + shift + ;; + *) + esac +done + +# Normalize labels into an array +IFS=',' read -r -a LABEL_ARRAY <<< "$LABELS" + +# Choose a label to search existing issues for; prefer the first label if present +SEARCH_LABEL="${LABEL_ARRAY[0]:-area/fuzzing}" + +# Build issue title if not provided +if [ -z "$ISSUE_TITLE" ]; then + ISSUE_TITLE="Job Failure - $(date '+%Y-%m-%d')" +fi + + +if [ "$TEST_MODE" = true ]; then + echo "✅ Running in test mode - script structure is valid" + echo "Would check for issues in $REPO" + echo "Workflow URL would be: $WORKFLOW_RUN_URL" + echo "Issue Title would be: $ISSUE_TITLE" + echo "Labels would be: $LABELS" + echo "Search Label would be: $SEARCH_LABEL" + exit 0 +fi + +# Extract owner and repo name from the repository +OWNER=$(echo "$REPO" | cut -d'/' -f1) +REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) + +echo "Checking for existing issues in $REPO with label '$SEARCH_LABEL'..." +EXISTING_ISSUES=$(gh api graphql -f query=' + query($owner: String!, $repo: String!, $label: String!) { + repository(owner: $owner, name: $repo) { + issues(first: 10, states: OPEN, labels: [$label]) { + totalCount + nodes { + number + title + url + labels(first: 20) { + nodes { + name + } + } + } + } + } + }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$SEARCH_LABEL" --jq '.data.repository.issues') || EXISTING_ISSUES="" + +FUZZING_ISSUES=$(echo "$EXISTING_ISSUES" | jq '.nodes[]' 2>/dev/null || echo "") +FUZZING_ISSUE_COUNT=0 +if [ -n "$FUZZING_ISSUES" ]; then + FUZZING_ISSUE_COUNT=$(echo "$FUZZING_ISSUES" | jq -s 'length' 2>/dev/null || echo "0") +fi + +echo "Found $FUZZING_ISSUE_COUNT existing issue(s) matching label '$SEARCH_LABEL'" + +if [ "$FUZZING_ISSUE_COUNT" -gt 0 ]; then + ISSUE_NUMBER=$(echo "$FUZZING_ISSUES" | jq -r '.number' | head -1) + ISSUE_URL=$(echo "$FUZZING_ISSUES" | jq -r '.url' | head -1) + if [ "$ISSUE_NUMBER" = "null" ] || [ -z "$ISSUE_NUMBER" ]; then + echo "âš ī¸ Could not parse issue number from search results; will create a new issue" + FUZZING_ISSUE_COUNT=0 + else + echo "Adding comment to existing issue #$ISSUE_NUMBER" + COMMENT_BODY="## Job Failed Again + +**Date:** $(date '+%Y-%m-%d %H:%M:%S UTC') +**Workflow Run:** [$WORKFLOW_RUN_URL]($WORKFLOW_RUN_URL) + +The scheduled job has failed again. Please check the workflow logs and artifacts for details." + + if gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY" --repo "$REPO"; then + echo "✅ Added comment to existing issue #$ISSUE_NUMBER: $ISSUE_URL" + exit 0 + else + echo "❌ Failed to add comment to existing issue. Will attempt to create a new issue instead." + FUZZING_ISSUE_COUNT=0 + fi + fi +fi + +if [ "$FUZZING_ISSUE_COUNT" -eq 0 ]; then + echo "No existing matching issues found. Creating a new issue..." + + ISSUE_BODY="## Job Failure Report + +**Date:** $(date '+%Y-%m-%d %H:%M:%S UTC') +**Workflow Run:** [$WORKFLOW_RUN_URL]($WORKFLOW_RUN_URL) + +### Details +The scheduled job failed during execution. This issue was automatically created to track the failure. Please check the workflow logs and any uploaded artifacts for more details. + +### Next Steps +- [ ] Review the workflow logs for error details +- [ ] Download and analyze any crash artifacts if available +- [ ] Determine the root cause of the failure +- [ ] Fix the underlying issue + +--- +*This issue was automatically created by the CI failure notification system.*" + + # Build label args for gh issue create + LABEL_ARGS=() + for lbl in "${LABEL_ARRAY[@]}"; do + LABEL_ARGS+=("--label" "$lbl") + done + + if ISSUE_URL=$(gh issue create \ + --title "$ISSUE_TITLE" \ + --body "$ISSUE_BODY" \ + "${LABEL_ARGS[@]}" \ + --repo "$REPO"); then + echo "✅ Created new issue: $ISSUE_URL" + else + echo "❌ Failed to create new issue" + exit 1 + fi +fi + +echo "Notification script completed successfully" \ No newline at end of file diff --git a/dev/verify-msrv.sh b/dev/verify-msrv.sh index 1b697fd13..acaf67cce 100755 --- a/dev/verify-msrv.sh +++ b/dev/verify-msrv.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -Eeuo pipefail cargo install -q jaq for CRATE in "$@"; do diff --git a/docs/README.md b/docs/README.md index fff51c662..3b36c5db1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,7 +18,7 @@ Given a guest, then, Hyperlight takes some simple steps prior to executing it, i ## Basics: Hyperlight architecture -This project is composed internally of several internal components, depicted in the below diagram: +This project is composed internally of several components, depicted in the below diagram: ![Hyperlight architecture](./assets/hyperlight_arch.png) diff --git a/docs/benchmarking-hyperlight.md b/docs/benchmarking-hyperlight.md index 3ae56d55c..5ac2662f4 100644 --- a/docs/benchmarking-hyperlight.md +++ b/docs/benchmarking-hyperlight.md @@ -72,4 +72,6 @@ Found 1 outliers among 100 measurements (1.00%) ## Running benchmarks locally -Use `just bench [debug/release]` parameter to run benchmarks. Comparing local benchmarks results to github-saved benchmarks doesn't make much sense, since you'd be using different hardware, but you can use `just bench-download os hypervisor [tag] ` to download and extract the GitHub release benchmarks to the correct place folder. You can then run `just bench-ci main` to compare to (and overwrite) the previous release benchmarks. Note that `main` is the name of the baselines stored in GitHub. +Use `just bench` to run benchmarks with release builds (the only supported configuration). Comparing local benchmark results to github-saved benchmarks doesn't make much sense, since you'd be using different hardware, but you can use `just bench-download os hypervisor [tag] ` to download and extract the GitHub release benchmarks to the correct place folder. You can then run `just bench-ci main` to compare to (and overwrite) the previous release benchmarks. Note that `main` is the name of the baselines stored in GitHub. + +**Important**: The `just bench` command uses release builds by default to ensure meaningful performance measurements. For profiling purposes, you can compile benchmarks with debug symbols by running `cargo bench` directly. diff --git a/docs/github-labels.md b/docs/github-labels.md index 888b0280d..5133f048a 100644 --- a/docs/github-labels.md +++ b/docs/github-labels.md @@ -39,6 +39,7 @@ In addition to lifecycle labels, we use the following labels to further categori - **good-first-issue** - The issue is suitable for new contributors or those looking for a simple task to start with. - **help-wanted** - The issue is a request for help or assistance. - **question** - The issue is a question or request for information. +- **release-blocker** - Critical issues that must be resolved before the next release can be made. The presence of this label on any open issue will prevent releases being created by the release workflow --- @@ -56,4 +57,4 @@ In addition to **kind/*** labels, we use optional **area/*** labels to specify t ## Notes -This document is a work in progress and may be updated as needed. The labels and categories are subject to change based on the evolving needs of the project and community feedback. \ No newline at end of file +This document is a work in progress and may be updated as needed. The labels and categories are subject to change based on the evolving needs of the project and community feedback. diff --git a/docs/glossary.md b/docs/glossary.md index eca0459dd..f5201ffa6 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -54,7 +54,7 @@ The interface that a guest must implement is specific to the associated [host](# A micro virtual machine is an execution environment managed by a hypervisor that isolates a [guest](#guest) from the [host](#host). A hypervisor prevents the guest from directly accessing the host's resources, such as memory, filesystem, devices, memory or CPU. -We use the term Micro Virtual Machine as the VMs are very lightweight compared to traditional VMs, they contains no operating system or other unnecessary components. The goal is to provide a minimal environment for executing workloads with low latency and high density. However the isolation provided by the hypervisor is the same as that of a traditional VM. +We use the term Micro Virtual Machine as the VMs are very lightweight compared to traditional VMs, containing no operating system or other unnecessary components. The goal is to provide a minimal environment for executing workloads with low latency and high density. However the isolation provided by the hypervisor is the same as that of a traditional VM. ## Workload diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index 0cf2e0753..f4b45e996 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -207,13 +207,12 @@ involved in the gdb debugging of a Hyperlight guest running inside a **KVM** or | └───────────────────────────────────────────────────────────────────────────────────────────────┘ ``` -## Dumping the guest state to an ELF core dump when an unhandled crash occurs +## Dumping the guest state to an ELF core dump -When a guest crashes because of an unknown VmExit or unhandled exception, the vCPU state is dumped to an `ELF` core dump file. +When a guest crashes because of an unknown VmExit or unhandled exception, the vCPU state can be optionally dumped to an `ELF` core dump file. This can be used to inspect the state of the guest at the time of the crash. -To make Hyperlight dump the state of the vCPU (general purpose registers, registers) to an `ELF` core dump file, enable the `crashdump` -feature and run. +To make Hyperlight dump the state of the vCPU (general purpose registers, registers) to an `ELF` core dump file, enable the `crashdump` feature and run. The feature enables the creation of core dump files for both debug and release builds of Hyperlight hosts. By default, Hyperlight places the core dumps in the temporary directory (platform specific). To change this, use the `HYPERLIGHT_CORE_DUMP_DIR` environment variable to specify a directory. @@ -227,6 +226,39 @@ To selectively disable this feature for a specific sandbox, you can set the `gue cfg.set_guest_core_dump(false); // Disable core dump for this sandbox ``` +## Creating a dump on demand + +You can also create a core dump of the current state of the guest on demand by calling the `generate_crashdump` method on the `InitializedMultiUseSandbox` instance. This can be useful for debugging issues in the guest that do not cause crashes (e.g., a guest function that does not return). + +This is only available when the `crashdump` feature is enabled and then only if the sandbox +is also configured to allow core dumps (which is the default behavior). + +### Example + +Attach to your running process with gdb and call this function: + +```shell +sudo gdb -p +(gdb) info threads +# find the thread that is running the guest function you want to debug +(gdb) thread +# switch to the frame where you have access to your MultiUseSandbox instance +(gdb) backtrace +(gdb) frame +# get the pointer to your MultiUseSandbox instance +# Get the sandbox pointer +(gdb) print sandbox +# Call the crashdump function with the pointer + # Call the crashdump function +call sandbox.generate_crashdump() +``` +The crashdump should be available `/tmp` or in the crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var). To make this process easier, you can also create a gdb script that automates these steps. You can find an example script [here](../scripts/dump_all_sandboxes.gdb). This script will try and generate a crashdump for every active thread except thread 1 , it assumes that the variable sandbox exists in frame 15 on every thread. You can edit it to fit your needs. Then use it like this: + +```shell +(gdb) source scripts/dump_all_sandboxes.gdb +(gdb) dump_all_sandboxes +``` + ### Inspecting the core dump After the core dump has been created, to inspect the state of the guest, load the core dump file using `gdb` or `lldb`. diff --git a/docs/how-to-use-flatbuffers.md b/docs/how-to-use-flatbuffers.md index dc26b58c7..c541b0406 100644 --- a/docs/how-to-use-flatbuffers.md +++ b/docs/how-to-use-flatbuffers.md @@ -1,6 +1,6 @@ # How to use FlatBuffers -> Note: the last generation of the flatbuffer code was with done with flatc version 25.2.10 (i.e., the last version as of May 1st, 2025). +> Note: the last generation of the flatbuffer code was with done with flatc version 25.9.23 (i.e., the last version as of Oct 2nd, 2025). Flatbuffers is used to serialize and deserialize some data structures. @@ -19,9 +19,3 @@ We recommend building `flatc` from source. To generate rust code, use ```console just gen-all-fbs-rust-code ``` - -### Note about generated code - -Because we invoke `flatc` multiple times when generating the Rust code, the `mod.rs` generated in `./src/hyperlight_common/src/flatbuffers` is overwritten multiple times and will likely be incorrect. Make sure to manually inspect and if necessary update this file before continuing with your changes as certain modules might be missing. After fixing `mod.rs`, you might need to re-run `just fmt`, since it might not have applied to all generated files if your `mod.rs` was invalid. - ->`flatc` does support passing multiple schema files (e.g. it is possible to pass `.\src\schema\*.fbs`), so we could regenerate all the files each time a change was made, however that generates incorrect code (see [here](https://github.com/google/flatbuffers/issues/6800) for details). diff --git a/docs/hyperlight-metrics-logs-and-traces.md b/docs/hyperlight-metrics-logs-and-traces.md index 3dc62438d..3e04f576a 100644 --- a/docs/hyperlight-metrics-logs-and-traces.md +++ b/docs/hyperlight-metrics-logs-and-traces.md @@ -81,13 +81,124 @@ The sample will run and generate trace data until any key is pressed. To view the trace data, leave the example running and use the jaegertracing/all-in-one container image with the following command: ```console - docker run -d --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 4317:4317 -p 16686:16686 jaegertracing/all-in-one:1.60 + docker run -p 16686:16686 -p 4317:4317 -p 4318:4318 -e COLLECTOR_OTLP_ENABLED=true jaegertracing/all-in-one:latest ``` NOTE: when running this on windows that this is a linux container, so you will need to ensure that docker is configured to run linux containers using WSL2. Alternatively, you can download the Jaeger binaries from [here](https://www.jaegertracing.io/download/). Extract the archive and run the `jaeger-all-in-one` executable as follows: ```powershell -.\jaeger-all-in-one.exe --collector.otlp.grpc.host-port=4317 +.\jaeger-all-in-one.exe ``` Once the container or the exe is running, the trace output can be viewed in the jaeger UI at [http://localhost:16686/search](http://localhost:16686/search). + +## Guest Tracing, Unwinding, and Memory Profiling + +Hyperlight provides advanced observability features for guest code running inside micro virtual machines. You can enable guest-side tracing, stack unwinding, and memory profiling using the `trace_guest` and `mem_profile` features. This section explains how to build, run, and inspect guest traces. + +The following features are available for guest tracing: +- `trace_guest`: Enables tracing for guest code, capturing function calls and execution time. +- `mem_profile`: Enables memory profiling for guest code with stack unwinding, capturing memory allocations and usage. + +### Building a Guest with Tracing Support + +To build a guest with tracing enabled, use the following commands: + +```bash +just build-rust-guests debug trace_guest +just move-rust-guests debug +``` + +This builds the guest binaries with the `trace_guest` feature enabled and move them to the appropriate location for use by the host. + +**NOTE**: To enable the tracing in your application you need to use the `trace_guest` feature on the `hyperlight-guest-bin` and `hyperlight-guest` crates. + +### Running a Hyperlight Example with Guest Tracing + +Once the guest is built, you can run a Hyperlight example with guest tracing enabled. For example: + +```bash +RUST_LOG="info,hyperlight_host::sandbox=info,hyperlight_guest=trace,hyperlight_guest_bin=trace" cargo run --example tracing-otlp --features trace_guest +``` + +This will execute the `tracing-otlp` example, loading the guest with tracing enabled. +During execution, trace data will be collected on the host and exported as `opentelemetry` spans/events. + +You can set up a collector to gather all the traces and inspect the traces from both host and guests. + +Due to the nature of execution inside a Sandbox, on a call basis, the guest tracing sets up a stack of spans to keep track of the correct parents for the incoming +guest spans. +We start with `call-to-guest` which contains all the spans coming from a guest. Additionally, for each exit into the host, we add another layer marking it with +a `call-to-host` span to follow the execution in the host and correctly set it as a child of the active span in the guest. +This logic simulates the propagation of `Opentelemetry` context that is usually done between two services, but cannot be done here seamlessly because the guest side +runs `no_std` which `opentelemetry` doesn't know. + +#### How it works + +##### Guest + +When the guest starts executing the `entrypoint` function, it receives a `max_log_level` parameter that tells the guest what kind of logging level is expected from it. + +The `trace_guest` logic takes advantage of this parameter and when the `max_log_level` is `trace`, it allocates a custom made `GuestSubscriber` that implements the `Subscriber` +trait from `tracing_core` that allows defining a subscriber for the `tracing` crate to handle new spans and events. + +This custom subscriber stores the spans and events in a buffer initialized only when tracing is enabled. For each new span and event, a method is called on the custom subscriber which not only stores the data, but also keeps track of the hierarchy and dependencies between the other spans/events. +**NOTE**: The spans/events attributes are truncated to fit in the allocated buffer. + +When the storage space is filled, the guest triggers a VM Exit that sends the guest pointers to the host. The host can access the guest memory, get the data and parse it to create the `spans` and `events` using the `opentelemetry` crate which allows specifying the starting and ending timestamps +which are captured in the guest using the `TSC`. + +To improve performance, for each VMExit, the guest adds metadata for the host to be able to report the tracing data and free space. + +##### Host + +When a guest exits, the host checks for metadata from the guest reporting tracing data. +If tracing data is found, the host starts parsing it and reconstructing a tree which represents the spans hierarchy. + +Additionally, the host also adds new children `span`s to the guest's reported active span, emphasizing the spans created on the host as a result of a temporary VM Exit. This helps visualize a call into the guest with context propagated across the VM boundary. + +The host creates `opentelemetry` spans and events for each guest span and event reported. + +### Inspecting Guest memory Trace Files (for mem_profile) + +To inspect the trace file generated by the guest, use the `trace_dump` crate. You will need the path to the guest symbols and the trace file. Run the following command: + +```bash +cargo run -p trace_dump list_frames +``` + +Replace `` with the path to the guest binary or symbol file, and `` with the path to the trace file in the `trace` directory. + +This command will list the stack frames and tracing information captured during guest execution, allowing you to analyze guest behavior, stack traces, and memory usage. + +#### Example + +```bash +cargo run -p trace_dump ./src/tests/rust_guests/bin/debug/simpleguest ./trace/.trace list_frames +``` + +You can use the `mem_profile` additional feature by enabling them during the build and run steps. + +> **Note:** Make sure to follow the build and run steps in order, and ensure that the guest binaries are up to date before running the host example. + +## System Prerequisites for `trace_dump` + +To build and use the `trace_dump` crate and related guest tracing features, you must have the following system libraries and development tools installed on your system: + +- **glib-2.0** development files + - Fedora/RHEL/CentOS: + ```bash + sudo dnf install glib2-devel pkgconf-pkg-config + ``` +- **cairo** and **cairo-gobject** development files + - Fedora/RHEL/CentOS: + ```bash + sudo dnf install cairo-devel cairo-gobject-devel + ``` +- **pango** development files + - Fedora/RHEL/CentOS: + ```bash + sudo dnf install pango-devel + ``` + +These libraries are required by Rust crates such as `glib-sys`, `cairo-sys-rs`, and `pango-sys`, which are dependencies of the tracing and visualization tools. If you encounter errors about missing `.pc` files (e.g., `glib-2.0.pc`, `cairo.pc`, `pango.pc`), ensure the corresponding `-devel` packages are installed. diff --git a/docs/paging-development-notes.md b/docs/paging-development-notes.md index ae63f1a18..e1f4d3c0b 100644 --- a/docs/paging-development-notes.md +++ b/docs/paging-development-notes.md @@ -1,6 +1,6 @@ # Paging in Hyperlight -Hyperlight uses paging, which means the all addresses inside a Hyperlight VM are treated as virtual addresses by the processor. Specifically, Hyperlight uses (ordinary) 4-level paging. 4-level paging is used because we set the following control registers on logical cores inside a VM: `CR0.PG = 1, CR4.PAE = 1, IA32_EFER.LME = 1, and CR4.LA57 = 0`. A Hyperlight VM is limited to 1GB of addressable memory, see below for more details. These control register settings have the following effects: +Hyperlight uses paging, which means that all addresses inside a Hyperlight VM are treated as virtual addresses by the processor. Specifically, Hyperlight uses (ordinary) 4-level paging. 4-level paging is used because we set the following control registers on logical cores inside a VM: `CR0.PG = 1, CR4.PAE = 1, IA32_EFER.LME = 1, and CR4.LA57 = 0`. A Hyperlight VM is limited to 1GB of addressable memory, see below for more details. These control register settings have the following effects: - `CR0.PG = 1`: Enables paging - `CR4.PAE = 1`: Enables Physical Address Extension (PAE) mode (this is required for 4-level paging) diff --git a/docs/security.md b/docs/security.md index 10f96bd46..3ee86780c 100644 --- a/docs/security.md +++ b/docs/security.md @@ -19,5 +19,3 @@ All communication between the host and the guest is done through a shared memory Hyperlight provides a mechanism for the host to register functions that may be called from the guest. This mechanism is useful to allow developers to provide guests with strictly controlled access to functionality we don't make available by default inside the VM. This mechanism likely represents the largest attack surface area of this project. To mitigate the risk, only functions that have been explicitly exposed to the guest by the host application, are allowed to be called from the guest. Any attempt to call other host functions will result in an error. - -Additionally, we provide an API for using Seccomp filters to further restrict the system calls available to the host-provided functions, to help limit the impact of the un-audited or un-managed functions. diff --git a/docs/signal-handlers-development-notes.md b/docs/signal-handlers-development-notes.md index fca9d31a9..3cde0dafc 100644 --- a/docs/signal-handlers-development-notes.md +++ b/docs/signal-handlers-development-notes.md @@ -1,17 +1,9 @@ # Signal Handling in Hyperlight -Hyperlight registers custom signal handlers to intercept and manage specific signals, primarily `SIGSYS` and `SIGRTMIN`. Here's an overview of the registration process: -- **Preserving Old Handlers**: When registering a new signal handler, Hyperlight first retrieves and stores the existing handler using `OnceCell`. This allows Hyperlight to delegate signals to the original handler if necessary. +Hyperlight registers custom signal handlers to intercept and manage specific signals, primarily `SIGRTMIN`. Here's an overview of the registration process: - **Custom Handlers**: - - **`SIGSYS` Handler**: Captures disallowed syscalls enforced by seccomp. If the signal originates from a hyperlight thread, Hyperlight logs the syscall details. Otherwise, it delegates the signal to the previously registered handler. - - **`SIGRTMIN` Handler**: Utilized for inter-thread signaling, such as execution cancellation. Similar to SIGSYS, it distinguishes between application and non-hyperlight threads to determine how to handle the signal. -- **Thread Differentiation**: Hyperlight uses thread-local storage (IS_HYPERLIGHT_THREAD) to identify whether the current thread is a hyperlight thread. This distinction ensures that signals are handled appropriately based on the thread's role. - -## Potential Issues and Considerations - -### Handler Invalidation - -**Issue**: After Hyperlight registers its custom signal handler and preserves the `old_handler`, if the host or another component modifies the signal handler for the same signal, it can lead to: - - **Invalidation of `old_handler`**: The stored old_handler reference may no longer point to a valid handler, causing undefined behavior when Hyperlight attempts to delegate signals. - - **Loss of Custom Handling**: Hyperlight's custom handler might not be invoked as expected, disrupting its ability to enforce syscall restrictions or manage inter-thread signals. - + - **`SIGRTMIN` Handler**: Utilized for inter-thread signaling, such as execution cancellation. +- **Killing a sandbox**: + - To stop a sandboxed process, a `SIGRTMIN` signal must be delivered to the thread running the sandboxed code. + - The sandbox provides an interface to obtain an interrupt handle, which includes the thread ID and a method to dispatch the signal. + - Hyperlight uses the `pthread_kill` function to send this signal directly to the targeted thread. diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..4efc77506 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1752950548, + "narHash": "sha256-NS6BLD0lxOrnCiEOcvQCDVPXafX1/ek1dfJHX1nUIzc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c87b95e25065c028d31a94f06a62927d18763fdf", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-mozilla": { + "locked": { + "lastModified": 1744624473, + "narHash": "sha256-S6zT/w5SyAkJ//dYdjbrXgm+6Vkd/k7qqUl4WgZ6jjk=", + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "rev": "2292d4b35aa854e312ad2e95c4bb5c293656f21a", + "type": "github" + }, + "original": { + "owner": "mozilla", + "ref": "master", + "repo": "nixpkgs-mozilla", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "nixpkgs-mozilla": "nixpkgs-mozilla" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..d700857ac --- /dev/null +++ b/flake.nix @@ -0,0 +1,150 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs.nixpkgs-mozilla.url = "github:mozilla/nixpkgs-mozilla/master"; + outputs = { self, nixpkgs, nixpkgs-mozilla, ... } @ inputs: + { + devShells.x86_64-linux.default = + let pkgs = import nixpkgs { + system = "x86_64-linux"; + overlays = [ (import (nixpkgs-mozilla + "/rust-overlay.nix")) ]; + }; + in with pkgs; let + # Work around the nixpkgs-mozilla equivalent of + # https://github.com/NixOS/nixpkgs/issues/278508 and an + # incompatibility between nixpkgs-mozilla and makeRustPlatform + rustChannelOf = args: let + orig = pkgs.rustChannelOf args; + patchRustPkg = pkg: (pkg.overrideAttrs (oA: { + buildCommand = builtins.replaceStrings + [ "rustc,rustdoc" ] + [ "rustc,rustdoc,clippy-driver,cargo-clippy" ] + oA.buildCommand; + })) // { + targetPlatforms = [ "x86_64-linux" ]; + badTargetPlatforms = [ ]; + }; + overrideRustPkg = pkg: lib.makeOverridable (origArgs: + patchRustPkg (pkg.override origArgs) + ) {}; + in builtins.mapAttrs (_: overrideRustPkg) orig; + + customisedRustChannelOf = args: + lib.flip builtins.mapAttrs (rustChannelOf args) (_: pkg: pkg.override { + targets = [ + "x86_64-unknown-linux-gnu" + "x86_64-pc-windows-msvc" "x86_64-unknown-none" + "wasm32-wasip1" "wasm32-wasip2" "wasm32-unknown-unknown" + ]; + extensions = [ "rust-src" ]; + }); + + # Hyperlight needs a variety of toolchains, since we use Nightly + # for rustfmt and old toolchains to verify MSRV + toolchains = lib.mapAttrs (_: customisedRustChannelOf) { + stable = { + # Stay on 1.87 for development due to the + # quickly-reversed default enablement of + # #[warn(clippy::uninlined_format_args)] + date = "2025-05-15"; + channel = "stable"; + sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU="; + }; + nightly = { + date = "2025-07-29"; + channel = "nightly"; + sha256 = "sha256-6D2b7glWC3jpbIGCq6Ta59lGCKN9sTexhgixH4Y7Nng="; + }; + "1.85" = { + date = "2025-02-20"; + channel = "stable"; + sha256 = "sha256-AJ6LX/Q/Er9kS15bn9iflkUwcgYqRQxiOIL2ToVAXaU="; + }; + "1.86" = { + date = "2025-04-03"; + channel = "stable"; + sha256 = "sha256-X/4ZBHO3iW0fOenQ3foEvscgAPJYl2abspaBThDOukI="; + }; + }; + + rust-platform = makeRustPlatform { + cargo = toolchains.stable.rust; + rustc = toolchains.stable.rust; + }; + + # Hyperlight scripts use cargo in a bunch of ways that don't + # make sense for Nix cargo, including the `rustup +toolchain` + # syntax to use a specific toolchain and `cargo install`, so we + # build wrappers for rustc and cargo that enable this. The + # scripts also use `rustup toolchain install` in some cases, in + # order to work in CI, so we provide a fake rustup that does + # nothing as well. + rustup-like-wrapper = name: pkgs.writeShellScriptBin name + (let + clause = name: toolchain: + "+${name}) base=\"${toolchain.rust}\"; shift 1; ;;"; + clauses = lib.strings.concatStringsSep "\n" + (lib.mapAttrsToList clause toolchains); + in '' + base="${toolchains.stable.rust}" + case "$1" in + ${clauses} + install) exit 0; ;; + esac + export PATH="$base/bin:$PATH" + exec "$base/bin/${name}" "$@" + ''); + fake-rustup = pkgs.symlinkJoin { + name = "fake-rustup"; + paths = [ + (pkgs.writeShellScriptBin "rustup" "") + (rustup-like-wrapper "rustc") + (rustup-like-wrapper "cargo") + ]; + }; + + buildRustPackageClang = rust-platform.buildRustPackage.override { stdenv = clangStdenv; }; + in (buildRustPackageClang rec { + pname = "hyperlight"; + version = "0.0.0"; + src = lib.cleanSource ./.; + cargoHash = "sha256-hoeJEBdxaoyLlhQQ4X4Wk5X1QVtQ7RRQYaxkiGg8rWA="; + + nativeBuildInputs = [ + azure-cli + just + dotnet-sdk_9 + llvmPackages_18.llvm + gh + lld + valgrind + pkg-config + ffmpeg + mkvtoolnix + wasm-tools + jq + jaq + gdb + ]; + buildInputs = [ + pango + cairo + openssl + ]; + + auditable = false; + + LIBCLANG_PATH = "${pkgs.llvmPackages_18.libclang.lib}/lib"; + # Use unwrapped clang for compiling guests + HYPERLIGHT_GUEST_clang = "${clang.cc}/bin/clang"; + + RUST_NIGHTLY = "${toolchains.nightly.rust}"; + # Set this through shellHook rather than nativeBuildInputs to be + # really sure that it overrides the real cargo. + shellHook = '' + export PATH="${fake-rustup}/bin:$PATH" + ''; + }).overrideAttrs(oA: { + hardeningDisable = [ "all" ]; + }); + }; +} diff --git a/fuzz/fuzz_targets/guest_call.rs b/fuzz/fuzz_targets/guest_call.rs index 2e0fcee00..bc0ff163f 100644 --- a/fuzz/fuzz_targets/guest_call.rs +++ b/fuzz/fuzz_targets/guest_call.rs @@ -20,8 +20,6 @@ use std::sync::{Mutex, OnceLock}; use hyperlight_host::func::{ParameterValue, ReturnType}; use hyperlight_host::sandbox::uninitialized::GuestBinary; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simple_guest_for_fuzzing_as_string; use libfuzzer_sys::fuzz_target; @@ -38,7 +36,7 @@ fuzz_target!( ) .unwrap(); - let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); + let mu_sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); SANDBOX.set(Mutex::new(mu_sbox)).unwrap(); }, diff --git a/fuzz/fuzz_targets/host_call.rs b/fuzz/fuzz_targets/host_call.rs index bdd8ad760..2bf241673 100644 --- a/fuzz/fuzz_targets/host_call.rs +++ b/fuzz/fuzz_targets/host_call.rs @@ -19,38 +19,56 @@ limitations under the License. use std::sync::{Mutex, OnceLock}; use hyperlight_host::func::{ParameterValue, ReturnType}; +use hyperlight_host::sandbox::SandboxConfiguration; +use hyperlight_host::sandbox::snapshot::Snapshot; use hyperlight_host::sandbox::uninitialized::GuestBinary; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{HyperlightError, MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simple_guest_for_fuzzing_as_string; use libfuzzer_sys::fuzz_target; + +// TODO: this SNAPSHOT is needed because of the memory leak in: https://github.com/hyperlight-dev/hyperlight/issues/826 +// This should be removed once the leak is fixed +static SNAPSHOT: OnceLock> = OnceLock::new(); static SANDBOX: OnceLock> = OnceLock::new(); // This fuzz target tests all combinations of ReturnType and Parameters for `call_guest_function_by_name`. // For fuzzing efficiency, we create one Sandbox and reuse it for all fuzzing iterations. fuzz_target!( init: { + let mut cfg = SandboxConfiguration::default(); + cfg.set_output_data_size(64 * 1024); // 64 KB output buffer + cfg.set_input_data_size(64 * 1024); // 64 KB input buffer let u_sbox = UninitializedSandbox::new( GuestBinary::FilePath(simple_guest_for_fuzzing_as_string().expect("Guest Binary Missing")), - None + Some(cfg) ) .unwrap(); - let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); + let mut mu_sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); + let snapshot = mu_sbox.snapshot().unwrap(); SANDBOX.set(Mutex::new(mu_sbox)).unwrap(); + SNAPSHOT.set(Mutex::new(snapshot)).map_err(|_| "Snapshot already set").unwrap(); }, |data: (String, ReturnType, Vec)| { let (host_func_name, host_func_return, mut host_func_params) = data; let mut sandbox = SANDBOX.get().unwrap().lock().unwrap(); + let snapshot = SNAPSHOT.get().unwrap().lock().unwrap(); + sandbox.restore(&snapshot).unwrap(); + host_func_params.insert(0, ParameterValue::String(host_func_name)); - match sandbox.call_type_erased_guest_function_by_name("FuzzHostFunc", host_func_return, host_func_params) { - Err(HyperlightError::GuestAborted(_, message)) if !message.contains("Host Function Not Found") => { - // We don't allow GuestAborted errors, except for the "Host Function Not Found" case - panic!("Guest Aborted: {}", message); + if let Err(e) = sandbox.call_type_erased_guest_function_by_name("FuzzHostFunc", host_func_return, host_func_params) { + match e { + // the following are expected errors and occur frequently since + // we are randomly generating the function name and parameters + // to call with. + HyperlightError::HostFunctionNotFound(_) => {} + HyperlightError::UnexpectedNoOfArguments(_, _) => {}, + HyperlightError::ParameterValueConversionFailure(_, _) => {}, + + // any other error should be reported + _ => panic!("Guest Aborted with Unexpected Error: {:?}", e), } - _ => {} } } ); diff --git a/fuzz/fuzz_targets/host_print.rs b/fuzz/fuzz_targets/host_print.rs index 0a7e90d40..59dc1ed13 100644 --- a/fuzz/fuzz_targets/host_print.rs +++ b/fuzz/fuzz_targets/host_print.rs @@ -3,8 +3,6 @@ use std::sync::{Mutex, OnceLock}; use hyperlight_host::sandbox::uninitialized::GuestBinary; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simple_guest_for_fuzzing_as_string; use libfuzzer_sys::{Corpus, fuzz_target}; @@ -23,13 +21,13 @@ fuzz_target!( ) .unwrap(); - let mu_sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); + let mu_sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); SANDBOX.set(Mutex::new(mu_sbox)).unwrap(); }, |data: String| -> Corpus { let mut sandbox = SANDBOX.get().unwrap().lock().unwrap(); - let len: i32 = sandbox.call_guest_function_by_name::( + let len: i32 = sandbox.call::( "PrintOutput", data, ) diff --git a/hack/clippy-package-features.sh b/hack/clippy-package-features.sh new file mode 100755 index 000000000..e769700ff --- /dev/null +++ b/hack/clippy-package-features.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Check for required arguments +if [[ $# -lt 2 ]]; then + echo "Usage: $0 [target_triple]" >&2 + echo "Example: $0 hyperlight-host debug x86_64-unknown-linux-musl" >&2 + exit 1 +fi + +PACKAGE="$1" +TARGET="$2" +TARGET_TRIPLE="${3:-}" + +CARGO="cargo" + +# Cargo target argument to append to cargo calls (empty if not provided) +TRIPLE_ARG="" +if [[ -n "${TARGET_TRIPLE}" ]]; then + TRIPLE_ARG="--target ${TARGET_TRIPLE} --target-dir ./target/host" + CARGO="cross" +fi + +# Convert target for cargo profile +PROFILE=$([ "$TARGET" = "debug" ] && echo "dev" || echo "$TARGET") + +# Required features needed so the rust packages can compile +if [[ "$PACKAGE" == "hyperlight-host" ]]; then + REQUIRED_FEATURES=("kvm" "mshv3") +elif [[ "$PACKAGE" == "hyperlight-guest-bin" ]]; then + REQUIRED_FEATURES=("printf") +else + REQUIRED_FEATURES=() +fi + +# Get all features for the package (excluding default and required features) +# Always exclude "default", and exclude any required features using jq +features=$(cargo metadata --format-version 1 --no-deps | jq -r --arg pkg "$PACKAGE" '.packages[] | select(.name == $pkg) | .features | keys[] | select(. != "default" and (IN($ARGS.positional[])|not))' --args "${REQUIRED_FEATURES[@]}" || true) + +# Convert required features array to comma-separated string for cargo +if [[ ${#REQUIRED_FEATURES[@]} -gt 0 ]]; then + required_features_str=$(IFS=,; echo "${REQUIRED_FEATURES[*]}") +else + required_features_str="" +fi + +# Test with minimal features +if [[ ${#REQUIRED_FEATURES[@]} -gt 0 ]]; then + echo "Testing $PACKAGE with required features only ($required_features_str)..." + (set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --no-default-features --features "$required_features_str" --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) +else + echo "Testing $PACKAGE with no features..." + (set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --no-default-features --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) +fi + +echo "Testing $PACKAGE with default features..." +(set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) + +# Test each additional feature individually +for feature in $features; do + if [[ ${#REQUIRED_FEATURES[@]} -gt 0 ]]; then + echo "Testing $PACKAGE with feature: $required_features_str,$feature" + (set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --no-default-features --features "$required_features_str,$feature" --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) + else + echo "Testing $PACKAGE with feature: $feature" + (set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --no-default-features --features "$feature" --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) + fi +done + +# Test all features together +if [[ -n "$features" ]]; then + all_features=$(echo $features | tr '\n' ',' | sed 's/,$//') + if [[ ${#REQUIRED_FEATURES[@]} -gt 0 ]]; then + echo "Testing $PACKAGE with all features: $required_features_str,$all_features" + (set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --no-default-features --features "$required_features_str,$all_features" --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) + else + echo "Testing $PACKAGE with all features: $all_features" + (set -x; "$CARGO" clippy -p "$PACKAGE" --all-targets --no-default-features --features "$all_features" --profile="$PROFILE" ${TRIPLE_ARG} -- -D warnings) + fi +fi \ No newline at end of file diff --git a/hack/rust-dependabot-patch.Dockerfile b/hack/rust-dependabot-patch.Dockerfile index 31c285ef9..03bed249a 100644 --- a/hack/rust-dependabot-patch.Dockerfile +++ b/hack/rust-dependabot-patch.Dockerfile @@ -1,2 +1,2 @@ FROM dependabot/dependabot-script -RUN rustup toolchain install 1.85 && rustup default 1.85 +RUN rustup toolchain install 1.86 && rustup default 1.86 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5432e53da..aec2748e0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.85" +channel = "1.89" # Target used for guest binaries. This is an additive list of targets in addition to host platform. # Will install the target if not already installed when building guest binaries. targets = ["x86_64-unknown-none", "x86_64-unknown-linux-musl"] diff --git a/src/hyperlight_common/Cargo.toml b/src/hyperlight_common/Cargo.toml index 1afea5022..a1020a881 100644 --- a/src/hyperlight_common/Cargo.toml +++ b/src/hyperlight_common/Cargo.toml @@ -15,16 +15,18 @@ Hyperlight's components common to host and guest. workspace = true [dependencies] -flatbuffers = { version = "25.2.10", default-features = false } -anyhow = { version = "1.0.98", default-features = false } -log = "0.4.27" +flatbuffers = { version = "25.9.23", default-features = false } +anyhow = { version = "1.0.100", default-features = false } +log = "0.4.28" tracing = { version = "0.1.41", optional = true } -arbitrary = {version = "1.4.1", optional = true, features = ["derive"]} +arbitrary = {version = "1.4.2", optional = true, features = ["derive"]} spin = "0.10.0" [features] default = ["tracing"] fuzzing = ["dep:arbitrary"] +trace_guest = [] +mem_profile = [] std = [] [dev-dependencies] @@ -32,4 +34,4 @@ hyperlight-testing = { workspace = true } [lib] bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options -doctest = false # reduce noise in test output \ No newline at end of file +doctest = false # reduce noise in test output diff --git a/src/hyperlight_common/src/clippy.toml b/src/hyperlight_common/src/clippy.toml index cda217749..05fdbf5a2 100644 --- a/src/hyperlight_common/src/clippy.toml +++ b/src/hyperlight_common/src/clippy.toml @@ -2,6 +2,4 @@ disallowed-macros = [ { path = "std::assert", reason = "no asserts in release builds" }, { path = "std::assert_eq", reason = "no asserts in release builds" }, { path = "std::assert_ne", reason = "no asserts in release builds" }, - { path = "std::assert_true", reason = "no asserts in release builds" }, - { path = "std::assert_false", reason = "no asserts in release builds" }, ] diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs b/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs index 67998fbbe..056ced8e0 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs @@ -18,7 +18,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use anyhow::{Error, Result, bail}; -use flatbuffers::{WIPOffset, size_prefixed_root}; +use flatbuffers::{FlatBufferBuilder, WIPOffset, size_prefixed_root}; #[cfg(feature = "tracing")] use tracing::{Span, instrument}; @@ -72,214 +72,136 @@ impl FunctionCall { pub fn function_call_type(&self) -> FunctionCallType { self.function_call_type.clone() } -} - -#[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] -pub fn validate_guest_function_call_buffer(function_call_buffer: &[u8]) -> Result<()> { - let guest_function_call_fb = size_prefixed_root::(function_call_buffer) - .map_err(|e| anyhow::anyhow!("Error reading function call buffer: {:?}", e))?; - match guest_function_call_fb.function_call_type() { - FbFunctionCallType::guest => Ok(()), - other => { - bail!("Invalid function call type: {:?}", other); - } - } -} - -#[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] -pub fn validate_host_function_call_buffer(function_call_buffer: &[u8]) -> Result<()> { - let host_function_call_fb = size_prefixed_root::(function_call_buffer) - .map_err(|e| anyhow::anyhow!("Error reading function call buffer: {:?}", e))?; - match host_function_call_fb.function_call_type() { - FbFunctionCallType::host => Ok(()), - other => { - bail!("Invalid function call type: {:?}", other); - } - } -} - -impl TryFrom<&[u8]> for FunctionCall { - type Error = Error; - #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] - fn try_from(value: &[u8]) -> Result { - let function_call_fb = size_prefixed_root::(value) - .map_err(|e| anyhow::anyhow!("Error reading function call buffer: {:?}", e))?; - let function_name = function_call_fb.function_name(); - let function_call_type = match function_call_fb.function_call_type() { - FbFunctionCallType::guest => FunctionCallType::Guest, - FbFunctionCallType::host => FunctionCallType::Host, - other => { - bail!("Invalid function call type: {:?}", other); - } - }; - let expected_return_type = function_call_fb.expected_return_type().try_into()?; - let parameters = function_call_fb - .parameters() - .map(|v| { - v.iter() - .map(|p| p.try_into()) - .collect::>>() - }) - .transpose()?; + /// Encodes self into the given builder and returns the encoded data. + /// + /// # Notes + /// + /// The builder should not be reused after a call to encode, since this function + /// does not reset the state of the builder. If you want to reuse the builder, + /// you'll need to reset it first. + pub fn encode<'a>(&self, builder: &'a mut FlatBufferBuilder) -> &'a [u8] { + let function_name = builder.create_string(&self.function_name); - Ok(Self { - function_name: function_name.to_string(), - parameters, - function_call_type, - expected_return_type, - }) - } -} - -impl TryFrom for Vec { - type Error = Error; - #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] - fn try_from(value: FunctionCall) -> Result> { - let mut builder = flatbuffers::FlatBufferBuilder::new(); - let function_name = builder.create_string(&value.function_name); - - let function_call_type = match value.function_call_type { + let function_call_type = match self.function_call_type { FunctionCallType::Guest => FbFunctionCallType::guest, FunctionCallType::Host => FbFunctionCallType::host, }; - let expected_return_type = value.expected_return_type.into(); - - let parameters = match &value.parameters { - Some(p) => { - let num_items = p.len(); - let mut parameters: Vec> = Vec::with_capacity(num_items); + let expected_return_type = self.expected_return_type.into(); - for param in p { - match param { + let parameters = match &self.parameters { + Some(p) if !p.is_empty() => { + let parameter_offsets: Vec> = p + .iter() + .map(|param| match param { ParameterValue::Int(i) => { - let hlint = hlint::create(&mut builder, &hlintArgs { value: *i }); - let parameter = Parameter::create( - &mut builder, + let hlint = hlint::create(builder, &hlintArgs { value: *i }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hlint, value: Some(hlint.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::UInt(ui) => { - let hluint = hluint::create(&mut builder, &hluintArgs { value: *ui }); - let parameter = Parameter::create( - &mut builder, + let hluint = hluint::create(builder, &hluintArgs { value: *ui }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hluint, value: Some(hluint.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::Long(l) => { - let hllong = hllong::create(&mut builder, &hllongArgs { value: *l }); - let parameter = Parameter::create( - &mut builder, + let hllong = hllong::create(builder, &hllongArgs { value: *l }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hllong, value: Some(hllong.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::ULong(ul) => { - let hlulong = - hlulong::create(&mut builder, &hlulongArgs { value: *ul }); - let parameter = Parameter::create( - &mut builder, + let hlulong = hlulong::create(builder, &hlulongArgs { value: *ul }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hlulong, value: Some(hlulong.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::Float(f) => { - let hlfloat = hlfloat::create(&mut builder, &hlfloatArgs { value: *f }); - let parameter = Parameter::create( - &mut builder, + let hlfloat = hlfloat::create(builder, &hlfloatArgs { value: *f }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hlfloat, value: Some(hlfloat.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::Double(d) => { - let hldouble = - hldouble::create(&mut builder, &hldoubleArgs { value: *d }); - let parameter = Parameter::create( - &mut builder, + let hldouble = hldouble::create(builder, &hldoubleArgs { value: *d }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hldouble, value: Some(hldouble.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::Bool(b) => { - let hlbool: WIPOffset> = - hlbool::create(&mut builder, &hlboolArgs { value: *b }); - let parameter = Parameter::create( - &mut builder, + let hlbool = hlbool::create(builder, &hlboolArgs { value: *b }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hlbool, value: Some(hlbool.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::String(s) => { - let hlstring = { - let val = builder.create_string(s.as_str()); - hlstring::create(&mut builder, &hlstringArgs { value: Some(val) }) - }; - let parameter = Parameter::create( - &mut builder, + let val = builder.create_string(s.as_str()); + let hlstring = + hlstring::create(builder, &hlstringArgs { value: Some(val) }); + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hlstring, value: Some(hlstring.as_union_value()), }, - ); - parameters.push(parameter); + ) } ParameterValue::VecBytes(v) => { let vec_bytes = builder.create_vector(v); - let hlvecbytes = hlvecbytes::create( - &mut builder, + builder, &hlvecbytesArgs { value: Some(vec_bytes), }, ); - let parameter = Parameter::create( - &mut builder, + Parameter::create( + builder, &ParameterArgs { value_type: FbParameterValue::hlvecbytes, value: Some(hlvecbytes.as_union_value()), }, - ); - parameters.push(parameter); + ) } - } - } - parameters + }) + .collect(); + Some(builder.create_vector(¶meter_offsets)) } - None => Vec::new(), - }; - - let parameters = if !parameters.is_empty() { - Some(builder.create_vector(¶meters)) - } else { - None + _ => None, }; let function_call = FbFunctionCall::create( - &mut builder, + builder, &FbFunctionCallArgs { function_name: Some(function_name), parameters, @@ -288,9 +210,65 @@ impl TryFrom for Vec { }, ); builder.finish_size_prefixed(function_call, None); - let res = builder.finished_data().to_vec(); + builder.finished_data() + } +} - Ok(res) +#[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] +pub fn validate_guest_function_call_buffer(function_call_buffer: &[u8]) -> Result<()> { + let guest_function_call_fb = size_prefixed_root::(function_call_buffer) + .map_err(|e| anyhow::anyhow!("Error reading function call buffer: {:?}", e))?; + match guest_function_call_fb.function_call_type() { + FbFunctionCallType::guest => Ok(()), + other => { + bail!("Invalid function call type: {:?}", other); + } + } +} + +#[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] +pub fn validate_host_function_call_buffer(function_call_buffer: &[u8]) -> Result<()> { + let host_function_call_fb = size_prefixed_root::(function_call_buffer) + .map_err(|e| anyhow::anyhow!("Error reading function call buffer: {:?}", e))?; + match host_function_call_fb.function_call_type() { + FbFunctionCallType::host => Ok(()), + other => { + bail!("Invalid function call type: {:?}", other); + } + } +} + +impl TryFrom<&[u8]> for FunctionCall { + type Error = Error; + #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] + fn try_from(value: &[u8]) -> Result { + let function_call_fb = size_prefixed_root::(value) + .map_err(|e| anyhow::anyhow!("Error reading function call buffer: {:?}", e))?; + let function_name = function_call_fb.function_name(); + let function_call_type = match function_call_fb.function_call_type() { + FbFunctionCallType::guest => FunctionCallType::Guest, + FbFunctionCallType::host => FunctionCallType::Host, + other => { + bail!("Invalid function call type: {:?}", other); + } + }; + let expected_return_type = function_call_fb.expected_return_type().try_into()?; + + let parameters = function_call_fb + .parameters() + .map(|v| { + v.iter() + .map(|p| p.try_into()) + .collect::>>() + }) + .transpose()?; + + Ok(Self { + function_name: function_name.to_string(), + parameters, + function_call_type, + expected_return_type, + }) } } @@ -303,7 +281,8 @@ mod tests { #[test] fn read_from_flatbuffer() -> Result<()> { - let test_data: Vec = FunctionCall::new( + let mut builder = FlatBufferBuilder::new(); + let test_data = FunctionCall::new( "PrintTwelveArgs".to_string(), Some(vec![ ParameterValue::String("1".to_string()), @@ -322,10 +301,9 @@ mod tests { FunctionCallType::Guest, ReturnType::Int, ) - .try_into() - .unwrap(); + .encode(&mut builder); - let function_call = FunctionCall::try_from(test_data.as_slice())?; + let function_call = FunctionCall::try_from(test_data)?; assert_eq!(function_call.function_name, "PrintTwelveArgs"); assert!(function_call.parameters.is_some()); let parameters = function_call.parameters.unwrap(); diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs b/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs index b726140db..42c7ff823 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/function_types.rs @@ -22,15 +22,166 @@ use flatbuffers::size_prefixed_root; #[cfg(feature = "tracing")] use tracing::{Span, instrument}; +use super::guest_error::GuestError; use crate::flatbuffers::hyperlight::generated::{ FunctionCallResult as FbFunctionCallResult, FunctionCallResultArgs as FbFunctionCallResultArgs, - Parameter, ParameterType as FbParameterType, ParameterValue as FbParameterValue, - ReturnType as FbReturnType, ReturnValue as FbReturnValue, hlbool, hlboolArgs, hldouble, - hldoubleArgs, hlfloat, hlfloatArgs, hlint, hlintArgs, hllong, hllongArgs, hlsizeprefixedbuffer, + FunctionCallResultType, Parameter, ParameterType as FbParameterType, + ParameterValue as FbParameterValue, ReturnType as FbReturnType, ReturnValue as FbReturnValue, + ReturnValueBox, ReturnValueBoxArgs, hlbool, hlboolArgs, hldouble, hldoubleArgs, hlfloat, + hlfloatArgs, hlint, hlintArgs, hllong, hllongArgs, hlsizeprefixedbuffer, hlsizeprefixedbufferArgs, hlstring, hlstringArgs, hluint, hluintArgs, hlulong, hlulongArgs, hlvoid, hlvoidArgs, }; +pub struct FunctionCallResult(core::result::Result); + +impl FunctionCallResult { + /// Encodes self into the given builder and returns the encoded data. + /// + /// # Notes + /// + /// The builder should not be reused after a call to encode, since this function + /// does not reset the state of the builder. If you want to reuse the builder, + /// you'll need to reset it first. + pub fn encode<'a>(&self, builder: &'a mut flatbuffers::FlatBufferBuilder) -> &'a [u8] { + match &self.0 { + Ok(rv) => { + // Encode ReturnValue as ReturnValueBox + let (value, value_type) = match rv { + ReturnValue::Int(i) => { + let off = hlint::create(builder, &hlintArgs { value: *i }); + (Some(off.as_union_value()), FbReturnValue::hlint) + } + ReturnValue::UInt(ui) => { + let off = hluint::create(builder, &hluintArgs { value: *ui }); + (Some(off.as_union_value()), FbReturnValue::hluint) + } + ReturnValue::Long(l) => { + let off = hllong::create(builder, &hllongArgs { value: *l }); + (Some(off.as_union_value()), FbReturnValue::hllong) + } + ReturnValue::ULong(ul) => { + let off = hlulong::create(builder, &hlulongArgs { value: *ul }); + (Some(off.as_union_value()), FbReturnValue::hlulong) + } + ReturnValue::Float(f) => { + let off = hlfloat::create(builder, &hlfloatArgs { value: *f }); + (Some(off.as_union_value()), FbReturnValue::hlfloat) + } + ReturnValue::Double(d) => { + let off = hldouble::create(builder, &hldoubleArgs { value: *d }); + (Some(off.as_union_value()), FbReturnValue::hldouble) + } + ReturnValue::Bool(b) => { + let off = hlbool::create(builder, &hlboolArgs { value: *b }); + (Some(off.as_union_value()), FbReturnValue::hlbool) + } + ReturnValue::String(s) => { + let val = builder.create_string(s.as_str()); + let off = hlstring::create(builder, &hlstringArgs { value: Some(val) }); + (Some(off.as_union_value()), FbReturnValue::hlstring) + } + ReturnValue::VecBytes(v) => { + let val = builder.create_vector(v); + let off = hlsizeprefixedbuffer::create( + builder, + &hlsizeprefixedbufferArgs { + value: Some(val), + size: v.len() as i32, + }, + ); + ( + Some(off.as_union_value()), + FbReturnValue::hlsizeprefixedbuffer, + ) + } + ReturnValue::Void(()) => { + let off = hlvoid::create(builder, &hlvoidArgs {}); + (Some(off.as_union_value()), FbReturnValue::hlvoid) + } + }; + let rv_box = + ReturnValueBox::create(builder, &ReturnValueBoxArgs { value, value_type }); + let fcr = FbFunctionCallResult::create( + builder, + &FbFunctionCallResultArgs { + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, + }, + ); + builder.finish_size_prefixed(fcr, None); + builder.finished_data() + } + Err(ge) => { + // Encode GuestError + let code: crate::flatbuffers::hyperlight::generated::ErrorCode = ge.code.into(); + let msg = builder.create_string(&ge.message); + let guest_error = crate::flatbuffers::hyperlight::generated::GuestError::create( + builder, + &crate::flatbuffers::hyperlight::generated::GuestErrorArgs { + code, + message: Some(msg), + }, + ); + let fcr = FbFunctionCallResult::create( + builder, + &FbFunctionCallResultArgs { + result: Some(guest_error.as_union_value()), + result_type: FunctionCallResultType::GuestError, + }, + ); + builder.finish_size_prefixed(fcr, None); + builder.finished_data() + } + } + } + pub fn new(value: core::result::Result) -> Self { + FunctionCallResult(value) + } + + pub fn into_inner(self) -> core::result::Result { + self.0 + } +} + +impl TryFrom<&[u8]> for FunctionCallResult { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + let function_call_result_fb = size_prefixed_root::(value) + .map_err(|e| anyhow!("Failed to get FunctionCallResult from bytes: {:?}", e))?; + + match function_call_result_fb.result_type() { + FunctionCallResultType::ReturnValueBox => { + let boxed = function_call_result_fb + .result_as_return_value_box() + .ok_or_else(|| { + anyhow!("Failed to get ReturnValueBox from function call result") + })?; + let return_value = ReturnValue::try_from(boxed)?; + Ok(FunctionCallResult(Ok(return_value))) + } + FunctionCallResultType::GuestError => { + let guest_error_table = function_call_result_fb + .result_as_guest_error() + .ok_or_else(|| anyhow!("Failed to get GuestError from function call result"))?; + let code = guest_error_table.code(); + let message = guest_error_table + .message() + .map(|s| s.to_string()) + .unwrap_or_default(); + Ok(FunctionCallResult(Err(GuestError::new( + code.into(), + message, + )))) + } + other => { + bail!("Unexpected function call result type: {:?}", other) + } + } + } +} + /// Supported parameter types with values for function calling. #[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq)] @@ -51,7 +202,7 @@ pub enum ParameterValue { String(String), /// bool Bool(bool), - /// Vec + /// `Vec` VecBytes(Vec), } @@ -75,7 +226,7 @@ pub enum ParameterType { String, /// bool Bool, - /// Vec + /// `Vec` VecBytes, } @@ -100,7 +251,7 @@ pub enum ReturnValue { Bool(bool), /// () Void(()), - /// Vec + /// `Vec` VecBytes(Vec), } @@ -128,7 +279,7 @@ pub enum ReturnType { Bool, /// () Void, - /// Vec + /// `Vec` VecBytes, } @@ -181,7 +332,7 @@ impl TryFrom> for ParameterValue { ParameterValue::String(hlstring.value().unwrap_or_default().to_string()) }), FbParameterValue::hlvecbytes => param.value_as_hlvecbytes().map(|hlvecbytes| { - ParameterValue::VecBytes(hlvecbytes.value().unwrap_or_default().iter().collect()) + ParameterValue::VecBytes(hlvecbytes.value().unwrap_or_default().bytes().to_vec()) }), other => { bail!("Unexpected flatbuffer parameter value type: {:?}", other); @@ -516,55 +667,55 @@ impl TryFrom for () { } } -impl TryFrom> for ReturnValue { +impl TryFrom> for ReturnValue { type Error = Error; #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] - fn try_from(function_call_result_fb: FbFunctionCallResult<'_>) -> Result { - match function_call_result_fb.return_value_type() { + fn try_from(return_value_box: ReturnValueBox<'_>) -> Result { + match return_value_box.value_type() { FbReturnValue::hlint => { - let hlint = function_call_result_fb - .return_value_as_hlint() + let hlint = return_value_box + .value_as_hlint() .ok_or_else(|| anyhow!("Failed to get hlint from return value"))?; Ok(ReturnValue::Int(hlint.value())) } FbReturnValue::hluint => { - let hluint = function_call_result_fb - .return_value_as_hluint() + let hluint = return_value_box + .value_as_hluint() .ok_or_else(|| anyhow!("Failed to get hluint from return value"))?; Ok(ReturnValue::UInt(hluint.value())) } FbReturnValue::hllong => { - let hllong = function_call_result_fb - .return_value_as_hllong() + let hllong = return_value_box + .value_as_hllong() .ok_or_else(|| anyhow!("Failed to get hllong from return value"))?; Ok(ReturnValue::Long(hllong.value())) } FbReturnValue::hlulong => { - let hlulong = function_call_result_fb - .return_value_as_hlulong() + let hlulong = return_value_box + .value_as_hlulong() .ok_or_else(|| anyhow!("Failed to get hlulong from return value"))?; Ok(ReturnValue::ULong(hlulong.value())) } FbReturnValue::hlfloat => { - let hlfloat = function_call_result_fb - .return_value_as_hlfloat() + let hlfloat = return_value_box + .value_as_hlfloat() .ok_or_else(|| anyhow!("Failed to get hlfloat from return value"))?; Ok(ReturnValue::Float(hlfloat.value())) } FbReturnValue::hldouble => { - let hldouble = function_call_result_fb - .return_value_as_hldouble() + let hldouble = return_value_box + .value_as_hldouble() .ok_or_else(|| anyhow!("Failed to get hldouble from return value"))?; Ok(ReturnValue::Double(hldouble.value())) } FbReturnValue::hlbool => { - let hlbool = function_call_result_fb - .return_value_as_hlbool() + let hlbool = return_value_box + .value_as_hlbool() .ok_or_else(|| anyhow!("Failed to get hlbool from return value"))?; Ok(ReturnValue::Bool(hlbool.value())) } FbReturnValue::hlstring => { - let hlstring = match function_call_result_fb.return_value_as_hlstring() { + let hlstring = match return_value_box.value_as_hlstring() { Some(hlstring) => hlstring.value().map(|v| v.to_string()), None => None, }; @@ -572,13 +723,12 @@ impl TryFrom> for ReturnValue { } FbReturnValue::hlvoid => Ok(ReturnValue::Void(())), FbReturnValue::hlsizeprefixedbuffer => { - let hlvecbytes = - match function_call_result_fb.return_value_as_hlsizeprefixedbuffer() { - Some(hlvecbytes) => hlvecbytes - .value() - .map(|val| val.iter().collect::>()), - None => None, - }; + let hlvecbytes = match return_value_box.value_as_hlsizeprefixedbuffer() { + Some(hlvecbytes) => hlvecbytes + .value() + .map(|val| val.iter().collect::>()), + None => None, + }; Ok(ReturnValue::VecBytes(hlvecbytes.unwrap_or(Vec::new()))) } other => { @@ -588,123 +738,169 @@ impl TryFrom> for ReturnValue { } } -impl TryFrom<&[u8]> for ReturnValue { - type Error = Error; - #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] - fn try_from(value: &[u8]) -> Result { - let function_call_result_fb = size_prefixed_root::(value) - .map_err(|e| anyhow!("Failed to get ReturnValue from bytes: {:?}", e))?; - function_call_result_fb.try_into() - } -} - impl TryFrom<&ReturnValue> for Vec { type Error = Error; #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] fn try_from(value: &ReturnValue) -> Result> { let mut builder = flatbuffers::FlatBufferBuilder::new(); - let result = match value { + let result_bytes = match value { ReturnValue::Int(i) => { - let hlint = hlint::create(&mut builder, &hlintArgs { value: *i }); - let function_call_result = FbFunctionCallResult::create( + let hlint_off = hlint::create(&mut builder, &hlintArgs { value: *i }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(hlint_off.as_union_value()), + value_type: FbReturnValue::hlint, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlint.as_union_value()), - return_value_type: FbReturnValue::hlint, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::UInt(ui) => { - let hluint = hluint::create(&mut builder, &hluintArgs { value: *ui }); - let function_call_result = FbFunctionCallResult::create( + let off = hluint::create(&mut builder, &hluintArgs { value: *ui }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hluint, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hluint.as_union_value()), - return_value_type: FbReturnValue::hluint, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::Long(l) => { - let hllong = hllong::create(&mut builder, &hllongArgs { value: *l }); - let function_call_result = FbFunctionCallResult::create( + let off = hllong::create(&mut builder, &hllongArgs { value: *l }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hllong, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hllong.as_union_value()), - return_value_type: FbReturnValue::hllong, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::ULong(ul) => { - let hlulong = hlulong::create(&mut builder, &hlulongArgs { value: *ul }); - let function_call_result = FbFunctionCallResult::create( + let off = hlulong::create(&mut builder, &hlulongArgs { value: *ul }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hlulong, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlulong.as_union_value()), - return_value_type: FbReturnValue::hlulong, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::Float(f) => { - let hlfloat = hlfloat::create(&mut builder, &hlfloatArgs { value: *f }); - let function_call_result = FbFunctionCallResult::create( + let off = hlfloat::create(&mut builder, &hlfloatArgs { value: *f }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hlfloat, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlfloat.as_union_value()), - return_value_type: FbReturnValue::hlfloat, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::Double(d) => { - let hldouble = hldouble::create(&mut builder, &hldoubleArgs { value: *d }); - let function_call_result = FbFunctionCallResult::create( + let off = hldouble::create(&mut builder, &hldoubleArgs { value: *d }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hldouble, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hldouble.as_union_value()), - return_value_type: FbReturnValue::hldouble, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::Bool(b) => { - let hlbool = hlbool::create(&mut builder, &hlboolArgs { value: *b }); - let function_call_result = FbFunctionCallResult::create( + let off = hlbool::create(&mut builder, &hlboolArgs { value: *b }); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hlbool, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlbool.as_union_value()), - return_value_type: FbReturnValue::hlbool, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::String(s) => { - let hlstring = { + let off = { let val = builder.create_string(s.as_str()); hlstring::create(&mut builder, &hlstringArgs { value: Some(val) }) }; - let function_call_result = FbFunctionCallResult::create( + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hlstring, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlstring.as_union_value()), - return_value_type: FbReturnValue::hlstring, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::VecBytes(v) => { - let hlvecbytes = { + let off = { let val = builder.create_vector(v.as_slice()); hlsizeprefixedbuffer::create( &mut builder, @@ -714,30 +910,77 @@ impl TryFrom<&ReturnValue> for Vec { }, ) }; - let function_call_result = FbFunctionCallResult::create( + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hlsizeprefixedbuffer, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlvecbytes.as_union_value()), - return_value_type: FbReturnValue::hlsizeprefixedbuffer, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } ReturnValue::Void(()) => { - let hlvoid = hlvoid::create(&mut builder, &hlvoidArgs {}); - let function_call_result = FbFunctionCallResult::create( + let off = hlvoid::create(&mut builder, &hlvoidArgs {}); + let rv_box = ReturnValueBox::create( + &mut builder, + &ReturnValueBoxArgs { + value: Some(off.as_union_value()), + value_type: FbReturnValue::hlvoid, + }, + ); + let fcr = FbFunctionCallResult::create( &mut builder, &FbFunctionCallResultArgs { - return_value: Some(hlvoid.as_union_value()), - return_value_type: FbReturnValue::hlvoid, + result: Some(rv_box.as_union_value()), + result_type: FunctionCallResultType::ReturnValueBox, }, ); - builder.finish_size_prefixed(function_call_result, None); + builder.finish_size_prefixed(fcr, None); builder.finished_data().to_vec() } }; - Ok(result) + Ok(result_bytes) + } +} + +#[cfg(test)] +mod tests { + use flatbuffers::FlatBufferBuilder; + + use super::super::guest_error::ErrorCode; + use super::*; + + #[test] + fn encode_success_result() { + let mut builder = FlatBufferBuilder::new(); + let test_data = FunctionCallResult::new(Ok(ReturnValue::Int(42))).encode(&mut builder); + + let function_call_result = FunctionCallResult::try_from(test_data).unwrap(); + let result = function_call_result.into_inner().unwrap(); + assert_eq!(result, ReturnValue::Int(42)); + } + + #[test] + fn encode_error_result() { + let mut builder = FlatBufferBuilder::new(); + let test_error = GuestError::new( + ErrorCode::GuestFunctionNotFound, + "Function not found".to_string(), + ); + let test_data = FunctionCallResult::new(Err(test_error.clone())).encode(&mut builder); + + let function_call_result = FunctionCallResult::try_from(test_data).unwrap(); + let error = function_call_result.into_inner().unwrap_err(); + assert_eq!(error.code, test_error.code); + assert_eq!(error.message, test_error.message); } } diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/guest_error.rs b/src/hyperlight_common/src/flatbuffer_wrappers/guest_error.rs index 289ae48b4..b3d1f457e 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/guest_error.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/guest_error.rs @@ -17,16 +17,11 @@ limitations under the License. extern crate flatbuffers; use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use anyhow::{Error, Result}; -use flatbuffers::size_prefixed_root; #[cfg(feature = "tracing")] use tracing::{Span, instrument}; -use crate::flatbuffers::hyperlight::generated::{ - ErrorCode as FbErrorCode, GuestError as FbGuestError, GuestErrorArgs, -}; +use crate::flatbuffers::hyperlight::generated::ErrorCode as FbErrorCode; #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[repr(C)] @@ -48,6 +43,7 @@ pub enum ErrorCode { GuestFunctionParameterTypeMismatch = 14, GuestError = 15, ArrayLengthParamIsMissing = 16, + HostFunctionError = 17, } impl From for FbErrorCode { @@ -73,6 +69,7 @@ impl From for FbErrorCode { } ErrorCode::GuestError => Self::GuestError, ErrorCode::ArrayLengthParamIsMissing => Self::ArrayLengthParamIsMissing, + ErrorCode::HostFunctionError => Self::HostError, } } } @@ -99,6 +96,7 @@ impl From for ErrorCode { } FbErrorCode::GuestError => Self::GuestError, FbErrorCode::ArrayLengthParamIsMissing => Self::ArrayLengthParamIsMissing, + FbErrorCode::HostError => Self::HostFunctionError, _ => Self::UnknownError, } } @@ -123,6 +121,7 @@ impl From for ErrorCode { 14 => Self::GuestFunctionParameterTypeMismatch, 15 => Self::GuestError, 16 => Self::ArrayLengthParamIsMissing, + 17 => Self::HostFunctionError, _ => Self::UnknownError, } } @@ -147,6 +146,7 @@ impl From for u64 { ErrorCode::GuestFunctionParameterTypeMismatch => 14, ErrorCode::GuestError => 15, ErrorCode::ArrayLengthParamIsMissing => 16, + ErrorCode::HostFunctionError => 17, } } } @@ -174,6 +174,7 @@ impl From for String { } ErrorCode::GuestError => "GuestError".to_string(), ErrorCode::ArrayLengthParamIsMissing => "ArrayLengthParamIsMissing".to_string(), + ErrorCode::HostFunctionError => "HostFunctionError".to_string(), } } } @@ -194,44 +195,6 @@ impl GuestError { } } -impl TryFrom<&[u8]> for GuestError { - type Error = Error; - fn try_from(value: &[u8]) -> Result { - let guest_error_fb = size_prefixed_root::(value) - .map_err(|e| anyhow::anyhow!("Error while reading GuestError: {:?}", e))?; - let code = guest_error_fb.code(); - let message = match guest_error_fb.message() { - Some(message) => message.to_string(), - None => String::new(), - }; - Ok(Self { - code: code.into(), - message, - }) - } -} - -impl TryFrom<&GuestError> for Vec { - type Error = Error; - #[cfg_attr(feature = "tracing", instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace"))] - fn try_from(value: &GuestError) -> Result> { - let mut builder = flatbuffers::FlatBufferBuilder::new(); - let message = builder.create_string(&value.message); - - let guest_error_fb = FbGuestError::create( - &mut builder, - &GuestErrorArgs { - code: value.code.into(), - message: Some(message), - }, - ); - builder.finish_size_prefixed(guest_error_fb, None); - let res = builder.finished_data().to_vec(); - - Ok(res) - } -} - impl Default for GuestError { #[cfg_attr(feature = "tracing", instrument(parent = Span::current(), level= "Trace"))] fn default() -> Self { diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/util.rs b/src/hyperlight_common/src/flatbuffer_wrappers/util.rs index ba3645b94..f1ce3de02 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/util.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/util.rs @@ -18,9 +18,11 @@ use alloc::vec::Vec; use flatbuffers::FlatBufferBuilder; +use crate::flatbuffer_wrappers::function_types::ParameterValue; use crate::flatbuffers::hyperlight::generated::{ FunctionCallResult as FbFunctionCallResult, FunctionCallResultArgs as FbFunctionCallResultArgs, - ReturnValue as FbReturnValue, hlbool as Fbhlbool, hlboolArgs as FbhlboolArgs, + FunctionCallResultType as FbFunctionCallResultType, ReturnValue as FbReturnValue, + ReturnValueBox, ReturnValueBoxArgs, hlbool as Fbhlbool, hlboolArgs as FbhlboolArgs, hldouble as Fbhldouble, hldoubleArgs as FbhldoubleArgs, hlfloat as Fbhlfloat, hlfloatArgs as FbhlfloatArgs, hlint as Fbhlint, hlintArgs as FbhlintArgs, hllong as Fbhllong, hllongArgs as FbhllongArgs, hlsizeprefixedbuffer as Fbhlsizeprefixedbuffer, @@ -33,8 +35,8 @@ use crate::flatbuffers::hyperlight::generated::{ /// Flatbuffer-encodes the given value pub fn get_flatbuffer_result(val: T) -> Vec { let mut builder = FlatBufferBuilder::new(); - let res = &T::serialize(&val, &mut builder); - let result_offset = FbFunctionCallResult::create(&mut builder, res); + let res = T::serialize(&val, &mut builder); + let result_offset = FbFunctionCallResult::create(&mut builder, &res); builder.finish_size_prefixed(result_offset, None); @@ -49,9 +51,17 @@ pub trait FlatbufferSerializable { impl FlatbufferSerializable for () { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let void_off = Fbhlvoid::create(builder, &FbhlvoidArgs {}); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlvoid, + value: Some(void_off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some(Fbhlvoid::create(builder, &FbhlvoidArgs {}).as_union_value()), - return_value_type: FbReturnValue::hlvoid, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } @@ -59,113 +69,512 @@ impl FlatbufferSerializable for () { impl FlatbufferSerializable for &str { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { let string_offset = builder.create_string(self); + let str_off = Fbhlstring::create( + builder, + &FbhlstringArgs { + value: Some(string_offset), + }, + ); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlstring, + value: Some(str_off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhlstring::create( - builder, - &FbhlstringArgs { - value: Some(string_offset), - }, - ) - .as_union_value(), - ), - return_value_type: FbReturnValue::hlstring, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for &[u8] { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { - let vec_offset = builder.create_vector(self); + let vec_off = builder.create_vector(self); + let buf_off = Fbhlsizeprefixedbuffer::create( + builder, + &FbhlsizeprefixedbufferArgs { + size: self.len() as i32, + value: Some(vec_off), + }, + ); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlsizeprefixedbuffer, + value: Some(buf_off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhlsizeprefixedbuffer::create( - builder, - &FbhlsizeprefixedbufferArgs { - size: self.len() as i32, - value: Some(vec_offset), - }, - ) - .as_union_value(), - ), - return_value_type: FbReturnValue::hlsizeprefixedbuffer, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for f32 { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhlfloat::create(builder, &FbhlfloatArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlfloat, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhlfloat::create(builder, &FbhlfloatArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hlfloat, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for f64 { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhldouble::create(builder, &FbhldoubleArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hldouble, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhldouble::create(builder, &FbhldoubleArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hldouble, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for i32 { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhlint::create(builder, &FbhlintArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlint, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhlint::create(builder, &FbhlintArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hlint, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for i64 { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhllong::create(builder, &FbhllongArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hllong, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhllong::create(builder, &FbhllongArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hllong, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for u32 { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhluint::create(builder, &FbhluintArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hluint, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhluint::create(builder, &FbhluintArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hluint, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for u64 { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhlulong::create(builder, &FbhlulongArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlulong, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhlulong::create(builder, &FbhlulongArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hlulong, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } impl FlatbufferSerializable for bool { fn serialize(&self, builder: &mut FlatBufferBuilder) -> FbFunctionCallResultArgs { + let off = Fbhlbool::create(builder, &FbhlboolArgs { value: *self }); + let rv_box = ReturnValueBox::create( + builder, + &ReturnValueBoxArgs { + value_type: FbReturnValue::hlbool, + value: Some(off.as_union_value()), + }, + ); FbFunctionCallResultArgs { - return_value: Some( - Fbhlbool::create(builder, &FbhlboolArgs { value: *self }).as_union_value(), - ), - return_value_type: FbReturnValue::hlbool, + result_type: FbFunctionCallResultType::ReturnValueBox, + result: Some(rv_box.as_union_value()), } } } + +/// Estimates the required buffer capacity for encoding a FunctionCall with the given parameters. +/// This helps avoid reallocation during FlatBuffer encoding when passing large slices and strings. +/// +/// The function aims to be lightweight and fast and run in O(1) as long as the number of parameters is limited +/// (which it is since hyperlight only currently supports up to 12). +/// +/// Note: This estimates the capacity needed for the inner vec inside a FlatBufferBuilder. It does not +/// necessarily match the size of the final encoded buffer. The estimation always rounds up to the +/// nearest power of two to match FlatBufferBuilder's allocation strategy. +/// +/// The estimations are numbers used are empirically derived based on the tests below and vaguely based +/// on https://flatbuffers.dev/internals/ and https://github.com/dvidelabs/flatcc/blob/f064cefb2034d1e7407407ce32a6085c322212a7/doc/binary-format.md#flatbuffers-binary-format +#[inline] // allow cross-crate inlining (for hyperlight-host calls) +pub fn estimate_flatbuffer_capacity(function_name: &str, args: &[ParameterValue]) -> usize { + let mut estimated_capacity = 20; + + // Function name overhead + estimated_capacity += function_name.len() + 12; + + // Parameters vector overhead + estimated_capacity += 12 + args.len() * 6; + + // Per-parameter overhead + for arg in args { + estimated_capacity += 16; // Base parameter structure + estimated_capacity += match arg { + ParameterValue::String(s) => s.len() + 20, + ParameterValue::VecBytes(v) => v.len() + 20, + ParameterValue::Int(_) | ParameterValue::UInt(_) => 16, + ParameterValue::Long(_) | ParameterValue::ULong(_) => 20, + ParameterValue::Float(_) => 16, + ParameterValue::Double(_) => 20, + ParameterValue::Bool(_) => 12, + }; + } + + // match how vec grows + estimated_capacity.next_power_of_two() +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + use alloc::vec; + use alloc::vec::Vec; + + use super::*; + use crate::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType}; + use crate::flatbuffer_wrappers::function_types::{ParameterValue, ReturnType}; + + /// Helper function to check that estimation is within reasonable bounds (Âą25%) + fn assert_estimation_accuracy( + function_name: &str, + args: Vec, + call_type: FunctionCallType, + return_type: ReturnType, + ) { + let estimated = estimate_flatbuffer_capacity(function_name, &args); + + let fc = FunctionCall::new( + function_name.to_string(), + Some(args), + call_type.clone(), + return_type, + ); + // Important that this FlatBufferBuilder is created with capacity 0 so it grows to its needed capacity + let mut builder = FlatBufferBuilder::new(); + let _buffer = fc.encode(&mut builder); + let actual = builder.collapse().0.capacity(); + + let lower_bound = (actual as f64 * 0.75) as usize; + let upper_bound = (actual as f64 * 1.25) as usize; + + assert!( + estimated >= lower_bound && estimated <= upper_bound, + "Estimation {} outside bounds [{}, {}] for actual size {} (function: {}, call_type: {:?}, return_type: {:?})", + estimated, + lower_bound, + upper_bound, + actual, + function_name, + call_type, + return_type + ); + } + + #[test] + fn test_estimate_no_parameters() { + assert_estimation_accuracy( + "simple_function", + vec![], + FunctionCallType::Guest, + ReturnType::Void, + ); + } + + #[test] + fn test_estimate_single_int_parameter() { + assert_estimation_accuracy( + "add_one", + vec![ParameterValue::Int(42)], + FunctionCallType::Guest, + ReturnType::Int, + ); + } + + #[test] + fn test_estimate_multiple_scalar_parameters() { + assert_estimation_accuracy( + "calculate", + vec![ + ParameterValue::Int(10), + ParameterValue::UInt(20), + ParameterValue::Long(30), + ParameterValue::ULong(40), + ParameterValue::Float(1.5), + ParameterValue::Double(2.5), + ParameterValue::Bool(true), + ], + FunctionCallType::Guest, + ReturnType::Double, + ); + } + + #[test] + fn test_estimate_string_parameters() { + assert_estimation_accuracy( + "process_strings", + vec![ + ParameterValue::String("hello".to_string()), + ParameterValue::String("world".to_string()), + ParameterValue::String("this is a longer string for testing".to_string()), + ], + FunctionCallType::Host, + ReturnType::String, + ); + } + + #[test] + fn test_estimate_very_long_string() { + let long_string = "a".repeat(1000); + assert_estimation_accuracy( + "process_long_string", + vec![ParameterValue::String(long_string)], + FunctionCallType::Guest, + ReturnType::String, + ); + } + + #[test] + fn test_estimate_vector_parameters() { + assert_estimation_accuracy( + "process_vectors", + vec![ + ParameterValue::VecBytes(vec![1, 2, 3, 4, 5]), + ParameterValue::VecBytes(vec![]), + ParameterValue::VecBytes(vec![0; 100]), + ], + FunctionCallType::Host, + ReturnType::VecBytes, + ); + } + + #[test] + fn test_estimate_mixed_parameters() { + assert_estimation_accuracy( + "complex_function", + vec![ + ParameterValue::String("test".to_string()), + ParameterValue::Int(42), + ParameterValue::VecBytes(vec![1, 2, 3, 4, 5]), + ParameterValue::Bool(true), + ParameterValue::Double(553.14159), + ParameterValue::String("another string".to_string()), + ParameterValue::Long(9223372036854775807), + ], + FunctionCallType::Guest, + ReturnType::VecBytes, + ); + } + + #[test] + fn test_estimate_large_function_name() { + let long_name = "very_long_function_name_that_exceeds_normal_lengths_for_testing_purposes"; + assert_estimation_accuracy( + long_name, + vec![ParameterValue::Int(1)], + FunctionCallType::Host, + ReturnType::Long, + ); + } + + #[test] + fn test_estimate_large_vector() { + let large_vector = vec![42u8; 10000]; + assert_estimation_accuracy( + "process_large_data", + vec![ParameterValue::VecBytes(large_vector)], + FunctionCallType::Guest, + ReturnType::Bool, + ); + } + + #[test] + fn test_estimate_all_parameter_types() { + assert_estimation_accuracy( + "comprehensive_test", + vec![ + ParameterValue::Int(i32::MIN), + ParameterValue::UInt(u32::MAX), + ParameterValue::Long(i64::MIN), + ParameterValue::ULong(u64::MAX), + ParameterValue::Float(f32::MIN), + ParameterValue::Double(f64::MAX), + ParameterValue::Bool(false), + ParameterValue::String("test string".to_string()), + ParameterValue::VecBytes(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + ], + FunctionCallType::Host, + ReturnType::ULong, + ); + } + + #[test] + fn test_different_function_call_types() { + assert_estimation_accuracy( + "guest_function", + vec![ParameterValue::String("guest call".to_string())], + FunctionCallType::Guest, + ReturnType::String, + ); + + assert_estimation_accuracy( + "host_function", + vec![ParameterValue::String("host call".to_string())], + FunctionCallType::Host, + ReturnType::String, + ); + } + + #[test] + fn test_different_return_types() { + let args = vec![ + ParameterValue::Int(42), + ParameterValue::String("test".to_string()), + ]; + + let void_est = estimate_flatbuffer_capacity("test_void", &args); + let int_est = estimate_flatbuffer_capacity("test_int", &args); + let string_est = estimate_flatbuffer_capacity("test_string", &args); + + assert!((void_est as i32 - int_est as i32).abs() < 10); + assert!((int_est as i32 - string_est as i32).abs() < 10); + + assert_estimation_accuracy( + "test_void", + args.clone(), + FunctionCallType::Guest, + ReturnType::Void, + ); + assert_estimation_accuracy( + "test_int", + args.clone(), + FunctionCallType::Guest, + ReturnType::Int, + ); + assert_estimation_accuracy( + "test_string", + args, + FunctionCallType::Guest, + ReturnType::String, + ); + } + + #[test] + fn test_estimate_many_large_vectors_and_strings() { + assert_estimation_accuracy( + "process_bulk_data", + vec![ + ParameterValue::String("Large string data: ".to_string() + &"x".repeat(2000)), + ParameterValue::VecBytes(vec![1u8; 5000]), + ParameterValue::String( + "Another large string with lots of content ".to_string() + &"y".repeat(3000), + ), + ParameterValue::VecBytes(vec![255u8; 7500]), + ParameterValue::String( + "Third massive string parameter ".to_string() + &"z".repeat(1500), + ), + ParameterValue::VecBytes(vec![128u8; 10000]), + ParameterValue::Int(42), + ParameterValue::String("Final large string ".to_string() + &"a".repeat(4000)), + ParameterValue::VecBytes(vec![64u8; 2500]), + ParameterValue::Bool(true), + ], + FunctionCallType::Host, + ReturnType::VecBytes, + ); + } + + #[test] + fn test_estimate_twenty_parameters() { + assert_estimation_accuracy( + "function_with_many_parameters", + vec![ + ParameterValue::Int(1), + ParameterValue::String("param2".to_string()), + ParameterValue::Bool(true), + ParameterValue::Float(3213.14), + ParameterValue::VecBytes(vec![1, 2, 3]), + ParameterValue::Long(1000000), + ParameterValue::Double(322.718), + ParameterValue::UInt(42), + ParameterValue::String("param9".to_string()), + ParameterValue::Bool(false), + ParameterValue::ULong(9999999999), + ParameterValue::VecBytes(vec![4, 5, 6, 7, 8]), + ParameterValue::Int(-100), + ParameterValue::Float(1.414), + ParameterValue::String("param15".to_string()), + ParameterValue::Double(1.732), + ParameterValue::Bool(true), + ParameterValue::VecBytes(vec![9, 10]), + ParameterValue::Long(-5000000), + ParameterValue::UInt(12345), + ], + FunctionCallType::Guest, + ReturnType::Int, + ); + } + + #[test] + fn test_estimate_megabyte_parameters() { + assert_estimation_accuracy( + "process_megabyte_data", + vec![ + ParameterValue::String("MB String 1: ".to_string() + &"x".repeat(1_048_576)), // 1MB string + ParameterValue::VecBytes(vec![42u8; 2_097_152]), // 2MB vector + ParameterValue::String("MB String 2: ".to_string() + &"y".repeat(1_572_864)), // 1.5MB string + ParameterValue::VecBytes(vec![128u8; 3_145_728]), // 3MB vector + ParameterValue::String("MB String 3: ".to_string() + &"z".repeat(2_097_152)), // 2MB string + ], + FunctionCallType::Host, + ReturnType::VecBytes, + ); + } +} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/error_code_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/error_code_generated.rs index dba1ed055..b299358e3 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/error_code_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/error_code_generated.rs @@ -19,13 +19,13 @@ pub const ENUM_MIN_ERROR_CODE: u64 = 0; since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021." )] -pub const ENUM_MAX_ERROR_CODE: u64 = 16; +pub const ENUM_MAX_ERROR_CODE: u64 = 17; #[deprecated( since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021." )] #[allow(non_camel_case_types)] -pub const ENUM_VALUES_ERROR_CODE: [ErrorCode; 16] = [ +pub const ENUM_VALUES_ERROR_CODE: [ErrorCode; 17] = [ ErrorCode::NoError, ErrorCode::UnsupportedParameterType, ErrorCode::GuestFunctionNameNotProvided, @@ -42,6 +42,7 @@ pub const ENUM_VALUES_ERROR_CODE: [ErrorCode; 16] = [ ErrorCode::GuestFunctionParameterTypeMismatch, ErrorCode::GuestError, ErrorCode::ArrayLengthParamIsMissing, + ErrorCode::HostError, ]; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] @@ -65,9 +66,10 @@ impl ErrorCode { pub const GuestFunctionParameterTypeMismatch: Self = Self(14); pub const GuestError: Self = Self(15); pub const ArrayLengthParamIsMissing: Self = Self(16); + pub const HostError: Self = Self(17); pub const ENUM_MIN: u64 = 0; - pub const ENUM_MAX: u64 = 16; + pub const ENUM_MAX: u64 = 17; pub const ENUM_VALUES: &'static [Self] = &[ Self::NoError, Self::UnsupportedParameterType, @@ -85,6 +87,7 @@ impl ErrorCode { Self::GuestFunctionParameterTypeMismatch, Self::GuestError, Self::ArrayLengthParamIsMissing, + Self::HostError, ]; /// Returns the variant's name or "" if unknown. pub fn variant_name(self) -> Option<&'static str> { @@ -107,6 +110,7 @@ impl ErrorCode { Self::GuestFunctionParameterTypeMismatch => Some("GuestFunctionParameterTypeMismatch"), Self::GuestError => Some("GuestError"), Self::ArrayLengthParamIsMissing => Some("ArrayLengthParamIsMissing"), + Self::HostError => Some("HostError"), _ => None, } } @@ -124,7 +128,7 @@ impl<'a> flatbuffers::Follow<'a> for ErrorCode { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -133,7 +137,9 @@ impl flatbuffers::Push for ErrorCode { type Output = ErrorCode; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_generated.rs index 17d31fa9f..7ea5b4d63 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for FunctionCall<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } @@ -237,80 +237,3 @@ impl core::fmt::Debug for FunctionCall<'_> { ds.finish() } } -#[inline] -/// Verifies that a buffer of bytes contains a `FunctionCall` -/// and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_function_call_unchecked`. -pub fn root_as_function_call(buf: &[u8]) -> Result { - flatbuffers::root::(buf) -} -#[inline] -/// Verifies that a buffer of bytes contains a size prefixed -/// `FunctionCall` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `size_prefixed_root_as_function_call_unchecked`. -pub fn size_prefixed_root_as_function_call( - buf: &[u8], -) -> Result { - flatbuffers::size_prefixed_root::(buf) -} -#[inline] -/// Verifies, with the given options, that a buffer of bytes -/// contains a `FunctionCall` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_function_call_unchecked`. -pub fn root_as_function_call_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::root_with_opts::>(opts, buf) -} -#[inline] -/// Verifies, with the given verifier options, that a buffer of -/// bytes contains a size prefixed `FunctionCall` and returns -/// it. Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_function_call_unchecked`. -pub fn size_prefixed_root_as_function_call_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::size_prefixed_root_with_opts::>(opts, buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a FunctionCall and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid `FunctionCall`. -pub unsafe fn root_as_function_call_unchecked(buf: &[u8]) -> FunctionCall { - flatbuffers::root_unchecked::(buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a size prefixed FunctionCall and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid size prefixed `FunctionCall`. -pub unsafe fn size_prefixed_root_as_function_call_unchecked(buf: &[u8]) -> FunctionCall { - flatbuffers::size_prefixed_root_unchecked::(buf) -} -#[inline] -pub fn finish_function_call_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish(root, None); -} - -#[inline] -pub fn finish_size_prefixed_function_call_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish_size_prefixed(root, None); -} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_generated.rs index 8ff674e6c..7f26e9c00 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_generated.rs @@ -22,14 +22,14 @@ impl<'a> flatbuffers::Follow<'a> for FunctionCallResult<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } impl<'a> FunctionCallResult<'a> { - pub const VT_RETURN_VALUE_TYPE: flatbuffers::VOffsetT = 4; - pub const VT_RETURN_VALUE: flatbuffers::VOffsetT = 6; + pub const VT_RESULT_TYPE: flatbuffers::VOffsetT = 4; + pub const VT_RESULT: flatbuffers::VOffsetT = 6; #[inline] pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { @@ -41,36 +41,36 @@ impl<'a> FunctionCallResult<'a> { args: &'args FunctionCallResultArgs, ) -> flatbuffers::WIPOffset> { let mut builder = FunctionCallResultBuilder::new(_fbb); - if let Some(x) = args.return_value { - builder.add_return_value(x); + if let Some(x) = args.result { + builder.add_result(x); } - builder.add_return_value_type(args.return_value_type); + builder.add_result_type(args.result_type); builder.finish() } #[inline] - pub fn return_value_type(&self) -> ReturnValue { + pub fn result_type(&self) -> FunctionCallResultType { // Safety: // Created from valid Table for this object // which contains a valid value in this slot unsafe { self._tab - .get::( - FunctionCallResult::VT_RETURN_VALUE_TYPE, - Some(ReturnValue::NONE), + .get::( + FunctionCallResult::VT_RESULT_TYPE, + Some(FunctionCallResultType::NONE), ) .unwrap() } } #[inline] - pub fn return_value(&self) -> flatbuffers::Table<'a> { + pub fn result(&self) -> flatbuffers::Table<'a> { // Safety: // Created from valid Table for this object // which contains a valid value in this slot unsafe { self._tab .get::>>( - FunctionCallResult::VT_RETURN_VALUE, + FunctionCallResult::VT_RESULT, None, ) .unwrap() @@ -78,13 +78,13 @@ impl<'a> FunctionCallResult<'a> { } #[inline] #[allow(non_snake_case)] - pub fn return_value_as_hlint(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlint { - let u = self.return_value(); + pub fn result_as_return_value_box(&self) -> Option> { + if self.result_type() == FunctionCallResultType::ReturnValueBox { + let u = self.result(); // Safety: // Created from a valid Table for this object // Which contains a valid union in this slot - Some(unsafe { hlint::init_from_table(u) }) + Some(unsafe { ReturnValueBox::init_from_table(u) }) } else { None } @@ -92,125 +92,13 @@ impl<'a> FunctionCallResult<'a> { #[inline] #[allow(non_snake_case)] - pub fn return_value_as_hluint(&self) -> Option> { - if self.return_value_type() == ReturnValue::hluint { - let u = self.return_value(); + pub fn result_as_guest_error(&self) -> Option> { + if self.result_type() == FunctionCallResultType::GuestError { + let u = self.result(); // Safety: // Created from a valid Table for this object // Which contains a valid union in this slot - Some(unsafe { hluint::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hllong(&self) -> Option> { - if self.return_value_type() == ReturnValue::hllong { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hllong::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hlulong(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlulong { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hlulong::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hlfloat(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlfloat { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hlfloat::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hldouble(&self) -> Option> { - if self.return_value_type() == ReturnValue::hldouble { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hldouble::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hlstring(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlstring { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hlstring::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hlbool(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlbool { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hlbool::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hlvoid(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlvoid { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hlvoid::init_from_table(u) }) - } else { - None - } - } - - #[inline] - #[allow(non_snake_case)] - pub fn return_value_as_hlsizeprefixedbuffer(&self) -> Option> { - if self.return_value_type() == ReturnValue::hlsizeprefixedbuffer { - let u = self.return_value(); - // Safety: - // Created from a valid Table for this object - // Which contains a valid union in this slot - Some(unsafe { hlsizeprefixedbuffer::init_from_table(u) }) + Some(unsafe { GuestError::init_from_table(u) }) } else { None } @@ -225,61 +113,21 @@ impl flatbuffers::Verifiable for FunctionCallResult<'_> { ) -> Result<(), flatbuffers::InvalidFlatbuffer> { use self::flatbuffers::Verifiable; v.visit_table(pos)? - .visit_union::( - "return_value_type", - Self::VT_RETURN_VALUE_TYPE, - "return_value", - Self::VT_RETURN_VALUE, + .visit_union::( + "result_type", + Self::VT_RESULT_TYPE, + "result", + Self::VT_RESULT, true, |key, v, pos| match key { - ReturnValue::hlint => v - .verify_union_variant::>( - "ReturnValue::hlint", - pos, - ), - ReturnValue::hluint => v - .verify_union_variant::>( - "ReturnValue::hluint", - pos, - ), - ReturnValue::hllong => v - .verify_union_variant::>( - "ReturnValue::hllong", - pos, - ), - ReturnValue::hlulong => v - .verify_union_variant::>( - "ReturnValue::hlulong", - pos, - ), - ReturnValue::hlfloat => v - .verify_union_variant::>( - "ReturnValue::hlfloat", + FunctionCallResultType::ReturnValueBox => v + .verify_union_variant::>( + "FunctionCallResultType::ReturnValueBox", pos, ), - ReturnValue::hldouble => v - .verify_union_variant::>( - "ReturnValue::hldouble", - pos, - ), - ReturnValue::hlstring => v - .verify_union_variant::>( - "ReturnValue::hlstring", - pos, - ), - ReturnValue::hlbool => v - .verify_union_variant::>( - "ReturnValue::hlbool", - pos, - ), - ReturnValue::hlvoid => v - .verify_union_variant::>( - "ReturnValue::hlvoid", - pos, - ), - ReturnValue::hlsizeprefixedbuffer => v - .verify_union_variant::>( - "ReturnValue::hlsizeprefixedbuffer", + FunctionCallResultType::GuestError => v + .verify_union_variant::>( + "FunctionCallResultType::GuestError", pos, ), _ => Ok(()), @@ -290,15 +138,15 @@ impl flatbuffers::Verifiable for FunctionCallResult<'_> { } } pub struct FunctionCallResultArgs { - pub return_value_type: ReturnValue, - pub return_value: Option>, + pub result_type: FunctionCallResultType, + pub result: Option>, } impl<'a> Default for FunctionCallResultArgs { #[inline] fn default() -> Self { FunctionCallResultArgs { - return_value_type: ReturnValue::NONE, - return_value: None, // required field + result_type: FunctionCallResultType::NONE, + result: None, // required field } } } @@ -309,22 +157,17 @@ pub struct FunctionCallResultBuilder<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> } impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> FunctionCallResultBuilder<'a, 'b, A> { #[inline] - pub fn add_return_value_type(&mut self, return_value_type: ReturnValue) { - self.fbb_.push_slot::( - FunctionCallResult::VT_RETURN_VALUE_TYPE, - return_value_type, - ReturnValue::NONE, + pub fn add_result_type(&mut self, result_type: FunctionCallResultType) { + self.fbb_.push_slot::( + FunctionCallResult::VT_RESULT_TYPE, + result_type, + FunctionCallResultType::NONE, ); } #[inline] - pub fn add_return_value( - &mut self, - return_value: flatbuffers::WIPOffset, - ) { - self.fbb_.push_slot_always::>( - FunctionCallResult::VT_RETURN_VALUE, - return_value, - ); + pub fn add_result(&mut self, result: flatbuffers::WIPOffset) { + self.fbb_ + .push_slot_always::>(FunctionCallResult::VT_RESULT, result); } #[inline] pub fn new( @@ -340,7 +183,7 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> FunctionCallResultBuilder<'a, ' pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); self.fbb_ - .required(o, FunctionCallResult::VT_RETURN_VALUE, "return_value"); + .required(o, FunctionCallResult::VT_RESULT, "result"); flatbuffers::WIPOffset::new(o.value()) } } @@ -348,194 +191,33 @@ impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> FunctionCallResultBuilder<'a, ' impl core::fmt::Debug for FunctionCallResult<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let mut ds = f.debug_struct("FunctionCallResult"); - ds.field("return_value_type", &self.return_value_type()); - match self.return_value_type() { - ReturnValue::hlint => { - if let Some(x) = self.return_value_as_hlint() { - ds.field("return_value", &x) + ds.field("result_type", &self.result_type()); + match self.result_type() { + FunctionCallResultType::ReturnValueBox => { + if let Some(x) = self.result_as_return_value_box() { + ds.field("result", &x) } else { ds.field( - "return_value", + "result", &"InvalidFlatbuffer: Union discriminant does not match value.", ) } } - ReturnValue::hluint => { - if let Some(x) = self.return_value_as_hluint() { - ds.field("return_value", &x) + FunctionCallResultType::GuestError => { + if let Some(x) = self.result_as_guest_error() { + ds.field("result", &x) } else { ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hllong => { - if let Some(x) = self.return_value_as_hllong() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hlulong => { - if let Some(x) = self.return_value_as_hlulong() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hlfloat => { - if let Some(x) = self.return_value_as_hlfloat() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hldouble => { - if let Some(x) = self.return_value_as_hldouble() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hlstring => { - if let Some(x) = self.return_value_as_hlstring() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hlbool => { - if let Some(x) = self.return_value_as_hlbool() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hlvoid => { - if let Some(x) = self.return_value_as_hlvoid() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", - &"InvalidFlatbuffer: Union discriminant does not match value.", - ) - } - } - ReturnValue::hlsizeprefixedbuffer => { - if let Some(x) = self.return_value_as_hlsizeprefixedbuffer() { - ds.field("return_value", &x) - } else { - ds.field( - "return_value", + "result", &"InvalidFlatbuffer: Union discriminant does not match value.", ) } } _ => { let x: Option<()> = None; - ds.field("return_value", &x) + ds.field("result", &x) } }; ds.finish() } } -#[inline] -/// Verifies that a buffer of bytes contains a `FunctionCallResult` -/// and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_function_call_result_unchecked`. -pub fn root_as_function_call_result( - buf: &[u8], -) -> Result { - flatbuffers::root::(buf) -} -#[inline] -/// Verifies that a buffer of bytes contains a size prefixed -/// `FunctionCallResult` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `size_prefixed_root_as_function_call_result_unchecked`. -pub fn size_prefixed_root_as_function_call_result( - buf: &[u8], -) -> Result { - flatbuffers::size_prefixed_root::(buf) -} -#[inline] -/// Verifies, with the given options, that a buffer of bytes -/// contains a `FunctionCallResult` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_function_call_result_unchecked`. -pub fn root_as_function_call_result_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::root_with_opts::>(opts, buf) -} -#[inline] -/// Verifies, with the given verifier options, that a buffer of -/// bytes contains a size prefixed `FunctionCallResult` and returns -/// it. Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_function_call_result_unchecked`. -pub fn size_prefixed_root_as_function_call_result_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::size_prefixed_root_with_opts::>(opts, buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a FunctionCallResult and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid `FunctionCallResult`. -pub unsafe fn root_as_function_call_result_unchecked(buf: &[u8]) -> FunctionCallResult { - flatbuffers::root_unchecked::(buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a size prefixed FunctionCallResult and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid size prefixed `FunctionCallResult`. -pub unsafe fn size_prefixed_root_as_function_call_result_unchecked( - buf: &[u8], -) -> FunctionCallResult { - flatbuffers::size_prefixed_root_unchecked::(buf) -} -#[inline] -pub fn finish_function_call_result_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish(root, None); -} - -#[inline] -pub fn finish_size_prefixed_function_call_result_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish_size_prefixed(root, None); -} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_type_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_type_generated.rs new file mode 100644 index 000000000..0c54dfed4 --- /dev/null +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_result_type_generated.rs @@ -0,0 +1,110 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +extern crate flatbuffers; +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::mem; + +use self::flatbuffers::{EndianScalar, Follow}; +use super::*; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +pub const ENUM_MIN_FUNCTION_CALL_RESULT_TYPE: u8 = 0; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +pub const ENUM_MAX_FUNCTION_CALL_RESULT_TYPE: u8 = 2; +#[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." +)] +#[allow(non_camel_case_types)] +pub const ENUM_VALUES_FUNCTION_CALL_RESULT_TYPE: [FunctionCallResultType; 3] = [ + FunctionCallResultType::NONE, + FunctionCallResultType::ReturnValueBox, + FunctionCallResultType::GuestError, +]; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(transparent)] +pub struct FunctionCallResultType(pub u8); +#[allow(non_upper_case_globals)] +impl FunctionCallResultType { + pub const NONE: Self = Self(0); + pub const ReturnValueBox: Self = Self(1); + pub const GuestError: Self = Self(2); + + pub const ENUM_MIN: u8 = 0; + pub const ENUM_MAX: u8 = 2; + pub const ENUM_VALUES: &'static [Self] = &[Self::NONE, Self::ReturnValueBox, Self::GuestError]; + /// Returns the variant's name or "" if unknown. + pub fn variant_name(self) -> Option<&'static str> { + match self { + Self::NONE => Some("NONE"), + Self::ReturnValueBox => Some("ReturnValueBox"), + Self::GuestError => Some("GuestError"), + _ => None, + } + } +} +impl core::fmt::Debug for FunctionCallResultType { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if let Some(name) = self.variant_name() { + f.write_str(name) + } else { + f.write_fmt(format_args!("", self.0)) + } + } +} +impl<'a> flatbuffers::Follow<'a> for FunctionCallResultType { + type Inner = Self; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; + Self(b) + } +} + +impl flatbuffers::Push for FunctionCallResultType { + type Output = FunctionCallResultType; + #[inline] + unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } + } +} + +impl flatbuffers::EndianScalar for FunctionCallResultType { + type Scalar = u8; + #[inline] + fn to_little_endian(self) -> u8 { + self.0.to_le() + } + #[inline] + #[allow(clippy::wrong_self_convention)] + fn from_little_endian(v: u8) -> Self { + let b = u8::from_le(v); + Self(b) + } +} + +impl<'a> flatbuffers::Verifiable for FunctionCallResultType { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, + pos: usize, + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + u8::run_verifier(v, pos) + } +} + +impl flatbuffers::SimpleToVerifyInSlice for FunctionCallResultType {} +pub struct FunctionCallResultTypeUnionTableOffset {} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_type_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_type_generated.rs index 67edacdf9..84356d89a 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_type_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/function_call_type_generated.rs @@ -66,7 +66,7 @@ impl<'a> flatbuffers::Follow<'a> for FunctionCallType { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -75,7 +75,9 @@ impl flatbuffers::Push for FunctionCallType { type Output = FunctionCallType; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_error_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_error_generated.rs index 57887a31b..21ef559b6 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_error_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_error_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for GuestError<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } @@ -139,80 +139,3 @@ impl core::fmt::Debug for GuestError<'_> { ds.finish() } } -#[inline] -/// Verifies that a buffer of bytes contains a `GuestError` -/// and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_guest_error_unchecked`. -pub fn root_as_guest_error(buf: &[u8]) -> Result { - flatbuffers::root::(buf) -} -#[inline] -/// Verifies that a buffer of bytes contains a size prefixed -/// `GuestError` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `size_prefixed_root_as_guest_error_unchecked`. -pub fn size_prefixed_root_as_guest_error( - buf: &[u8], -) -> Result { - flatbuffers::size_prefixed_root::(buf) -} -#[inline] -/// Verifies, with the given options, that a buffer of bytes -/// contains a `GuestError` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_guest_error_unchecked`. -pub fn root_as_guest_error_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::root_with_opts::>(opts, buf) -} -#[inline] -/// Verifies, with the given verifier options, that a buffer of -/// bytes contains a size prefixed `GuestError` and returns -/// it. Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_guest_error_unchecked`. -pub fn size_prefixed_root_as_guest_error_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::size_prefixed_root_with_opts::>(opts, buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a GuestError and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid `GuestError`. -pub unsafe fn root_as_guest_error_unchecked(buf: &[u8]) -> GuestError { - flatbuffers::root_unchecked::(buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a size prefixed GuestError and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid size prefixed `GuestError`. -pub unsafe fn size_prefixed_root_as_guest_error_unchecked(buf: &[u8]) -> GuestError { - flatbuffers::size_prefixed_root_unchecked::(buf) -} -#[inline] -pub fn finish_guest_error_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish(root, None); -} - -#[inline] -pub fn finish_size_prefixed_guest_error_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish_size_prefixed(root, None); -} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_log_data_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_log_data_generated.rs index 8deec7cad..d3de70427 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_log_data_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/guest_log_data_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for GuestLogData<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } @@ -235,80 +235,3 @@ impl core::fmt::Debug for GuestLogData<'_> { ds.finish() } } -#[inline] -/// Verifies that a buffer of bytes contains a `GuestLogData` -/// and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_guest_log_data_unchecked`. -pub fn root_as_guest_log_data(buf: &[u8]) -> Result { - flatbuffers::root::(buf) -} -#[inline] -/// Verifies that a buffer of bytes contains a size prefixed -/// `GuestLogData` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `size_prefixed_root_as_guest_log_data_unchecked`. -pub fn size_prefixed_root_as_guest_log_data( - buf: &[u8], -) -> Result { - flatbuffers::size_prefixed_root::(buf) -} -#[inline] -/// Verifies, with the given options, that a buffer of bytes -/// contains a `GuestLogData` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_guest_log_data_unchecked`. -pub fn root_as_guest_log_data_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::root_with_opts::>(opts, buf) -} -#[inline] -/// Verifies, with the given verifier options, that a buffer of -/// bytes contains a size prefixed `GuestLogData` and returns -/// it. Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_guest_log_data_unchecked`. -pub fn size_prefixed_root_as_guest_log_data_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::size_prefixed_root_with_opts::>(opts, buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a GuestLogData and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid `GuestLogData`. -pub unsafe fn root_as_guest_log_data_unchecked(buf: &[u8]) -> GuestLogData { - flatbuffers::root_unchecked::(buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a size prefixed GuestLogData and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid size prefixed `GuestLogData`. -pub unsafe fn size_prefixed_root_as_guest_log_data_unchecked(buf: &[u8]) -> GuestLogData { - flatbuffers::size_prefixed_root_unchecked::(buf) -} -#[inline] -pub fn finish_guest_log_data_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish(root, None); -} - -#[inline] -pub fn finish_size_prefixed_guest_log_data_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish_size_prefixed(root, None); -} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlbool_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlbool_generated.rs index d2005e5a5..c4a2b6673 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlbool_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlbool_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlbool<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hldouble_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hldouble_generated.rs index 5c71c2c9e..0cbabb8b1 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hldouble_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hldouble_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hldouble<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlfloat_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlfloat_generated.rs index 93673efe1..ca59467d4 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlfloat_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlfloat_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlfloat<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlint_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlint_generated.rs index a162f6eec..726f3ba29 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlint_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlint_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlint<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hllong_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hllong_generated.rs index 690afaa26..7c088ff8c 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hllong_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hllong_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hllong<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlsizeprefixedbuffer_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlsizeprefixedbuffer_generated.rs index ed1e74b37..cbbb2729c 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlsizeprefixedbuffer_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlsizeprefixedbuffer_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlsizeprefixedbuffer<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlstring_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlstring_generated.rs index ba8c2eb7f..cb2d7db09 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlstring_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlstring_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlstring<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hluint_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hluint_generated.rs index e10699658..67f8cbd6d 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hluint_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hluint_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hluint<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlulong_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlulong_generated.rs index db83b936d..f701f3d6c 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlulong_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlulong_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlulong<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvecbytes_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvecbytes_generated.rs index e673cd425..8b1c4a109 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvecbytes_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvecbytes_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlvecbytes<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvoid_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvoid_generated.rs index 76e0e9f97..a218c60ba 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvoid_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/hlvoid_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for hlvoid<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_definition_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_definition_generated.rs index 381f77fcc..8ffb6f35e 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_definition_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_definition_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for HostFunctionDefinition<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_details_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_details_generated.rs index 71edd22b3..54cf45c73 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_details_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/host_function_details_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for HostFunctionDetails<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } @@ -132,84 +132,3 @@ impl core::fmt::Debug for HostFunctionDetails<'_> { ds.finish() } } -#[inline] -/// Verifies that a buffer of bytes contains a `HostFunctionDetails` -/// and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_host_function_details_unchecked`. -pub fn root_as_host_function_details( - buf: &[u8], -) -> Result { - flatbuffers::root::(buf) -} -#[inline] -/// Verifies that a buffer of bytes contains a size prefixed -/// `HostFunctionDetails` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `size_prefixed_root_as_host_function_details_unchecked`. -pub fn size_prefixed_root_as_host_function_details( - buf: &[u8], -) -> Result { - flatbuffers::size_prefixed_root::(buf) -} -#[inline] -/// Verifies, with the given options, that a buffer of bytes -/// contains a `HostFunctionDetails` and returns it. -/// Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_host_function_details_unchecked`. -pub fn root_as_host_function_details_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::root_with_opts::>(opts, buf) -} -#[inline] -/// Verifies, with the given verifier options, that a buffer of -/// bytes contains a size prefixed `HostFunctionDetails` and returns -/// it. Note that verification is still experimental and may not -/// catch every error, or be maximally performant. For the -/// previous, unchecked, behavior use -/// `root_as_host_function_details_unchecked`. -pub fn size_prefixed_root_as_host_function_details_with_opts<'b, 'o>( - opts: &'o flatbuffers::VerifierOptions, - buf: &'b [u8], -) -> Result, flatbuffers::InvalidFlatbuffer> { - flatbuffers::size_prefixed_root_with_opts::>(opts, buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a HostFunctionDetails and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid `HostFunctionDetails`. -pub unsafe fn root_as_host_function_details_unchecked(buf: &[u8]) -> HostFunctionDetails { - flatbuffers::root_unchecked::(buf) -} -#[inline] -/// Assumes, without verification, that a buffer of bytes contains a size prefixed HostFunctionDetails and returns it. -/// # Safety -/// Callers must trust the given bytes do indeed contain a valid size prefixed `HostFunctionDetails`. -pub unsafe fn size_prefixed_root_as_host_function_details_unchecked( - buf: &[u8], -) -> HostFunctionDetails { - flatbuffers::size_prefixed_root_unchecked::(buf) -} -#[inline] -pub fn finish_host_function_details_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish(root, None); -} - -#[inline] -pub fn finish_size_prefixed_host_function_details_buffer<'a, 'b, A: flatbuffers::Allocator + 'a>( - fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, - root: flatbuffers::WIPOffset>, -) { - fbb.finish_size_prefixed(root, None); -} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/log_level_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/log_level_generated.rs index a47458489..6fb5ce2c6 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/log_level_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/log_level_generated.rs @@ -86,7 +86,7 @@ impl<'a> flatbuffers::Follow<'a> for LogLevel { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -95,7 +95,9 @@ impl flatbuffers::Push for LogLevel { type Output = LogLevel; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_generated.rs index 41e80c4ac..b0e803ec5 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_generated.rs @@ -22,7 +22,7 @@ impl<'a> flatbuffers::Follow<'a> for Parameter<'a> { #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { - _tab: flatbuffers::Table::new(buf, loc), + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_type_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_type_generated.rs index 5ef4b56e1..cf46560b1 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_type_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_type_generated.rs @@ -94,7 +94,7 @@ impl<'a> flatbuffers::Follow<'a> for ParameterType { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -103,7 +103,9 @@ impl flatbuffers::Push for ParameterType { type Output = ParameterType; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_value_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_value_generated.rs index 91d51b456..8113df5fc 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_value_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/parameter_value_generated.rs @@ -98,7 +98,7 @@ impl<'a> flatbuffers::Follow<'a> for ParameterValue { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -107,7 +107,9 @@ impl flatbuffers::Push for ParameterValue { type Output = ParameterValue; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_type_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_type_generated.rs index 0610cdd55..913b1fe78 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_type_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_type_generated.rs @@ -98,7 +98,7 @@ impl<'a> flatbuffers::Follow<'a> for ReturnType { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -107,7 +107,9 @@ impl flatbuffers::Push for ReturnType { type Output = ReturnType; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_box_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_box_generated.rs new file mode 100644 index 000000000..cecd8b6c1 --- /dev/null +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_box_generated.rs @@ -0,0 +1,451 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// @generated +extern crate alloc; +extern crate flatbuffers; +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::mem; + +use self::flatbuffers::{EndianScalar, Follow}; +use super::*; +pub enum ReturnValueBoxOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct ReturnValueBox<'a> { + pub _tab: flatbuffers::Table<'a>, +} + +impl<'a> flatbuffers::Follow<'a> for ReturnValueBox<'a> { + type Inner = ReturnValueBox<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: unsafe { flatbuffers::Table::new(buf, loc) }, + } + } +} + +impl<'a> ReturnValueBox<'a> { + pub const VT_VALUE_TYPE: flatbuffers::VOffsetT = 4; + pub const VT_VALUE: flatbuffers::VOffsetT = 6; + + #[inline] + pub unsafe fn init_from_table(table: flatbuffers::Table<'a>) -> Self { + ReturnValueBox { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args ReturnValueBoxArgs, + ) -> flatbuffers::WIPOffset> { + let mut builder = ReturnValueBoxBuilder::new(_fbb); + if let Some(x) = args.value { + builder.add_value(x); + } + builder.add_value_type(args.value_type); + builder.finish() + } + + #[inline] + pub fn value_type(&self) -> ReturnValue { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::(ReturnValueBox::VT_VALUE_TYPE, Some(ReturnValue::NONE)) + .unwrap() + } + } + #[inline] + pub fn value(&self) -> flatbuffers::Table<'a> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { + self._tab + .get::>>( + ReturnValueBox::VT_VALUE, + None, + ) + .unwrap() + } + } + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlint(&self) -> Option> { + if self.value_type() == ReturnValue::hlint { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlint::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hluint(&self) -> Option> { + if self.value_type() == ReturnValue::hluint { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hluint::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hllong(&self) -> Option> { + if self.value_type() == ReturnValue::hllong { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hllong::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlulong(&self) -> Option> { + if self.value_type() == ReturnValue::hlulong { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlulong::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlfloat(&self) -> Option> { + if self.value_type() == ReturnValue::hlfloat { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlfloat::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hldouble(&self) -> Option> { + if self.value_type() == ReturnValue::hldouble { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hldouble::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlstring(&self) -> Option> { + if self.value_type() == ReturnValue::hlstring { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlstring::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlbool(&self) -> Option> { + if self.value_type() == ReturnValue::hlbool { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlbool::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlvoid(&self) -> Option> { + if self.value_type() == ReturnValue::hlvoid { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlvoid::init_from_table(u) }) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn value_as_hlsizeprefixedbuffer(&self) -> Option> { + if self.value_type() == ReturnValue::hlsizeprefixedbuffer { + let u = self.value(); + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + Some(unsafe { hlsizeprefixedbuffer::init_from_table(u) }) + } else { + None + } + } +} + +impl flatbuffers::Verifiable for ReturnValueBox<'_> { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, + pos: usize, + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + v.visit_table(pos)? + .visit_union::( + "value_type", + Self::VT_VALUE_TYPE, + "value", + Self::VT_VALUE, + true, + |key, v, pos| match key { + ReturnValue::hlint => v + .verify_union_variant::>( + "ReturnValue::hlint", + pos, + ), + ReturnValue::hluint => v + .verify_union_variant::>( + "ReturnValue::hluint", + pos, + ), + ReturnValue::hllong => v + .verify_union_variant::>( + "ReturnValue::hllong", + pos, + ), + ReturnValue::hlulong => v + .verify_union_variant::>( + "ReturnValue::hlulong", + pos, + ), + ReturnValue::hlfloat => v + .verify_union_variant::>( + "ReturnValue::hlfloat", + pos, + ), + ReturnValue::hldouble => v + .verify_union_variant::>( + "ReturnValue::hldouble", + pos, + ), + ReturnValue::hlstring => v + .verify_union_variant::>( + "ReturnValue::hlstring", + pos, + ), + ReturnValue::hlbool => v + .verify_union_variant::>( + "ReturnValue::hlbool", + pos, + ), + ReturnValue::hlvoid => v + .verify_union_variant::>( + "ReturnValue::hlvoid", + pos, + ), + ReturnValue::hlsizeprefixedbuffer => v + .verify_union_variant::>( + "ReturnValue::hlsizeprefixedbuffer", + pos, + ), + _ => Ok(()), + }, + )? + .finish(); + Ok(()) + } +} +pub struct ReturnValueBoxArgs { + pub value_type: ReturnValue, + pub value: Option>, +} +impl<'a> Default for ReturnValueBoxArgs { + #[inline] + fn default() -> Self { + ReturnValueBoxArgs { + value_type: ReturnValue::NONE, + value: None, // required field + } + } +} + +pub struct ReturnValueBoxBuilder<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> { + fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, + start_: flatbuffers::WIPOffset, +} +impl<'a: 'b, 'b, A: flatbuffers::Allocator + 'a> ReturnValueBoxBuilder<'a, 'b, A> { + #[inline] + pub fn add_value_type(&mut self, value_type: ReturnValue) { + self.fbb_.push_slot::( + ReturnValueBox::VT_VALUE_TYPE, + value_type, + ReturnValue::NONE, + ); + } + #[inline] + pub fn add_value(&mut self, value: flatbuffers::WIPOffset) { + self.fbb_ + .push_slot_always::>(ReturnValueBox::VT_VALUE, value); + } + #[inline] + pub fn new( + _fbb: &'b mut flatbuffers::FlatBufferBuilder<'a, A>, + ) -> ReturnValueBoxBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + ReturnValueBoxBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + self.fbb_.required(o, ReturnValueBox::VT_VALUE, "value"); + flatbuffers::WIPOffset::new(o.value()) + } +} + +impl core::fmt::Debug for ReturnValueBox<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut ds = f.debug_struct("ReturnValueBox"); + ds.field("value_type", &self.value_type()); + match self.value_type() { + ReturnValue::hlint => { + if let Some(x) = self.value_as_hlint() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hluint => { + if let Some(x) = self.value_as_hluint() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hllong => { + if let Some(x) = self.value_as_hllong() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hlulong => { + if let Some(x) = self.value_as_hlulong() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hlfloat => { + if let Some(x) = self.value_as_hlfloat() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hldouble => { + if let Some(x) = self.value_as_hldouble() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hlstring => { + if let Some(x) = self.value_as_hlstring() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hlbool => { + if let Some(x) = self.value_as_hlbool() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hlvoid => { + if let Some(x) = self.value_as_hlvoid() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + ReturnValue::hlsizeprefixedbuffer => { + if let Some(x) = self.value_as_hlsizeprefixedbuffer() { + ds.field("value", &x) + } else { + ds.field( + "value", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + _ => { + let x: Option<()> = None; + ds.field("value", &x) + } + }; + ds.finish() + } +} diff --git a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_generated.rs b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_generated.rs index 2c9f94872..d13c73623 100644 --- a/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_generated.rs +++ b/src/hyperlight_common/src/flatbuffers/hyperlight/generated/return_value_generated.rs @@ -102,7 +102,7 @@ impl<'a> flatbuffers::Follow<'a> for ReturnValue { type Inner = Self; #[inline] unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - let b = flatbuffers::read_scalar_at::(buf, loc); + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; Self(b) } } @@ -111,7 +111,9 @@ impl flatbuffers::Push for ReturnValue { type Output = ReturnValue; #[inline] unsafe fn push(&self, dst: &mut [u8], _written_len: usize) { - flatbuffers::emplace_scalar::(dst, self.0); + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } } } diff --git a/src/hyperlight_common/src/flatbuffers/mod.rs b/src/hyperlight_common/src/flatbuffers/mod.rs index f8ac0929d..bf8582877 100644 --- a/src/hyperlight_common/src/flatbuffers/mod.rs +++ b/src/hyperlight_common/src/flatbuffers/mod.rs @@ -12,6 +12,14 @@ pub mod hyperlight { pub use self::return_type_generated::*; mod return_value_generated; pub use self::return_value_generated::*; + mod error_code_generated; + pub use self::error_code_generated::*; + mod function_call_result_type_generated; + pub use self::function_call_result_type_generated::*; + mod function_call_type_generated; + pub use self::function_call_type_generated::*; + mod log_level_generated; + pub use self::log_level_generated::*; mod hlint_generated; pub use self::hlint_generated::*; mod hluint_generated; @@ -30,29 +38,25 @@ pub mod hyperlight { pub use self::hlbool_generated::*; mod hlvecbytes_generated; pub use self::hlvecbytes_generated::*; + mod hlsizeprefixedbuffer_generated; + pub use self::hlsizeprefixedbuffer_generated::*; mod hlvoid_generated; pub use self::hlvoid_generated::*; + mod guest_error_generated; + pub use self::guest_error_generated::*; + mod return_value_box_generated; + pub use self::return_value_box_generated::*; mod function_call_result_generated; pub use self::function_call_result_generated::*; mod parameter_generated; pub use self::parameter_generated::*; mod function_call_generated; pub use self::function_call_generated::*; - mod function_call_type_generated; - pub use self::function_call_type_generated::*; - mod error_code_generated; - pub use self::error_code_generated::*; - mod guest_error_generated; - pub use self::guest_error_generated::*; + mod guest_log_data_generated; + pub use self::guest_log_data_generated::*; mod host_function_definition_generated; pub use self::host_function_definition_generated::*; mod host_function_details_generated; pub use self::host_function_details_generated::*; - mod hlsizeprefixedbuffer_generated; - pub use self::hlsizeprefixedbuffer_generated::*; - mod log_level_generated; - pub use self::log_level_generated::*; - mod guest_log_data_generated; - pub use self::guest_log_data_generated::*; - } -} + } // generated +} // hyperlight diff --git a/src/hyperlight_common/src/lib.rs b/src/hyperlight_common/src/lib.rs index e22cf6417..3ec6aa6fc 100644 --- a/src/hyperlight_common/src/lib.rs +++ b/src/hyperlight_common/src/lib.rs @@ -25,14 +25,7 @@ extern crate alloc; pub mod flatbuffer_wrappers; /// cbindgen:ignore /// FlatBuffers-related utilities and (mostly) generated code -#[allow( - dead_code, - unused_imports, - clippy::all, - clippy::unwrap_used, - unsafe_op_in_unsafe_fn, - non_camel_case_types -)] +#[allow(clippy::all, warnings)] mod flatbuffers; /// cbindgen:ignore pub mod mem; diff --git a/src/hyperlight_common/src/outb.rs b/src/hyperlight_common/src/outb.rs index 1d15cfb90..b060ae5cf 100644 --- a/src/hyperlight_common/src/outb.rs +++ b/src/hyperlight_common/src/outb.rs @@ -77,7 +77,7 @@ impl TryFrom for Exception { 19 => SIMDFloatingPointException, 20 => VirtualizationException, 30 => SecurityException, - 0x7F => NoException, + 0xFF => NoException, _ => return Err(anyhow!("Unknown exception code: {:#x}", value)), }; @@ -90,11 +90,20 @@ impl TryFrom for Exception { /// - CallFunction: makes a call to a host function, /// - Abort: aborts the execution of the guest, /// - DebugPrint: prints a message to the host +/// - TraceBatch: reports a batch of spans and events from the guest +/// - TraceMemoryAlloc: records memory allocation events +/// - TraceMemoryFree: records memory deallocation events pub enum OutBAction { Log = 99, CallFunction = 101, Abort = 102, DebugPrint = 103, + #[cfg(feature = "trace_guest")] + TraceBatch = 104, + #[cfg(feature = "mem_profile")] + TraceMemoryAlloc = 105, + #[cfg(feature = "mem_profile")] + TraceMemoryFree = 106, } impl TryFrom for OutBAction { @@ -105,6 +114,12 @@ impl TryFrom for OutBAction { 101 => Ok(OutBAction::CallFunction), 102 => Ok(OutBAction::Abort), 103 => Ok(OutBAction::DebugPrint), + #[cfg(feature = "trace_guest")] + 104 => Ok(OutBAction::TraceBatch), + #[cfg(feature = "mem_profile")] + 105 => Ok(OutBAction::TraceMemoryAlloc), + #[cfg(feature = "mem_profile")] + 106 => Ok(OutBAction::TraceMemoryFree), _ => Err(anyhow::anyhow!("Invalid OutBAction value: {}", val)), } } diff --git a/src/hyperlight_component_macro/Cargo.toml b/src/hyperlight_component_macro/Cargo.toml index b23c0fef9..c9e2f4406 100644 --- a/src/hyperlight_component_macro/Cargo.toml +++ b/src/hyperlight_component_macro/Cargo.toml @@ -16,11 +16,11 @@ name = "hyperlight_component_macro" proc-macro = true [dependencies] -wasmparser = { version = "0.224.0" } -quote = { version = "1.0.38" } -proc-macro2 = { version = "1.0.95" } -syn = { version = "2.0.104" } +wasmparser = { version = "0.240.0" } +quote = { version = "1.0.41" } +proc-macro2 = { version = "1.0.103" } +syn = { version = "2.0.108" } itertools = { version = "0.14.0" } -prettyplease = { version = "0.2.35" } +prettyplease = { version = "0.2.37" } hyperlight-component-util = { workspace = true } env_logger = { version = "0.11.8" } \ No newline at end of file diff --git a/src/hyperlight_component_util/Cargo.toml b/src/hyperlight_component_util/Cargo.toml index ece9bdb79..338335f10 100644 --- a/src/hyperlight_component_util/Cargo.toml +++ b/src/hyperlight_component_util/Cargo.toml @@ -15,10 +15,10 @@ Shared implementation for the procedural macros that generate Hyperlight host an name = "hyperlight_component_util" [dependencies] -wasmparser = { version = "0.224.0" } -quote = { version = "1.0.38" } -proc-macro2 = { version = "1.0.95" } -syn = { version = "2.0.104" } +wasmparser = { version = "0.240.0" } +quote = { version = "1.0.41" } +proc-macro2 = { version = "1.0.103" } +syn = { version = "2.0.108" } itertools = { version = "0.14.0" } -prettyplease = { version = "0.2.35" } +prettyplease = { version = "0.2.37" } log = { version = "0.4" } \ No newline at end of file diff --git a/src/hyperlight_component_util/src/elaborate.rs b/src/hyperlight_component_util/src/elaborate.rs index c38051725..120092922 100644 --- a/src/hyperlight_component_util/src/elaborate.rs +++ b/src/hyperlight_component_util/src/elaborate.rs @@ -24,14 +24,14 @@ limitations under the License. //! substitute.rs for more details of the approach here). use wasmparser::{ - ComponentAlias, ComponentDefinedType, ComponentFuncResult, ComponentFuncType, - ComponentOuterAliasKind, ComponentType, ComponentTypeDeclaration, ComponentTypeRef, - ComponentValType, CompositeInnerType, CoreType, InstanceTypeDeclaration, ModuleTypeDeclaration, - OuterAliasKind, PrimitiveValType, TypeBounds, TypeRef, + ComponentAlias, ComponentDefinedType, ComponentFuncType, ComponentOuterAliasKind, + ComponentType, ComponentTypeDeclaration, ComponentTypeRef, ComponentValType, + CompositeInnerType, CoreType, InstanceTypeDeclaration, ModuleTypeDeclaration, OuterAliasKind, + PrimitiveValType, TypeBounds, TypeRef, }; use crate::etypes::{ - self, BoundedTyvar, Component, CoreDefined, CoreExportDecl, CoreExternDesc, CoreModule, + BoundedTyvar, Component, CoreDefined, CoreExportDecl, CoreExternDesc, CoreModule, CoreOrComponentExternDesc, Ctx, Defined, ExternDecl, ExternDesc, FloatWidth, Func, Handleable, Instance, IntWidth, Name, Param, QualifiedInstance, RecordField, Resource, ResourceId, TypeBound, Tyvar, Value, VariantCase, @@ -366,6 +366,7 @@ impl<'p, 'a> Ctx<'p, 'a> { PrimitiveValType::F64 => Value::F(FloatWidth::F64), PrimitiveValType::Char => Value::Char, PrimitiveValType::String => Value::String, + PrimitiveValType::ErrorContext => panic!("async not yet supported"), }), } } @@ -428,9 +429,12 @@ impl<'p, 'a> Ctx<'p, 'a> { Defined::Handleable(h) => Ok(Value::Borrow(h.clone())), _ => Err(Error::HandleToNonResource), }, - ComponentDefinedType::Future(_) - | ComponentDefinedType::Stream(_) - | ComponentDefinedType::ErrorContext => panic!("async not yet supported"), + ComponentDefinedType::FixedSizeList(vt, size) => { + Ok(Value::FixList(Box::new(self.elab_value(vt)?), *size)) + } + ComponentDefinedType::Future(_) | ComponentDefinedType::Stream(_) => { + panic!("async not yet supported") + } } } @@ -446,19 +450,11 @@ impl<'p, 'a> Ctx<'p, 'a> { }) }) .collect::, Error<'a>>>()?, - result: match &ft.results { - ComponentFuncResult::Unnamed(vt) => etypes::Result::Unnamed(self.elab_value(vt)?), - ComponentFuncResult::Named(rs) => etypes::Result::Named( - rs.iter() - .map(|(n, vt)| { - Ok(Param { - name: Name { name: n }, - ty: self.elab_value(vt)?, - }) - }) - .collect::, Error<'a>>>()?, - ), - }, + result: ft + .result + .as_ref() + .map(|vt| self.elab_value(vt)) + .transpose()?, }) } diff --git a/src/hyperlight_component_util/src/emit.rs b/src/hyperlight_component_util/src/emit.rs index 447467aff..1b3545813 100644 --- a/src/hyperlight_component_util/src/emit.rs +++ b/src/hyperlight_component_util/src/emit.rs @@ -196,6 +196,28 @@ impl Mod { } } +/// Unlike [`tv::ResolvedTyvar`], which is mostly concerned with free +/// variables and leaves bound variables alone, this tells us the most +/// information that we have at codegen time for a top level bound +/// variable. +pub enum ResolvedBoundVar<'a> { + Definite { + /// The final variable offset (relative to s.var_offset) that + /// we followed to get to this definite type, used + /// occasionally to name things. + final_bound_var: u32, + /// The actual definite type that this resolved to + ty: Defined<'a>, + }, + Resource { + /// A resource-type index. Currently a resource-type index is + /// the same as the de Bruijn index of the tyvar that + /// introduced the resource type, but is never affected by + /// e.g. s.var_offset. + rtidx: u32, + }, +} + /// A whole grab-bag of useful state to have while emitting Rust #[derive(Debug)] pub struct State<'a, 'b> { @@ -260,6 +282,8 @@ pub struct State<'a, 'b> { /// wasmtime guest emit. When that is refactored to use the host /// guest emit, this can go away. pub is_wasmtime_guest: bool, + /// Are we working on an export or an import of the component type? + pub is_export: bool, } /// Create a State with all of its &mut references pointing to @@ -311,6 +335,7 @@ impl<'a, 'b> State<'a, 'b> { root_component_name: None, is_guest, is_wasmtime_guest, + is_export: false, } } pub fn clone<'c>(&'c mut self) -> State<'c, 'b> { @@ -331,6 +356,7 @@ impl<'a, 'b> State<'a, 'b> { root_component_name: self.root_component_name.clone(), is_guest: self.is_guest, is_wasmtime_guest: self.is_wasmtime_guest, + is_export: self.is_export, } } /// Obtain a reference to the [`Mod`] that we are currently @@ -508,9 +534,17 @@ impl<'a, 'b> State<'a, 'b> { } /// Add an import/export to [`State::origin`], reflecting that we are now /// looking at code underneath it - pub fn push_origin<'c>(&'c mut self, is_export: bool, name: &'b str) -> State<'c, 'b> { + /// + /// origin_was_export differs from s.is_export in that s.is_export + /// keeps track of whether the item overall was imported or exported + /// from the root component (taking into account positivity), whereas + /// origin_was_export just checks if this particular extern_decl was + /// imported or exported from its parent instance (and so e.g. an + /// export of an instance that is imported by the root component has + /// !s.is_export && origin_was_export) + pub fn push_origin<'c>(&'c mut self, origin_was_export: bool, name: &'b str) -> State<'c, 'b> { let mut s = self.clone(); - s.origin.push(if is_export { + s.origin.push(if origin_was_export { ImportExport::Export(name) } else { ImportExport::Import(name) @@ -588,15 +622,24 @@ impl<'a, 'b> State<'a, 'b> { /// up with a definition, in which case, let's get that, or it /// ends up with a resource type, in which case we return the /// resource index - pub fn resolve_tv(&self, n: u32) -> (u32, Option>) { - match &self.bound_vars[self.var_offset + n as usize].bound { + /// + /// Distinct from [`Ctx::resolve_tv`], which is mostly concerned + /// with free variables, because this is concerned entirely with + /// bound variables. + pub fn resolve_bound_var(&self, n: u32) -> ResolvedBoundVar<'b> { + let noff = self.var_offset as u32 + n; + match &self.bound_vars[noff as usize].bound { TypeBound::Eq(Defined::Handleable(Handleable::Var(Tyvar::Bound(nn)))) => { - self.resolve_tv(n + 1 + nn) + self.resolve_bound_var(n + 1 + nn) } - TypeBound::Eq(t) => (n, Some(t.clone())), - TypeBound::SubResource => (n, None), + TypeBound::Eq(t) => ResolvedBoundVar::Definite { + final_bound_var: n, + ty: t.clone(), + }, + TypeBound::SubResource => ResolvedBoundVar::Resource { rtidx: noff }, } } + /// Construct a namespace path referring to the resource trait for /// a resource with the given name pub fn resource_trait_path(&self, r: Ident) -> Vec { @@ -621,7 +664,7 @@ pub struct WitName<'a> { pub _version: Vec<&'a str>, } impl<'a> WitName<'a> { - /// Extract a list of Rust module names correspondign to the WIT + /// Extract a list of Rust module names corresponding to the WIT /// namespace/package pub fn namespace_idents(&self) -> Vec { self.namespaces @@ -637,7 +680,7 @@ impl<'a> WitName<'a> { } } /// Parse a kebab-name as a WIT name -pub fn split_wit_name(n: &str) -> WitName { +pub fn split_wit_name(n: &str) -> WitName<'_> { let mut namespaces = Vec::new(); let mut colon_components = n.split(':').rev(); let last = colon_components.next().unwrap(); diff --git a/src/hyperlight_component_util/src/etypes.rs b/src/hyperlight_component_util/src/etypes.rs index 0cec42887..5723eaf9a 100644 --- a/src/hyperlight_component_util/src/etypes.rs +++ b/src/hyperlight_component_util/src/etypes.rs @@ -83,6 +83,7 @@ pub enum Value<'a> { Char, String, List(Box>), + FixList(Box>, u32), Record(Vec>), Tuple(Vec>), Flags(Vec>), @@ -136,11 +137,7 @@ pub struct Param<'a> { pub ty: Value<'a>, } -#[derive(Debug, Clone)] -pub enum Result<'a> { - Unnamed(Value<'a>), - Named(Vec>), -} +pub type Result<'a> = Option>; /// functype_e in the specification #[derive(Debug, Clone)] diff --git a/src/hyperlight_component_util/src/guest.rs b/src/hyperlight_component_util/src/guest.rs index 864f9cd0f..09e909034 100644 --- a/src/hyperlight_component_util/src/guest.rs +++ b/src/hyperlight_component_util/src/guest.rs @@ -18,8 +18,9 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use crate::emit::{ - FnName, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, kebab_to_getter, - kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name, + FnName, ResolvedBoundVar, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, + kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, + split_wit_name, }; use crate::etypes::{Component, Defined, ExternDecl, ExternDesc, Handleable, Instance, Tyvar}; use crate::hl::{ @@ -98,10 +99,10 @@ fn emit_import_extern_decl<'a, 'b, 'c>( ExternDesc::Type(t) => match t { Defined::Handleable(Handleable::Var(Tyvar::Bound(b))) => { // only resources need something emitted - let (b, None) = s.resolve_tv(*b) else { + let ResolvedBoundVar::Resource { rtidx } = s.resolve_bound_var(*b) else { return quote! {}; }; - let rtid = format_ident!("HostResource{}", s.var_offset + b as usize); + let rtid = format_ident!("HostResource{}", rtidx as usize); let path = s.resource_trait_path(kebab_to_type(ed.kebab_name)); s.root_mod .r#impl(path, format_ident!("Host")) @@ -314,6 +315,8 @@ fn emit_component<'a, 'b, 'c>( s.var_offset = 0; + s.is_export = true; + let exports = ct .instance .unqualified diff --git a/src/hyperlight_component_util/src/hl.rs b/src/hyperlight_component_util/src/hl.rs index 5a8e984f3..d58eb7aaf 100644 --- a/src/hyperlight_component_util/src/hl.rs +++ b/src/hyperlight_component_util/src/hl.rs @@ -18,8 +18,8 @@ use itertools::Itertools; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; -use crate::emit::{State, kebab_to_cons, kebab_to_var}; -use crate::etypes::{self, Defined, Handleable, TypeBound, Tyvar, Value}; +use crate::emit::{ResolvedBoundVar, State, kebab_to_cons, kebab_to_var}; +use crate::etypes::{self, Defined, Handleable, Tyvar, Value}; use crate::rtypes; /// Construct a string that can be used "on the wire" to identify a @@ -151,25 +151,16 @@ pub fn emit_hl_unmarshal_toplevel_value( } } -/// Find the resource index that the given type variable refers to. -/// -/// Precondition: this type variable does refer to a resource type -fn resolve_tyvar_to_resource(s: &mut State, v: u32) -> u32 { - match s.bound_vars[v as usize].bound { - TypeBound::SubResource => v, - TypeBound::Eq(Defined::Handleable(Handleable::Var(Tyvar::Bound(vv)))) => { - resolve_tyvar_to_resource(s, v + vv + 1) - } - _ => panic!("impossible: resource var is not resource"), - } -} /// Find the resource index that the given Handleable refers to. /// /// Precondition: this type variable does refer to a resource type pub fn resolve_handleable_to_resource(s: &mut State, ht: &Handleable) -> u32 { match ht { Handleable::Var(Tyvar::Bound(vi)) => { - resolve_tyvar_to_resource(s, s.var_offset as u32 + *vi) + let ResolvedBoundVar::Resource { rtidx } = s.resolve_bound_var(*vi) else { + panic!("impossible: resource var is not resource"); + }; + rtidx } _ => panic!("impossible handleable in type"), } @@ -223,6 +214,20 @@ pub fn emit_hl_unmarshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStr (#retid, cursor) } } + Value::FixList(vt, _) => { + let inid = format_ident!("{}_elem", id); + let vtun = emit_hl_unmarshal_value(s, inid.clone(), vt); + quote! { + let mut cursor = 0; + let arr = ::core::array::from_fn(|_i| { + let #inid = &#id[cursor..]; + let (x, b) = { #vtun }; + cursor += b; + x + }); + (arr, cursor) + } + } Value::Record(_) => panic!("record not at top level of valtype"), Value::Tuple(vts) => { let inid = format_ident!("{}_elem", id); @@ -324,9 +329,29 @@ pub fn emit_hl_unmarshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStr log::debug!("resolved ht to r (2) {:?} {:?}", ht, vi); if s.is_guest { let rid = format_ident!("HostResource{}", vi); - quote! { - let i = u32::from_ne_bytes(#id[0..4].try_into().unwrap()); - (::wasmtime::component::Resource::<#rid>::new_borrow(i), 4) + if s.is_wasmtime_guest { + quote! { + let i = u32::from_ne_bytes(#id[0..4].try_into().unwrap()); + (::wasmtime::component::Resource::<#rid>::new_borrow(i), 4) + } + } else { + // TODO: When we add the Drop impl (#810), we need + // to make sure it does not get called here + // + // If we tried to actually return a reference + // here, rustc would get mad about the temporary + // constructed here not living long enough, so + // instead we return the temporary and construct + // the reference elsewhere. It might be a bit more + // principled to have a separate + // HostResourceXXBorrow struct that implements + // AsRef or something in the + // future... + quote! { + let i = u32::from_ne_bytes(#id[0..4].try_into().unwrap()); + + (#rid { rep: i }, 4) + } } } else { let rid = format_ident!("resource{}", vi); @@ -344,7 +369,11 @@ pub fn emit_hl_unmarshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStr let Some(Tyvar::Bound(n)) = tv else { panic!("impossible tyvar") }; - let (n, Some(Defined::Value(vt))) = s.resolve_tv(*n) else { + let ResolvedBoundVar::Definite { + final_bound_var: n, + ty: Defined::Value(vt), + } = s.resolve_bound_var(*n) + else { panic!("unresolvable tyvar (2)"); }; let vt = vt.clone(); @@ -515,6 +544,18 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea #retid } } + Value::FixList(vt, _size) => { + let retid = format_ident!("{}_fixlist", id); + let inid = format_ident!("{}_elem", id); + let vtun = emit_hl_marshal_value(s, inid.clone(), vt); + quote! { + let mut #retid = alloc::vec::Vec::new(); + for #inid in #id { + #retid.extend({ #vtun }) + } + #retid + } + } Value::Record(_) => panic!("record not at top level of valtype"), Value::Tuple(vts) => { let retid = format_ident!("{}_tuple", id); @@ -618,7 +659,9 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea let rid = format_ident!("resource{}", vi); quote! { let i = rts.#rid.len(); - rts.#rid.push_back(::hyperlight_common::resource::ResourceEntry::lend(#id)); + let (lrg, re) = ::hyperlight_common::resource::ResourceEntry::lend(#id); + to_cleanup.push(Box::new(lrg)); + rts.#rid.push_back(re); alloc::vec::Vec::from(u32::to_ne_bytes(i as u32)) } } @@ -627,7 +670,11 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea let Some(Tyvar::Bound(n)) = tv else { panic!("impossible tyvar") }; - let (n, Some(Defined::Value(vt))) = s.resolve_tv(*n) else { + let ResolvedBoundVar::Definite { + final_bound_var: n, + ty: Defined::Value(vt), + } = s.resolve_bound_var(*n) + else { panic!("unresolvable tyvar (2)"); }; let vt = vt.clone(); @@ -642,7 +689,20 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea /// [`crate::rtypes`] module) of the given value type. pub fn emit_hl_unmarshal_param(s: &mut State, id: Ident, pt: &Value) -> TokenStream { let toks = emit_hl_unmarshal_value(s, id, pt); - quote! { { #toks }.0 } + // Slight hack to avoid rust complaints about deserialised + // resource borrow lifetimes. + fn is_borrow(vt: &Value) -> bool { + match vt { + Value::Borrow(_) => true, + Value::Var(_, vt) => is_borrow(vt), + _ => false, + } + } + if s.is_guest && !s.is_wasmtime_guest && is_borrow(pt) { + quote! { &({ #toks }.0) } + } else { + quote! { { #toks }.0 } + } } /// Emit code to unmarshal the result of a function with result type @@ -653,14 +713,13 @@ pub fn emit_hl_unmarshal_param(s: &mut State, id: Ident, pt: &Value) -> TokenStr /// /// Precondition: the result type must only be a named result if there /// are no names in it (i.e. a unit type) -pub fn emit_hl_unmarshal_result(s: &mut State, id: Ident, rt: &etypes::Result) -> TokenStream { +pub fn emit_hl_unmarshal_result(s: &mut State, id: Ident, rt: &etypes::Result<'_>) -> TokenStream { match rt { - etypes::Result::Named(rs) if rs.is_empty() => quote! { () }, - etypes::Result::Unnamed(vt) => { + Some(vt) => { let toks = emit_hl_unmarshal_value(s, id, vt); quote! { { #toks }.0 } } - _ => panic!("named results not supported"), + None => quote! { () }, } } @@ -680,11 +739,10 @@ pub fn emit_hl_marshal_param(s: &mut State, id: Ident, pt: &Value) -> TokenStrea /// are no names in it (a unit type) pub fn emit_hl_marshal_result(s: &mut State, id: Ident, rt: &etypes::Result) -> TokenStream { match rt { - etypes::Result::Named(rs) if rs.is_empty() => quote! { ::alloc::vec::Vec::new() }, - etypes::Result::Unnamed(vt) => { + None => quote! { ::alloc::vec::Vec::new() }, + Some(vt) => { let toks = emit_hl_marshal_value(s, id, vt); quote! { { #toks } } } - _ => panic!("named results not supported"), } } diff --git a/src/hyperlight_component_util/src/host.rs b/src/hyperlight_component_util/src/host.rs index 4b822b10c..0f6bca65b 100644 --- a/src/hyperlight_component_util/src/host.rs +++ b/src/hyperlight_component_util/src/host.rs @@ -56,12 +56,20 @@ fn emit_export_extern_decl<'a, 'b, 'c>( let unmarshal = emit_hl_unmarshal_result(s, ret.clone(), &ft.result); quote! { fn #n(&mut self, #(#param_decls),*) -> #result_decl { + let mut to_cleanup = Vec::>::new(); + let marshalled = { + let mut rts = self.rt.lock().unwrap(); + #[allow(clippy::unused_unit)] + (#(#marshal,)*) + }; let #ret = ::hyperlight_host::sandbox::Callable::call::<::std::vec::Vec::>(&mut self.sb, #hln, - (#(#marshal,)*) + marshalled, ); let ::std::result::Result::Ok(#ret) = #ret else { panic!("bad return from guest {:?}", #ret) }; #[allow(clippy::unused_unit)] + let mut rts = self.rt.lock().unwrap(); + #[allow(clippy::unused_unit)] #unmarshal } } @@ -333,6 +341,8 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com s.root_component_name = Some((ns.clone(), wn.name)); s.cur_trait = Some(export_trait.clone()); s.import_param_var = Some(format_ident!("I")); + s.is_export = true; + let exports = ct .instance .unqualified @@ -347,7 +357,6 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com pub(crate) rt: ::std::sync::Arc<::std::sync::Mutex<#rtsid>>, } pub(crate) fn register_host_functions(sb: &mut S, i: I) -> ::std::sync::Arc<::std::sync::Mutex<#rtsid>> { - use ::hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; let rts = ::std::sync::Arc::new(::std::sync::Mutex::new(#rtsid::new())); let #import_id = ::std::sync::Arc::new(::std::sync::Mutex::new(i)); #(#imports)* @@ -357,14 +366,12 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com #(#exports)* } impl #ns::#r#trait for ::hyperlight_host::sandbox::UninitializedSandbox { - type Exports = #wrapper_name; + type Exports = #wrapper_name; fn instantiate(mut self, i: I) -> Self::Exports { let rts = register_host_functions(&mut self, i); - let noop = ::core::default::Default::default(); - let sb = ::hyperlight_host::sandbox_state::sandbox::EvolvableSandbox::evolve(self, noop).unwrap(); - let cc = ::hyperlight_host::func::call_ctx::MultiUseGuestCallContext::start(sb); + let sb = self.evolve().unwrap(); #wrapper_name { - sb: cc, + sb, rt: rts, } } diff --git a/src/hyperlight_component_util/src/rtypes.rs b/src/hyperlight_component_util/src/rtypes.rs index a6b2266a3..18665db31 100644 --- a/src/hyperlight_component_util/src/rtypes.rs +++ b/src/hyperlight_component_util/src/rtypes.rs @@ -29,8 +29,8 @@ use crate::emit::{ split_wit_name, }; use crate::etypes::{ - Component, Defined, ExternDecl, ExternDesc, Func, Handleable, ImportExport, Instance, Param, - Result, TypeBound, Tyvar, Value, + self, Component, Defined, ExternDecl, ExternDesc, Func, Handleable, ImportExport, Instance, + Param, TypeBound, Tyvar, Value, }; /// When referring to an instance or resource trait, emit a token @@ -281,6 +281,11 @@ pub fn emit_value(s: &mut State, vt: &Value) -> TokenStream { let vt = emit_value(s, vt); quote! { alloc::vec::Vec<#vt> } } + Value::FixList(vt, size) => { + let vt = emit_value(s, vt); + let size = *size as usize; + quote! { [#vt; #size] } + } Value::Record(_) => panic!("record not at top level of valtype"), Value::Tuple(vts) => { let vts = vts.iter().map(|vt| emit_value(s, vt)).collect::>(); @@ -340,7 +345,11 @@ pub fn emit_value(s: &mut State, vt: &Value) -> TokenStream { } } else { let vr = emit_var_ref(s, tv); - quote! { ::hyperlight_common::resource::BorrowedResourceGuard<#vr> } + if s.is_export { + quote! { &#vr } + } else { + quote! { ::hyperlight_common::resource::BorrowedResourceGuard<#vr> } + } } } }, @@ -521,11 +530,10 @@ pub fn emit_func_param(s: &mut State, p: &Param) -> TokenStream { /// /// Precondition: the result type must only be a named result if there /// are no names in it (i.e. a unit type) -pub fn emit_func_result(s: &mut State, r: &Result) -> TokenStream { +pub fn emit_func_result(s: &mut State, r: &etypes::Result<'_>) -> TokenStream { match r { - Result::Unnamed(vt) => emit_value(s, vt), - Result::Named(rs) if rs.is_empty() => quote! { () }, - _ => panic!("multiple named function results are not currently supported"), + Some(vt) => emit_value(s, vt), + None => quote! { () }, } } @@ -603,8 +611,11 @@ fn emit_type_alias TokenStream>( /// Emit (via returning) a Rust trait item corresponding to this /// extern decl +/// +/// See note on emit.rs push_origin for the difference between +/// origin_was_export and s.is_export. fn emit_extern_decl<'a, 'b, 'c>( - is_export: bool, + origin_was_export: bool, s: &'c mut State<'a, 'b>, ed: &'c ExternDecl<'b>, ) -> TokenStream { @@ -612,7 +623,7 @@ fn emit_extern_decl<'a, 'b, 'c>( match &ed.desc { ExternDesc::CoreModule(_) => panic!("core module (im/ex)ports are not supported"), ExternDesc::Func(ft) => { - let mut s = s.push_origin(is_export, ed.kebab_name); + let mut s = s.push_origin(origin_was_export, ed.kebab_name); match kebab_to_fn(ed.kebab_name) { FnName::Plain(n) => { let params = ft @@ -677,7 +688,7 @@ fn emit_extern_decl<'a, 'b, 'c>( TokenStream::new() } let edn: &'b str = ed.kebab_name; - let mut s: State<'_, 'b> = s.push_origin(is_export, edn); + let mut s: State<'_, 'b> = s.push_origin(origin_was_export, edn); if let Some((n, bound)) = s.is_var_defn(t) { match bound { TypeBound::Eq(t) => { @@ -704,7 +715,7 @@ fn emit_extern_decl<'a, 'b, 'c>( } } ExternDesc::Instance(it) => { - let mut s = s.push_origin(is_export, ed.kebab_name); + let mut s = s.push_origin(origin_was_export, ed.kebab_name); let wn = split_wit_name(ed.kebab_name); emit_instance(&mut s, wn.clone(), it); @@ -827,8 +838,8 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com s.cur_trait().items.extend(quote! { #(#imports)* }); s.adjust_vars(ct.instance.evars.len() as u32); - s.import_param_var = Some(format_ident!("I")); + s.is_export = true; let export_name = kebab_to_exports_name(wn.name); *s.bound_vars = ct diff --git a/src/hyperlight_component_util/src/substitute.rs b/src/hyperlight_component_util/src/substitute.rs index 94f95cf7b..5ea07d609 100644 --- a/src/hyperlight_component_util/src/substitute.rs +++ b/src/hyperlight_component_util/src/substitute.rs @@ -96,6 +96,7 @@ where Value::Char => Value::Char, Value::String => Value::String, Value::List(vt) => Value::List(Box::new(self.value(vt)?)), + Value::FixList(vt, size) => Value::FixList(Box::new(self.value(vt)?), *size), Value::Record(rfs) => Value::Record(self.record_fields(rfs)?), Value::Variant(vcs) => Value::Variant(self.variant_cases(vcs)?), Value::Flags(ns) => Value::Flags(ns.clone()), @@ -139,8 +140,8 @@ where rt: &crate::etypes::Result<'a>, ) -> Result, Self::Error> { Ok(match rt { - crate::etypes::Result::Unnamed(vt) => crate::etypes::Result::Unnamed(self.value(vt)?), - crate::etypes::Result::Named(pts) => crate::etypes::Result::Named(self.params(pts)?), + Some(vt) => Some(self.value(vt)?), + None => None, }) } diff --git a/src/hyperlight_component_util/src/subtype.rs b/src/hyperlight_component_util/src/subtype.rs index cb39eea4d..f5ccaf7a5 100644 --- a/src/hyperlight_component_util/src/subtype.rs +++ b/src/hyperlight_component_util/src/subtype.rs @@ -36,7 +36,7 @@ pub enum Error<'r> { /// A value type was present, but incompatible with its expected type MismatchedValue(Value<'r>, Value<'r>), /// A defined type was present, but incompatible with its expected type - MismatchedDefined(Defined<'r>, Defined<'r>), + MismatchedDefined(Box>, Box>), /// A resource was present, but was not the same resource as was expected MismatchedResources(ResourceId, ResourceId), /// A type variable could not be resolved to be the same as the @@ -239,7 +239,10 @@ impl<'p, 'a> Ctx<'p, 'a> { self.subtype_qualified_instance(it1, it2) } (Defined::Component(ct1), Defined::Component(ct2)) => self.subtype_component(ct1, ct2), - _ => Err(Error::MismatchedDefined(dt1.clone(), dt2.clone())), + _ => Err(Error::MismatchedDefined( + Box::new(dt1.clone()), + Box::new(dt2.clone()), + )), } } pub fn subtype_handleable_is_resource<'r>(&self, ht: &'r Handleable) -> Result<(), Error<'a>> { diff --git a/src/hyperlight_component_util/src/wf.rs b/src/hyperlight_component_util/src/wf.rs index db15a7613..20321d6d7 100644 --- a/src/hyperlight_component_util/src/wf.rs +++ b/src/hyperlight_component_util/src/wf.rs @@ -213,6 +213,7 @@ impl<'p, 'a> Ctx<'p, 'a> { Value::Char => Ok(()), Value::String => Ok(()), Value::List(vt) => self.wf_value(p_, vt), + Value::FixList(vt, _) => self.wf_value(p_, vt), Value::Record(rfs) => anon_err.and(self.wf_record_fields(p_, rfs)), Value::Variant(vcs) => anon_err.and(self.wf_variant_cases(p_, vcs)), Value::Flags(ns) => anon_err.and(error_if_duplicates_by( @@ -268,10 +269,8 @@ impl<'p, 'a> Ctx<'p, 'a> { .iter() .try_for_each(|fp: &'r Param<'a>| self.wf_value(param_pos, &fp.ty))?; match &ft.result { - crate::etypes::Result::Unnamed(vt) => self.wf_value(result_pos, vt), - crate::etypes::Result::Named(ps) => ps - .iter() - .try_for_each(|fp: &'r Param<'a>| self.wf_value(result_pos, &fp.ty)), + Some(vt) => self.wf_value(result_pos, vt), + None => Ok(()), } } fn wf_type_bound<'r>( diff --git a/src/hyperlight_guest/Cargo.toml b/src/hyperlight_guest/Cargo.toml index 8fce35293..f031e2d6c 100644 --- a/src/hyperlight_guest/Cargo.toml +++ b/src/hyperlight_guest/Cargo.toml @@ -12,6 +12,13 @@ Provides only the essential building blocks for interacting with the host enviro """ [dependencies] -anyhow = { version = "1.0.98", default-features = false } +anyhow = { version = "1.0.100", default-features = false } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } hyperlight-common = { workspace = true } +hyperlight-guest-tracing = { workspace = true, default-features = false } +flatbuffers = { version= "25.9.23", default-features = false } +tracing = { version = "0.1.41", default-features = false, features = ["attributes"] } + +[features] +default = [] +trace_guest = ["hyperlight-guest-tracing/trace"] diff --git a/src/hyperlight_guest/src/exit.rs b/src/hyperlight_guest/src/exit.rs index 82d8a9545..cc91aad9e 100644 --- a/src/hyperlight_guest/src/exit.rs +++ b/src/hyperlight_guest/src/exit.rs @@ -18,11 +18,40 @@ use core::arch::asm; use core::ffi::{CStr, c_char}; use hyperlight_common::outb::OutBAction; +use tracing::instrument; /// Halt the execution of the guest and returns control to the host. #[inline(never)] +#[instrument(skip_all, level = "Trace")] pub fn halt() { - unsafe { asm!("hlt", options(nostack)) } + #[cfg(feature = "trace_guest")] + { + // End any ongoing trace before halting + hyperlight_guest_tracing::end_trace(); + // If tracing is enabled, we need to pass the trace batch info + // along with the halt instruction so the host can retrieve it + if let Some(tbi) = hyperlight_guest_tracing::guest_trace_info() { + unsafe { + asm!("hlt", + in("r8") OutBAction::TraceBatch as u64, + in("r9") tbi.guest_start_tsc, + in("r10") tbi.spans_ptr, + in("r11") tbi.events_ptr, + options(nostack) + ) + }; + hyperlight_guest_tracing::clean_trace_state(); + } else { + // If tracing is not enabled, we can directly halt + unsafe { asm!("hlt", options(nostack)) }; + } + } + + #[cfg(not(feature = "trace_guest"))] + { + // If tracing is not enabled, we can directly halt + unsafe { asm!("hlt", options(nostack)) }; + } } /// Exits the VM with an Abort OUT action and code 0. @@ -33,6 +62,9 @@ pub extern "C" fn abort() -> ! { /// Exits the VM with an Abort OUT action and a specific code. pub fn abort_with_code(code: &[u8]) -> ! { + // End any ongoing trace before aborting + #[cfg(feature = "trace_guest")] + hyperlight_guest_tracing::end_trace(); outb(OutBAction::Abort as u16, code); outb(OutBAction::Abort as u16, &[0xFF]); // send abort terminator (if not included in code) unreachable!() @@ -43,6 +75,9 @@ pub fn abort_with_code(code: &[u8]) -> ! { /// # Safety /// This function is unsafe because it dereferences a raw pointer. pub unsafe fn abort_with_code_and_message(code: &[u8], message_ptr: *const c_char) -> ! { + // End any ongoing trace before aborting + #[cfg(feature = "trace_guest")] + hyperlight_guest_tracing::end_trace(); unsafe { // Step 1: Send abort code (typically 1 byte, but `code` allows flexibility) outb(OutBAction::Abort as u16, code); @@ -61,8 +96,17 @@ pub unsafe fn abort_with_code_and_message(code: &[u8], message_ptr: *const c_cha } } +/// This function exists to give the guest more manual control +/// over the abort sequence. For example, in `hyperlight_guest_bin`'s panic handler, +/// we have a message of unknown length that we want to stream +/// to the host, which requires sending the message in chunks +pub fn write_abort(code: &[u8]) { + outb(OutBAction::Abort as u16, code); +} + /// OUT bytes to the host through multiple exits. pub(crate) fn outb(port: u16, data: &[u8]) { + // Ensure all tracing data is flushed before sending OUT bytes unsafe { let mut i = 0; while i < data.len() { @@ -79,7 +123,33 @@ pub(crate) fn outb(port: u16, data: &[u8]) { } /// OUT function for sending a 32-bit value to the host. +#[instrument(skip_all, level = "Trace")] pub(crate) unsafe fn out32(port: u16, val: u32) { + #[cfg(feature = "trace_guest")] + { + if let Some(tbi) = hyperlight_guest_tracing::guest_trace_info() { + // If tracing is enabled, send the trace batch info along with the OUT action + unsafe { + asm!("out dx, eax", + in("dx") port, + in("eax") val, + in("r8") OutBAction::TraceBatch as u64, + in("r9") tbi.guest_start_tsc, + in("r10") tbi.spans_ptr, + in("r11") tbi.events_ptr, + options(preserves_flags, nomem, nostack) + ) + }; + + hyperlight_guest_tracing::clean_trace_state(); + } else { + // If tracing is not enabled, just send the value + unsafe { + asm!("out dx, eax", in("dx") port, in("eax") val, options(preserves_flags, nomem, nostack)) + }; + } + } + #[cfg(not(feature = "trace_guest"))] unsafe { asm!("out dx, eax", in("dx") port, in("eax") val, options(preserves_flags, nomem, nostack)); } diff --git a/src/hyperlight_guest/src/guest_handle/handle.rs b/src/hyperlight_guest/src/guest_handle/handle.rs index c18ba3540..3a22ee26c 100644 --- a/src/hyperlight_guest/src/guest_handle/handle.rs +++ b/src/hyperlight_guest/src/guest_handle/handle.rs @@ -25,7 +25,7 @@ use hyperlight_common::mem::HyperlightPEB; /// /// Guests are expected to initialize this and store it. For example, you /// could store it in a global variable. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct GuestHandle { peb: Option<*mut HyperlightPEB>, } diff --git a/src/hyperlight_guest/src/guest_handle/host_comm.rs b/src/hyperlight_guest/src/guest_handle/host_comm.rs index 97e90f3a6..8ae22fb49 100644 --- a/src/hyperlight_guest/src/guest_handle/host_comm.rs +++ b/src/hyperlight_guest/src/guest_handle/host_comm.rs @@ -19,15 +19,18 @@ use alloc::string::ToString; use alloc::vec::Vec; use core::slice::from_raw_parts; +use flatbuffers::FlatBufferBuilder; use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType}; use hyperlight_common::flatbuffer_wrappers::function_types::{ - ParameterValue, ReturnType, ReturnValue, + FunctionCallResult, ParameterValue, ReturnType, ReturnValue, }; -use hyperlight_common::flatbuffer_wrappers::guest_error::{ErrorCode, GuestError}; +use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData; use hyperlight_common::flatbuffer_wrappers::guest_log_level::LogLevel; use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails; +use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity; use hyperlight_common::outb::OutBAction; +use tracing::instrument; use super::handle::GuestHandle; use crate::error::{HyperlightGuestError, Result}; @@ -35,19 +38,20 @@ use crate::exit::out32; impl GuestHandle { /// Get user memory region as bytes. + #[instrument(skip_all, level = "Trace")] pub fn read_n_bytes_from_user_memory(&self, num: u64) -> Result> { let peb_ptr = self.peb().unwrap(); let user_memory_region_ptr = unsafe { (*peb_ptr).init_data.ptr as *mut u8 }; let user_memory_region_size = unsafe { (*peb_ptr).init_data.size }; if num > user_memory_region_size { - return Err(HyperlightGuestError::new( + Err(HyperlightGuestError::new( ErrorCode::GuestError, format!( "Requested {} bytes from user memory, but only {} bytes are available", num, user_memory_region_size ), - )); + )) } else { let user_memory_region_slice = unsafe { core::slice::from_raw_parts(user_memory_region_ptr, num as usize) }; @@ -64,18 +68,24 @@ impl GuestHandle { /// When calling `call_host_function`, this function is called /// internally to get the return value. pub fn get_host_return_value>(&self) -> Result { - let return_value = self - .try_pop_shared_input_data_into::() - .expect("Unable to deserialize a return value from host"); - T::try_from(return_value).map_err(|_| { - HyperlightGuestError::new( - ErrorCode::GuestError, - format!( - "Host return value was not a {} as expected", - core::any::type_name::() - ), - ) - }) + let inner = self + .try_pop_shared_input_data_into::() + .expect("Unable to deserialize a return value from host") + .into_inner(); + + match inner { + Ok(ret) => T::try_from(ret).map_err(|_| { + let expected = core::any::type_name::(); + HyperlightGuestError::new( + ErrorCode::UnsupportedParameterType, + format!("Host return value could not be converted to expected {expected}",), + ) + }), + Err(e) => Err(HyperlightGuestError { + kind: e.code, + message: e.message, + }), + } } /// Call a host function without reading its return value from shared mem. @@ -89,6 +99,9 @@ impl GuestHandle { parameters: Option>, return_type: ReturnType, ) -> Result<()> { + let estimated_capacity = + estimate_flatbuffer_capacity(function_name, parameters.as_deref().unwrap_or(&[])); + let host_function_call = FunctionCall::new( function_name.to_string(), parameters, @@ -96,10 +109,9 @@ impl GuestHandle { return_type, ); - let host_function_call_buffer: Vec = host_function_call - .try_into() - .expect("Unable to serialize host function call"); + let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity); + let host_function_call_buffer = host_function_call.encode(&mut builder); self.push_shared_output_data(host_function_call_buffer)?; unsafe { @@ -114,6 +126,7 @@ impl GuestHandle { /// sends it to the host, and then retrieves the return value. /// /// The return value is deserialized into the specified type `T`. + #[instrument(skip_all, level = "Trace")] pub fn call_host_function>( &self, function_name: &str, @@ -124,6 +137,7 @@ impl GuestHandle { self.get_host_return_value::() } + #[instrument(skip_all, level = "Trace")] pub fn get_host_function_details(&self) -> HostFunctionDetails { let peb_ptr = self.peb().unwrap(); let host_function_details_buffer = @@ -139,21 +153,6 @@ impl GuestHandle { .expect("Failed to convert buffer to HostFunctionDetails") } - /// Write an error to the shared output data buffer. - pub fn write_error(&self, error_code: ErrorCode, message: Option<&str>) { - let guest_error: GuestError = GuestError::new( - error_code.clone(), - message.map_or("".to_string(), |m| m.to_string()), - ); - let guest_error_buffer: Vec = (&guest_error) - .try_into() - .expect("Invalid guest_error_buffer, could not be converted to a Vec"); - - if let Err(e) = self.push_shared_output_data(guest_error_buffer) { - panic!("Unable to push guest error to shared output data: {:#?}", e); - } - } - /// Log a message with the specified log level, source, caller, source file, and line number. pub fn log_message( &self, @@ -164,24 +163,46 @@ impl GuestHandle { source_file: &str, line: u32, ) { - let guest_log_data = GuestLogData::new( - message.to_string(), - source.to_string(), - log_level, - caller.to_string(), - source_file.to_string(), - line, - ); - - let bytes: Vec = guest_log_data - .try_into() - .expect("Failed to convert GuestLogData to bytes"); - - self.push_shared_output_data(bytes) - .expect("Unable to push log data to shared output data"); - - unsafe { - out32(OutBAction::Log as u16, 0); + // Closure to send log message to host + let send_to_host = || { + let guest_log_data = GuestLogData::new( + message.to_string(), + source.to_string(), + log_level, + caller.to_string(), + source_file.to_string(), + line, + ); + + let bytes: Vec = guest_log_data + .try_into() + .expect("Failed to convert GuestLogData to bytes"); + + self.push_shared_output_data(&bytes) + .expect("Unable to push log data to shared output data"); + + unsafe { + out32(OutBAction::Log as u16, 0); + } + }; + + #[cfg(feature = "trace_guest")] + if hyperlight_guest_tracing::is_trace_enabled() { + // If the "trace_guest" feature is enabled and tracing is initialized, log using tracing + tracing::trace!( + event = message, + level = ?log_level, + code.filepath = source, + caller = caller, + source_file = source_file, + code.lineno = line, + ); + } else { + send_to_host(); + } + #[cfg(not(feature = "trace_guest"))] + { + send_to_host(); } } } diff --git a/src/hyperlight_guest/src/guest_handle/io.rs b/src/hyperlight_guest/src/guest_handle/io.rs index ce8c82b0c..a494033dd 100644 --- a/src/hyperlight_guest/src/guest_handle/io.rs +++ b/src/hyperlight_guest/src/guest_handle/io.rs @@ -16,17 +16,18 @@ limitations under the License. use alloc::format; use alloc::string::ToString; -use alloc::vec::Vec; use core::any::type_name; use core::slice::from_raw_parts_mut; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; +use tracing::{Span, instrument}; use super::handle::GuestHandle; use crate::error::{HyperlightGuestError, Result}; impl GuestHandle { /// Pops the top element from the shared input data buffer and returns it as a T + #[instrument(skip_all, parent = Span::current(), level= "Trace")] pub fn try_pop_shared_input_data_into(&self) -> Result where T: for<'a> TryFrom<&'a [u8]>, @@ -88,7 +89,8 @@ impl GuestHandle { } /// Pushes the given data onto the shared output data buffer. - pub fn push_shared_output_data(&self, data: Vec) -> Result<()> { + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub fn push_shared_output_data(&self, data: &[u8]) -> Result<()> { let peb_ptr = self.peb().unwrap(); let output_stack_size = unsafe { (*peb_ptr).output_stack.size as usize }; let output_stack_ptr = unsafe { (*peb_ptr).output_stack.ptr as *mut u8 }; @@ -133,7 +135,7 @@ impl GuestHandle { } // write the actual data - odb[stack_ptr_rel as usize..stack_ptr_rel as usize + data.len()].copy_from_slice(&data); + odb[stack_ptr_rel as usize..stack_ptr_rel as usize + data.len()].copy_from_slice(data); // write the offset to the newly written data, to the top of the stack let bytes: [u8; 8] = stack_ptr_rel.to_le_bytes(); diff --git a/src/hyperlight_guest_bin/Cargo.toml b/src/hyperlight_guest_bin/Cargo.toml index 1b8202f51..24f4c1d1b 100644 --- a/src/hyperlight_guest_bin/Cargo.toml +++ b/src/hyperlight_guest_bin/Cargo.toml @@ -16,14 +16,19 @@ and third-party code used by our C-API needed to build a native hyperlight-guest [features] default = ["libc", "printf"] libc = [] # compile musl libc -printf = [] # compile printf +printf = [ "libc" ] # compile printf +trace_guest = ["hyperlight-common/trace_guest", "hyperlight-guest/trace_guest", "hyperlight-guest-tracing/trace"] +mem_profile = ["hyperlight-common/mem_profile"] [dependencies] hyperlight-guest = { workspace = true, default-features = false } hyperlight-common = { workspace = true, default-features = false } +hyperlight-guest-tracing = { workspace = true, default-features = false } buddy_system_allocator = "0.11.0" log = { version = "0.4", default-features = false } spin = "0.10.0" +flatbuffers = { version = "25.2.10", default-features = false } +tracing = { version = "0.1.41", default-features = false, features = ["attributes"] } [lints] workspace = true @@ -31,4 +36,4 @@ workspace = true [build-dependencies] cc = "1.2" cfg-if = "1.0" -glob = "0.3.2" +glob = "0.3.3" diff --git a/src/hyperlight_guest_bin/build.rs b/src/hyperlight_guest_bin/build.rs index 35a39b469..a7bce651b 100644 --- a/src/hyperlight_guest_bin/build.rs +++ b/src/hyperlight_guest_bin/build.rs @@ -88,12 +88,20 @@ fn cargo_main() { // targets will eventually show up. cfg.flag("--target=x86_64-unknown-linux-none"); + // We don't use a different stack for all interrupts, so there + // can be no red zone + cfg.flag("-mno-red-zone"); + // We don't support stack protectors at the moment, but Arch Linux clang // auto-enables them for -linux platforms, so explicitly disable them. cfg.flag("-fno-stack-protector"); cfg.flag("-fstack-clash-protection"); cfg.flag("-mstack-probe-size=4096"); - cfg.compiler("clang"); + cfg.compiler( + env::var("HYPERLIGHT_GUEST_clang") + .as_deref() + .unwrap_or("clang"), + ); if cfg!(windows) { unsafe { env::set_var("AR_x86_64_unknown_none", "llvm-ar") }; @@ -193,6 +201,9 @@ impl From<&std::ffi::OsStr> for Tool { } fn find_next(root_dir: &Path, tool_name: &str) -> PathBuf { + if let Some(path) = env::var_os(format!("HYPERLIGHT_GUEST_{tool_name}")) { + return path.into(); + } let path = env::var_os("PATH").expect("$PATH should exist"); let paths: Vec<_> = env::split_paths(&path).collect(); for path in &paths { @@ -245,6 +256,7 @@ fn main() -> std::process::ExitCode { "-fno-stack-protector", "-fstack-clash-protection", "-mstack-probe-size=4096", + "-mno-red-zone", ]) .arg("-nostdinc") .arg("-isystem") diff --git a/src/hyperlight_guest_bin/src/exceptions/handler.rs b/src/hyperlight_guest_bin/src/exceptions/handler.rs index 5bc1a7e09..ab0da4cfe 100644 --- a/src/hyperlight_guest_bin/src/exceptions/handler.rs +++ b/src/hyperlight_guest_bin/src/exceptions/handler.rs @@ -21,6 +21,43 @@ use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::outb::Exception; use hyperlight_guest::exit::abort_with_code_and_message; +/// See AMD64 Architecture Programmer's Manual, Volume 2 +/// §8.9.3 Interrupt Stack Frame, pp. 283--284 +/// Figure 8-14: Long-Mode Stack After Interrupt---Same Privilege, +/// Figure 8-15: Long-Mode Stack After Interrupt---Higher Privilege +/// Subject to the proviso that we push a dummy error code of 0 for exceptions +/// for which the processor does not provide one +#[repr(C)] +pub struct ExceptionInfo { + pub error_code: u64, + pub rip: u64, + pub cs: u64, + pub rflags: u64, + pub rsp: u64, + pub ss: u64, +} +const _: () = assert!(core::mem::offset_of!(ExceptionInfo, rip) == 8); +const _: () = assert!(core::mem::offset_of!(ExceptionInfo, rsp) == 32); + +#[repr(C)] +/// Saved context, pushed onto the stack by exception entry code +pub struct Context { + /// in order: gs, fs, es + pub segments: [u64; 3], + pub fxsave: [u8; 512], + pub ds: u64, + /// no `rsp`, since the processor saved it + /// `rax` is at the top, `r15` the bottom + pub gprs: [u64; 15], +} +const _: () = assert!(size_of::() == 152 + 512); + +// TODO: This will eventually need to end up in a per-thread context, +// when there are threads. +pub static HANDLERS: [core::sync::atomic::AtomicU64; 31] = + [const { core::sync::atomic::AtomicU64::new(0) }; 31]; +pub type HandlerT = fn(n: u64, info: *mut ExceptionInfo, ctx: *mut Context, pf_addr: u64) -> bool; + /// Exception handler #[unsafe(no_mangle)] pub extern "C" fn hl_exception_handler( @@ -28,13 +65,40 @@ pub extern "C" fn hl_exception_handler( exception_number: u64, page_fault_address: u64, ) { + let ctx = stack_pointer as *mut Context; + let exn_info = (stack_pointer + size_of::() as u64) as *mut ExceptionInfo; + let exception = Exception::try_from(exception_number as u8).expect("Invalid exception number"); + + let saved_rip = unsafe { (&raw const (*exn_info).rip).read_volatile() }; + let error_code = unsafe { (&raw const (*exn_info).error_code).read_volatile() }; + let msg = format!( - "Page Fault Address: {:#x}\n\ - Stack Pointer: {:#x}", - page_fault_address, stack_pointer + "Exception vector: {:#}\n\ + Faulting Instruction: {:#x}\n\ + Page Fault Address: {:#x}\n\ + Error code: {:#x}\n\ + Stack Pointer: {:#x}", + exception_number, saved_rip, page_fault_address, error_code, stack_pointer ); + // We don't presently have any need for user-defined interrupts, + // so we only support handlers for the architecture-defined + // vectors (0-31) + if exception_number < 31 { + let handler = + HANDLERS[exception_number as usize].load(core::sync::atomic::Ordering::Acquire); + if handler != 0 + && unsafe { + core::mem::transmute:: bool>( + handler, + )(exception_number, exn_info, ctx, page_fault_address) + } + { + return; + } + } + unsafe { abort_with_code_and_message( &[ErrorCode::GuestError as u8, exception as u8], diff --git a/src/hyperlight_guest_bin/src/exceptions/idtr.rs b/src/hyperlight_guest_bin/src/exceptions/idtr.rs index dc547a199..d1d54830b 100644 --- a/src/hyperlight_guest_bin/src/exceptions/idtr.rs +++ b/src/hyperlight_guest_bin/src/exceptions/idtr.rs @@ -49,6 +49,7 @@ pub(crate) unsafe fn load_idt() { // Use &raw mut to get a mutable raw pointer, then dereference it // this is to avoid the clippy warning "shared reference to mutable static" + #[allow(clippy::deref_addrof)] let idtr = &mut *(&raw mut IDTR); idtr.init(expected_base, idt_size as u16); idtr.load(); diff --git a/src/hyperlight_guest_bin/src/exceptions/interrupt_entry.rs b/src/hyperlight_guest_bin/src/exceptions/interrupt_entry.rs index 0a0d63775..bbfdd96fa 100644 --- a/src/hyperlight_guest_bin/src/exceptions/interrupt_entry.rs +++ b/src/hyperlight_guest_bin/src/exceptions/interrupt_entry.rs @@ -67,9 +67,19 @@ macro_rules! context_save { " push r13\n", " push r14\n", " push r15\n", - // Save segment registers + // Save one of the segment registers to get 16-byte alignment for + // FXSAVE. TODO: consider packing the segment registers " mov rax, ds\n", " push rax\n", + // Save floating-point/SSE registers + // TODO: Don't do this unconditionally: get the exn + // handlers compiled without sse + // TODO: Check if we ever generate code with ymm/zmm in + // the handlers and save/restore those as well + " sub rsp, 512\n", + " mov rax, rsp\n", + " fxsave [rax]\n", + // Save the rest of the segment registers " mov rax, es\n", " push rax\n", " mov rax, fs\n", @@ -83,13 +93,18 @@ macro_rules! context_save { macro_rules! context_restore { () => { concat!( - // Restore segment registers + // Restore most segment registers " pop rax\n", " mov gs, rax\n", " pop rax\n", " mov fs, rax\n", " pop rax\n", " mov es, rax\n", + // Restore floating-point/SSE registers + " mov rax, rsp\n", + " fxrstor [rax]\n", + " add rsp, 512\n", + // Restore the last segment register " pop rax\n", " mov ds, rax\n", // Restore general-purpose registers @@ -123,7 +138,8 @@ macro_rules! generate_exceptions { " mov rdi, rsp\n", " call {hl_exception_handler}\n", context_restore!(), - " iretq\n", // iretq is used to return from exception in x86_64 + " add rsp, 8\n", // error code + " iretq\n", // iretq is used to return from exception in x86_64 generate_excp!(0, pusherrcode), generate_excp!(1, pusherrcode), generate_excp!(2, pusherrcode), diff --git a/src/hyperlight_guest_bin/src/guest_err.rs b/src/hyperlight_guest_bin/src/guest_err.rs deleted file mode 100644 index 0b384a391..000000000 --- a/src/hyperlight_guest_bin/src/guest_err.rs +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use core::ffi::{CStr, c_char}; - -use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; -use hyperlight_guest::exit::halt; - -use crate::GUEST_HANDLE; - -/// Exposes a C API to allow the guest to set an error -/// -/// # Safety -/// TODO -/// cbindgen:ignore -#[unsafe(no_mangle)] -#[allow(non_camel_case_types)] -pub unsafe extern "C" fn setError(code: u64, message: *const c_char) { - let handle = unsafe { GUEST_HANDLE }; - - let error_code = ErrorCode::from(code); - match message.is_null() { - true => handle.write_error(error_code, None), - false => { - let message = unsafe { CStr::from_ptr(message).to_str().ok() } - .expect("Invalid error message, could not be converted to a string"); - handle.write_error(error_code, Some(message)); - } - } - - halt(); -} diff --git a/src/hyperlight_guest_bin/src/guest_function/call.rs b/src/hyperlight_guest_bin/src/guest_function/call.rs index bdaed4212..b5bba301b 100644 --- a/src/hyperlight_guest_bin/src/guest_function/call.rs +++ b/src/hyperlight_guest_bin/src/guest_function/call.rs @@ -17,16 +17,19 @@ limitations under the License. use alloc::format; use alloc::vec::Vec; +use flatbuffers::FlatBufferBuilder; use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType}; -use hyperlight_common::flatbuffer_wrappers::function_types::ParameterType; -use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; +use hyperlight_common::flatbuffer_wrappers::function_types::{FunctionCallResult, ParameterType}; +use hyperlight_common::flatbuffer_wrappers::guest_error::{ErrorCode, GuestError}; use hyperlight_guest::error::{HyperlightGuestError, Result}; use hyperlight_guest::exit::halt; +use tracing::{Span, instrument}; use crate::{GUEST_HANDLE, REGISTERED_GUEST_FUNCTIONS}; type GuestFunc = fn(&FunctionCall) -> Result>; +#[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result> { // Validate this is a Guest Function Call if function_call.function_call_type() != FunctionCallType::Guest { @@ -42,6 +45,7 @@ pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result // Find the function definition for the function call. // Use &raw const to get an immutable reference to the static HashMap // this is to avoid the clippy warning "shared reference to mutable static" + #[allow(clippy::deref_addrof)] if let Some(registered_function_definition) = unsafe { (*(&raw const REGISTERED_GUEST_FUNCTIONS)).get(&function_call.function_name) } { @@ -77,9 +81,12 @@ pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result // This function is marked as no_mangle/inline to prevent the compiler from inlining it , if its inlined the epilogue will not be called // and we will leak memory as the epilogue will not be called as halt() is not going to return. +// +// This function may panic, as we have no other ways of dealing with errors at this level #[unsafe(no_mangle)] #[inline(never)] -fn internal_dispatch_function() -> Result<()> { +#[instrument(skip_all, parent = Span::current(), level= "Trace")] +fn internal_dispatch_function() { let handle = unsafe { GUEST_HANDLE }; #[cfg(debug_assertions)] @@ -89,17 +96,51 @@ fn internal_dispatch_function() -> Result<()> { .try_pop_shared_input_data_into::() .expect("Function call deserialization failed"); - let result_vec = call_guest_function(function_call).inspect_err(|e| { - handle.write_error(e.kind.clone(), Some(e.message.as_str())); - })?; + let res = call_guest_function(function_call); - handle.push_shared_output_data(result_vec) + match res { + Ok(bytes) => { + handle + .push_shared_output_data(bytes.as_slice()) + .expect("Failed to serialize function call result"); + } + Err(err) => { + let guest_error = Err(GuestError::new(err.kind, err.message)); + let fcr = FunctionCallResult::new(guest_error); + let mut builder = FlatBufferBuilder::new(); + let data = fcr.encode(&mut builder); + handle + .push_shared_output_data(data) + .expect("Failed to serialize function call result"); + } + } } // This is implemented as a separate function to make sure that epilogue in the internal_dispatch_function is called before the halt() // which if it were included in the internal_dispatch_function cause the epilogue to not be called because the halt() would not return // when running in the hypervisor. +#[instrument(skip_all, parent = Span::current(), level= "Trace")] pub(crate) extern "C" fn dispatch_function() { - let _ = internal_dispatch_function(); + // The hyperlight host likes to use one partition and reset it in + // various ways; if that has happened, there might stale TLB + // entries hanging around from the former user of the + // partition. Flushing the TLB here is not quite the right thing + // to do, since incorrectly cached entries could make even this + // code not exist, but regrettably there is not a simple way for + // the host to trigger flushing when it ought to happen, so for + // now this works in practice, since the text segment is always + // part of the big identity-mapped region at the base of the + // guest. + crate::paging::flush_tlb(); + + // Read the current TSC to report it to the host with the spans/events + // This helps calculating the timestamps relative to the guest call + #[cfg(feature = "trace_guest")] + { + let guest_start_tsc = hyperlight_guest_tracing::invariant_tsc::read_tsc(); + hyperlight_guest_tracing::set_start_tsc(guest_start_tsc); + } + + internal_dispatch_function(); halt(); } diff --git a/src/hyperlight_guest_bin/src/host_comm.rs b/src/hyperlight_guest_bin/src/host_comm.rs index e21097661..ab0b3d46a 100644 --- a/src/hyperlight_guest_bin/src/host_comm.rs +++ b/src/hyperlight_guest_bin/src/host_comm.rs @@ -101,7 +101,7 @@ pub fn print_output_with_host_print(function_call: &FunctionCall) -> Result(LockedHeap); +#[cfg(feature = "mem_profile")] +unsafe impl alloc::alloc::GlobalAlloc for ProfiledLockedHeap { + unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 { + let addr = unsafe { self.0.alloc(layout) }; + unsafe { + core::arch::asm!("out dx, al", + in("dx") OutBAction::TraceMemoryAlloc as u16, + in("rax") layout.size() as u64, + in("rcx") addr as u64); + } + addr + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: core::alloc::Layout) { + unsafe { + core::arch::asm!("out dx, al", + in("dx") OutBAction::TraceMemoryFree as u16, + in("rax") layout.size() as u64, + in("rcx") ptr as u64); + self.0.dealloc(ptr, layout) + } + } + unsafe fn alloc_zeroed(&self, layout: core::alloc::Layout) -> *mut u8 { + let addr = unsafe { self.0.alloc_zeroed(layout) }; + unsafe { + core::arch::asm!("out dx, al", + in("dx") OutBAction::TraceMemoryAlloc as u16, + in("rax") layout.size() as u64, + in("rcx") addr as u64); + } + addr + } + unsafe fn realloc( + &self, + ptr: *mut u8, + layout: core::alloc::Layout, + new_size: usize, + ) -> *mut u8 { + let new_ptr = unsafe { self.0.realloc(ptr, layout, new_size) }; + unsafe { + core::arch::asm!("out dx, al", + in("dx") OutBAction::TraceMemoryFree as u16, + in("rax") layout.size() as u64, + in("rcx") ptr); + core::arch::asm!("out dx, al", + in("dx") OutBAction::TraceMemoryAlloc as u16, + in("rax") new_size as u64, + in("rcx") new_ptr); + } + new_ptr + } +} // === Globals === +#[cfg(not(feature = "mem_profile"))] #[global_allocator] pub(crate) static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::empty(); +#[cfg(feature = "mem_profile")] +#[global_allocator] +pub(crate) static HEAP_ALLOCATOR: ProfiledLockedHeap<32> = + ProfiledLockedHeap(LockedHeap::<32>::empty()); -pub(crate) static mut GUEST_HANDLE: GuestHandle = GuestHandle::new(); +pub static mut GUEST_HANDLE: GuestHandle = GuestHandle::new(); pub(crate) static mut REGISTERED_GUEST_FUNCTIONS: GuestFunctionRegister = GuestFunctionRegister::new(); @@ -75,11 +137,37 @@ pub static mut OS_PAGE_SIZE: u32 = 0; // to satisfy the clippy when cfg == test #[allow(dead_code)] fn panic(info: &core::panic::PanicInfo) -> ! { - let msg = info.to_string(); - let c_string = alloc::ffi::CString::new(msg) - .unwrap_or_else(|_| alloc::ffi::CString::new("panic (invalid utf8)").unwrap()); + _panic_handler(info) +} - unsafe { abort_with_code_and_message(&[ErrorCode::UnknownError as u8], c_string.as_ptr()) } +/// A writer that sends all output to the hyperlight host +/// using output ports. This allows us to not impose a +/// buffering limit on error message size on the guest end, +/// though one exists for the host. +struct HyperlightAbortWriter; +impl core::fmt::Write for HyperlightAbortWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + write_abort(s.as_bytes()); + Ok(()) + } +} + +#[inline(always)] +fn _panic_handler(info: &core::panic::PanicInfo) -> ! { + let mut w = HyperlightAbortWriter; + + // begin abort sequence by writing the error code + write_abort(&[ErrorCode::UnknownError as u8]); + + let write_res = write!(w, "{}", info); + if write_res.is_err() { + write_abort("panic: message format failed".as_bytes()); + } + + // write abort terminator to finish the abort + // and signal to the host that the message can now be read + write_abort(&[0xFF]); + unreachable!(); } // === Entrypoint === @@ -93,6 +181,10 @@ static INIT: Once = Once::new(); #[unsafe(no_mangle)] pub extern "C" fn entrypoint(peb_address: u64, seed: u64, ops: u64, max_log_level: u64) { + // Save the guest start TSC for tracing + #[cfg(feature = "trace_guest")] + let guest_start_tsc = hyperlight_guest_tracing::invariant_tsc::read_tsc(); + if peb_address == 0 { panic!("PEB address is null"); } @@ -103,17 +195,11 @@ pub extern "C" fn entrypoint(peb_address: u64, seed: u64, ops: u64, max_log_leve #[allow(static_mut_refs)] let peb_ptr = GUEST_HANDLE.peb().unwrap(); - let srand_seed = ((peb_address << 8 ^ seed >> 4) >> 32) as u32; + let srand_seed = (((peb_address << 8) ^ (seed >> 4)) >> 32) as u32; // Set the seed for the random number generator for C code using rand; srand(srand_seed); - // set up the logger - let max_log_level = LevelFilter::iter() - .nth(max_log_level as usize) - .expect("Invalid log level"); - init_logger(max_log_level); - // This static is to make it easier to implement the __chkstk function in assembly. // It also means that should we change the layout of the struct in the future, we // don't have to change the assembly code. @@ -128,7 +214,11 @@ pub extern "C" fn entrypoint(peb_address: u64, seed: u64, ops: u64, max_log_leve let heap_start = (*peb_ptr).guest_heap.ptr as usize; let heap_size = (*peb_ptr).guest_heap.size as usize; - HEAP_ALLOCATOR + #[cfg(not(feature = "mem_profile"))] + let heap_allocator = &HEAP_ALLOCATOR; + #[cfg(feature = "mem_profile")] + let heap_allocator = &HEAP_ALLOCATOR.0; + heap_allocator .try_lock() .expect("Failed to access HEAP_ALLOCATOR") .init(heap_start, heap_size); @@ -137,6 +227,18 @@ pub extern "C" fn entrypoint(peb_address: u64, seed: u64, ops: u64, max_log_leve (*peb_ptr).guest_function_dispatch_ptr = dispatch_function as usize as u64; + // set up the logger + let max_log_level = LevelFilter::iter() + .nth(max_log_level as usize) + .expect("Invalid log level"); + init_logger(max_log_level); + + // It is important that all the tracing events are produced after the tracing is initialized. + #[cfg(feature = "trace_guest")] + if max_log_level != LevelFilter::Off { + hyperlight_guest_tracing::init_guest_tracing(guest_start_tsc); + } + hyperlight_main(); } }); diff --git a/src/hyperlight_guest_bin/src/memory.rs b/src/hyperlight_guest_bin/src/memory.rs index 48eb4f602..5cc72c8ab 100644 --- a/src/hyperlight_guest_bin/src/memory.rs +++ b/src/hyperlight_guest_bin/src/memory.rs @@ -40,36 +40,51 @@ use hyperlight_guest::exit::abort_with_code; */ // We assume the maximum alignment for any value is the alignment of u128. -const MAX_ALIGN: usize = align_of::(); +const DEFAULT_ALIGN: usize = align_of::(); +const HEADER_LEN: usize = size_of::
(); + +#[repr(transparent)] +// A header that stores the layout information for the allocated memory block. +struct Header(Layout); /// Allocates a block of memory with the given size. The memory is only guaranteed to be initialized to 0s if `zero` is true, otherwise /// it may or may not be initialized. /// +/// # Invariants +/// `alignment` must be non-zero and a power of two +/// /// # Safety /// The returned pointer must be freed with `memory::free` when it is no longer needed, otherwise memory will leak. -unsafe fn alloc_helper(size: usize, zero: bool) -> *mut c_void { +unsafe fn alloc_helper(size: usize, alignment: usize, zero: bool) -> *mut c_void { if size == 0 { return ptr::null_mut(); } - // Allocate a block that includes space for both layout information and data - let total_size = size - .checked_add(size_of::()) - .expect("data and layout size should not overflow in alloc"); - let layout = Layout::from_size_align(total_size, MAX_ALIGN).expect("Invalid layout"); + let actual_align = alignment.max(align_of::
()); + let data_offset = HEADER_LEN.next_multiple_of(actual_align); + + let Some(total_size) = data_offset.checked_add(size) else { + abort_with_code(&[ErrorCode::MallocFailed as u8]); + }; + + // Create layout for entire allocation + let layout = + Layout::from_size_align(total_size, actual_align).expect("Invalid layout parameters"); unsafe { let raw_ptr = match zero { true => alloc::alloc::alloc_zeroed(layout), false => alloc::alloc::alloc(layout), }; + if raw_ptr.is_null() { abort_with_code(&[ErrorCode::MallocFailed as u8]); - } else { - let layout_ptr = raw_ptr as *mut Layout; - layout_ptr.write(layout); - layout_ptr.add(1) as *mut c_void } + + // Place Header immediately before the user data region + let header_ptr = raw_ptr.add(data_offset - HEADER_LEN).cast::
(); + header_ptr.write(Header(layout)); + raw_ptr.add(data_offset) as *mut c_void } } @@ -80,7 +95,7 @@ unsafe fn alloc_helper(size: usize, zero: bool) -> *mut c_void { /// The returned pointer must be freed with `memory::free` when it is no longer needed, otherwise memory will leak. #[unsafe(no_mangle)] pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void { - unsafe { alloc_helper(size, false) } + unsafe { alloc_helper(size, DEFAULT_ALIGN, false) } } /// Allocates a block of memory for an array of `nmemb` elements, each of `size` bytes. @@ -95,22 +110,45 @@ pub unsafe extern "C" fn calloc(nmemb: usize, size: usize) -> *mut c_void { .checked_mul(size) .expect("nmemb * size should not overflow in calloc"); - alloc_helper(total_size, true) + alloc_helper(total_size, DEFAULT_ALIGN, true) } } +/// Allocates aligned memory. +/// +/// # Safety +/// The returned pointer must be freed with `free` when it is no longer needed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn aligned_alloc(alignment: usize, size: usize) -> *mut c_void { + // Validate alignment + if alignment == 0 || (alignment & (alignment - 1)) != 0 { + return ptr::null_mut(); + } + + unsafe { alloc_helper(size, alignment, false) } +} + /// Frees the memory block pointed to by `ptr`. /// /// # Safety /// `ptr` must be a pointer to a memory block previously allocated by `memory::malloc`, `memory::calloc`, or `memory::realloc`. #[unsafe(no_mangle)] pub unsafe extern "C" fn free(ptr: *mut c_void) { - if !ptr.is_null() { - unsafe { - let block_start = (ptr as *const Layout).sub(1); - let layout = block_start.read(); - alloc::alloc::dealloc(block_start as *mut u8, layout) - } + if ptr.is_null() { + return; + } + + let user_ptr = ptr as *const u8; + + unsafe { + // Read the Header just before the user data + let header_ptr = user_ptr.sub(HEADER_LEN).cast::
(); + let layout = header_ptr.read().0; + + // Deallocate from the original base pointer + let offset = HEADER_LEN.next_multiple_of(layout.align()); + let raw_ptr = user_ptr.sub(offset) as *mut u8; + alloc::alloc::dealloc(raw_ptr, layout); } } @@ -134,26 +172,24 @@ pub unsafe extern "C" fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void { return ptr::null_mut(); } - let total_new_size = size - .checked_add(size_of::()) - .expect("data and layout size should not overflow in realloc"); + let user_ptr = ptr as *const u8; - let block_start = unsafe { (ptr as *const Layout).sub(1) }; - let old_layout = unsafe { block_start.read() }; - let new_layout = Layout::from_size_align(total_new_size, MAX_ALIGN).unwrap(); + unsafe { + let header_ptr = user_ptr.sub(HEADER_LEN).cast::
(); - let new_block_start = - unsafe { alloc::alloc::realloc(block_start as *mut u8, old_layout, total_new_size) } - as *mut Layout; + let old_layout = header_ptr.read().0; + let old_offset = HEADER_LEN.next_multiple_of(old_layout.align()); + let old_user_size = old_layout.size() - old_offset; - if new_block_start.is_null() { - // Realloc failed - abort_with_code(&[ErrorCode::MallocFailed as u8]); - } else { - // Update the stored Layout, then return ptr to memory right after the Layout. - unsafe { - new_block_start.write(new_layout); - new_block_start.add(1) as *mut c_void + let new_ptr = alloc_helper(size, old_layout.align(), false); + if new_ptr.is_null() { + return ptr::null_mut(); } + + let copy_size = old_user_size.min(size); + ptr::copy_nonoverlapping(user_ptr, new_ptr as *mut u8, copy_size); + + free(ptr); + new_ptr } } diff --git a/src/hyperlight_guest_bin/src/paging.rs b/src/hyperlight_guest_bin/src/paging.rs new file mode 100644 index 000000000..4ee3d827a --- /dev/null +++ b/src/hyperlight_guest_bin/src/paging.rs @@ -0,0 +1,287 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +use alloc::alloc::Layout; +use core::arch::asm; + +use tracing::{Span, instrument}; + +use crate::OS_PAGE_SIZE; + +/// Convert a physical address in main memory to a virtual address +/// through the pysmap +/// +/// This is _not guaranteed_ to work with device memory +pub fn ptov(x: u64) -> *mut u8 { + // Currently, all of main memory is identity mapped + x as *mut u8 +} + +// TODO: This is not at all thread-safe atm +// TODO: A lot of code in this file uses inline assembly to load and +// store page table entries. It would be nice to use pointer +// volatile read/writes instead, but unfortunately we have a PTE +// at physical address 0, which is currently identity-mapped at +// virtual address 0, and Rust raw pointer operations can't be +// used to read/write from address 0. + +/// A helper structure indicating a mapping operation that needs to be +/// performed +struct MapRequest { + table_base: u64, + vmin: *mut u8, + len: u64, +} + +/// A helper structure indicating that a particular PTE needs to be +/// modified +struct MapResponse { + entry_ptr: *mut u64, + vmin: *mut u8, + len: u64, +} + +/// Assumption: all are page-aligned +/// # Safety +/// This function modifies pages backing a virtual memory range which is inherently unsafe w.r.t. +/// the Rust memory model. +/// When using this function note: +/// - No locking is performed before touching page table data structures, +/// as such do not use concurrently with any other page table operations +/// - TLB invalidation is not performed, +/// if previously-unmapped ranges are not being mapped, TLB invalidation may need to be performed afterwards. +#[instrument(skip_all, parent = Span::current(), level= "Trace")] +pub unsafe fn map_region(phys_base: u64, virt_base: *mut u8, len: u64) { + let mut pml4_base: u64; + unsafe { + asm!("mov {}, cr3", out(reg) pml4_base); + } + pml4_base &= !0xfff; + modify_ptes::<47, 39>(MapRequest { + table_base: pml4_base, + vmin: virt_base, + len, + }) + .map(|r| unsafe { alloc_pte_if_needed(r) }) + .flat_map(modify_ptes::<38, 30>) + .map(|r| unsafe { alloc_pte_if_needed(r) }) + .flat_map(modify_ptes::<29, 21>) + .map(|r| unsafe { alloc_pte_if_needed(r) }) + .flat_map(modify_ptes::<20, 12>) + .map(|r| map_normal(phys_base, virt_base, r)) + .for_each(drop); +} + +#[allow(unused)] +/// This function is not presently used for anything, but is useful +/// for debugging +/// # Safety +/// This function traverses page table data structures, and should not be called concurrently +/// with any other operations that modify the page table. +/// # Panics +/// This function will panic if: +/// - A page map request resolves to multiple page table entries +pub unsafe fn dbg_print_address_pte(address: u64) -> u64 { + let mut pml4_base: u64 = 0; + unsafe { + asm!("mov {}, cr3", out(reg) pml4_base); + } + pml4_base &= !0xfff; + let addrs = modify_ptes::<47, 39>(MapRequest { + table_base: pml4_base, + vmin: address as *mut u8, + len: unsafe { OS_PAGE_SIZE as u64 }, + }) + .map(|r| unsafe { require_pte_exist(r) }) + .flat_map(modify_ptes::<38, 30>) + .map(|r| unsafe { require_pte_exist(r) }) + .flat_map(modify_ptes::<29, 21>) + .map(|r| unsafe { require_pte_exist(r) }) + .flat_map(modify_ptes::<20, 12>) + .map(|r| { + let mut pte: u64 = 0; + unsafe { + asm!("mov {}, qword ptr [{}]", out(reg) pte, in(reg) r.entry_ptr); + } + pte + }) + .collect::>(); + if addrs.len() != 1 { + panic!("impossible: 1 page map request resolved to multiple PTEs"); + } + addrs[0] +} + +/// Allocate n contiguous physical pages and return the physical +/// addresses of the pages in question. +/// # Safety +/// This function is not inherently unsafe but will likely become so in the future +/// when a real physical page allocator is implemented. +/// # Panics +/// This function will panic if: +/// - The Layout creation fails +/// - Memory allocation fails +pub unsafe fn alloc_phys_pages(n: u64) -> u64 { + // Currently, since all of main memory is idmap'd, we can just + // allocate any appropriately aligned section of memory. + unsafe { + let v = alloc::alloc::alloc_zeroed( + Layout::from_size_align(n as usize * OS_PAGE_SIZE as usize, OS_PAGE_SIZE as usize) + .expect("could not create physical page allocation layout"), + ); + if v.is_null() { + panic!("could not allocate a physical page"); + } + v as u64 + } +} + +/// # Safety +/// This function traverses page table data structures, and should not be called concurrently +/// with any other operations that modify the page table. +unsafe fn require_pte_exist(x: MapResponse) -> MapRequest { + let mut pte: u64; + unsafe { + asm!("mov {}, qword ptr [{}]", out(reg) pte, in(reg) x.entry_ptr); + } + let present = pte & 0x1; + if present == 0 { + panic!("debugging: found not-present pte"); + } + MapRequest { + table_base: pte & !0xfff, + vmin: x.vmin, + len: x.len, + } +} + +/// Page-mapping callback to allocate a next-level page table if necessary. +/// # Safety +/// This function modifies page table data structures, and should not be called concurrently +/// with any other operations that modify the page table. +unsafe fn alloc_pte_if_needed(x: MapResponse) -> MapRequest { + let mut pte: u64; + unsafe { + asm!("mov {}, qword ptr [{}]", out(reg) pte, in(reg) x.entry_ptr); + } + let present = pte & 0x1; + if present != 0 { + return MapRequest { + table_base: pte & !0xfff, + vmin: x.vmin, + len: x.len, + }; + } + let page_addr = unsafe { alloc_phys_pages(1) }; + unsafe { ptov(page_addr).write_bytes(0u8, OS_PAGE_SIZE as usize) }; + + #[allow(clippy::identity_op)] + #[allow(clippy::precedence)] + let pte = page_addr | + 1 << 5 | // A - we don't track accesses at table level + 0 << 4 | // PCD - leave caching enabled + 0 << 3 | // PWT - write-back + 1 << 2 | // U/S - allow user access to everything (for now) + 1 << 1 | // R/W - we don't use block-level permissions + 1 << 0; // P - this entry is present + unsafe { + asm!("mov qword ptr [{}], {}", in(reg) x.entry_ptr, in(reg) pte); + } + MapRequest { + table_base: page_addr, + vmin: x.vmin, + len: x.len, + } +} + +/// Map a normal memory page +/// +/// TODO: support permissions; currently mapping is always RWX +fn map_normal(phys_base: u64, virt_base: *mut u8, r: MapResponse) { + #[allow(clippy::identity_op)] + #[allow(clippy::precedence)] + let pte = (phys_base + (r.vmin as u64 - virt_base as u64)) | + 1 << 6 | // D - we don't presently track dirty state for anything + 1 << 5 | // A - we don't presently track access for anything + 0 << 4 | // PCD - leave caching enabled + 0 << 3 | // PWT - write-back + 1 << 2 | // U/S - allow user access to everything (for now) + 1 << 1 | // R/W - for now make everything r/w + 1 << 0; // P - this entry is present + unsafe { + r.entry_ptr.write_volatile(pte); + } +} + +#[inline(always)] +/// Utility function to extract an (inclusive on both ends) bit range +/// from a quadword. +fn bits(x: u64) -> u64 { + (x & ((1 << (HIGH_BIT + 1)) - 1)) >> LOW_BIT +} + +struct ModifyPteIterator { + request: MapRequest, + n: u64, +} +impl Iterator for ModifyPteIterator { + type Item = MapResponse; + fn next(&mut self) -> Option { + if (self.n << LOW_BIT) >= self.request.len { + return None; + } + // next stage parameters + let next_vmin = self.request.vmin.wrapping_add((self.n << LOW_BIT) as usize); + let entry_ptr = ptov(self.request.table_base) + .wrapping_add((bits::(next_vmin as u64) << 3) as usize) + as *mut u64; + let len_from_here = self.request.len - (self.n << LOW_BIT); + let next_len = core::cmp::min(len_from_here, 1 << LOW_BIT); + + // update our state + self.n += 1; + + Some(MapResponse { + entry_ptr, + vmin: next_vmin, + len: next_len, + }) + } +} +fn modify_ptes( + r: MapRequest, +) -> ModifyPteIterator { + ModifyPteIterator { request: r, n: 0 } +} + +pub fn flush_tlb() { + // Currently this just always flips CR4.PGE back and forth to + // trigger a tlb flush. We should use a faster approach where + // available + let mut orig_cr4: u64; + unsafe { + asm!("mov {}, cr4", out(reg) orig_cr4); + } + let tmp_cr4: u64 = orig_cr4 ^ (1 << 7); // CR4.PGE + unsafe { + asm!( + "mov cr4, {}", + "mov cr4, {}", + in(reg) tmp_cr4, + in(reg) orig_cr4 + ); + } +} diff --git a/src/hyperlight_guest_capi/Cargo.toml b/src/hyperlight_guest_capi/Cargo.toml index 66bc454ab..5727fbdf2 100644 --- a/src/hyperlight_guest_capi/Cargo.toml +++ b/src/hyperlight_guest_capi/Cargo.toml @@ -15,7 +15,9 @@ workspace = true hyperlight-guest = { workspace = true, default-features = false } hyperlight-guest-bin = { workspace = true, default-features = true } hyperlight-common = { workspace = true, default-features = false } + +flatbuffers = { version = "25.2.10", default-features = false } log = { version = "0.4", default-features = false } [build-dependencies] -cbindgen = "0.29.0" +cbindgen = "0.29.2" diff --git a/src/hyperlight_guest_capi/src/error.rs b/src/hyperlight_guest_capi/src/error.rs index a6be2bf59..03217600e 100644 --- a/src/hyperlight_guest_capi/src/error.rs +++ b/src/hyperlight_guest_capi/src/error.rs @@ -14,15 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -use core::ffi::c_char; +use core::ffi::{CStr, c_char}; -use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; -use hyperlight_guest_bin::guest_err::setError; +use flatbuffers::FlatBufferBuilder; +use hyperlight_common::flatbuffer_wrappers::function_types::FunctionCallResult; +use hyperlight_common::flatbuffer_wrappers::guest_error::{ErrorCode, GuestError}; +use hyperlight_guest_bin::GUEST_HANDLE; + +use crate::alloc::borrow::ToOwned; #[unsafe(no_mangle)] pub extern "C" fn hl_set_error(err: ErrorCode, message: *const c_char) { + let cstr = unsafe { CStr::from_ptr(message) }; + let guest_error = Err(GuestError::new( + err.into(), + cstr.to_str() + .expect("Failed to convert CStr to &str") + .to_owned(), + )); + let fcr = FunctionCallResult::new(guest_error); + let mut builder = FlatBufferBuilder::new(); + let data = fcr.encode(&mut builder); unsafe { - setError(err.into(), message); + #[allow(static_mut_refs)] // we are single threaded + GUEST_HANDLE + .push_shared_output_data(data) + .expect("Failed to set error") } } diff --git a/src/hyperlight_guest_capi/src/flatbuffer.rs b/src/hyperlight_guest_capi/src/flatbuffer.rs index b710d4245..ff12400d6 100644 --- a/src/hyperlight_guest_capi/src/flatbuffer.rs +++ b/src/hyperlight_guest_capi/src/flatbuffer.rs @@ -15,6 +15,9 @@ limitations under the License. */ use alloc::boxed::Box; +use alloc::ffi::CString; +use alloc::string::String; +use alloc::vec::Vec; use core::ffi::{CStr, c_char}; use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result; @@ -92,6 +95,13 @@ pub extern "C" fn hl_flatbuffer_result_from_Bytes(data: *const u8, len: usize) - Box::new(unsafe { FfiVec::from_vec(vec) }) } +#[unsafe(no_mangle)] +pub extern "C" fn hl_flatbuffer_result_from_Bool(value: bool) -> Box { + let vec = get_flatbuffer_result(value); + + Box::new(unsafe { FfiVec::from_vec(vec) }) +} + //--- Functions for getting values returned by host functions calls #[unsafe(no_mangle)] @@ -115,4 +125,34 @@ pub extern "C" fn hl_get_host_return_value_as_ULong() -> u64 { get_host_return_value().expect("Unable to get host return value as ulong") } -// TODO add bool, float, double, string, vecbytes +#[unsafe(no_mangle)] +pub extern "C" fn hl_get_host_return_value_as_Bool() -> bool { + get_host_return_value().expect("Unable to get host return value as bool") +} + +#[unsafe(no_mangle)] +pub extern "C" fn hl_get_host_return_value_as_Float() -> f32 { + get_host_return_value().expect("Unable to get host return value as f32") +} + +#[unsafe(no_mangle)] +pub extern "C" fn hl_get_host_return_value_as_Double() -> f64 { + get_host_return_value().expect("Unable to get host return value as f64") +} + +#[unsafe(no_mangle)] +pub extern "C" fn hl_get_host_return_value_as_String() -> *const c_char { + let string_value: String = + get_host_return_value().expect("Unable to get host return value as string"); + + let c_string = CString::new(string_value).expect("Failed to create CString"); + c_string.into_raw() +} + +#[unsafe(no_mangle)] +pub extern "C" fn hl_get_host_return_value_as_VecBytes() -> Box { + let vec_value: Vec = + get_host_return_value().expect("Unable to get host return value as vec bytes"); + + Box::new(unsafe { FfiVec::from_vec(vec_value) }) +} diff --git a/src/hyperlight_guest_tracing/Cargo.toml b/src/hyperlight_guest_tracing/Cargo.toml new file mode 100644 index 000000000..5109aba85 --- /dev/null +++ b/src/hyperlight_guest_tracing/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "hyperlight-guest-tracing" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +description = """Provides the tracing functionality for the hyperlight guest.""" + +[dependencies] +heapless = { version = "0.9.1", features = ["serde"] } +hyperlight-common = { workspace = true, default-features = false } +spin = "0.10.0" +tracing = { version = "0.1.41", default-features = false, features = ["attributes"] } +tracing-core = { version = "0.1.34", default-features = false } + +[lints] +workspace = true + +[features] +default = [] +trace = [ "hyperlight-common/trace_guest" ] diff --git a/src/hyperlight_guest_tracing/src/invariant_tsc.rs b/src/hyperlight_guest_tracing/src/invariant_tsc.rs new file mode 100644 index 000000000..13521f906 --- /dev/null +++ b/src/hyperlight_guest_tracing/src/invariant_tsc.rs @@ -0,0 +1,47 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +/// Module for checking invariant TSC support and reading the timestamp counter +use core::arch::x86_64::{__cpuid, _rdtsc}; + +/// Check if the processor supports invariant TSC +/// +/// Returns true if CPUID.80000007H:EDX[8] is set, indicating invariant TSC support +pub fn has_invariant_tsc() -> bool { + // Check if extended CPUID functions are available + let max_extended = unsafe { __cpuid(0x80000000) }; + if max_extended.eax < 0x80000007 { + return false; + } + + // Query CPUID.80000007H for invariant TSC support + let cpuid_result = unsafe { __cpuid(0x80000007) }; + + // Check bit 8 of EDX register for invariant TSC support + (cpuid_result.edx & (1 << 8)) != 0 +} + +/// Read the timestamp counter +/// +/// This function provides a high-performance timestamp by reading the TSC. +/// Should only be used when invariant TSC is supported for reliable timing. +/// +/// # Safety +/// This function uses unsafe assembly instructions but is safe to call. +/// However, the resulting timestamp is only meaningful if invariant TSC is supported. +pub fn read_tsc() -> u64 { + unsafe { _rdtsc() } +} diff --git a/src/hyperlight_guest_tracing/src/lib.rs b/src/hyperlight_guest_tracing/src/lib.rs new file mode 100644 index 000000000..ded4e91aa --- /dev/null +++ b/src/hyperlight_guest_tracing/src/lib.rs @@ -0,0 +1,224 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +#![no_std] +use heapless as hl; + +/// Expose invariant TSC module +pub mod invariant_tsc; + +/// Defines internal guest state +#[cfg(feature = "trace")] +mod state; + +/// Defines guest tracing Subscriber +#[cfg(feature = "trace")] +mod subscriber; + +/// Defines a type to iterate over spans/events fields +#[cfg(feature = "trace")] +mod visitor; + +/// Type to get the relevant information from the internal state +/// and expose it to the host +#[cfg(feature = "trace")] +pub use state::TraceBatchInfo; +#[cfg(feature = "trace")] +pub use trace::{ + clean_trace_state, end_trace, guest_trace_info, init_guest_tracing, is_trace_enabled, + set_start_tsc, +}; + +/// Maximum number of spans that the guest can store +const MAX_NO_OF_SPANS: usize = 10; +/// Maximum number of events that the guest can store +const MAX_NO_OF_EVENTS: usize = 10; +/// Maximum length a name can have in a span/event +const MAX_NAME_LENGTH: usize = 64; +/// Maximum length the target can have in a span/event +const MAX_TARGET_LENGTH: usize = 64; +/// Maximum length key of a Field can have +const MAX_FIELD_KEY_LENGTH: usize = 32; +/// Maximum length value of a Field can have +const MAX_FIELD_VALUE_LENGTH: usize = 96; +/// Maximum number of fields a span/event can have +const MAX_NO_OF_FIELDS: usize = 8; + +/// Alias for the complicated heapless::Vec type for Spans +pub type Spans = hl::Vec< + GuestSpan< + MAX_NAME_LENGTH, + MAX_TARGET_LENGTH, + MAX_FIELD_KEY_LENGTH, + MAX_FIELD_VALUE_LENGTH, + MAX_NO_OF_FIELDS, + >, + MAX_NO_OF_SPANS, +>; + +/// Alias for the complicated heapless::Vec type for Events +pub type Events = hl::Vec< + GuestEvent, + MAX_NO_OF_EVENTS, +>; + +/// The trace level assigned to a span/event +#[derive(Debug, Copy, Clone)] +pub enum TraceLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for TraceLevel { + fn from(value: tracing::Level) -> Self { + match value { + tracing::Level::ERROR => Self::Error, + tracing::Level::WARN => Self::Warn, + tracing::Level::INFO => Self::Info, + tracing::Level::DEBUG => Self::Debug, + tracing::Level::TRACE => Self::Trace, + } + } +} +impl From for tracing::Level { + fn from(value: TraceLevel) -> Self { + match value { + TraceLevel::Error => Self::ERROR, + TraceLevel::Warn => Self::WARN, + TraceLevel::Info => Self::INFO, + TraceLevel::Debug => Self::DEBUG, + TraceLevel::Trace => Self::TRACE, + } + } +} + +/// The structure in which a guest stores Span information +pub struct GuestSpan< + const N: usize, + const T: usize, + const FK: usize, + const FV: usize, + const F: usize, +> { + pub id: u64, + pub parent_id: Option, + pub level: TraceLevel, + /// Span name + pub name: hl::String, + /// Filename + pub target: hl::String, + pub start_tsc: u64, + pub end_tsc: Option, + pub fields: hl::Vec<(hl::String, hl::String), F>, +} + +/// The structure in which a guest stores Event information +pub struct GuestEvent { + pub parent_id: u64, + pub level: TraceLevel, + pub name: hl::String, + /// Event name + pub tsc: u64, + pub fields: hl::Vec<(hl::String, hl::String), F>, +} + +/// This module is gated because some of these types are also used on the host, but we want +/// only the guest to allocate and allow the functionality intended for the guest. +#[cfg(feature = "trace")] +mod trace { + extern crate alloc; + use alloc::sync::{Arc, Weak}; + + use spin::Mutex; + + use super::*; + use crate::state::GuestState; + use crate::subscriber::GuestSubscriber; + + /// Weak reference to the guest state so we can manually trigger flush to host + static GUEST_STATE: spin::Once>> = spin::Once::new(); + + /// Initialize the guest tracing subscriber as global default. + pub fn init_guest_tracing(guest_start_tsc: u64) { + // Set as global default if not already set. + if tracing_core::dispatcher::has_been_set() { + return; + } + let sub = GuestSubscriber::new(guest_start_tsc); + let state = sub.state(); + // Store state Weak to use later at runtime + GUEST_STATE.call_once(|| Arc::downgrade(state)); + + // Set global dispatcher + let _ = tracing_core::dispatcher::set_global_default(tracing_core::Dispatch::new(sub)); + } + + /// Sets the guset starting timestamp reported to the host on a VMExit + pub fn set_start_tsc(guest_start_tsc: u64) { + if let Some(w) = GUEST_STATE.get() + && let Some(state) = w.upgrade() + { + state.lock().set_start_tsc(guest_start_tsc); + } + } + + /// Ends the current trace by ending all active spans in the + /// internal state and storing the end timestamps. + /// + /// This expects an outb call to send the spans to the host. + /// After calling this function, the internal state is marked + /// for cleaning on the next access. + pub fn end_trace() { + if let Some(w) = GUEST_STATE.get() + && let Some(state) = w.upgrade() + { + state.lock().end_trace(); + } + } + + /// Cleans the internal trace state by removing closed spans and events. + /// This ensures that after a VM exit, we keep the spans that + /// are still active (in the stack) and remove all other spans and events. + pub fn clean_trace_state() { + if let Some(w) = GUEST_STATE.get() + && let Some(state) = w.upgrade() + { + state.lock().clean(); + } + } + + /// Returns information about the current trace state needed by the host to read the spans. + pub fn guest_trace_info() -> Option { + let mut res = None; + if let Some(w) = GUEST_STATE.get() + && let Some(state) = w.upgrade() + { + res = Some(state.lock().guest_trace_info()); + } + res + } + + /// Returns true if tracing is enabled (the guest tracing state is initialized). + pub fn is_trace_enabled() -> bool { + GUEST_STATE + .get() + .map(|w| w.upgrade().is_some()) + .unwrap_or(false) + } +} diff --git a/src/hyperlight_guest_tracing/src/state.rs b/src/hyperlight_guest_tracing/src/state.rs new file mode 100644 index 000000000..f7196a350 --- /dev/null +++ b/src/hyperlight_guest_tracing/src/state.rs @@ -0,0 +1,283 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ +extern crate alloc; + +use core::sync::atomic::{AtomicU64, Ordering}; + +use heapless as hl; +use hyperlight_common::outb::OutBAction; +use tracing_core::Event; +use tracing_core::span::{Attributes, Id, Record}; + +use crate::visitor::FieldsVisitor; +use crate::{ + GuestEvent, GuestSpan, MAX_FIELD_KEY_LENGTH, MAX_FIELD_VALUE_LENGTH, MAX_NAME_LENGTH, + MAX_NO_OF_EVENTS, MAX_NO_OF_FIELDS, MAX_NO_OF_SPANS, MAX_TARGET_LENGTH, invariant_tsc, +}; + +pub struct TraceBatchInfo { + /// The timestamp counter at the start of the guest execution. + pub guest_start_tsc: u64, + /// Pointer to the spans in the guest memory. + pub spans_ptr: u64, + /// Pointer to the events in the guest memory. + pub events_ptr: u64, +} + +/// Helper type to define the guest state with the configured constants +pub type GuestState = TraceState< + MAX_NO_OF_SPANS, + MAX_NO_OF_EVENTS, + MAX_NAME_LENGTH, + MAX_TARGET_LENGTH, + MAX_FIELD_KEY_LENGTH, + MAX_FIELD_VALUE_LENGTH, + MAX_NO_OF_FIELDS, +>; + +/// Internal state of the tracing subscriber +pub(crate) struct TraceState< + const SP: usize, + const EV: usize, + const N: usize, + const T: usize, + const FK: usize, + const FV: usize, + const F: usize, +> { + /// Whether we need to cleanup the state on next access + cleanup_needed: bool, + /// The timestamp counter at the start of the guest execution. + guest_start_tsc: u64, + /// Next span ID to allocate + next_id: AtomicU64, + /// All spans collected + spans: hl::Vec, SP>, + /// All events collected + events: hl::Vec, EV>, + /// Stack of active spans + stack: hl::Vec, +} + +impl< + const SP: usize, + const EV: usize, + const N: usize, + const T: usize, + const FK: usize, + const FV: usize, + const F: usize, +> TraceState +{ + pub(crate) fn new(guest_start_tsc: u64) -> Self { + Self { + cleanup_needed: false, + guest_start_tsc, + next_id: AtomicU64::new(1), + spans: hl::Vec::new(), + stack: hl::Vec::new(), + events: hl::Vec::new(), + } + } + + pub(crate) fn alloc_id(&self) -> (u64, Id) { + let n = self.next_id.load(Ordering::Relaxed); + self.next_id.store(n + 1, Ordering::Relaxed); + + (n, Id::from_u64(n)) + } + + /// Cleanup internal state by removing closed spans and events + /// This ensures that after a VM exit, we keep the spans that + /// are still active (in the stack) and remove all other spans and events. + pub fn clean(&mut self) { + // Remove all spans that have an end timestamp (closed spans) + self.spans.retain(|s| s.end_tsc.is_none()); + + // Remove all events + self.events.clear(); + } + + #[inline(always)] + fn verify_and_clean(&mut self) { + if self.cleanup_needed { + self.clean(); + self.cleanup_needed = false; + } + } + + /// Triggers a VM exit to flush the current spans to the host. + /// This also clears the internal state to start fresh. + fn send_to_host(&mut self) { + let guest_start_tsc = self.guest_start_tsc; + let spans_ptr = &self.spans as *const _ as u64; + let events_ptr = &self.events as *const _ as u64; + + unsafe { + core::arch::asm!("out dx, al", + // Port value for tracing + in("dx") OutBAction::TraceBatch as u16, + // Additional magic number to identify the action + in("r8") OutBAction::TraceBatch as u64, + in("r9") guest_start_tsc, + in("r10") spans_ptr, + in("r11") events_ptr, + ); + } + + self.clean(); + } + + /// Set a new guest start tsc + pub(crate) fn set_start_tsc(&mut self, guest_start_tsc: u64) { + self.guest_start_tsc = guest_start_tsc; + } + + /// Closes the trace by ending all spans + /// NOTE: This expects an outb call to send the spans to the host. + pub(crate) fn end_trace(&mut self) { + for span in self.spans.iter_mut() { + if span.end_tsc.is_none() { + span.end_tsc = Some(invariant_tsc::read_tsc()); + } + } + + // Empty the stack + while self.stack.pop().is_some() { + // Pop all remaining spans from the stack + } + + // Mark for clearing when re-entering the VM because we might + // not enter on the same place as we exited (e.g. halt) + self.cleanup_needed = true; + } + + /// Returns information about the information needed by the host to read the spans. + pub(crate) fn guest_trace_info(&mut self) -> TraceBatchInfo { + TraceBatchInfo { + guest_start_tsc: self.guest_start_tsc, + spans_ptr: &self.spans as *const _ as u64, + events_ptr: &self.events as *const _ as u64, + } + } + + /// Create a new span and push it on the stack + pub(crate) fn new_span(&mut self, attrs: &Attributes) -> Id { + self.verify_and_clean(); + let (idn, id) = self.alloc_id(); + + let md = attrs.metadata(); + let mut name = hl::String::::new(); + let mut target = hl::String::::new(); + // Shorten name and target if they are bigger than the space allocated + let _ = name.push_str(&md.name()[..usize::min(md.name().len(), name.capacity())]); + let _ = target.push_str(&md.target()[..usize::min(md.target().len(), target.capacity())]); + + // Visit fields to collect them + let mut fields = hl::Vec::new(); + attrs.record(&mut FieldsVisitor:: { out: &mut fields }); + + // Find parent from current stack top (if any) + let parent_id = self.stack.last().copied(); + + let span = GuestSpan:: { + id: idn, + parent_id, + level: (*md.level()).into(), + name, + target, + start_tsc: invariant_tsc::read_tsc(), + end_tsc: None, + fields, + }; + + let spans = &mut self.spans; + // Should never fail because we flush when full + let _ = spans.push(span); + + // In case the spans Vec is full, we need to report them to the host + if spans.len() == spans.capacity() { + self.send_to_host(); + } + + id + } + + /// Record an event in the current span (top of the stack) + pub(crate) fn event(&mut self, event: &Event<'_>) { + self.verify_and_clean(); + let stack = &mut self.stack; + let parent_id = stack.last().copied().unwrap_or(0); + + let md = event.metadata(); + let mut name = hl::String::::new(); + // Shorten name and target if they are bigger than the space allocated + let _ = name.push_str(&md.name()[..usize::min(md.name().len(), name.capacity())]); + + let mut fields = hl::Vec::new(); + event.record(&mut FieldsVisitor:: { out: &mut fields }); + + let ev = GuestEvent { + parent_id, + level: (*md.level()).into(), + name, + tsc: invariant_tsc::read_tsc(), + fields, + }; + + // Should never fail because we flush when full + let _ = self.events.push(ev); + + // Flush buffer to host if full + if self.events.len() >= self.events.capacity() { + self.send_to_host(); + } + } + + /// Record new values for an existing span + pub(crate) fn record(&mut self, id: &Id, values: &Record<'_>) { + let spans = &mut self.spans; + if let Some(s) = spans.iter_mut().find(|s| s.id == id.into_u64()) { + let mut v = hl::Vec::new(); + values.record(&mut FieldsVisitor:: { out: &mut v }); + s.fields.extend(v); + } + } + + /// Enter a span (push it on the stack) + pub(crate) fn enter(&mut self, id: &Id) { + let st = &mut self.stack; + let _ = st.push(id.into_u64()); + } + + /// Exit a span (pop it from the stack) + pub(crate) fn exit(&mut self, _id: &Id) { + let st = &mut self.stack; + let _ = st.pop(); + } + + /// Try to close a span by ID, returning true if successful + /// Records the end timestamp for the span. + pub(crate) fn try_close(&mut self, id: Id) -> bool { + let spans = &mut self.spans; + if let Some(s) = spans.iter_mut().find(|s| s.id == id.into_u64()) { + s.end_tsc = Some(invariant_tsc::read_tsc()); + true + } else { + false + } + } +} diff --git a/src/hyperlight_guest_tracing/src/subscriber.rs b/src/hyperlight_guest_tracing/src/subscriber.rs new file mode 100644 index 000000000..fca202bb0 --- /dev/null +++ b/src/hyperlight_guest_tracing/src/subscriber.rs @@ -0,0 +1,78 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ +extern crate alloc; + +use alloc::sync::Arc; + +use spin::Mutex; +use tracing_core::span::{Attributes, Id, Record}; +use tracing_core::subscriber::Subscriber; +use tracing_core::{Event, Metadata}; + +use crate::state::GuestState; + +/// The subscriber is used to collect spans and events in the guest. +pub(crate) struct GuestSubscriber { + /// Internal state that holds the spans and events + /// Protected by a Mutex for inner mutability + /// A reference to this state is stored in a static variable + state: Arc>, +} + +impl GuestSubscriber { + pub(crate) fn new(guest_start_tsc: u64) -> Self { + Self { + state: Arc::new(Mutex::new(GuestState::new(guest_start_tsc))), + } + } + pub(crate) fn state(&self) -> &Arc> { + &self.state + } +} + +impl Subscriber for GuestSubscriber { + fn enabled(&self, _md: &Metadata<'_>) -> bool { + true + } + + fn new_span(&self, attrs: &Attributes<'_>) -> Id { + self.state.lock().new_span(attrs) + } + + fn record(&self, id: &Id, values: &Record<'_>) { + self.state.lock().record(id, values) + } + + fn event(&self, event: &Event<'_>) { + self.state.lock().event(event) + } + + fn enter(&self, id: &Id) { + self.state.lock().enter(id) + } + + fn exit(&self, id: &Id) { + self.state.lock().exit(id) + } + + fn try_close(&self, id: Id) -> bool { + self.state.lock().try_close(id) + } + + fn record_follows_from(&self, _span: &Id, _follows: &Id) { + // no-op: we don't track follows-from relationships + } +} diff --git a/src/hyperlight_guest_tracing/src/visitor.rs b/src/hyperlight_guest_tracing/src/visitor.rs new file mode 100644 index 000000000..7955ca77e --- /dev/null +++ b/src/hyperlight_guest_tracing/src/visitor.rs @@ -0,0 +1,73 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ +extern crate alloc; + +use core::fmt::Debug; + +use heapless as hl; +use tracing_core::field::{Field, Visit}; + +/// Visitor implementation to collect fields into a vector of key-value pairs +pub(crate) struct FieldsVisitor<'a, const FK: usize, const FV: usize, const F: usize> { + pub out: &'a mut hl::Vec<(hl::String, hl::String), F>, +} + +impl Visit for FieldsVisitor<'_, FK, FV, F> { + /// Record a byte slice field + /// # Arguments + /// * `field` - The field metadata + /// * `value` - The byte slice value + /// NOTE: This implementation truncates the key and value if they exceed the allocated capacity + fn record_bytes(&mut self, field: &Field, value: &[u8]) { + let mut k = hl::String::::new(); + let mut val = hl::String::::new(); + // Shorten key and value if they are bigger than the space allocated + let _ = k.push_str(&field.name()[..usize::min(field.name().len(), k.capacity())]); + let _ = + val.push_str(&alloc::format!("{value:?}")[..usize::min(value.len(), val.capacity())]); + let _ = self.out.push((k, val)); + } + + /// Record a string field + /// # Arguments + /// * `f` - The field metadata + /// * `v` - The string value + /// NOTE: This implementation truncates the key and value if they exceed the allocated capacity + fn record_str(&mut self, f: &Field, v: &str) { + let mut k = heapless::String::::new(); + let mut val = heapless::String::::new(); + // Shorten key and value if they are bigger than the space allocated + let _ = k.push_str(&f.name()[..usize::min(f.name().len(), k.capacity())]); + let _ = val.push_str(&v[..usize::min(v.len(), val.capacity())]); + let _ = self.out.push((k, val)); + } + + /// Record a debug field + /// # Arguments + /// * `f` - The field metadata + /// * `v` - The debug value + /// NOTE: This implementation truncates the key and value if they exceed the allocated capacity + fn record_debug(&mut self, f: &Field, v: &dyn Debug) { + use heapless::String; + let mut k = String::::new(); + let mut val = String::::new(); + // Shorten key and value if they are bigger than the space allocated + let _ = k.push_str(&f.name()[..usize::min(f.name().len(), k.capacity())]); + let v = alloc::format!("{v:?}"); + let _ = val.push_str(&v[..usize::min(v.len(), val.capacity())]); + let _ = self.out.push((k, val)); + } +} diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index 3dfd9482b..1f4899c12 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -21,33 +21,39 @@ bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo- workspace = true [dependencies] -gdbstub = { version = "0.7.6", optional = true } +gdbstub = { version = "0.7.8", optional = true } gdbstub_arch = { version = "0.3.2", optional = true } -goblin = { version = "0.10" } +goblin = { version = "0.10", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] } rand = { version = "0.9" } -cfg-if = { version = "1.0.1" } -libc = { version = "0.2.174" } -flatbuffers = "25.2.10" +cfg-if = { version = "1.0.4" } +libc = { version = "0.2.177" } +flatbuffers = "25.9.23" +framehop = { version = "0.15.0", optional = true } +fallible-iterator = { version = "0.3.0", optional = true } +blake3 = "1.8.2" page_size = "0.6.0" termcolor = "1.2.0" -bitflags = "2.9.1" -log = "0.4.27" +bitflags = "2.10.0" +log = "0.4.28" +opentelemetry = { version = "0.31.0", optional = true } tracing = { version = "0.1.41", features = ["log"] } tracing-log = "0.2.0" tracing-core = "0.1.34" +tracing-opentelemetry = { version = "0.32.0", optional = true } hyperlight-common = { workspace = true, default-features = true, features = [ "std" ] } -vmm-sys-util = "0.14.0" -crossbeam = "0.8.0" +hyperlight-guest-tracing = { workspace = true, default-features = true, optional = true } +vmm-sys-util = "0.15.0" crossbeam-channel = "0.5.15" -thiserror = "2.0.12" +thiserror = "2.0.17" chrono = { version = "0.4", optional = true } anyhow = "1.0" metrics = "0.24.2" serde_json = "1.0" elfcore = "2.0" +uuid = { version = "1.18.1", features = ["v4"] } [target.'cfg(windows)'.dependencies] -windows = { version = "0.61", features = [ +windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_System_LibraryLoader", "Win32_System_Threading", @@ -61,52 +67,52 @@ windows = { version = "0.61", features = [ "Win32_System_JobObjects", "Win32_System_SystemServices", ] } -windows-sys = { version = "0.59", features = ["Win32"] } -windows-result = "0.3" -rust-embed = { version = "8.7.2", features = ["debug-embed", "include-exclude", "interpolate-folder-path"] } +windows-sys = { version = "0.61", features = ["Win32"] } +windows-result = "0.4" +rust-embed = { version = "8.8.0", features = ["debug-embed", "include-exclude", "interpolate-folder-path"] } sha256 = "1.6.0" windows-version = "0.1" lazy_static = "1.4.0" [target.'cfg(unix)'.dependencies] -seccompiler = { version = "0.5.0", optional = true } -kvm-bindings = { version = "0.13", features = ["fam-wrappers"], optional = true } -kvm-ioctls = { version = "0.23", optional = true } +kvm-bindings = { version = "0.14", features = ["fam-wrappers"], optional = true } +kvm-ioctls = { version = "0.24", optional = true } mshv-bindings2 = { package="mshv-bindings", version = "=0.2.1", optional = true } mshv-ioctls2 = { package="mshv-ioctls", version = "=0.2.1", optional = true} -mshv-bindings3 = { package="mshv-bindings", version = "=0.3.2", optional = true } -mshv-ioctls3 = { package="mshv-ioctls", version = "=0.3.2", optional = true} +mshv-bindings3 = { package="mshv-bindings", version = "0.6.1", optional = true } +mshv-ioctls3 = { package="mshv-ioctls", version = "0.6.1", optional = true} [dev-dependencies] -uuid = { version = "1.17.0", features = ["v4"] } -signal-hook-registry = "1.4.5" +uuid = { version = "1.18.1", features = ["v4"] } +signal-hook-registry = "1.4.6" +envy = { version = "0.4.2" } serde = "1.0" -proptest = "1.7.0" -tempfile = "3.20.0" +proptest = "1.8.0" +tempfile = "3.23.0" crossbeam-queue = "0.3.12" tracing-serde = "0.2.0" serial_test = "3.1.1" hyperlight-testing = { workspace = true } env_logger = "0.11.8" -tracing-forest = { version = "0.1.6", features = ["uuid", "chrono", "smallvec", "serde", "env-filter"] } +tracing-forest = { version = "0.2.0", features = ["uuid", "chrono", "smallvec", "serde", "env-filter"] } tracing = "0.1.41" -tracing-subscriber = {version = "0.3.19", features = ["std", "env-filter"]} -tracing-opentelemetry = "0.31.0" -opentelemetry = "0.30.0" -opentelemetry-otlp = { version = "0.30.0", features = ["default", "grpc-tonic"] } -opentelemetry-semantic-conventions = "0.30" -opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] } -tokio = { version = "1.45.1", features = ["full"] } -criterion = "0.6.0" +tracing-subscriber = {version = "0.3.20", features = ["std", "env-filter"]} +tracing-opentelemetry = "0.32.0" +opentelemetry = "0.31.0" +opentelemetry-otlp = { version = "0.31.0", default-features = false, features = ["http-proto", "reqwest-blocking-client", "grpc-tonic"] } +opentelemetry-semantic-conventions = "0.31" +opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] } +tokio = { version = "1.48.0", features = ["full"] } +criterion = "0.7.0" tracing-chrome = "0.7.2" metrics-util = "0.20.0" -metrics-exporter-prometheus = "0.17.2" +metrics-exporter-prometheus = { version = "0.17.2", default-features = false } tracing-tracy = "0.11.4" serde_json = "1.0" hyperlight-component-macro = { workspace = true } [target.'cfg(windows)'.dev-dependencies] -windows = { version = "0.61", features = [ +windows = { version = "0.62", features = [ "Win32_System_Diagnostics_ToolHelp", ] } @@ -114,20 +120,22 @@ windows = { version = "0.61", features = [ proc-maps = "0.4.0" [build-dependencies] -anyhow = { version = "1.0.98" } +anyhow = { version = "1.0.100" } cfg_aliases = "0.2.1" built = { version = "0.8.0", optional = true, features = ["chrono", "git2"] } [features] -default = ["kvm", "mshv2", "seccomp", "build-metadata", "init-paging"] -seccomp = ["dep:seccompiler"] +default = ["kvm", "mshv3", "build-metadata", "init-paging"] function_call_metrics = [] executable_heap = [] # This feature enables printing of debug information to stdout in debug builds print_debug = [] # Dumps the VM state to a file on unexpected errors or crashes. The path of the file will be printed on stdout and logged. crashdump = ["dep:chrono"] +trace_guest = ["dep:opentelemetry", "dep:tracing-opentelemetry", "dep:hyperlight-guest-tracing", "hyperlight-common/trace_guest"] +mem_profile = [ "trace_guest", "dep:framehop", "dep:fallible-iterator", "hyperlight-common/mem_profile" ] kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"] +# This feature is deprecated in favor of mshv3 mshv2 = ["dep:mshv-bindings2", "dep:mshv-ioctls2"] mshv3 = ["dep:mshv-bindings3", "dep:mshv-ioctls3"] # This enables easy debug in the guest diff --git a/src/hyperlight_host/benches/benchmarks.rs b/src/hyperlight_host/benches/benchmarks.rs index a0943a1ea..df9808494 100644 --- a/src/hyperlight_host/benches/benchmarks.rs +++ b/src/hyperlight_host/benches/benchmarks.rs @@ -14,14 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ +#![expect( + clippy::disallowed_macros, + reason = "This is a benchmark file, so using disallowed macros is fine here." +)] + +use std::sync::Mutex; + use criterion::{Criterion, criterion_group, criterion_main}; +use flatbuffers::FlatBufferBuilder; +use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType}; +use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnType}; +use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity; use hyperlight_host::GuestBinary; -use hyperlight_host::sandbox::{ - Callable, MultiUseSandbox, SandboxConfiguration, UninitializedSandbox, -}; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_testing::simple_guest_as_string; +use hyperlight_host::sandbox::{MultiUseSandbox, SandboxConfiguration, UninitializedSandbox}; +use hyperlight_testing::{c_simple_guest_as_string, simple_guest_as_string}; fn create_uninit_sandbox() -> UninitializedSandbox { let path = simple_guest_as_string().unwrap(); @@ -29,7 +36,7 @@ fn create_uninit_sandbox() -> UninitializedSandbox { } fn create_multiuse_sandbox() -> MultiUseSandbox { - create_uninit_sandbox().evolve(Noop::default()).unwrap() + create_uninit_sandbox().evolve().unwrap() } fn guest_call_benchmark(c: &mut Criterion) { @@ -38,24 +45,20 @@ fn guest_call_benchmark(c: &mut Criterion) { // Benchmarks a single guest function call. // The benchmark does **not** include the time to reset the sandbox memory after the call. group.bench_function("guest_call", |b| { - let mut call_ctx = create_multiuse_sandbox().new_call_context(); + let mut sbox = create_multiuse_sandbox(); - b.iter(|| { - call_ctx - .call::("Echo", "hello\n".to_string()) - .unwrap() - }); + b.iter(|| sbox.call::("Echo", "hello\n".to_string()).unwrap()); }); // Benchmarks a single guest function call. // The benchmark does include the time to reset the sandbox memory after the call. - group.bench_function("guest_call_with_reset", |b| { - let mut sandbox = create_multiuse_sandbox(); + group.bench_function("guest_call_with_restore", |b| { + let mut sbox = create_multiuse_sandbox(); + let snapshot = sbox.snapshot().unwrap(); b.iter(|| { - sandbox - .call_guest_function_by_name::("Echo", "hello\n".to_string()) - .unwrap() + sbox.call::("Echo", "hello\n".to_string()).unwrap(); + sbox.restore(&snapshot).unwrap(); }); }); @@ -69,11 +72,102 @@ fn guest_call_benchmark(c: &mut Criterion) { .register("HostAdd", |a: i32, b: i32| Ok(a + b)) .unwrap(); - let multiuse_sandbox: MultiUseSandbox = - uninitialized_sandbox.evolve(Noop::default()).unwrap(); - let mut call_ctx = multiuse_sandbox.new_call_context(); + let mut multiuse_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve().unwrap(); - b.iter(|| call_ctx.call::("Add", (1_i32, 41_i32)).unwrap()); + b.iter(|| { + multiuse_sandbox + .call::("Add", (1_i32, 41_i32)) + .unwrap() + }); + }); + + // same as guest_call, but the call will be made on different thread + group.bench_function("guest_call_on_different_thread", |b| { + use std::sync::{Arc, Barrier}; + use std::thread; + use std::time::Instant; + + b.iter_custom(|iters| { + let mut total_duration = std::time::Duration::ZERO; + let sbox = Arc::new(Mutex::new(create_multiuse_sandbox())); + + for _ in 0..iters { + // Ensure vcpu is "bound" on this main thread + { + let mut sbox = sbox.lock().unwrap(); + sbox.call::("Echo", "warmup\n".to_string()).unwrap(); + } + + let barrier = Arc::new(Barrier::new(2)); + let barrier_clone = Arc::clone(&barrier); + let sbox_clone = Arc::clone(&sbox); + + let handle = thread::spawn(move || { + barrier_clone.wait(); + + let mut sbox = sbox_clone.lock().unwrap(); + let start = Instant::now(); + // Measure the first call after thread switch + // According to KVM docs, this should show performance impact + sbox.call::("Echo", "hello\n".to_string()).unwrap(); + start.elapsed() + }); + + barrier.wait(); + + total_duration += handle.join().unwrap(); + } + + total_duration + }); + }); + + // Measure the time between calling interrupt_handle.kill() and the guest function returning. + group.bench_function("guest_call_time_to_interrupt", |b| { + use std::sync::{Arc, Barrier}; + use std::thread; + use std::time::Instant; + + b.iter_custom(|iters| { + let mut total_interrupt_latency = std::time::Duration::ZERO; + + for _ in 0..iters { + let mut sbox = create_multiuse_sandbox(); + let interrupt_handle = sbox.interrupt_handle(); + + let start_barrier = Arc::new(Barrier::new(2)); + let start_barrier_clone = Arc::clone(&start_barrier); + + let observer_thread = thread::spawn(move || { + start_barrier_clone.wait(); + + // Small delay to ensure the guest function is running in VM before interrupting + thread::sleep(std::time::Duration::from_millis(1)); + let kill_start = Instant::now(); + interrupt_handle.kill(); + kill_start + }); + + start_barrier.wait(); + + let result = sbox.call::("Spin", ()); + + let call_end = Instant::now(); + let kill_start = observer_thread.join().unwrap(); + + assert!( + matches!( + result, + Err(hyperlight_host::HyperlightError::ExecutionCanceledByHost()) + ), + "Guest function should be interrupted" + ); + + total_interrupt_latency += call_end.duration_since(kill_start); + } + + total_interrupt_latency + }); }); group.finish(); @@ -82,7 +176,7 @@ fn guest_call_benchmark(c: &mut Criterion) { fn guest_call_benchmark_large_param(c: &mut Criterion) { let mut group = c.benchmark_group("guest_functions_with_large_parameters"); #[cfg(target_os = "windows")] - group.sample_size(10); // This benchark is very slow on Windows, so we reduce the sample size to avoid long test runs. + group.sample_size(10); // This benchmark is very slow on Windows, so we reduce the sample size to avoid long test runs. // This benchmark includes time to first clone a vector and string, so it is not a "pure' benchmark of the guest call, but it's still useful group.bench_function("guest_call_with_large_parameters", |b| { @@ -99,14 +193,11 @@ fn guest_call_benchmark_large_param(c: &mut Criterion) { Some(config), ) .unwrap(); - let mut sandbox = sandbox.evolve(Noop::default()).unwrap(); + let mut sandbox = sandbox.evolve().unwrap(); b.iter(|| { sandbox - .call_guest_function_by_name::<()>( - "LargeParameters", - (large_vec.clone(), large_string.clone()), - ) + .call::<()>("LargeParameters", (large_vec.clone(), large_string.clone())) .unwrap() }); }); @@ -139,15 +230,85 @@ fn sandbox_benchmark(c: &mut Criterion) { b.iter(create_multiuse_sandbox); }); - // Benchmarks the time to create a new sandbox and create a new call context. - // Does **not** include the time to drop the sandbox or the call context. - group.bench_function("create_sandbox_and_call_context", |b| { - b.iter_with_large_drop(|| create_multiuse_sandbox().new_call_context()); + group.finish(); +} + +fn function_call_serialization_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("function_call_serialization"); + + let function_call = FunctionCall::new( + "TestFunction".to_string(), + Some(vec![ + ParameterValue::VecBytes(vec![1; 10 * 1024 * 1024]), + ParameterValue::String(String::from_utf8(vec![2; 10 * 1024 * 1024]).unwrap()), + ParameterValue::Int(42), + ParameterValue::UInt(100), + ParameterValue::Long(1000), + ParameterValue::ULong(2000), + ParameterValue::Float(521521.53), + ParameterValue::Double(432.53), + ParameterValue::Bool(true), + ParameterValue::VecBytes(vec![1; 10 * 1024 * 1024]), + ParameterValue::String(String::from_utf8(vec![2; 10 * 1024 * 1024]).unwrap()), + ]), + FunctionCallType::Guest, + ReturnType::Int, + ); + + group.bench_function("serialize_function_call", |b| { + b.iter(|| { + // We specifically want to include the time to estimate the capacity in this benchmark + let estimated_capacity = estimate_flatbuffer_capacity( + function_call.function_name.as_str(), + function_call.parameters.as_deref().unwrap_or(&[]), + ); + let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity); + let serialized: &[u8] = function_call.encode(&mut builder); + std::hint::black_box(serialized); + }); + }); + + group.bench_function("deserialize_function_call", |b| { + let mut builder = FlatBufferBuilder::new(); + let bytes = function_call.clone().encode(&mut builder); + + b.iter(|| { + let deserialized: FunctionCall = bytes.try_into().unwrap(); + std::hint::black_box(deserialized); + }); + }); + + group.finish(); +} + +fn sample_workloads_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("sample_workloads"); + + fn bench_24k_in_8k_out(b: &mut criterion::Bencher, guest_path: String) { + let mut cfg = SandboxConfiguration::default(); + cfg.set_input_data_size(25 * 1024); + + let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(guest_path), Some(cfg)) + .unwrap() + .evolve() + .unwrap(); + + b.iter_with_setup( + || vec![1; 24 * 1024], + |input| { + let ret: Vec = sandbox.call("24K_in_8K_out", (input,)).unwrap(); + assert_eq!(ret.len(), 8 * 1024, "Expected output length to be 8K"); + std::hint::black_box(ret); + }, + ); + } + + group.bench_function("24K_in_8K_out_c", |b| { + bench_24k_in_8k_out(b, c_simple_guest_as_string().unwrap()); }); - // Benchmarks the time to create a new sandbox, create a new call context, and drop the call context. - group.bench_function("create_sandbox_and_call_context_and_drop", |b| { - b.iter(|| create_multiuse_sandbox().new_call_context()); + group.bench_function("24K_in_8K_out_rust", |b| { + bench_24k_in_8k_out(b, simple_guest_as_string().unwrap()); }); group.finish(); @@ -156,6 +317,6 @@ fn sandbox_benchmark(c: &mut Criterion) { criterion_group! { name = benches; config = Criterion::default(); - targets = guest_call_benchmark, sandbox_benchmark, guest_call_benchmark_large_param + targets = guest_call_benchmark, sandbox_benchmark, guest_call_benchmark_large_param, function_call_serialization_benchmark, sample_workloads_benchmark } criterion_main!(benches); diff --git a/src/hyperlight_host/build.rs b/src/hyperlight_host/build.rs index ca2fd8bf2..6c90a485f 100644 --- a/src/hyperlight_host/build.rs +++ b/src/hyperlight_host/build.rs @@ -96,11 +96,11 @@ fn main() -> Result<()> { crashdump: { all(feature = "crashdump") }, // print_debug feature is aliased with debug_assertions to make it only available in debug-builds. print_debug: { all(feature = "print_debug", debug_assertions) }, - // the following features are mutually exclusive but rather than enforcing that here we are enabling mshv3 to override mshv2 when both are enabled - // because mshv2 is in the default feature set we want to allow users to enable mshv3 without having to set --no-default-features and the re-enable + // the following features are mutually exclusive but rather than enforcing that here we are enabling mshv2 to override mshv3 when both are enabled + // because mshv3 is in the default feature set we want to allow users to enable mshv2 without having to set --no-default-features and the re-enable // the other features they want. - mshv2: { all(feature = "mshv2", not(feature="mshv3"), target_os = "linux") }, - mshv3: { all(feature = "mshv3", target_os = "linux") }, + mshv2: { all(feature = "mshv2", target_os = "linux") }, + mshv3: { all(feature = "mshv3", not(feature="mshv2"), target_os = "linux") }, } #[cfg(feature = "build-metadata")] diff --git a/src/hyperlight_host/clippy.toml b/src/hyperlight_host/clippy.toml index cda217749..05fdbf5a2 100644 --- a/src/hyperlight_host/clippy.toml +++ b/src/hyperlight_host/clippy.toml @@ -2,6 +2,4 @@ disallowed-macros = [ { path = "std::assert", reason = "no asserts in release builds" }, { path = "std::assert_eq", reason = "no asserts in release builds" }, { path = "std::assert_ne", reason = "no asserts in release builds" }, - { path = "std::assert_true", reason = "no asserts in release builds" }, - { path = "std::assert_false", reason = "no asserts in release builds" }, ] diff --git a/src/hyperlight_host/examples/func_ctx/main.rs b/src/hyperlight_host/examples/func_ctx/main.rs index ce427a651..8aedf0983 100644 --- a/src/hyperlight_host/examples/func_ctx/main.rs +++ b/src/hyperlight_host/examples/func_ctx/main.rs @@ -14,42 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -use hyperlight_host::func::call_ctx::MultiUseGuestCallContext; -use hyperlight_host::sandbox::{Callable, MultiUseSandbox, UninitializedSandbox}; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, Result}; +use hyperlight_host::GuestBinary; +use hyperlight_host::sandbox::UninitializedSandbox; use hyperlight_testing::simple_guest_as_string; fn main() { // create a new `MultiUseSandbox` configured to run the `simpleguest.exe` // test guest binary - let sbox1: MultiUseSandbox = { - let path = simple_guest_as_string().unwrap(); - let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); - u_sbox.evolve(Noop::default()) - } - .unwrap(); - - // create a new call context from the sandbox, then do some calls with it. - let ctx1 = sbox1.new_call_context(); - let sbox2 = do_calls(ctx1).unwrap(); - // create a new call context from the returned sandbox, then do some calls - // with that one - let ctx2 = sbox2.new_call_context(); - do_calls(ctx2).unwrap(); -} - -/// Given a `MultiUseGuestCallContext` derived from an existing -/// `MultiUseSandbox` configured to run the `simpleguest.exe` test guest -/// binary, do several calls against that binary, print their results, then -/// call `ctx.finish()` and return the resulting `MultiUseSandbox`. Return an `Err` -/// if anything failed. -fn do_calls(mut ctx: MultiUseGuestCallContext) -> Result { - let res: String = ctx.call("Echo", "hello".to_string())?; + let path = simple_guest_as_string().unwrap(); + let mut sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None) + .unwrap() + .evolve() + .unwrap(); + + // Do several calls against a sandbox running the `simpleguest.exe` binary, + // and print their results + let res: String = sbox.call("Echo", "hello".to_string()).unwrap(); println!("got Echo res: {res}"); - let res: i32 = ctx.call("CallMalloc", 200_i32)?; + let res: i32 = sbox.call("CallMalloc", 200_i32).unwrap(); println!("got CallMalloc res: {res}"); - ctx.finish() } diff --git a/src/hyperlight_host/examples/guest-debugging/main.rs b/src/hyperlight_host/examples/guest-debugging/main.rs index 42b97598d..9d9ac230e 100644 --- a/src/hyperlight_host/examples/guest-debugging/main.rs +++ b/src/hyperlight_host/examples/guest-debugging/main.rs @@ -19,8 +19,6 @@ use std::thread; use hyperlight_host::sandbox::SandboxConfiguration; #[cfg(gdb)] use hyperlight_host::sandbox::config::DebugInfo; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; /// Build a sandbox configuration that enables GDB debugging when the `gdb` feature is enabled. @@ -41,14 +39,27 @@ fn get_sandbox_cfg() -> Option { fn main() -> hyperlight_host::Result<()> { let cfg = get_sandbox_cfg(); + // Create an uninitialized sandbox with a guest binary and debug enabled + let mut uninitialized_sandbox_dbg = UninitializedSandbox::new( + hyperlight_host::GuestBinary::FilePath( + hyperlight_testing::simple_guest_as_string().unwrap(), + ), + cfg, // sandbox configuration + )?; + // Create an uninitialized sandbox with a guest binary let mut uninitialized_sandbox = UninitializedSandbox::new( hyperlight_host::GuestBinary::FilePath( hyperlight_testing::simple_guest_as_string().unwrap(), ), - cfg, // sandbox configuration + None, // sandbox configuration )?; + // Register a host functions + uninitialized_sandbox_dbg.register("Sleep5Secs", || { + thread::sleep(std::time::Duration::from_secs(5)); + Ok(()) + })?; // Register a host functions uninitialized_sandbox.register("Sleep5Secs", || { thread::sleep(std::time::Duration::from_secs(5)); @@ -56,13 +67,28 @@ fn main() -> hyperlight_host::Result<()> { })?; // Note: This function is unused, it's just here for demonstration purposes - // Initialize sandbox to be able to call host functions - let mut multi_use_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve(Noop::default())?; + // Initialize sandboxes to be able to call host functions + let mut multi_use_sandbox_dbg: MultiUseSandbox = uninitialized_sandbox_dbg.evolve()?; + let mut multi_use_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve()?; // Call guest function - let message = "Hello, World! I am executing inside of a VM :)\n".to_string(); + multi_use_sandbox_dbg + .call::<()>("UseSSE2Registers", ()) + .unwrap(); + + let message = + "Hello, World! I am executing inside of a VM with debugger attached :)\n".to_string(); + multi_use_sandbox_dbg + .call::( + "PrintOutput", // function must be defined in the guest binary + message.clone(), + ) + .unwrap(); + + let message = + "Hello, World! I am executing inside of a VM without debugger attached :)\n".to_string(); multi_use_sandbox - .call_guest_function_by_name::( + .call::( "PrintOutput", // function must be defined in the guest binary message.clone(), ) @@ -81,54 +107,52 @@ mod tests { use hyperlight_host::{Result, new_error}; use io::{BufReader, BufWriter, Read, Write}; + use serial_test::serial; use super::*; - fn write_cmds_file(cmd_file_path: &str, out_file_path: &str) -> io::Result<()> { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get manifest dir"); + #[cfg(not(windows))] + const GDB_COMMAND: &str = "rust-gdb"; + #[cfg(windows)] + const GDB_COMMAND: &str = "gdb"; + + fn write_cmds_file(cmd_file_path: &str, cmd: &str) -> io::Result<()> { let file = File::create(cmd_file_path)?; let mut writer = BufWriter::new(file); // write from string to file - writer.write_all( - format!( - "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest - target remote :8080 - - set pagination off - set logging file {out_file_path} - set logging on - - break hyperlight_main - commands - echo \"Stopped at hyperlight_main breakpoint\\n\" - backtrace - continue - end - - continue - - set logging off - quit - " - ) - .as_bytes(), - )?; + writer.write_all(cmd.as_bytes())?; writer.flush() } - fn run_guest_and_gdb(cmd_file_path: &str, out_file_path: &str) -> Result<()> { + fn run_guest_and_gdb( + cmd_file_path: &str, + out_file_path: &str, + cmd: &str, + checker: fn(String) -> bool, + ) -> Result<()> { // write gdb commands to file - write_cmds_file(&cmd_file_path, &out_file_path) - .expect("Failed to write gdb commands to file"); + write_cmds_file(&cmd_file_path, cmd).expect("Failed to write gdb commands to file"); - #[cfg(mshv3)] - let features = "gdb,mshv3"; - #[cfg(not(mshv3))] + #[cfg(mshv2)] // mshv3 is a default feature is mutually exclusive with the mshv2 feature + let features = "gdb,mshv2"; + #[cfg(not(mshv2))] let features = "gdb"; + // build it before running to avoid a race condition below + let mut guest_child = Command::new("cargo") + .arg("build") + .arg("--example") + .arg("guest-debugging") + .arg("--features") + .arg(features) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .status() + .map_err(|e| new_error!("Failed to build guest process: {}", e))?; + let mut guest_child = Command::new("cargo") .arg("run") .arg("--example") @@ -143,7 +167,7 @@ mod tests { // wait 3 seconds for the gdb to connect thread::sleep(Duration::from_secs(3)); - let mut gdb = Command::new("rust-gdb") + let mut gdb = Command::new(GDB_COMMAND) .arg("--nw") .arg("--batch") .arg("-x") @@ -193,17 +217,17 @@ mod tests { } } - check_output(&out_file_path) + check_output(&out_file_path, checker) } - fn check_output(out_file_path: &str) -> Result<()> { + fn check_output(out_file_path: &str, checker: fn(contents: String) -> bool) -> Result<()> { let results = File::open(out_file_path) .map_err(|e| new_error!("Failed to open gdb.output file: {}", e))?; let mut reader = BufReader::new(results); let mut contents = String::new(); reader.read_to_string(&mut contents).unwrap(); - if contents.contains("Stopped at hyperlight_main breakpoint") { + if checker(contents) { Ok(()) } else { Err(new_error!( @@ -225,12 +249,95 @@ mod tests { } #[test] + #[serial] fn test_gdb_end_to_end() { let out_dir = std::env::var("OUT_DIR").expect("Failed to get out dir"); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("Failed to get manifest dir") + .replace('\\', "/"); let out_file_path = format!("{out_dir}/gdb.output"); let cmd_file_path = format!("{out_dir}/gdb-commands.txt"); - let result = run_guest_and_gdb(&cmd_file_path, &out_file_path); + let cmd = format!( + "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + + set pagination off + set logging file {out_file_path} + set logging on + + break hyperlight_main + commands + echo \"Stopped at hyperlight_main breakpoint\\n\" + backtrace + + continue + end + + continue + + set logging off + quit + " + ); + + #[cfg(windows)] + let cmd = format!("set osabi none\n{}", cmd); + + let checker = |contents: String| contents.contains("Stopped at hyperlight_main breakpoint"); + + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + + // cleanup + let cleanup_result = cleanup(&out_file_path, &cmd_file_path); + assert!(cleanup_result.is_ok(), "{}", cleanup_result.unwrap_err()); + // check if the test passed - done at the end to ensure cleanup is done + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + + #[test] + #[serial] + fn test_gdb_sse_check() { + let out_dir = std::env::var("OUT_DIR").expect("Failed to get out dir"); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("Failed to get manifest dir") + .replace('\\', "/"); + println!("manifest dir {manifest_dir}"); + let out_file_path = format!("{out_dir}/gdb-sse.output"); + let cmd_file_path = format!("{out_dir}/gdb-sse--commands.txt"); + + let cmd = format!( + "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + + set pagination off + set logging file {out_file_path} + set logging on + + break main.rs:simpleguest::use_sse2_registers + commands 1 + print $xmm1.v4_float + break +2 + commands 2 + print $xmm1.v4_float + continue + end + continue + end + + + continue + + set logging off + quit + " + ); + + #[cfg(windows)] + let cmd = format!("set osabi none\n{}", cmd); + + let checker = |contents: String| contents.contains("$2 = [1.20000005, 0, 0, 0]"); + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); // cleanup let cleanup_result = cleanup(&out_file_path, &cmd_file_path); diff --git a/src/hyperlight_host/examples/hello-world/main.rs b/src/hyperlight_host/examples/hello-world/main.rs index 57c0b4398..4a403515b 100644 --- a/src/hyperlight_host/examples/hello-world/main.rs +++ b/src/hyperlight_host/examples/hello-world/main.rs @@ -16,8 +16,6 @@ limitations under the License. #![allow(clippy::disallowed_macros)] use std::thread; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{MultiUseSandbox, UninitializedSandbox}; fn main() -> hyperlight_host::Result<()> { @@ -37,12 +35,12 @@ fn main() -> hyperlight_host::Result<()> { // Note: This function is unused, it's just here for demonstration purposes // Initialize sandbox to be able to call host functions - let mut multi_use_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve(Noop::default())?; + let mut multi_use_sandbox: MultiUseSandbox = uninitialized_sandbox.evolve()?; // Call guest function let message = "Hello, World! I am executing inside of a VM :)\n".to_string(); multi_use_sandbox - .call_guest_function_by_name::( + .call::( "PrintOutput", // function must be defined in the guest binary message, ) diff --git a/src/hyperlight_host/examples/logging/main.rs b/src/hyperlight_host/examples/logging/main.rs index ea6e18b31..23a642886 100644 --- a/src/hyperlight_host/examples/logging/main.rs +++ b/src/hyperlight_host/examples/logging/main.rs @@ -18,11 +18,8 @@ extern crate hyperlight_host; use std::sync::{Arc, Barrier}; -use hyperlight_host::sandbox::Callable; use hyperlight_host::sandbox::uninitialized::UninitializedSandbox; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result}; +use hyperlight_host::{GuestBinary, Result}; use hyperlight_testing::simple_guest_as_string; fn fn_writer(_msg: String) -> Result { @@ -48,15 +45,12 @@ fn main() -> Result<()> { usandbox.register_print(fn_writer)?; // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op)?; + let mut multiuse_sandbox = usandbox.evolve()?; // Call a guest function 5 times to generate some log entries. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("Echo", "a".to_string()) + .call::("Echo", "a".to_string()) .unwrap(); } @@ -67,7 +61,7 @@ fn main() -> Result<()> { // Call a guest function that calls the HostPrint host function 5 times to generate some log entries. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("PrintOutput", msg.clone()) + .call::("PrintOutput", msg.clone()) .unwrap(); } Ok(()) @@ -81,10 +75,7 @@ fn main() -> Result<()> { UninitializedSandbox::new(GuestBinary::FilePath(hyperlight_guest_path.clone()), None)?; // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op)?; + let mut multiuse_sandbox = usandbox.evolve()?; let interrupt_handle = multiuse_sandbox.interrupt_handle(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); @@ -102,10 +93,8 @@ fn main() -> Result<()> { // Call a function that gets cancelled by the host function 5 times to generate some log entries. for _ in 0..NUM_CALLS { - let mut ctx = multiuse_sandbox.new_call_context(); barrier.wait(); - ctx.call::<()>("Spin", ()).unwrap_err(); - multiuse_sandbox = ctx.finish().unwrap(); + multiuse_sandbox.call::<()>("Spin", ()).unwrap_err(); } thread.join().unwrap(); diff --git a/src/hyperlight_host/examples/metrics/main.rs b/src/hyperlight_host/examples/metrics/main.rs index 5b7cc103f..3401a3140 100644 --- a/src/hyperlight_host/examples/metrics/main.rs +++ b/src/hyperlight_host/examples/metrics/main.rs @@ -18,11 +18,8 @@ extern crate hyperlight_host; use std::sync::{Arc, Barrier}; use std::thread::{JoinHandle, spawn}; -use hyperlight_host::sandbox::Callable; use hyperlight_host::sandbox::uninitialized::UninitializedSandbox; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result}; +use hyperlight_host::{GuestBinary, Result}; use hyperlight_testing::simple_guest_as_string; // Run this rust example with the flag --features "function_call_metrics" to enable more metrics to be emitted @@ -59,15 +56,12 @@ fn do_hyperlight_stuff() { usandbox.register_print(fn_writer)?; // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op).expect("Failed to evolve sandbox"); + let mut multiuse_sandbox = usandbox.evolve().expect("Failed to evolve sandbox"); // Call a guest function 5 times to generate some metrics. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("Echo", "a".to_string()) + .call::("Echo", "a".to_string()) .unwrap(); } @@ -78,7 +72,7 @@ fn do_hyperlight_stuff() { // Call a guest function that calls the HostPrint host function 5 times to generate some metrics. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("PrintOutput", msg.clone()) + .call::("PrintOutput", msg.clone()) .unwrap(); } Ok(()) @@ -93,10 +87,7 @@ fn do_hyperlight_stuff() { .expect("Failed to create UninitializedSandbox"); // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op).expect("Failed to evolve sandbox"); + let mut multiuse_sandbox = usandbox.evolve().expect("Failed to evolve sandbox"); let interrupt_handle = multiuse_sandbox.interrupt_handle(); const NUM_CALLS: i32 = 5; @@ -116,10 +107,8 @@ fn do_hyperlight_stuff() { // Call a function that gets cancelled by the host function 5 times to generate some metrics. for _ in 0..NUM_CALLS { - let mut ctx = multiuse_sandbox.new_call_context(); barrier.wait(); - ctx.call::<()>("Spin", ()).unwrap_err(); - multiuse_sandbox = ctx.finish().unwrap(); + multiuse_sandbox.call::<()>("Spin", ()).unwrap_err(); } for join_handle in join_handles { diff --git a/src/hyperlight_host/examples/tracing-chrome/main.rs b/src/hyperlight_host/examples/tracing-chrome/main.rs index a93e03fab..7c4001106 100644 --- a/src/hyperlight_host/examples/tracing-chrome/main.rs +++ b/src/hyperlight_host/examples/tracing-chrome/main.rs @@ -15,9 +15,7 @@ limitations under the License. */ #![allow(clippy::disallowed_macros)] use hyperlight_host::sandbox::uninitialized::UninitializedSandbox; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result}; +use hyperlight_host::{GuestBinary, Result}; use hyperlight_testing::simple_guest_as_string; use tracing_chrome::ChromeLayerBuilder; use tracing_subscriber::prelude::*; @@ -34,13 +32,11 @@ fn main() -> Result<()> { // Create a new sandbox. let usandbox = UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_path), None)?; - let mut sbox = usandbox - .evolve(Noop::::default()) - .unwrap(); + let mut sbox = usandbox.evolve().unwrap(); // do the function call let current_time = std::time::Instant::now(); - let res: String = sbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?; + let res: String = sbox.call("Echo", "Hello, World!".to_string())?; let elapsed = current_time.elapsed(); println!("Function call finished in {:?}.", elapsed); assert_eq!(res, "Hello, World!"); diff --git a/src/hyperlight_host/examples/tracing-otlp/main.rs b/src/hyperlight_host/examples/tracing-otlp/main.rs index ab8196972..db5c9baf4 100644 --- a/src/hyperlight_host/examples/tracing-otlp/main.rs +++ b/src/hyperlight_host/examples/tracing-otlp/main.rs @@ -26,22 +26,19 @@ use std::io::stdin; use std::sync::{Arc, Barrier, Mutex}; use std::thread::{JoinHandle, spawn}; -use hyperlight_host::sandbox::Callable; use hyperlight_host::sandbox::uninitialized::UninitializedSandbox; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result as HyperlightResult}; +use hyperlight_host::{GuestBinary, Result as HyperlightResult}; use hyperlight_testing::simple_guest_as_string; use opentelemetry::trace::TracerProvider; use opentelemetry::{KeyValue, global}; -use opentelemetry_otlp::{SpanExporter, WithExportConfig}; +use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig}; //use opentelemetry_sdk::runtime::Tokio; use opentelemetry_sdk::Resource; use opentelemetry_semantic_conventions::attribute::SERVICE_VERSION; use tracing_subscriber::EnvFilter; use uuid::Uuid; -const ENDPOINT_ADDR: &str = "http://localhost:4317/"; +const ENDPOINT_ADDR: &str = "http://localhost:4318/v1/traces"; fn fn_writer(_msg: String) -> HyperlightResult { Ok(0) @@ -64,7 +61,8 @@ fn init_tracing_subscriber( addr: &str, ) -> Result> { let exporter = SpanExporter::builder() - .with_tonic() + .with_http() + .with_protocol(Protocol::HttpBinary) .with_endpoint(addr) .build()?; @@ -133,15 +131,12 @@ fn run_example(wait_input: bool) -> HyperlightResult<()> { usandbox.register_print(fn_writer)?; // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op)?; + let mut multiuse_sandbox = usandbox.evolve()?; // Call a guest function 5 times to generate some log entries. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("Echo", "a".to_string()) + .call::("Echo", "a".to_string()) .unwrap(); } @@ -152,7 +147,7 @@ fn run_example(wait_input: bool) -> HyperlightResult<()> { // Call a guest function that calls the HostPrint host function 5 times to generate some log entries. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("PrintOutput", msg.clone()) + .call::("PrintOutput", msg.clone()) .unwrap(); } @@ -183,10 +178,8 @@ fn run_example(wait_input: bool) -> HyperlightResult<()> { uuid = %id, ); let _entered = span.enter(); - let mut ctx = multiuse_sandbox.new_call_context(); barrier.wait(); - ctx.call::<()>("Spin", ()).unwrap_err(); - multiuse_sandbox = ctx.finish().unwrap(); + multiuse_sandbox.call::<()>("Spin", ()).unwrap_err(); } thread.join().expect("Thread panicked"); } @@ -218,7 +211,7 @@ mod test { use super::*; - const TESTER_ADDR: &str = "127.0.0.1:4317"; + const TESTER_ADDR: &str = "127.0.0.1:4318"; async fn handle(mut stream: TcpStream) -> Result<()> { let mut buf = Vec::with_capacity(128); diff --git a/src/hyperlight_host/examples/tracing-tracy/main.rs b/src/hyperlight_host/examples/tracing-tracy/main.rs index 615b59c38..1c3912ebf 100644 --- a/src/hyperlight_host/examples/tracing-tracy/main.rs +++ b/src/hyperlight_host/examples/tracing-tracy/main.rs @@ -15,9 +15,7 @@ limitations under the License. */ #![allow(clippy::disallowed_macros)] use hyperlight_host::sandbox::uninitialized::UninitializedSandbox; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result}; +use hyperlight_host::{GuestBinary, Result}; use hyperlight_testing::simple_guest_as_string; use tracing_subscriber::EnvFilter; use tracing_subscriber::layer::SubscriberExt; @@ -40,13 +38,11 @@ fn main() -> Result<()> { // Create a new sandbox. let usandbox = UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_path), None)?; - let mut sbox = usandbox - .evolve(Noop::::default()) - .unwrap(); + let mut sbox = usandbox.evolve().unwrap(); // do the function call let current_time = std::time::Instant::now(); - let res: String = sbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?; + let res: String = sbox.call("Echo", "Hello, World!".to_string())?; let elapsed = current_time.elapsed(); println!("Function call finished in {:?}.", elapsed); assert_eq!(res, "Hello, World!"); diff --git a/src/hyperlight_host/examples/tracing/main.rs b/src/hyperlight_host/examples/tracing/main.rs index dfc680576..fce5215e4 100644 --- a/src/hyperlight_host/examples/tracing/main.rs +++ b/src/hyperlight_host/examples/tracing/main.rs @@ -19,11 +19,8 @@ extern crate hyperlight_host; use std::sync::{Arc, Barrier}; use std::thread::{JoinHandle, spawn}; -use hyperlight_host::sandbox::Callable; use hyperlight_host::sandbox::uninitialized::UninitializedSandbox; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; -use hyperlight_host::{GuestBinary, MultiUseSandbox, Result}; +use hyperlight_host::{GuestBinary, Result}; use hyperlight_testing::simple_guest_as_string; use tracing_forest::ForestLayer; use tracing_subscriber::layer::SubscriberExt; @@ -76,15 +73,12 @@ fn run_example() -> Result<()> { usandbox.register_print(fn_writer)?; // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op)?; + let mut multiuse_sandbox = usandbox.evolve()?; // Call a guest function 5 times to generate some log entries. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("Echo", "a".to_string()) + .call::("Echo", "a".to_string()) .unwrap(); } @@ -95,7 +89,7 @@ fn run_example() -> Result<()> { // Call a guest function that calls the HostPrint host function 5 times to generate some log entries. for _ in 0..5 { multiuse_sandbox - .call_guest_function_by_name::("PrintOutput", msg.clone()) + .call::("PrintOutput", msg.clone()) .unwrap(); } Ok(()) @@ -108,10 +102,7 @@ fn run_example() -> Result<()> { UninitializedSandbox::new(GuestBinary::FilePath(hyperlight_guest_path.clone()), None)?; // Initialize the sandbox. - - let no_op = Noop::::default(); - - let mut multiuse_sandbox = usandbox.evolve(no_op)?; + let mut multiuse_sandbox = usandbox.evolve()?; let interrupt_handle = multiuse_sandbox.interrupt_handle(); // Call a function that gets cancelled by the host function 5 times to generate some log entries. @@ -139,10 +130,8 @@ fn run_example() -> Result<()> { uuid = %id, ); let _entered = span.enter(); - let mut ctx = multiuse_sandbox.new_call_context(); barrier.wait(); - ctx.call::<()>("Spin", ()).unwrap_err(); - multiuse_sandbox = ctx.finish().unwrap(); + multiuse_sandbox.call::<()>("Spin", ()).unwrap_err(); } for join_handle in join_handles { diff --git a/src/hyperlight_host/scripts/dump_all_sandboxes.gdb b/src/hyperlight_host/scripts/dump_all_sandboxes.gdb new file mode 100644 index 000000000..f2ef79c5c --- /dev/null +++ b/src/hyperlight_host/scripts/dump_all_sandboxes.gdb @@ -0,0 +1,41 @@ +define dump_all_sandboxes + set pagination off + + # Get the total number of threads + info threads + + # Loop through all threads (adjust max if you have more than 200 threads) + set $thread_num = 2 + while $thread_num <= 200 + # Try to switch to this thread + thread $thread_num + + # Check if thread switch succeeded (GDB sets $_thread to current thread) + if $_thread == $thread_num + echo \n=== Thread + p $thread_num + echo ===\n + + # Go to frame 15 + frame 15 + + + set $sb = &sandbox + call sandbox.generate_crashdump() + + set $thread_num = $thread_num + 1 + else + # No more threads, exit loop + set $thread_num = 201 + end + end + + echo \nDone dumping all sandboxes\n + set pagination on +end + +document dump_all_sandboxes +Dump crashdumps for sandboxes on all threads (except thread 1). +Assumes sandbox is in frame 15 on each thread. +Usage: dump_all_sandboxes +end \ No newline at end of file diff --git a/src/hyperlight_host/src/error.rs b/src/hyperlight_host/src/error.rs index df8ee8a37..99e46b7c9 100644 --- a/src/hyperlight_host/src/error.rs +++ b/src/hyperlight_host/src/error.rs @@ -69,11 +69,6 @@ pub enum HyperlightError { #[error("Error converting CString {0:?}")] CStringConversionError(#[from] std::ffi::NulError), - /// A disallowed syscall was caught - #[error("Seccomp filter trapped on disallowed syscall (check STDERR for offending syscall)")] - #[cfg(all(feature = "seccomp", target_os = "linux"))] - DisallowedSyscall, - /// A generic error with a message #[error("{0}")] Error(String), @@ -138,6 +133,11 @@ pub enum HyperlightError { #[error("Conversion of str data to json failed")] JsonConversionFailure(#[from] serde_json::Error), + /// KVM Error Occurred + #[error("KVM Error {0:?}")] + #[cfg(kvm)] + KVMError(#[from] kvm_ioctls::Error), + /// An attempt to get a lock from a Mutex failed. #[error("Unable to lock resource")] LockAttemptFailed(String), @@ -211,15 +211,9 @@ pub enum HyperlightError { #[error("Stack overflow detected")] StackOverflow(), - /// a backend error occurred with seccomp filters - #[error("Backend Error with Seccomp Filter {0:?}")] - #[cfg(all(feature = "seccomp", target_os = "linux"))] - SeccompFilterBackendError(#[from] seccompiler::BackendError), - - /// an error occurred with seccomp filters - #[error("Error with Seccomp Filter {0:?}")] - #[cfg(all(feature = "seccomp", target_os = "linux"))] - SeccompFilterError(#[from] seccompiler::Error), + /// Tried to restore snapshot to a sandbox that is not the same as the one the snapshot was taken from + #[error("Snapshot was taken from a different sandbox")] + SnapshotSandboxMismatch, /// SystemTimeError #[error("SystemTimeError {0:?}")] @@ -259,7 +253,7 @@ pub enum HyperlightError { /// vmm sys Error Occurred #[error("vmm sys Error {0:?}")] #[cfg(target_os = "linux")] - VmmSysError(#[from] vmm_sys_util::errno::Error), + VmmSysError(vmm_sys_util::errno::Error), /// Windows Error #[cfg(target_os = "windows")] diff --git a/src/hyperlight_host/src/func/call_ctx.rs b/src/hyperlight_host/src/func/call_ctx.rs deleted file mode 100644 index 180731910..000000000 --- a/src/hyperlight_host/src/func/call_ctx.rs +++ /dev/null @@ -1,254 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use tracing::{Span, instrument}; - -use super::{ParameterTuple, SupportedReturnType}; -use crate::sandbox::Callable; -use crate::{MultiUseSandbox, Result}; -/// A context for calling guest functions. -/// -/// Takes ownership of an existing `MultiUseSandbox`. -/// Once created, guest function calls may be made through this and only this context -/// until it is converted back to the `MultiUseSandbox` from which it originated. -/// -/// Upon this conversion,the memory associated with the `MultiUseSandbox` it owns will be reset to the state it was in before -/// this context was created. -/// -/// Calls made through this context will cause state to be retained across calls, until such time as the context -/// is converted back to a `MultiUseSandbox` -/// -/// If dropped, the `MultiUseSandbox` from which it came will be also be dropped as it is owned by the -/// `MultiUseGuestCallContext` until it is converted back to a `MultiUseSandbox` -/// -#[derive(Debug)] -pub struct MultiUseGuestCallContext { - sbox: MultiUseSandbox, -} - -impl MultiUseGuestCallContext { - /// Take ownership of a `MultiUseSandbox` and - /// return a new `MultiUseGuestCallContext` instance. - /// - #[instrument(skip_all, parent = Span::current())] - pub fn start(sbox: MultiUseSandbox) -> Self { - Self { sbox } - } - - /// Close out the context and get back the internally-stored - /// `MultiUseSandbox`. Future contexts opened by the returned sandbox - /// will have guest state restored. - #[instrument(err(Debug), skip(self), parent = Span::current())] - pub fn finish(mut self) -> Result { - self.sbox.restore_state()?; - Ok(self.sbox) - } - /// Close out the context and get back the internally-stored - /// `MultiUseSandbox`. - /// - /// Note that this method is pub(crate) and does not reset the state of the - /// sandbox. - /// - /// It is intended to be used when evolving a MultiUseSandbox to a new state - /// and is not intended to be called publicly. It allows the state of the guest to be altered - /// during the evolution of one sandbox state to another, enabling the new state created - /// to be captured and stored in the Sandboxes state stack. - /// - pub(crate) fn finish_no_reset(self) -> MultiUseSandbox { - self.sbox - } -} - -impl Callable for MultiUseGuestCallContext { - /// Call the guest function called `func_name` with the given arguments - /// `args`, and expect the return value have the same type as - /// `func_ret_type`. - /// - /// Every call to a guest function through this method will be made with the same "context" - /// meaning that the guest state resulting from any previous call will be present/osbservable - /// by the guest function called. - /// - /// If you want to reset state, call `finish()` on this `MultiUseGuestCallContext` - /// and get a new one from the resulting `MultiUseSandbox` - #[instrument(err(Debug),skip(self, args),parent = Span::current())] - fn call( - &mut self, - func_name: &str, - args: impl ParameterTuple, - ) -> Result { - // we are guaranteed to be holding a lock now, since `self` can't - // exist without doing so. Since GuestCallContext is effectively - // !Send (and !Sync), we also don't need to worry about - // synchronization - - let ret = self.sbox.call_guest_function_by_name_no_reset( - func_name, - Output::TYPE, - args.into_value(), - ); - Output::from_value(ret?) - } -} - -#[cfg(test)] -mod tests { - use std::sync::mpsc::sync_channel; - use std::thread::{self, JoinHandle}; - - use hyperlight_testing::simple_guest_as_string; - - use super::MultiUseGuestCallContext; - use crate::sandbox::Callable; - use crate::sandbox_state::sandbox::EvolvableSandbox; - use crate::sandbox_state::transition::Noop; - use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox}; - - fn new_uninit() -> Result { - let path = simple_guest_as_string().map_err(|e| { - HyperlightError::Error(format!("failed to get simple guest path ({e:?})")) - })?; - UninitializedSandbox::new(GuestBinary::FilePath(path), None) - } - - /// Test to create a `MultiUseSandbox`, then call several guest functions - /// on it across different threads. - /// - /// This test works by passing messages between threads using Rust's - /// [mpsc crate](https://doc.rust-lang.org/std/sync/mpsc). Details of this - /// interaction are as follows. - /// - /// One thread acts as the receiver (AKA: consumer) and owns the - /// `MultiUseSandbox`. This receiver fields requests from N senders - /// (AKA: producers) to make batches of calls. - /// - /// Upon receipt of a message to execute a batch, a new - /// `MultiUseGuestCallContext` is created in the receiver thread from the - /// existing `MultiUseSandbox`, and the batch is executed. - /// - /// After the batch is complete, the `MultiUseGuestCallContext` is done - /// and it is converted back to the underlying `MultiUseSandbox` - #[test] - fn test_multi_call_multi_thread() { - let (snd, recv) = sync_channel::>(0); - - // create new receiver thread and on it, begin listening for - // requests to execute batches of calls - let recv_hdl = thread::spawn(move || { - let mut sbox: MultiUseSandbox = new_uninit().unwrap().evolve(Noop::default()).unwrap(); - while let Ok(calls) = recv.recv() { - let mut ctx = sbox.new_call_context(); - for call in calls { - call.call(&mut ctx); - } - sbox = ctx.finish().unwrap(); - } - }); - - // create new sender threads - let send_handles: Vec> = (0..10) - .map(|i| { - let sender = snd.clone(); - thread::spawn(move || { - let calls = vec![ - TestFuncCall::new(move |ctx| { - let msg = format!("Hello {}", i); - let ret: String = ctx.call("Echo", msg.clone()).unwrap(); - assert_eq!(ret, msg) - }), - TestFuncCall::new(move |ctx| { - let ret: i32 = ctx.call("CallMalloc", i + 2).unwrap(); - assert_eq!(ret, i + 2) - }), - ]; - sender.send(calls).unwrap(); - }) - }) - .collect(); - - for hdl in send_handles { - hdl.join().unwrap(); - } - // after all sender threads are done, drop the sender itself - // so the receiver thread can exit. then, ensure the receiver - // thread has exited. - drop(snd); - recv_hdl.join().unwrap(); - } - - pub struct TestSandbox { - sandbox: MultiUseSandbox, - } - - impl TestSandbox { - pub fn new() -> Self { - let sbox: MultiUseSandbox = new_uninit().unwrap().evolve(Noop::default()).unwrap(); - Self { sandbox: sbox } - } - pub fn call_add_to_static_multiple_times(mut self, i: i32) -> Result { - let mut ctx = self.sandbox.new_call_context(); - let mut sum: i32 = 0; - for n in 0..i { - let result = ctx.call::("AddToStatic", n); - sum += n; - println!("{:?}", result); - let result = result.unwrap(); - assert_eq!(result, sum); - } - let result = ctx.finish(); - assert!(result.is_ok()); - self.sandbox = result.unwrap(); - Ok(self) - } - - pub fn call_add_to_static(mut self, i: i32) -> Result<()> { - for n in 0..i { - let result = self - .sandbox - .call_guest_function_by_name::("AddToStatic", n); - println!("{:?}", result); - let result = result.unwrap(); - assert_eq!(result, n); - } - Ok(()) - } - } - - #[test] - fn ensure_multiusesandbox_multi_calls_dont_reset_state() { - let sandbox = TestSandbox::new(); - let result = sandbox.call_add_to_static_multiple_times(5); - assert!(result.is_ok()); - } - - #[test] - fn ensure_multiusesandbox_single_calls_do_reset_state() { - let sandbox = TestSandbox::new(); - let result = sandbox.call_add_to_static(5); - assert!(result.is_ok()); - } - - struct TestFuncCall(Box); - - impl TestFuncCall { - fn new(f: impl FnOnce(&mut MultiUseGuestCallContext) + Send + 'static) -> Self { - TestFuncCall(Box::new(f)) - } - - fn call(self, ctx: &mut MultiUseGuestCallContext) { - (self.0)(ctx); - } - } -} diff --git a/src/hyperlight_host/src/func/guest_err.rs b/src/hyperlight_host/src/func/guest_err.rs deleted file mode 100644 index af3a0bbe3..000000000 --- a/src/hyperlight_host/src/func/guest_err.rs +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; - -use crate::error::HyperlightError::{GuestError, StackOverflow}; -use crate::mem::shared_mem::HostSharedMemory; -use crate::metrics::{METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE}; -use crate::sandbox::mem_mgr::MemMgrWrapper; -use crate::{Result, log_then_return}; - -/// Check for a guest error and return an `Err` if one was found, -/// and `Ok` if one was not found. -pub(crate) fn check_for_guest_error(mgr: &mut MemMgrWrapper) -> Result<()> { - let guest_err = mgr.as_mut().get_guest_error().ok(); - let Some(guest_err) = guest_err else { - return Ok(()); - }; - - metrics::counter!( - METRIC_GUEST_ERROR, - METRIC_GUEST_ERROR_LABEL_CODE => (guest_err.code as u64).to_string() - ) - .increment(1); - - match guest_err.code { - ErrorCode::NoError => Ok(()), - ErrorCode::StackOverflow => { - log_then_return!(StackOverflow()); - } - _ => { - log_then_return!(GuestError(guest_err.code, guest_err.message.clone())); - } - } -} diff --git a/src/hyperlight_host/src/func/host_functions.rs b/src/hyperlight_host/src/func/host_functions.rs index 3944b0703..6764a07c2 100644 --- a/src/hyperlight_host/src/func/host_functions.rs +++ b/src/hyperlight_host/src/func/host_functions.rs @@ -20,8 +20,8 @@ use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, Ret use super::utils::for_each_tuple; use super::{ParameterTuple, ResultType, SupportedReturnType}; +use crate::sandbox::UninitializedSandbox; use crate::sandbox::host_funcs::FunctionEntry; -use crate::sandbox::{ExtraAllowedSyscall, UninitializedSandbox}; use crate::{Result, new_error}; /// A sandbox on which (primitive) host functions can be registered @@ -33,15 +33,6 @@ pub trait Registerable { name: &str, hf: impl Into>, ) -> Result<()>; - /// Register a primitive host function whose worker thread has - /// extra permissive seccomp filters installed - #[cfg(all(feature = "seccomp", target_os = "linux"))] - fn register_host_function_with_syscalls( - &mut self, - name: &str, - hf: impl Into>, - eas: Vec, - ) -> Result<()>; } impl Registerable for UninitializedSandbox { fn register_host_function( @@ -56,33 +47,11 @@ impl Registerable for UninitializedSandbox { let entry = FunctionEntry { function: hf.into().into(), - extra_allowed_syscalls: None, - parameter_types: Args::TYPE, - return_type: Output::TYPE, - }; - - (*hfs).register_host_function(name.to_string(), entry, self.mgr.unwrap_mgr_mut()) - } - #[cfg(all(feature = "seccomp", target_os = "linux"))] - fn register_host_function_with_syscalls( - &mut self, - name: &str, - hf: impl Into>, - eas: Vec, - ) -> Result<()> { - let mut hfs = self - .host_funcs - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?; - - let entry = FunctionEntry { - function: hf.into().into(), - extra_allowed_syscalls: Some(eas), parameter_types: Args::TYPE, return_type: Output::TYPE, }; - (*hfs).register_host_function(name.to_string(), entry, self.mgr.unwrap_mgr_mut()) + (*hfs).register_host_function(name.to_string(), entry, &mut self.mgr) } } @@ -195,13 +164,11 @@ pub(crate) fn register_host_function>, sandbox: &mut UninitializedSandbox, name: &str, - extra_allowed_syscalls: Option>, ) -> Result<()> { let func = func.into().into(); let entry = FunctionEntry { function: func, - extra_allowed_syscalls: extra_allowed_syscalls.clone(), parameter_types: Args::TYPE, return_type: Output::TYPE, }; @@ -210,7 +177,7 @@ pub(crate) fn register_host_function Result; } -/// A trait to handle either a SupportedReturnType or a Result +/// A trait to handle either a [`SupportedReturnType`] or a [`Result`] pub trait ResultType { /// The return type of the supported return value type ReturnType: SupportedReturnType; - /// Convert the return type into a Result + /// Convert the return type into a `Result` fn into_result(self) -> Result; } diff --git a/src/hyperlight_host/src/func/utils.rs b/src/hyperlight_host/src/func/utils.rs index f06c35a3c..6e92ca041 100644 --- a/src/hyperlight_host/src/func/utils.rs +++ b/src/hyperlight_host/src/func/utils.rs @@ -18,10 +18,9 @@ limitations under the License. /// up to 32 parameters. This is useful to implement traits on functions /// for may parameter tuples. /// -/// Usage: -/// ```rust -/// use hyperlight_host::func::for_each_tuple; +/// This is an internal utility macro used within the func module. /// +/// ```ignore /// macro_rules! my_macro { /// ([$count:expr] ($($name:ident: $type:ident),*)) => { /// // $count is the arity of the tuple @@ -30,7 +29,7 @@ limitations under the License. /// }; /// } /// -/// for_each_tuple!(impl_host_function); +/// for_each_tuple!(my_macro); /// ``` macro_rules! for_each_tuple { (@ diff --git a/src/hyperlight_host/src/hypervisor/crashdump.rs b/src/hyperlight_host/src/hypervisor/crashdump.rs index ae865ef56..86dce75ff 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -43,8 +43,8 @@ const CORE_DUMP_PAGE_SIZE: usize = 0x1000; /// Structure to hold the crash dump context /// This structure contains the information needed to create a core dump #[derive(Debug)] -pub(crate) struct CrashDumpContext<'a> { - regions: &'a [MemoryRegion], +pub(crate) struct CrashDumpContext { + regions: Vec, regs: [u64; 27], xsave: Vec, entry: u64, @@ -52,9 +52,9 @@ pub(crate) struct CrashDumpContext<'a> { filename: Option, } -impl<'a> CrashDumpContext<'a> { +impl CrashDumpContext { pub(crate) fn new( - regions: &'a [MemoryRegion], + regions: Vec, regs: [u64; 27], xsave: Vec, entry: u64, @@ -208,7 +208,7 @@ struct GuestMemReader { impl GuestMemReader { fn new(ctx: &CrashDumpContext) -> Self { Self { - regions: ctx.regions.to_vec(), + regions: ctx.regions.clone(), } } } @@ -440,7 +440,7 @@ mod test { fn test_crashdump_write_fails_when_no_regions() { // Create a dummy context let ctx = CrashDumpContext::new( - &[], + vec![], [0; 27], vec![], 0, @@ -471,7 +471,7 @@ mod test { }]; // Create a dummy context let ctx = CrashDumpContext::new( - ®ions, + regions, [0; 27], vec![], 0x1000, diff --git a/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs index 7a902d6de..f5b332d02 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/hyperv_debug.rs @@ -19,9 +19,10 @@ use std::collections::HashMap; use windows::Win32::System::Hypervisor::WHV_VP_EXCEPTION_CONTEXT; use super::arch::{MAX_NO_OF_HW_BP, vcpu_stop_reason}; -use super::{GuestDebug, SW_BP_SIZE, VcpuStopReason, X86_64Regs}; +use super::{GuestDebug, SW_BP_SIZE, VcpuStopReason}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::hypervisor::windows_hypervisor_platform::VMProcessor; -use crate::hypervisor::wrappers::{WHvDebugRegisters, WHvGeneralRegisters}; +use crate::hypervisor::wrappers::WHvDebugRegisters; use crate::{HyperlightError, Result, new_error}; /// KVM Debug struct @@ -54,7 +55,7 @@ impl HypervDebug { /// Returns the instruction pointer from the stopped vCPU fn get_instruction_pointer(&self, vcpu_fd: &VMProcessor) -> Result { let regs = vcpu_fd - .get_regs() + .regs() .map_err(|e| new_error!("Could not retrieve registers from vCPU: {:?}", e))?; Ok(regs.rip) @@ -103,7 +104,7 @@ impl HypervDebug { self.single_step = step; let mut regs = vcpu_fd - .get_regs() + .regs() .map_err(|e| new_error!("Could not get registers: {:?}", e))?; // Set TF Flag to enable Traps @@ -114,7 +115,7 @@ impl HypervDebug { } vcpu_fd - .set_general_purpose_registers(®s) + .set_regs(®s) .map_err(|e| new_error!("Could not set guest registers: {:?}", e))?; Ok(()) @@ -185,33 +186,17 @@ impl GuestDebug for HypervDebug { self.sw_breakpoints.remove(addr) } - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> Result<()> { + fn read_regs(&self, vcpu_fd: &Self::Vcpu) -> Result<(CommonRegisters, CommonFpu)> { log::debug!("Read registers"); - let vcpu_regs = vcpu_fd - .get_regs() + let regs = vcpu_fd + .regs() .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; - regs.rax = vcpu_regs.rax; - regs.rbx = vcpu_regs.rbx; - regs.rcx = vcpu_regs.rcx; - regs.rdx = vcpu_regs.rdx; - regs.rsi = vcpu_regs.rsi; - regs.rdi = vcpu_regs.rdi; - regs.rbp = vcpu_regs.rbp; - regs.rsp = vcpu_regs.rsp; - regs.r8 = vcpu_regs.r8; - regs.r9 = vcpu_regs.r9; - regs.r10 = vcpu_regs.r10; - regs.r11 = vcpu_regs.r11; - regs.r12 = vcpu_regs.r12; - regs.r13 = vcpu_regs.r13; - regs.r14 = vcpu_regs.r14; - regs.r15 = vcpu_regs.r15; - - regs.rip = vcpu_regs.rip; - regs.rflags = vcpu_regs.rflags; + let fpu = vcpu_fd + .fpu() + .map_err(|e| new_error!("Could not read guest FPU registers: {:?}", e))?; - Ok(()) + Ok((regs, fpu)) } fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> Result<()> { @@ -224,32 +209,28 @@ impl GuestDebug for HypervDebug { .map_err(|_| HyperlightError::TranslateGuestAddress(gva)) } - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> Result<()> { + fn write_regs( + &self, + vcpu_fd: &Self::Vcpu, + regs: &CommonRegisters, + fpu: &CommonFpu, + ) -> Result<()> { log::debug!("Write registers"); - let regs = WHvGeneralRegisters { - rax: regs.rax, - rbx: regs.rbx, - rcx: regs.rcx, - rdx: regs.rdx, - rsi: regs.rsi, - rdi: regs.rdi, - rbp: regs.rbp, - rsp: regs.rsp, - r8: regs.r8, - r9: regs.r9, - r10: regs.r10, - r11: regs.r11, - r12: regs.r12, - r13: regs.r13, - r14: regs.r14, - r15: regs.r15, - - rip: regs.rip, - rflags: regs.rflags, - }; vcpu_fd - .set_general_purpose_registers(®s) - .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) + .set_regs(regs) + .map_err(|e| new_error!("Could not write guest registers: {:?}", e))?; + + // Only xmm and mxcsr is piped though in the given fpu, so only set those + let mut current_fpu: CommonFpu = vcpu_fd + .fpu() + .map_err(|e| new_error!("Could not read guest FPU registers: {:?}", e))?; + current_fpu.mxcsr = fpu.mxcsr; + current_fpu.xmm = fpu.xmm; + + vcpu_fd + .set_fpu(¤t_fpu) + .map_err(|e| new_error!("Could not write guest FPU registers: {:?}", e))?; + Ok(()) } } diff --git a/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs index c20b3b279..ae5996d49 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/kvm_debug.rs @@ -18,12 +18,13 @@ use std::collections::HashMap; use kvm_bindings::{ KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_SINGLESTEP, KVM_GUESTDBG_USE_HW_BP, KVM_GUESTDBG_USE_SW_BP, - kvm_debug_exit_arch, kvm_guest_debug, kvm_regs, + kvm_debug_exit_arch, kvm_guest_debug, }; use kvm_ioctls::VcpuFd; use super::arch::{MAX_NO_OF_HW_BP, SW_BP_SIZE, vcpu_stop_reason}; -use super::{GuestDebug, VcpuStopReason, X86_64Regs}; +use super::{GuestDebug, VcpuStopReason}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::{HyperlightError, Result, new_error}; /// KVM Debug struct @@ -167,33 +168,29 @@ impl GuestDebug for KvmDebug { self.sw_breakpoints.remove(addr) } - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> Result<()> { + fn read_regs(&self, vcpu_fd: &Self::Vcpu) -> Result<(CommonRegisters, CommonFpu)> { log::debug!("Read registers"); - let vcpu_regs = vcpu_fd + let regs = vcpu_fd .get_regs() .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; - regs.rax = vcpu_regs.rax; - regs.rbx = vcpu_regs.rbx; - regs.rcx = vcpu_regs.rcx; - regs.rdx = vcpu_regs.rdx; - regs.rsi = vcpu_regs.rsi; - regs.rdi = vcpu_regs.rdi; - regs.rbp = vcpu_regs.rbp; - regs.rsp = vcpu_regs.rsp; - regs.r8 = vcpu_regs.r8; - regs.r9 = vcpu_regs.r9; - regs.r10 = vcpu_regs.r10; - regs.r11 = vcpu_regs.r11; - regs.r12 = vcpu_regs.r12; - regs.r13 = vcpu_regs.r13; - regs.r14 = vcpu_regs.r14; - regs.r15 = vcpu_regs.r15; - - regs.rip = vcpu_regs.rip; - regs.rflags = vcpu_regs.rflags; + let fpu_data = vcpu_fd + .get_fpu() + .map_err(|e| new_error!("Could not read guest FPU registers: {:?}", e))?; + let mut fpu: CommonFpu = CommonFpu::from(&fpu_data); + + // Read MXCSR from XSAVE (MXCSR is at byte offset 24 -> u32 index 6) + // 11.5.10 Mode-Specific XSAVE/XRSTOR State Management + match vcpu_fd.get_xsave() { + Ok(xsave) => { + fpu.mxcsr = xsave.region[6]; + } + Err(e) => { + log::warn!("Failed to read XSAVE for MXCSR: {:?}", e); + } + } - Ok(()) + Ok((CommonRegisters::from(®s), fpu)) } fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> Result<()> { @@ -212,32 +209,45 @@ impl GuestDebug for KvmDebug { } } - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> Result<()> { + fn write_regs( + &self, + vcpu_fd: &Self::Vcpu, + regs: &CommonRegisters, + fpu: &CommonFpu, + ) -> Result<()> { log::debug!("Write registers"); - let new_regs = kvm_regs { - rax: regs.rax, - rbx: regs.rbx, - rcx: regs.rcx, - rdx: regs.rdx, - rsi: regs.rsi, - rdi: regs.rdi, - rbp: regs.rbp, - rsp: regs.rsp, - r8: regs.r8, - r9: regs.r9, - r10: regs.r10, - r11: regs.r11, - r12: regs.r12, - r13: regs.r13, - r14: regs.r14, - r15: regs.r15, - - rip: regs.rip, - rflags: regs.rflags, - }; + let new_regs = regs.into(); vcpu_fd .set_regs(&new_regs) - .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) + .map_err(|e| new_error!("Could not write guest registers: {:?}", e))?; + + // Only xmm and mxcsr is piped though in the given fpu, so only set those + let mut current_fpu: CommonFpu = (&vcpu_fd.get_fpu()?).into(); + current_fpu.mxcsr = fpu.mxcsr; + current_fpu.xmm = fpu.xmm; + vcpu_fd + .set_fpu(&(¤t_fpu).into()) + .map_err(|e| new_error!("Could not write guest registers: {:?}", e))?; + + // Read XMM registers from FPU state + // note kvm get_fpu doesn't actually set or read the mxcsr value + // https://elixir.bootlin.com/linux/v6.16/source/arch/x86/kvm/x86.c#L12229 + // Update MXCSR using XSAVE region entry 6 (MXCSR) if available. + let mut xsave = match vcpu_fd.get_xsave() { + Ok(xsave) => xsave, + Err(e) => { + return Err(new_error!("Could not write guest registers: {:?}", e)); + } + }; + + xsave.region[6] = fpu.mxcsr; + unsafe { + vcpu_fd + .set_xsave(&xsave) + .map_err(|e| new_error!("Could not write guest registers: {:?}", e))? + }; + + Ok(()) } } diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 3cc8ecd7c..4c57958e5 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -27,9 +27,9 @@ mod x86_64_target; use std::io::{self, ErrorKind}; use std::net::TcpListener; use std::sync::{Arc, Mutex}; -use std::thread; +use std::{slice, thread}; -use arch::{SW_BP, SW_BP_SIZE}; +pub(crate) use arch::{SW_BP, SW_BP_SIZE}; use crossbeam_channel::{Receiver, Sender, TryRecvError}; use event_loop::event_loop_thread; use gdbstub::conn::ConnectionExt; @@ -46,8 +46,11 @@ use thiserror::Error; use x86_64_target::HyperlightSandboxTarget; use super::InterruptHandle; -use crate::hypervisor::handlers::DbgMemAccessHandlerCaller; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::mem::layout::SandboxMemoryLayout; +use crate::mem::memory_region::MemoryRegion; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::HostSharedMemory; use crate::{HyperlightError, new_error}; #[derive(Debug, Error)] @@ -87,27 +90,148 @@ impl From for TargetError { } } -/// Struct that contains the x86_64 core registers -#[derive(Debug, Default)] -pub(crate) struct X86_64Regs { - pub(crate) rax: u64, - pub(crate) rbx: u64, - pub(crate) rcx: u64, - pub(crate) rdx: u64, - pub(crate) rsi: u64, - pub(crate) rdi: u64, - pub(crate) rbp: u64, - pub(crate) rsp: u64, - pub(crate) r8: u64, - pub(crate) r9: u64, - pub(crate) r10: u64, - pub(crate) r11: u64, - pub(crate) r12: u64, - pub(crate) r13: u64, - pub(crate) r14: u64, - pub(crate) r15: u64, - pub(crate) rip: u64, - pub(crate) rflags: u64, +/// This abstracts the memory access functions that debugging needs from a sandbox +pub(crate) struct DebugMemoryAccess { + /// Memory manager that provides access to the guest memory + pub(crate) dbg_mem_access_fn: Arc>>, + /// Guest mapped memory regions + pub(crate) guest_mmap_regions: Vec, +} + +impl DebugMemoryAccess { + /// Reads memory from the guest's address space with a maximum length of a PAGE_SIZE + /// + /// # Arguments + /// * `data` - Buffer to store the read data + /// * `gpa` - Guest physical address to read from. + /// This address is shall be translated before calling this function + /// # Returns + /// * `Result<(), HyperlightError>` - Ok if successful, Err otherwise + fn read(&self, data: &mut [u8], gpa: u64) -> crate::Result<()> { + let read_len = data.len(); + + let mem_offset = (gpa as usize) + .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) + .ok_or_else(|| { + log::warn!( + "gpa={:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", + gpa, + gpa, + SandboxMemoryLayout::BASE_ADDRESS + ); + HyperlightError::TranslateGuestAddress(gpa) + })?; + + // First check the mapped memory regions to see if the address is within any of them + let mut region_found = false; + for reg in self.guest_mmap_regions.iter() { + if reg.guest_region.contains(&mem_offset) { + log::debug!("Found mapped region containing {:X}: {:#?}", gpa, reg); + + // Region found - calculate the offset within the region + let region_offset = mem_offset.checked_sub(reg.guest_region.start).ok_or_else(|| { + log::warn!( + "Cannot calculate offset in memory region: mem_offset={:#X}, base={:#X}", + mem_offset, + reg.guest_region.start, + ); + HyperlightError::TranslateGuestAddress(mem_offset as u64) + })?; + + let bytes: &[u8] = unsafe { + slice::from_raw_parts(reg.host_region.start as *const u8, reg.host_region.len()) + }; + data[..read_len].copy_from_slice(&bytes[region_offset..region_offset + read_len]); + + region_found = true; + break; + } + } + + if !region_found { + log::debug!( + "No mapped region found containing {:X}. Trying shared memory ...", + gpa + ); + + self.dbg_mem_access_fn + .try_lock() + .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? + .get_shared_mem_mut() + .copy_to_slice(&mut data[..read_len], mem_offset)?; + } + + Ok(()) + } + + /// Writes memory from the guest's address space with a maximum length of a PAGE_SIZE + /// + /// # Arguments + /// * `data` - Buffer containing the data to write + /// * `gpa` - Guest physical address to write to. + /// This address is shall be translated before calling this function + /// # Returns + /// * `Result<(), HyperlightError>` - Ok if successful, Err otherwise + fn write(&self, data: &[u8], gpa: u64) -> crate::Result<()> { + let write_len = data.len(); + + let mem_offset = (gpa as usize) + .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) + .ok_or_else(|| { + log::warn!( + "gpa={:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", + gpa, + gpa, + SandboxMemoryLayout::BASE_ADDRESS + ); + HyperlightError::TranslateGuestAddress(gpa) + })?; + + // First check the mapped memory regions to see if the address is within any of them + let mut region_found = false; + for reg in self.guest_mmap_regions.iter() { + if reg.guest_region.contains(&mem_offset) { + log::debug!("Found mapped region containing {:X}: {:#?}", gpa, reg); + + // Region found - calculate the offset within the region + let region_offset = mem_offset.checked_sub(reg.guest_region.start).ok_or_else(|| { + log::warn!( + "Cannot calculate offset in memory region: mem_offset={:#X}, base={:#X}", + mem_offset, + reg.guest_region.start, + ); + HyperlightError::TranslateGuestAddress(mem_offset as u64) + })?; + + let bytes: &mut [u8] = unsafe { + slice::from_raw_parts_mut( + reg.host_region.start as *mut u8, + reg.host_region.len(), + ) + }; + bytes[region_offset..region_offset + write_len].copy_from_slice(&data[..write_len]); + + region_found = true; + break; + } + } + + if !region_found { + log::debug!( + "No mapped region found containing {:X}. Trying shared memory at offset {:X} ...", + gpa, + mem_offset + ); + + self.dbg_mem_access_fn + .try_lock() + .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? + .get_shared_mem_mut() + .copy_from_slice(&data[..write_len], mem_offset)?; + } + + Ok(()) + } } /// Defines the possible reasons for which a vCPU can be stopped when debugging @@ -139,7 +263,7 @@ pub(crate) enum DebugMsg { RemoveSwBreakpoint(u64), Step, WriteAddr(u64, Vec), - WriteRegisters(X86_64Regs), + WriteRegisters(Box<(CommonRegisters, CommonFpu)>), } /// Enumerates the possible responses that a hypervisor can provide to a debugger @@ -154,7 +278,7 @@ pub(crate) enum DebugResponse { NotAllowed, InterruptHandle(Arc), ReadAddr(Vec), - ReadRegisters(X86_64Regs), + ReadRegisters(Box<(CommonRegisters, CommonFpu)>), RemoveHwBreakpoint(bool), RemoveSwBreakpoint(bool), Step, @@ -182,13 +306,18 @@ pub(super) trait GuestDebug { fn delete_sw_breakpoint_data(&mut self, addr: &u64) -> Option<[u8; 1]>; /// Read registers - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> crate::Result<()>; + fn read_regs(&self, vcpu_fd: &Self::Vcpu) -> crate::Result<(CommonRegisters, CommonFpu)>; /// Enables or disables stepping and sets the vCPU debug configuration fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> crate::Result<()>; /// Translates the guest address to physical address fn translate_gva(&self, vcpu_fd: &Self::Vcpu, gva: u64) -> crate::Result; /// Write registers - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> crate::Result<()>; + fn write_regs( + &self, + vcpu_fd: &Self::Vcpu, + regs: &CommonRegisters, + fpu: &CommonFpu, + ) -> crate::Result<()>; /// Adds hardware breakpoint fn add_hw_breakpoint(&mut self, vcpu_fd: &Self::Vcpu, addr: u64) -> crate::Result<()> { @@ -209,7 +338,7 @@ pub(super) trait GuestDebug { &mut self, vcpu_fd: &Self::Vcpu, addr: u64, - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> crate::Result<()> { let addr = self.translate_gva(vcpu_fd, addr)?; @@ -219,8 +348,8 @@ pub(super) trait GuestDebug { // Write breakpoint OP code to write to guest memory let mut save_data = [0; SW_BP_SIZE]; - self.read_addrs(vcpu_fd, addr, &mut save_data[..], dbg_mem_access_fn.clone())?; - self.write_addrs(vcpu_fd, addr, &SW_BP, dbg_mem_access_fn)?; + self.read_addrs(vcpu_fd, addr, &mut save_data[..], mem_access)?; + self.write_addrs(vcpu_fd, addr, &SW_BP, mem_access)?; // Save guest memory to restore when breakpoint is removed self.save_sw_breakpoint_data(addr, save_data); @@ -234,7 +363,7 @@ pub(super) trait GuestDebug { vcpu_fd: &Self::Vcpu, mut gva: u64, mut data: &mut [u8], - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> crate::Result<()> { let data_len = data.len(); log::debug!("Read addr: {:X} len: {:X}", gva, data_len); @@ -246,19 +375,8 @@ pub(super) trait GuestDebug { data.len(), (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(), ); - let offset = (gpa as usize) - .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) - .ok_or_else(|| { - log::warn!( - "gva=0x{:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", - gva, gpa, SandboxMemoryLayout::BASE_ADDRESS); - HyperlightError::TranslateGuestAddress(gva) - })?; - dbg_mem_access_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .read(offset, &mut data[..read_len])?; + mem_access.read(&mut data[..read_len], gpa)?; data = &mut data[read_len..]; gva += read_len as u64; @@ -282,7 +400,7 @@ pub(super) trait GuestDebug { &mut self, vcpu_fd: &Self::Vcpu, addr: u64, - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> crate::Result<()> { let addr = self.translate_gva(vcpu_fd, addr)?; @@ -292,7 +410,7 @@ pub(super) trait GuestDebug { .ok_or_else(|| new_error!("Expected to contain the sw breakpoint address"))?; // Restore saved data to the guest's memory - self.write_addrs(vcpu_fd, addr, &save_data, dbg_mem_access_fn)?; + self.write_addrs(vcpu_fd, addr, &save_data, mem_access)?; Ok(()) } else { @@ -306,7 +424,7 @@ pub(super) trait GuestDebug { vcpu_fd: &Self::Vcpu, mut gva: u64, mut data: &[u8], - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> crate::Result<()> { let data_len = data.len(); log::debug!("Write addr: {:X} len: {:X}", gva, data_len); @@ -318,19 +436,9 @@ pub(super) trait GuestDebug { data.len(), (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(), ); - let offset = (gpa as usize) - .checked_sub(SandboxMemoryLayout::BASE_ADDRESS) - .ok_or_else(|| { - log::warn!( - "gva=0x{:#X} causes subtract with underflow: \"gpa - BASE_ADDRESS={:#X}-{:#X}\"", - gva, gpa, SandboxMemoryLayout::BASE_ADDRESS); - HyperlightError::TranslateGuestAddress(gva) - })?; - dbg_mem_access_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .write(offset, data)?; + // Use the memory access to write to guest memory + mem_access.write(&data[..write_len], gpa)?; data = &data[write_len..]; gva += write_len as u64; @@ -446,10 +554,189 @@ mod tests { let res = gdb_conn.try_recv(); assert!(res.is_err()); - let res = hyp_conn.send(DebugResponse::ReadRegisters(X86_64Regs::default())); + let res = hyp_conn.send(DebugResponse::ReadRegisters(Box::new(( + Default::default(), + Default::default(), + )))); assert!(res.is_ok()); let res = gdb_conn.recv(); assert!(res.is_ok()); } + + #[cfg(target_os = "linux")] + mod mem_access_tests { + use std::os::fd::AsRawFd; + use std::os::linux::fs::MetadataExt; + use std::sync::{Arc, Mutex}; + + use hyperlight_testing::dummy_guest_as_string; + + use super::*; + use crate::mem::memory_region::{MemoryRegionFlags, MemoryRegionType}; + use crate::sandbox::UninitializedSandbox; + use crate::sandbox::uninitialized::GuestBinary; + use crate::{log_then_return, new_error}; + + #[cfg(target_os = "linux")] + const BASE_VIRT: usize = 0x10000000 + SandboxMemoryLayout::BASE_ADDRESS; + + /// Dummy memory region to test memory access + /// This maps a file into memory and uses it as guest memory + fn get_mem_access() -> crate::Result { + let filename = dummy_guest_as_string().map_err(|e| new_error!("{}", e))?; + + let file = std::fs::File::options() + .read(true) + .write(true) + .open(&filename)?; + let file_size = file.metadata()?.st_size(); + let page_size = page_size::get(); + let size = (file_size as usize).div_ceil(page_size) * page_size; + let mapped_mem = unsafe { + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC, + libc::MAP_PRIVATE, + file.as_raw_fd(), + 0, + ) + }; + if mapped_mem == libc::MAP_FAILED { + log_then_return!("mmap error: {:?}", std::io::Error::last_os_error()); + } + + // Create a sandbox memory manager with the mapped memory region + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(filename.clone()), None) + .inspect_err(|_| unsafe { + libc::munmap(mapped_mem, size); + })?; + let (mem_mgr, _) = sandbox.mgr.build(); + + // Create the memory access struct + let mem_access = DebugMemoryAccess { + dbg_mem_access_fn: Arc::new(Mutex::new(mem_mgr)), + guest_mmap_regions: vec![MemoryRegion { + host_region: mapped_mem as usize..mapped_mem.wrapping_add(size) as usize, + guest_region: BASE_VIRT..BASE_VIRT + size, + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::Heap, + }], + }; + + Ok(mem_access) + } + + /// Gets a slice to the mapped memory region to be able to modify it + /// + /// NOTE: By returning a mutable slice from a mutable reference, we ensure + /// that the memory is not deallocated while the slice is in use. + unsafe fn get_mmap_slice(mem_access: &mut DebugMemoryAccess) -> &mut [u8] { + unsafe { + std::slice::from_raw_parts_mut( + mem_access.guest_mmap_regions[0].host_region.start as *mut u8, + mem_access.guest_mmap_regions[0].host_region.end + - mem_access.guest_mmap_regions[0].host_region.start, + ) + } + } + + /// Drops the mapped memory region + fn drop_mem_access(mem_access: DebugMemoryAccess) { + let mapped_mem = + mem_access.guest_mmap_regions[0].host_region.start as *mut libc::c_void; + let size = mem_access.guest_mmap_regions[0].host_region.end + - mem_access.guest_mmap_regions[0].host_region.start; + + unsafe { + libc::munmap(mapped_mem, size); + } + } + + #[test] + fn test_mem_access_read_single_byte() -> crate::Result<()> { + let mut mem_access = get_mem_access()?; + let offset = 2000; + + // Modify the memory directly to have a known value to read + { + let slice = unsafe { get_mmap_slice(&mut mem_access) }; + slice[offset] = 0xAA; + } + + let mut read_data = [0u8; 1]; + mem_access.read(&mut read_data, (BASE_VIRT + offset) as u64)?; + + assert_eq!(read_data[0], 0xAA); + + drop_mem_access(mem_access); + + Ok(()) + } + + #[test] + fn test_mem_access_read_multiple_bytes() -> crate::Result<()> { + let mut mem_access = get_mem_access()?; + let offset = 20; + + // Modify the memory directly to have a known value to read + { + let slice = unsafe { get_mmap_slice(&mut mem_access) }; + for i in 0..16 { + slice[offset + i] = i as u8; + } + } + + let mut read_data = [0u8; 16]; + mem_access.read(&mut read_data, (BASE_VIRT + offset) as u64)?; + + assert_eq!( + read_data, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + ); + drop_mem_access(mem_access); + Ok(()) + } + + #[test] + fn test_mem_access_write_single_byte() -> crate::Result<()> { + let mut mem_access = get_mem_access()?; + let offset = 3000; + { + let slice = unsafe { get_mmap_slice(&mut mem_access) }; + slice[offset] = 0xBB; + } + + let write_data = [0xCCu8; 1]; + mem_access.write(&write_data, (BASE_VIRT + offset) as u64)?; + + let slice = unsafe { get_mmap_slice(&mut mem_access) }; + assert_eq!(slice[offset], write_data[0]); + drop_mem_access(mem_access); + + Ok(()) + } + + #[test] + fn test_mem_access_write_multiple_bytes() -> crate::Result<()> { + let mut mem_access = get_mem_access()?; + let offset = 56; + { + let slice = unsafe { get_mmap_slice(&mut mem_access) }; + for i in 0..16 { + slice[offset + i] = i as u8; + } + } + + let write_data = [0xAAu8; 16]; + mem_access.write(&write_data, (BASE_VIRT + offset) as u64)?; + + let slice = unsafe { get_mmap_slice(&mut mem_access) }; + assert_eq!(slice[offset..offset + 16], write_data); + drop_mem_access(mem_access); + + Ok(()) + } + } } diff --git a/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs b/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs index 9688d9ed1..92569a2fd 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mshv_debug.rs @@ -28,12 +28,12 @@ use std::collections::HashMap; use mshv_bindings::{ DebugRegisters, HV_TRANSLATE_GVA_VALIDATE_READ, HV_TRANSLATE_GVA_VALIDATE_WRITE, - StandardRegisters, }; use mshv_ioctls::VcpuFd; use super::arch::{MAX_NO_OF_HW_BP, SW_BP_SIZE, vcpu_stop_reason}; -use super::{GuestDebug, VcpuStopReason, X86_64Regs}; +use super::{GuestDebug, VcpuStopReason}; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::{HyperlightError, Result, new_error}; #[derive(Debug, Default)] @@ -194,33 +194,20 @@ impl GuestDebug for MshvDebug { self.sw_breakpoints.remove(addr) } - fn read_regs(&self, vcpu_fd: &Self::Vcpu, regs: &mut X86_64Regs) -> Result<()> { + fn read_regs(&self, vcpu_fd: &Self::Vcpu) -> Result<(CommonRegisters, CommonFpu)> { log::debug!("Read registers"); - let vcpu_regs = vcpu_fd + + let regs = vcpu_fd .get_regs() .map_err(|e| new_error!("Could not read guest registers: {:?}", e))?; + let regs = CommonRegisters::from(®s); - regs.rax = vcpu_regs.rax; - regs.rbx = vcpu_regs.rbx; - regs.rcx = vcpu_regs.rcx; - regs.rdx = vcpu_regs.rdx; - regs.rsi = vcpu_regs.rsi; - regs.rdi = vcpu_regs.rdi; - regs.rbp = vcpu_regs.rbp; - regs.rsp = vcpu_regs.rsp; - regs.r8 = vcpu_regs.r8; - regs.r9 = vcpu_regs.r9; - regs.r10 = vcpu_regs.r10; - regs.r11 = vcpu_regs.r11; - regs.r12 = vcpu_regs.r12; - regs.r13 = vcpu_regs.r13; - regs.r14 = vcpu_regs.r14; - regs.r15 = vcpu_regs.r15; - - regs.rip = vcpu_regs.rip; - regs.rflags = vcpu_regs.rflags; + let fpu_data = vcpu_fd + .get_fpu() + .map_err(|e| new_error!("Could not read guest FPU registers: {:?}", e))?; + let fpu = CommonFpu::from(&fpu_data); - Ok(()) + Ok((regs, fpu)) } fn set_single_step(&mut self, vcpu_fd: &Self::Vcpu, enable: bool) -> Result<()> { @@ -236,32 +223,25 @@ impl GuestDebug for MshvDebug { Ok(addr) } - fn write_regs(&self, vcpu_fd: &Self::Vcpu, regs: &X86_64Regs) -> Result<()> { + fn write_regs( + &self, + vcpu_fd: &Self::Vcpu, + regs: &CommonRegisters, + fpu: &CommonFpu, + ) -> Result<()> { log::debug!("Write registers"); - let new_regs = StandardRegisters { - rax: regs.rax, - rbx: regs.rbx, - rcx: regs.rcx, - rdx: regs.rdx, - rsi: regs.rsi, - rdi: regs.rdi, - rbp: regs.rbp, - rsp: regs.rsp, - r8: regs.r8, - r9: regs.r9, - r10: regs.r10, - r11: regs.r11, - r12: regs.r12, - r13: regs.r13, - r14: regs.r14, - r15: regs.r15, - - rip: regs.rip, - rflags: regs.rflags, - }; vcpu_fd - .set_regs(&new_regs) + .set_regs(®s.into()) + .map_err(|e| new_error!("Could not write guest registers: {:?}", e))?; + + // Only xmm and mxcsr is piped though in the given fpu, so only set those + let mut current_fpu: CommonFpu = (&vcpu_fd.get_fpu()?).into(); + current_fpu.mxcsr = fpu.mxcsr; + current_fpu.xmm = fpu.xmm; + + vcpu_fd + .set_fpu(&(¤t_fpu).into()) .map_err(|e| new_error!("Could not write guest registers: {:?}", e)) } } diff --git a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs b/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs index 3248e6082..7ba38fc48 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs @@ -31,8 +31,9 @@ use gdbstub::target::ext::section_offsets::{Offsets, SectionOffsets}; use gdbstub::target::{Target, TargetError, TargetResult}; use gdbstub_arch::x86::X86_64_SSE as GdbTargetArch; -use super::{DebugCommChannel, DebugMsg, DebugResponse, GdbTargetError, X86_64Regs}; +use super::{DebugCommChannel, DebugMsg, DebugResponse, GdbTargetError}; use crate::hypervisor::InterruptHandle; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; /// Gdbstub target used by the gdbstub crate to provide GDB protocol implementation pub(crate) struct HyperlightSandboxTarget { @@ -133,18 +134,18 @@ impl Target for HyperlightSandboxTarget { type Arch = GdbTargetArch; type Error = GdbTargetError; - fn support_breakpoints(&mut self) -> Option> { + fn support_breakpoints(&mut self) -> Option> { Some(self) } #[inline(always)] - fn base_ops(&mut self) -> BaseOps { + fn base_ops(&mut self) -> BaseOps<'_, Self::Arch, Self::Error> { BaseOps::SingleThread(self) } fn support_section_offsets( &mut self, - ) -> Option> { + ) -> Option> { Some(self) } } @@ -208,7 +209,8 @@ impl SingleThreadBase for HyperlightSandboxTarget { log::debug!("Read regs"); match self.send_command(DebugMsg::ReadRegisters)? { - DebugResponse::ReadRegisters(read_regs) => { + DebugResponse::ReadRegisters(boxed_regs) => { + let (read_regs, read_fpu) = boxed_regs.as_ref(); regs.regs[0] = read_regs.rax; regs.regs[1] = read_regs.rbp; regs.regs[2] = read_regs.rcx; @@ -228,6 +230,9 @@ impl SingleThreadBase for HyperlightSandboxTarget { regs.rip = read_regs.rip; regs.eflags = read_regs.rflags as u32; + regs.xmm = read_fpu.xmm.map(u128::from_le_bytes); + regs.mxcsr = read_fpu.mxcsr; + Ok(()) } DebugResponse::ErrorOccurred => { @@ -248,7 +253,7 @@ impl SingleThreadBase for HyperlightSandboxTarget { ) -> TargetResult<(), Self> { log::debug!("Write regs"); - let regs = X86_64Regs { + let common_regs = CommonRegisters { rax: regs.regs[0], rbx: regs.regs[1], rcx: regs.regs[2], @@ -269,7 +274,21 @@ impl SingleThreadBase for HyperlightSandboxTarget { rflags: u64::from(regs.eflags), }; - match self.send_command(DebugMsg::WriteRegisters(regs))? { + let mut xmm = [[0u8; 16]; 16]; + for (i, ®) in regs.xmm.iter().enumerate() { + xmm[i] = reg.to_le_bytes(); + } + + let common_fpu = CommonFpu { + xmm, + mxcsr: regs.mxcsr, + ..Default::default() + }; + + match self.send_command(DebugMsg::WriteRegisters(Box::new(( + common_regs, + common_fpu, + ))))? { DebugResponse::WriteRegisters => Ok(()), DebugResponse::NotAllowed => { log::error!("Action not allowed at this time, crash might have occurred"); @@ -288,7 +307,7 @@ impl SingleThreadBase for HyperlightSandboxTarget { } } - fn support_resume(&mut self) -> Option> { + fn support_resume(&mut self) -> Option> { Some(self) } } @@ -315,7 +334,7 @@ impl SectionOffsets for HyperlightSandboxTarget { } impl Breakpoints for HyperlightSandboxTarget { - fn support_hw_breakpoint(&mut self) -> Option> { + fn support_hw_breakpoint(&mut self) -> Option> { Some(self) } fn support_sw_breakpoint(&mut self) -> Option> { @@ -438,7 +457,7 @@ impl SingleThreadResume for HyperlightSandboxTarget { log::debug!("Resume"); self.resume_vcpu() } - fn support_single_step(&mut self) -> Option> { + fn support_single_step(&mut self) -> Option> { Some(self) } } @@ -482,7 +501,7 @@ mod tests { // Check response to read registers - send the response first to not be blocked // by the recv call in the target - let msg = DebugResponse::ReadRegisters(X86_64Regs::default()); + let msg = DebugResponse::ReadRegisters(Box::default()); let res = gdb_conn.send(msg); assert!(res.is_ok()); diff --git a/src/hyperlight_host/src/hypervisor/handlers.rs b/src/hyperlight_host/src/hypervisor/handlers.rs deleted file mode 100644 index 9a091b2b1..000000000 --- a/src/hyperlight_host/src/hypervisor/handlers.rs +++ /dev/null @@ -1,126 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use std::sync::{Arc, Mutex}; - -use tracing::{Span, instrument}; - -use crate::{Result, new_error}; - -/// The trait representing custom logic to handle the case when -/// a Hypervisor's virtual CPU (vCPU) informs Hyperlight the guest -/// has initiated an outb operation. -pub trait OutBHandlerCaller: Sync + Send { - /// Function that gets called when an outb operation has occurred. - fn call(&mut self, port: u16, payload: u32) -> Result<()>; -} - -/// A convenient type representing a common way `OutBHandler` implementations -/// are passed as parameters to functions -/// -/// Note: This needs to be wrapped in a Mutex to be able to grab a mutable -/// reference to the underlying data (i.e., handle_outb in `Sandbox` takes -/// a &mut self). -pub type OutBHandlerWrapper = Arc>; - -pub(crate) type OutBHandlerFunction = Box Result<()> + Send>; - -/// A `OutBHandler` implementation using a `OutBHandlerFunction` -/// -/// Note: This handler must live no longer than the `Sandbox` to which it belongs -pub(crate) struct OutBHandler(Arc>); - -impl From for OutBHandler { - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn from(func: OutBHandlerFunction) -> Self { - Self(Arc::new(Mutex::new(func))) - } -} - -impl OutBHandlerCaller for OutBHandler { - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - fn call(&mut self, port: u16, payload: u32) -> Result<()> { - let mut func = self - .0 - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?; - func(port, payload) - } -} - -/// The trait representing custom logic to handle the case when -/// a Hypervisor's virtual CPU (vCPU) informs Hyperlight a memory access -/// outside the designated address space has occurred. -pub trait MemAccessHandlerCaller: Send { - /// Function that gets called when unexpected memory access has occurred. - fn call(&mut self) -> Result<()>; -} - -/// A convenient type representing a common way `MemAccessHandler` implementations -/// are passed as parameters to functions -/// -/// Note: This needs to be wrapped in a Mutex to be able to grab a mutable -/// reference to the underlying data (i.e., handle_mmio_exit in `Sandbox` takes -/// a &mut self). -pub type MemAccessHandlerWrapper = Arc>; - -pub(crate) type MemAccessHandlerFunction = Box Result<()> + Send>; - -/// A `MemAccessHandler` implementation using `MemAccessHandlerFunction`. -/// -/// Note: This handler must live for as long as its Sandbox or for -/// static in the case of its C API usage. -pub(crate) struct MemAccessHandler(Arc>); - -impl From for MemAccessHandler { - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn from(func: MemAccessHandlerFunction) -> Self { - Self(Arc::new(Mutex::new(func))) - } -} - -impl MemAccessHandlerCaller for MemAccessHandler { - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - fn call(&mut self) -> Result<()> { - let mut func = self - .0 - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?; - func() - } -} - -/// The trait representing custom logic to handle the case when -/// a Hypervisor's virtual CPU (vCPU) informs Hyperlight a debug memory access -/// has been requested. -#[cfg(gdb)] -pub trait DbgMemAccessHandlerCaller: Send { - /// Function that gets called when a read is requested. - fn read(&mut self, addr: usize, data: &mut [u8]) -> Result<()>; - - /// Function that gets called when a write is requested. - fn write(&mut self, addr: usize, data: &[u8]) -> Result<()>; - - /// Function that gets called for a request to get guest code offset. - fn get_code_offset(&mut self) -> Result; -} - -/// A convenient type representing an implementer of `DbgMemAccessHandlerCaller` -/// -/// Note: This needs to be wrapped in a Mutex to be able to grab a mutable -/// reference to the underlying data -#[cfg(gdb)] -pub type DbgMemAccessHandlerWrapper = Arc>; diff --git a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs b/src/hyperlight_host/src/hypervisor/hyperv_linux.rs index db3f81c9b..801f361a7 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_linux.rs +++ b/src/hyperlight_host/src/hypervisor/hyperv_linux.rs @@ -25,14 +25,14 @@ extern crate mshv_bindings3 as mshv_bindings; extern crate mshv_ioctls3 as mshv_ioctls; use std::fmt::{Debug, Formatter}; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; use log::{LevelFilter, error}; #[cfg(mshv2)] use mshv_bindings::hv_message; use mshv_bindings::{ - FloatingPointUnit, SegmentRegister, SpecialRegisters, StandardRegisters, hv_message_type, + FloatingPointUnit, SpecialRegisters, StandardRegisters, hv_message_type, hv_message_type_HVMSG_GPA_INTERCEPT, hv_message_type_HVMSG_UNMAPPED_GPA, hv_message_type_HVMSG_X64_HALT, hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT, hv_register_assoc, hv_register_name_HV_X64_REGISTER_RIP, hv_register_value, mshv_user_mem_region, @@ -48,42 +48,41 @@ use mshv_bindings::{ hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, hv_partition_synthetic_processor_features, }; -use mshv_ioctls::{Mshv, MshvError, VcpuFd, VmFd}; +use mshv_ioctls::{Mshv, VcpuFd, VmFd}; use tracing::{Span, instrument}; +#[cfg(feature = "trace_guest")] +use tracing_opentelemetry::OpenTelemetrySpanExt; #[cfg(crashdump)] use {super::crashdump, std::path::Path}; -use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT}; #[cfg(gdb)] use super::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, MshvDebug, VcpuStopReason, -}; -#[cfg(gdb)] -use super::handlers::DbgMemAccessHandlerWrapper; -use super::handlers::{MemAccessHandlerWrapper, OutBHandlerWrapper}; -#[cfg(feature = "init-paging")] -use super::{ - CR0_AM, CR0_ET, CR0_MP, CR0_NE, CR0_PE, CR0_PG, CR0_WP, CR4_OSFXSR, CR4_OSXMMEXCPT, CR4_PAE, - EFER_LMA, EFER_LME, EFER_NX, EFER_SCE, + DebugCommChannel, DebugMemoryAccess, DebugMsg, DebugResponse, GuestDebug, MshvDebug, + VcpuStopReason, }; use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU}; #[cfg(gdb)] use crate::HyperlightError; +use crate::hypervisor::get_memory_access_violation; +use crate::hypervisor::regs::CommonFpu; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; +use crate::mem::shared_mem::HostSharedMemory; use crate::sandbox::SandboxConfiguration; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::outb::handle_outb; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; #[cfg(crashdump)] use crate::sandbox::uninitialized::SandboxRuntimeConfig; use crate::{Result, log_then_return, new_error}; #[cfg(gdb)] mod debug { - use std::sync::{Arc, Mutex}; - use super::mshv_bindings::hv_x64_exception_intercept_message; use super::{HypervLinuxDriver, *}; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason, X86_64Regs}; - use crate::hypervisor::handlers::DbgMemAccessHandlerCaller; + use crate::hypervisor::gdb::{DebugMemoryAccess, DebugMsg, DebugResponse, VcpuStopReason}; use crate::{Result, new_error}; impl HypervLinuxDriver { @@ -114,7 +113,7 @@ mod debug { pub(crate) fn process_dbg_request( &mut self, req: DebugMsg, - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> Result { if let Some(debug) = self.debug.as_mut() { match req { @@ -130,7 +129,7 @@ mod debug { )), DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( debug - .add_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) + .add_sw_breakpoint(&self.vcpu_fd, addr, mem_access) .map_err(|e| { log::error!("Failed to add sw breakpoint: {:?}", e); @@ -157,45 +156,32 @@ mod debug { Ok(DebugResponse::DisableDebug) } DebugMsg::GetCodeSectionOffset => { - let offset = dbg_mem_access_fn + let offset = mem_access + .dbg_mem_access_fn .try_lock() .map_err(|e| { new_error!("Error locking at {}:{}: {}", file!(), line!(), e) })? - .get_code_offset() - .map_err(|e| { - log::error!("Failed to get code offset: {:?}", e); - - e - })?; + .layout + .get_guest_code_address(); Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) } DebugMsg::ReadAddr(addr, len) => { let mut data = vec![0u8; len]; - debug - .read_addrs(&self.vcpu_fd, addr, &mut data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to read from address: {:?}", e); - - e - })?; + debug.read_addrs(&self.vcpu_fd, addr, &mut data, mem_access)?; Ok(DebugResponse::ReadAddr(data)) } - DebugMsg::ReadRegisters => { - let mut regs = X86_64Regs::default(); - - debug - .read_regs(&self.vcpu_fd, &mut regs) - .map_err(|e| { - log::error!("Failed to read registers: {:?}", e); + DebugMsg::ReadRegisters => debug + .read_regs(&self.vcpu_fd) + .map_err(|e| { + log::error!("Failed to read registers: {:?}", e); - e - }) - .map(|_| DebugResponse::ReadRegisters(regs)) - } + e + }) + .map(|(regs, fpu)| DebugResponse::ReadRegisters(Box::new((regs, fpu)))), DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( debug .remove_hw_breakpoint(&self.vcpu_fd, addr) @@ -208,7 +194,7 @@ mod debug { )), DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( debug - .remove_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) + .remove_sw_breakpoint(&self.vcpu_fd, addr, mem_access) .map_err(|e| { log::error!("Failed to remove sw breakpoint: {:?}", e); @@ -226,24 +212,21 @@ mod debug { Ok(DebugResponse::Step) } DebugMsg::WriteAddr(addr, data) => { + debug.write_addrs(&self.vcpu_fd, addr, &data, mem_access)?; + + Ok(DebugResponse::WriteAddr) + } + DebugMsg::WriteRegisters(boxed_regs) => { + let (regs, fpu) = boxed_regs.as_ref(); debug - .write_addrs(&self.vcpu_fd, addr, &data, dbg_mem_access_fn) + .write_regs(&self.vcpu_fd, regs, fpu) .map_err(|e| { - log::error!("Failed to write to address: {:?}", e); + log::error!("Failed to write registers: {:?}", e); e - })?; - - Ok(DebugResponse::WriteAddr) + }) + .map(|_| DebugResponse::WriteRegisters) } - DebugMsg::WriteRegisters(regs) => debug - .write_regs(&self.vcpu_fd, ®s) - .map_err(|e| { - log::error!("Failed to write registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::WriteRegisters), } } else { Err(new_error!("Debugging is not enabled")) @@ -297,12 +280,17 @@ pub(crate) fn is_hypervisor_present() -> bool { /// called the Microsoft Hypervisor (MSHV) pub(crate) struct HypervLinuxDriver { _mshv: Mshv, + page_size: usize, vm_fd: VmFd, vcpu_fd: VcpuFd, - entrypoint: u64, - mem_regions: Vec, orig_rsp: GuestPtr, + entrypoint: u64, interrupt_handle: Arc, + mem_mgr: Option>, + host_funcs: Option>>, + + sandbox_regions: Vec, // Initially mapped regions when sandbox is created + mmap_regions: Vec, // Later mapped regions #[cfg(gdb)] debug: Option, @@ -310,6 +298,8 @@ pub(crate) struct HypervLinuxDriver { gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] + trace_info: MemTraceInfo, } impl HypervLinuxDriver { @@ -321,6 +311,8 @@ impl HypervLinuxDriver { /// the underlying virtual CPU after this function returns. Call the /// `apply_registers` method to do that, or more likely call /// `initialise` to do it for you. + #[allow(clippy::too_many_arguments)] + // TODO: refactor this function to take fewer arguments. Add trace_info to rt_cfg #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn new( mem_regions: Vec, @@ -330,6 +322,7 @@ impl HypervLinuxDriver { config: &SandboxConfiguration, #[cfg(gdb)] gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] trace_info: MemTraceInfo, ) -> Result { let mshv = Mshv::new()?; let pr = Default::default(); @@ -343,7 +336,7 @@ impl HypervLinuxDriver { let vm_fd = mshv.create_vm_with_args(&pr)?; let features: hv_partition_synthetic_processor_features = Default::default(); - vm_fd.hvcall_set_partition_property( + vm_fd.set_partition_property( hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES, unsafe { features.as_uint64[0] }, )?; @@ -351,7 +344,7 @@ impl HypervLinuxDriver { vm_fd }; - let mut vcpu_fd = vm_fd.create_vcpu(0)?; + let vcpu_fd = vm_fd.create_vcpu(0)?; #[cfg(gdb)] let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { @@ -395,8 +388,6 @@ impl HypervLinuxDriver { vm_fd.map_user_memory(mshv_region) })?; - Self::setup_initial_sregs(&mut vcpu_fd, pml4_ptr.absolute()?)?; - let interrupt_handle = Arc::new(LinuxInterruptHandle { running: AtomicU64::new(0), cancel_requested: AtomicBool::new(false), @@ -421,67 +412,39 @@ impl HypervLinuxDriver { dropped: AtomicBool::new(false), }); - #[allow(unused_mut)] let mut hv = Self { _mshv: mshv, + page_size: 0, vm_fd, vcpu_fd, - mem_regions, + sandbox_regions: mem_regions, + mmap_regions: Vec::new(), entrypoint: entrypoint_ptr.absolute()?, orig_rsp: rsp_ptr, interrupt_handle: interrupt_handle.clone(), + mem_mgr: None, + host_funcs: None, #[cfg(gdb)] debug, #[cfg(gdb)] gdb_conn, #[cfg(crashdump)] rt_cfg, + #[cfg(feature = "mem_profile")] + trace_info, }; + hv.setup_initial_sregs(pml4_ptr.absolute()?)?; + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] - hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; + if hv.debug.is_some() { + hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; + } Ok(hv) } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn setup_initial_sregs(vcpu: &mut VcpuFd, _pml4_addr: u64) -> Result<()> { - #[cfg(feature = "init-paging")] - let sregs = SpecialRegisters { - cr0: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, - cr4: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, - cr3: _pml4_addr, - efer: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, - cs: SegmentRegister { - type_: 11, - present: 1, - s: 1, - l: 1, - ..Default::default() - }, - tr: SegmentRegister { - limit: 65535, - type_: 11, - present: 1, - ..Default::default() - }, - ..Default::default() - }; - - #[cfg(not(feature = "init-paging"))] - let sregs = SpecialRegisters { - cs: SegmentRegister { - base: 0, - selector: 0, - ..Default::default() - }, - ..Default::default() - }; - vcpu.set_sregs(&sregs)?; - Ok(()) - } } impl Debug for HypervLinuxDriver { @@ -491,8 +454,11 @@ impl Debug for HypervLinuxDriver { f.field("Entrypoint", &self.entrypoint) .field("Original RSP", &self.orig_rsp); - for region in &self.mem_regions { - f.field("Memory Region", ®ion); + for region in &self.sandbox_regions { + f.field("Sandbox Memory Region", ®ion); + } + for region in &self.mmap_regions { + f.field("Mapped Memory Region", ®ion); } let regs = self.vcpu_fd.get_regs(); @@ -518,11 +484,15 @@ impl Hypervisor for HypervLinuxDriver { peb_addr: RawPtr, seed: u64, page_size: u32, - outb_hdl: OutBHandlerWrapper, - mem_access_hdl: MemAccessHandlerWrapper, + mem_mgr: SandboxMemoryManager, + host_funcs: Arc>, max_guest_log_level: Option, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()> { + self.mem_mgr = Some(mem_mgr); + self.host_funcs = Some(host_funcs); + self.page_size = page_size as usize; + let max_guest_log_level: u64 = match max_guest_log_level { Some(level) => level as u64, None => self.get_max_log_level().into(), @@ -545,22 +515,51 @@ impl Hypervisor for HypervLinuxDriver { VirtualCPU::run( self.as_mut_hypervisor(), - outb_hdl, - mem_access_hdl, #[cfg(gdb)] dbg_mem_access_fn, - )?; + ) + } + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> { + if [ + rgn.guest_region.start, + rgn.guest_region.end, + rgn.host_region.start, + rgn.host_region.end, + ] + .iter() + .any(|x| x % self.page_size != 0) + { + log_then_return!("region is not page-aligned"); + } + let mshv_region: mshv_user_mem_region = rgn.to_owned().into(); + self.vm_fd.map_user_memory(mshv_region)?; + self.mmap_regions.push(rgn.to_owned()); Ok(()) } + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + unsafe fn unmap_region(&mut self, region: &MemoryRegion) -> Result<()> { + if let Some(pos) = self.mmap_regions.iter().position(|r| r == region) { + let removed_region = self.mmap_regions.remove(pos); + let mshv_region: mshv_user_mem_region = removed_region.into(); + self.vm_fd.unmap_user_memory(mshv_region)?; + Ok(()) + } else { + Err(new_error!("Tried to unmap region that is not mapped")) + } + } + + fn get_mapped_regions(&self) -> Box + '_> { + Box::new(self.mmap_regions.iter()) + } + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] fn dispatch_call_from_host( &mut self, dispatch_func_addr: RawPtr, - outb_handle_fn: OutBHandlerWrapper, - mem_access_fn: MemAccessHandlerWrapper, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()> { // Reset general purpose registers, then set RIP and RSP let regs = StandardRegisters { @@ -572,19 +571,11 @@ impl Hypervisor for HypervLinuxDriver { self.vcpu_fd.set_regs(®s)?; // reset fpu state - let fpu = FloatingPointUnit { - fcw: FP_CONTROL_WORD_DEFAULT, - ftwx: FP_TAG_WORD_DEFAULT, - mxcsr: MXCSR_DEFAULT, - ..Default::default() // zero out the rest - }; - self.vcpu_fd.set_fpu(&fpu)?; + self.set_fpu(&CommonFpu::default())?; // run VirtualCPU::run( self.as_mut_hypervisor(), - outb_handle_fn, - mem_access_fn, #[cfg(gdb)] dbg_mem_access_fn, )?; @@ -599,17 +590,47 @@ impl Hypervisor for HypervLinuxDriver { data: Vec, rip: u64, instruction_length: u64, - outb_handle_fn: OutBHandlerWrapper, ) -> Result<()> { let mut padded = [0u8; 4]; let copy_len = data.len().min(4); padded[..copy_len].copy_from_slice(&data[..copy_len]); let val = u32::from_le_bytes(padded); - outb_handle_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .call(port, val)?; + #[cfg(feature = "mem_profile")] + { + // We need to handle the borrow checker issue where we need both: + // - &mut SandboxMemoryManager (from self.mem_mgr) + // - &mut dyn Hypervisor (from self) + // We'll use a temporary approach to extract the mem_mgr temporarily + let mem_mgr_option = self.mem_mgr.take(); + let mut mem_mgr = mem_mgr_option + .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? + .clone(); + + handle_outb(&mut mem_mgr, host_funcs, self, port, val)?; + + // Put the mem_mgr back + self.mem_mgr = Some(mem_mgr); + } + + #[cfg(not(feature = "mem_profile"))] + { + let mem_mgr = self + .mem_mgr + .as_mut() + .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? + .clone(); + + handle_outb(mem_mgr, host_funcs, port, val)?; + } // update rip self.vcpu_fd.set_reg(&[hv_register_assoc { @@ -623,7 +644,10 @@ impl Hypervisor for HypervLinuxDriver { } #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn run(&mut self) -> Result { + fn run( + &mut self, + #[cfg(feature = "trace_guest")] tc: &mut crate::sandbox::trace::TraceContext, + ) -> Result { const HALT_MESSAGE: hv_message_type = hv_message_type_HVMSG_X64_HALT; const IO_PORT_INTERCEPT_MESSAGE: hv_message_type = hv_message_type_HVMSG_X64_IO_PORT_INTERCEPT; @@ -663,10 +687,11 @@ impl Hypervisor for HypervLinuxDriver { .load(Ordering::Relaxed) || debug_interrupt { - Err(MshvError::Errno(vmm_sys_util::errno::Error::new( - libc::EINTR, - ))) + Err(mshv_ioctls::MshvError::from(libc::EINTR)) } else { + #[cfg(feature = "trace_guest")] + tc.setup_guest_trace(Span::current().context()); + // Note: if a `InterruptHandle::kill()` called while this thread is **here** // Then the vcpu will run, but we will keep sending signals to this thread // to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will @@ -708,7 +733,7 @@ impl Hypervisor for HypervLinuxDriver { HyperlightExit::Halt() } IO_PORT_INTERCEPT_MESSAGE => { - let io_message = m.to_ioport_info()?; + let io_message = m.to_ioport_info().map_err(mshv_ioctls::MshvError::from)?; let port_number = io_message.port_number; let rip = io_message.header.rip; let rax = io_message.rax; @@ -722,7 +747,7 @@ impl Hypervisor for HypervLinuxDriver { ) } UNMAPPED_GPA_MESSAGE => { - let mimo_message = m.to_memory_info()?; + let mimo_message = m.to_memory_info().map_err(mshv_ioctls::MshvError::from)?; let addr = mimo_message.guest_physical_address; crate::debug!( "mshv MMIO unmapped GPA -Details: Address: {} \n {:#?}", @@ -732,7 +757,7 @@ impl Hypervisor for HypervLinuxDriver { HyperlightExit::Mmio(addr) } INVALID_GPA_ACCESS_MESSAGE => { - let mimo_message = m.to_memory_info()?; + let mimo_message = m.to_memory_info().map_err(mshv_ioctls::MshvError::from)?; let gpa = mimo_message.guest_physical_address; let access_info = MemoryRegionFlags::try_from(mimo_message)?; crate::debug!( @@ -740,9 +765,9 @@ impl Hypervisor for HypervLinuxDriver { gpa, &self ); - match self.get_memory_access_violation( + match get_memory_access_violation( gpa as usize, - &self.mem_regions, + self.sandbox_regions.iter().chain(self.mmap_regions.iter()), access_info, ) { Some(access_info_violation) => access_info_violation, @@ -757,7 +782,8 @@ impl Hypervisor for HypervLinuxDriver { EXCEPTION_INTERCEPT => { // Extract exception info from the message so we can figure out // more information about the vCPU state - let ex_info = match m.to_exception_info() { + let ex_info = match m.to_exception_info().map_err(mshv_ioctls::MshvError::from) + { Ok(info) => info, Err(e) => { log_then_return!("Error converting to exception info: {:?}", e); @@ -773,6 +799,8 @@ impl Hypervisor for HypervLinuxDriver { } other => { crate::debug!("mshv Other Exit: Exit: {:#?} \n {:#?}", other, &self); + #[cfg(crashdump)] + let _ = crashdump::generate_crashdump(self); log_then_return!("unknown Hyper-V run message type {:?}", other); } }, @@ -815,6 +843,39 @@ impl Hypervisor for HypervLinuxDriver { Ok(result) } + fn regs(&self) -> Result { + let mshv_regs = self.vcpu_fd.get_regs()?; + Ok((&mshv_regs).into()) + } + + fn set_regs(&mut self, regs: &super::regs::CommonRegisters) -> Result<()> { + let mshv_regs: StandardRegisters = regs.into(); + self.vcpu_fd.set_regs(&mshv_regs)?; + Ok(()) + } + + fn fpu(&self) -> Result { + let mshv_fpu = self.vcpu_fd.get_fpu()?; + Ok((&mshv_fpu).into()) + } + + fn set_fpu(&mut self, fpu: &super::regs::CommonFpu) -> Result<()> { + let mshv_fpu: FloatingPointUnit = fpu.into(); + self.vcpu_fd.set_fpu(&mshv_fpu)?; + Ok(()) + } + + fn sregs(&self) -> Result { + let mshv_sregs = self.vcpu_fd.get_sregs()?; + Ok((&mshv_sregs).into()) + } + + fn set_sregs(&mut self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { + let mshv_sregs: SpecialRegisters = sregs.into(); + self.vcpu_fd.set_sregs(&mshv_sregs)?; + Ok(()) + } + #[instrument(skip_all, parent = Span::current(), level = "Trace")] fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { self as &mut dyn Hypervisor @@ -869,8 +930,11 @@ impl Hypervisor for HypervLinuxDriver { .and_then(|name| name.to_os_string().into_string().ok()) }); + // Include both initial sandbox regions and dynamically mapped regions + let mut regions: Vec = self.sandbox_regions.clone(); + regions.extend(self.mmap_regions.iter().cloned()); Ok(Some(crashdump::CrashDumpContext::new( - &self.mem_regions, + regions, regs, xsave.buffer.to_vec(), self.entrypoint, @@ -885,15 +949,18 @@ impl Hypervisor for HypervLinuxDriver { #[cfg(gdb)] fn handle_debug( &mut self, - dbg_mem_access_fn: std::sync::Arc< - std::sync::Mutex, - >, + dbg_mem_access_fn: Arc>>, stop_reason: VcpuStopReason, ) -> Result<()> { if self.debug.is_none() { return Err(new_error!("Debugging is not enabled")); } + let mem_access = DebugMemoryAccess { + dbg_mem_access_fn, + guest_mmap_regions: self.mmap_regions.to_vec(), + }; + match stop_reason { // If the vCPU stopped because of a crash, we need to handle it differently // We do not want to allow resuming execution or placing breakpoints @@ -937,7 +1004,7 @@ impl Hypervisor for HypervLinuxDriver { // For all other requests, we will process them normally _ => { - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + let result = self.process_dbg_request(req, &mem_access); match result { Ok(response) => response, Err(HyperlightError::TranslateGuestAddress(_)) => { @@ -985,7 +1052,7 @@ impl Hypervisor for HypervLinuxDriver { // Wait for a message from gdb let req = self.recv_dbg_msg()?; - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + let result = self.process_dbg_request(req, &mem_access); let response = match result { Ok(response) => response, @@ -1017,13 +1084,37 @@ impl Hypervisor for HypervLinuxDriver { Ok(()) } + + fn check_stack_guard(&self) -> Result { + if let Some(mgr) = self.mem_mgr.as_ref() { + mgr.check_stack_guard() + } else { + Err(new_error!("Memory manager is not initialized")) + } + } + + #[cfg(feature = "trace_guest")] + fn handle_trace(&mut self, tc: &mut crate::sandbox::trace::TraceContext) -> Result<()> { + let regs = self.regs()?; + tc.handle_trace( + ®s, + self.mem_mgr.as_ref().ok_or_else(|| { + new_error!("Memory manager is not initialized before handling trace") + })?, + ) + } + + #[cfg(feature = "mem_profile")] + fn trace_info_mut(&mut self) -> &mut MemTraceInfo { + &mut self.trace_info + } } impl Drop for HypervLinuxDriver { #[instrument(skip_all, parent = Span::current(), level = "Trace")] fn drop(&mut self) { self.interrupt_handle.dropped.store(true, Ordering::Relaxed); - for region in &self.mem_regions { + for region in self.sandbox_regions.iter().chain(self.mmap_regions.iter()) { let mshv_region: mshv_user_mem_region = region.to_owned().into(); match self.vm_fd.unmap_user_memory(mshv_region) { Ok(_) => (), @@ -1036,6 +1127,8 @@ impl Drop for HypervLinuxDriver { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "mem_profile")] + use crate::mem::exe::DummyUnwindInfo; use crate::mem::memory_region::MemoryRegionVecBuilder; use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory}; @@ -1103,6 +1196,8 @@ mod tests { #[cfg(crashdump)] guest_core_dump: true, }, + #[cfg(feature = "mem_profile")] + MemTraceInfo::new(Arc::new(DummyUnwindInfo {})).unwrap(), ) .unwrap(); } diff --git a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs b/src/hyperlight_host/src/hypervisor/hyperv_windows.rs index 0d6d794bc..5c6c9db9c 100644 --- a/src/hyperlight_host/src/hypervisor/hyperv_windows.rs +++ b/src/hyperlight_host/src/hypervisor/hyperv_windows.rs @@ -17,60 +17,55 @@ limitations under the License. use std::fmt; use std::fmt::{Debug, Formatter}; use std::string::String; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; use log::LevelFilter; use tracing::{Span, instrument}; +#[cfg(feature = "trace_guest")] +use tracing_opentelemetry::OpenTelemetrySpanExt; use windows::Win32::System::Hypervisor::{ - WHV_MEMORY_ACCESS_TYPE, WHV_PARTITION_HANDLE, WHV_REGISTER_VALUE, WHV_RUN_VP_EXIT_CONTEXT, - WHV_RUN_VP_EXIT_REASON, WHV_X64_SEGMENT_REGISTER, WHV_X64_SEGMENT_REGISTER_0, - WHvCancelRunVirtualProcessor, WHvX64RegisterCr0, WHvX64RegisterCr3, WHvX64RegisterCr4, - WHvX64RegisterCs, WHvX64RegisterEfer, + WHV_MEMORY_ACCESS_TYPE, WHV_PARTITION_HANDLE, WHV_RUN_VP_EXIT_CONTEXT, WHV_RUN_VP_EXIT_REASON, + WHvCancelRunVirtualProcessor, }; #[cfg(crashdump)] use {super::crashdump, std::path::Path}; #[cfg(gdb)] use { super::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, HypervDebug, VcpuStopReason, + DebugCommChannel, DebugMemoryAccess, DebugMsg, DebugResponse, GuestDebug, HypervDebug, + VcpuStopReason, }, - super::handlers::DbgMemAccessHandlerWrapper, - crate::hypervisor::handlers::DbgMemAccessHandlerCaller, - crate::{HyperlightError, log_then_return}, - std::sync::Mutex, + crate::HyperlightError, }; -use super::fpu::{FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT}; -use super::handlers::{MemAccessHandlerWrapper, OutBHandlerWrapper}; +use super::regs::CommonSpecialRegisters; use super::surrogate_process::SurrogateProcess; use super::surrogate_process_manager::*; use super::windows_hypervisor_platform::{VMPartition, VMProcessor}; -use super::wrappers::{HandleWrapper, WHvFPURegisters}; -#[cfg(feature = "init-paging")] -use super::{ - CR0_AM, CR0_ET, CR0_MP, CR0_NE, CR0_PE, CR0_PG, CR0_WP, CR4_OSFXSR, CR4_OSXMMEXCPT, CR4_PAE, - EFER_LMA, EFER_LME, EFER_NX, EFER_SCE, -}; +use super::wrappers::HandleWrapper; use super::{HyperlightExit, Hypervisor, InterruptHandle, VirtualCPU}; -use crate::hypervisor::fpu::FP_CONTROL_WORD_DEFAULT; -use crate::hypervisor::wrappers::WHvGeneralRegisters; +use crate::hypervisor::get_memory_access_violation; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; +use crate::mem::shared_mem::HostSharedMemory; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::outb::handle_outb; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; #[cfg(crashdump)] use crate::sandbox::uninitialized::SandboxRuntimeConfig; -use crate::{Result, debug, new_error}; +use crate::{Result, debug, log_then_return, new_error}; #[cfg(gdb)] mod debug { - use std::sync::{Arc, Mutex}; - use windows::Win32::System::Hypervisor::WHV_VP_EXCEPTION_CONTEXT; use super::{HypervWindowsDriver, *}; - use crate::hypervisor::gdb::{DebugMsg, DebugResponse, VcpuStopReason, X86_64Regs}; - use crate::hypervisor::handlers::DbgMemAccessHandlerCaller; - use crate::{Result, new_error}; + use crate::Result; + use crate::hypervisor::gdb::{DebugMemoryAccess, DebugMsg, DebugResponse, VcpuStopReason}; impl HypervWindowsDriver { /// Resets the debug information to disable debugging @@ -100,7 +95,7 @@ mod debug { pub(crate) fn process_dbg_request( &mut self, req: DebugMsg, - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> Result { if let Some(debug) = self.debug.as_mut() { match req { @@ -116,7 +111,7 @@ mod debug { )), DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( debug - .add_sw_breakpoint(&self.processor, addr, dbg_mem_access_fn) + .add_sw_breakpoint(&self.processor, addr, mem_access) .map_err(|e| { log::error!("Failed to add sw breakpoint: {:?}", e); @@ -143,17 +138,14 @@ mod debug { Ok(DebugResponse::DisableDebug) } DebugMsg::GetCodeSectionOffset => { - let offset = dbg_mem_access_fn + let offset = mem_access + .dbg_mem_access_fn .try_lock() .map_err(|e| { new_error!("Error locking at {}:{}: {}", file!(), line!(), e) })? - .get_code_offset() - .map_err(|e| { - log::error!("Failed to get code offset: {:?}", e); - - e - })?; + .layout + .get_guest_code_address(); Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) } @@ -161,7 +153,7 @@ mod debug { let mut data = vec![0u8; len]; debug - .read_addrs(&self.processor, addr, &mut data, dbg_mem_access_fn) + .read_addrs(&self.processor, addr, &mut data, mem_access) .map_err(|e| { log::error!("Failed to read from address: {:?}", e); @@ -170,18 +162,14 @@ mod debug { Ok(DebugResponse::ReadAddr(data)) } - DebugMsg::ReadRegisters => { - let mut regs = X86_64Regs::default(); - - debug - .read_regs(&self.processor, &mut regs) - .map_err(|e| { - log::error!("Failed to read registers: {:?}", e); + DebugMsg::ReadRegisters => debug + .read_regs(&self.processor) + .map_err(|e| { + log::error!("Failed to read registers: {:?}", e); - e - }) - .map(|_| DebugResponse::ReadRegisters(regs)) - } + e + }) + .map(|(regs, fpu)| DebugResponse::ReadRegisters(Box::new((regs, fpu)))), DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( debug .remove_hw_breakpoint(&self.processor, addr) @@ -194,7 +182,7 @@ mod debug { )), DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( debug - .remove_sw_breakpoint(&self.processor, addr, dbg_mem_access_fn) + .remove_sw_breakpoint(&self.processor, addr, mem_access) .map_err(|e| { log::error!("Failed to remove sw breakpoint: {:?}", e); @@ -213,7 +201,7 @@ mod debug { } DebugMsg::WriteAddr(addr, data) => { debug - .write_addrs(&self.processor, addr, &data, dbg_mem_access_fn) + .write_addrs(&self.processor, addr, &data, mem_access) .map_err(|e| { log::error!("Failed to write to address: {:?}", e); @@ -222,14 +210,17 @@ mod debug { Ok(DebugResponse::WriteAddr) } - DebugMsg::WriteRegisters(regs) => debug - .write_regs(&self.processor, ®s) - .map_err(|e| { - log::error!("Failed to write registers: {:?}", e); + DebugMsg::WriteRegisters(boxed_regs) => { + let (regs, fpu) = boxed_regs.as_ref(); + debug + .write_regs(&self.processor, regs, fpu) + .map_err(|e| { + log::error!("Failed to write registers: {:?}", e); - e - }) - .map(|_| DebugResponse::WriteRegisters), + e + }) + .map(|_| DebugResponse::WriteRegisters) + } } } else { Err(new_error!("Debugging is not enabled")) @@ -272,25 +263,32 @@ pub(crate) struct HypervWindowsDriver { _surrogate_process: SurrogateProcess, // we need to keep a reference to the SurrogateProcess for the duration of the driver since otherwise it will dropped and the memory mapping will be unmapped and the surrogate process will be returned to the pool entrypoint: u64, orig_rsp: GuestPtr, - mem_regions: Vec, interrupt_handle: Arc, + mem_mgr: Option>, + host_funcs: Option>>, + + sandbox_regions: Vec, // Initially mapped regions when sandbox is created + mmap_regions: Vec, // Later mapped regions + #[cfg(gdb)] debug: Option, #[cfg(gdb)] gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] + trace_info: MemTraceInfo, } -/* This does not automatically impl Send/Sync because the host +/* This does not automatically impl Send because the host * address of the shared memory region is a raw pointer, which are - * marked as !Send and !Sync. However, the access patterns used + * marked as !Send (and !Sync). However, the access patterns used * here are safe. */ unsafe impl Send for HypervWindowsDriver {} -unsafe impl Sync for HypervWindowsDriver {} impl HypervWindowsDriver { #[allow(clippy::too_many_arguments)] + // TODO: refactor this function to take fewer arguments. Add trace_info to rt_cfg #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn new( mem_regions: Vec, @@ -301,6 +299,7 @@ impl HypervWindowsDriver { mmap_file_handle: HandleWrapper, #[cfg(gdb)] gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] trace_info: MemTraceInfo, ) -> Result { // create and setup hypervisor partition let mut partition = VMPartition::new(1)?; @@ -314,8 +313,7 @@ impl HypervWindowsDriver { partition.map_gpa_range(&mem_regions, &surrogate_process)?; - let mut proc = VMProcessor::new(partition)?; - Self::setup_initial_sregs(&mut proc, pml4_address)?; + let proc = VMProcessor::new(partition)?; let partition_handle = proc.get_partition_hdl(); #[cfg(gdb)] @@ -337,80 +335,38 @@ impl HypervWindowsDriver { dropped: AtomicBool::new(false), }); - #[allow(unused_mut)] let mut hv = Self { processor: proc, _surrogate_process: surrogate_process, entrypoint, orig_rsp: GuestPtr::try_from(RawPtr::from(rsp))?, - mem_regions, interrupt_handle: interrupt_handle.clone(), + mem_mgr: None, + host_funcs: None, + sandbox_regions: mem_regions, + mmap_regions: Vec::new(), #[cfg(gdb)] debug, #[cfg(gdb)] gdb_conn, #[cfg(crashdump)] rt_cfg, + #[cfg(feature = "mem_profile")] + trace_info, }; + hv.setup_initial_sregs(pml4_address)?; + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] - hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; + if hv.debug.is_some() { + hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; + } Ok(hv) } - fn setup_initial_sregs(proc: &mut VMProcessor, _pml4_addr: u64) -> Result<()> { - #[cfg(feature = "init-paging")] - proc.set_registers(&[ - (WHvX64RegisterCr3, WHV_REGISTER_VALUE { Reg64: _pml4_addr }), - ( - WHvX64RegisterCr4, - WHV_REGISTER_VALUE { - Reg64: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, - }, - ), - ( - WHvX64RegisterCr0, - WHV_REGISTER_VALUE { - Reg64: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, - }, - ), - ( - WHvX64RegisterEfer, - WHV_REGISTER_VALUE { - Reg64: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, - }, - ), - ( - WHvX64RegisterCs, - WHV_REGISTER_VALUE { - Segment: WHV_X64_SEGMENT_REGISTER { - Anonymous: WHV_X64_SEGMENT_REGISTER_0 { - Attributes: 0b1011 | (1 << 4) | (1 << 7) | (1 << 13), // Type (11: Execute/Read, accessed) | L (64-bit mode) | P (present) | S (code segment) - }, - ..Default::default() // zero out the rest - }, - }, - ), - ])?; - - #[cfg(not(feature = "init-paging"))] - proc.set_registers(&[( - WHvX64RegisterCs, - WHV_REGISTER_VALUE { - Segment: WHV_X64_SEGMENT_REGISTER { - Base: 0, - Selector: 0, - ..Default::default() - }, - }, - )])?; - - Ok(()) - } - #[inline] #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] fn get_exit_details(&self, exit_reason: WHV_RUN_VP_EXIT_REASON) -> Result { @@ -418,7 +374,7 @@ impl HypervWindowsDriver { error.push_str(&format!( "Did not receive a halt from Hypervisor as expected - Received {exit_reason:?}!\n" )); - error.push_str(&format!("Registers: \n{:#?}", self.processor.get_regs()?)); + error.push_str(&format!("Registers: \n{:#?}", self.processor.regs()?)); Ok(error) } } @@ -430,132 +386,22 @@ impl Debug for HypervWindowsDriver { fs.field("Entrypoint", &self.entrypoint) .field("Original RSP", &self.orig_rsp); - for region in &self.mem_regions { - fs.field("Memory Region", ®ion); + for region in &self.sandbox_regions { + fs.field("Sandbox Memory Region", ®ion); + } + for region in &self.mmap_regions { + fs.field("Mapped Memory Region", ®ion); } // Get the registers - - let regs = self.processor.get_regs(); - - if let Ok(regs) = regs { - { - fs.field("Registers", ®s); - } + if let Ok(regs) = self.processor.regs() { + fs.field("Registers", ®s); } // Get the special registers - - let special_regs = self.processor.get_sregs(); - if let Ok(special_regs) = special_regs { - fs.field("CR0", unsafe { &special_regs.cr0.Reg64 }); - fs.field("CR2", unsafe { &special_regs.cr2.Reg64 }); - fs.field("CR3", unsafe { &special_regs.cr3.Reg64 }); - fs.field("CR4", unsafe { &special_regs.cr4.Reg64 }); - fs.field("CR8", unsafe { &special_regs.cr8.Reg64 }); - fs.field("EFER", unsafe { &special_regs.efer.Reg64 }); - fs.field("APIC_BASE", unsafe { &special_regs.apic_base.Reg64 }); - - // Segment registers - fs.field( - "CS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.cs.Segment.Base }, - unsafe { &special_regs.cs.Segment.Limit }, - unsafe { &special_regs.cs.Segment.Selector }, - unsafe { &special_regs.cs.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "DS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.ds.Segment.Base }, - unsafe { &special_regs.ds.Segment.Limit }, - unsafe { &special_regs.ds.Segment.Selector }, - unsafe { &special_regs.ds.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "ES", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.es.Segment.Base }, - unsafe { &special_regs.es.Segment.Limit }, - unsafe { &special_regs.es.Segment.Selector }, - unsafe { &special_regs.es.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "FS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.fs.Segment.Base }, - unsafe { &special_regs.fs.Segment.Limit }, - unsafe { &special_regs.fs.Segment.Selector }, - unsafe { &special_regs.fs.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "GS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.gs.Segment.Base }, - unsafe { &special_regs.gs.Segment.Limit }, - unsafe { &special_regs.gs.Segment.Selector }, - unsafe { &special_regs.gs.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "SS", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.ss.Segment.Base }, - unsafe { &special_regs.ss.Segment.Limit }, - unsafe { &special_regs.ss.Segment.Selector }, - unsafe { &special_regs.ss.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "TR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.tr.Segment.Base }, - unsafe { &special_regs.tr.Segment.Limit }, - unsafe { &special_regs.tr.Segment.Selector }, - unsafe { &special_regs.tr.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "LDTR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Selector: {:?}, Attributes: {:?} }}", - unsafe { &special_regs.ldtr.Segment.Base }, - unsafe { &special_regs.ldtr.Segment.Limit }, - unsafe { &special_regs.ldtr.Segment.Selector }, - unsafe { &special_regs.ldtr.Segment.Anonymous.Attributes } - ), - ); - fs.field( - "GDTR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Pad: {:?} }}", - unsafe { &special_regs.gdtr.Table.Base }, - unsafe { &special_regs.gdtr.Table.Limit }, - unsafe { &special_regs.gdtr.Table.Pad } - ), - ); - fs.field( - "IDTR", - &format_args!( - "{{ Base: {:?}, Limit: {:?}, Pad: {:?} }}", - unsafe { &special_regs.idtr.Table.Base }, - unsafe { &special_regs.idtr.Table.Limit }, - unsafe { &special_regs.idtr.Table.Pad } - ), - ); - }; + if let Ok(special_regs) = self.processor.sregs() { + fs.field("SpecialRegisters", &special_regs); + } fs.finish() } @@ -568,17 +414,20 @@ impl Hypervisor for HypervWindowsDriver { peb_address: RawPtr, seed: u64, page_size: u32, - outb_hdl: OutBHandlerWrapper, - mem_access_hdl: MemAccessHandlerWrapper, + mem_mgr: SandboxMemoryManager, + host_funcs: Arc>, max_guest_log_level: Option, - #[cfg(gdb)] dbg_mem_access_hdl: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_hdl: Arc>>, ) -> Result<()> { + self.mem_mgr = Some(mem_mgr); + self.host_funcs = Some(host_funcs); + let max_guest_log_level: u64 = match max_guest_log_level { Some(level) => level as u64, None => self.get_max_log_level().into(), }; - let regs = WHvGeneralRegisters { + let regs = CommonRegisters { rip: self.entrypoint, rsp: self.orig_rsp.absolute()?, @@ -591,48 +440,49 @@ impl Hypervisor for HypervWindowsDriver { ..Default::default() }; - self.processor.set_general_purpose_registers(®s)?; + self.set_regs(®s)?; VirtualCPU::run( self.as_mut_hypervisor(), - outb_hdl, - mem_access_hdl, #[cfg(gdb)] dbg_mem_access_hdl, - )?; + ) + } - Ok(()) + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + unsafe fn map_region(&mut self, _region: &MemoryRegion) -> Result<()> { + log_then_return!("Mapping host memory into the guest not yet supported on this platform"); + } + + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + unsafe fn unmap_region(&mut self, _region: &MemoryRegion) -> Result<()> { + log_then_return!("Mapping host memory into the guest not yet supported on this platform"); + } + + fn get_mapped_regions(&self) -> Box + '_> { + Box::new(self.mmap_regions.iter()) } #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] fn dispatch_call_from_host( &mut self, dispatch_func_addr: RawPtr, - outb_hdl: OutBHandlerWrapper, - mem_access_hdl: MemAccessHandlerWrapper, - #[cfg(gdb)] dbg_mem_access_hdl: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_hdl: Arc>>, ) -> Result<()> { // Reset general purpose registers, then set RIP and RSP - let regs = WHvGeneralRegisters { + let regs = CommonRegisters { rip: dispatch_func_addr.into(), rsp: self.orig_rsp.absolute()?, rflags: 1 << 1, // eflags bit index 1 is reserved and always needs to be 1 ..Default::default() }; - self.processor.set_general_purpose_registers(®s)?; + self.processor.set_regs(®s)?; // reset fpu state - self.processor.set_fpu(&WHvFPURegisters { - fp_control_word: FP_CONTROL_WORD_DEFAULT, - fp_tag_word: FP_TAG_WORD_DEFAULT, - mxcsr: MXCSR_DEFAULT, - ..Default::default() // zero out the rest - })?; + self.processor.set_fpu(&CommonFpu::default())?; VirtualCPU::run( self.as_mut_hypervisor(), - outb_hdl, - mem_access_hdl, #[cfg(gdb)] dbg_mem_access_hdl, )?; @@ -647,25 +497,58 @@ impl Hypervisor for HypervWindowsDriver { data: Vec, rip: u64, instruction_length: u64, - outb_handle_fn: OutBHandlerWrapper, ) -> Result<()> { let mut padded = [0u8; 4]; let copy_len = data.len().min(4); padded[..copy_len].copy_from_slice(&data[..copy_len]); let val = u32::from_le_bytes(padded); - outb_handle_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .call(port, val)?; + #[cfg(feature = "mem_profile")] + { + // We need to handle the borrow checker issue where we need both: + // - &mut SandboxMemoryManager (from self.mem_mgr.as_mut()) + // - &mut dyn Hypervisor (from self) + // We'll use a temporary approach to extract the mem_mgr temporarily + let mem_mgr_option = self.mem_mgr.take(); + let mut mem_mgr = mem_mgr_option + .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? + .clone(); + + handle_outb(&mut mem_mgr, host_funcs, self, port, val)?; + + // Put the mem_mgr back + self.mem_mgr = Some(mem_mgr); + } - let mut regs = self.processor.get_regs()?; + #[cfg(not(feature = "mem_profile"))] + { + let mem_mgr = self + .mem_mgr + .as_mut() + .ok_or_else(|| new_error!("mem_mgr should be initialized before handling IO"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs should be initialized before handling IO"))? + .clone(); + + handle_outb(mem_mgr, host_funcs, port, val)?; + } + + let mut regs = self.regs()?; regs.rip = rip + instruction_length; - self.processor.set_general_purpose_registers(®s) + self.set_regs(®s) } #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn run(&mut self) -> Result { + fn run( + &mut self, + #[cfg(feature = "trace_guest")] tc: &mut crate::sandbox::trace::TraceContext, + ) -> Result { self.interrupt_handle.running.store(true, Ordering::Relaxed); #[cfg(not(gdb))] @@ -690,6 +573,9 @@ impl Hypervisor for HypervWindowsDriver { Reserved: Default::default(), } } else { + #[cfg(feature = "trace_guest")] + tc.setup_guest_trace(Span::current().context()); + self.processor.run()? }; self.interrupt_handle @@ -749,8 +635,11 @@ impl Hypervisor for HypervWindowsDriver { gpa, access_info, &self ); - match self.get_memory_access_violation(gpa as usize, &self.mem_regions, access_info) - { + match get_memory_access_violation( + gpa as usize, + self.sandbox_regions.iter().chain(self.mmap_regions.iter()), + access_info, + ) { Some(access_info) => access_info, None => HyperlightExit::Mmio(gpa), } @@ -804,6 +693,35 @@ impl Hypervisor for HypervWindowsDriver { Ok(result) } + /// Get regs + #[allow(dead_code)] + fn regs(&self) -> Result { + self.processor.regs() + } + /// Set regs + fn set_regs(&mut self, regs: &CommonRegisters) -> Result<()> { + self.processor.set_regs(regs) + } + /// Get fpu regs + #[allow(dead_code)] + fn fpu(&self) -> Result { + self.processor.fpu() + } + /// Set fpu regs + fn set_fpu(&mut self, fpu: &CommonFpu) -> Result<()> { + self.processor.set_fpu(fpu) + } + /// Get special regs + #[allow(dead_code)] + fn sregs(&self) -> Result { + self.processor.sregs() + } + /// Set special regs + #[allow(dead_code)] + fn set_sregs(&mut self, sregs: &CommonSpecialRegisters) -> Result<()> { + self.processor.set_sregs(sregs) + } + fn interrupt_handle(&self) -> Arc { self.interrupt_handle.clone() } @@ -818,8 +736,8 @@ impl Hypervisor for HypervWindowsDriver { if self.rt_cfg.guest_core_dump { let mut regs = [0; 27]; - let vcpu_regs = self.processor.get_regs()?; - let sregs = self.processor.get_sregs()?; + let vcpu_regs = self.processor.regs()?; + let sregs = self.processor.sregs()?; let xsave = self.processor.get_xsave()?; // Set the registers in the order expected by the crashdump context @@ -840,16 +758,16 @@ impl Hypervisor for HypervWindowsDriver { regs[14] = vcpu_regs.rdi; // rdi regs[15] = 0; // orig rax regs[16] = vcpu_regs.rip; // rip - regs[17] = unsafe { sregs.cs.Segment.Selector } as u64; // cs + regs[17] = sregs.cs.selector as u64; // cs regs[18] = vcpu_regs.rflags; // eflags regs[19] = vcpu_regs.rsp; // rsp - regs[20] = unsafe { sregs.ss.Segment.Selector } as u64; // ss - regs[21] = unsafe { sregs.fs.Segment.Base }; // fs_base - regs[22] = unsafe { sregs.gs.Segment.Base }; // gs_base - regs[23] = unsafe { sregs.ds.Segment.Selector } as u64; // ds - regs[24] = unsafe { sregs.es.Segment.Selector } as u64; // es - regs[25] = unsafe { sregs.fs.Segment.Selector } as u64; // fs - regs[26] = unsafe { sregs.gs.Segment.Selector } as u64; // gs + regs[20] = sregs.ss.selector as u64; // ss + regs[21] = sregs.fs.base; // fs_base + regs[22] = sregs.gs.base; // gs_base + regs[23] = sregs.ds.selector as u64; // ds + regs[24] = sregs.es.selector as u64; // es + regs[25] = sregs.fs.selector as u64; // fs + regs[26] = sregs.gs.selector as u64; // gs // Get the filename from the config let filename = self.rt_cfg.binary_path.clone().and_then(|path| { @@ -858,8 +776,11 @@ impl Hypervisor for HypervWindowsDriver { .and_then(|name| name.to_os_string().into_string().ok()) }); + // Include both initial sandbox regions and dynamically mapped regions + let mut regions: Vec = self.sandbox_regions.clone(); + regions.extend(self.mmap_regions.iter().cloned()); Ok(Some(crashdump::CrashDumpContext::new( - &self.mem_regions, + regions, regs, xsave, self.entrypoint, @@ -874,12 +795,18 @@ impl Hypervisor for HypervWindowsDriver { #[cfg(gdb)] fn handle_debug( &mut self, - dbg_mem_access_fn: Arc>, + dbg_mem_access_fn: Arc>>, stop_reason: super::gdb::VcpuStopReason, ) -> Result<()> { if self.debug.is_none() { return Err(new_error!("Debugging is not enabled")); } + + let mem_access = DebugMemoryAccess { + dbg_mem_access_fn, + guest_mmap_regions: self.mmap_regions.to_vec(), + }; + match stop_reason { // If the vCPU stopped because of a crash, we need to handle it differently // We do not want to allow resuming execution or placing breakpoints @@ -923,7 +850,7 @@ impl Hypervisor for HypervWindowsDriver { // For all other requests, we will process them normally _ => { - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + let result = self.process_dbg_request(req, &mem_access); match result { Ok(response) => response, Err(HyperlightError::TranslateGuestAddress(_)) => { @@ -972,7 +899,7 @@ impl Hypervisor for HypervWindowsDriver { // Wait for a message from gdb let req = self.recv_dbg_msg()?; - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + let result = self.process_dbg_request(req, &mem_access); let response = match result { Ok(response) => response, @@ -1003,6 +930,30 @@ impl Hypervisor for HypervWindowsDriver { Ok(()) } + + fn check_stack_guard(&self) -> Result { + if let Some(mgr) = self.mem_mgr.as_ref() { + mgr.check_stack_guard() + } else { + Err(new_error!("Memory manager is not initialized")) + } + } + + #[cfg(feature = "trace_guest")] + fn handle_trace(&mut self, tc: &mut crate::sandbox::trace::TraceContext) -> Result<()> { + let regs = self.regs()?; + tc.handle_trace( + ®s, + self.mem_mgr.as_ref().ok_or_else(|| { + new_error!("Memory manager is not initialized before handling trace") + })?, + ) + } + + #[cfg(feature = "mem_profile")] + fn trace_info_mut(&mut self) -> &mut MemTraceInfo { + &mut self.trace_info + } } impl Drop for HypervWindowsDriver { diff --git a/src/hyperlight_host/src/hypervisor/kvm.rs b/src/hyperlight_host/src/hypervisor/kvm.rs index d6b348cc0..330a5f5b5 100644 --- a/src/hyperlight_host/src/hypervisor/kvm.rs +++ b/src/hyperlight_host/src/hypervisor/kvm.rs @@ -14,38 +14,39 @@ See the License for the specific language governing permissions and limitations under the License. */ -use std::convert::TryFrom; use std::fmt::Debug; -use std::sync::Arc; -#[cfg(gdb)] -use std::sync::Mutex; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; -use kvm_bindings::{KVM_MEM_READONLY, kvm_fpu, kvm_regs, kvm_userspace_memory_region}; +use kvm_bindings::{kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region}; use kvm_ioctls::Cap::UserMemory; use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use log::LevelFilter; use tracing::{Span, instrument}; +#[cfg(feature = "trace_guest")] +use tracing_opentelemetry::OpenTelemetrySpanExt; #[cfg(crashdump)] use {super::crashdump, std::path::Path}; -use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT}; -#[cfg(gdb)] -use super::gdb::{DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason}; #[cfg(gdb)] -use super::handlers::DbgMemAccessHandlerWrapper; -use super::handlers::{MemAccessHandlerWrapper, OutBHandlerWrapper}; -#[cfg(feature = "init-paging")] -use super::{ - CR0_AM, CR0_ET, CR0_MP, CR0_NE, CR0_PE, CR0_PG, CR0_WP, CR4_OSFXSR, CR4_OSXMMEXCPT, CR4_PAE, - EFER_LMA, EFER_LME, EFER_NX, EFER_SCE, +use super::gdb::{ + DebugCommChannel, DebugMemoryAccess, DebugMsg, DebugResponse, GuestDebug, KvmDebug, + VcpuStopReason, }; use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU}; #[cfg(gdb)] use crate::HyperlightError; +use crate::hypervisor::get_memory_access_violation; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; +use crate::mem::shared_mem::HostSharedMemory; use crate::sandbox::SandboxConfiguration; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::outb::handle_outb; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; #[cfg(crashdump)] use crate::sandbox::uninitialized::SandboxRuntimeConfig; use crate::{Result, log_then_return, new_error}; @@ -74,15 +75,12 @@ pub(crate) fn is_hypervisor_present() -> bool { #[cfg(gdb)] mod debug { - use std::sync::{Arc, Mutex}; - use kvm_bindings::kvm_debug_exit_arch; use super::KVMDriver; use crate::hypervisor::gdb::{ - DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason, X86_64Regs, + DebugMemoryAccess, DebugMsg, DebugResponse, GuestDebug, KvmDebug, VcpuStopReason, }; - use crate::hypervisor::handlers::DbgMemAccessHandlerCaller; use crate::{Result, new_error}; impl KVMDriver { @@ -113,7 +111,7 @@ mod debug { pub(crate) fn process_dbg_request( &mut self, req: DebugMsg, - dbg_mem_access_fn: Arc>, + mem_access: &DebugMemoryAccess, ) -> Result { if let Some(debug) = self.debug.as_mut() { match req { @@ -129,7 +127,7 @@ mod debug { )), DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( debug - .add_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) + .add_sw_breakpoint(&self.vcpu_fd, addr, mem_access) .map_err(|e| { log::error!("Failed to add sw breakpoint: {:?}", e); @@ -156,45 +154,32 @@ mod debug { Ok(DebugResponse::DisableDebug) } DebugMsg::GetCodeSectionOffset => { - let offset = dbg_mem_access_fn + let offset = mem_access + .dbg_mem_access_fn .try_lock() .map_err(|e| { new_error!("Error locking at {}:{}: {}", file!(), line!(), e) })? - .get_code_offset() - .map_err(|e| { - log::error!("Failed to get code offset: {:?}", e); - - e - })?; + .layout + .get_guest_code_address(); Ok(DebugResponse::GetCodeSectionOffset(offset as u64)) } DebugMsg::ReadAddr(addr, len) => { let mut data = vec![0u8; len]; - debug - .read_addrs(&self.vcpu_fd, addr, &mut data, dbg_mem_access_fn) - .map_err(|e| { - log::error!("Failed to read from address: {:?}", e); - - e - })?; + debug.read_addrs(&self.vcpu_fd, addr, &mut data, mem_access)?; Ok(DebugResponse::ReadAddr(data)) } - DebugMsg::ReadRegisters => { - let mut regs = X86_64Regs::default(); - - debug - .read_regs(&self.vcpu_fd, &mut regs) - .map_err(|e| { - log::error!("Failed to read registers: {:?}", e); + DebugMsg::ReadRegisters => debug + .read_regs(&self.vcpu_fd) + .map_err(|e| { + log::error!("Failed to read registers: {:?}", e); - e - }) - .map(|_| DebugResponse::ReadRegisters(regs)) - } + e + }) + .map(|(regs, fpu)| DebugResponse::ReadRegisters(Box::new((regs, fpu)))), DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( debug .remove_hw_breakpoint(&self.vcpu_fd, addr) @@ -207,7 +192,7 @@ mod debug { )), DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( debug - .remove_sw_breakpoint(&self.vcpu_fd, addr, dbg_mem_access_fn) + .remove_sw_breakpoint(&self.vcpu_fd, addr, mem_access) .map_err(|e| { log::error!("Failed to remove sw breakpoint: {:?}", e); @@ -225,24 +210,21 @@ mod debug { Ok(DebugResponse::Step) } DebugMsg::WriteAddr(addr, data) => { + debug.write_addrs(&self.vcpu_fd, addr, &data, mem_access)?; + + Ok(DebugResponse::WriteAddr) + } + DebugMsg::WriteRegisters(boxed_regs) => { + let (regs, fpu) = boxed_regs.as_ref(); debug - .write_addrs(&self.vcpu_fd, addr, &data, dbg_mem_access_fn) + .write_regs(&self.vcpu_fd, regs, fpu) .map_err(|e| { - log::error!("Failed to write to address: {:?}", e); + log::error!("Failed to write registers: {:?}", e); e - })?; - - Ok(DebugResponse::WriteAddr) + }) + .map(|_| DebugResponse::WriteRegisters) } - DebugMsg::WriteRegisters(regs) => debug - .write_regs(&self.vcpu_fd, ®s) - .map_err(|e| { - log::error!("Failed to write registers: {:?}", e); - - e - }) - .map(|_| DebugResponse::WriteRegisters), } } else { Err(new_error!("Debugging is not enabled")) @@ -284,12 +266,19 @@ mod debug { /// A Hypervisor driver for KVM on Linux pub(crate) struct KVMDriver { _kvm: Kvm, - _vm_fd: VmFd, + vm_fd: VmFd, + page_size: usize, vcpu_fd: VcpuFd, entrypoint: u64, orig_rsp: GuestPtr, - mem_regions: Vec, interrupt_handle: Arc, + mem_mgr: Option>, + host_funcs: Option>>, + + sandbox_regions: Vec, // Initially mapped regions when sandbox is created + mmap_regions: Vec<(MemoryRegion, u32)>, // Later mapped regions (region, slot number) + next_slot: u32, // Monotonically increasing slot number + freed_slots: Vec, // Reusable slots from unmapped regions #[cfg(gdb)] debug: Option, @@ -297,12 +286,16 @@ pub(crate) struct KVMDriver { gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] + trace_info: MemTraceInfo, } impl KVMDriver { /// Create a new instance of a `KVMDriver`, with only control registers /// set. Standard registers will not be set, and `initialise` must /// be called to do so. + #[allow(clippy::too_many_arguments)] + // TODO: refactor this function to take fewer arguments. Add trace_info to rt_cfg #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn new( mem_regions: Vec, @@ -312,31 +305,19 @@ impl KVMDriver { config: &SandboxConfiguration, #[cfg(gdb)] gdb_conn: Option>, #[cfg(crashdump)] rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] trace_info: MemTraceInfo, ) -> Result { let kvm = Kvm::new()?; let vm_fd = kvm.create_vm_with_type(0)?; - let perm_flags = - MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE; - mem_regions.iter().enumerate().try_for_each(|(i, region)| { - let perm_flags = perm_flags.intersection(region.flags); - let kvm_region = kvm_userspace_memory_region { - slot: i as u32, - guest_phys_addr: region.guest_region.start as u64, - memory_size: (region.guest_region.end - region.guest_region.start) as u64, - userspace_addr: region.host_region.start as u64, - flags: match perm_flags { - MemoryRegionFlags::READ => KVM_MEM_READONLY, - _ => 0, // normal, RWX - }, - }; + let mut kvm_region: kvm_userspace_memory_region = region.clone().into(); + kvm_region.slot = i as u32; unsafe { vm_fd.set_user_memory_region(kvm_region) } })?; - let mut vcpu_fd = vm_fd.create_vcpu(0)?; - Self::setup_initial_sregs(&mut vcpu_fd, pml4_addr)?; + let vcpu_fd = vm_fd.create_vcpu(0)?; #[cfg(gdb)] let (debug, gdb_conn) = if let Some(gdb_conn) = gdb_conn { @@ -375,49 +356,40 @@ impl KVMDriver { sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(), }); - #[allow(unused_mut)] - let mut hv = Self { + let mut kvm = Self { _kvm: kvm, - _vm_fd: vm_fd, + vm_fd, + page_size: 0, vcpu_fd, entrypoint, orig_rsp: rsp_gp, - mem_regions, + next_slot: mem_regions.len() as u32, + sandbox_regions: mem_regions, + mmap_regions: Vec::new(), + freed_slots: Vec::new(), interrupt_handle: interrupt_handle.clone(), + mem_mgr: None, + host_funcs: None, #[cfg(gdb)] debug, #[cfg(gdb)] gdb_conn, #[cfg(crashdump)] rt_cfg, + #[cfg(feature = "mem_profile")] + trace_info, }; + kvm.setup_initial_sregs(pml4_addr)?; + // Send the interrupt handle to the GDB thread if debugging is enabled // This is used to allow the GDB thread to stop the vCPU #[cfg(gdb)] - hv.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; - - Ok(hv) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn setup_initial_sregs(vcpu_fd: &mut VcpuFd, _pml4_addr: u64) -> Result<()> { - // setup paging and IA-32e (64-bit) mode - let mut sregs = vcpu_fd.get_sregs()?; - cfg_if::cfg_if! { - if #[cfg(feature = "init-paging")] { - sregs.cr3 = _pml4_addr; - sregs.cr4 = CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT; - sregs.cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP; - sregs.efer = EFER_LME | EFER_LMA | EFER_SCE | EFER_NX; - sregs.cs.l = 1; // required for 64-bit mode - } else { - sregs.cs.base = 0; - sregs.cs.selector = 0; - } + if kvm.debug.is_some() { + kvm.send_dbg_msg(DebugResponse::InterruptHandle(interrupt_handle))?; } - vcpu_fd.set_sregs(&sregs)?; - Ok(()) + + Ok(kvm) } } @@ -426,8 +398,11 @@ impl Debug for KVMDriver { let mut f = f.debug_struct("KVM Driver"); // Output each memory region - for region in &self.mem_regions { - f.field("Memory Region", ®ion); + for region in &self.sandbox_regions { + f.field("Sandbox Memory Region", ®ion); + } + for region in &self.mmap_regions { + f.field("Mapped Memory Region", ®ion); } let regs = self.vcpu_fd.get_regs(); // check that regs is OK and then set field in debug struct @@ -456,17 +431,21 @@ impl Hypervisor for KVMDriver { peb_addr: RawPtr, seed: u64, page_size: u32, - outb_hdl: OutBHandlerWrapper, - mem_access_hdl: MemAccessHandlerWrapper, + mem_mgr: SandboxMemoryManager, + host_funcs: Arc>, max_guest_log_level: Option, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()> { + self.mem_mgr = Some(mem_mgr); + self.host_funcs = Some(host_funcs); + self.page_size = page_size as usize; + let max_guest_log_level: u64 = match max_guest_log_level { Some(level) => level as u64, None => self.get_max_log_level().into(), }; - let regs = kvm_regs { + let regs = CommonRegisters { rip: self.entrypoint, rsp: self.orig_rsp.absolute()?, @@ -478,49 +457,94 @@ impl Hypervisor for KVMDriver { ..Default::default() }; - self.vcpu_fd.set_regs(®s)?; + self.set_regs(®s)?; VirtualCPU::run( self.as_mut_hypervisor(), - outb_hdl, - mem_access_hdl, #[cfg(gdb)] dbg_mem_access_fn, - )?; + ) + } + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + unsafe fn map_region(&mut self, region: &MemoryRegion) -> Result<()> { + if [ + region.guest_region.start, + region.guest_region.end, + region.host_region.start, + region.host_region.end, + ] + .iter() + .any(|x| x % self.page_size != 0) + { + log_then_return!( + "region is not page-aligned {:x}, {region:?}", + self.page_size + ); + } + + let mut kvm_region: kvm_userspace_memory_region = region.clone().into(); + + // Try to reuse a freed slot first, otherwise use next_slot + let slot = if let Some(freed_slot) = self.freed_slots.pop() { + freed_slot + } else { + let slot = self.next_slot; + self.next_slot += 1; + slot + }; + + kvm_region.slot = slot; + unsafe { self.vm_fd.set_user_memory_region(kvm_region) }?; + self.mmap_regions.push((region.to_owned(), slot)); Ok(()) } + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + unsafe fn unmap_region(&mut self, region: &MemoryRegion) -> Result<()> { + if let Some(idx) = self.mmap_regions.iter().position(|(r, _)| r == region) { + let (region, slot) = self.mmap_regions.remove(idx); + let mut kvm_region: kvm_userspace_memory_region = region.into(); + kvm_region.slot = slot; + // Setting memory_size to 0 unmaps the slot's region + // From https://docs.kernel.org/virt/kvm/api.html + // > Deleting a slot is done by passing zero for memory_size. + kvm_region.memory_size = 0; + unsafe { self.vm_fd.set_user_memory_region(kvm_region) }?; + + // Add the freed slot to the reuse list + self.freed_slots.push(slot); + + Ok(()) + } else { + Err(new_error!("Tried to unmap region that is not mapped")) + } + } + + fn get_mapped_regions(&self) -> Box + '_> { + Box::new(self.mmap_regions.iter().map(|(region, _)| region)) + } + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] fn dispatch_call_from_host( &mut self, dispatch_func_addr: RawPtr, - outb_handle_fn: OutBHandlerWrapper, - mem_access_fn: MemAccessHandlerWrapper, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()> { // Reset general purpose registers, then set RIP and RSP - let regs = kvm_regs { + let regs = CommonRegisters { rip: dispatch_func_addr.into(), rsp: self.orig_rsp.absolute()?, ..Default::default() }; - self.vcpu_fd.set_regs(®s)?; + self.set_regs(®s)?; // reset fpu state - let fpu = kvm_fpu { - fcw: FP_CONTROL_WORD_DEFAULT, - ftwx: FP_TAG_WORD_DEFAULT, - mxcsr: MXCSR_DEFAULT, - ..Default::default() // zero out the rest - }; - self.vcpu_fd.set_fpu(&fpu)?; + self.set_fpu(&CommonFpu::default())?; // run VirtualCPU::run( self.as_mut_hypervisor(), - outb_handle_fn, - mem_access_fn, #[cfg(gdb)] dbg_mem_access_fn, )?; @@ -535,7 +559,6 @@ impl Hypervisor for KVMDriver { data: Vec, _rip: u64, _instruction_length: u64, - outb_handle_fn: OutBHandlerWrapper, ) -> Result<()> { // KVM does not need RIP or instruction length, as it automatically sets the RIP @@ -550,17 +573,51 @@ impl Hypervisor for KVMDriver { padded[..copy_len].copy_from_slice(&data[..copy_len]); let value = u32::from_le_bytes(padded); - outb_handle_fn - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .call(port, value)?; + #[cfg(feature = "mem_profile")] + { + // We need to handle the borrow checker issue where we need both: + // - &mut SandboxMemoryManager (from self.mem_mgr.as_mut()) + // - &mut dyn Hypervisor (from self) + // We'll use a temporary approach to extract the mem_mgr temporarily + let mem_mgr_option = self.mem_mgr.take(); + let mut mem_mgr = + mem_mgr_option.ok_or_else(|| new_error!("mem_mgr not initialized"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs not initialized"))? + .clone(); + + handle_outb(&mut mem_mgr, host_funcs, self, port, value)?; + + // Put the mem_mgr back + self.mem_mgr = Some(mem_mgr); + } + + #[cfg(not(feature = "mem_profile"))] + { + let mem_mgr = self + .mem_mgr + .as_mut() + .ok_or_else(|| new_error!("mem_mgr not initialized"))?; + let host_funcs = self + .host_funcs + .as_ref() + .ok_or_else(|| new_error!("host_funcs not initialized"))? + .clone(); + + handle_outb(mem_mgr, host_funcs, port, value)?; + } } Ok(()) } #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn run(&mut self) -> Result { + fn run( + &mut self, + #[cfg(feature = "trace_guest")] tc: &mut crate::sandbox::trace::TraceContext, + ) -> Result { self.interrupt_handle .tid .store(unsafe { libc::pthread_self() as u64 }, Ordering::Relaxed); @@ -593,6 +650,9 @@ impl Hypervisor for KVMDriver { { Err(kvm_ioctls::Error::new(libc::EINTR)) } else { + #[cfg(feature = "trace_guest")] + tc.setup_guest_trace(Span::current().context()); + // Note: if a `InterruptHandle::kill()` called while this thread is **here** // Then the vcpu will run, but we will keep sending signals to this thread // to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will @@ -635,9 +695,11 @@ impl Hypervisor for KVMDriver { Ok(VcpuExit::MmioRead(addr, _)) => { crate::debug!("KVM MMIO Read -Details: Address: {} \n {:#?}", addr, &self); - match self.get_memory_access_violation( + match get_memory_access_violation( addr as usize, - &self.mem_regions, + self.sandbox_regions + .iter() + .chain(self.mmap_regions.iter().map(|(r, _)| r)), MemoryRegionFlags::READ, ) { Some(access_violation_exit) => access_violation_exit, @@ -647,9 +709,11 @@ impl Hypervisor for KVMDriver { Ok(VcpuExit::MmioWrite(addr, _)) => { crate::debug!("KVM MMIO Write -Details: Address: {} \n {:#?}", addr, &self); - match self.get_memory_access_violation( + match get_memory_access_violation( addr as usize, - &self.mem_regions, + self.sandbox_regions + .iter() + .chain(self.mmap_regions.iter().map(|(r, _)| r)), MemoryRegionFlags::WRITE, ) { Some(access_violation_exit) => access_violation_exit, @@ -708,6 +772,39 @@ impl Hypervisor for KVMDriver { Ok(result) } + fn regs(&self) -> Result { + let kvm_regs = self.vcpu_fd.get_regs()?; + Ok((&kvm_regs).into()) + } + + fn set_regs(&mut self, regs: &super::regs::CommonRegisters) -> Result<()> { + let kvm_regs: kvm_regs = regs.into(); + self.vcpu_fd.set_regs(&kvm_regs)?; + Ok(()) + } + + fn fpu(&self) -> Result { + let kvm_fpu = self.vcpu_fd.get_fpu()?; + Ok((&kvm_fpu).into()) + } + + fn set_fpu(&mut self, fpu: &super::regs::CommonFpu) -> Result<()> { + let kvm_fpu: kvm_fpu = fpu.into(); + self.vcpu_fd.set_fpu(&kvm_fpu)?; + Ok(()) + } + + fn sregs(&self) -> Result { + let kvm_sregs = self.vcpu_fd.get_sregs()?; + Ok((&kvm_sregs).into()) + } + + fn set_sregs(&mut self, sregs: &super::regs::CommonSpecialRegisters) -> Result<()> { + let kvm_sregs: kvm_sregs = sregs.into(); + self.vcpu_fd.set_sregs(&kvm_sregs)?; + Ok(()) + } + #[instrument(skip_all, parent = Span::current(), level = "Trace")] fn as_mut_hypervisor(&mut self) -> &mut dyn Hypervisor { self as &mut dyn Hypervisor @@ -764,8 +861,11 @@ impl Hypervisor for KVMDriver { // The [`CrashDumpContext`] accepts xsave as a vector of u8, so we need to convert the // xsave region to a vector of u8 + // Also include mapped regions in addition to the initial sandbox regions + let mut regions: Vec = self.sandbox_regions.clone(); + regions.extend(self.mmap_regions.iter().map(|(r, _)| r.clone())); Ok(Some(crashdump::CrashDumpContext::new( - &self.mem_regions, + regions, regs, xsave .region @@ -784,13 +884,18 @@ impl Hypervisor for KVMDriver { #[cfg(gdb)] fn handle_debug( &mut self, - dbg_mem_access_fn: Arc>, + dbg_mem_access_fn: Arc>>, stop_reason: VcpuStopReason, ) -> Result<()> { if self.debug.is_none() { return Err(new_error!("Debugging is not enabled")); } + let mem_access = DebugMemoryAccess { + dbg_mem_access_fn, + guest_mmap_regions: self.mmap_regions.iter().map(|(r, _)| r.clone()).collect(), + }; + match stop_reason { // If the vCPU stopped because of a crash, we need to handle it differently // We do not want to allow resuming execution or placing breakpoints @@ -834,7 +939,7 @@ impl Hypervisor for KVMDriver { // For all other requests, we will process them normally _ => { - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + let result = self.process_dbg_request(req, &mem_access); match result { Ok(response) => response, Err(HyperlightError::TranslateGuestAddress(_)) => { @@ -882,7 +987,7 @@ impl Hypervisor for KVMDriver { // Wait for a message from gdb let req = self.recv_dbg_msg()?; - let result = self.process_dbg_request(req, dbg_mem_access_fn.clone()); + let result = self.process_dbg_request(req, &mem_access); let response = match result { Ok(response) => response, @@ -914,6 +1019,30 @@ impl Hypervisor for KVMDriver { Ok(()) } + + fn check_stack_guard(&self) -> Result { + if let Some(mgr) = self.mem_mgr.as_ref() { + mgr.check_stack_guard() + } else { + Err(new_error!("Memory manager is not initialized")) + } + } + + #[cfg(feature = "trace_guest")] + fn handle_trace(&mut self, tc: &mut crate::sandbox::trace::TraceContext) -> Result<()> { + let regs = self.regs()?; + tc.handle_trace( + ®s, + self.mem_mgr.as_ref().ok_or_else(|| { + new_error!("Memory manager is not initialized before handling trace") + })?, + ) + } + + #[cfg(feature = "mem_profile")] + fn trace_info_mut(&mut self) -> &mut MemTraceInfo { + &mut self.trace_info + } } impl Drop for KVMDriver { diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index 0a31ee468..e5592509a 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -17,16 +17,17 @@ limitations under the License. use log::LevelFilter; use tracing::{Span, instrument}; +use crate::HyperlightError::StackOverflow; use crate::error::HyperlightError::ExecutionCanceledByHost; +use crate::hypervisor::regs::{ + CommonFpu, CommonRegisters, CommonSegmentRegister, CommonSpecialRegisters, +}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::metrics::METRIC_GUEST_CANCELLATION; -use crate::{HyperlightError, Result, log_then_return, new_error}; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; +use crate::{HyperlightError, Result, log_then_return}; -/// Util for handling x87 fpu state -#[cfg(any(kvm, mshv, target_os = "windows"))] -pub mod fpu; -/// Handlers for Hypervisor custom logic -pub mod handlers; /// HyperV-on-linux functionality #[cfg(mshv)] pub mod hyperv_linux; @@ -38,6 +39,9 @@ pub(crate) mod hyperv_windows; #[cfg(gdb)] pub(crate) mod gdb; +/// Abstracts over different hypervisor register representations +pub(crate) mod regs; + #[cfg(kvm)] /// Functionality to manipulate KVM-based virtual machines pub mod kvm; @@ -68,12 +72,10 @@ use std::time::Duration; #[cfg(gdb)] use gdb::VcpuStopReason; -#[cfg(gdb)] -use self::handlers::{DbgMemAccessHandlerCaller, DbgMemAccessHandlerWrapper}; -use self::handlers::{ - MemAccessHandlerCaller, MemAccessHandlerWrapper, OutBHandlerCaller, OutBHandlerWrapper, -}; +use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::RawPtr; +use crate::mem::shared_mem::HostSharedMemory; +use crate::sandbox::host_funcs::FunctionRegistry; cfg_if::cfg_if! { if #[cfg(feature = "init-paging")] { @@ -117,7 +119,7 @@ pub enum HyperlightExit { } /// A common set of hypervisor functionality -pub(crate) trait Hypervisor: Debug + Sync + Send { +pub(crate) trait Hypervisor: Debug + Send { /// Initialise the internally stored vCPU with the given PEB address and /// random number seed, then run it until a HLT instruction. #[allow(clippy::too_many_arguments)] @@ -126,12 +128,26 @@ pub(crate) trait Hypervisor: Debug + Sync + Send { peb_addr: RawPtr, seed: u64, page_size: u32, - outb_handle_fn: OutBHandlerWrapper, - mem_access_fn: MemAccessHandlerWrapper, + mem_mgr: SandboxMemoryManager, + host_funcs: Arc>, guest_max_log_level: Option, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()>; + /// Map a region of host memory into the sandbox. + /// + /// Depending on the host platform, there are likely alignment + /// requirements of at least one page for base and len. + unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()>; + + /// Unmap a memory region from the sandbox + unsafe fn unmap_region(&mut self, rgn: &MemoryRegion) -> Result<()>; + + /// Get the currently mapped dynamic memory regions (not including sandbox regions) + /// + /// Note: Box needed for trait to be object-safe :( + fn get_mapped_regions(&self) -> Box + '_>; + /// Dispatch a call from the host to the guest using the given pointer /// to the dispatch function _in the guest's address space_. /// @@ -142,9 +158,7 @@ pub(crate) trait Hypervisor: Debug + Sync + Send { fn dispatch_call_from_host( &mut self, dispatch_func_addr: RawPtr, - outb_handle_fn: OutBHandlerWrapper, - mem_access_fn: MemAccessHandlerWrapper, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()>; /// Handle an IO exit from the internally stored vCPU. @@ -154,42 +168,98 @@ pub(crate) trait Hypervisor: Debug + Sync + Send { data: Vec, rip: u64, instruction_length: u64, - outb_handle_fn: OutBHandlerWrapper, ) -> Result<()>; /// Run the vCPU - fn run(&mut self) -> Result; - - /// Returns a Some(HyperlightExit::AccessViolation(..)) if the given gpa doesn't have - /// access its corresponding region. Returns None otherwise, or if the region is not found. - fn get_memory_access_violation( - &self, - gpa: usize, - mem_regions: &[MemoryRegion], - access_info: MemoryRegionFlags, - ) -> Option { - // find the region containing the given gpa - let region = mem_regions - .iter() - .find(|region| region.guest_region.contains(&gpa)); - - if let Some(region) = region { - if !region.flags.contains(access_info) - || region.flags.contains(MemoryRegionFlags::STACK_GUARD) - { - return Some(HyperlightExit::AccessViolation( - gpa as u64, - access_info, - region.flags, - )); - } - } - None - } + fn run( + &mut self, + #[cfg(feature = "trace_guest")] tc: &mut crate::sandbox::trace::TraceContext, + ) -> Result; /// Get InterruptHandle to underlying VM fn interrupt_handle(&self) -> Arc; + /// Get regs + #[allow(dead_code)] + fn regs(&self) -> Result; + /// Set regs + #[allow(dead_code)] + fn set_regs(&mut self, regs: &CommonRegisters) -> Result<()>; + /// Get fpu regs + #[allow(dead_code)] + fn fpu(&self) -> Result; + /// Set fpu regs + #[allow(dead_code)] + fn set_fpu(&mut self, fpu: &CommonFpu) -> Result<()>; + /// Get special regs + #[allow(dead_code)] + fn sregs(&self) -> Result; + /// Set special regs + #[allow(dead_code)] + fn set_sregs(&mut self, sregs: &CommonSpecialRegisters) -> Result<()>; + + /// Setup initial special registers for the hypervisor + /// This is a default implementation that works for all hypervisors + fn setup_initial_sregs(&mut self, _pml4_addr: u64) -> Result<()> { + #[cfg(feature = "init-paging")] + let sregs = CommonSpecialRegisters { + cr0: CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_AM | CR0_PG | CR0_WP, + cr4: CR4_PAE | CR4_OSFXSR | CR4_OSXMMEXCPT, + cr3: _pml4_addr, + efer: EFER_LME | EFER_LMA | EFER_SCE | EFER_NX, + cs: CommonSegmentRegister { + type_: 11, + present: 1, + s: 1, + l: 1, + ..Default::default() + }, + tr: CommonSegmentRegister { + limit: 65535, + type_: 11, + present: 1, + s: 0, + ..Default::default() + }, + ..Default::default() + }; + + #[cfg(not(feature = "init-paging"))] + let sregs = CommonSpecialRegisters { + cs: CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 11, + present: 1, + s: 1, + ..Default::default() + }, + ds: CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 3, + present: 1, + s: 1, + ..Default::default() + }, + tr: CommonSegmentRegister { + base: 0, + selector: 0, + limit: 0xFFFF, + type_: 11, + present: 1, + s: 0, + ..Default::default() + }, + ..Default::default() + }; + + self.set_sregs(&sregs)?; + Ok(()) + } + /// Get the logging level to pass to the guest entrypoint fn get_max_log_level(&self) -> u32 { // Check to see if the RUST_LOG environment variable is set @@ -237,11 +307,44 @@ pub(crate) trait Hypervisor: Debug + Sync + Send { /// handles the cases when the vCPU stops due to a Debug event fn handle_debug( &mut self, - _dbg_mem_access_fn: Arc>, + _dbg_mem_access_fn: Arc>>, _stop_reason: VcpuStopReason, ) -> Result<()> { unimplemented!() } + + /// Check stack guard to see if the stack is still valid + fn check_stack_guard(&self) -> Result; + + #[cfg(feature = "trace_guest")] + fn handle_trace(&mut self, tc: &mut crate::sandbox::trace::TraceContext) -> Result<()>; + + /// Get a mutable reference of the trace info for the guest + #[cfg(feature = "mem_profile")] + fn trace_info_mut(&mut self) -> &mut MemTraceInfo; +} + +/// Returns a Some(HyperlightExit::AccessViolation(..)) if the given gpa doesn't have +/// access its corresponding region. Returns None otherwise, or if the region is not found. +pub(crate) fn get_memory_access_violation<'a>( + gpa: usize, + mut mem_regions: impl Iterator, + access_info: MemoryRegionFlags, +) -> Option { + // find the region containing the given gpa + let region = mem_regions.find(|region| region.guest_region.contains(&gpa)); + + if let Some(region) = region + && (!region.flags.contains(access_info) + || region.flags.contains(MemoryRegionFlags::STACK_GUARD)) + { + return Some(HyperlightExit::AccessViolation( + gpa as u64, + access_info, + region.flags, + )); + } + None } /// A virtual CPU that can be run until an exit occurs @@ -250,14 +353,35 @@ pub struct VirtualCPU {} impl VirtualCPU { /// Run the given hypervisor until a halt instruction is reached #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - pub fn run( + pub(crate) fn run( hv: &mut dyn Hypervisor, - outb_handle_fn: Arc>, - mem_access_fn: Arc>, - #[cfg(gdb)] dbg_mem_access_fn: Arc>, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> Result<()> { + // Keeps the trace context and open spans + #[cfg(feature = "trace_guest")] + let mut tc = crate::sandbox::trace::TraceContext::new(); + loop { - match hv.run() { + #[cfg(feature = "trace_guest")] + let result = { + let result = hv.run(&mut tc); + // End current host trace by closing the current span that captures traces + // happening when a guest exits and re-enters. + tc.end_host_trace(); + + // Handle the guest trace data if any + if let Err(e) = hv.handle_trace(&mut tc) { + // If no trace data is available, we just log a message and continue + // Is this the right thing to do? + log::debug!("Error handling guest trace: {:?}", e); + } + + result + }; + #[cfg(not(feature = "trace_guest"))] + let result = hv.run(); + + match result { #[cfg(gdb)] Ok(HyperlightExit::Debug(stop_reason)) => { if let Err(e) = hv.handle_debug(dbg_mem_access_fn.clone(), stop_reason) { @@ -269,17 +393,15 @@ impl VirtualCPU { break; } Ok(HyperlightExit::IoOut(port, data, rip, instruction_length)) => { - hv.handle_io(port, data, rip, instruction_length, outb_handle_fn.clone())? + hv.handle_io(port, data, rip, instruction_length)? } Ok(HyperlightExit::Mmio(addr)) => { #[cfg(crashdump)] crashdump::generate_crashdump(hv)?; - mem_access_fn - .clone() - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .call()?; + if !hv.check_stack_guard()? { + log_then_return!(StackOverflow()); + } log_then_return!("MMIO access address {:#x}", addr); } @@ -341,8 +463,8 @@ pub trait InterruptHandle: Debug + Send + Sync { /// /// - If this is called while the vcpu is running, then it will interrupt the vcpu and return `true`. /// - If this is called while the vcpu is not running, (for example during a host call), the - /// vcpu will not immediately be interrupted, but will prevent the vcpu from running **the next time** - /// it's scheduled, and returns `false`. + /// vcpu will not immediately be interrupted, but will prevent the vcpu from running **the next time** + /// it's scheduled, and returns `false`. /// /// # Note /// This function will block for the duration of the time it takes for the vcpu thread to be interrupted. @@ -352,8 +474,8 @@ pub trait InterruptHandle: Debug + Send + Sync { /// /// - If this is called while the vcpu is running, then it will interrupt the vcpu and return `true`. /// - If this is called while the vcpu is not running, (for example during a host call), the - /// vcpu will not immediately be interrupted, but will prevent the vcpu from running **the next time** - /// it's scheduled, and returns `false`. + /// vcpu will not immediately be interrupted, but will prevent the vcpu from running **the next time** + /// it's scheduled, and returns `false`. /// /// # Note /// This function will block for the duration of the time it takes for the vcpu thread to be interrupted. @@ -375,7 +497,7 @@ pub(super) struct LinuxInterruptHandle { /// 1. The VCPU is running (generation N), /// 2. It gets cancelled, /// 3. Then quickly restarted (generation N+1), - /// before the original thread has observed that it was cancelled. + /// before the original thread has observed that it was cancelled. /// /// Without this generation counter, the interrupt logic might assume the VCPU is still /// in the *original* run (generation N), see that it's `running`, and re-send the signal. @@ -391,9 +513,9 @@ pub(super) struct LinuxInterruptHandle { /// `kill()` is called, and cleared when the vcpu is no longer running. /// This is used to /// 1. make sure stale signals do not interrupt the - /// the wrong vcpu (a vcpu may only be interrupted iff `cancel_requested` is true), + /// the wrong vcpu (a vcpu may only be interrupted iff `cancel_requested` is true), /// 2. ensure that if a vm is killed while a host call is running, - /// the vm will not re-enter the guest after the host call returns. + /// the vm will not re-enter the guest after the host call returns. cancel_requested: AtomicBool, /// True when the debugger has requested the VM to be interrupted. Set immediately when /// `kill_from_debugger()` is called, and cleared when the vcpu is no longer running. @@ -494,10 +616,6 @@ pub(crate) mod tests { use hyperlight_testing::dummy_guest_as_string; - use super::handlers::{MemAccessHandler, OutBHandler}; - #[cfg(gdb)] - use crate::hypervisor::DbgMemAccessHandlerCaller; - use crate::mem::ptr::RawPtr; use crate::sandbox::uninitialized::GuestBinary; #[cfg(any(crashdump, gdb))] use crate::sandbox::uninitialized::SandboxRuntimeConfig; @@ -505,41 +623,14 @@ pub(crate) mod tests { use crate::sandbox::{SandboxConfiguration, UninitializedSandbox}; use crate::{Result, is_hypervisor_present, new_error}; - #[cfg(gdb)] - struct DbgMemAccessHandler {} - - #[cfg(gdb)] - impl DbgMemAccessHandlerCaller for DbgMemAccessHandler { - fn read(&mut self, _offset: usize, _data: &mut [u8]) -> Result<()> { - Ok(()) - } - - fn write(&mut self, _offset: usize, _data: &[u8]) -> Result<()> { - Ok(()) - } - - fn get_code_offset(&mut self) -> Result { - Ok(0) - } - } - #[test] fn test_initialise() -> Result<()> { if !is_hypervisor_present() { return Ok(()); } - let outb_handler: Arc> = { - let func: Box Result<()> + Send> = - Box::new(|_, _| -> Result<()> { Ok(()) }); - Arc::new(Mutex::new(OutBHandler::from(func))) - }; - let mem_access_handler = { - let func: Box Result<()> + Send> = Box::new(|| -> Result<()> { Ok(()) }); - Arc::new(Mutex::new(MemAccessHandler::from(func))) - }; - #[cfg(gdb)] - let dbg_mem_access_handler = Arc::new(Mutex::new(DbgMemAccessHandler {})); + use crate::mem::ptr::RawPtr; + use crate::sandbox::host_funcs::FunctionRegistry; let filename = dummy_guest_as_string().map_err(|e| new_error!("{}", e))?; @@ -548,22 +639,37 @@ pub(crate) mod tests { let rt_cfg: SandboxRuntimeConfig = Default::default(); let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(filename.clone()), Some(config))?; - let (_hshm, mut gshm) = sandbox.mgr.build(); + let (mem_mgr, mut gshm) = sandbox.mgr.build(); let mut vm = set_up_hypervisor_partition( &mut gshm, &config, #[cfg(any(crashdump, gdb))] &rt_cfg, + sandbox.load_info, )?; + + // Set up required parameters for initialise + let peb_addr = RawPtr::from(0x1000u64); // Dummy PEB address + let seed = 12345u64; // Random seed + let page_size = 4096u32; // Standard page size + let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default())); + let guest_max_log_level = Some(log::LevelFilter::Error); + + #[cfg(gdb)] + let dbg_mem_access_fn = Arc::new(Mutex::new(mem_mgr.clone())); + + // Test the initialise method vm.initialise( - RawPtr::from(0x230000), - 1234567890, - 4096, - outb_handler, - mem_access_handler, - None, + peb_addr, + seed, + page_size, + mem_mgr, + host_funcs, + guest_max_log_level, #[cfg(gdb)] - dbg_mem_access_handler, - ) + dbg_mem_access_fn, + )?; + + Ok(()) } } diff --git a/src/hyperlight_host/src/hypervisor/fpu.rs b/src/hyperlight_host/src/hypervisor/regs.rs similarity index 56% rename from src/hyperlight_host/src/hypervisor/fpu.rs rename to src/hyperlight_host/src/hypervisor/regs.rs index f0b6cb6a2..d29edf4bf 100644 --- a/src/hyperlight_host/src/hypervisor/fpu.rs +++ b/src/hyperlight_host/src/hypervisor/regs.rs @@ -14,6 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -pub(crate) const FP_CONTROL_WORD_DEFAULT: u16 = 0x37f; // mask all fp-exception, set rounding to nearest, set precision to 64-bit -pub(crate) const FP_TAG_WORD_DEFAULT: u8 = 0xff; // each 8 of x87 fpu registers is empty -pub(crate) const MXCSR_DEFAULT: u32 = 0x1f80; // mask simd fp-exceptions, clear exception flags, set rounding to nearest, disable flush-to-zero mode, disable denormals-are-zero mode +mod fpu; +mod special_regs; +mod standard_regs; + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +pub(crate) use fpu::*; +pub(crate) use special_regs::*; +pub(crate) use standard_regs::*; + +#[cfg(target_os = "windows")] +#[derive(Debug, PartialEq)] +pub(crate) enum FromWhpRegisterError { + MissingRegister(HashSet), + InvalidLength(usize), + InvalidEncoding, + DuplicateRegister(i32), + InvalidRegister(i32), +} diff --git a/src/hyperlight_host/src/hypervisor/regs/fpu.rs b/src/hyperlight_host/src/hypervisor/regs/fpu.rs new file mode 100644 index 000000000..0ccd080ab --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/fpu.rs @@ -0,0 +1,438 @@ +/* +Copyright 2024 The Hyperlight Authors. + +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. +*/ +#[cfg(mshv2)] +extern crate mshv_bindings2 as mshv_bindings; +#[cfg(mshv2)] +extern crate mshv_ioctls2 as mshv_ioctls; + +#[cfg(mshv3)] +extern crate mshv_bindings3 as mshv_bindings; +#[cfg(mshv3)] +extern crate mshv_ioctls3 as mshv_ioctls; + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +#[cfg(kvm)] +use kvm_bindings::kvm_fpu; +#[cfg(mshv)] +use mshv_bindings::FloatingPointUnit; + +#[cfg(target_os = "windows")] +use super::Align16; +#[cfg(target_os = "windows")] +use crate::hypervisor::regs::FromWhpRegisterError; + +pub(crate) const FP_CONTROL_WORD_DEFAULT: u16 = 0x37f; // mask all fp-exception, set rounding to nearest, set precision to 64-bit +pub(crate) const MXCSR_DEFAULT: u32 = 0x1f80; // mask simd fp-exceptions, clear exception flags, set rounding to nearest, disable flush-to-zero mode, disable denormals-are-zero mode + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct CommonFpu { + pub fpr: [[u8; 16]; 8], + pub fcw: u16, + pub fsw: u16, + pub ftwx: u8, + pub pad1: u8, + pub last_opcode: u16, + pub last_ip: u64, + pub last_dp: u64, + pub xmm: [[u8; 16]; 16], + pub mxcsr: u32, + pub pad2: u32, +} + +impl Default for CommonFpu { + fn default() -> Self { + Self { + fpr: [[0u8; 16]; 8], + fcw: FP_CONTROL_WORD_DEFAULT, + fsw: 0, + ftwx: 0, + pad1: 0, + last_opcode: 0, + last_ip: 0, + last_dp: 0, + xmm: [[0u8; 16]; 16], + mxcsr: MXCSR_DEFAULT, + pad2: 0, + } + } +} + +#[cfg(kvm)] +impl From<&CommonFpu> for kvm_fpu { + fn from(common_fpu: &CommonFpu) -> Self { + kvm_fpu { + fpr: common_fpu.fpr, + fcw: common_fpu.fcw, + fsw: common_fpu.fsw, + ftwx: common_fpu.ftwx, + pad1: common_fpu.pad1, + last_opcode: common_fpu.last_opcode, + last_ip: common_fpu.last_ip, + last_dp: common_fpu.last_dp, + xmm: common_fpu.xmm, + mxcsr: common_fpu.mxcsr, + pad2: common_fpu.pad2, + } + } +} + +#[cfg(mshv)] +impl From<&CommonFpu> for FloatingPointUnit { + fn from(common_fpu: &CommonFpu) -> FloatingPointUnit { + FloatingPointUnit { + fpr: common_fpu.fpr, + fcw: common_fpu.fcw, + fsw: common_fpu.fsw, + ftwx: common_fpu.ftwx, + pad1: common_fpu.pad1, + last_opcode: common_fpu.last_opcode, + last_ip: common_fpu.last_ip, + last_dp: common_fpu.last_dp, + xmm: common_fpu.xmm, + mxcsr: common_fpu.mxcsr, + pad2: common_fpu.pad2, + } + } +} + +#[cfg(kvm)] +impl From<&kvm_fpu> for CommonFpu { + fn from(kvm_fpu: &kvm_fpu) -> Self { + Self { + fpr: kvm_fpu.fpr, + fcw: kvm_fpu.fcw, + fsw: kvm_fpu.fsw, + ftwx: kvm_fpu.ftwx, + pad1: kvm_fpu.pad1, + last_opcode: kvm_fpu.last_opcode, + last_ip: kvm_fpu.last_ip, + last_dp: kvm_fpu.last_dp, + xmm: kvm_fpu.xmm, + mxcsr: kvm_fpu.mxcsr, + pad2: kvm_fpu.pad2, + } + } +} + +#[cfg(mshv)] +impl From<&FloatingPointUnit> for CommonFpu { + fn from(mshv_fpu: &FloatingPointUnit) -> Self { + Self { + fpr: mshv_fpu.fpr, + fcw: mshv_fpu.fcw, + fsw: mshv_fpu.fsw, + ftwx: mshv_fpu.ftwx, + pad1: mshv_fpu.pad1, + last_opcode: mshv_fpu.last_opcode, + last_ip: mshv_fpu.last_ip, + last_dp: mshv_fpu.last_dp, + xmm: mshv_fpu.xmm, + mxcsr: mshv_fpu.mxcsr, + pad2: mshv_fpu.pad2, + } + } +} + +#[cfg(target_os = "windows")] +use windows::Win32::System::Hypervisor::*; + +#[cfg(target_os = "windows")] +impl From<&CommonFpu> for [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] { + fn from(fpu: &CommonFpu) -> Self { + let mut regs: [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] = + [Default::default(); WHP_FPU_NAMES_LEN]; + let mut idx = 0; + + // FPU/MMX registers (8 x 128-bit) + for (i, reg) in fpu.fpr.iter().enumerate() { + let mut value = WHV_REGISTER_VALUE::default(); + value.Reg128 = WHV_UINT128 { + Dword: [ + u32::from_le_bytes([reg[0], reg[1], reg[2], reg[3]]), + u32::from_le_bytes([reg[4], reg[5], reg[6], reg[7]]), + u32::from_le_bytes([reg[8], reg[9], reg[10], reg[11]]), + u32::from_le_bytes([reg[12], reg[13], reg[14], reg[15]]), + ], + }; + regs[idx] = ( + WHV_REGISTER_NAME(WHvX64RegisterFpMmx0.0 + i as i32), + Align16(value), + ); + idx += 1; + } + + // FpControlStatus + let mut fp_control_status = WHV_REGISTER_VALUE::default(); + fp_control_status.FpControlStatus = WHV_X64_FP_CONTROL_STATUS_REGISTER { + Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0 { + FpControl: fpu.fcw, + FpStatus: fpu.fsw, + FpTag: fpu.ftwx, + Reserved: fpu.pad1, + LastFpOp: fpu.last_opcode, + Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0_0 { + LastFpRip: fpu.last_ip, + }, + }, + }; + regs[idx] = (WHvX64RegisterFpControlStatus, Align16(fp_control_status)); + idx += 1; + + // XMM registers (16 x 128-bit) + for (i, reg) in fpu.xmm.iter().enumerate() { + let mut value = WHV_REGISTER_VALUE::default(); + value.Reg128 = WHV_UINT128 { + Dword: [ + u32::from_le_bytes([reg[0], reg[1], reg[2], reg[3]]), + u32::from_le_bytes([reg[4], reg[5], reg[6], reg[7]]), + u32::from_le_bytes([reg[8], reg[9], reg[10], reg[11]]), + u32::from_le_bytes([reg[12], reg[13], reg[14], reg[15]]), + ], + }; + regs[idx] = ( + WHV_REGISTER_NAME(WHvX64RegisterXmm0.0 + i as i32), + Align16(value), + ); + idx += 1; + } + + // XmmControlStatus + let mut xmm_control_status = WHV_REGISTER_VALUE::default(); + xmm_control_status.XmmControlStatus = WHV_X64_XMM_CONTROL_STATUS_REGISTER { + Anonymous: WHV_X64_XMM_CONTROL_STATUS_REGISTER_0 { + XmmStatusControl: fpu.mxcsr, + XmmStatusControlMask: !0, + Anonymous: WHV_X64_XMM_CONTROL_STATUS_REGISTER_0_0 { + LastFpRdp: fpu.last_dp, + }, + }, + }; + regs[idx] = (WHvX64RegisterXmmControlStatus, Align16(xmm_control_status)); + + regs + } +} + +#[cfg(target_os = "windows")] +pub(crate) const WHP_FPU_NAMES_LEN: usize = 26; +#[cfg(target_os = "windows")] +pub(crate) const WHP_FPU_NAMES: [WHV_REGISTER_NAME; WHP_FPU_NAMES_LEN] = [ + WHvX64RegisterFpMmx0, + WHvX64RegisterFpMmx1, + WHvX64RegisterFpMmx2, + WHvX64RegisterFpMmx3, + WHvX64RegisterFpMmx4, + WHvX64RegisterFpMmx5, + WHvX64RegisterFpMmx6, + WHvX64RegisterFpMmx7, + WHvX64RegisterFpControlStatus, + WHvX64RegisterXmm0, + WHvX64RegisterXmm1, + WHvX64RegisterXmm2, + WHvX64RegisterXmm3, + WHvX64RegisterXmm4, + WHvX64RegisterXmm5, + WHvX64RegisterXmm6, + WHvX64RegisterXmm7, + WHvX64RegisterXmm8, + WHvX64RegisterXmm9, + WHvX64RegisterXmm10, + WHvX64RegisterXmm11, + WHvX64RegisterXmm12, + WHvX64RegisterXmm13, + WHvX64RegisterXmm14, + WHvX64RegisterXmm15, + WHvX64RegisterXmmControlStatus, +]; + +#[cfg(target_os = "windows")] +impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonFpu { + type Error = FromWhpRegisterError; + + fn try_from( + regs: &[(WHV_REGISTER_NAME, Align16)], + ) -> Result { + if regs.len() != WHP_FPU_NAMES_LEN { + return Err(FromWhpRegisterError::InvalidLength(regs.len())); + } + + let mut fpu = CommonFpu::default(); + let mut seen_registers = HashSet::new(); + + for (name, value) in regs { + let name_id = name.0; + + // Check for duplicates + if !seen_registers.insert(name_id) { + return Err(FromWhpRegisterError::DuplicateRegister(name_id)); + } + + match name_id { + id if (WHvX64RegisterFpMmx0.0..WHvX64RegisterFpMmx0.0 + 8).contains(&id) => { + let idx = (id - WHvX64RegisterFpMmx0.0) as usize; + let dwords = unsafe { value.0.Reg128.Dword }; + fpu.fpr[idx] = [ + dwords[0].to_le_bytes(), + dwords[1].to_le_bytes(), + dwords[2].to_le_bytes(), + dwords[3].to_le_bytes(), + ] + .concat() + .try_into() + .map_err(|_| FromWhpRegisterError::InvalidEncoding)?; + } + + id if id == WHvX64RegisterFpControlStatus.0 => { + let control = unsafe { value.0.FpControlStatus.Anonymous }; + fpu.fcw = control.FpControl; + fpu.fsw = control.FpStatus; + fpu.ftwx = control.FpTag; + fpu.pad1 = control.Reserved; + fpu.last_opcode = control.LastFpOp; + fpu.last_ip = unsafe { control.Anonymous.LastFpRip }; + } + + id if (WHvX64RegisterXmm0.0..WHvX64RegisterXmm0.0 + 16).contains(&id) => { + let idx = (id - WHvX64RegisterXmm0.0) as usize; + let dwords = unsafe { value.0.Reg128.Dword }; + fpu.xmm[idx] = [ + dwords[0].to_le_bytes(), + dwords[1].to_le_bytes(), + dwords[2].to_le_bytes(), + dwords[3].to_le_bytes(), + ] + .concat() + .try_into() + .map_err(|_| FromWhpRegisterError::InvalidEncoding)?; + } + + id if id == WHvX64RegisterXmmControlStatus.0 => { + let control = unsafe { value.0.XmmControlStatus.Anonymous }; + fpu.mxcsr = control.XmmStatusControl; + fpu.last_dp = unsafe { control.Anonymous.LastFpRdp }; + } + + _ => { + return Err(FromWhpRegisterError::InvalidRegister(name_id)); + } + } + } + + // Set of all expected register names + let expected_registers: HashSet = WHP_FPU_NAMES.iter().map(|reg| reg.0).collect(); + + // Technically it should not be possible to have any missing registers at this point + // since we are guaranteed to have WHP_FPU_NAMES_LEN non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway + let missing: HashSet = expected_registers + .difference(&seen_registers) + .cloned() + .collect(); + + if !missing.is_empty() { + return Err(FromWhpRegisterError::MissingRegister(missing)); + } + + Ok(fpu) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_common_fpu() -> CommonFpu { + CommonFpu { + fpr: [ + [1u8; 16], [2u8; 16], [3u8; 16], [4u8; 16], [5u8; 16], [6u8; 16], [7u8; 16], + [8u8; 16], + ], + fcw: 0x1234, + fsw: 0x5678, + ftwx: 0x9a, + pad1: 0xbc, + last_opcode: 0xdef0, + last_ip: 0xdeadbeefcafebabe, + last_dp: 0xabad1deaf00dbabe, + xmm: [ + [8u8; 16], [9u8; 16], [10u8; 16], [11u8; 16], [12u8; 16], [13u8; 16], [14u8; 16], + [15u8; 16], [16u8; 16], [17u8; 16], [18u8; 16], [19u8; 16], [20u8; 16], [21u8; 16], + [22u8; 16], [23u8; 16], + ], + mxcsr: 0x1f80, + pad2: 0, + } + } + + #[cfg(kvm)] + #[test] + fn round_trip_kvm_fpu() { + use kvm_bindings::kvm_fpu; + + let original = sample_common_fpu(); + let kvm: kvm_fpu = (&original).into(); + let round_tripped = CommonFpu::from(&kvm); + + assert_eq!(original, round_tripped); + } + + #[cfg(mshv)] + #[test] + fn round_trip_mshv_fpu() { + use mshv_bindings::FloatingPointUnit; + + let original = sample_common_fpu(); + let mshv: FloatingPointUnit = (&original).into(); + let round_tripped = CommonFpu::from(&mshv); + + assert_eq!(original, round_tripped); + } + + #[cfg(target_os = "windows")] + #[test] + fn round_trip_windows_fpu() { + use windows::Win32::System::Hypervisor::*; + + let original = sample_common_fpu(); + let windows: [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] = + (&original).into(); + let round_tripped = CommonFpu::try_from(windows.as_ref()).unwrap(); + assert_eq!(original, round_tripped); + + // test for duplicate register error handling + let original = sample_common_fpu(); + let mut windows: [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] = + (&original).into(); + windows[0].0 = WHvX64RegisterFpMmx1; + let err = CommonFpu::try_from(windows.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::DuplicateRegister(WHvX64RegisterFpMmx1.0) + ); + + // test for passing non-fpu register (e.g. RAX) + let original = sample_common_fpu(); + let mut windows: [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] = + (&original).into(); + windows[0] = (WHvX64RegisterRax, windows[0].1); + let err = CommonFpu::try_from(windows.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::InvalidRegister(WHvX64RegisterRax.0) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs new file mode 100644 index 000000000..b20291d16 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/special_regs.rs @@ -0,0 +1,667 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +#[cfg(mshv2)] +extern crate mshv_bindings2 as mshv_bindings; +#[cfg(mshv2)] +extern crate mshv_ioctls2 as mshv_ioctls; + +#[cfg(mshv3)] +extern crate mshv_bindings3 as mshv_bindings; +#[cfg(mshv3)] +extern crate mshv_ioctls3 as mshv_ioctls; + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +#[cfg(kvm)] +use kvm_bindings::{kvm_dtable, kvm_segment, kvm_sregs}; +#[cfg(mshv)] +use mshv_bindings::{SegmentRegister, SpecialRegisters, TableRegister}; +#[cfg(target_os = "windows")] +use windows::Win32::System::Hypervisor::*; + +#[cfg(target_os = "windows")] +use super::FromWhpRegisterError; + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSpecialRegisters { + pub cs: CommonSegmentRegister, + pub ds: CommonSegmentRegister, + pub es: CommonSegmentRegister, + pub fs: CommonSegmentRegister, + pub gs: CommonSegmentRegister, + pub ss: CommonSegmentRegister, + pub tr: CommonSegmentRegister, + pub ldt: CommonSegmentRegister, + pub gdt: CommonTableRegister, + pub idt: CommonTableRegister, + pub cr0: u64, + pub cr2: u64, + pub cr3: u64, + pub cr4: u64, + pub cr8: u64, + pub efer: u64, + pub apic_base: u64, + pub interrupt_bitmap: [u64; 4], +} + +#[cfg(mshv)] +impl From<&SpecialRegisters> for CommonSpecialRegisters { + fn from(value: &SpecialRegisters) -> Self { + CommonSpecialRegisters { + cs: value.cs.into(), + ds: value.ds.into(), + es: value.es.into(), + fs: value.fs.into(), + gs: value.gs.into(), + ss: value.ss.into(), + tr: value.tr.into(), + ldt: value.ldt.into(), + gdt: value.gdt.into(), + idt: value.idt.into(), + cr0: value.cr0, + cr2: value.cr2, + cr3: value.cr3, + cr4: value.cr4, + cr8: value.cr8, + efer: value.efer, + apic_base: value.apic_base, + interrupt_bitmap: value.interrupt_bitmap, + } + } +} + +#[cfg(mshv)] +impl From<&CommonSpecialRegisters> for SpecialRegisters { + fn from(other: &CommonSpecialRegisters) -> Self { + SpecialRegisters { + cs: other.cs.into(), + ds: other.ds.into(), + es: other.es.into(), + fs: other.fs.into(), + gs: other.gs.into(), + ss: other.ss.into(), + tr: other.tr.into(), + ldt: other.ldt.into(), + gdt: other.gdt.into(), + idt: other.idt.into(), + cr0: other.cr0, + cr2: other.cr2, + cr3: other.cr3, + cr4: other.cr4, + cr8: other.cr8, + efer: other.efer, + apic_base: other.apic_base, + interrupt_bitmap: other.interrupt_bitmap, + } + } +} + +#[cfg(kvm)] +impl From<&kvm_sregs> for CommonSpecialRegisters { + fn from(kvm_sregs: &kvm_sregs) -> Self { + CommonSpecialRegisters { + cs: kvm_sregs.cs.into(), + ds: kvm_sregs.ds.into(), + es: kvm_sregs.es.into(), + fs: kvm_sregs.fs.into(), + gs: kvm_sregs.gs.into(), + ss: kvm_sregs.ss.into(), + tr: kvm_sregs.tr.into(), + ldt: kvm_sregs.ldt.into(), + gdt: kvm_sregs.gdt.into(), + idt: kvm_sregs.idt.into(), + cr0: kvm_sregs.cr0, + cr2: kvm_sregs.cr2, + cr3: kvm_sregs.cr3, + cr4: kvm_sregs.cr4, + cr8: kvm_sregs.cr8, + efer: kvm_sregs.efer, + apic_base: kvm_sregs.apic_base, + interrupt_bitmap: kvm_sregs.interrupt_bitmap, + } + } +} + +#[cfg(kvm)] +impl From<&CommonSpecialRegisters> for kvm_sregs { + fn from(common_sregs: &CommonSpecialRegisters) -> Self { + kvm_sregs { + cs: common_sregs.cs.into(), + ds: common_sregs.ds.into(), + es: common_sregs.es.into(), + fs: common_sregs.fs.into(), + gs: common_sregs.gs.into(), + ss: common_sregs.ss.into(), + tr: common_sregs.tr.into(), + ldt: common_sregs.ldt.into(), + gdt: common_sregs.gdt.into(), + idt: common_sregs.idt.into(), + cr0: common_sregs.cr0, + cr2: common_sregs.cr2, + cr3: common_sregs.cr3, + cr4: common_sregs.cr4, + cr8: common_sregs.cr8, + efer: common_sregs.efer, + apic_base: common_sregs.apic_base, + interrupt_bitmap: common_sregs.interrupt_bitmap, + } + } +} + +/// WHV_REGISTER_VALUE must be 16-byte aligned, but the rust struct is incorrectly generated +/// as 8-byte aligned. This is a workaround to ensure that the struct is 16-byte aligned. +#[cfg(target_os = "windows")] +#[repr(C, align(16))] +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct Align16(pub(crate) T); + +#[cfg(target_os = "windows")] +#[allow(clippy::disallowed_macros)] // compile time +const _: () = { + assert!( + std::mem::size_of::>() + == std::mem::size_of::() + ); +}; + +#[cfg(target_os = "windows")] +pub(crate) const WHP_SREGS_NAMES_LEN: usize = 17; +#[cfg(target_os = "windows")] +pub(crate) static WHP_SREGS_NAMES: [WHV_REGISTER_NAME; WHP_SREGS_NAMES_LEN] = [ + WHvX64RegisterCs, + WHvX64RegisterDs, + WHvX64RegisterEs, + WHvX64RegisterFs, + WHvX64RegisterGs, + WHvX64RegisterSs, + WHvX64RegisterTr, + WHvX64RegisterLdtr, + WHvX64RegisterGdtr, + WHvX64RegisterIdtr, + WHvX64RegisterCr0, + WHvX64RegisterCr2, + WHvX64RegisterCr3, + WHvX64RegisterCr4, + WHvX64RegisterCr8, + WHvX64RegisterEfer, + WHvX64RegisterApicBase, +]; + +#[cfg(target_os = "windows")] +impl From<&CommonSpecialRegisters> + for [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] +{ + fn from(other: &CommonSpecialRegisters) -> Self { + [ + (WHvX64RegisterCs, Align16(other.cs.into())), + (WHvX64RegisterDs, Align16(other.ds.into())), + (WHvX64RegisterEs, Align16(other.es.into())), + (WHvX64RegisterFs, Align16(other.fs.into())), + (WHvX64RegisterGs, Align16(other.gs.into())), + (WHvX64RegisterSs, Align16(other.ss.into())), + (WHvX64RegisterTr, Align16(other.tr.into())), + (WHvX64RegisterLdtr, Align16(other.ldt.into())), + (WHvX64RegisterGdtr, Align16(other.gdt.into())), + (WHvX64RegisterIdtr, Align16(other.idt.into())), + ( + WHvX64RegisterCr0, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr0 }), + ), + ( + WHvX64RegisterCr2, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr2 }), + ), + ( + WHvX64RegisterCr3, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr3 }), + ), + ( + WHvX64RegisterCr4, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr4 }), + ), + ( + WHvX64RegisterCr8, + Align16(WHV_REGISTER_VALUE { Reg64: other.cr8 }), + ), + ( + WHvX64RegisterEfer, + Align16(WHV_REGISTER_VALUE { Reg64: other.efer }), + ), + ( + WHvX64RegisterApicBase, + Align16(WHV_REGISTER_VALUE { + Reg64: other.apic_base, + }), + ), + ] + } +} + +#[cfg(target_os = "windows")] +impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonSpecialRegisters { + type Error = FromWhpRegisterError; + + #[expect( + non_upper_case_globals, + reason = "Windows API has lowercase register names" + )] + fn try_from( + regs: &[(WHV_REGISTER_NAME, Align16)], + ) -> Result { + if regs.len() != WHP_SREGS_NAMES_LEN { + return Err(FromWhpRegisterError::InvalidLength(regs.len())); + } + let mut registers = CommonSpecialRegisters::default(); + let mut seen_registers = HashSet::new(); + + for &(name, ref value) in regs { + let name_id = name.0; + + // Check for duplicates + if !seen_registers.insert(name_id) { + return Err(FromWhpRegisterError::DuplicateRegister(name_id)); + } + + unsafe { + match name { + WHvX64RegisterCs => registers.cs = value.0.into(), + WHvX64RegisterDs => registers.ds = value.0.into(), + WHvX64RegisterEs => registers.es = value.0.into(), + WHvX64RegisterFs => registers.fs = value.0.into(), + WHvX64RegisterGs => registers.gs = value.0.into(), + WHvX64RegisterSs => registers.ss = value.0.into(), + WHvX64RegisterTr => registers.tr = value.0.into(), + WHvX64RegisterLdtr => registers.ldt = value.0.into(), + WHvX64RegisterGdtr => registers.gdt = value.0.into(), + WHvX64RegisterIdtr => registers.idt = value.0.into(), + WHvX64RegisterCr0 => registers.cr0 = value.0.Reg64, + WHvX64RegisterCr2 => registers.cr2 = value.0.Reg64, + WHvX64RegisterCr3 => registers.cr3 = value.0.Reg64, + WHvX64RegisterCr4 => registers.cr4 = value.0.Reg64, + WHvX64RegisterCr8 => registers.cr8 = value.0.Reg64, + WHvX64RegisterEfer => registers.efer = value.0.Reg64, + WHvX64RegisterApicBase => registers.apic_base = value.0.Reg64, + _ => { + // Given unexpected register + return Err(FromWhpRegisterError::InvalidRegister(name_id)); + } + } + } + } + + // TODO: I'm not sure how to get this from WHP at the moment + registers.interrupt_bitmap = Default::default(); + + // Set of all expected register names + let expected_registers: HashSet = + WHP_SREGS_NAMES.map(|name| name.0).into_iter().collect(); + + // Technically it should not be possible to have any missing registers at this point + // since we are guaranteed to have WHP_SREGS_NAMES_LEN (17) non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway + let missing: HashSet<_> = expected_registers + .difference(&seen_registers) + .cloned() + .collect(); + + if !missing.is_empty() { + return Err(FromWhpRegisterError::MissingRegister(missing)); + } + + Ok(registers) + } +} + +// --- Segment Register --- + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSegmentRegister { + pub base: u64, + pub limit: u32, + pub selector: u16, + pub type_: u8, + pub present: u8, + pub dpl: u8, + pub db: u8, + pub s: u8, + pub l: u8, + pub g: u8, + pub avl: u8, + pub unusable: u8, + pub padding: u8, +} + +#[cfg(mshv)] +impl From for CommonSegmentRegister { + fn from(other: SegmentRegister) -> Self { + CommonSegmentRegister { + base: other.base, + limit: other.limit, + selector: other.selector, + type_: other.type_, + present: other.present, + dpl: other.dpl, + db: other.db, + s: other.s, + l: other.l, + g: other.g, + avl: other.avl, + unusable: other.unusable, + padding: other.padding, + } + } +} + +#[cfg(mshv)] +impl From for SegmentRegister { + fn from(other: CommonSegmentRegister) -> Self { + SegmentRegister { + base: other.base, + limit: other.limit, + selector: other.selector, + type_: other.type_, + present: other.present, + dpl: other.dpl, + db: other.db, + s: other.s, + l: other.l, + g: other.g, + avl: other.avl, + unusable: other.unusable, + padding: other.padding, + } + } +} + +#[cfg(kvm)] +impl From for CommonSegmentRegister { + fn from(kvm_segment: kvm_segment) -> Self { + CommonSegmentRegister { + base: kvm_segment.base, + limit: kvm_segment.limit, + selector: kvm_segment.selector, + type_: kvm_segment.type_, + present: kvm_segment.present, + dpl: kvm_segment.dpl, + db: kvm_segment.db, + s: kvm_segment.s, + l: kvm_segment.l, + g: kvm_segment.g, + avl: kvm_segment.avl, + unusable: kvm_segment.unusable, + padding: kvm_segment.padding, + } + } +} + +#[cfg(kvm)] +impl From for kvm_segment { + fn from(common_segment: CommonSegmentRegister) -> Self { + kvm_segment { + base: common_segment.base, + limit: common_segment.limit, + selector: common_segment.selector, + type_: common_segment.type_, + present: common_segment.present, + dpl: common_segment.dpl, + db: common_segment.db, + s: common_segment.s, + l: common_segment.l, + g: common_segment.g, + avl: common_segment.avl, + unusable: common_segment.unusable, + padding: common_segment.padding, + } + } +} + +#[cfg(target_os = "windows")] +impl From for CommonSegmentRegister { + fn from(other: WHV_REGISTER_VALUE) -> Self { + unsafe { + let segment = other.Segment; + let bits = segment.Anonymous.Attributes; + + // Source of bit layout: https://learn.microsoft.com/en-us/virtualization/api/hypervisor-platform/funcs/whvvirtualprocessordatatypes + CommonSegmentRegister { + base: segment.Base, + limit: segment.Limit, + selector: segment.Selector, + type_: (bits & 0b1111) as u8, // bits 0–3: SegmentType + s: ((bits >> 4) & 0b1) as u8, // bit 4: NonSystemSegment + dpl: ((bits >> 5) & 0b11) as u8, // bits 5–6: DPL + present: ((bits >> 7) & 0b1) as u8, // bit 7: Present + // bits 8–11: Reserved + avl: ((bits >> 12) & 0b1) as u8, // bit 12: Available + l: ((bits >> 13) & 0b1) as u8, // bit 13: Long mode + db: ((bits >> 14) & 0b1) as u8, // bit 14: Default + g: ((bits >> 15) & 0b1) as u8, // bit 15: Granularity + unusable: 0, + padding: 0, + } + } + } +} + +#[cfg(target_os = "windows")] +impl From for WHV_REGISTER_VALUE { + fn from(other: CommonSegmentRegister) -> Self { + // Truncate each field to its valid bit width before composing `Attributes`. + let type_ = other.type_ & 0xF; // 4 bits + let s = other.s & 0x1; // 1 bit + let dpl = other.dpl & 0x3; // 2 bits + let present = other.present & 0x1; // 1 bit + let avl = other.avl & 0x1; // 1 bit + let l = other.l & 0x1; // 1 bit + let db = other.db & 0x1; // 1 bit + let g = other.g & 0x1; // 1 bit + + WHV_REGISTER_VALUE { + Segment: WHV_X64_SEGMENT_REGISTER { + Base: other.base, + Limit: other.limit, + Selector: other.selector, + Anonymous: WHV_X64_SEGMENT_REGISTER_0 { + Attributes: (type_ as u16) // bit 0-3 + | ((s as u16) << 4) // bit 4 + | ((dpl as u16) << 5) // bit 5-6 + | ((present as u16) << 7) // bit 7 + | ((avl as u16) << 12) // bit 12 + | ((l as u16) << 13) // bit 13 + | ((db as u16) << 14) // bit 14 + | ((g as u16) << 15), // bit 15 + }, + }, + } + } +} + +// --- Table Register --- + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonTableRegister { + pub base: u64, + pub limit: u16, +} + +#[cfg(mshv)] +impl From for CommonTableRegister { + fn from(other: TableRegister) -> Self { + CommonTableRegister { + base: other.base, + limit: other.limit, + } + } +} + +#[cfg(mshv)] +impl From for TableRegister { + fn from(other: CommonTableRegister) -> Self { + TableRegister { + base: other.base, + limit: other.limit, + } + } +} + +#[cfg(kvm)] +impl From for CommonTableRegister { + fn from(kvm_dtable: kvm_dtable) -> Self { + CommonTableRegister { + base: kvm_dtable.base, + limit: kvm_dtable.limit, + } + } +} + +#[cfg(kvm)] +impl From for kvm_dtable { + fn from(common_dtable: CommonTableRegister) -> Self { + kvm_dtable { + base: common_dtable.base, + limit: common_dtable.limit, + padding: Default::default(), + } + } +} + +#[cfg(target_os = "windows")] +impl From for CommonTableRegister { + fn from(other: WHV_REGISTER_VALUE) -> Self { + unsafe { + let table = other.Table; + CommonTableRegister { + base: table.Base, + limit: table.Limit, + } + } + } +} + +#[cfg(target_os = "windows")] +impl From for WHV_REGISTER_VALUE { + fn from(other: CommonTableRegister) -> Self { + WHV_REGISTER_VALUE { + Table: WHV_X64_TABLE_REGISTER { + Base: other.base, + Limit: other.limit, + Pad: Default::default(), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_common_special_registers() -> CommonSpecialRegisters { + let sample_segment = CommonSegmentRegister { + base: 0x1000, + limit: 0xFFFF, + selector: 0x10, + type_: 0xB, + present: 1, + dpl: 0, + db: 1, + s: 1, + l: 0, + g: 1, + avl: 0, + unusable: 0, + padding: 0, + }; + + let sample_table = CommonTableRegister { + base: 0x2000, + limit: 0x1000, + }; + + CommonSpecialRegisters { + cs: sample_segment, + ds: sample_segment, + es: sample_segment, + fs: sample_segment, + gs: sample_segment, + ss: sample_segment, + tr: sample_segment, + ldt: sample_segment, + gdt: sample_table, + idt: sample_table, + cr0: 0xDEAD_BEEF, + cr2: 0xBAD_C0DE, + cr3: 0xC0FFEE, + cr4: 0xFACE_CAFE, + cr8: 0x1234, + efer: 0x5678, + apic_base: 0x9ABC, + interrupt_bitmap: [0; 4], + } + } + + #[cfg(kvm)] + #[test] + fn round_trip_kvm_sregs() { + let original = sample_common_special_registers(); + let kvm_sregs: kvm_sregs = (&original).into(); + let roundtrip = CommonSpecialRegisters::from(&kvm_sregs); + + assert_eq!(original, roundtrip); + } + + #[cfg(mshv)] + #[test] + fn round_trip_mshv_sregs() { + let original = sample_common_special_registers(); + let mshv_sregs: SpecialRegisters = (&original).into(); + let roundtrip = CommonSpecialRegisters::from(&mshv_sregs); + + assert_eq!(original, roundtrip); + } + + #[cfg(target_os = "windows")] + #[test] + fn round_trip_whp_sregs() { + let original = sample_common_special_registers(); + let whp_sregs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + (&original).into(); + let roundtrip = CommonSpecialRegisters::try_from(whp_sregs.as_ref()).unwrap(); + assert_eq!(original, roundtrip); + + // Test duplicate register error + let original = sample_common_special_registers(); + let mut whp_sregs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + (&original).into(); + whp_sregs[0].0 = WHvX64RegisterDs; + let err = CommonSpecialRegisters::try_from(whp_sregs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::DuplicateRegister(WHvX64RegisterDs.0) + ); + + // Test passing non-sregs register (e.g. RIP) + let original = sample_common_special_registers(); + let mut whp_sregs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + (&original).into(); + whp_sregs[0].0 = WHvX64RegisterRip; + let err = CommonSpecialRegisters::try_from(whp_sregs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::InvalidRegister(WHvX64RegisterRip.0) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs b/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs new file mode 100644 index 000000000..cc39b3247 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs @@ -0,0 +1,423 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +#[cfg(mshv2)] +extern crate mshv_bindings2 as mshv_bindings; +#[cfg(mshv2)] +extern crate mshv_ioctls2 as mshv_ioctls; + +#[cfg(mshv3)] +extern crate mshv_bindings3 as mshv_bindings; +#[cfg(mshv3)] +extern crate mshv_ioctls3 as mshv_ioctls; + +#[cfg(kvm)] +use kvm_bindings::kvm_regs; +#[cfg(mshv)] +use mshv_bindings::StandardRegisters; + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonRegisters { + pub rax: u64, + pub rbx: u64, + pub rcx: u64, + pub rdx: u64, + pub rsi: u64, + pub rdi: u64, + pub rsp: u64, + pub rbp: u64, + pub r8: u64, + pub r9: u64, + pub r10: u64, + pub r11: u64, + pub r12: u64, + pub r13: u64, + pub r14: u64, + pub r15: u64, + pub rip: u64, + pub rflags: u64, +} + +// --- KVM --- +#[cfg(kvm)] +impl From<&kvm_regs> for CommonRegisters { + fn from(kvm_regs: &kvm_regs) -> Self { + CommonRegisters { + rax: kvm_regs.rax, + rbx: kvm_regs.rbx, + rcx: kvm_regs.rcx, + rdx: kvm_regs.rdx, + rsi: kvm_regs.rsi, + rdi: kvm_regs.rdi, + rsp: kvm_regs.rsp, + rbp: kvm_regs.rbp, + r8: kvm_regs.r8, + r9: kvm_regs.r9, + r10: kvm_regs.r10, + r11: kvm_regs.r11, + r12: kvm_regs.r12, + r13: kvm_regs.r13, + r14: kvm_regs.r14, + r15: kvm_regs.r15, + rip: kvm_regs.rip, + rflags: kvm_regs.rflags, + } + } +} + +#[cfg(kvm)] +impl From<&CommonRegisters> for kvm_regs { + fn from(regs: &CommonRegisters) -> Self { + kvm_regs { + rax: regs.rax, + rbx: regs.rbx, + rcx: regs.rcx, + rdx: regs.rdx, + rsi: regs.rsi, + rdi: regs.rdi, + rsp: regs.rsp, + rbp: regs.rbp, + r8: regs.r8, + r9: regs.r9, + r10: regs.r10, + r11: regs.r11, + r12: regs.r12, + r13: regs.r13, + r14: regs.r14, + r15: regs.r15, + rip: regs.rip, + rflags: regs.rflags, + } + } +} + +// --- MSHV --- + +#[cfg(mshv)] +impl From<&StandardRegisters> for CommonRegisters { + fn from(mshv_regs: &StandardRegisters) -> Self { + CommonRegisters { + rax: mshv_regs.rax, + rbx: mshv_regs.rbx, + rcx: mshv_regs.rcx, + rdx: mshv_regs.rdx, + rsi: mshv_regs.rsi, + rdi: mshv_regs.rdi, + rsp: mshv_regs.rsp, + rbp: mshv_regs.rbp, + r8: mshv_regs.r8, + r9: mshv_regs.r9, + r10: mshv_regs.r10, + r11: mshv_regs.r11, + r12: mshv_regs.r12, + r13: mshv_regs.r13, + r14: mshv_regs.r14, + r15: mshv_regs.r15, + rip: mshv_regs.rip, + rflags: mshv_regs.rflags, + } + } +} + +#[cfg(mshv)] +impl From<&CommonRegisters> for StandardRegisters { + fn from(regs: &CommonRegisters) -> Self { + StandardRegisters { + rax: regs.rax, + rbx: regs.rbx, + rcx: regs.rcx, + rdx: regs.rdx, + rsi: regs.rsi, + rdi: regs.rdi, + rsp: regs.rsp, + rbp: regs.rbp, + r8: regs.r8, + r9: regs.r9, + r10: regs.r10, + r11: regs.r11, + r12: regs.r12, + r13: regs.r13, + r14: regs.r14, + r15: regs.r15, + rip: regs.rip, + rflags: regs.rflags, + } + } +} + +#[cfg(target_os = "windows")] +use windows::Win32::System::Hypervisor::*; + +#[cfg(target_os = "windows")] +impl From<&CommonRegisters> + for [(WHV_REGISTER_NAME, Align16); WHP_REGS_NAMES_LEN] +{ + fn from(regs: &CommonRegisters) -> Self { + [ + ( + WHvX64RegisterRax, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rax }), + ), + ( + WHvX64RegisterRbx, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rbx }), + ), + ( + WHvX64RegisterRcx, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rcx }), + ), + ( + WHvX64RegisterRdx, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rdx }), + ), + ( + WHvX64RegisterRsi, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rsi }), + ), + ( + WHvX64RegisterRdi, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rdi }), + ), + ( + WHvX64RegisterRsp, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rsp }), + ), + ( + WHvX64RegisterRbp, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rbp }), + ), + ( + WHvX64RegisterR8, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r8 }), + ), + ( + WHvX64RegisterR9, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r9 }), + ), + ( + WHvX64RegisterR10, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r10 }), + ), + ( + WHvX64RegisterR11, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r11 }), + ), + ( + WHvX64RegisterR12, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r12 }), + ), + ( + WHvX64RegisterR13, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r13 }), + ), + ( + WHvX64RegisterR14, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r14 }), + ), + ( + WHvX64RegisterR15, + Align16(WHV_REGISTER_VALUE { Reg64: regs.r15 }), + ), + ( + WHvX64RegisterRip, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rip }), + ), + ( + WHvX64RegisterRflags, + Align16(WHV_REGISTER_VALUE { Reg64: regs.rflags }), + ), + ] + } +} + +#[cfg(target_os = "windows")] +use std::collections::HashSet; + +#[cfg(target_os = "windows")] +use super::{Align16, FromWhpRegisterError}; + +#[cfg(target_os = "windows")] +pub(crate) const WHP_REGS_NAMES_LEN: usize = 18; +#[cfg(target_os = "windows")] +pub(crate) const WHP_REGS_NAMES: [WHV_REGISTER_NAME; WHP_REGS_NAMES_LEN] = [ + WHvX64RegisterRax, + WHvX64RegisterRbx, + WHvX64RegisterRcx, + WHvX64RegisterRdx, + WHvX64RegisterRsi, + WHvX64RegisterRdi, + WHvX64RegisterRsp, + WHvX64RegisterRbp, + WHvX64RegisterR8, + WHvX64RegisterR9, + WHvX64RegisterR10, + WHvX64RegisterR11, + WHvX64RegisterR12, + WHvX64RegisterR13, + WHvX64RegisterR14, + WHvX64RegisterR15, + WHvX64RegisterRip, + WHvX64RegisterRflags, +]; + +#[cfg(target_os = "windows")] +impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonRegisters { + type Error = FromWhpRegisterError; + + #[expect( + non_upper_case_globals, + reason = "Windows API has lowercase register names" + )] + fn try_from( + regs: &[(WHV_REGISTER_NAME, Align16)], + ) -> Result { + if regs.len() != WHP_REGS_NAMES_LEN { + return Err(FromWhpRegisterError::InvalidLength(regs.len())); + } + let mut registers = CommonRegisters::default(); + let mut seen_registers = HashSet::new(); + + for &(name, value) in regs { + let name_id = name.0; + + // Check for duplicates + if !seen_registers.insert(name_id) { + return Err(FromWhpRegisterError::DuplicateRegister(name_id)); + } + + unsafe { + match name { + WHvX64RegisterRax => registers.rax = value.0.Reg64, + WHvX64RegisterRbx => registers.rbx = value.0.Reg64, + WHvX64RegisterRcx => registers.rcx = value.0.Reg64, + WHvX64RegisterRdx => registers.rdx = value.0.Reg64, + WHvX64RegisterRsi => registers.rsi = value.0.Reg64, + WHvX64RegisterRdi => registers.rdi = value.0.Reg64, + WHvX64RegisterRsp => registers.rsp = value.0.Reg64, + WHvX64RegisterRbp => registers.rbp = value.0.Reg64, + WHvX64RegisterR8 => registers.r8 = value.0.Reg64, + WHvX64RegisterR9 => registers.r9 = value.0.Reg64, + WHvX64RegisterR10 => registers.r10 = value.0.Reg64, + WHvX64RegisterR11 => registers.r11 = value.0.Reg64, + WHvX64RegisterR12 => registers.r12 = value.0.Reg64, + WHvX64RegisterR13 => registers.r13 = value.0.Reg64, + WHvX64RegisterR14 => registers.r14 = value.0.Reg64, + WHvX64RegisterR15 => registers.r15 = value.0.Reg64, + WHvX64RegisterRip => registers.rip = value.0.Reg64, + WHvX64RegisterRflags => registers.rflags = value.0.Reg64, + _ => { + // Given unexpected register + return Err(FromWhpRegisterError::InvalidRegister(name_id)); + } + } + } + } + + // Set of all expected register names + let expected_registers: HashSet = + WHP_REGS_NAMES.map(|name| name.0).into_iter().collect(); + + // Technically it should not be possible to have any missing registers at this point + // since we are guaranteed to have WHP_REGS_NAMES_LEN (18) non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway + let missing: HashSet<_> = expected_registers + .difference(&seen_registers) + .cloned() + .collect(); + + if !missing.is_empty() { + return Err(FromWhpRegisterError::MissingRegister(missing)); + } + + Ok(registers) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn common_regs() -> CommonRegisters { + CommonRegisters { + rax: 1, + rbx: 2, + rcx: 3, + rdx: 4, + rsi: 5, + rdi: 6, + rsp: 7, + rbp: 8, + r8: 9, + r9: 10, + r10: 11, + r11: 12, + r12: 13, + r13: 14, + r14: 15, + r15: 16, + rip: 17, + rflags: 18, + } + } + #[cfg(kvm)] + #[test] + fn round_trip_kvm_regs() { + let original = common_regs(); + let kvm_regs: kvm_regs = (&original).into(); + let converted: CommonRegisters = (&kvm_regs).into(); + assert_eq!(original, converted); + } + + #[cfg(mshv)] + #[test] + fn round_trip_mshv_regs() { + let original = common_regs(); + let mshv_regs: StandardRegisters = (&original).into(); + let converted: CommonRegisters = (&mshv_regs).into(); + assert_eq!(original, converted); + } + + #[cfg(target_os = "windows")] + #[test] + fn round_trip_whp_regs() { + let original = common_regs(); + let whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_REGS_NAMES_LEN] = + (&original).into(); + let converted: CommonRegisters = whp_regs.as_ref().try_into().unwrap(); + assert_eq!(original, converted); + + // test for duplicate register error handling + let original = common_regs(); + let mut whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_REGS_NAMES_LEN] = + (&original).into(); + whp_regs[0].0 = WHvX64RegisterRbx; + let err = CommonRegisters::try_from(whp_regs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::DuplicateRegister(WHvX64RegisterRbx.0) + ); + + // test for passing non-standard register (e.g. CR8) + let original = common_regs(); + let mut whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_REGS_NAMES_LEN] = + (&original).into(); + whp_regs[0].0 = WHvX64RegisterCr8; + let err = CommonRegisters::try_from(whp_regs.as_ref()).unwrap_err(); + assert_eq!( + err, + FromWhpRegisterError::InvalidRegister(WHvX64RegisterCr8.0) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/surrogate_process.rs b/src/hyperlight_host/src/hypervisor/surrogate_process.rs index caaa79d00..d026362d2 100644 --- a/src/hyperlight_host/src/hypervisor/surrogate_process.rs +++ b/src/hyperlight_host/src/hypervisor/surrogate_process.rs @@ -83,7 +83,6 @@ impl Drop for SurrogateProcess { "Failed to return surrogate process to surrogate process manager when dropping : {:?}", e ); - return; } }, Err(e) => { @@ -91,7 +90,6 @@ impl Drop for SurrogateProcess { "Failed to get surrogate process manager when dropping SurrogateProcess: {:?}", e ); - return; } } } diff --git a/src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs b/src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs index dbef96a93..f805e94ad 100644 --- a/src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs +++ b/src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs @@ -422,7 +422,6 @@ mod tests { use hyperlight_common::mem::PAGE_SIZE_USIZE; use rand::{Rng, rng}; - use serial_test::serial; use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; use windows::Win32::System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS, @@ -437,7 +436,6 @@ mod tests { use super::*; use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory}; #[test] - #[serial] fn test_surrogate_process_manager() { let mut threads = Vec::new(); // create more threads than surrogate processes as we want to test that @@ -533,11 +531,11 @@ mod tests { while result { if let Ok(process_name) = unsafe { CStr::from_ptr(process_entry.szExeFile.as_ptr()).to_str() } + && process_name == SURROGATE_PROCESS_BINARY_NAME { - if process_name == SURROGATE_PROCESS_BINARY_NAME { - count += 1; - } + count += 1; } + unsafe { result = Process32Next(snapshot_handle, &mut process_entry).is_ok(); } diff --git a/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs b/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs index d6064443b..4f4e82555 100644 --- a/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs +++ b/src/hyperlight_host/src/hypervisor/windows_hypervisor_platform.rs @@ -24,12 +24,16 @@ use windows::Win32::System::LibraryLoader::*; use windows::core::s; use windows_result::HRESULT; +use super::regs::{ + Align16, CommonFpu, CommonRegisters, CommonSpecialRegisters, WHP_FPU_NAMES_LEN, WHP_REGS_NAMES, + WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, WHP_SREGS_NAMES_LEN, +}; use super::surrogate_process::SurrogateProcess; #[cfg(crashdump)] use crate::HyperlightError; +use crate::hypervisor::regs::WHP_FPU_NAMES; #[cfg(gdb)] use crate::hypervisor::wrappers::WHvDebugRegisters; -use crate::hypervisor::wrappers::{WHvFPURegisters, WHvGeneralRegisters, WHvSpecialRegisters}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; use crate::{Result, new_error}; @@ -213,7 +217,7 @@ impl VMPartition { // with an error about a missing entrypoint // This function should always succeed since before we get here we have already checked that the hypervisor is present and // that we are on a supported version of windows. -type WHvMapGpaRange2Func = unsafe extern "cdecl" fn( +type WHvMapGpaRange2Func = unsafe extern "system" fn( WHV_PARTITION_HANDLE, HANDLE, *const c_void, @@ -303,15 +307,16 @@ impl VMProcessor { part.0 } + /// Helper for setting arbitrary registers. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] pub(super) fn set_registers( &self, - registers: &[(WHV_REGISTER_NAME, WHV_REGISTER_VALUE)], + registers: &[(WHV_REGISTER_NAME, Align16)], ) -> Result<()> { - let partition_handle = self.get_partition_hdl(); let register_count = registers.len(); - let mut register_names: Vec = vec![]; - let mut register_values: Vec = vec![]; + + let mut register_names = Vec::with_capacity(register_count); + let mut register_values = Vec::with_capacity(register_count); for (key, value) in registers.iter() { register_names.push(*key); @@ -320,188 +325,115 @@ impl VMProcessor { unsafe { WHvSetVirtualProcessorRegisters( - partition_handle, + self.get_partition_hdl(), 0, register_names.as_ptr(), register_count as u32, - register_values.as_ptr(), + register_values.as_ptr() as *const WHV_REGISTER_VALUE, )?; } Ok(()) } - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn get_sregs(&self) -> Result { - const LEN: usize = 17; - - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterCr0, - WHvX64RegisterCr2, - WHvX64RegisterCr3, - WHvX64RegisterCr4, - WHvX64RegisterCr8, - WHvX64RegisterEfer, - WHvX64RegisterApicBase, - WHvX64RegisterCs, - WHvX64RegisterDs, - WHvX64RegisterEs, - WHvX64RegisterFs, - WHvX64RegisterGs, - WHvX64RegisterSs, - WHvX64RegisterTr, - WHvX64RegisterLdtr, - WHvX64RegisterGdtr, - WHvX64RegisterIdtr, - ]; + pub(super) fn regs(&self) -> Result { + let mut whv_regs_values: [Align16; WHP_REGS_NAMES_LEN] = + unsafe { std::mem::zeroed() }; - let mut out: [WHV_REGISTER_VALUE; LEN] = unsafe { std::mem::zeroed() }; unsafe { WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, - names.as_ptr(), - LEN as u32, - out.as_mut_ptr(), + WHP_REGS_NAMES.as_ptr(), + whv_regs_values.len() as u32, + whv_regs_values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; } - let res: WHvSpecialRegisters = WHvSpecialRegisters { - cr0: out[0], - cr2: out[1], - cr3: out[2], - cr4: out[3], - cr8: out[4], - efer: out[5], - apic_base: out[6], - cs: out[7], - ds: out[8], - es: out[9], - fs: out[10], - gs: out[11], - ss: out[12], - tr: out[13], - ldtr: out[14], - gdtr: out[15], - idtr: out[16], - }; - - Ok(res) + WHP_REGS_NAMES + .into_iter() + .zip(whv_regs_values) + .collect::)>>() + .as_slice() + .try_into() + .map_err(|e| { + new_error!( + "Failed to convert WHP registers to CommonRegisters: {:?}", + e + ) + }) } - // Sets the registers for the VMProcessor to the given general purpose registers. - // If you want to set other registers, use `set_registers` instead. - pub(super) fn set_general_purpose_registers(&self, regs: &WHvGeneralRegisters) -> Result<()> { - const LEN: usize = 18; - - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterRax, - WHvX64RegisterRbx, - WHvX64RegisterRcx, - WHvX64RegisterRdx, - WHvX64RegisterRsi, - WHvX64RegisterRdi, - WHvX64RegisterRsp, - WHvX64RegisterRbp, - WHvX64RegisterR8, - WHvX64RegisterR9, - WHvX64RegisterR10, - WHvX64RegisterR11, - WHvX64RegisterR12, - WHvX64RegisterR13, - WHvX64RegisterR14, - WHvX64RegisterR15, - WHvX64RegisterRip, - WHvX64RegisterRflags, - ]; + pub(super) fn set_regs(&self, regs: &CommonRegisters) -> Result<()> { + let whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_REGS_NAMES_LEN] = + regs.into(); + self.set_registers(&whp_regs)?; + Ok(()) + } - let values: [WHV_REGISTER_VALUE; LEN] = [ - WHV_REGISTER_VALUE { Reg64: regs.rax }, - WHV_REGISTER_VALUE { Reg64: regs.rbx }, - WHV_REGISTER_VALUE { Reg64: regs.rcx }, - WHV_REGISTER_VALUE { Reg64: regs.rdx }, - WHV_REGISTER_VALUE { Reg64: regs.rsi }, - WHV_REGISTER_VALUE { Reg64: regs.rdi }, - WHV_REGISTER_VALUE { Reg64: regs.rsp }, - WHV_REGISTER_VALUE { Reg64: regs.rbp }, - WHV_REGISTER_VALUE { Reg64: regs.r8 }, - WHV_REGISTER_VALUE { Reg64: regs.r9 }, - WHV_REGISTER_VALUE { Reg64: regs.r10 }, - WHV_REGISTER_VALUE { Reg64: regs.r11 }, - WHV_REGISTER_VALUE { Reg64: regs.r12 }, - WHV_REGISTER_VALUE { Reg64: regs.r13 }, - WHV_REGISTER_VALUE { Reg64: regs.r14 }, - WHV_REGISTER_VALUE { Reg64: regs.r15 }, - WHV_REGISTER_VALUE { Reg64: regs.rip }, - WHV_REGISTER_VALUE { Reg64: regs.rflags }, - ]; + pub(super) fn sregs(&self) -> Result { + let mut whp_sregs_values: [Align16; WHP_SREGS_NAMES_LEN] = + unsafe { std::mem::zeroed() }; unsafe { - WHvSetVirtualProcessorRegisters( + WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, - names.as_ptr(), - LEN as u32, - values.as_ptr(), + WHP_SREGS_NAMES.as_ptr(), + whp_sregs_values.len() as u32, + whp_sregs_values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; } - Ok(()) + + WHP_SREGS_NAMES + .into_iter() + .zip(whp_sregs_values) + .collect::)>>() + .as_slice() + .try_into() + .map_err(|e| { + new_error!( + "Failed to convert WHP registers to CommonSpecialRegisters: {:?}", + e + ) + }) } - pub(super) fn get_regs(&self) -> Result { - const LEN: usize = 18; + pub(super) fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()> { + let whp_regs: [(WHV_REGISTER_NAME, Align16); WHP_SREGS_NAMES_LEN] = + sregs.into(); + self.set_registers(&whp_regs)?; + Ok(()) + } - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterRax, - WHvX64RegisterRbx, - WHvX64RegisterRcx, - WHvX64RegisterRdx, - WHvX64RegisterRsi, - WHvX64RegisterRdi, - WHvX64RegisterRsp, - WHvX64RegisterRbp, - WHvX64RegisterR8, - WHvX64RegisterR9, - WHvX64RegisterR10, - WHvX64RegisterR11, - WHvX64RegisterR12, - WHvX64RegisterR13, - WHvX64RegisterR14, - WHvX64RegisterR15, - WHvX64RegisterRip, - WHvX64RegisterRflags, - ]; + pub(super) fn fpu(&self) -> Result { + let mut whp_fpu_values: [Align16; WHP_FPU_NAMES_LEN] = + unsafe { std::mem::zeroed() }; - let mut out: [WHV_REGISTER_VALUE; LEN] = unsafe { std::mem::zeroed() }; unsafe { WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, - names.as_ptr(), - LEN as u32, - out.as_mut_ptr(), + WHP_FPU_NAMES.as_ptr(), + whp_fpu_values.len() as u32, + whp_fpu_values.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; - Ok(WHvGeneralRegisters { - rax: out[0].Reg64, - rbx: out[1].Reg64, - rcx: out[2].Reg64, - rdx: out[3].Reg64, - rsi: out[4].Reg64, - rdi: out[5].Reg64, - rsp: out[6].Reg64, - rbp: out[7].Reg64, - r8: out[8].Reg64, - r9: out[9].Reg64, - r10: out[10].Reg64, - r11: out[11].Reg64, - r12: out[12].Reg64, - r13: out[13].Reg64, - r14: out[14].Reg64, - r15: out[15].Reg64, - rip: out[16].Reg64, - rflags: out[17].Reg64, - }) } + + WHP_FPU_NAMES + .into_iter() + .zip(whp_fpu_values) + .collect::)>>() + .as_slice() + .try_into() + .map_err(|e| new_error!("Failed to convert WHP registers to CommonFpu: {:?}", e)) + } + + pub(super) fn set_fpu(&self, fpu: &CommonFpu) -> Result<()> { + let whp_fpu: [(WHV_REGISTER_NAME, Align16); WHP_FPU_NAMES_LEN] = + fpu.into(); + self.set_registers(&whp_fpu)?; + Ok(()) } #[cfg(crashdump)] @@ -523,10 +455,10 @@ impl VMProcessor { ); // If it failed for reasons other than insufficient buffer, return error - if let Err(e) = result { - if e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER { - return Err(HyperlightError::WindowsAPIError(e)); - } + if let Err(e) = result + && e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER + { + return Err(HyperlightError::WindowsAPIError(e)); } } @@ -560,12 +492,30 @@ impl VMProcessor { #[cfg(gdb)] pub(super) fn set_debug_regs(&self, regs: &WHvDebugRegisters) -> Result<()> { let registers = vec![ - (WHvX64RegisterDr0, WHV_REGISTER_VALUE { Reg64: regs.dr0 }), - (WHvX64RegisterDr1, WHV_REGISTER_VALUE { Reg64: regs.dr1 }), - (WHvX64RegisterDr2, WHV_REGISTER_VALUE { Reg64: regs.dr2 }), - (WHvX64RegisterDr3, WHV_REGISTER_VALUE { Reg64: regs.dr3 }), - (WHvX64RegisterDr6, WHV_REGISTER_VALUE { Reg64: regs.dr6 }), - (WHvX64RegisterDr7, WHV_REGISTER_VALUE { Reg64: regs.dr7 }), + ( + WHvX64RegisterDr0, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr0 }), + ), + ( + WHvX64RegisterDr1, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr1 }), + ), + ( + WHvX64RegisterDr2, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr2 }), + ), + ( + WHvX64RegisterDr3, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr3 }), + ), + ( + WHvX64RegisterDr6, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr6 }), + ), + ( + WHvX64RegisterDr7, + Align16(WHV_REGISTER_VALUE { Reg64: regs.dr7 }), + ), ]; self.set_registers(®isters) @@ -584,116 +534,24 @@ impl VMProcessor { WHvX64RegisterDr7, ]; - let mut out: [WHV_REGISTER_VALUE; LEN] = unsafe { std::mem::zeroed() }; + let mut out: [Align16; LEN] = unsafe { std::mem::zeroed() }; unsafe { WHvGetVirtualProcessorRegisters( self.get_partition_hdl(), 0, names.as_ptr(), LEN as u32, - out.as_mut_ptr(), + out.as_mut_ptr() as *mut WHV_REGISTER_VALUE, )?; Ok(WHvDebugRegisters { - dr0: out[0].Reg64, - dr1: out[1].Reg64, - dr2: out[2].Reg64, - dr3: out[3].Reg64, - dr6: out[4].Reg64, - dr7: out[5].Reg64, - }) - } - } - - pub(super) fn set_fpu(&self, regs: &WHvFPURegisters) -> Result<()> { - const LEN: usize = 26; - - let names: [WHV_REGISTER_NAME; LEN] = [ - WHvX64RegisterXmm0, - WHvX64RegisterXmm1, - WHvX64RegisterXmm2, - WHvX64RegisterXmm3, - WHvX64RegisterXmm4, - WHvX64RegisterXmm5, - WHvX64RegisterXmm6, - WHvX64RegisterXmm7, - WHvX64RegisterXmm8, - WHvX64RegisterXmm9, - WHvX64RegisterXmm10, - WHvX64RegisterXmm11, - WHvX64RegisterXmm12, - WHvX64RegisterXmm13, - WHvX64RegisterXmm14, - WHvX64RegisterXmm15, - WHvX64RegisterFpMmx0, - WHvX64RegisterFpMmx1, - WHvX64RegisterFpMmx2, - WHvX64RegisterFpMmx3, - WHvX64RegisterFpMmx4, - WHvX64RegisterFpMmx5, - WHvX64RegisterFpMmx6, - WHvX64RegisterFpMmx7, - WHvX64RegisterFpControlStatus, - WHvX64RegisterXmmControlStatus, - ]; - - let xmm_regs = [ - regs.xmm0, regs.xmm1, regs.xmm2, regs.xmm3, regs.xmm4, regs.xmm5, regs.xmm6, regs.xmm7, - regs.xmm8, regs.xmm9, regs.xmm10, regs.xmm11, regs.xmm12, regs.xmm13, regs.xmm14, - regs.xmm15, - ]; - - let mut values: Vec = xmm_regs - .iter() - .map(|®| WHV_REGISTER_VALUE { - Fp: WHV_X64_FP_REGISTER { - AsUINT128: WHV_UINT128 { - Anonymous: WHV_UINT128_0 { - Low64: reg as u64, - High64: (reg >> 64) as u64, - }, - }, - }, + dr0: out[0].0.Reg64, + dr1: out[1].0.Reg64, + dr2: out[2].0.Reg64, + dr3: out[3].0.Reg64, + dr6: out[4].0.Reg64, + dr7: out[5].0.Reg64, }) - .collect(); - - values.extend_from_slice(&[ - WHV_REGISTER_VALUE { Reg64: regs.mmx0 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx1 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx2 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx3 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx4 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx5 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx6 }, - WHV_REGISTER_VALUE { Reg64: regs.mmx7 }, - WHV_REGISTER_VALUE { - FpControlStatus: WHV_X64_FP_CONTROL_STATUS_REGISTER { - Anonymous: WHV_X64_FP_CONTROL_STATUS_REGISTER_0 { - FpControl: regs.fp_control_word, - FpTag: regs.fp_tag_word, - ..Default::default() - }, - }, - }, - WHV_REGISTER_VALUE { - XmmControlStatus: WHV_X64_XMM_CONTROL_STATUS_REGISTER { - Anonymous: WHV_X64_XMM_CONTROL_STATUS_REGISTER_0 { - XmmStatusControl: regs.mxcsr, - ..Default::default() - }, - }, - }, - ]); - - unsafe { - WHvSetVirtualProcessorRegisters( - self.get_partition_hdl(), - 0, - names.as_ptr(), - LEN as u32, - values.as_ptr(), - )?; } - Ok(()) } #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] diff --git a/src/hyperlight_host/src/hypervisor/wrappers.rs b/src/hyperlight_host/src/hypervisor/wrappers.rs index 4a2a1ff8b..135ae2b82 100644 --- a/src/hyperlight_host/src/hypervisor/wrappers.rs +++ b/src/hyperlight_host/src/hypervisor/wrappers.rs @@ -18,7 +18,6 @@ use std::ffi::CString; use tracing::{Span, instrument}; use windows::Win32::Foundation::{HANDLE, HMODULE}; -use windows::Win32::System::Hypervisor::WHV_REGISTER_VALUE; use windows::core::PSTR; use crate::{HyperlightError, Result}; @@ -57,29 +56,6 @@ impl From<&PSTRWrapper> for PSTR { } } -// only used on windows. mshv and kvm already has this implemented -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(super) struct WHvGeneralRegisters { - pub rax: u64, - pub rbx: u64, - pub rcx: u64, - pub rdx: u64, - pub rsi: u64, - pub rdi: u64, - pub rsp: u64, - pub rbp: u64, - pub r8: u64, - pub r9: u64, - pub r10: u64, - pub r11: u64, - pub r12: u64, - pub r13: u64, - pub r14: u64, - pub r15: u64, - pub rip: u64, - pub rflags: u64, -} - /// only used on widos for handling debug registers with the VMProcessor #[cfg(gdb)] #[derive(Debug, Default, Copy, Clone, PartialEq)] @@ -92,61 +68,6 @@ pub(super) struct WHvDebugRegisters { pub dr7: u64, } -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(super) struct WHvFPURegisters { - pub xmm0: u128, - pub xmm1: u128, - pub xmm2: u128, - pub xmm3: u128, - pub xmm4: u128, - pub xmm5: u128, - pub xmm6: u128, - pub xmm7: u128, - pub xmm8: u128, - pub xmm9: u128, - pub xmm10: u128, - pub xmm11: u128, - pub xmm12: u128, - pub xmm13: u128, - pub xmm14: u128, - pub xmm15: u128, - - pub mmx0: u64, - pub mmx1: u64, - pub mmx2: u64, - pub mmx3: u64, - pub mmx4: u64, - pub mmx5: u64, - pub mmx6: u64, - pub mmx7: u64, - - pub fp_control_word: u16, - pub fp_tag_word: u8, - - pub mxcsr: u32, -} - -#[derive(Default, Copy, Clone)] -pub(super) struct WHvSpecialRegisters { - pub cr0: WHV_REGISTER_VALUE, - pub cr2: WHV_REGISTER_VALUE, - pub cr3: WHV_REGISTER_VALUE, - pub cr4: WHV_REGISTER_VALUE, - pub cr8: WHV_REGISTER_VALUE, - pub efer: WHV_REGISTER_VALUE, - pub apic_base: WHV_REGISTER_VALUE, - pub cs: WHV_REGISTER_VALUE, - pub ds: WHV_REGISTER_VALUE, - pub es: WHV_REGISTER_VALUE, - pub fs: WHV_REGISTER_VALUE, - pub gs: WHV_REGISTER_VALUE, - pub ss: WHV_REGISTER_VALUE, - pub tr: WHV_REGISTER_VALUE, - pub ldtr: WHV_REGISTER_VALUE, - pub gdtr: WHV_REGISTER_VALUE, - pub idtr: WHV_REGISTER_VALUE, -} - /// Wrapper for HANDLE, required since HANDLE is no longer Send. #[derive(Debug, Copy, Clone)] pub struct HandleWrapper(HANDLE); diff --git a/src/hyperlight_host/src/lib.rs b/src/hyperlight_host/src/lib.rs index 998bc9c9e..5f034dc79 100644 --- a/src/hyperlight_host/src/lib.rs +++ b/src/hyperlight_host/src/lib.rs @@ -13,9 +13,27 @@ 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. */ -#![deny(dead_code, missing_docs, unused_mut)] -//! This crate contains an SDK that is used to execute specially- -// compiled binaries within a very lightweight hypervisor environment. +#![warn(dead_code, missing_docs, unused_mut)] +//! Hyperlight host runtime for executing guest code in lightweight virtual machines. +//! +//! This crate provides the host-side runtime for Hyperlight, enabling safe execution +//! of untrusted guest code within micro virtual machines with minimal overhead. +//! The runtime manages sandbox creation, guest function calls, memory isolation, +//! and host-guest communication. +//! +//! The primary entry points are [`UninitializedSandbox`] for initial setup and +//! [`MultiUseSandbox`] for executing guest functions. +//! +//! ## Guest Requirements +//! +//! Hyperlight requires specially compiled guest binaries and cannot run regular +//! container images or executables. Guests must be built using either the Rust +//! API ([`hyperlight_guest`] with optional use of [`hyperlight_guest_bin`]), +//! or with the C API (`hyperlight_guest_capi`). +//! +//! [`hyperlight_guest`]: https://docs.rs/hyperlight_guest +//! [`hyperlight_guest_bin`]: https://docs.rs/hyperlight_guest_bin +//! #![cfg_attr(not(any(test, debug_assertions)), warn(clippy::panic))] #![cfg_attr(not(any(test, debug_assertions)), warn(clippy::expect_used))] @@ -50,7 +68,7 @@ pub mod hypervisor; /// present and code length will be zero; /// /// - The pointer passed to the Entrypoint in the Guest application is the size of page table + size of code, -/// at this address structs below are laid out in this order +/// at this address structs below are laid out in this order pub mod mem; /// Metric definitions and helpers pub mod metrics; @@ -58,23 +76,15 @@ pub mod metrics; /// outside this file. Types from this module needed for public consumption are /// re-exported below. pub mod sandbox; -/// `trait`s and other functionality for dealing with defining sandbox -/// states and moving between them -pub mod sandbox_state; -#[cfg(all(feature = "seccomp", target_os = "linux"))] -pub(crate) mod seccomp; /// Signal handling for Linux #[cfg(target_os = "linux")] pub(crate) mod signal_handlers; -/// Utilities for testing including interacting with `simpleguest.exe` -/// and `callbackguest.exe`, our two most basic guest binaries for testing +/// Utilities for testing including interacting with `simpleguest` testing guest binary #[cfg(test)] pub(crate) mod testing; /// The re-export for the `HyperlightError` type pub use error::HyperlightError; -/// Re-export for `HypervisorWrapper` trait -/// Re-export for `MemMgrWrapper` type /// A sandbox that can call be used to make multiple calls to guest functions, /// and otherwise reused multiple times pub use sandbox::MultiUseSandbox; @@ -85,9 +95,6 @@ pub use sandbox::is_hypervisor_present; /// The re-export for the `GuestBinary` type pub use sandbox::uninitialized::GuestBinary; -/// The re-export for the `MultiUseGuestCallContext` type` -pub use crate::func::call_ctx::MultiUseGuestCallContext; - /// The universal `Result` type used throughout the Hyperlight codebase. pub type Result = core::result::Result; diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index 3efe09b4f..3a30354ef 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -14,6 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +#[cfg(feature = "mem_profile")] +use std::sync::Arc; + #[cfg(target_arch = "aarch64")] use goblin::elf::reloc::{R_AARCH64_NONE, R_AARCH64_RELATIVE}; #[cfg(target_arch = "x86_64")] @@ -26,13 +29,85 @@ use goblin::elf64::program_header::PT_LOAD; use crate::{Result, log_then_return, new_error}; +#[cfg(feature = "mem_profile")] +struct ResolvedSectionHeader { + name: String, + addr: u64, + offset: u64, + size: u64, +} + pub(crate) struct ElfInfo { payload: Vec, phdrs: ProgramHeaders, + #[cfg(feature = "mem_profile")] + shdrs: Vec, entry: u64, relocs: Vec, } +#[cfg(feature = "mem_profile")] +struct UnwindInfo { + payload: Vec, + load_addr: u64, + va_size: u64, + base_svma: u64, + shdrs: Vec, +} + +#[cfg(feature = "mem_profile")] +impl super::exe::UnwindInfo for UnwindInfo { + fn as_module(&self) -> framehop::Module> { + framehop::Module::new( + // TODO: plumb through a name from from_file if this + // came from a file + "guest".to_string(), + self.load_addr..self.load_addr + self.va_size, + self.load_addr, + self, + ) + } + fn hash(&self) -> blake3::Hash { + blake3::hash(&self.payload) + } +} + +#[cfg(feature = "mem_profile")] +impl UnwindInfo { + fn resolved_section_header(&self, name: &[u8]) -> Option<&ResolvedSectionHeader> { + self.shdrs + .iter() + .find(|&sh| sh.name.as_bytes()[0..core::cmp::min(name.len(), sh.name.len())] == *name) + } +} + +#[cfg(feature = "mem_profile")] +impl framehop::ModuleSectionInfo> for &UnwindInfo { + fn base_svma(&self) -> u64 { + self.base_svma + } + fn section_svma_range(&mut self, name: &[u8]) -> Option> { + let shdr = self.resolved_section_header(name)?; + Some(shdr.addr..shdr.addr + shdr.size) + } + fn section_data(&mut self, name: &[u8]) -> Option> { + if name == b".eh_frame" && self.resolved_section_header(b".debug_frame").is_some() { + /* Rustc does not always emit enough information for stack + * unwinding in .eh_frame, presumably because we use panic = + * abort in the guest. Framehop defaults to ignoring + * .debug_frame if .eh_frame exists, but we want the opposite + * behaviour here, since .debug_frame will actually contain + * frame information whereas .eh_frame often doesn't because + * of the aforementioned behaviour. Consequently, we hack + * around this by pretending that .eh_frame doesn't exist if + * .debug_frame does. */ + return None; + } + let shdr = self.resolved_section_header(name)?; + Some(self.payload[shdr.offset as usize..(shdr.offset + shdr.size) as usize].to_vec()) + } +} + impl ElfInfo { pub(crate) fn new(bytes: &[u8]) -> Result { let elf = Elf::parse(bytes)?; @@ -47,6 +122,19 @@ impl ElfInfo { Ok(ElfInfo { payload: bytes.to_vec(), phdrs: elf.program_headers, + #[cfg(feature = "mem_profile")] + shdrs: elf + .section_headers + .iter() + .filter_map(|sh| { + Some(ResolvedSectionHeader { + name: elf.shdr_strtab.get_at(sh.sh_name)?.to_string(), + addr: sh.sh_addr, + offset: sh.sh_offset, + size: sh.sh_size, + }) + }) + .collect(), entry: elf.entry, relocs, }) @@ -73,7 +161,11 @@ impl ElfInfo { .unwrap(); (max_phdr.p_vaddr + max_phdr.p_memsz - self.get_base_va()) as usize } - pub(crate) fn load_at(&self, load_addr: usize, target: &mut [u8]) -> Result<()> { + pub(crate) fn load_at( + self, + load_addr: usize, + target: &mut [u8], + ) -> Result { let base_va = self.get_base_va(); for phdr in self.phdrs.iter().filter(|phdr| phdr.p_type == PT_LOAD) { let start_va = (phdr.p_vaddr - base_va) as usize; @@ -113,6 +205,20 @@ impl ElfInfo { } } } - Ok(()) + cfg_if::cfg_if! { + if #[cfg(feature = "mem_profile")] { + let va_size = self.get_va_size() as u64; + let base_svma = self.get_base_va(); + Ok(Arc::new(UnwindInfo { + payload: self.payload, + load_addr: load_addr as u64, + va_size, + base_svma, + shdrs: self.shdrs, + })) + } else { + Ok(()) + } + } } } diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index bf1724317..19b6a9f41 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -16,6 +16,8 @@ limitations under the License. use std::fs::File; use std::io::Read; +#[cfg(feature = "mem_profile")] +use std::sync::Arc; use std::vec::Vec; use super::elf::ElfInfo; @@ -37,6 +39,41 @@ pub enum ExeInfo { const DEFAULT_ELF_STACK_RESERVE: u64 = 65536; const DEFAULT_ELF_HEAP_RESERVE: u64 = 131072; +#[cfg(feature = "mem_profile")] +pub(crate) trait UnwindInfo: Send + Sync { + fn as_module(&self) -> framehop::Module>; + fn hash(&self) -> blake3::Hash; +} + +#[cfg(feature = "mem_profile")] +pub(crate) struct DummyUnwindInfo {} +#[cfg(feature = "mem_profile")] +impl UnwindInfo for DummyUnwindInfo { + fn as_module(&self) -> framehop::Module> { + framehop::Module::new("unsupported".to_string(), 0..0, 0, self) + } + fn hash(&self) -> blake3::Hash { + blake3::Hash::from_bytes([0; 32]) + } +} +#[cfg(feature = "mem_profile")] +impl framehop::ModuleSectionInfo for &DummyUnwindInfo { + fn base_svma(&self) -> u64 { + 0 + } + fn section_svma_range(&mut self, _name: &[u8]) -> Option> { + None + } + fn section_data(&mut self, _name: &[u8]) -> Option { + None + } +} + +#[cfg(feature = "mem_profile")] +pub(crate) type LoadInfo = Arc; +#[cfg(not(feature = "mem_profile"))] +pub(crate) type LoadInfo = (); + impl ExeInfo { pub fn from_file(path: &str) -> Result { let mut file = File::open(path)?; @@ -71,12 +108,9 @@ impl ExeInfo { // copying into target, but the PE loader chooses to apply // relocations in its owned representation of the PE contents, // which requires it to be &mut. - pub fn load(&mut self, load_addr: usize, target: &mut [u8]) -> Result<()> { + pub fn load(self, load_addr: usize, target: &mut [u8]) -> Result { match self { - ExeInfo::Elf(elf) => { - elf.load_at(load_addr, target)?; - } + ExeInfo::Elf(elf) => elf.load_at(load_addr, target), } - Ok(()) } } diff --git a/src/hyperlight_host/src/mem/layout.rs b/src/hyperlight_host/src/mem/layout.rs index 04edc9bcc..e08e256af 100644 --- a/src/hyperlight_host/src/mem/layout.rs +++ b/src/hyperlight_host/src/mem/layout.rs @@ -64,7 +64,7 @@ use crate::{Result, new_error}; // +-------------------------------------------+ 0x0_000 /// - `InitData` - some extra data that can be loaded onto the sandbox during -/// initialization. +/// initialization. /// /// - `HostDefinitions` - the length of this is the `HostFunctionDefinitionSize` /// field from `SandboxConfiguration` @@ -224,18 +224,24 @@ impl SandboxMemoryLayout { pub(crate) const PML4_OFFSET: usize = 0x0000; /// The offset into the sandbox's memory where the Page Directory Pointer /// Table starts. + #[cfg(feature = "init-paging")] pub(super) const PDPT_OFFSET: usize = 0x1000; /// The offset into the sandbox's memory where the Page Directory starts. + #[cfg(feature = "init-paging")] pub(super) const PD_OFFSET: usize = 0x2000; /// The offset into the sandbox's memory where the Page Tables start. + #[cfg(feature = "init-paging")] pub(super) const PT_OFFSET: usize = 0x3000; /// The address (not the offset) to the start of the page directory + #[cfg(feature = "init-paging")] pub(super) const PD_GUEST_ADDRESS: usize = Self::BASE_ADDRESS + Self::PD_OFFSET; /// The address (not the offset) into sandbox memory where the Page /// Directory Pointer Table starts + #[cfg(feature = "init-paging")] pub(super) const PDPT_GUEST_ADDRESS: usize = Self::BASE_ADDRESS + Self::PDPT_OFFSET; /// The address (not the offset) into sandbox memory where the Page /// Tables start + #[cfg(feature = "init-paging")] pub(super) const PT_GUEST_ADDRESS: usize = Self::BASE_ADDRESS + Self::PT_OFFSET; /// The maximum amount of memory a single sandbox will be allowed. /// The addressable virtual memory with current paging setup is virtual address 0x0 - 0x40000000 (excl.), @@ -379,22 +385,6 @@ impl SandboxMemoryLayout { self.stack_size } - /// Get the offset in guest memory to the OutB pointer. - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn get_outb_pointer_offset(&self) -> usize { - // The outb pointer is immediately after the code pointer - // in the `CodeAndOutBPointers` struct which is a u64 - self.peb_code_pointer_offset + size_of::() - } - - /// Get the offset in guest memory to the OutB context. - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn get_outb_context_offset(&self) -> usize { - // The outb context is immediately after the outb pointer - // in the `CodeAndOutBPointers` struct which is a u64 - self.get_outb_pointer_offset() + size_of::() - } - /// Get the offset in guest memory to the output data pointer. #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_output_data_pointer_offset(&self) -> usize { @@ -412,10 +402,8 @@ impl SandboxMemoryLayout { } /// Get the offset in guest memory to the start of output data. - /// - /// This function exists to accommodate the macro that generates C API - /// compatible functions. #[instrument(skip_all, parent = Span::current(), level= "Trace")] + #[cfg(test)] pub(crate) fn get_output_data_offset(&self) -> usize { self.output_data_buffer_offset } @@ -450,12 +438,6 @@ impl SandboxMemoryLayout { self.peb_guest_dispatch_function_ptr_offset } - /// Get the offset in guest memory to the PEB address - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn get_in_process_peb_offset(&self) -> usize { - self.peb_offset - } - /// Get the offset in guest memory to the heap size #[instrument(skip_all, parent = Span::current(), level= "Trace")] fn get_heap_size_offset(&self) -> usize { @@ -484,12 +466,6 @@ impl SandboxMemoryLayout { self.get_min_guest_stack_address_offset() + size_of::() } - /// Get the offset to the guest guard page - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub fn get_guard_page_offset(&self) -> usize { - self.guard_page_offset - } - /// Get the total size of guest memory in `self`'s memory /// layout. #[instrument(skip_all, parent = Span::current(), level= "Trace")] diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index a7e22255b..22f71d65b 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -30,6 +30,8 @@ use bitflags::bitflags; #[cfg(mshv)] use hyperlight_common::mem::PAGE_SHIFT; use hyperlight_common::mem::PAGE_SIZE_USIZE; +#[cfg(kvm)] +use kvm_bindings::{KVM_MEM_READONLY, kvm_userspace_memory_region}; #[cfg(mshv2)] use mshv_bindings::{ HV_MAP_GPA_EXECUTABLE, HV_MAP_GPA_PERMISSIONS_NONE, HV_MAP_GPA_READABLE, HV_MAP_GPA_WRITABLE, @@ -50,7 +52,7 @@ pub(crate) const DEFAULT_GUEST_BLOB_MEM_FLAGS: MemoryRegionFlags = MemoryRegionF bitflags! { /// flags representing memory permission for a memory region - #[derive(Copy, Clone, Debug, PartialEq, Eq)] + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct MemoryRegionFlags: u32 { /// no permissions const NONE = 0; @@ -152,7 +154,7 @@ impl TryFrom for MemoryRegionFlags { } // only used for debugging -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] /// The type of memory region pub enum MemoryRegionType { /// The region contains the guest's page tables @@ -179,16 +181,16 @@ pub enum MemoryRegionType { /// represents a single memory region inside the guest. All memory within a region has /// the same memory permissions -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MemoryRegion { /// the range of guest memory addresses - pub(crate) guest_region: Range, + pub guest_region: Range, /// the range of host memory addresses - pub(crate) host_region: Range, + pub host_region: Range, /// memory access flags for the given region - pub(crate) flags: MemoryRegionFlags, + pub flags: MemoryRegionFlags, /// the type of memory region - pub(crate) region_type: MemoryRegionType, + pub region_type: MemoryRegionType, } pub(crate) struct MemoryRegionVecBuilder { @@ -308,3 +310,26 @@ impl From for mshv_user_mem_region { } } } + +#[cfg(kvm)] +impl From for kvm_bindings::kvm_userspace_memory_region { + fn from(region: MemoryRegion) -> Self { + let perm_flags = + MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE; + + let perm_flags = perm_flags.intersection(region.flags); + + kvm_userspace_memory_region { + slot: 0, + guest_phys_addr: region.guest_region.start as u64, + memory_size: (region.guest_region.end - region.guest_region.start) as u64, + userspace_addr: region.host_region.start as u64, + flags: if perm_flags.contains(MemoryRegionFlags::WRITE) { + 0 // RWX + } else { + // Note: KVM_MEM_READONLY is executable + KVM_MEM_READONLY // RX + }, + } + } +} diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 7910d9dc2..2434dd118 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -15,26 +15,25 @@ limitations under the License. */ use std::cmp::Ordering; -use std::sync::{Arc, Mutex}; +use flatbuffers::FlatBufferBuilder; use hyperlight_common::flatbuffer_wrappers::function_call::{ FunctionCall, validate_guest_function_call_buffer, }; -use hyperlight_common::flatbuffer_wrappers::function_types::ReturnValue; -use hyperlight_common::flatbuffer_wrappers::guest_error::GuestError; +use hyperlight_common::flatbuffer_wrappers::function_types::FunctionCallResult; use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData; use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails; use tracing::{Span, instrument}; use super::exe::ExeInfo; use super::layout::SandboxMemoryLayout; +use super::memory_region::MemoryRegion; #[cfg(feature = "init-paging")] -use super::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegion, MemoryRegionType}; +use super::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionType}; use super::ptr::{GuestPtr, RawPtr}; use super::ptr_offset::Offset; use super::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, HostSharedMemory, SharedMemory}; use super::shared_mem_snapshot::SharedMemorySnapshot; -use crate::HyperlightError::NoMemorySnapshot; use crate::sandbox::SandboxConfiguration; use crate::sandbox::uninitialized::GuestBlob; use crate::{Result, log_then_return, new_error}; @@ -73,9 +72,12 @@ pub(crate) struct SandboxMemoryManager { pub(crate) load_addr: RawPtr, /// Offset for the execution entrypoint from `load_addr` pub(crate) entrypoint_offset: Offset, - /// A vector of memory snapshots that can be used to save and restore the state of the memory - /// This is used by the Rust Sandbox implementation (rather than the mem_snapshot field above which only exists to support current C API) - snapshots: Arc>>, + /// How many memory regions were mapped after sandbox creation + pub(crate) mapped_rgns: u64, + /// Stack cookie for stack guard verification + pub(crate) stack_cookie: [u8; STACK_COOKIE_LEN], + /// Buffer for accumulating guest abort messages + pub(crate) abort_buffer: Vec, } impl SandboxMemoryManager @@ -84,22 +86,37 @@ where { /// Create a new `SandboxMemoryManager` with the given parameters #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn new( + pub(crate) fn new( layout: SandboxMemoryLayout, shared_mem: S, load_addr: RawPtr, entrypoint_offset: Offset, + stack_cookie: [u8; STACK_COOKIE_LEN], ) -> Self { Self { layout, shared_mem, load_addr, entrypoint_offset, - snapshots: Arc::new(Mutex::new(Vec::new())), + mapped_rgns: 0, + stack_cookie, + abort_buffer: Vec::new(), } } + /// Get the stack cookie + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn get_stack_cookie(&self) -> &[u8; STACK_COOKIE_LEN] { + &self.stack_cookie + } + + /// Get mutable access to the abort buffer + pub(crate) fn get_abort_buffer_mut(&mut self) -> &mut Vec { + &mut self.abort_buffer + } + /// Get `SharedMemory` in `self` as a mutable reference + #[cfg(any(gdb, test))] pub(crate) fn get_shared_mem_mut(&mut self) -> &mut S { &mut self.shared_mem } @@ -225,10 +242,11 @@ where let addr = (p << 21) + (i << 12); // First check if we're still in the cached region - if let Some(cached_idx) = *cached_region_idx { - if cached_idx < regions.len() && regions[cached_idx].guest_region.contains(&addr) { - return Ok(regions[cached_idx].region_type); - } + if let Some(cached_idx) = *cached_region_idx + && cached_idx < regions.len() + && regions[cached_idx].guest_region.contains(&addr) + { + return Ok(regions[cached_idx].region_type); } // If not in cached region, try adjacent regions first (common for sequential access) @@ -262,62 +280,26 @@ where } } - /// this function will create a memory snapshot and push it onto the stack of snapshots - /// It should be used when you want to save the state of the memory, for example, when evolving a sandbox to a new state - pub(crate) fn push_state(&mut self) -> Result<()> { - let snapshot = SharedMemorySnapshot::new(&mut self.shared_mem)?; - self.snapshots - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .push(snapshot); - Ok(()) - } - - /// this function restores a memory snapshot from the last snapshot in the list but does not pop the snapshot - /// off the stack - /// It should be used when you want to restore the state of the memory to a previous state but still want to - /// retain that state, for example after calling a function in the guest - pub(crate) fn restore_state_from_last_snapshot(&mut self) -> Result<()> { - let mut snapshots = self - .snapshots - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?; - let last = snapshots.last_mut(); - if last.is_none() { - log_then_return!(NoMemorySnapshot); - } - #[allow(clippy::unwrap_used)] // We know that last is not None because we checked it above - let snapshot = last.unwrap(); - snapshot.restore_from_snapshot(&mut self.shared_mem) + /// Create a snapshot with the given mapped regions + pub(crate) fn snapshot( + &mut self, + sandbox_id: u64, + mapped_regions: Vec, + ) -> Result { + SharedMemorySnapshot::new(&mut self.shared_mem, sandbox_id, mapped_regions) } - /// this function pops the last snapshot off the stack and restores the memory to the previous state - /// It should be used when you want to restore the state of the memory to a previous state and do not need to retain that state - /// for example when devolving a sandbox to a previous state. - pub(crate) fn pop_and_restore_state_from_snapshot(&mut self) -> Result<()> { - let last = self - .snapshots - .try_lock() - .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .pop(); - if last.is_none() { - log_then_return!(NoMemorySnapshot); + /// This function restores a memory snapshot from a given snapshot. + pub(crate) fn restore_snapshot(&mut self, snapshot: &SharedMemorySnapshot) -> Result<()> { + if self.shared_mem.mem_size() != snapshot.mem_size() { + return Err(new_error!( + "Snapshot size does not match current memory size: {} != {}", + self.shared_mem.raw_mem_size(), + snapshot.mem_size() + )); } - self.restore_state_from_last_snapshot() - } - - /// Sets `addr` to the correct offset in the memory referenced by - /// `shared_mem` to indicate the address of the outb pointer and context - /// for calling outb function - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn set_outb_address_and_context(&mut self, addr: u64, context: u64) -> Result<()> { - let pointer_offset = self.layout.get_outb_pointer_offset(); - let context_offset = self.layout.get_outb_context_offset(); - self.shared_mem.with_exclusivity(|excl| -> Result<()> { - excl.write_u64(pointer_offset, addr)?; - excl.write_u64(context_offset, context)?; - Ok(()) - })? + snapshot.restore_from_snapshot(&mut self.shared_mem)?; + Ok(()) } } @@ -336,17 +318,17 @@ impl SandboxMemoryManager { #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] pub(crate) fn load_guest_binary_into_memory( cfg: SandboxConfiguration, - exe_info: &mut ExeInfo, + exe_info: ExeInfo, guest_blob: Option<&GuestBlob>, - ) -> Result { + ) -> Result<(Self, super::exe::LoadInfo)> { let guest_blob_size = guest_blob.map(|b| b.data.len()).unwrap_or(0); let guest_blob_mem_flags = guest_blob.map(|b| b.permissions); let layout = SandboxMemoryLayout::new( cfg, exe_info.loaded_size(), - usize::try_from(cfg.get_stack_size(exe_info))?, - usize::try_from(cfg.get_heap_size(exe_info))?, + usize::try_from(cfg.get_stack_size(&exe_info))?, + usize::try_from(cfg.get_heap_size(&exe_info))?, guest_blob_size, guest_blob_mem_flags, )?; @@ -364,12 +346,28 @@ impl SandboxMemoryManager { shared_mem.write_u64(offset, load_addr_u64)?; } - exe_info.load( + // The load method returns a LoadInfo which can also be a different type once the + // `mem_profile` feature is enabled. + #[allow(clippy::let_unit_value)] + let load_info = exe_info.load( load_addr.clone().try_into()?, &mut shared_mem.as_mut_slice()[layout.get_guest_code_offset()..], )?; - Ok(Self::new(layout, shared_mem, load_addr, entrypoint_offset)) + let stack_cookie = rand::random::<[u8; STACK_COOKIE_LEN]>(); + let stack_offset = layout.get_top_of_user_stack_offset(); + shared_mem.copy_from_slice(&stack_cookie, stack_offset)?; + + Ok(( + Self::new( + layout, + shared_mem, + load_addr, + entrypoint_offset, + stack_cookie, + ), + load_info, + )) } /// Writes host function details to memory @@ -408,12 +406,23 @@ impl SandboxMemoryManager { Ok(()) } - /// Set the stack guard to `cookie` using `layout` to calculate - /// its location and `shared_mem` to write it. + /// Write memory layout #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn set_stack_guard(&mut self, cookie: &[u8; STACK_COOKIE_LEN]) -> Result<()> { - let stack_offset = self.layout.get_top_of_user_stack_offset(); - self.shared_mem.copy_from_slice(cookie, stack_offset) + pub(crate) fn write_memory_layout(&mut self) -> Result<()> { + let mem_size = self.shared_mem.mem_size(); + self.layout.write( + &mut self.shared_mem, + SandboxMemoryLayout::BASE_ADDRESS, + mem_size, + ) + } + + /// Write init data + #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] + pub(crate) fn write_init_data(&mut self, user_memory: &[u8]) -> Result<()> { + self.layout + .write_init_data(&mut self.shared_mem, user_memory)?; + Ok(()) } /// Wraps ExclusiveSharedMemory::build @@ -430,14 +439,18 @@ impl SandboxMemoryManager { layout: self.layout, load_addr: self.load_addr.clone(), entrypoint_offset: self.entrypoint_offset, - snapshots: Arc::new(Mutex::new(Vec::new())), + mapped_rgns: self.mapped_rgns, + stack_cookie: self.stack_cookie, + abort_buffer: self.abort_buffer, }, SandboxMemoryManager { shared_mem: gshm, layout: self.layout, load_addr: self.load_addr.clone(), entrypoint_offset: self.entrypoint_offset, - snapshots: Arc::new(Mutex::new(Vec::new())), + mapped_rgns: self.mapped_rgns, + stack_cookie: self.stack_cookie, + abort_buffer: Vec::new(), // Guest doesn't need abort buffer }, ) } @@ -457,10 +470,11 @@ impl SandboxMemoryManager { /// documentation at the bottom `set_stack_guard` for description /// of why it isn't. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn check_stack_guard(&self, cookie: [u8; STACK_COOKIE_LEN]) -> Result { + pub(crate) fn check_stack_guard(&self) -> Result { + let expected = self.stack_cookie; let offset = self.layout.get_top_of_user_stack_offset(); - let test_cookie: [u8; STACK_COOKIE_LEN] = self.shared_mem.read(offset)?; - let cmp_res = cookie.iter().cmp(test_cookie.iter()); + let actual: [u8; STACK_COOKIE_LEN] = self.shared_mem.read(offset)?; + let cmp_res = expected.iter().cmp(actual.iter()); Ok(cmp_res == Ordering::Equal) } @@ -487,18 +501,19 @@ impl SandboxMemoryManager { ) } - /// Writes a function call result to memory + /// Writes a host function call result to memory #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn write_response_from_host_method_call(&mut self, res: &ReturnValue) -> Result<()> { - let function_call_ret_val_buffer = Vec::::try_from(res).map_err(|_| { - new_error!( - "write_response_from_host_method_call: failed to convert ReturnValue to Vec" - ) - })?; + pub(crate) fn write_response_from_host_function_call( + &mut self, + res: &FunctionCallResult, + ) -> Result<()> { + let mut builder = FlatBufferBuilder::new(); + let data = res.encode(&mut builder); + self.shared_mem.push_buffer( self.layout.input_data_buffer_offset, self.layout.sandbox_memory_config.get_input_data_size(), - function_call_ret_val_buffer.as_slice(), + data, ) } @@ -519,10 +534,11 @@ impl SandboxMemoryManager { ) } - /// Reads a function call result from memory + /// Reads a function call result from memory. + /// A function call result can be either an error or a successful return value. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn get_guest_function_call_result(&mut self) -> Result { - self.shared_mem.try_pop_buffer_into::( + pub(crate) fn get_guest_function_call_result(&mut self) -> Result { + self.shared_mem.try_pop_buffer_into::( self.layout.output_data_buffer_offset, self.layout.sandbox_memory_config.get_output_data_size(), ) @@ -537,11 +553,24 @@ impl SandboxMemoryManager { ) } - /// Get the guest error data - pub(crate) fn get_guest_error(&mut self) -> Result { - self.shared_mem.try_pop_buffer_into::( - self.layout.output_data_buffer_offset, - self.layout.sandbox_memory_config.get_output_data_size(), - ) + pub(crate) fn clear_io_buffers(&mut self) { + // Clear the output data buffer + loop { + let Ok(_) = self.shared_mem.try_pop_buffer_into::>( + self.layout.output_data_buffer_offset, + self.layout.sandbox_memory_config.get_output_data_size(), + ) else { + break; + }; + } + // Clear the input data buffer + loop { + let Ok(_) = self.shared_mem.try_pop_buffer_into::>( + self.layout.input_data_buffer_offset, + self.layout.sandbox_memory_config.get_input_data_size(), + ) else { + break; + }; + } } } diff --git a/src/hyperlight_host/src/mem/ptr_offset.rs b/src/hyperlight_host/src/mem/ptr_offset.rs index 673767e36..a8105ed70 100644 --- a/src/hyperlight_host/src/mem/ptr_offset.rs +++ b/src/hyperlight_host/src/mem/ptr_offset.rs @@ -32,11 +32,13 @@ pub(crate) struct Offset(u64); impl Offset { /// Get the offset representing `0` #[instrument(skip_all, parent = Span::current(), level= "Trace")] + #[allow(dead_code)] pub(super) fn zero() -> Self { Self::default() } /// round up to the nearest multiple of `alignment` + #[allow(dead_code)] pub(super) fn round_up_to(self, alignment: u64) -> Self { let remainder = self.0 % alignment; let multiples = self.0 / alignment; diff --git a/src/hyperlight_host/src/mem/shared_mem.rs b/src/hyperlight_host/src/mem/shared_mem.rs index 50c809f44..030c2c958 100644 --- a/src/hyperlight_host/src/mem/shared_mem.rs +++ b/src/hyperlight_host/src/mem/shared_mem.rs @@ -30,7 +30,7 @@ use windows::Win32::System::Memory::PAGE_READWRITE; #[cfg(target_os = "windows")] use windows::Win32::System::Memory::{ CreateFileMappingA, FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, - PAGE_EXECUTE_READWRITE, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, UnmapViewOfFile, VirtualProtect, + PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, UnmapViewOfFile, VirtualProtect, }; #[cfg(target_os = "windows")] use windows::core::PCSTR; @@ -183,11 +183,11 @@ unsafe impl Send for GuestSharedMemory {} /// /// Unfortunately, there appears to be no way to do this with defined /// behaviour in present Rust (see -/// e.g. https://github.com/rust-lang/unsafe-code-guidelines/issues/152). +/// e.g. ). /// Rust does not yet have its own defined memory model, but in the /// interim, it is widely treated as inheriting the current C/C++ /// memory models. The most immediate problem is that regardless of -/// anything else, under those memory models [1, p. 17-18; 2, p. 88], +/// anything else, under those memory models \[1, p. 17-18; 2, p. 88\], /// /// > The execution of a program contains a _data race_ if it /// > contains two [C++23: "potentially concurrent"] conflicting @@ -205,7 +205,7 @@ unsafe impl Send for GuestSharedMemory {} /// Despite Rust's de jure inheritance of the C memory model at the /// present time, the compiler in many cases de facto adheres to LLVM /// semantics, so it is worthwhile to consider what LLVM does in this -/// case as well. According to the the LangRef [3] memory model, +/// case as well. According to the the LangRef \[3\] memory model, /// loads which are involved in a race that includes at least one /// non-atomic access (whether the load or a store) return `undef`, /// making them roughly equivalent to reading uninitialized @@ -213,20 +213,20 @@ unsafe impl Send for GuestSharedMemory {} /// /// Considering a different direction, recent C++ papers have seemed /// to lean towards using `volatile` for similar use cases. For -/// example, in P1152R0 [4], JF Bastien notes that +/// example, in P1152R0 \[4\], JF Bastien notes that /// /// > We’ve shown that volatile is purposely defined to denote /// > external modifications. This happens for: /// > - Shared memory with untrusted code, where volatile is the /// > right way to avoid time-of-check time-of-use (ToCToU) -/// > races which lead to security bugs such as [PWN2OWN] and -/// > [XENXSA155]. +/// > races which lead to security bugs such as \[PWN2OWN\] and +/// > \[XENXSA155\]. /// /// Unfortunately, although this paper was adopted for C++20 (and, /// sadly, mostly un-adopted for C++23, although that does not concern /// us), the paper did not actually redefine volatile accesses or data /// races to prevent volatile accesses from racing with other accesses -/// and causing undefined behaviour. P1382R1 [5] would have amended +/// and causing undefined behaviour. P1382R1 \[5\] would have amended /// the wording of the data race definition to specifically exclude /// volatile, but, unfortunately, despite receiving a /// generally-positive reception at its first WG21 meeting more than @@ -272,8 +272,8 @@ unsafe impl Send for GuestSharedMemory {} /// the guest in this case. Unfortunately, while those operations are /// defined in LLVM, they are not presently exposed to Rust. While /// atomic fences that are not associated with memory accesses -/// (std::sync::atomic::fence) might at first glance seem to help with -/// this problem, they unfortunately do not [6]: +/// ([`std::sync::atomic::fence`]) might at first glance seem to help with +/// this problem, they unfortunately do not \[6\]: /// /// > A fence ‘A’ which has (at least) Release ordering semantics, /// > synchronizes with a fence ‘B’ with (at least) Acquire @@ -289,12 +289,12 @@ unsafe impl Send for GuestSharedMemory {} /// fence on a vmenter/vmexit between data being read and written. /// This is unsafe (not guaranteed in the type system)! /// -/// [1] N3047 C23 Working Draft. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3047.pdf -/// [2] N4950 C++23 Working Draft. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf -/// [3] LLVM Language Reference Manual, Memory Model for Concurrent Operations. https://llvm.org/docs/LangRef.html#memmodel -/// [4] P1152R0: Deprecating `volatile`. JF Bastien. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1152r0.html -/// [5] P1382R1: `volatile_load` and `volatile_store`. JF Bastien, Paul McKenney, Jeffrey Yasskin, and the indefatigable TBD. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1382r1.pdf -/// [6] Documentation for std::sync::atomic::fence. https://doc.rust-lang.org/std/sync/atomic/fn.fence.html +/// \[1\] N3047 C23 Working Draft. +/// \[2\] N4950 C++23 Working Draft. +/// \[3\] LLVM Language Reference Manual, Memory Model for Concurrent Operations. +/// \[4\] P1152R0: Deprecating `volatile`. JF Bastien. +/// \[5\] P1382R1: `volatile_load` and `volatile_store`. JF Bastien, Paul McKenney, Jeffrey Yasskin, and the indefatigable TBD. +/// \[6\] Documentation for std::sync::atomic::fence. #[derive(Clone, Debug)] pub struct HostSharedMemory { region: Arc, @@ -502,45 +502,6 @@ impl ExclusiveSharedMemory { }) } - pub(super) fn make_memory_executable(&self) -> Result<()> { - #[cfg(target_os = "windows")] - { - let mut _old_flags = PAGE_PROTECTION_FLAGS::default(); - if let Err(e) = unsafe { - VirtualProtect( - self.region.ptr as *const c_void, - self.region.size, - PAGE_EXECUTE_READWRITE, - &mut _old_flags as *mut PAGE_PROTECTION_FLAGS, - ) - } { - log_then_return!(WindowsAPIError(e.clone())); - } - } - - // make the memory executable on Linux - #[cfg(target_os = "linux")] - { - use libc::{PROT_EXEC, PROT_READ, PROT_WRITE, mprotect}; - - let res = unsafe { - mprotect( - self.region.ptr as *mut c_void, - self.region.size, - PROT_READ | PROT_WRITE | PROT_EXEC, - ) - }; - - if res != 0 { - return Err(new_error!( - "Failed to make memory executable: {:#?}", - Error::last_os_error().raw_os_error() - )); - } - } - Ok(()) - } - /// Internal helper method to get the backing memory as a mutable slice. /// /// # Safety @@ -613,14 +574,6 @@ impl ExclusiveSharedMemory { Ok(()) } - /// Return the address of memory at an offset to this `SharedMemory` checking - /// that the memory is within the bounds of the `SharedMemory`. - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn calculate_address(&self, offset: usize) -> Result { - bounds_check!(offset, 0, self.mem_size()); - Ok(self.base_addr() + offset) - } - generate_reader!(read_u8, u8); generate_reader!(read_i8, i8); generate_reader!(read_u16, u16); @@ -783,7 +736,7 @@ impl HostSharedMemory { /// patterns pub fn read(&self, offset: usize) -> Result { bounds_check!(offset, std::mem::size_of::(), self.mem_size()); - let ret = unsafe { + unsafe { let mut ret: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); { let slice: &mut [u8] = core::slice::from_raw_parts_mut( @@ -793,8 +746,7 @@ impl HostSharedMemory { self.copy_to_slice(slice, offset)?; } Ok(ret.assume_init()) - }; - ret + } } /// Write a value of type T, whose representation is the same @@ -1278,10 +1230,17 @@ mod tests { #[test] fn guard_page_testing_shim() { let tests = vec!["read", "write", "exec"]; - for test in tests { + let triple = std::env::var("TARGET_TRIPLE").ok(); + let target_args = if let Some(triple) = triple.filter(|t| !t.is_empty()) { + vec!["--target".to_string(), triple.to_string()] + } else { + vec![] + }; let status = std::process::Command::new("cargo") - .args(["test", "-p", "hyperlight-host", "--", "--ignored", test]) + .args(["test", "-p", "hyperlight-host"]) + .args(target_args) + .args(["--", "--ignored", test]) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) diff --git a/src/hyperlight_host/src/mem/shared_mem_snapshot.rs b/src/hyperlight_host/src/mem/shared_mem_snapshot.rs index d5cf565de..dd44422df 100644 --- a/src/hyperlight_host/src/mem/shared_mem_snapshot.rs +++ b/src/hyperlight_host/src/mem/shared_mem_snapshot.rs @@ -16,43 +16,62 @@ limitations under the License. use tracing::{Span, instrument}; +use super::memory_region::MemoryRegion; use super::shared_mem::SharedMemory; use crate::Result; /// A wrapper around a `SharedMemory` reference and a snapshot /// of the memory therein #[derive(Clone)] -pub(super) struct SharedMemorySnapshot { +pub(crate) struct SharedMemorySnapshot { + // Unique ID of the sandbox this snapshot was taken from + sandbox_id: u64, + // Memory of the sandbox at the time this snapshot was taken snapshot: Vec, + /// The memory regions that were mapped when this snapshot was taken (excluding initial sandbox regions) + regions: Vec, } impl SharedMemorySnapshot { /// Take a snapshot of the memory in `shared_mem`, then create a new /// instance of `Self` with the snapshot stored therein. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn new(shared_mem: &mut S) -> Result { + pub(super) fn new( + shared_mem: &mut S, + sandbox_id: u64, + regions: Vec, + ) -> Result { // TODO: Track dirty pages instead of copying entire memory let snapshot = shared_mem.with_exclusivity(|e| e.copy_all_to_vec())??; - Ok(Self { snapshot }) + Ok(Self { + sandbox_id, + snapshot, + regions, + }) } - /// Take another snapshot of the internally-stored `SharedMemory`, - /// then store it internally. + /// Copy the memory from the internally-stored memory snapshot + /// into the internally-stored `SharedMemory`. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - - pub(super) fn replace_snapshot(&mut self, shared_mem: &mut S) -> Result<()> { - self.snapshot = shared_mem.with_exclusivity(|e| e.copy_all_to_vec())??; + pub(super) fn restore_from_snapshot(&self, shared_mem: &mut S) -> Result<()> { + shared_mem.with_exclusivity(|e| e.copy_from_slice(self.snapshot.as_slice(), 0))??; Ok(()) } - /// Copy the memory from the internally-stored memory snapshot - /// into the internally-stored `SharedMemory` - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn restore_from_snapshot( - &mut self, - shared_mem: &mut S, - ) -> Result<()> { - shared_mem.with_exclusivity(|e| e.copy_from_slice(self.snapshot.as_slice(), 0))? + /// The id of the sandbox this snapshot was taken from. + pub(crate) fn sandbox_id(&self) -> u64 { + self.sandbox_id + } + + /// Get the mapped regions from this snapshot + pub(crate) fn regions(&self) -> &[MemoryRegion] { + &self.regions + } + + /// Return the size of the snapshot in bytes. + #[instrument(skip_all, parent = Span::current(), level= "Trace")] + pub(super) fn mem_size(&self) -> usize { + self.snapshot.len() } } @@ -63,36 +82,58 @@ mod tests { use crate::mem::shared_mem::ExclusiveSharedMemory; #[test] - fn restore_replace() { - let mut data1 = vec![b'a', b'b', b'c']; - data1.resize_with(PAGE_SIZE_USIZE, || 0); - let data2 = data1.iter().map(|b| b + 1).collect::>(); + fn restore() { + // Simplified version of the original test + let data1 = vec![b'a'; PAGE_SIZE_USIZE]; + let data2 = vec![b'b'; PAGE_SIZE_USIZE]; + let mut gm = ExclusiveSharedMemory::new(PAGE_SIZE_USIZE).unwrap(); - gm.copy_from_slice(data1.as_slice(), 0).unwrap(); - let mut snap = super::SharedMemorySnapshot::new(&mut gm).unwrap(); - { - // after the first snapshot is taken, make sure gm has the equivalent - // of data1 - assert_eq!(data1, gm.copy_all_to_vec().unwrap()); - } - - { - // modify gm with data2 rather than data1 and restore from - // snapshot. we should have the equivalent of data1 again - gm.copy_from_slice(data2.as_slice(), 0).unwrap(); - assert_eq!(data2, gm.copy_all_to_vec().unwrap()); - snap.restore_from_snapshot(&mut gm).unwrap(); - assert_eq!(data1, gm.copy_all_to_vec().unwrap()); - } - { - // modify gm with data2, then retake the snapshot and restore - // from the new snapshot. we should have the equivalent of data2 - gm.copy_from_slice(data2.as_slice(), 0).unwrap(); - assert_eq!(data2, gm.copy_all_to_vec().unwrap()); - snap.replace_snapshot(&mut gm).unwrap(); - assert_eq!(data2, gm.copy_all_to_vec().unwrap()); - snap.restore_from_snapshot(&mut gm).unwrap(); - assert_eq!(data2, gm.copy_all_to_vec().unwrap()); - } + gm.copy_from_slice(&data1, 0).unwrap(); + + // Take snapshot of data1 + let snapshot = super::SharedMemorySnapshot::new(&mut gm, 0, Vec::new()).unwrap(); + + // Modify memory to data2 + gm.copy_from_slice(&data2, 0).unwrap(); + assert_eq!(gm.as_slice(), &data2[..]); + + // Restore should bring back data1 + snapshot.restore_from_snapshot(&mut gm).unwrap(); + assert_eq!(gm.as_slice(), &data1[..]); + } + + #[test] + fn snapshot_mem_size() { + let size = PAGE_SIZE_USIZE * 2; + let mut gm = ExclusiveSharedMemory::new(size).unwrap(); + + let snapshot = super::SharedMemorySnapshot::new(&mut gm, 0, Vec::new()).unwrap(); + assert_eq!(snapshot.mem_size(), size); + } + + #[test] + fn multiple_snapshots_independent() { + let mut gm = ExclusiveSharedMemory::new(PAGE_SIZE_USIZE).unwrap(); + + // Create first snapshot with pattern A + let pattern_a = vec![0xAA; PAGE_SIZE_USIZE]; + gm.copy_from_slice(&pattern_a, 0).unwrap(); + let snapshot_a = super::SharedMemorySnapshot::new(&mut gm, 1, Vec::new()).unwrap(); + + // Create second snapshot with pattern B + let pattern_b = vec![0xBB; PAGE_SIZE_USIZE]; + gm.copy_from_slice(&pattern_b, 0).unwrap(); + let snapshot_b = super::SharedMemorySnapshot::new(&mut gm, 2, Vec::new()).unwrap(); + + // Clear memory + gm.copy_from_slice(&[0; PAGE_SIZE_USIZE], 0).unwrap(); + + // Restore snapshot A + snapshot_a.restore_from_snapshot(&mut gm).unwrap(); + assert_eq!(gm.as_slice(), &pattern_a[..]); + + // Restore snapshot B + snapshot_b.restore_from_snapshot(&mut gm).unwrap(); + assert_eq!(gm.as_slice(), &pattern_b[..]); } } diff --git a/src/hyperlight_host/src/metrics/mod.rs b/src/hyperlight_host/src/metrics/mod.rs index 3181d3b89..3a630fa44 100644 --- a/src/hyperlight_host/src/metrics/mod.rs +++ b/src/hyperlight_host/src/metrics/mod.rs @@ -93,8 +93,6 @@ mod tests { use metrics_util::CompositeKey; use super::*; - use crate::sandbox_state::sandbox::EvolvableSandbox; - use crate::sandbox_state::transition::Noop; use crate::{GuestBinary, UninitializedSandbox}; #[test] @@ -108,7 +106,7 @@ mod tests { ) .unwrap(); - let mut multi = uninit.evolve(Noop::default()).unwrap(); + let mut multi = uninit.evolve().unwrap(); let interrupt_handle = multi.interrupt_handle(); // interrupt the guest function call to "Spin" after 1 second @@ -118,12 +116,10 @@ mod tests { }); multi - .call_guest_function_by_name::("PrintOutput", "Hello".to_string()) + .call::("PrintOutput", "Hello".to_string()) .unwrap(); - multi - .call_guest_function_by_name::("Spin", ()) - .unwrap_err(); + multi.call::("Spin", ()).unwrap_err(); thread.join().unwrap(); snapshotter.snapshot() @@ -137,11 +133,7 @@ mod tests { if #[cfg(feature = "function_call_metrics")] { use metrics::Label; - let expected_num_metrics = if cfg!(all(feature = "seccomp", target_os = "linux")) { - 3 // if seccomp enabled, the host call duration metric is emitted on a separate thread which this local recorder doesn't capture - } else { - 4 - }; + let expected_num_metrics = 4; // Verify that the histogram metrics are recorded correctly assert_eq!(snapshot.len(), expected_num_metrics); @@ -189,25 +181,6 @@ mod tests { ), "Histogram metric does not match expected value" ); - - if !cfg!(all(feature = "seccomp", target_os = "linux")) { - // 4. Host call duration - let histogram_key = CompositeKey::new( - metrics_util::MetricKind::Histogram, - Key::from_parts( - METRIC_HOST_FUNC_DURATION, - vec![Label::new("function_name", "HostPrint")], - ), - ); - let histogram_value = &snapshot.get(&histogram_key).unwrap().2; - assert!( - matches!( - histogram_value, - metrics_util::debugging::DebugValue::Histogram(histogram) if histogram.len() == 1 - ), - "Histogram metric does not match expected value" - ); - } } else { // Verify that the counter metrics are recorded correctly assert_eq!(snapshot.len(), 1); diff --git a/src/hyperlight_host/src/sandbox/host_funcs.rs b/src/hyperlight_host/src/sandbox/host_funcs.rs index 158ff87d5..52160539a 100644 --- a/src/hyperlight_host/src/sandbox/host_funcs.rs +++ b/src/hyperlight_host/src/sandbox/host_funcs.rs @@ -25,7 +25,6 @@ use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionD use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use tracing::{Span, instrument}; -use super::ExtraAllowedSyscall; use crate::HyperlightError::HostFunctionNotFound; use crate::func::host_functions::TypeErasedHostFunction; use crate::mem::mgr::SandboxMemoryManager; @@ -58,7 +57,6 @@ impl From<&mut FunctionRegistry> for HostFunctionDetails { pub struct FunctionEntry { pub function: TypeErasedHostFunction, - pub extra_allowed_syscalls: Option>, pub parameter_types: &'static [ParameterType], pub return_type: ReturnType, } @@ -93,6 +91,7 @@ impl FunctionRegistry { /// Return `Ok` if the function was found and was of the right signature, /// and `Err` otherwise. #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + #[allow(dead_code)] pub(super) fn host_print(&mut self, msg: String) -> Result { let res = self.call_host_func_impl("HostPrint", vec![ParameterValue::String(msg)])?; res.try_into() @@ -118,7 +117,6 @@ impl FunctionRegistry { fn call_host_func_impl(&self, name: &str, args: Vec) -> Result { let FunctionEntry { function, - extra_allowed_syscalls, parameter_types: _, return_type: _, } = self @@ -126,10 +124,8 @@ impl FunctionRegistry { .get(name) .ok_or_else(|| HostFunctionNotFound(name.to_string()))?; - // Create a new thread when seccomp is enabled on Linux - maybe_with_seccomp(name, extra_allowed_syscalls.as_deref(), || { - crate::metrics::maybe_time_and_emit_host_call(name, || function.call(args)) - }) + // Make the host function call + crate::metrics::maybe_time_and_emit_host_call(name, || function.call(args)) } } @@ -152,57 +148,3 @@ pub(super) fn default_writer_func(s: String) -> Result { } } } - -#[cfg(all(feature = "seccomp", target_os = "linux"))] -fn maybe_with_seccomp( - name: &str, - syscalls: Option<&[ExtraAllowedSyscall]>, - f: impl FnOnce() -> Result + Send, -) -> Result { - use crate::seccomp::guest::get_seccomp_filter_for_host_function_worker_thread; - - // Use a scoped thread so that we can pass around references without having to clone them. - crossbeam::thread::scope(|s| { - s.builder() - .name(format!("Host Function Worker Thread for: {name:?}")) - .spawn(move |_| { - let seccomp_filter = get_seccomp_filter_for_host_function_worker_thread(syscalls)?; - seccomp_filter - .iter() - .try_for_each(|filter| seccompiler::apply_filter(filter))?; - - // We have a `catch_unwind` here because, if a disallowed syscall is issued, - // we handle it by panicking. This is to avoid returning execution to the - // offending host function—for two reasons: (1) if a host function is issuing - // disallowed syscalls, it could be unsafe to return to, and (2) returning - // execution after trapping the disallowed syscall can lead to UB (e.g., try - // running a host function that attempts to sleep without `SYS_clock_nanosleep`, - // you'll block the syscall but panic in the aftermath). - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { - Ok(val) => val, - Err(err) => { - if let Some(crate::HyperlightError::DisallowedSyscall) = - err.downcast_ref::() - { - return Err(crate::HyperlightError::DisallowedSyscall); - } - - crate::log_then_return!("Host function {} panicked", name); - } - } - })? - .join() - .map_err(|_| new_error!("Error joining thread executing host function"))? - }) - .map_err(|_| new_error!("Error joining thread executing host function"))? -} - -#[cfg(not(all(feature = "seccomp", target_os = "linux")))] -fn maybe_with_seccomp( - _name: &str, - _syscalls: Option<&[ExtraAllowedSyscall]>, - f: impl FnOnce() -> Result + Send, -) -> Result { - // No seccomp, just call the function - f() -} diff --git a/src/hyperlight_host/src/sandbox/hypervisor.rs b/src/hyperlight_host/src/sandbox/hypervisor.rs index 7cd1154ee..114083c78 100644 --- a/src/hyperlight_host/src/sandbox/hypervisor.rs +++ b/src/hyperlight_host/src/sandbox/hypervisor.rs @@ -77,3 +77,9 @@ pub(crate) enum HypervisorType { #[cfg(target_os = "windows")] Whp, } + +// Compiler error if no hypervisor type is available +#[cfg(not(any(kvm, mshv, target_os = "windows")))] +compile_error!( + "No hypervisor type is available for the current platform. Please enable either the `kvm` or `mshv` cargo feature." +); diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 8d1e27918..765a1a898 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -14,48 +14,61 @@ See the License for the specific language governing permissions and limitations under the License. */ +use std::collections::HashSet; +#[cfg(unix)] +use std::os::fd::AsRawFd; +#[cfg(unix)] +use std::os::linux::fs::MetadataExt; +use std::path::Path; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +use flatbuffers::FlatBufferBuilder; use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType}; use hyperlight_common::flatbuffer_wrappers::function_types::{ ParameterValue, ReturnType, ReturnValue, }; +use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; +use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity; use tracing::{Span, instrument}; +use super::Callable; use super::host_funcs::FunctionRegistry; -use super::{MemMgrWrapper, WrapperGetter}; -use crate::func::call_ctx::MultiUseGuestCallContext; -use crate::func::guest_err::check_for_guest_error; +use super::snapshot::Snapshot; +use crate::HyperlightError::{self, SnapshotSandboxMismatch}; use crate::func::{ParameterTuple, SupportedReturnType}; -#[cfg(gdb)] -use crate::hypervisor::handlers::DbgMemAccessHandlerWrapper; -use crate::hypervisor::handlers::{MemAccessHandlerCaller, OutBHandlerCaller}; use crate::hypervisor::{Hypervisor, InterruptHandle}; +#[cfg(unix)] +use crate::mem::memory_region::MemoryRegionType; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::RawPtr; use crate::mem::shared_mem::HostSharedMemory; -use crate::metrics::maybe_time_and_emit_guest_call; -use crate::sandbox_state::sandbox::{DevolvableSandbox, EvolvableSandbox, Sandbox}; -use crate::sandbox_state::transition::{MultiUseContextCallback, Noop}; -use crate::{HyperlightError, Result}; +use crate::metrics::{ + METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call, +}; +use crate::{Result, log_then_return}; -/// A sandbox that supports being used Multiple times. -/// The implication of being used multiple times is two-fold: -/// -/// 1. The sandbox can be used to call guest functions multiple times, each time a -/// guest function is called the state of the sandbox is reset to the state it was in before the call was made. +/// Global counter for assigning unique IDs to sandboxes +static SANDBOX_ID_COUNTER: AtomicU64 = AtomicU64::new(0); + +/// A fully initialized sandbox that can execute guest functions multiple times. /// -/// 2. A MultiUseGuestCallContext can be created from the sandbox and used to make multiple guest function calls to the Sandbox. -/// in this case the state of the sandbox is not reset until the context is finished and the `MultiUseSandbox` is returned. +/// Guest functions can be called repeatedly while maintaining state between calls. +/// The sandbox supports creating snapshots and restoring to previous states. pub struct MultiUseSandbox { + /// Unique identifier for this sandbox instance + id: u64, // We need to keep a reference to the host functions, even if the compiler marks it as unused. The compiler cannot detect our dynamic usages of the host function in `HyperlightFunction::call`. pub(super) _host_funcs: Arc>, - pub(crate) mem_mgr: MemMgrWrapper, + pub(crate) mem_mgr: SandboxMemoryManager, vm: Box, - out_hdl: Arc>, - mem_hdl: Arc>, dispatch_ptr: RawPtr, #[cfg(gdb)] - dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + dbg_mem_access_fn: Arc>>, + /// If the current state of the sandbox has been captured in a snapshot, + /// that snapshot is stored here. + snapshot: Option, } impl MultiUseSandbox { @@ -67,113 +80,295 @@ impl MultiUseSandbox { #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(super) fn from_uninit( host_funcs: Arc>, - mgr: MemMgrWrapper, + mgr: SandboxMemoryManager, vm: Box, - out_hdl: Arc>, - mem_hdl: Arc>, dispatch_ptr: RawPtr, - #[cfg(gdb)] dbg_mem_access_fn: DbgMemAccessHandlerWrapper, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, ) -> MultiUseSandbox { Self { + id: SANDBOX_ID_COUNTER.fetch_add(1, Ordering::Relaxed), _host_funcs: host_funcs, mem_mgr: mgr, vm, - out_hdl, - mem_hdl, dispatch_ptr, #[cfg(gdb)] dbg_mem_access_fn, + snapshot: None, } } - /// Create a new `MultiUseCallContext` suitable for making 0 or more - /// calls to guest functions within the same context. + /// Creates a snapshot of the sandbox's current memory state. + /// + /// The snapshot is tied to this specific sandbox instance and can only be + /// restored to the same sandbox it was created from. + /// + /// # Examples /// - /// Since this function consumes `self`, the returned - /// `MultiUseGuestCallContext` is guaranteed mutual exclusion for calling - /// functions within the sandbox. This guarantee is enforced at compile - /// time, and no locks, atomics, or any other mutual exclusion mechanisms - /// are used at runtime. + /// ```no_run + /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary}; + /// # fn example() -> Result<(), Box> { + /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new( + /// GuestBinary::FilePath("guest.bin".into()), + /// None + /// )?.evolve()?; + /// + /// // Modify sandbox state + /// sandbox.call_guest_function_by_name::("SetValue", 42)?; + /// + /// // Create snapshot belonging to this sandbox + /// let snapshot = sandbox.snapshot()?; + /// # Ok(()) + /// # } + /// ``` + #[instrument(err(Debug), skip_all, parent = Span::current())] + pub fn snapshot(&mut self) -> Result { + if let Some(snapshot) = &self.snapshot { + return Ok(snapshot.clone()); + } + let mapped_regions_iter = self.vm.get_mapped_regions(); + let mapped_regions_vec: Vec = mapped_regions_iter.cloned().collect(); + let memory_snapshot = self.mem_mgr.snapshot(self.id, mapped_regions_vec)?; + let inner = Arc::new(memory_snapshot); + let snapshot = Snapshot { inner }; + self.snapshot = Some(snapshot.clone()); + Ok(snapshot) + } + + /// Restores the sandbox's memory to a previously captured snapshot state. /// - /// If you have called this function, have a `MultiUseGuestCallContext`, - /// and wish to "return" it to a `MultiUseSandbox`, call the `finish` - /// method on the context. + /// The snapshot must have been created from this same sandbox instance. + /// Attempting to restore a snapshot from a different sandbox will return + /// a [`SnapshotSandboxMismatch`](crate::HyperlightError::SnapshotSandboxMismatch) error. /// - /// Example usage (compiled as a "no_run" doctest since the test binary - /// will not be found): + /// # Examples /// /// ```no_run - /// use hyperlight_host::sandbox::{UninitializedSandbox, MultiUseSandbox}; - /// use hyperlight_common::flatbuffer_wrappers::function_types::{ReturnType, ParameterValue, ReturnValue}; - /// use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; - /// use hyperlight_host::sandbox_state::transition::Noop; - /// use hyperlight_host::GuestBinary; - /// - /// // First, create a new uninitialized sandbox, then evolve it to become - /// // an initialized, single-use one. - /// let u_sbox = UninitializedSandbox::new( - /// GuestBinary::FilePath("some_guest_binary".to_string()), - /// None, - /// ).unwrap(); - /// let sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap(); - /// // Next, create a new call context from the single-use sandbox. - /// // After this line, your code will not compile if you try to use the - /// // original `sbox` variable. - /// let mut ctx = sbox.new_call_context(); - /// - /// // Do a guest call with the context. Assumes that the loaded binary - /// // ("some_guest_binary") has a function therein called "SomeGuestFunc" - /// // that takes a single integer argument and returns an integer. - /// match ctx.call( - /// "SomeGuestFunc", - /// ReturnType::Int, - /// Some(vec![ParameterValue::Int(1)]) - /// ) { - /// Ok(ReturnValue::Int(i)) => println!( - /// "got successful return value {}", - /// i, - /// ), - /// other => panic!( - /// "failed to get return value as expected ({:?})", - /// other, - /// ), - /// }; - /// // You can make further calls with the same context if you want. - /// // Otherwise, `ctx` will be dropped and all resources, including the - /// // underlying `MultiUseSandbox`, will be released and no further - /// // contexts can be created from that sandbox. - /// // - /// // If you want to avoid - /// // that behavior, call `finish` to convert the context back to - /// // the original `MultiUseSandbox`, as follows: - /// let _orig_sbox = ctx.finish(); - /// // Now, you can operate on the original sandbox again (i.e. add more - /// // host functions etc...), create new contexts, and so on. + /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary}; + /// # fn example() -> Result<(), Box> { + /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new( + /// GuestBinary::FilePath("guest.bin".into()), + /// None + /// )?.evolve()?; + /// + /// // Take initial snapshot from this sandbox + /// let snapshot = sandbox.snapshot()?; + /// + /// // Modify sandbox state + /// sandbox.call_guest_function_by_name::("SetValue", 100)?; + /// let value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?; + /// assert_eq!(value, 100); + /// + /// // Restore to previous state (same sandbox) + /// sandbox.restore(&snapshot)?; + /// let restored_value: i32 = sandbox.call_guest_function_by_name("GetValue", ())?; + /// assert_eq!(restored_value, 0); // Back to initial state + /// # Ok(()) + /// # } /// ``` - #[instrument(skip_all, parent = Span::current())] - pub fn new_call_context(self) -> MultiUseGuestCallContext { - MultiUseGuestCallContext::start(self) + #[instrument(err(Debug), skip_all, parent = Span::current())] + pub fn restore(&mut self, snapshot: &Snapshot) -> Result<()> { + if let Some(snap) = &self.snapshot + && Arc::ptr_eq(&snap.inner, &snapshot.inner) + { + // If the snapshot is already the current one, no need to restore + return Ok(()); + } + + if self.id != snapshot.inner.sandbox_id() { + return Err(SnapshotSandboxMismatch); + } + + self.mem_mgr.restore_snapshot(&snapshot.inner)?; + + let current_regions: HashSet<_> = self.vm.get_mapped_regions().cloned().collect(); + let snapshot_regions: HashSet<_> = snapshot.inner.regions().iter().cloned().collect(); + + let regions_to_unmap = current_regions.difference(&snapshot_regions); + let regions_to_map = snapshot_regions.difference(¤t_regions); + + for region in regions_to_unmap { + unsafe { self.vm.unmap_region(region)? }; + } + + for region in regions_to_map { + unsafe { self.vm.map_region(region)? }; + } + + // The restored snapshot is now our most current snapshot + self.snapshot = Some(snapshot.clone()); + + Ok(()) } - /// Call a guest function by name, with the given return type and arguments. + /// Calls a guest function by name with the specified arguments. + /// + /// Changes made to the sandbox during execution are *not* persisted. + /// + /// # Examples + /// + /// ```no_run + /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary}; + /// # fn example() -> Result<(), Box> { + /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new( + /// GuestBinary::FilePath("guest.bin".into()), + /// None + /// )?.evolve()?; + /// + /// // Call function with no arguments + /// let result: i32 = sandbox.call_guest_function_by_name("GetCounter", ())?; + /// + /// // Call function with single argument + /// let doubled: i32 = sandbox.call_guest_function_by_name("Double", 21)?; + /// assert_eq!(doubled, 42); + /// + /// // Call function with multiple arguments + /// let sum: i32 = sandbox.call_guest_function_by_name("Add", (10, 32))?; + /// assert_eq!(sum, 42); + /// + /// // Call function returning string + /// let message: String = sandbox.call_guest_function_by_name("Echo", "Hello, World!".to_string())?; + /// assert_eq!(message, "Hello, World!"); + /// # Ok(()) + /// # } + /// ``` + #[doc(hidden)] + #[deprecated( + since = "0.8.0", + note = "Deprecated in favour of call and snapshot/restore." + )] #[instrument(err(Debug), skip(self, args), parent = Span::current())] pub fn call_guest_function_by_name( &mut self, func_name: &str, args: impl ParameterTuple, ) -> Result { + let snapshot = self.snapshot()?; + let res = self.call(func_name, args); + self.restore(&snapshot)?; + res + } + + /// Calls a guest function by name with the specified arguments. + /// + /// Changes made to the sandbox during execution are persisted. + /// + /// # Examples + /// + /// ```no_run + /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary}; + /// # fn example() -> Result<(), Box> { + /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new( + /// GuestBinary::FilePath("guest.bin".into()), + /// None + /// )?.evolve()?; + /// + /// // Call function with no arguments + /// let result: i32 = sandbox.call("GetCounter", ())?; + /// + /// // Call function with single argument + /// let doubled: i32 = sandbox.call("Double", 21)?; + /// assert_eq!(doubled, 42); + /// + /// // Call function with multiple arguments + /// let sum: i32 = sandbox.call("Add", (10, 32))?; + /// assert_eq!(sum, 42); + /// + /// // Call function returning string + /// let message: String = sandbox.call("Echo", "Hello, World!".to_string())?; + /// assert_eq!(message, "Hello, World!"); + /// # Ok(()) + /// # } + /// ``` + #[instrument(err(Debug), skip(self, args), parent = Span::current())] + pub fn call( + &mut self, + func_name: &str, + args: impl ParameterTuple, + ) -> Result { + // Reset snapshot since we are mutating the sandbox state + self.snapshot = None; maybe_time_and_emit_guest_call(func_name, || { let ret = self.call_guest_function_by_name_no_reset( func_name, Output::TYPE, args.into_value(), ); - self.restore_state()?; Output::from_value(ret?) }) } - /// This function is kept here for fuzz testing the parameter and return types + /// Maps a region of host memory into the sandbox address space. + /// + /// The base address and length must meet platform alignment requirements + /// (typically page-aligned). The `region_type` field is ignored as guest + /// page table entries are not created. + /// + /// # Safety + /// + /// The caller must ensure the host memory region remains valid and unmodified + /// for the lifetime of `self`. + #[instrument(err(Debug), skip(self, rgn), parent = Span::current())] + pub unsafe fn map_region(&mut self, rgn: &MemoryRegion) -> Result<()> { + if rgn.flags.contains(MemoryRegionFlags::STACK_GUARD) { + // Stack guard pages are an internal implementation detail + // (which really should be moved into the guest) + log_then_return!("Cannot map host memory as a stack guard page"); + } + if rgn.flags.contains(MemoryRegionFlags::WRITE) { + // TODO: Implement support for writable mappings, which + // need to be registered with the memory manager so that + // writes can be rolled back when necessary. + log_then_return!("TODO: Writable mappings not yet supported"); + } + // Reset snapshot since we are mutating the sandbox state + self.snapshot = None; + unsafe { self.vm.map_region(rgn) }?; + self.mem_mgr.mapped_rgns += 1; + Ok(()) + } + + /// Map the contents of a file into the guest at a particular address + /// + /// Returns the length of the mapping in bytes. + #[instrument(err(Debug), skip(self, _fp, _guest_base), parent = Span::current())] + pub fn map_file_cow(&mut self, _fp: &Path, _guest_base: u64) -> Result { + #[cfg(windows)] + log_then_return!("mmap'ing a file into the guest is not yet supported on Windows"); + #[cfg(unix)] + unsafe { + let file = std::fs::File::options().read(true).write(true).open(_fp)?; + let file_size = file.metadata()?.st_size(); + let page_size = page_size::get(); + let size = (file_size as usize).div_ceil(page_size) * page_size; + let base = libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC, + libc::MAP_PRIVATE, + file.as_raw_fd(), + 0, + ); + if base == libc::MAP_FAILED { + log_then_return!("mmap error: {:?}", std::io::Error::last_os_error()); + } + + if let Err(err) = self.map_region(&MemoryRegion { + host_region: base as usize..base.wrapping_add(size) as usize, + guest_region: _guest_base as usize.._guest_base as usize + size, + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::Heap, + }) { + libc::munmap(base, size); + return Err(err); + }; + + Ok(size as u64) + } + } + + /// Calls a guest function with type-erased parameters and return values. + /// + /// This function is used for fuzz testing parameter and return type handling. #[cfg(feature = "fuzzing")] #[instrument(err(Debug), skip(self, args), parent = Span::current())] pub fn call_type_erased_guest_function_by_name( @@ -182,76 +377,154 @@ impl MultiUseSandbox { ret_type: ReturnType, args: Vec, ) -> Result { + // Reset snapshot since we are mutating the sandbox state + self.snapshot = None; maybe_time_and_emit_guest_call(func_name, || { - let ret = self.call_guest_function_by_name_no_reset(func_name, ret_type, args); - self.restore_state()?; - ret + self.call_guest_function_by_name_no_reset(func_name, ret_type, args) }) } - /// Restore the Sandbox's state - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - pub(crate) fn restore_state(&mut self) -> Result<()> { - let mem_mgr = self.mem_mgr.unwrap_mgr_mut(); - mem_mgr.restore_state_from_last_snapshot() - } - - pub(crate) fn call_guest_function_by_name_no_reset( + fn call_guest_function_by_name_no_reset( &mut self, function_name: &str, return_type: ReturnType, args: Vec, ) -> Result { - let fc = FunctionCall::new( - function_name.to_string(), - Some(args), - FunctionCallType::Guest, - return_type, - ); + let res = (|| { + let estimated_capacity = estimate_flatbuffer_capacity(function_name, &args); + + let fc = FunctionCall::new( + function_name.to_string(), + Some(args), + FunctionCallType::Guest, + return_type, + ); - let buffer: Vec = fc - .try_into() - .map_err(|_| HyperlightError::Error("Failed to serialize FunctionCall".to_string()))?; + let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity); + let buffer = fc.encode(&mut builder); - self.get_mgr_wrapper_mut() - .as_mut() - .write_guest_function_call(&buffer)?; + self.mem_mgr.write_guest_function_call(buffer)?; - self.vm.dispatch_call_from_host( - self.dispatch_ptr.clone(), - self.out_hdl.clone(), - self.mem_hdl.clone(), - #[cfg(gdb)] - self.dbg_mem_access_fn.clone(), - )?; + self.vm.dispatch_call_from_host( + self.dispatch_ptr.clone(), + #[cfg(gdb)] + self.dbg_mem_access_fn.clone(), + )?; + + self.mem_mgr.check_stack_guard()?; - self.check_stack_guard()?; - check_for_guest_error(self.get_mgr_wrapper_mut())?; + let guest_result = self.mem_mgr.get_guest_function_call_result()?.into_inner(); + + match guest_result { + Ok(val) => Ok(val), + Err(guest_error) => { + metrics::counter!( + METRIC_GUEST_ERROR, + METRIC_GUEST_ERROR_LABEL_CODE => (guest_error.code as u64).to_string() + ) + .increment(1); - self.get_mgr_wrapper_mut() - .as_mut() - .get_guest_function_call_result() + Err(match guest_error.code { + ErrorCode::StackOverflow => HyperlightError::StackOverflow(), + _ => HyperlightError::GuestError(guest_error.code, guest_error.message), + }) + } + } + })(); + + // In the happy path we do not need to clear io-buffers from the host because: + // - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::()` + // - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result` + // - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call` + // - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value` + if res.is_err() { + self.mem_mgr.clear_io_buffers(); + } + res } - /// Get a handle to the interrupt handler for this sandbox, - /// capable of interrupting guest execution. + /// Returns a handle for interrupting guest execution. + /// + /// # Examples + /// + /// ```no_run + /// # use hyperlight_host::{MultiUseSandbox, UninitializedSandbox, GuestBinary}; + /// # use std::thread; + /// # use std::time::Duration; + /// # fn example() -> Result<(), Box> { + /// let mut sandbox: MultiUseSandbox = UninitializedSandbox::new( + /// GuestBinary::FilePath("guest.bin".into()), + /// None + /// )?.evolve()?; + /// + /// // Get interrupt handle before starting long-running operation + /// let interrupt_handle = sandbox.interrupt_handle(); + /// + /// // Spawn thread to interrupt after timeout + /// let handle_clone = interrupt_handle.clone(); + /// thread::spawn(move || { + /// thread::sleep(Duration::from_secs(5)); + /// handle_clone.kill(); + /// }); + /// + /// // This call may be interrupted by the spawned thread + /// let result = sandbox.call_guest_function_by_name::("LongRunningFunction", ()); + /// # Ok(()) + /// # } + /// ``` pub fn interrupt_handle(&self) -> Arc { self.vm.interrupt_handle() } -} + /// Generate a crash dump of the current state of the VM underlying this sandbox. + /// + /// Creates an ELF core dump file that can be used for debugging. The dump + /// captures the current state of the sandbox including registers, memory regions, + /// and other execution context. + /// + /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR` + /// environment variable. If not set, it defaults to the system's temporary directory. + /// + /// This is only available when the `crashdump` feature is enabled and then only if the sandbox + /// is also configured to allow core dumps (which is the default behavior). + /// + /// This can be useful for generating a crash dump from gdb when trying to debug issues in the + /// guest that dont cause crashes (e.g. a guest function that does not return) + /// + /// # Examples + /// + /// Attach to your running process with gdb and call this function: + /// + /// ```shell + /// sudo gdb -p + /// (gdb) info threads + /// # find the thread that is running the guest function you want to debug + /// (gdb) thread + /// # switch to the frame where you have access to your MultiUseSandbox instance + /// (gdb) backtrace + /// (gdb) frame + /// # get the pointer to your MultiUseSandbox instance + /// # Get the sandbox pointer + /// (gdb) print sandbox + /// # Call the crashdump function + /// call sandbox.generate_crashdump() + /// ``` + /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var). + /// + #[cfg(crashdump)] + #[instrument(err(Debug), skip_all, parent = Span::current())] -impl WrapperGetter for MultiUseSandbox { - fn get_mgr_wrapper(&self) -> &MemMgrWrapper { - &self.mem_mgr - } - fn get_mgr_wrapper_mut(&mut self) -> &mut MemMgrWrapper { - &mut self.mem_mgr + pub fn generate_crashdump(&self) -> Result<()> { + crate::hypervisor::crashdump::generate_crashdump(self.vm.as_ref()) } } -impl Sandbox for MultiUseSandbox { - fn check_stack_guard(&self) -> Result { - self.mem_mgr.check_stack_guard() +impl Callable for MultiUseSandbox { + fn call( + &mut self, + func_name: &str, + args: impl ParameterTuple, + ) -> Result { + self.call(func_name, args) } } @@ -263,285 +536,164 @@ impl std::fmt::Debug for MultiUseSandbox { } } -impl DevolvableSandbox> - for MultiUseSandbox -{ - /// Consume `self` and move it back to a `MultiUseSandbox` with previous state. - /// - /// The purpose of this function is to allow multiple states to be associated with a single MultiUseSandbox. - /// - /// An implementation such as HyperlightJs or HyperlightWasm can use this to call guest functions to load JS or WASM code and then evolve the sandbox causing state to be captured. - /// The new MultiUseSandbox can then be used to call guest functions to execute the loaded code. - /// The devolve can be used to return the MultiUseSandbox to the state before the code was loaded. Thus avoiding initialisation overhead - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn devolve(mut self, _tsn: Noop) -> Result { - self.mem_mgr - .unwrap_mgr_mut() - .pop_and_restore_state_from_snapshot()?; - Ok(self) - } -} - -impl<'a, F> - EvolvableSandbox< - MultiUseSandbox, - MultiUseSandbox, - MultiUseContextCallback<'a, MultiUseSandbox, F>, - > for MultiUseSandbox -where - F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()> + 'a, -{ - /// The purpose of this function is to allow multiple states to be associated with a single MultiUseSandbox. - /// - /// An implementation such as HyperlightJs or HyperlightWasm can use this to call guest functions to load JS or WASM code and then evolve the sandbox causing state to be captured. - /// The new MultiUseSandbox can then be used to call guest functions to execute the loaded code. - /// - /// The evolve function creates a new MultiUseCallContext which is then passed to a callback function allowing the - /// callback function to call guest functions as part of the evolve process, once the callback function is complete - /// the context is finished using a crate internal method that does not restore the prior state of the Sandbox. - /// It then creates a mew memory snapshot on the snapshot stack and returns the MultiUseSandbox - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn evolve( - self, - transition_func: MultiUseContextCallback<'a, MultiUseSandbox, F>, - ) -> Result { - let mut ctx = self.new_call_context(); - transition_func.call(&mut ctx)?; - let mut sbox = ctx.finish_no_reset(); - sbox.mem_mgr.unwrap_mgr_mut().push_state()?; - Ok(sbox) - } -} - #[cfg(test)] mod tests { use std::sync::{Arc, Barrier}; use std::thread; + use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_testing::simple_guest_as_string; - use crate::func::call_ctx::MultiUseGuestCallContext; - use crate::sandbox::{Callable, SandboxConfiguration}; - use crate::sandbox_state::sandbox::{DevolvableSandbox, EvolvableSandbox}; - use crate::sandbox_state::transition::{MultiUseContextCallback, Noop}; + #[cfg(target_os = "linux")] + use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; + #[cfg(target_os = "linux")] + use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _}; + use crate::sandbox::SandboxConfiguration; use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox}; - // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (1K) and heap(14K). - // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest. + /// Make sure input/output buffers are properly reset after guest call (with host call) #[test] - fn test_with_small_stack_and_heap() { - let mut cfg = SandboxConfiguration::default(); - cfg.set_heap_size(20 * 1024); - cfg.set_stack_size(16 * 1024); - - let sbox1: MultiUseSandbox = { - let path = simple_guest_as_string().unwrap(); - let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap(); - u_sbox.evolve(Noop::default()) - } - .unwrap(); - - let mut ctx = sbox1.new_call_context(); + fn host_func_error() { + let path = simple_guest_as_string().unwrap(); + let mut sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + sandbox + .register("HostError", || -> Result<()> { + Err(HyperlightError::Error("hi".to_string())) + }) + .unwrap(); + let mut sandbox = sandbox.evolve().unwrap(); + // will exhaust io if leaky for _ in 0..1000 { - ctx.call::("Echo", "hello".to_string()).unwrap(); - } + let result = sandbox + .call::( + "CallGivenParamlessHostFuncThatReturnsI64", + "HostError".to_string(), + ) + .unwrap_err(); - let sbox2: MultiUseSandbox = { - let path = simple_guest_as_string().unwrap(); - let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap(); - u_sbox.evolve(Noop::default()) + assert!( + matches!(result, HyperlightError::GuestError(code, msg) if code == ErrorCode::HostFunctionError && msg == "hi"), + ); } - .unwrap(); - - let mut ctx = sbox2.new_call_context(); + } - for i in 0..1000 { - ctx.call::( - "PrintUsingPrintf", - format!("Hello World {}\n", i).to_string(), - ) + #[test] + fn call_host_func_expect_error() { + let path = simple_guest_as_string().unwrap(); + let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + let mut sandbox = sandbox.evolve().unwrap(); + sandbox + .call::<()>("CallHostExpectError", "SomeUnknownHostFunc".to_string()) .unwrap(); + } + + /// Make sure input/output buffers are properly reset after guest call (with host call) + #[test] + fn io_buffer_reset() { + let mut cfg = SandboxConfiguration::default(); + cfg.set_input_data_size(4096); + cfg.set_output_data_size(4096); + let path = simple_guest_as_string().unwrap(); + let mut sandbox = + UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap(); + sandbox.register("HostAdd", |a: i32, b: i32| a + b).unwrap(); + let mut sandbox = sandbox.evolve().unwrap(); + + // will exhaust io if leaky. Tests both success and error paths + for _ in 0..1000 { + let result = sandbox.call::("Add", (5i32, 10i32)).unwrap(); + assert_eq!(result, 15); + let result = sandbox.call::("AddToStaticAndFail", ()).unwrap_err(); + assert!( + matches!(result, HyperlightError::GuestError (code, msg ) if code == ErrorCode::GuestError && msg == "Crash on purpose") + ); } } - /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state - /// and devolving from MultiUseSandbox to MultiUseSandbox restores the previous state + /// Tests that call_guest_function_by_name restores the state correctly #[test] - fn evolve_devolve_handles_state_correctly() { - let sbox1: MultiUseSandbox = { + fn test_call_guest_function_by_name() { + let mut sbox: MultiUseSandbox = { let path = simple_guest_as_string().unwrap(); let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); - u_sbox.evolve(Noop::default()) + u_sbox.evolve() } .unwrap(); - let func = Box::new(|call_ctx: &mut MultiUseGuestCallContext| { - call_ctx.call::("AddToStatic", 5i32)?; - Ok(()) - }); - let transition_func = MultiUseContextCallback::from(func); - let mut sbox2 = sbox1.evolve(transition_func).unwrap(); - let res: i32 = sbox2.call_guest_function_by_name("GetStatic", ()).unwrap(); + let snapshot = sbox.snapshot().unwrap(); + + let _ = sbox.call::("AddToStatic", 5i32).unwrap(); + let res: i32 = sbox.call("GetStatic", ()).unwrap(); assert_eq!(res, 5); - let mut sbox3: MultiUseSandbox = sbox2.devolve(Noop::default()).unwrap(); - let res: i32 = sbox3.call_guest_function_by_name("GetStatic", ()).unwrap(); + + sbox.restore(&snapshot).unwrap(); + #[allow(deprecated)] + let _ = sbox + .call_guest_function_by_name::("AddToStatic", 5i32) + .unwrap(); + #[allow(deprecated)] + let res: i32 = sbox.call_guest_function_by_name("GetStatic", ()).unwrap(); assert_eq!(res, 0); } + // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (1K) and heap(14K). + // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest. #[test] - // TODO: Investigate why this test fails with an incorrect error when run alongside other tests - #[ignore] - #[cfg(target_os = "linux")] - fn test_violate_seccomp_filters() -> Result<()> { - fn make_get_pid_syscall() -> Result { - let pid = unsafe { libc::syscall(libc::SYS_getpid) }; - Ok(pid as u64) - } - - // First, run to make sure it fails. - { - let mut usbox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), - None, - ) - .unwrap(); - - usbox.register("MakeGetpidSyscall", make_get_pid_syscall)?; - - let mut sbox: MultiUseSandbox = usbox.evolve(Noop::default())?; - - let res: Result = sbox.call_guest_function_by_name("ViolateSeccompFilters", ()); - - #[cfg(feature = "seccomp")] - match res { - Ok(_) => panic!("Expected to fail due to seccomp violation"), - Err(e) => match e { - HyperlightError::DisallowedSyscall => {} - _ => panic!("Expected DisallowedSyscall error: {}", e), - }, - } + fn test_with_small_stack_and_heap() { + let mut cfg = SandboxConfiguration::default(); + cfg.set_heap_size(20 * 1024); + cfg.set_stack_size(18 * 1024); - #[cfg(not(feature = "seccomp"))] - match res { - Ok(_) => (), - Err(e) => panic!("Expected to succeed without seccomp: {}", e), - } + let mut sbox1: MultiUseSandbox = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap(); + u_sbox.evolve() } + .unwrap(); - // Second, run with allowing `SYS_getpid` - #[cfg(feature = "seccomp")] - { - let mut usbox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), - None, - ) - .unwrap(); - - usbox.register_with_extra_allowed_syscalls( - "MakeGetpidSyscall", - make_get_pid_syscall, - vec![libc::SYS_getpid], - )?; - // ^^^ note, we are allowing SYS_getpid - - let mut sbox: MultiUseSandbox = usbox.evolve(Noop::default())?; - - let res: Result = sbox.call_guest_function_by_name("ViolateSeccompFilters", ()); + for _ in 0..1000 { + sbox1.call::("Echo", "hello".to_string()).unwrap(); + } - match res { - Ok(_) => {} - Err(e) => panic!("Expected to succeed due to seccomp violation: {}", e), - } + let mut sbox2: MultiUseSandbox = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap(); + u_sbox.evolve() } + .unwrap(); - Ok(()) + for i in 0..1000 { + sbox2 + .call::( + "PrintUsingPrintf", + format!("Hello World {}\n", i).to_string(), + ) + .unwrap(); + } } - // We have a secomp specifically for `openat`, but we don't want to crash on `openat`, but rather make sure `openat` returns `EACCES` + /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state + /// and restoring a snapshot from before evolving restores the previous state #[test] - #[cfg(target_os = "linux")] - fn violate_seccomp_filters_openat() -> Result<()> { - // Hostcall to call `openat`. - fn make_openat_syscall() -> Result { - use std::ffi::CString; - - let path = CString::new("/proc/sys/vm/overcommit_memory").unwrap(); - - let fd_or_err = unsafe { - libc::syscall( - libc::SYS_openat, - libc::AT_FDCWD, - path.as_ptr(), - libc::O_RDONLY, - ) - }; - - if fd_or_err == -1 { - Ok((-std::io::Error::last_os_error().raw_os_error().unwrap()).into()) - } else { - Ok(fd_or_err) - } + fn snapshot_evolve_restore_handles_state_correctly() { + let mut sbox: MultiUseSandbox = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + u_sbox.evolve() } - { - // First make sure a regular call to `openat` on /proc/sys/vm/overcommit_memory succeeds - let ret = make_openat_syscall()?; - assert!( - ret >= 0, - "Expected openat syscall to succeed, got: {:?}", - ret - ); + .unwrap(); - let mut ubox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), - None, - ) - .unwrap(); - ubox.register("Openat_Hostfunc", make_openat_syscall)?; + let snapshot = sbox.snapshot().unwrap(); - let mut sbox = ubox.evolve(Noop::default()).unwrap(); - let host_func_result = sbox - .call_guest_function_by_name::( - "CallGivenParamlessHostFuncThatReturnsI64", - "Openat_Hostfunc".to_string(), - ) - .expect("Expected to call host function that returns i64"); - - if cfg!(feature = "seccomp") { - // If seccomp is enabled, we expect the syscall to return EACCES, as setup by our seccomp filter - assert_eq!(host_func_result, -libc::EACCES as i64); - } else { - // If seccomp is not enabled, we expect the syscall to succeed - assert!(host_func_result >= 0); - } - } + let _ = sbox.call::("AddToStatic", 5i32).unwrap(); - #[cfg(feature = "seccomp")] - { - // Now let's make sure if we register the `openat` syscall as an extra allowed syscall, it will succeed - let mut ubox = UninitializedSandbox::new( - GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), - None, - ) - .unwrap(); - ubox.register_with_extra_allowed_syscalls( - "Openat_Hostfunc", - make_openat_syscall, - [libc::SYS_openat], - )?; - let mut sbox = ubox.evolve(Noop::default()).unwrap(); - let host_func_result = sbox - .call_guest_function_by_name::( - "CallGivenParamlessHostFuncThatReturnsI64", - "Openat_Hostfunc".to_string(), - ) - .expect("Expected to call host function that returns i64"); - - // should pass regardless of seccomp feature - assert!(host_func_result >= 0); - } + let res: i32 = sbox.call("GetStatic", ()).unwrap(); + assert_eq!(res, 5); - Ok(()) + sbox.restore(&snapshot).unwrap(); + let res: i32 = sbox.call("GetStatic", ()).unwrap(); + assert_eq!(res, 0); } #[test] @@ -552,9 +704,9 @@ mod tests { ) .unwrap(); - let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve(Noop::default()).unwrap(); + let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap(); - let res: Result<()> = multi_use_sandbox.call_guest_function_by_name("TriggerException", ()); + let res: Result<()> = multi_use_sandbox.call("TriggerException", ()); assert!(res.is_err()); @@ -592,12 +744,9 @@ mod tests { ) .unwrap(); - let mut multi_use_sandbox: MultiUseSandbox = - usbox.evolve(Noop::default()).unwrap(); + let mut multi_use_sandbox: MultiUseSandbox = usbox.evolve().unwrap(); - let res: i32 = multi_use_sandbox - .call_guest_function_by_name("GetStatic", ()) - .unwrap(); + let res: i32 = multi_use_sandbox.call("GetStatic", ()).unwrap(); assert_eq!(res, 0); } @@ -612,4 +761,204 @@ mod tests { handle.join().unwrap(); } } + + #[cfg(target_os = "linux")] + #[test] + fn test_mmap() { + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let expected = b"hello world"; + let map_mem = page_aligned_memory(expected); + let guest_base = 0x1_0000_0000; // Arbitrary guest base address + + unsafe { + sbox.map_region(®ion_for_memory( + &map_mem, + guest_base, + MemoryRegionFlags::READ, + )) + .unwrap(); + } + + let _guard = map_mem.lock.try_read().unwrap(); + let actual: Vec = sbox + .call( + "ReadMappedBuffer", + (guest_base as u64, expected.len() as u64), + ) + .unwrap(); + + assert_eq!(actual, expected); + } + + // Makes sure MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE executable but not writable + #[cfg(target_os = "linux")] + #[test] + fn test_mmap_write_exec() { + let mut sbox = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), + None, + ) + .unwrap() + .evolve() + .unwrap(); + + let expected = &[0x90, 0x90, 0x90, 0xC3]; // NOOP slide to RET + let map_mem = page_aligned_memory(expected); + let guest_base = 0x1_0000_0000; // Arbitrary guest base address + + unsafe { + sbox.map_region(®ion_for_memory( + &map_mem, + guest_base, + MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + )) + .unwrap(); + } + + let _guard = map_mem.lock.try_read().unwrap(); + + // Execute should pass since memory is executable + let succeed = sbox + .call::( + "ExecMappedBuffer", + (guest_base as u64, expected.len() as u64), + ) + .unwrap(); + assert!(succeed, "Expected execution of mapped buffer to succeed"); + + // write should fail because the memory is mapped as read-only + let err = sbox + .call::( + "WriteMappedBuffer", + (guest_base as u64, expected.len() as u64), + ) + .unwrap_err(); + + match err { + HyperlightError::MemoryAccessViolation(addr, ..) if addr == guest_base as u64 => {} + _ => panic!("Expected MemoryAccessViolation error"), + }; + } + + #[cfg(target_os = "linux")] + fn page_aligned_memory(src: &[u8]) -> GuestSharedMemory { + use hyperlight_common::mem::PAGE_SIZE_USIZE; + + let len = src.len().div_ceil(PAGE_SIZE_USIZE) * PAGE_SIZE_USIZE; + + let mut mem = ExclusiveSharedMemory::new(len).unwrap(); + mem.copy_from_slice(src, 0).unwrap(); + + let (_, guest_mem) = mem.build(); + + guest_mem + } + + #[cfg(target_os = "linux")] + fn region_for_memory( + mem: &GuestSharedMemory, + guest_base: usize, + flags: MemoryRegionFlags, + ) -> MemoryRegion { + let ptr = mem.base_addr(); + let len = mem.mem_size(); + MemoryRegion { + host_region: ptr..(ptr + len), + guest_region: guest_base..(guest_base + len), + flags, + region_type: MemoryRegionType::Heap, + } + } + + #[cfg(target_os = "linux")] + fn allocate_guest_memory() -> GuestSharedMemory { + page_aligned_memory(b"test data for snapshot") + } + + #[test] + #[cfg(target_os = "linux")] + fn snapshot_restore_handles_remapping_correctly() { + let mut sbox: MultiUseSandbox = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + u_sbox.evolve().unwrap() + }; + + // 1. Take snapshot 1 with no additional regions mapped + let snapshot1 = sbox.snapshot().unwrap(); + assert_eq!(sbox.vm.get_mapped_regions().len(), 0); + + // 2. Map a memory region + let map_mem = allocate_guest_memory(); + let guest_base = 0x200000000_usize; + let region = region_for_memory(&map_mem, guest_base, MemoryRegionFlags::READ); + + unsafe { sbox.map_region(®ion).unwrap() }; + assert_eq!(sbox.vm.get_mapped_regions().len(), 1); + + // 3. Take snapshot 2 with 1 region mapped + let snapshot2 = sbox.snapshot().unwrap(); + assert_eq!(sbox.vm.get_mapped_regions().len(), 1); + + // 4. Restore to snapshot 1 (should unmap the region) + sbox.restore(&snapshot1).unwrap(); + assert_eq!(sbox.vm.get_mapped_regions().len(), 0); + + // 5. Restore forward to snapshot 2 (should remap the region) + sbox.restore(&snapshot2).unwrap(); + assert_eq!(sbox.vm.get_mapped_regions().len(), 1); + + // Verify the region is the same + let mut restored_regions = sbox.vm.get_mapped_regions(); + assert_eq!(*restored_regions.next().unwrap(), region); + assert!(restored_regions.next().is_none()); + drop(restored_regions); + + // 6. Try map the region again (should fail since already mapped) + let err = unsafe { sbox.map_region(®ion) }; + assert!( + err.is_err(), + "Expected error when remapping existing region: {:?}", + err + ); + } + + #[test] + fn snapshot_different_sandbox() { + let mut sandbox = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + u_sbox.evolve().unwrap() + }; + + let mut sandbox2 = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + u_sbox.evolve().unwrap() + }; + assert_ne!(sandbox.id, sandbox2.id); + + let snapshot = sandbox.snapshot().unwrap(); + let err = sandbox2.restore(&snapshot); + assert!(matches!(err, Err(HyperlightError::SnapshotSandboxMismatch))); + + let sandbox_id = sandbox.id; + drop(sandbox); + drop(sandbox2); + drop(snapshot); + + let sandbox3 = { + let path = simple_guest_as_string().unwrap(); + let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap(); + u_sbox.evolve().unwrap() + }; + assert_ne!(sandbox3.id, sandbox_id); + } } diff --git a/src/hyperlight_host/src/sandbox/mem_access.rs b/src/hyperlight_host/src/sandbox/mem_access.rs deleted file mode 100644 index 6a6078b2c..000000000 --- a/src/hyperlight_host/src/sandbox/mem_access.rs +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use std::sync::{Arc, Mutex}; - -use tracing::{Span, instrument}; - -use super::mem_mgr::MemMgrWrapper; -use crate::error::HyperlightError::StackOverflow; -#[cfg(gdb)] -use crate::hypervisor::handlers::{DbgMemAccessHandlerCaller, DbgMemAccessHandlerWrapper}; -use crate::hypervisor::handlers::{ - MemAccessHandler, MemAccessHandlerFunction, MemAccessHandlerWrapper, -}; -use crate::mem::shared_mem::HostSharedMemory; -use crate::{Result, log_then_return}; - -#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] -pub(super) fn handle_mem_access_impl(wrapper: &MemMgrWrapper) -> Result<()> { - if !wrapper.check_stack_guard()? { - log_then_return!(StackOverflow()); - } - - Ok(()) -} - -#[instrument(skip_all, parent = Span::current(), level= "Trace")] -pub(crate) fn mem_access_handler_wrapper( - wrapper: MemMgrWrapper, -) -> MemAccessHandlerWrapper { - let mem_access_func: MemAccessHandlerFunction = - Box::new(move || handle_mem_access_impl(&wrapper)); - let mem_access_hdl = MemAccessHandler::from(mem_access_func); - Arc::new(Mutex::new(mem_access_hdl)) -} - -#[cfg(gdb)] -struct DbgMemAccessContainer { - wrapper: MemMgrWrapper, -} - -#[cfg(gdb)] -impl DbgMemAccessHandlerCaller for DbgMemAccessContainer { - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - fn read(&mut self, addr: usize, data: &mut [u8]) -> Result<()> { - self.wrapper - .unwrap_mgr_mut() - .get_shared_mem_mut() - .copy_to_slice(data, addr) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - fn write(&mut self, addr: usize, data: &[u8]) -> Result<()> { - self.wrapper - .unwrap_mgr_mut() - .get_shared_mem_mut() - .copy_from_slice(data, addr) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - fn get_code_offset(&mut self) -> Result { - Ok(self.wrapper.unwrap_mgr().layout.get_guest_code_address()) - } -} - -#[cfg(gdb)] -#[instrument(skip_all, parent = Span::current(), level= "Trace")] -pub(crate) fn dbg_mem_access_handler_wrapper( - wrapper: MemMgrWrapper, -) -> DbgMemAccessHandlerWrapper { - let container = DbgMemAccessContainer { wrapper }; - - Arc::new(Mutex::new(container)) -} diff --git a/src/hyperlight_host/src/sandbox/mem_mgr.rs b/src/hyperlight_host/src/sandbox/mem_mgr.rs deleted file mode 100644 index eb25af9da..000000000 --- a/src/hyperlight_host/src/sandbox/mem_mgr.rs +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use tracing::{Span, instrument}; - -use crate::Result; -use crate::mem::layout::SandboxMemoryLayout; -use crate::mem::mgr::{STACK_COOKIE_LEN, SandboxMemoryManager}; -use crate::mem::shared_mem::{ - ExclusiveSharedMemory, GuestSharedMemory, HostSharedMemory, SharedMemory, -}; - -/// StackCookie -pub type StackCookie = [u8; STACK_COOKIE_LEN]; - -/// A container with methods for accessing `SandboxMemoryManager` and other -/// related objects -#[derive(Clone)] -pub(crate) struct MemMgrWrapper { - mgr: SandboxMemoryManager, - stack_cookie: StackCookie, - abort_buffer: Vec, -} - -impl MemMgrWrapper { - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn new(mgr: SandboxMemoryManager, stack_cookie: StackCookie) -> Self { - Self { - mgr, - stack_cookie, - abort_buffer: Vec::new(), - } - } - - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn unwrap_mgr(&self) -> &SandboxMemoryManager { - &self.mgr - } - - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn unwrap_mgr_mut(&mut self) -> &mut SandboxMemoryManager { - &mut self.mgr - } - - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn get_stack_cookie(&self) -> &StackCookie { - &self.stack_cookie - } - - pub fn get_abort_buffer_mut(&mut self) -> &mut Vec { - &mut self.abort_buffer - } -} - -impl AsMut> for MemMgrWrapper { - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn as_mut(&mut self) -> &mut SandboxMemoryManager { - self.unwrap_mgr_mut() - } -} - -impl AsRef> for MemMgrWrapper { - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn as_ref(&self) -> &SandboxMemoryManager { - self.unwrap_mgr() - } -} - -impl MemMgrWrapper { - pub(crate) fn build( - self, - ) -> ( - MemMgrWrapper, - SandboxMemoryManager, - ) { - let (hshm, gshm) = self.mgr.build(); - (MemMgrWrapper::new(hshm, self.stack_cookie), gshm) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn write_memory_layout(&mut self) -> Result<()> { - let mgr = self.unwrap_mgr_mut(); - let layout = mgr.layout; - let shared_mem = mgr.get_shared_mem_mut(); - let mem_size = shared_mem.mem_size(); - layout.write(shared_mem, SandboxMemoryLayout::BASE_ADDRESS, mem_size) - } - - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(super) fn write_init_data(&mut self, user_memory: &[u8]) -> Result<()> { - let mgr = self.unwrap_mgr_mut(); - let layout = mgr.layout; - let shared_mem = mgr.get_shared_mem_mut(); - layout.write_init_data(shared_mem, user_memory)?; - Ok(()) - } -} - -impl MemMgrWrapper { - /// Check the stack guard against the given `stack_cookie`. - /// - /// Return `Ok(true)` if the given cookie matches the one in guest memory, - /// and `Ok(false)` otherwise. Return `Err` if it could not be found or - /// there was some other error. - #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] - pub(crate) fn check_stack_guard(&self) -> Result { - self.unwrap_mgr() - .check_stack_guard(*self.get_stack_cookie()) - } -} diff --git a/src/hyperlight_host/src/sandbox/mod.rs b/src/hyperlight_host/src/sandbox/mod.rs index c6e33c84b..381b6a89e 100644 --- a/src/hyperlight_host/src/sandbox/mod.rs +++ b/src/hyperlight_host/src/sandbox/mod.rs @@ -23,12 +23,6 @@ pub(crate) mod hypervisor; /// Functionality for dealing with initialized sandboxes that can /// call 0 or more guest functions pub mod initialized_multi_use; -/// Functionality for dealing with memory access from the VM guest -/// executable -pub(crate) mod mem_access; -/// Functionality for interacting with a sandbox's internally-stored -/// `SandboxMemoryManager` -pub(crate) mod mem_mgr; pub(crate) mod outb; /// Functionality for creating uninitialized sandboxes, manipulating them, /// and converting them to initialized sandboxes. @@ -37,9 +31,16 @@ pub mod uninitialized; /// initialized `Sandbox`es. pub(crate) mod uninitialized_evolve; +/// Representation of a snapshot of a `Sandbox`. +pub mod snapshot; + /// Trait used by the macros to paper over the differences between hyperlight and hyperlight-wasm mod callable; +/// Module for tracing guest execution +#[cfg(feature = "trace_guest")] +pub(crate) mod trace; + /// Trait used by the macros to paper over the differences between hyperlight and hyperlight-wasm pub use callable::Callable; /// Re-export for `SandboxConfiguration` type @@ -52,10 +53,8 @@ pub use uninitialized::GuestBinary; /// Re-export for `UninitializedSandbox` type pub use uninitialized::UninitializedSandbox; -use self::mem_mgr::MemMgrWrapper; #[cfg(target_os = "windows")] use crate::hypervisor::windows_hypervisor_platform; -use crate::mem::shared_mem::HostSharedMemory; // In case its not obvious why there are separate is_supported_platform and is_hypervisor_present functions its because // Hyperlight is designed to be able to run on a host that doesn't have a hypervisor. @@ -80,18 +79,12 @@ pub type ExtraAllowedSyscall = i64; /// Determine whether a suitable hypervisor is available to run /// this sandbox. /// -// Returns a boolean indicating whether a suitable hypervisor is present. +/// Returns a boolean indicating whether a suitable hypervisor is present. #[instrument(skip_all, parent = Span::current())] pub fn is_hypervisor_present() -> bool { hypervisor::get_available_hypervisor().is_some() } -pub(crate) trait WrapperGetter { - #[allow(dead_code)] - fn get_mgr_wrapper(&self) -> &MemMgrWrapper; - fn get_mgr_wrapper_mut(&mut self) -> &mut MemMgrWrapper; -} - #[cfg(test)] mod tests { use std::sync::Arc; @@ -101,8 +94,6 @@ mod tests { use hyperlight_testing::simple_guest_as_string; use crate::sandbox::uninitialized::GuestBinary; - use crate::sandbox_state::sandbox::EvolvableSandbox; - use crate::sandbox_state::transition::Noop; use crate::{MultiUseSandbox, UninitializedSandbox, new_error}; #[test] @@ -163,11 +154,9 @@ mod tests { )) .unwrap(); - let sandbox = uninitialized_sandbox - .evolve(Noop::default()) - .unwrap_or_else(|_| { - panic!("Failed to initialize UninitializedSandbox thread {}", i) - }); + let sandbox = uninitialized_sandbox.evolve().unwrap_or_else(|_| { + panic!("Failed to initialize UninitializedSandbox thread {}", i) + }); sq.push(sandbox).unwrap_or_else(|_| { panic!("Failed to push UninitializedSandbox thread {}", i) diff --git a/src/hyperlight_host/src/sandbox/outb.rs b/src/hyperlight_host/src/sandbox/outb.rs index dcdd96589..e5e938ae3 100644 --- a/src/hyperlight_host/src/sandbox/outb.rs +++ b/src/hyperlight_host/src/sandbox/outb.rs @@ -16,8 +16,8 @@ limitations under the License. use std::sync::{Arc, Mutex}; -use hyperlight_common::flatbuffer_wrappers::function_types::ParameterValue; -use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; +use hyperlight_common::flatbuffer_wrappers::function_types::{FunctionCallResult, ParameterValue}; +use hyperlight_common::flatbuffer_wrappers::guest_error::{ErrorCode, GuestError}; use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData; use hyperlight_common::outb::{Exception, OutBAction}; use log::{Level, Record}; @@ -25,8 +25,8 @@ use tracing::{Span, instrument}; use tracing_log::format_trace; use super::host_funcs::FunctionRegistry; -use super::mem_mgr::MemMgrWrapper; -use crate::hypervisor::handlers::{OutBHandler, OutBHandlerFunction, OutBHandlerWrapper}; +#[cfg(feature = "mem_profile")] +use crate::hypervisor::Hypervisor; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::HostSharedMemory; use crate::{HyperlightError, Result, new_error}; @@ -96,7 +96,7 @@ pub(super) fn outb_log(mgr: &mut SandboxMemoryManager) -> Resu const ABORT_TERMINATOR: u8 = 0xFF; const MAX_ABORT_BUFFER_LEN: usize = 1024; -fn outb_abort(mem_mgr: &mut MemMgrWrapper, data: u32) -> Result<()> { +fn outb_abort(mem_mgr: &mut SandboxMemoryManager, data: u32) -> Result<()> { let buffer = mem_mgr.get_abort_buffer_mut(); let bytes = data.to_le_bytes(); // [len, b1, b2, b3] @@ -140,31 +140,33 @@ fn outb_abort(mem_mgr: &mut MemMgrWrapper, data: u32) -> Resul buffer.push(b); } - Ok(()) } /// Handles OutB operations from the guest. #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")] -fn handle_outb_impl( - mem_mgr: &mut MemMgrWrapper, +pub(crate) fn handle_outb( + mem_mgr: &mut SandboxMemoryManager, host_funcs: Arc>, + #[cfg(feature = "mem_profile")] _hv: &mut dyn Hypervisor, port: u16, data: u32, ) -> Result<()> { match port.try_into()? { - OutBAction::Log => outb_log(mem_mgr.as_mut()), + OutBAction::Log => outb_log(mem_mgr), OutBAction::CallFunction => { - let call = mem_mgr.as_mut().get_host_function_call()?; // pop output buffer + let call = mem_mgr.get_host_function_call()?; // pop output buffer let name = call.function_name.clone(); let args: Vec = call.parameters.unwrap_or(vec![]); let res = host_funcs .try_lock() .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))? - .call_host_function(&name, args)?; - mem_mgr - .as_mut() - .write_response_from_host_method_call(&res)?; // push input buffers + .call_host_function(&name, args) + .map_err(|e| GuestError::new(ErrorCode::HostFunctionError, e.to_string())); + + let func_result = FunctionCallResult::new(res); + + mem_mgr.write_response_from_host_function_call(&func_result)?; Ok(()) } @@ -180,30 +182,22 @@ fn handle_outb_impl( eprint!("{}", ch); Ok(()) } + #[cfg(feature = "trace_guest")] + OutBAction::TraceBatch => Ok(()), + #[cfg(feature = "mem_profile")] + OutBAction::TraceMemoryAlloc => { + let regs = _hv.regs()?; + let trace_info = _hv.trace_info_mut(); + trace_info.handle_trace_mem_alloc(®s, mem_mgr) + } + #[cfg(feature = "mem_profile")] + OutBAction::TraceMemoryFree => { + let regs = _hv.regs()?; + let trace_info = _hv.trace_info_mut(); + trace_info.handle_trace_mem_free(®s, mem_mgr) + } } } - -/// Given a `MemMgrWrapper` and ` HostFuncsWrapper` -- both passed by _value_ -/// -- return an `OutBHandlerWrapper` wrapping the core OUTB handler logic. -/// -/// TODO: pass at least the `host_funcs_wrapper` param by reference. -#[instrument(skip_all, parent = Span::current(), level= "Trace")] -pub(crate) fn outb_handler_wrapper( - mut mem_mgr_wrapper: MemMgrWrapper, - host_funcs_wrapper: Arc>, -) -> OutBHandlerWrapper { - let outb_func: OutBHandlerFunction = Box::new(move |port, payload| { - handle_outb_impl( - &mut mem_mgr_wrapper, - host_funcs_wrapper.clone(), - port, - payload, - ) - }); - let outb_hdl = OutBHandler::from(outb_func); - Arc::new(Mutex::new(outb_hdl)) -} - #[cfg(test)] mod tests { use hyperlight_common::flatbuffer_wrappers::guest_log_level::LogLevel; @@ -240,13 +234,10 @@ mod tests { let sandbox_cfg = SandboxConfiguration::default(); let new_mgr = || { - let mut exe_info = simple_guest_exe_info().unwrap(); - let mut mgr = SandboxMemoryManager::load_guest_binary_into_memory( - sandbox_cfg, - &mut exe_info, - None, - ) - .unwrap(); + let exe_info = simple_guest_exe_info().unwrap(); + let (mut mgr, _) = + SandboxMemoryManager::load_guest_binary_into_memory(sandbox_cfg, exe_info, None) + .unwrap(); let mem_size = mgr.get_shared_mem_mut().mem_size(); let layout = mgr.layout; let shared_mem = mgr.get_shared_mem_mut(); @@ -355,10 +346,10 @@ mod tests { let sandbox_cfg = SandboxConfiguration::default(); tracing::subscriber::with_default(subscriber.clone(), || { let new_mgr = || { - let mut exe_info = simple_guest_exe_info().unwrap(); - let mut mgr = SandboxMemoryManager::load_guest_binary_into_memory( + let exe_info = simple_guest_exe_info().unwrap(); + let (mut mgr, _) = SandboxMemoryManager::load_guest_binary_into_memory( sandbox_cfg, - &mut exe_info, + exe_info, None, ) .unwrap(); diff --git a/src/hyperlight_host/src/sandbox/snapshot.rs b/src/hyperlight_host/src/sandbox/snapshot.rs new file mode 100644 index 000000000..c00aa4487 --- /dev/null +++ b/src/hyperlight_host/src/sandbox/snapshot.rs @@ -0,0 +1,25 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +use std::sync::Arc; + +use crate::mem::shared_mem_snapshot::SharedMemorySnapshot; + +/// A snapshot capturing the state of the memory in a `MultiUseSandbox`. +#[derive(Clone)] +pub struct Snapshot { + pub(crate) inner: Arc, +} diff --git a/src/hyperlight_host/src/sandbox/trace/context.rs b/src/hyperlight_host/src/sandbox/trace/context.rs new file mode 100644 index 000000000..f17a466ef --- /dev/null +++ b/src/hyperlight_host/src/sandbox/trace/context.rs @@ -0,0 +1,353 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +use std::collections::HashMap; +use std::time::{Duration, Instant, SystemTime}; + +use hyperlight_common::outb::OutBAction; +use hyperlight_guest_tracing::{Events, Spans}; +use opentelemetry::global::BoxedSpan; +use opentelemetry::trace::{Span as _, TraceContextExt, Tracer as _}; +use opentelemetry::{Context, KeyValue, global}; +use tracing::span::{EnteredSpan, Span}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +use crate::hypervisor::regs::CommonRegisters; +use crate::mem::layout::SandboxMemoryLayout; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::HostSharedMemory; +use crate::{HyperlightError, Result, new_error}; + +/// Type that helps get the data from the guest provided the registers and memory access +struct TraceBatch { + pub guest_start_tsc: u64, + pub spans: Spans, + pub events: Events, +} + +impl TryFrom<(&CommonRegisters, &SandboxMemoryManager)> for TraceBatch { + type Error = HyperlightError; + fn try_from( + (regs, mem_mgr): (&CommonRegisters, &SandboxMemoryManager), + ) -> Result { + let magic_no = regs.r8; + let guest_start_tsc = regs.r9; + let spans_ptr = regs.r10 as usize; + let events_ptr = regs.r11 as usize; + + if magic_no != OutBAction::TraceBatch as u64 { + return Err(new_error!("A TraceBatch is not present")); + } + + // Transmute spans_ptr to Spans type + let mut spans = vec![0u8; std::mem::size_of::()]; + mem_mgr + .shared_mem + .copy_to_slice(&mut spans, spans_ptr - SandboxMemoryLayout::BASE_ADDRESS) + .map_err(|e| { + new_error!( + "Failed to copy guest trace batch from guest memory to host: {:?}", + e + ) + })?; + + let spans: Spans = unsafe { + let raw = spans.as_slice() as *const _ as *const Spans; + raw.read_unaligned() + }; + + // Transmute events_ptr to Events type + let mut events = vec![0u8; std::mem::size_of::()]; + mem_mgr + .shared_mem + .copy_to_slice(&mut events, events_ptr - SandboxMemoryLayout::BASE_ADDRESS) + .map_err(|e| { + new_error!( + "Failed to copy guest trace batch from guest memory to host: {:?}", + e + ) + })?; + + let events: Events = unsafe { + let raw = events.as_slice() as *const _ as *const Events; + raw.read_unaligned() + }; + + Ok(TraceBatch { + guest_start_tsc, + spans, + events, + }) + } +} + +/// This structure handles the guest tracing information. +pub struct TraceContext { + host_spans: Vec, + guest_spans: HashMap, + in_host_call: bool, + + // Lazily initialized members + start_wall: Option, + /// The epoch at which the call into the guest started, if it has started. + /// This is used to calculate the time spent in the guest relative to the + /// time when the call into the guest was first made. + start_instant: Option, + /// The start guest time, in TSC cycles, for the current guest measured on the host. + /// It contains the TSC value recorded on the host before a call is made into the guest. + /// This is used to calculate the TSC frequency which is the same on the host and guest. + /// The TSC frequency is used to convert TSC values to timestamps in the trace. + /// **NOTE**: This is only used until the TSC frequency is calculated, when the first + /// records are received. + start_tsc: Option, + /// The frequency of the timestamp counter. + tsc_freq: Option, + current_parent_ctx: Option, +} + +impl TraceContext { + /// Initialize with current context + pub fn new() -> Self { + if !hyperlight_guest_tracing::invariant_tsc::has_invariant_tsc() { + // If the platform does not support invariant TSC, warn the user. + // On Azure nested virtualization, the TSC invariant bit is not correctly reported, this is a known issue. + log::warn!( + "Invariant TSC is not supported on this platform, trace timestamps may be inaccurate" + ); + } + + let current_ctx = Span::current().context(); + + let span = tracing::trace_span!("call-to-guest"); + let _ = span.set_parent(current_ctx); + let entered = span.entered(); + + Self { + host_spans: vec![entered], + guest_spans: HashMap::new(), + in_host_call: false, + + start_wall: None, + start_instant: None, + start_tsc: None, + tsc_freq: None, + current_parent_ctx: None, + } + } + + /// Calculate the frequency of the TimeStamp Counter. + /// This is done by: + /// - first reading a timestamp and an `Instant` + /// - secondly reading another timestamp and `Instant` + /// - calculate the frequency based on the `Duration` between + /// the two `Instant`s read. + fn calculate_tsc_freq(&mut self) -> Result<()> { + let (start, start_time) = match (self.start_tsc.as_ref(), self.start_instant.as_ref()) { + (Some(start), Some(start_time)) => (*start, *start_time), + _ => { + // If the guest start TSC and time are not set, we use the current time and TSC. + // This is not ideal, but it allows us to calculate the TSC frequency without + // failing. + // This is a fallback mechanism to ensure that we can still calculate, however it + // should be noted that this may lead to inaccuracies in the TSC frequency. + // The start time should be already set before running the guest for each sandbox. + log::error!( + "Guest start TSC and time are not set. Calculating TSC frequency will use current time and TSC." + ); + ( + hyperlight_guest_tracing::invariant_tsc::read_tsc(), + std::time::Instant::now(), + ) + } + }; + + let end_time = std::time::Instant::now(); + let end = hyperlight_guest_tracing::invariant_tsc::read_tsc(); + + let elapsed = end_time.duration_since(start_time).as_secs_f64(); + let tsc_freq = ((end - start) as f64 / elapsed) as u64; + + log::info!("Calculated TSC frequency: {} Hz", tsc_freq); + self.tsc_freq = Some(tsc_freq); + + Ok(()) + } + + /// Calculate timestamp relative to wall time stored on host + fn calculate_guest_time_relative_to_host( + &self, + guest_start_tsc: u64, + tsc: u64, + ) -> Result { + // Should never fail as it is extracted after it is set + let tsc_freq = self.tsc_freq.ok_or(new_error!("TSC frequency not set"))?; + + // Number of cycles relative to guest start + let rel_cycles = tsc.saturating_sub(guest_start_tsc); + + // Number of micro seconds from guest start to `tsc` argument + let rel_start_us = rel_cycles as f64 / tsc_freq as f64 * 1_000_000f64; + + // Final timestamp is calculated by: + // - starting from the wall time when the sandbox was created + // - adding the Duration to the guest start + // - adding the Duration from the guest start to the provided `tsc` + Ok(self.start_wall.ok_or(new_error!("start_wall not set"))? + + Duration::from_micros(rel_start_us as u64)) + } + + pub fn handle_trace( + &mut self, + regs: &CommonRegisters, + mem_mgr: &SandboxMemoryManager, + ) -> Result<()> { + if self.tsc_freq.is_none() { + self.calculate_tsc_freq()?; + } + + // Get the guest sent info + let trace_batch = TraceBatch::try_from((regs, mem_mgr))?; + + let tracer = global::tracer("guest-tracer"); + let mut spans_to_remove = vec![]; + + let mut current_active_span = None; + + // Update the spans map + for s in trace_batch.spans.iter() { + let start_ts = self + .calculate_guest_time_relative_to_host(trace_batch.guest_start_tsc, s.start_tsc)?; + let end_ts = s.end_tsc.map(|tsc| { + self.calculate_guest_time_relative_to_host(trace_batch.guest_start_tsc, tsc) + }); + let parent_id = s.parent_id; + let parent_ctx = if let Some(parent_id) = parent_id { + if let Some(span) = self.guest_spans.get(&parent_id) { + Context::new().with_remote_span_context(span.span_context().clone()) + } else if let Some(parent_ctx) = self.current_parent_ctx.as_ref() { + parent_ctx.clone() + } else { + Span::current().context().clone() + } + } else if let Some(parent_ctx) = self.current_parent_ctx.as_ref() { + parent_ctx.clone() + } else { + Span::current().context().clone() + }; + + // Get the saved span, modify it and set it back to avoid borrow checker + let mut span = self.guest_spans.remove(&s.id).unwrap_or_else(|| { + let mut sb = tracer + .span_builder(s.name.to_string()) + .with_start_time(start_ts); + sb.attributes = Some(vec![KeyValue::new("target", s.target.to_string())]); + let mut span = sb.start_with_context(&tracer, &parent_ctx); + + for (k, v) in s.fields.iter() { + span.set_attribute(KeyValue::new( + k.as_str().to_string(), + v.as_str().to_string(), + )); + } + + span + }); + + // If we find an end timestamp it means the span has been closed + // otherwise store it for later + if let Some(ts) = end_ts { + span.end_with_timestamp(ts?); + spans_to_remove.push(s.id); + } else { + current_active_span = + Some(Context::current().with_remote_span_context(span.span_context().clone())); + } + + self.guest_spans.insert(s.id, span); + } + + // Create the events + for ev in trace_batch.events.iter() { + let ts = + self.calculate_guest_time_relative_to_host(trace_batch.guest_start_tsc, ev.tsc)?; + let mut attributes: Vec = ev + .fields + .iter() + .map(|(k, v)| KeyValue::new(k.to_string(), v.to_string())) + .collect(); + + attributes.push(KeyValue::new( + "level", + tracing::Level::from(ev.level).to_string(), + )); + + // Add the event to the parent span + // It should always have a parent span + if let Some(span) = self.guest_spans.get_mut(&ev.parent_id) { + span.add_event_with_timestamp(ev.name.to_string(), ts, attributes); + } + } + + // Remove the spans that have been closed + for id in spans_to_remove.into_iter() { + self.guest_spans.remove(&id); + } + + if let Some(ctx) = current_active_span { + self.new_host_trace(ctx); + }; + + Ok(()) + } + + pub(crate) fn setup_guest_trace(&mut self, ctx: Context) { + if self.start_instant.is_none() { + crate::debug!("Guest Start Epoch set"); + self.start_wall = Some(SystemTime::now()); + self.start_tsc = Some(hyperlight_guest_tracing::invariant_tsc::read_tsc()); + self.start_instant = Some(std::time::Instant::now()); + } + self.current_parent_ctx = Some(ctx); + } + + pub fn new_host_trace(&mut self, ctx: Context) { + let span = tracing::trace_span!("call-to-host"); + let _ = span.set_parent(ctx); + let entered = span.entered(); + self.host_spans.push(entered); + self.in_host_call = true; + } + + pub fn end_host_trace(&mut self) { + if self.in_host_call + && let Some(entered) = self.host_spans.pop() + { + entered.exit(); + } + } +} + +impl Drop for TraceContext { + fn drop(&mut self) { + for (k, mut v) in self.guest_spans.drain() { + v.end(); + log::debug!("Dropped guest span with id {}", k); + } + while let Some(entered) = self.host_spans.pop() { + entered.exit(); + } + } +} diff --git a/src/hyperlight_host/src/sandbox/trace/mem_profile.rs b/src/hyperlight_host/src/sandbox/trace/mem_profile.rs new file mode 100644 index 000000000..89ff7b24b --- /dev/null +++ b/src/hyperlight_host/src/sandbox/trace/mem_profile.rs @@ -0,0 +1,182 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ +use std::io::Write; +use std::sync::{Arc, Mutex}; + +use fallible_iterator::FallibleIterator; +use framehop::Unwinder; + +use crate::hypervisor::regs::CommonRegisters; +use crate::mem::layout::SandboxMemoryLayout; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::HostSharedMemory; +use crate::{Result, new_error}; + +/// The type of trace frame being recorded. +/// This is used to identify the type of frame being recorded in the trace file. +enum TraceFrameType { + /// A frame that records a memory allocation. + MemAlloc = 2, + /// A frame that records a memory free. + MemFree = 3, +} + +/// This structure handles the memory profiling trace information. +pub(crate) struct MemTraceInfo { + /// The epoch against which trace events are timed; at least as + /// early as the creation of the sandbox being traced. + epoch: std::time::Instant, + /// The file to which the trace is being written + pub file: Arc>, + /// The unwind information for the current guest + #[allow(dead_code)] + pub unwind_module: Arc, + /// The framehop unwinder for the current guest + pub unwinder: framehop::x86_64::UnwinderX86_64>, + /// The framehop cache + pub unwind_cache: Arc>, +} + +impl MemTraceInfo { + pub fn new(unwind_module: Arc) -> Result { + let mut path = std::env::current_dir()?; + path.push("trace"); + + // create directory if it does not exist + if !path.exists() { + std::fs::create_dir(&path)?; + } + path.push(uuid::Uuid::new_v4().to_string()); + path.set_extension("trace"); + + log::info!("Creating trace file at: {}", path.display()); + println!("Creating trace file at: {}", path.display()); + + let hash = unwind_module.hash(); + let (unwinder, unwind_cache) = { + let mut unwinder = framehop::x86_64::UnwinderX86_64::new(); + unwinder.add_module(unwind_module.clone().as_module()); + let cache = framehop::x86_64::CacheX86_64::new(); + (unwinder, Arc::new(Mutex::new(cache))) + }; + + let ret = Self { + epoch: std::time::Instant::now(), + file: Arc::new(Mutex::new(std::fs::File::create_new(path)?)), + unwind_module, + unwinder, + unwind_cache, + }; + + /* write a frame identifying the binary */ + ret.record_trace_frame(ret.epoch, 0, |f| { + let _ = f.write_all(hash.as_bytes()); + })?; + + Ok(ret) + } + + fn unwind( + &self, + regs: &CommonRegisters, + mem_mgr: &SandboxMemoryManager, + ) -> Result> { + let mut read_stack = |addr| { + mem_mgr + .shared_mem + .read::((addr - SandboxMemoryLayout::BASE_ADDRESS as u64) as usize) + .map_err(|_| ()) + }; + let mut cache = self + .unwind_cache + .try_lock() + .map_err(|e| new_error!("could not lock unwinder cache {}\n", e))?; + let iter = self.unwinder.iter_frames( + regs.rip, + framehop::x86_64::UnwindRegsX86_64::new(regs.rip, regs.rsp, regs.rbp), + &mut *cache, + &mut read_stack, + ); + iter.map(|f| Ok(f.address() - mem_mgr.layout.get_guest_code_address() as u64)) + .collect() + .map_err(|e| new_error!("couldn't unwind: {}", e)) + } + + fn write_stack(&self, out: &mut std::fs::File, stack: &[u64]) { + let _ = out.write_all(&stack.len().to_ne_bytes()); + for frame in stack { + let _ = out.write_all(&frame.to_ne_bytes()); + } + } + + fn record_trace_frame( + &self, + start_instant: std::time::Instant, + frame_id: u64, + write_frame: F, + ) -> Result<()> { + let Ok(mut out) = self.file.lock() else { + return Ok(()); + }; + // frame structure: + // 16 bytes timestamp + let now = std::time::Instant::now().saturating_duration_since(start_instant); + let _ = out.write_all(&now.as_micros().to_ne_bytes()); + // 8 bytes frame type id + let _ = out.write_all(&frame_id.to_ne_bytes()); + // frame data + write_frame(&mut out); + Ok(()) + } + + fn handle_trace( + &self, + regs: &CommonRegisters, + mem_mgr: &SandboxMemoryManager, + trace_identifier: TraceFrameType, + ) -> Result<()> { + let Ok(stack) = self.unwind(regs, mem_mgr) else { + return Ok(()); + }; + + let amt = regs.rax; + let ptr = regs.rcx; + + self.record_trace_frame(self.epoch, trace_identifier as u64, |f| { + let _ = f.write_all(&ptr.to_ne_bytes()); + let _ = f.write_all(&amt.to_ne_bytes()); + self.write_stack(f, &stack); + }) + } + + #[inline(always)] + pub(crate) fn handle_trace_mem_alloc( + &self, + regs: &CommonRegisters, + mem_mgr: &SandboxMemoryManager, + ) -> Result<()> { + self.handle_trace(regs, mem_mgr, TraceFrameType::MemAlloc) + } + + #[inline(always)] + pub(crate) fn handle_trace_mem_free( + &self, + regs: &CommonRegisters, + mem_mgr: &SandboxMemoryManager, + ) -> Result<()> { + self.handle_trace(regs, mem_mgr, TraceFrameType::MemFree) + } +} diff --git a/src/hyperlight_host/src/sandbox_state/mod.rs b/src/hyperlight_host/src/sandbox/trace/mod.rs similarity index 68% rename from src/hyperlight_host/src/sandbox_state/mod.rs rename to src/hyperlight_host/src/sandbox/trace/mod.rs index 474cad72a..f955a6d76 100644 --- a/src/hyperlight_host/src/sandbox_state/mod.rs +++ b/src/hyperlight_host/src/sandbox/trace/mod.rs @@ -14,8 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -/// The standardized `Sandbox` trait and the ways it ban be transitioned -/// to a different `Sandbox` trait -pub mod sandbox; -/// Metadata about transitions between `Sandbox` states -pub mod transition; +/// Tracing context support for sandboxes. +mod context; +pub(crate) use context::TraceContext; + +/// Tracing and profiling support for sandboxes. +#[cfg(feature = "mem_profile")] +mod mem_profile; +#[cfg(feature = "mem_profile")] +pub(crate) use mem_profile::MemTraceInfo; diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index e27f91ff2..fb09572a9 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -23,7 +23,6 @@ use log::LevelFilter; use tracing::{Span, instrument}; use super::host_funcs::{FunctionRegistry, default_writer_func}; -use super::mem_mgr::MemMgrWrapper; use super::uninitialized_evolve::evolve_impl_multi_use; use crate::func::host_functions::{HostFunction, register_host_function}; use crate::func::{ParameterTuple, SupportedReturnType}; @@ -31,27 +30,10 @@ use crate::func::{ParameterTuple, SupportedReturnType}; use crate::log_build_details; use crate::mem::exe::ExeInfo; use crate::mem::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionFlags}; -use crate::mem::mgr::{STACK_COOKIE_LEN, SandboxMemoryManager}; +use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::ExclusiveSharedMemory; use crate::sandbox::SandboxConfiguration; -use crate::sandbox_state::sandbox::EvolvableSandbox; -use crate::sandbox_state::transition::Noop; -use crate::{MultiUseSandbox, Result, log_then_return, new_error}; - -#[cfg(all(target_os = "linux", feature = "seccomp"))] -const EXTRA_ALLOWED_SYSCALLS_FOR_WRITER_FUNC: &[super::ExtraAllowedSyscall] = &[ - // Fuzzing fails without `mmap` being an allowed syscall on our seccomp filter. - // All fuzzing does is call `PrintOutput` (which calls `HostPrint` ). Thing is, `println!` - // is designed to be thread-safe in Rust and the std lib ensures this by using - // buffered I/O, which I think relies on `mmap`. This gets surfaced in fuzzing with an - // OOM error, which I think is happening because `println!` is not being able to allocate - // more memory for its buffers for the fuzzer's huge inputs. - libc::SYS_mmap, - libc::SYS_brk, - libc::SYS_mprotect, - #[cfg(mshv)] - libc::SYS_close, -]; +use crate::{MultiUseSandbox, Result, new_error}; #[cfg(any(crashdump, gdb))] #[derive(Clone, Debug, Default)] @@ -64,66 +46,37 @@ pub(crate) struct SandboxRuntimeConfig { pub(crate) guest_core_dump: bool, } -/// A preliminary `Sandbox`, not yet ready to execute guest code. +/// A preliminary sandbox that represents allocated memory and registered host functions, +/// but has not yet created the underlying virtual machine. /// -/// Prior to initializing a full-fledged `Sandbox`, you must create one of -/// these `UninitializedSandbox`es with the `new` function, register all the -/// host-implemented functions you need to be available to the guest, then -/// call `evolve` to transform your -/// `UninitializedSandbox` into an initialized `Sandbox`. +/// This struct holds the configuration and setup needed for a sandbox without actually +/// creating the VM. It allows you to: +/// - Set up memory layout and load guest binary data +/// - Register host functions that will be available to the guest +/// - Configure sandbox settings before VM creation +/// +/// The virtual machine is not created until you call [`evolve`](Self::evolve) to transform +/// this into an initialized [`MultiUseSandbox`]. pub struct UninitializedSandbox { /// Registered host functions pub(crate) host_funcs: Arc>, /// The memory manager for the sandbox. - pub(crate) mgr: MemMgrWrapper, + pub(crate) mgr: SandboxMemoryManager, pub(crate) max_guest_log_level: Option, pub(crate) config: SandboxConfiguration, #[cfg(any(crashdump, gdb))] pub(crate) rt_cfg: SandboxRuntimeConfig, -} - -impl crate::sandbox_state::sandbox::UninitializedSandbox for UninitializedSandbox { - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn get_uninitialized_sandbox(&self) -> &crate::sandbox::UninitializedSandbox { - self - } - - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn get_uninitialized_sandbox_mut(&mut self) -> &mut crate::sandbox::UninitializedSandbox { - self - } + pub(crate) load_info: crate::mem::exe::LoadInfo, } impl Debug for UninitializedSandbox { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UninitializedSandbox") - .field("memory_layout", &self.mgr.unwrap_mgr().layout) + .field("memory_layout", &self.mgr.layout) .finish() } } -impl crate::sandbox_state::sandbox::Sandbox for UninitializedSandbox { - fn check_stack_guard(&self) -> Result { - log_then_return!( - "Checking the stack cookie before the sandbox is initialized is unsupported" - ); - } -} - -impl - EvolvableSandbox< - UninitializedSandbox, - MultiUseSandbox, - Noop, - > for UninitializedSandbox -{ - /// Evolve `self` to a `MultiUseSandbox` without any additional metadata. - #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] - fn evolve(self, _: Noop) -> Result { - evolve_impl_multi_use(self) - } -} - /// A `GuestBinary` is either a buffer or the file path to some data (e.g., a guest binary). #[derive(Debug)] pub enum GuestBinary<'a> { @@ -152,7 +105,10 @@ impl<'a> From<&'a [u8]> for GuestBlob<'a> { } } -/// A `GuestEnvironment` is a structure that contains the guest binary and an optional GuestBinary. +/// Container for a guest binary and optional initialization data. +/// +/// This struct combines a guest binary (either from a file or memory buffer) with +/// optional data that will be available to the guest during execution. #[derive(Debug)] pub struct GuestEnvironment<'a, 'b> { /// The guest binary, which can be a file path or a buffer. @@ -181,13 +137,12 @@ impl<'a> From> for GuestEnvironment<'a, '_> { } impl UninitializedSandbox { - /// Create a new sandbox configured to run the binary at path - /// `bin_path`. + /// Creates a new uninitialized sandbox for the given guest environment. /// - /// The instrument attribute is used to generate tracing spans and also to emit an error should the Result be an error. - /// The skip attribute is used to skip the guest binary from being printed in the tracing span. - /// The name attribute is used to name the tracing span. - /// The err attribute is used to emit an error should the Result be an error, it uses the std::`fmt::Debug trait` to print the error. + /// The guest binary can be provided as either a file path or memory buffer. + /// An optional configuration can customize memory sizes and sandbox settings. + /// After creation, register host functions using [`register`](Self::register) + /// before calling [`evolve`](Self::evolve) to complete initialization and create the VM. #[instrument( err(Debug), skip(env), @@ -250,17 +205,11 @@ impl UninitializedSandbox { } }; - let mut mem_mgr_wrapper = { - let mut mgr = UninitializedSandbox::load_guest_binary( - sandbox_cfg, - &guest_binary, - guest_blob.as_ref(), - )?; - - let stack_guard = Self::create_stack_guard(); - mgr.set_stack_guard(&stack_guard)?; - MemMgrWrapper::new(mgr, stack_guard) - }; + let (mut mem_mgr_wrapper, load_info) = UninitializedSandbox::load_guest_binary( + sandbox_cfg, + &guest_binary, + guest_blob.as_ref(), + )?; mem_mgr_wrapper.write_memory_layout()?; @@ -278,6 +227,7 @@ impl UninitializedSandbox { config: sandbox_cfg, #[cfg(any(crashdump, gdb))] rt_cfg, + load_info, }; // If we were passed a writer for host print register it otherwise use the default. @@ -288,9 +238,14 @@ impl UninitializedSandbox { Ok(sandbox) } - #[instrument(skip_all, parent = Span::current(), level = "Trace")] - fn create_stack_guard() -> [u8; STACK_COOKIE_LEN] { - rand::random::<[u8; STACK_COOKIE_LEN]>() + /// Creates and initializes the virtual machine, transforming this into a ready-to-use sandbox. + /// + /// This method consumes the `UninitializedSandbox` and performs the final initialization + /// steps to create the underlying virtual machine. Once evolved, the resulting + /// [`MultiUseSandbox`] can execute guest code and handle function calls. + #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] + pub fn evolve(self) -> Result { + evolve_impl_multi_use(self) } /// Load the file at `bin_path_str` into a PE file, then attempt to @@ -308,92 +263,45 @@ impl UninitializedSandbox { cfg: SandboxConfiguration, guest_binary: &GuestBinary, guest_blob: Option<&GuestBlob>, - ) -> Result> { - let mut exe_info = match guest_binary { + ) -> Result<( + SandboxMemoryManager, + crate::mem::exe::LoadInfo, + )> { + let exe_info = match guest_binary { GuestBinary::FilePath(bin_path_str) => ExeInfo::from_file(bin_path_str)?, GuestBinary::Buffer(buffer) => ExeInfo::from_buf(buffer)?, }; - SandboxMemoryManager::load_guest_binary_into_memory(cfg, &mut exe_info, guest_blob) + SandboxMemoryManager::load_guest_binary_into_memory(cfg, exe_info, guest_blob) } - /// Set the max log level to be used by the guest. - /// If this is not set then the log level will be determined by parsing the RUST_LOG environment variable. - /// If the RUST_LOG environment variable is not set then the max log level will be set to `LevelFilter::Error`. + /// Sets the maximum log level for guest code execution. + /// + /// If not set, the log level is determined by the `RUST_LOG` environment variable, + /// defaulting to [`LevelFilter::Error`] if unset. pub fn set_max_guest_log_level(&mut self, log_level: LevelFilter) { self.max_guest_log_level = Some(log_level); } - /// Register a host function with the given name in the sandbox. + /// Registers a host function that the guest can call. pub fn register( &mut self, name: impl AsRef, host_func: impl Into>, ) -> Result<()> { - register_host_function(host_func, self, name.as_ref(), None) - } - - /// Register the host function with the given name in the sandbox. - /// Unlike `register`, this variant takes a list of extra syscalls that will - /// allowed during the execution of the function handler. - #[cfg(all(feature = "seccomp", target_os = "linux"))] - pub fn register_with_extra_allowed_syscalls< - Args: ParameterTuple, - Output: SupportedReturnType, - >( - &mut self, - name: impl AsRef, - host_func: impl Into>, - extra_allowed_syscalls: impl IntoIterator, - ) -> Result<()> { - let extra_allowed_syscalls: Vec<_> = extra_allowed_syscalls.into_iter().collect(); - register_host_function(host_func, self, name.as_ref(), Some(extra_allowed_syscalls)) + register_host_function(host_func, self, name.as_ref()) } - /// Register a host function named "HostPrint" that will be called by the guest - /// when it wants to print to the console. - /// The "HostPrint" host function is kind of special, as we expect it to have the - /// `FnMut(String) -> i32` signature. + /// Registers the special "HostPrint" function for guest printing. + /// + /// This overrides the default behavior of writing to stdout. + /// The function expects the signature `FnMut(String) -> i32` + /// and will be called when the guest wants to print output. pub fn register_print( &mut self, print_func: impl Into>, ) -> Result<()> { - #[cfg(not(all(target_os = "linux", feature = "seccomp")))] - self.register("HostPrint", print_func)?; - - #[cfg(all(target_os = "linux", feature = "seccomp"))] - self.register_with_extra_allowed_syscalls( - "HostPrint", - print_func, - EXTRA_ALLOWED_SYSCALLS_FOR_WRITER_FUNC.iter().copied(), - )?; - - Ok(()) - } - - /// Register a host function named "HostPrint" that will be called by the guest - /// when it wants to print to the console. - /// The "HostPrint" host function is kind of special, as we expect it to have the - /// `FnMut(String) -> i32` signature. - /// Unlike `register_print`, this variant takes a list of extra syscalls that will - /// allowed during the execution of the function handler. - #[cfg(all(feature = "seccomp", target_os = "linux"))] - pub fn register_print_with_extra_allowed_syscalls( - &mut self, - print_func: impl Into>, - extra_allowed_syscalls: impl IntoIterator, - ) -> Result<()> { - #[cfg(all(target_os = "linux", feature = "seccomp"))] - self.register_with_extra_allowed_syscalls( - "HostPrint", - print_func, - EXTRA_ALLOWED_SYSCALLS_FOR_WRITER_FUNC - .iter() - .copied() - .chain(extra_allowed_syscalls), - )?; - - Ok(()) + self.register("HostPrint", print_func) } } // Check to see if the current version of Windows is supported @@ -433,8 +341,6 @@ mod tests { use crate::sandbox::SandboxConfiguration; use crate::sandbox::uninitialized::{GuestBinary, GuestEnvironment}; - use crate::sandbox_state::sandbox::EvolvableSandbox; - use crate::sandbox_state::transition::Noop; use crate::{MultiUseSandbox, Result, UninitializedSandbox, new_error}; #[test] @@ -445,10 +351,10 @@ mod tests { GuestEnvironment::new(GuestBinary::FilePath(binary_path.clone()), Some(&buffer)); let uninitialized_sandbox = UninitializedSandbox::new(guest_env, None).unwrap(); - let mut sandbox: MultiUseSandbox = uninitialized_sandbox.evolve(Noop::default()).unwrap(); + let mut sandbox: MultiUseSandbox = uninitialized_sandbox.evolve().unwrap(); let res = sandbox - .call_guest_function_by_name::>("ReadFromUserMemory", (4u64, buffer.to_vec())) + .call::>("ReadFromUserMemory", (4u64, buffer.to_vec())) .expect("Failed to call ReadFromUserMemory"); assert_eq!(res, buffer.to_vec()); @@ -489,7 +395,7 @@ mod tests { // Get a Sandbox from an uninitialized sandbox without a call back function - let _sandbox: MultiUseSandbox = uninitialized_sandbox.evolve(Noop::default()).unwrap(); + let _sandbox: MultiUseSandbox = uninitialized_sandbox.evolve().unwrap(); // Test with a valid guest binary buffer @@ -537,7 +443,7 @@ mod tests { usbox.register("test0", |arg: i32| Ok(arg + 1)).unwrap(); - let sandbox: Result = usbox.evolve(Noop::default()); + let sandbox: Result = usbox.evolve(); assert!(sandbox.is_ok()); let sandbox = sandbox.unwrap(); @@ -562,7 +468,7 @@ mod tests { usbox.register("test1", |a: i32, b: i32| Ok(a + b)).unwrap(); - let sandbox: Result = usbox.evolve(Noop::default()); + let sandbox: Result = usbox.evolve(); assert!(sandbox.is_ok()); let sandbox = sandbox.unwrap(); @@ -595,7 +501,7 @@ mod tests { }) .unwrap(); - let sandbox: Result = usbox.evolve(Noop::default()); + let sandbox: Result = usbox.evolve(); assert!(sandbox.is_ok()); let sandbox = sandbox.unwrap(); @@ -613,7 +519,7 @@ mod tests { // calling a function that doesn't exist { let usbox = uninitialized_sandbox(); - let sandbox: Result = usbox.evolve(Noop::default()); + let sandbox: Result = usbox.evolve(); assert!(sandbox.is_ok()); let sandbox = sandbox.unwrap(); @@ -836,11 +742,9 @@ mod tests { .host_print(format!("Print from UninitializedSandbox on Thread {}\n", i)) .unwrap(); - let sandbox = uninitialized_sandbox - .evolve(Noop::default()) - .unwrap_or_else(|_| { - panic!("Failed to initialize UninitializedSandbox thread {}", i) - }); + let sandbox = uninitialized_sandbox.evolve().unwrap_or_else(|_| { + panic!("Failed to initialize UninitializedSandbox thread {}", i) + }); sq.push(sandbox).unwrap_or_else(|_| { panic!("Failed to push UninitializedSandbox thread {}", i) @@ -974,14 +878,13 @@ mod tests { (metadata_values_map, "module_path"), (metadata_values_map, "target"), ]); - if let Ok(err_vals) = err_vals_res { - if err_vals[0] == "ERROR" - && err_vals[1].starts_with(expected_error_start) - && err_vals[2] == "hyperlight_host::sandbox::uninitialized" - && err_vals[3] == "hyperlight_host::sandbox::uninitialized" - { - count_matching_events += 1; - } + if let Ok(err_vals) = err_vals_res + && err_vals[0] == "ERROR" + && err_vals[1].starts_with(expected_error_start) + && err_vals[2] == "hyperlight_host::sandbox::uninitialized" + && err_vals[3] == "hyperlight_host::sandbox::uninitialized" + { + count_matching_events += 1; } } assert!( @@ -1128,7 +1031,7 @@ mod tests { ); res.unwrap() }; - let _: Result = sbox.evolve(Noop::default()); + let _: Result = sbox.evolve(); let num_calls = TEST_LOGGER.num_log_calls(); diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index a37f747e2..d1ba71ef9 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +#[cfg(gdb)] use std::sync::{Arc, Mutex}; use rand::Rng; @@ -21,65 +22,36 @@ use tracing::{Span, instrument}; use super::SandboxConfiguration; use super::hypervisor::{HypervisorType, get_available_hypervisor}; -#[cfg(gdb)] -use super::mem_access::dbg_mem_access_handler_wrapper; #[cfg(any(crashdump, gdb))] use super::uninitialized::SandboxRuntimeConfig; use crate::HyperlightError::NoHypervisorFound; use crate::hypervisor::Hypervisor; -use crate::hypervisor::handlers::{MemAccessHandlerCaller, OutBHandlerCaller}; +use crate::mem::exe::LoadInfo; use crate::mem::layout::SandboxMemoryLayout; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::{GuestPtr, RawPtr}; use crate::mem::ptr_offset::Offset; use crate::mem::shared_mem::GuestSharedMemory; -#[cfg(feature = "init-paging")] +#[cfg(any(feature = "init-paging", target_os = "windows"))] use crate::mem::shared_mem::SharedMemory; #[cfg(gdb)] use crate::sandbox::config::DebugInfo; -use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::mem_access::mem_access_handler_wrapper; -use crate::sandbox::outb::outb_handler_wrapper; -use crate::sandbox::{HostSharedMemory, MemMgrWrapper}; -use crate::sandbox_state::sandbox::Sandbox; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; #[cfg(target_os = "linux")] use crate::signal_handlers::setup_signal_handlers; use crate::{MultiUseSandbox, Result, UninitializedSandbox, log_then_return, new_error}; -/// The implementation for evolving `UninitializedSandbox`es to -/// `Sandbox`es. -/// -/// Note that `cb_opt`'s type has been carefully considered. -/// Particularly, it's not using a constrained generic to define -/// the type of the callback because if it did, you'd have to provide -/// type hints to the compiler if you want to pass `None` to the function. -/// With this type signature, you can pass `None` without having to do that. -/// -/// If this doesn't make sense, and you want to change this type, -/// please reach out to a Hyperlight developer before making the change. -#[instrument(err(Debug), skip_all, , parent = Span::current(), level = "Trace")] -fn evolve_impl( - u_sbox: UninitializedSandbox, - transform: TransformFunc, -) -> Result -where - TransformFunc: Fn( - Arc>, - MemMgrWrapper, - Box, - Arc>, - Arc>, - RawPtr, - ) -> Result, -{ +#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] +pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result { let (hshm, mut gshm) = u_sbox.mgr.build(); let mut vm = set_up_hypervisor_partition( &mut gshm, &u_sbox.config, #[cfg(any(crashdump, gdb))] &u_sbox.rt_cfg, + u_sbox.load_info, )?; - let outb_hdl = outb_handler_wrapper(hshm.clone(), u_sbox.host_funcs.clone()); let seed = { let mut rng = rand::rng(); @@ -91,10 +63,9 @@ where }; let page_size = u32::try_from(page_size::get())?; - let mem_access_hdl = mem_access_handler_wrapper(hshm.clone()); #[cfg(gdb)] - let dbg_mem_access_hdl = dbg_mem_access_handler_wrapper(hshm.clone()); + let dbg_mem_access_hdl = Arc::new(Mutex::new(hshm.clone())); #[cfg(target_os = "linux")] setup_signal_handlers(&u_sbox.config)?; @@ -103,54 +74,38 @@ where peb_addr, seed, page_size, - outb_hdl.clone(), - mem_access_hdl.clone(), + hshm.clone(), + u_sbox.host_funcs.clone(), u_sbox.max_guest_log_level, #[cfg(gdb)] dbg_mem_access_hdl, )?; - let dispatch_function_addr = hshm.as_ref().get_pointer_to_dispatch_function()?; + let dispatch_function_addr = hshm.get_pointer_to_dispatch_function()?; if dispatch_function_addr == 0 { return Err(new_error!("Dispatch function address is null")); } - transform( + let dispatch_ptr = RawPtr::from(dispatch_function_addr); + + #[cfg(gdb)] + let dbg_mem_wrapper = Arc::new(Mutex::new(hshm.clone())); + + Ok(MultiUseSandbox::from_uninit( u_sbox.host_funcs, hshm, vm, - outb_hdl, - mem_access_hdl, - RawPtr::from(dispatch_function_addr), - ) -} - -#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] -pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result { - evolve_impl( - u_sbox, - |hf, mut hshm, vm, out_hdl, mem_hdl, dispatch_ptr| { - { - hshm.as_mut().push_state()?; - } - Ok(MultiUseSandbox::from_uninit( - hf, - hshm.clone(), - vm, - out_hdl, - mem_hdl, - dispatch_ptr, - #[cfg(gdb)] - dbg_mem_access_handler_wrapper(hshm), - )) - }, - ) + dispatch_ptr, + #[cfg(gdb)] + dbg_mem_wrapper, + )) } pub(crate) fn set_up_hypervisor_partition( mgr: &mut SandboxMemoryManager, #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, #[cfg(any(crashdump, gdb))] rt_cfg: &SandboxRuntimeConfig, + _load_info: LoadInfo, ) -> Result> { #[cfg(feature = "init-paging")] let rsp_ptr = { @@ -209,6 +164,9 @@ pub(crate) fn set_up_hypervisor_partition( None }; + #[cfg(feature = "mem_profile")] + let trace_info = MemTraceInfo::new(_load_info)?; + match *get_available_hypervisor() { #[cfg(mshv)] Some(HypervisorType::Mshv) => { @@ -222,6 +180,8 @@ pub(crate) fn set_up_hypervisor_partition( gdb_conn, #[cfg(crashdump)] rt_cfg.clone(), + #[cfg(feature = "mem_profile")] + trace_info, )?; Ok(Box::new(hv)) } @@ -238,6 +198,8 @@ pub(crate) fn set_up_hypervisor_partition( gdb_conn, #[cfg(crashdump)] rt_cfg.clone(), + #[cfg(feature = "mem_profile")] + trace_info, )?; Ok(Box::new(hv)) } @@ -260,6 +222,8 @@ pub(crate) fn set_up_hypervisor_partition( gdb_conn, #[cfg(crashdump)] rt_cfg.clone(), + #[cfg(feature = "mem_profile")] + trace_info, )?; Ok(Box::new(hv)) } @@ -272,7 +236,7 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(test)] mod tests { - use hyperlight_testing::{callback_guest_as_string, simple_guest_as_string}; + use hyperlight_testing::simple_guest_as_string; use super::evolve_impl_multi_use; use crate::UninitializedSandbox; @@ -280,10 +244,7 @@ mod tests { #[test] fn test_evolve() { - let guest_bin_paths = vec![ - simple_guest_as_string().unwrap(), - callback_guest_as_string().unwrap(), - ]; + let guest_bin_paths = vec![simple_guest_as_string().unwrap()]; for guest_bin_path in guest_bin_paths { let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(guest_bin_path.clone()), None) diff --git a/src/hyperlight_host/src/sandbox_state/sandbox.rs b/src/hyperlight_host/src/sandbox_state/sandbox.rs deleted file mode 100644 index 4f9641af8..000000000 --- a/src/hyperlight_host/src/sandbox_state/sandbox.rs +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use std::fmt::Debug; - -use tracing::{Span, instrument}; - -use super::transition::TransitionMetadata; -use crate::{Result, new_error}; - -/// The minimal functionality of a Hyperlight sandbox. Most of the types -/// and operations within this crate require `Sandbox` implementations. -/// -/// `Sandbox`es include the notion of an ordering in a state machine. -/// For example, a given `Sandbox` implementation may be the root node -/// in the state machine to which it belongs, and it may know how to "evolve" -/// into a next state. That "next state" may in turn know how to roll back -/// to the root node. -/// -/// These transitions are expressed as `EvolvableSandbox` and -/// `DevolvableSandbox` implementations any `Sandbox` implementation can -/// opt into. -pub trait Sandbox: Sized + Debug { - /// Check to ensure the current stack cookie matches the one that - /// was selected when the stack was constructed. - /// - /// Return an `Err` if there was an error inspecting the stack, `Ok(false)` - /// if there was no such error but the stack guard doesn't match, and - /// `Ok(true)` in the same situation where the stack guard does match. - /// - - // NOTE: this is only needed for UninitializedSandbox and MultiUseSandbox - // Those are the only types that need implement this trait - // The default implementation is provided so that types that implement Sandbox (e.g. JSSandbox) but do not need to implement this trait do not need to provide an implementation - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn check_stack_guard(&self) -> Result { - Err(new_error!( - "check_stack_guard not implemented for this type" - )) - } -} - -/// A utility trait to recognize a Sandbox that has not yet been initialized. -/// It allows retrieval of a strongly typed UninitializedSandbox. -pub trait UninitializedSandbox: Sandbox { - /// Retrieves reference to strongly typed `UninitializedSandbox` - fn get_uninitialized_sandbox(&self) -> &crate::sandbox::UninitializedSandbox; - - /// Retrieves mutable reference to strongly typed `UninitializedSandbox` - fn get_uninitialized_sandbox_mut(&mut self) -> &mut crate::sandbox::UninitializedSandbox; -} - -/// A `Sandbox` that knows how to "evolve" into a next state. -pub trait EvolvableSandbox>: - Sandbox -{ - /// Evolve `Self` to `Next` providing Metadata. - fn evolve(self, tsn: T) -> Result; -} - -/// A `Sandbox` that knows how to roll back to a "previous" `Sandbox` -pub trait DevolvableSandbox>: - Sandbox -{ - /// Devolve `Self` to `Prev` providing Metadata. - fn devolve(self, tsn: T) -> Result; -} diff --git a/src/hyperlight_host/src/sandbox_state/transition.rs b/src/hyperlight_host/src/sandbox_state/transition.rs deleted file mode 100644 index 3cd7aaadf..000000000 --- a/src/hyperlight_host/src/sandbox_state/transition.rs +++ /dev/null @@ -1,178 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use std::marker::PhantomData; - -use tracing::{Span, instrument}; - -use super::sandbox::Sandbox; -use crate::Result; -use crate::func::call_ctx::MultiUseGuestCallContext; - -/// Metadata about an evolution or devolution. Any `Sandbox` implementation -/// that also implements `EvolvableSandbox` or `DevolvableSandbox` -/// can decide the following things in a type-safe way: -/// -/// 1. That transition is possible -/// 2. That transition requires a specific kind of metadata -/// -/// For example, if you have the following structs: -/// -/// ```ignore -/// struct MySandbox1 {} -/// struct MySandbox2 {} -/// -/// impl Sandbox for MySandbox1 {...} -/// impl Sandbox for MySandbox2 {...} -/// ``` -/// -/// ...then you can define a metadata-free evolve transition between -/// `MySandbox1` and `MySandbox2`, and a devolve transition that requires -/// a callback between `MySandbox2` and `MySandbox` as follows: -/// -/// ```ignore -/// impl EvolvableSandbox< -/// MySandbox1, -/// MySandbox2, -/// Noop -/// > for MySandbox1 { -/// fn evolve( -/// self, -/// _: Noop -/// ) -> Result { -/// Ok(MySandbox2{}) -/// } -/// } -/// -/// ``` -/// -/// Most transitions will likely involve `Noop`, but some may involve -/// implementing their own. -pub trait TransitionMetadata {} - -/// Transition metadata that contains and does nothing. `Noop` is a -/// placeholder when you want to implement an `EvolvableSandbox` -/// or `DevolvableSandbox` that needs no additional metadata to succeed. -/// -/// Construct one of these by using the `default()` method. -pub struct Noop { - cur_ph: PhantomData, - next_ph: PhantomData, -} - -impl Default for Noop { - fn default() -> Self { - Self { - cur_ph: PhantomData, - next_ph: PhantomData, - } - } -} - -impl TransitionMetadata for Noop {} - -/// A `TransitionMetadata` that calls a callback. The callback function takes -/// a mutable reference to a `MultiUseGuestCallContext` and returns a `Result<()>` -/// to signify success or failure of the function. -/// -/// The function use the context to call guest functions. -/// -/// Construct one of these by passing your callback to -/// `MultiUseContextCallback::from`, as in the following code (assuming `MySandbox` -/// is a `Sandbox` implementation): -/// -/// ```ignore -/// let my_cb_fn: dyn FnOnce(&mut MultiUseGuestCallContext) -> Result<()> = |_sbox| { -/// println!("hello world!"); -/// }; -/// let mutating_cb = MultiUseContextCallback::from(my_cb_fn); -/// ``` -pub struct MultiUseContextCallback<'func, Cur: Sandbox, F> -where - F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()> + 'func, -{ - cur_ph: PhantomData, - fn_life_ph: PhantomData<&'func ()>, - cb: F, -} - -impl TransitionMetadata - for MultiUseContextCallback<'_, Cur, F> -where - F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()>, -{ -} - -impl MultiUseContextCallback<'_, Cur, F> -where - F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()>, -{ - /// Invokes the callback on the provided guest context - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - pub fn call(self, cur: &mut MultiUseGuestCallContext) -> Result<()> { - (self.cb)(cur) - } -} - -impl<'a, Cur: Sandbox, F> From for MultiUseContextCallback<'a, Cur, F> -where - F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()> + 'a, -{ - #[instrument(skip_all, parent = Span::current(), level= "Trace")] - fn from(val: F) -> Self { - MultiUseContextCallback { - cur_ph: PhantomData, - fn_life_ph: PhantomData, - cb: val, - } - } -} -#[cfg(test)] -mod tests { - use super::Noop; - use crate::Result; - use crate::sandbox_state::sandbox::{DevolvableSandbox, EvolvableSandbox, Sandbox}; - - #[derive(Debug, Eq, PartialEq, Clone)] - struct MySandbox1 {} - #[derive(Debug, Eq, PartialEq, Clone)] - struct MySandbox2 {} - - impl Sandbox for MySandbox1 {} - impl Sandbox for MySandbox2 {} - - impl EvolvableSandbox> for MySandbox1 { - fn evolve(self, _: Noop) -> Result { - Ok(MySandbox2 {}) - } - } - - impl DevolvableSandbox> for MySandbox2 { - fn devolve(self, _: Noop) -> Result { - Ok(MySandbox1 {}) - } - } - - #[test] - fn test_evolve_devolve() { - let sbox_1_1 = MySandbox1 {}; - let sbox_2_1 = sbox_1_1.clone().evolve(Noop::default()).unwrap(); - let sbox_1_2 = sbox_2_1.clone().devolve(Noop::default()).unwrap(); - let sbox_2_2 = sbox_1_2.clone().evolve(Noop::default()).unwrap(); - assert_eq!(sbox_1_1, sbox_1_2); - assert_eq!(sbox_2_1, sbox_2_2); - } -} diff --git a/src/hyperlight_host/src/seccomp/guest.rs b/src/hyperlight_host/src/seccomp/guest.rs deleted file mode 100644 index cd4801d89..000000000 --- a/src/hyperlight_host/src/seccomp/guest.rs +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -use seccompiler::SeccompCmpOp::Eq; -use seccompiler::{ - BpfProgram, SeccompAction, SeccompCmpArgLen as ArgLen, SeccompCondition as Cond, SeccompFilter, - SeccompRule, TargetArch, -}; - -use crate::sandbox::ExtraAllowedSyscall; -use crate::{Result, and, or}; - -fn syscalls_allowlist() -> Result)>> { - Ok(vec![ - // SYS_signalstack, SYS_munmap, SYS_rt_sigprocmask, SYS_madvise, and SYS_exit - // are minimally required syscalls to be able to setup our seccomp filter. - (libc::SYS_sigaltstack, vec![]), - (libc::SYS_munmap, vec![]), - (libc::SYS_rt_sigprocmask, vec![]), - (libc::SYS_madvise, vec![]), - (libc::SYS_exit, vec![]), - // SYS_rt_sigaction, SYS_write, and SYS_rt_sigreturn are required for the - // signal handler inside the host function worker thread. - (libc::SYS_rt_sigaction, vec![]), - ( - libc::SYS_write, - or![ - and![Cond::new(0, ArgLen::Dword, Eq, 1)?], // stdout - and![Cond::new(0, ArgLen::Dword, Eq, 2)?], // stderr - ], - ), - (libc::SYS_rt_sigreturn, vec![]), - // Note: This `ioctl` is used to get information about the terminal. - // I believe it is used to get terminal information by our default writer function. - // That said, I am registering it here instead of in the function specifically - // because we don't currently support registering parameterized syscalls. - ( - libc::SYS_ioctl, - or![and![Cond::new( - 1, - ArgLen::Dword, - Eq, - #[cfg(all( - target_arch = "x86_64", - target_vendor = "unknown", - target_os = "linux", - target_env = "musl" - ))] - libc::TCGETS.try_into()?, - #[cfg(not(all( - target_arch = "x86_64", - target_vendor = "unknown", - target_os = "linux", - target_env = "musl" - )))] - libc::TCGETS, - )?]], - ), - // `futex` is needed for some tests that run in parallel (`simple_test_parallel`, - // and `callback_test_parallel`). - (libc::SYS_futex, vec![]), - // `sched_yield` is needed for many synchronization primitives that may be invoked - // on the host function worker thread - (libc::SYS_sched_yield, vec![]), - // `mprotect` is needed by malloc during memory allocation - (libc::SYS_mprotect, vec![]), - // `openat` is marked allowed here because it may be called by `libc::free()` - // since it will try to open /proc/sys/vm/overcommit_memory (https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/malloc-sysdep.h;h=778d8971d53e284397c3a5dcdd923e93be5e4731;hb=HEAD) - // We have another more restrictive filter for it below so it will return EACCES instead of trap, in which case libc will use the default value - (libc::SYS_openat, vec![]), - ]) -} - -/// Creates two `BpfProgram`s for a `SeccompFilter` over specific syscalls/`SeccompRule`s -/// intended to be applied on host function threads. -/// -/// Note: This does not provide coverage over the Hyperlight host, which is why we don't need -/// `SeccompRules` for operations we definitely perform but are outside the handler thread -/// (e.g., `KVM_SET_USER_MEMORY_REGION`, `KVM_GET_API_VERSION`, `KVM_CREATE_VM`, -/// or `KVM_CREATE_VCPU`). -pub(crate) fn get_seccomp_filter_for_host_function_worker_thread( - extra_allowed_syscalls: Option<&[ExtraAllowedSyscall]>, -) -> Result> { - let mut allowed_syscalls = syscalls_allowlist()?; - - if let Some(extra_allowed_syscalls) = extra_allowed_syscalls { - allowed_syscalls.extend( - extra_allowed_syscalls - .iter() - .copied() - .map(|syscall| (syscall, vec![])), - ); - - // Remove duplicates - allowed_syscalls.sort_by(|a, b| a.0.cmp(&b.0)); - allowed_syscalls.dedup(); - } - - let arch: TargetArch = std::env::consts::ARCH.try_into()?; - - // Allowlist filter that traps on unknown syscalls - let allowlist = SeccompFilter::new( - allowed_syscalls.into_iter().collect(), - SeccompAction::Trap, - SeccompAction::Allow, - arch, - )? - .try_into()?; - - // If `openat` is an exclicitly allowed syscall, we shouldn't return the filter that forces it to return EACCES. - if let Some(extra_syscalls) = extra_allowed_syscalls { - if extra_syscalls.contains(&libc::SYS_openat) { - return Ok(vec![allowlist]); - } - } - // Otherwise, we return both filters. - - // Filter that forces `openat` to return EACCES - let errno_on_openat = SeccompFilter::new( - [(libc::SYS_openat, vec![])].into_iter().collect(), - SeccompAction::Allow, - SeccompAction::Errno(libc::EACCES.try_into()?), - arch, - )? - .try_into()?; - - // Note: the order of the 2 filters are important. If we applied the strict filter first, - // we wouldn't be allowed to setup the second filter (would be trapped since the syscalls to setup seccomp are not allowed). - // However, from an seccomp filter perspective, the order of the filters is not important: - // - // If multiple filters exist, they are all executed, in reverse order - // of their addition to the filter tree—that is, the most recently - // installed filter is executed first. (Note that all filters will - // be called even if one of the earlier filters returns - // SECCOMP_RET_KILL. This is done to simplify the kernel code and to - // provide a tiny speed-up in the execution of sets of filters by - // avoiding a check for this uncommon case.) The return value for - // the evaluation of a given system call is the first-seen action - // value of highest precedence (along with its accompanying data) - // returned by execution of all of the filters. - // - // (https://man7.org/linux/man-pages/man2/seccomp.2.html). - // - Ok(vec![errno_on_openat, allowlist]) -} diff --git a/src/hyperlight_host/src/seccomp/mod.rs b/src/hyperlight_host/src/seccomp/mod.rs deleted file mode 100644 index 9edce3ed2..000000000 --- a/src/hyperlight_host/src/seccomp/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -// For more information on seccomp and its implementation in Hyperlight, -// refer to: https://github.com/hyperlight-dev/hyperlight/blob/dev/docs/seccomp.md - -/// This module defines all seccomp filters (i.e., used for blockage of non-specified syscalls) -/// needed for execution of guest code within Hyperlight through a syscalls allow-list. -pub(crate) mod guest; - -// The credit on the creation of the macros below goes to the cloud-hypervisor team -// (https://github.com/cloud-hypervisor/cloud-hypervisor/blob/main/vmm/src/seccomp_filters.rs) - -/// Shorthand for chaining `SeccompCondition`s with the `and` operator in a `SeccompRule`. -/// The rule will take the `Allow` action if _all_ the conditions are true. -#[macro_export] -macro_rules! and { - ($($x:expr),*) => (SeccompRule::new(vec![$($x),*]).unwrap()) -} - -/// Shorthand for chaining `SeccompRule`s with the `or` operator in a `SeccompFilter`. -#[macro_export] -macro_rules! or { - ($($x:expr,)*) => (vec![$($x),*]); - ($($x:expr),*) => (vec![$($x),*]) -} diff --git a/src/hyperlight_host/src/signal_handlers/mod.rs b/src/hyperlight_host/src/signal_handlers/mod.rs index bf35d499d..71bba7326 100644 --- a/src/hyperlight_host/src/signal_handlers/mod.rs +++ b/src/hyperlight_host/src/signal_handlers/mod.rs @@ -18,41 +18,17 @@ use libc::c_int; use crate::sandbox::SandboxConfiguration; -#[cfg(feature = "seccomp")] -pub mod sigsys_signal_handler; - pub(crate) fn setup_signal_handlers(config: &SandboxConfiguration) -> crate::Result<()> { // This is unsafe because signal handlers only allow a very restrictive set of // functions (i.e., async-signal-safe functions) to be executed inside them. // Anything that performs memory allocations, locks, and others are non-async-signal-safe. // Hyperlight signal handlers are all designed to be async-signal-safe, so this function // should be safe to call. - #[cfg(feature = "seccomp")] - { - vmm_sys_util::signal::register_signal_handler( - libc::SIGSYS, - sigsys_signal_handler::handle_sigsys, - )?; - - let original_hook = std::panic::take_hook(); - // Set a custom panic hook that checks for "DisallowedSyscall" - std::panic::set_hook(Box::new(move |panic_info| { - // Check if the panic payload matches "DisallowedSyscall" - if let Some(crate::HyperlightError::DisallowedSyscall) = panic_info - .payload() - .downcast_ref::( - ) { - // Do nothing to avoid superfluous syscalls - return; - } - // If not "DisallowedSyscall", use the original hook - original_hook(panic_info); - })); - } vmm_sys_util::signal::register_signal_handler( libc::SIGRTMIN() + config.get_interrupt_vcpu_sigrtmin_offset() as c_int, vm_kill_signal, - )?; + ) + .map_err(crate::HyperlightError::VmmSysError)?; // Note: For libraries registering signal handlers, it's important to keep in mind that // the user of the library could have their own signal handlers that we don't want to diff --git a/src/hyperlight_host/src/signal_handlers/sigsys_signal_handler.rs b/src/hyperlight_host/src/signal_handlers/sigsys_signal_handler.rs index 1bab82e1e..dc0572d55 100644 --- a/src/hyperlight_host/src/signal_handlers/sigsys_signal_handler.rs +++ b/src/hyperlight_host/src/signal_handlers/sigsys_signal_handler.rs @@ -14,93 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -#[cfg(feature = "seccomp")] -pub(super) extern "C" fn handle_sigsys( - signal: i32, - info: *mut libc::siginfo_t, - context: *mut libc::c_void, -) { - #[cfg(target_arch = "x86_64")] - { - unsafe { - // si_code contains the reason for the SIGSYS signal. - // SYS_SECCOMP is 1 as per: - // https://github.com/torvalds/linux/blob/81983758430957d9a5cb3333fe324fd70cf63e7e/include/uapi/asm-generic/siginfo.h#L301C9-L301C21 - const SYS_SECCOMP: libc::c_int = 1; - // Sanity checks to make sure SIGSYS was triggered by a BPF filter. - // If something else triggered a SIGSYS (i.e., kill()), we do nothing. - // Inspired by Chromium's sandbox: - // https://chromium.googlesource.com/chromium/chromium/+/master/sandbox/linux/seccomp-bpf/sandbox_bpf.cc#572 - if signal != libc::SIGSYS - || (*info).si_code != SYS_SECCOMP - || context.is_null() - || (*info).si_errno < 0 - { - let err_msg = - b"[ERROR][HYPERLIGHT] SIGSYS triggered by something other than a BPF filter\n"; - libc::write( - libc::STDERR_FILENO, - err_msg.as_ptr() as *const _, - err_msg.len(), - ); - return; - } - - let err_msg = b"[ERROR][HYPERLIGHT] Handling disallowed syscall\n"; - libc::write( - libc::STDERR_FILENO, - err_msg.as_ptr() as *const _, - err_msg.len(), - ); - - // We get the syscall number by accessing a particular offset in the `siginfo_t` struct. - // This only works because this is handling a SIGSYS signal (i.e., the `siginfo_t` struct - // is implemented as a union in the kernel: - // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/siginfo.h). - // Note: This is not necessarily platform-agnostic, so we might want to be more careful here - // in the future. - const SI_OFF_SYSCALL: isize = 6; - let syscall = *(info as *const i32).offset(SI_OFF_SYSCALL) as usize; - let syscall_bytes = raw_format(b"[ERROR][HYPERLIGHT] Disallowed Syscall: ", syscall); - - // `write` as per https://man7.org/linux/man-pages/man7/signal-safety.7.html - // is async-signal-safe. - libc::write( - libc::STDERR_FILENO, - syscall_bytes.as_ptr() as *const _, - syscall_bytes.len(), - ); - - // Note: This is not necessarily platform-agnostic, so we might want to be more careful here - // in the future. - let ucontext = context as *mut libc::ucontext_t; - let mcontext = &mut (*ucontext).uc_mcontext; - - if syscall == libc::SYS_ioctl as usize { - let ioctl_param = mcontext.gregs[libc::REG_EBRACE as usize] as usize; - let ioctl_param_bytes = - raw_format(b"[ERROR][HYPERLIGHT] IOCTL Param: ", ioctl_param); - libc::write( - libc::STDERR_FILENO, - ioctl_param_bytes.as_ptr() as *const _, - ioctl_param_bytes.len(), - ); - } - - // We don't want to return execution to the offending host function, so - // we alter the RIP register to point to a function that will panic out of - // the host function call. - mcontext.gregs[libc::REG_RIP as usize] = - after_syscall_violation as usize as libc::greg_t; - } - } - - #[cfg(not(target_arch = "x86_64"))] - { - compile_error!("Unsupported architecture for seccomp feature"); - } -} - extern "C-unwind" fn after_syscall_violation() { #[allow(clippy::panic)] std::panic::panic_any(crate::HyperlightError::DisallowedSyscall); diff --git a/src/hyperlight_host/tests/common/mod.rs b/src/hyperlight_host/tests/common/mod.rs index b3d465bf3..41016d1be 100644 --- a/src/hyperlight_host/tests/common/mod.rs +++ b/src/hyperlight_host/tests/common/mod.rs @@ -14,13 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ use hyperlight_host::func::HostFunction; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{GuestBinary, MultiUseSandbox, Result, UninitializedSandbox}; -use hyperlight_testing::{ - c_callback_guest_as_string, c_simple_guest_as_string, callback_guest_as_string, - simple_guest_as_string, -}; +use hyperlight_testing::{c_simple_guest_as_string, simple_guest_as_string}; /// Returns a rust/c simpleguest depending on environment variable GUEST. /// Uses rust guest by default. Run test with environment variable GUEST="c" to use the c version @@ -40,6 +35,14 @@ pub fn new_uninit_rust() -> Result { ) } +/// Returns a c-language simpleguest. +pub fn new_uninit_c() -> Result { + UninitializedSandbox::new( + GuestBinary::FilePath(c_simple_guest_as_string().unwrap()), + None, + ) +} + pub fn get_simpleguest_sandboxes( writer: Option>, // An optional writer to make sure correct info is passed to the host printer ) -> Vec { @@ -56,15 +59,15 @@ pub fn get_simpleguest_sandboxes( if let Some(writer) = writer.clone() { sandbox.register_print(writer).unwrap(); } - sandbox.evolve(Noop::default()).unwrap() + sandbox.evolve().unwrap() }) .collect() } -pub fn get_callbackguest_uninit_sandboxes( +pub fn get_uninit_simpleguest_sandboxes( writer: Option>, // An optional writer to make sure correct info is passed to the host printer ) -> Vec { - let elf_path = get_c_or_rust_callbackguest_path(); + let elf_path = get_c_or_rust_simpleguest_path(); let sandboxes = [ // in hypervisor elf @@ -91,13 +94,3 @@ pub(crate) fn get_c_or_rust_simpleguest_path() -> String { _ => panic!("Unknown guest type '{guest_type}', use either 'rust' or 'c'"), } } - -// returns the the path of callbackguest binary. Picks rust/ version depending on environment variable GUEST (or rust by default if unset) -fn get_c_or_rust_callbackguest_path() -> String { - let guest_type = std::env::var("GUEST").unwrap_or("rust".to_string()); - match guest_type.as_str() { - "rust" => callback_guest_as_string().unwrap(), - "c" => c_callback_guest_as_string().unwrap(), - _ => panic!("Unknown guest type '{guest_type}', use either 'rust' or 'c'"), - } -} diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 2db32b4a0..b252aa947 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -22,17 +22,13 @@ use std::time::Duration; use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; use hyperlight_common::mem::PAGE_SIZE; use hyperlight_host::sandbox::SandboxConfiguration; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; use hyperlight_host::{GuestBinary, HyperlightError, MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::simplelogger::{LOGGER, SimpleLogger}; -use hyperlight_testing::{ - c_simple_guest_as_string, callback_guest_as_string, simple_guest_as_string, -}; +use hyperlight_testing::{c_simple_guest_as_string, simple_guest_as_string}; use log::LevelFilter; pub mod common; // pub to disable dead_code warning -use crate::common::{new_uninit, new_uninit_rust}; +use crate::common::{new_uninit, new_uninit_c, new_uninit_rust}; // A host function cannot be interrupted, but we can at least make sure after requesting to interrupt a host call, // we don't re-enter the guest again once the host call is done @@ -42,7 +38,7 @@ fn interrupt_host_call() { let barrier2 = barrier.clone(); let mut usbox = UninitializedSandbox::new( - GuestBinary::FilePath(callback_guest_as_string().expect("Guest Binary Missing")), + GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")), None, ) .unwrap(); @@ -53,15 +49,9 @@ fn interrupt_host_call() { Ok(()) }; - #[cfg(any(target_os = "windows", not(feature = "seccomp")))] usbox.register("Spin", spin).unwrap(); - #[cfg(all(target_os = "linux", feature = "seccomp"))] - usbox - .register_with_extra_allowed_syscalls("Spin", spin, vec![libc::SYS_clock_nanosleep]) - .unwrap(); - - let mut sandbox: MultiUseSandbox = usbox.evolve(Noop::default()).unwrap(); + let mut sandbox: MultiUseSandbox = usbox.evolve().unwrap(); let interrupt_handle = sandbox.interrupt_handle(); assert!(!interrupt_handle.dropped()); // not yet dropped @@ -72,9 +62,7 @@ fn interrupt_host_call() { } }); - let result = sandbox - .call_guest_function_by_name::("CallHostSpin", ()) - .unwrap_err(); + let result = sandbox.call::("CallHostSpin", ()).unwrap_err(); assert!(matches!(result, HyperlightError::ExecutionCanceledByHost())); thread.join().unwrap(); @@ -83,7 +71,7 @@ fn interrupt_host_call() { /// Makes sure a running guest call can be interrupted by the host #[test] fn interrupt_in_progress_guest_call() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); let interrupt_handle = sbox1.interrupt_handle(); @@ -98,16 +86,12 @@ fn interrupt_in_progress_guest_call() { assert!(interrupt_handle.dropped()); }); - let res = sbox1 - .call_guest_function_by_name::("Spin", ()) - .unwrap_err(); + let res = sbox1.call::("Spin", ()).unwrap_err(); assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); barrier.wait(); // Make sure we can still call guest functions after the VM was interrupted - sbox1 - .call_guest_function_by_name::("Echo", "hello".to_string()) - .unwrap(); + sbox1.call::("Echo", "hello".to_string()).unwrap(); // drop vm to make sure other thread can detect it drop(sbox1); @@ -118,7 +102,7 @@ fn interrupt_in_progress_guest_call() { /// Makes sure interrupting a vm before the guest call has started also prevents the guest call from being executed #[test] fn interrupt_guest_call_in_advance() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); let interrupt_handle = sbox1.interrupt_handle(); @@ -133,15 +117,11 @@ fn interrupt_guest_call_in_advance() { }); barrier.wait(); // wait until `kill()` is called before starting the guest call - let res = sbox1 - .call_guest_function_by_name::("Spin", ()) - .unwrap_err(); + let res = sbox1.call::("Spin", ()).unwrap_err(); assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); // Make sure we can still call guest functions after the VM was interrupted - sbox1 - .call_guest_function_by_name::("Echo", "hello".to_string()) - .unwrap(); + sbox1.call::("Echo", "hello".to_string()).unwrap(); // drop vm to make sure other thread can detect it drop(sbox1); @@ -160,9 +140,9 @@ fn interrupt_guest_call_in_advance() { /// all possible interleavings, but can hopefully increases confidence somewhat. #[test] fn interrupt_same_thread() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let mut sbox3: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox3: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); @@ -183,9 +163,9 @@ fn interrupt_same_thread() { for _ in 0..NUM_ITERS { barrier.wait(); sbox1 - .call_guest_function_by_name::("Echo", "hello".to_string()) + .call::("Echo", "hello".to_string()) .expect("Only sandbox 2 is allowed to be interrupted"); - match sbox2.call_guest_function_by_name::("Echo", "hello".to_string()) { + match sbox2.call::("Echo", "hello".to_string()) { Ok(_) | Err(HyperlightError::ExecutionCanceledByHost()) => { // Only allow successful calls or interrupted. // The call can be successful in case the call is finished before kill() is called. @@ -193,7 +173,7 @@ fn interrupt_same_thread() { _ => panic!("Unexpected return"), }; sbox3 - .call_guest_function_by_name::("Echo", "hello".to_string()) + .call::("Echo", "hello".to_string()) .expect("Only sandbox 2 is allowed to be interrupted"); } thread.join().expect("Thread should finish"); @@ -202,9 +182,9 @@ fn interrupt_same_thread() { /// Same test as above but with no per-iteration barrier, to get more possible interleavings. #[test] fn interrupt_same_thread_no_barrier() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let mut sbox3: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox3: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); let barrier = Arc::new(Barrier::new(2)); let barrier2 = barrier.clone(); @@ -227,9 +207,9 @@ fn interrupt_same_thread_no_barrier() { barrier.wait(); for _ in 0..NUM_ITERS { sbox1 - .call_guest_function_by_name::("Echo", "hello".to_string()) + .call::("Echo", "hello".to_string()) .expect("Only sandbox 2 is allowed to be interrupted"); - match sbox2.call_guest_function_by_name::("Echo", "hello".to_string()) { + match sbox2.call::("Echo", "hello".to_string()) { Ok(_) | Err(HyperlightError::ExecutionCanceledByHost()) => { // Only allow successful calls or interrupted. // The call can be successful in case the call is finished before kill() is called. @@ -237,7 +217,7 @@ fn interrupt_same_thread_no_barrier() { _ => panic!("Unexpected return"), }; sbox3 - .call_guest_function_by_name::("Echo", "hello".to_string()) + .call::("Echo", "hello".to_string()) .expect("Only sandbox 2 is allowed to be interrupted"); } workload_done.store(true, Ordering::Relaxed); @@ -248,8 +228,8 @@ fn interrupt_same_thread_no_barrier() { // and that anther sandbox on the original thread does not get incorrectly killed #[test] fn interrupt_moved_sandbox() { - let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); + let mut sbox2: MultiUseSandbox = new_uninit_rust().unwrap().evolve().unwrap(); let interrupt_handle = sbox1.interrupt_handle(); let interrupt_handle2 = sbox2.interrupt_handle(); @@ -259,9 +239,7 @@ fn interrupt_moved_sandbox() { let thread = thread::spawn(move || { barrier2.wait(); - let res = sbox1 - .call_guest_function_by_name::("Spin", ()) - .unwrap_err(); + let res = sbox1.call::("Spin", ()).unwrap_err(); assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); }); @@ -274,9 +252,7 @@ fn interrupt_moved_sandbox() { assert!(interrupt_handle2.kill()); }); - let res = sbox2 - .call_guest_function_by_name::("Spin", ()) - .unwrap_err(); + let res = sbox2.call::("Spin", ()).unwrap_err(); assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); thread.join().expect("Thread should finish"); @@ -300,7 +276,7 @@ fn interrupt_custom_signal_no_and_retry_delay() { Some(config), ) .unwrap() - .evolve(Noop::default()) + .evolve() .unwrap(); let interrupt_handle = sbox1.interrupt_handle(); @@ -317,9 +293,7 @@ fn interrupt_custom_signal_no_and_retry_delay() { }); for _ in 0..NUM_ITERS { - let res = sbox1 - .call_guest_function_by_name::("Spin", ()) - .unwrap_err(); + let res = sbox1.call::("Spin", ()).unwrap_err(); assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); // immediately reenter another guest function call after having being cancelled, // so that the vcpu is running again before the interruptor-thread has a chance to see that the vcpu is not running @@ -330,7 +304,7 @@ fn interrupt_custom_signal_no_and_retry_delay() { #[test] fn interrupt_spamming_host_call() { let mut uninit = UninitializedSandbox::new( - GuestBinary::FilePath(callback_guest_as_string().unwrap()), + GuestBinary::FilePath(simple_guest_as_string().unwrap()), None, ) .unwrap(); @@ -340,7 +314,7 @@ fn interrupt_spamming_host_call() { // do nothing }) .unwrap(); - let mut sbox1: MultiUseSandbox = uninit.evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = uninit.evolve().unwrap(); let interrupt_handle = sbox1.interrupt_handle(); @@ -356,7 +330,7 @@ fn interrupt_spamming_host_call() { barrier.wait(); // This guest call calls "HostFunc1" in a loop let res = sbox1 - .call_guest_function_by_name::("HostCallLoop", "HostFunc1".to_string()) + .call::("HostCallLoop", "HostFunc1".to_string()) .unwrap_err(); assert!(matches!(res, HyperlightError::ExecutionCanceledByHost())); @@ -369,9 +343,9 @@ fn print_four_args_c_guest() { let path = c_simple_guest_as_string().unwrap(); let guest_path = GuestBinary::FilePath(path); let uninit = UninitializedSandbox::new(guest_path, None); - let mut sbox1 = uninit.unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = uninit.unwrap().evolve().unwrap(); - let res = sbox1.call_guest_function_by_name::( + let res = sbox1.call::( "PrintFourArgs", ("Test4".to_string(), 3_i32, 4_i64, "Tested".to_string()), ); @@ -382,10 +356,10 @@ fn print_four_args_c_guest() { // Checks that guest can abort with a specific code. #[test] fn guest_abort() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); let error_code: u8 = 13; // this is arbitrary let res = sbox1 - .call_guest_function_by_name::<()>("GuestAbortWithCode", error_code as i32) + .call::<()>("GuestAbortWithCode", error_code as i32) .unwrap_err(); println!("{:?}", res); assert!( @@ -395,10 +369,10 @@ fn guest_abort() { #[test] fn guest_abort_with_context1() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); let res = sbox1 - .call_guest_function_by_name::<()>("GuestAbortWithMessage", (25_i32, "Oh no".to_string())) + .call::<()>("GuestAbortWithMessage", (25_i32, "Oh no".to_string())) .unwrap_err(); println!("{:?}", res); assert!( @@ -408,7 +382,7 @@ fn guest_abort_with_context1() { #[test] fn guest_abort_with_context2() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); // The buffer size for the panic context is 1024 bytes. // This test will see what happens if the panic message is longer than that @@ -443,10 +417,7 @@ fn guest_abort_with_context2() { Proin sagittis nisl rhoncus mattis rhoncus urna. Magna eget est lorem ipsum."; let res = sbox1 - .call_guest_function_by_name::<()>( - "GuestAbortWithMessage", - (60_i32, abort_message.to_string()), - ) + .call::<()>("GuestAbortWithMessage", (60_i32, abort_message.to_string())) .unwrap_err(); println!("{:?}", res); assert!( @@ -462,10 +433,10 @@ fn guest_abort_c_guest() { let path = c_simple_guest_as_string().unwrap(); let guest_path = GuestBinary::FilePath(path); let uninit = UninitializedSandbox::new(guest_path, None); - let mut sbox1 = uninit.unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = uninit.unwrap().evolve().unwrap(); let res = sbox1 - .call_guest_function_by_name::<()>( + .call::<()>( "GuestAbortWithMessage", (75_i32, "This is a test error message".to_string()), ) @@ -479,10 +450,10 @@ fn guest_abort_c_guest() { #[test] fn guest_panic() { // this test is rust-specific - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); let res = sbox1 - .call_guest_function_by_name::<()>("guest_panic", "Error... error...".to_string()) + .call::<()>("guest_panic", "Error... error...".to_string()) .unwrap_err(); println!("{:?}", res); assert!( @@ -493,22 +464,20 @@ fn guest_panic() { #[test] fn guest_malloc() { // this test is rust-only - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); let size_to_allocate = 2000_i32; - sbox1 - .call_guest_function_by_name::("TestMalloc", size_to_allocate) - .unwrap(); + sbox1.call::("TestMalloc", size_to_allocate).unwrap(); } #[test] fn guest_allocate_vec() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); let size_to_allocate = 2000_i32; let res = sbox1 - .call_guest_function_by_name::( + .call::( "CallMalloc", // uses the rust allocator to allocate a vector on heap size_to_allocate, ) @@ -520,13 +489,11 @@ fn guest_allocate_vec() { // checks that malloc failures are captured correctly #[test] fn guest_malloc_abort() { - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); let size = 20000000_i32; // some big number that should fail when allocated - let res = sbox1 - .call_guest_function_by_name::("TestMalloc", size) - .unwrap_err(); + let res = sbox1.call::("TestMalloc", size).unwrap_err(); println!("{:?}", res); assert!( matches!(res, HyperlightError::GuestAborted(code, _) if code == ErrorCode::MallocFailed as u8) @@ -544,9 +511,9 @@ fn guest_malloc_abort() { Some(cfg), ) .unwrap(); - let mut sbox2 = uninit.evolve(Noop::default()).unwrap(); + let mut sbox2 = uninit.evolve().unwrap(); - let res = sbox2.call_guest_function_by_name::( + let res = sbox2.call::( "CallMalloc", // uses the rust allocator to allocate a vector on heap size_to_allocate as i32, ); @@ -558,21 +525,49 @@ fn guest_malloc_abort() { )); } +#[test] +fn guest_panic_no_alloc() { + let heap_size = 0x4000; + + let mut cfg = SandboxConfiguration::default(); + cfg.set_heap_size(heap_size); + let uninit = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_string().unwrap()), + Some(cfg), + ) + .unwrap(); + let mut sbox: MultiUseSandbox = uninit.evolve().unwrap(); + + let res = sbox + .call::( + "ExhaustHeap", // uses the rust allocator to allocate small blocks on the heap until OOM + (), + ) + .unwrap_err(); + + if let HyperlightError::StackOverflow() = res { + panic!("panic on OOM caused stack overflow, this implies allocation in panic handler"); + } + + assert!(matches!( + res, + HyperlightError::GuestAborted(code, msg) if code == ErrorCode::UnknownError as u8 && msg.contains("memory allocation of ") && msg.contains("bytes failed") + )); +} + // Tests libc alloca #[test] fn dynamic_stack_allocate_c_guest() { let path = c_simple_guest_as_string().unwrap(); let guest_path = GuestBinary::FilePath(path); let uninit = UninitializedSandbox::new(guest_path, None); - let mut sbox1: MultiUseSandbox = uninit.unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1: MultiUseSandbox = uninit.unwrap().evolve().unwrap(); - let res: i32 = sbox1 - .call_guest_function_by_name("StackAllocate", 100_i32) - .unwrap(); + let res: i32 = sbox1.call("StackAllocate", 100_i32).unwrap(); assert_eq!(res, 100); let res = sbox1 - .call_guest_function_by_name::("StackAllocate", 0x800_0000_i32) + .call::("StackAllocate", 0x800_0000_i32) .unwrap_err(); assert!(matches!(res, HyperlightError::StackOverflow())); } @@ -580,32 +575,28 @@ fn dynamic_stack_allocate_c_guest() { // checks that a small buffer on stack works #[test] fn static_stack_allocate() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - let res: i32 = sbox1.call_guest_function_by_name("SmallVar", ()).unwrap(); + let res: i32 = sbox1.call("SmallVar", ()).unwrap(); assert_eq!(res, 1024); } // checks that a huge buffer on stack fails with stackoverflow #[test] fn static_stack_allocate_overflow() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); - let res = sbox1 - .call_guest_function_by_name::("LargeVar", ()) - .unwrap_err(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); + let res = sbox1.call::("LargeVar", ()).unwrap_err(); assert!(matches!(res, HyperlightError::StackOverflow())); } // checks that a recursive function with stack allocation works, (that chkstk can be called without overflowing) #[test] fn recursive_stack_allocate() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); let iterations = 1_i32; - sbox1 - .call_guest_function_by_name::("StackOverflow", iterations) - .unwrap(); + sbox1.call::("StackOverflow", iterations).unwrap(); } // checks stack guard page (between guest stack and heap) @@ -629,8 +620,8 @@ fn guard_page_check() { for offset in offsets_from_page_guard_start { // we have to create a sandbox each iteration because can't reuse after MMIO error in release mode - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let result = sbox1.call_guest_function_by_name::("test_write_raw_ptr", offset); + let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); + let result = sbox1.call::("test_write_raw_ptr", offset); if guard_range.contains(&offset) { // should have failed assert!(matches!( @@ -646,21 +637,17 @@ fn guard_page_check() { #[test] fn guard_page_check_2() { // this test is rust-guest only - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); - let result = sbox1 - .call_guest_function_by_name::<()>("InfiniteRecursion", ()) - .unwrap_err(); + let result = sbox1.call::<()>("InfiniteRecursion", ()).unwrap_err(); assert!(matches!(result, HyperlightError::StackOverflow())); } #[test] fn execute_on_stack() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); - let result = sbox1 - .call_guest_function_by_name::("ExecuteOnStack", ()) - .unwrap_err(); + let result = sbox1.call::("ExecuteOnStack", ()).unwrap_err(); let err = result.to_string(); assert!( @@ -672,8 +659,8 @@ fn execute_on_stack() { #[test] #[ignore] // ran from Justfile because requires feature "executable_heap" fn execute_on_heap() { - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - let result = sbox1.call_guest_function_by_name::("ExecuteOnHeap", ()); + let mut sbox1 = new_uninit_rust().unwrap().evolve().unwrap(); + let result = sbox1.call::("ExecuteOnHeap", ()); println!("{:#?}", result); #[cfg(feature = "executable_heap")] @@ -688,28 +675,14 @@ fn execute_on_heap() { } } -#[test] -fn memory_resets_after_failed_guestcall() { - let mut sbox1 = new_uninit_rust().unwrap().evolve(Noop::default()).unwrap(); - sbox1 - .call_guest_function_by_name::("AddToStaticAndFail", ()) - .unwrap_err(); - let res = sbox1 - .call_guest_function_by_name::("GetStatic", ()) - .unwrap(); - assert_eq!(res, 0, "Expected 0, got {:?}", res); -} - // checks that a recursive function with stack allocation eventually fails with stackoverflow #[test] fn recursive_stack_allocate_overflow() { - let mut sbox1 = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sbox1 = new_uninit().unwrap().evolve().unwrap(); let iterations = 10_i32; - let res = sbox1 - .call_guest_function_by_name::<()>("StackOverflow", iterations) - .unwrap_err(); + let res = sbox1.call::<()>("StackOverflow", iterations).unwrap_err(); println!("{:?}", res); assert!(matches!(res, HyperlightError::StackOverflow())); } @@ -778,11 +751,103 @@ fn log_test_messages(levelfilter: Option) { sbox.set_max_guest_log_level(levelfilter); } - let mut sbox1 = sbox.evolve(Noop::default()).unwrap(); + let mut sbox1 = sbox.evolve().unwrap(); let message = format!("Hello from log_message level {}", level as i32); sbox1 - .call_guest_function_by_name::<()>("LogMessage", (message.to_string(), level as i32)) + .call::<()>("LogMessage", (message.to_string(), level as i32)) .unwrap(); } } + +/// Tests whether host is able to return Bool as return type +/// or not +#[test] +fn test_if_guest_is_able_to_get_bool_return_values_from_host() { + let mut sbox1 = new_uninit_c().unwrap(); + + sbox1 + .register("HostBool", |a: i32, b: i32| a + b > 10) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + + for i in 1..10 { + if i < 6 { + let res = sbox3 + .call::("GuestRetrievesBoolValue", (i, i)) + .unwrap(); + println!("{:?}", res); + assert!(!res); + } else { + let res = sbox3 + .call::("GuestRetrievesBoolValue", (i, i)) + .unwrap(); + println!("{:?}", res); + assert!(res); + } + } +} + +/// Tests whether host is able to return Float/f32 as return type +/// or not +/// Adding Ignore attribute, due known issues with float and double +/// calculations - see Github issue #179. Once it is fixed we can +/// remove ignore attribute +#[ignore] +#[test] +fn test_if_guest_is_able_to_get_float_return_values_from_host() { + let mut sbox1 = new_uninit_c().unwrap(); + + sbox1 + .register("HostAddFloat", |a: f32, b: f32| a + b) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + let res = sbox3 + .call::("GuestRetrievesFloatValue", (1.34_f32, 1.34_f32)) + .unwrap(); + println!("{:?}", res); + assert_eq!(res, 2.68_f32); +} + +/// Tests whether host is able to return Double/f64 as return type +/// or not +/// Adding Ignore attribute, due known issues with float and double +/// calculations - see Github issue #179. Once it is fixed we can +/// remove ignore attribute +#[ignore] +#[test] +fn test_if_guest_is_able_to_get_double_return_values_from_host() { + let mut sbox1 = new_uninit_c().unwrap(); + + sbox1 + .register("HostAddDouble", |a: f64, b: f64| a + b) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + let res = sbox3 + .call::("GuestRetrievesDoubleValue", (1.34_f64, 1.34_f64)) + .unwrap(); + println!("{:?}", res); + assert_eq!(res, 2.68_f64); +} + +/// Tests whether host is able to return String as return type +/// or not +#[test] +fn test_if_guest_is_able_to_get_string_return_values_from_host() { + let mut sbox1 = new_uninit_c().unwrap(); + + sbox1 + .register("HostAddStrings", |a: String| { + a + ", string added by Host Function" + }) + .unwrap(); + let mut sbox3 = sbox1.evolve().unwrap(); + let res = sbox3 + .call::("GuestRetrievesStringValue", ()) + .unwrap(); + println!("{:?}", res); + assert_eq!( + res, + "Guest Function, string added by Host Function".to_string() + ); +} diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index 1c8bf92b6..cfcc0e6c6 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -18,40 +18,34 @@ use core::f64; use std::sync::mpsc::channel; use std::sync::{Arc, Mutex}; -use common::new_uninit; -use hyperlight_host::sandbox::{Callable, SandboxConfiguration}; -use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox; -use hyperlight_host::sandbox_state::transition::Noop; +use common::{get_uninit_simpleguest_sandboxes, new_uninit}; +use hyperlight_host::sandbox::SandboxConfiguration; use hyperlight_host::{ GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox, new_error, }; use hyperlight_testing::simple_guest_as_string; -#[cfg(target_os = "windows")] -use serial_test::serial; // using LoadLibrary requires serial tests pub mod common; // pub to disable dead_code warning -use crate::common::{get_callbackguest_uninit_sandboxes, get_simpleguest_sandboxes}; +use crate::common::get_simpleguest_sandboxes; #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn pass_byte_array() { - for sandbox in get_simpleguest_sandboxes(None).into_iter() { - let mut ctx = sandbox.new_call_context(); + for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { const LEN: usize = 10; let bytes = vec![1u8; LEN]; - let res: Vec = ctx + let res: Vec = sandbox .call("SetByteArrayToZero", bytes.clone()) .expect("Expected VecBytes"); assert_eq!(res, [0; LEN]); - ctx.call::("SetByteArrayToZeroNoLength", bytes.clone()) + sandbox + .call::("SetByteArrayToZeroNoLength", bytes.clone()) .unwrap_err(); // missing length param } } #[test] #[ignore = "Fails with mismatched float only when c .exe guest?!"] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn float_roundtrip() { let doubles = [ 0.0, @@ -85,11 +79,9 @@ fn float_roundtrip() { f32::NAN, -f32::NAN, ]; - let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve(Noop::default()).unwrap(); + let mut sandbox: MultiUseSandbox = new_uninit().unwrap().evolve().unwrap(); for f in doubles.iter() { - let res: f64 = sandbox - .call_guest_function_by_name("EchoDouble", *f) - .unwrap(); + let res: f64 = sandbox.call("EchoDouble", *f).unwrap(); assert!( res.total_cmp(f).is_eq(), @@ -99,9 +91,7 @@ fn float_roundtrip() { ); } for f in floats.iter() { - let res: f32 = sandbox - .call_guest_function_by_name("EchoFloat", *f) - .unwrap(); + let res: f32 = sandbox.call("EchoFloat", *f).unwrap(); assert!( res.total_cmp(f).is_eq(), @@ -113,11 +103,10 @@ fn float_roundtrip() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn invalid_guest_function_name() { for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { let fn_name = "FunctionDoesntExist"; - let res = sandbox.call_guest_function_by_name::(fn_name, ()); + let res = sandbox.call::(fn_name, ()); println!("{:?}", res); assert!( matches!(res.unwrap_err(), HyperlightError::GuestError(hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode::GuestFunctionNotFound, error_name) if error_name == fn_name) @@ -126,11 +115,10 @@ fn invalid_guest_function_name() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn set_static() { for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { let fn_name = "SetStatic"; - let res = sandbox.call_guest_function_by_name::(fn_name, ()); + let res = sandbox.call::(fn_name, ()); println!("{:?}", res); assert!(res.is_ok()); // the result is the size of the static array in the guest @@ -139,7 +127,6 @@ fn set_static() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn multiple_parameters() { let (tx, rx) = channel(); let writer = move |msg: String| { @@ -164,7 +151,7 @@ fn multiple_parameters() { macro_rules! test_case { ($sandbox:ident, $rx:ident, $name:literal, ($($p:ident),+)) => {{ let ($($p),+, ..) = args.clone(); - let res: i32 = $sandbox.call_guest_function_by_name($name, ($($p.0,)+)).unwrap(); + let res: i32 = $sandbox.call($name, ($($p.0,)+)).unwrap(); println!("{res:?}"); let output = $rx.try_recv().unwrap(); println!("{output:?}"); @@ -187,10 +174,9 @@ fn multiple_parameters() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn incorrect_parameter_type() { for mut sandbox in get_simpleguest_sandboxes(None) { - let res = sandbox.call_guest_function_by_name::( + let res = sandbox.call::( "Echo", 2_i32, // should be string ); @@ -205,10 +191,9 @@ fn incorrect_parameter_type() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn incorrect_parameter_num() { for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { - let res = sandbox.call_guest_function_by_name::("Echo", ("1".to_string(), 2_i32)); + let res = sandbox.call::("Echo", ("1".to_string(), 2_i32)); assert!(matches!( res.unwrap_err(), HyperlightError::GuestError( @@ -235,11 +220,10 @@ fn max_memory_sandbox() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn iostack_is_working() { for mut sandbox in get_simpleguest_sandboxes(None).into_iter() { let res: i32 = sandbox - .call_guest_function_by_name::("ThisIsNotARealFunctionButTheNameIsImportant", ()) + .call::("ThisIsNotARealFunctionButTheNameIsImportant", ()) .unwrap(); assert_eq!(res, 99); } @@ -262,19 +246,15 @@ fn simple_test_helper() -> Result<()> { let message2 = "world"; for mut sandbox in get_simpleguest_sandboxes(Some(writer.into())).into_iter() { - let res: i32 = sandbox - .call_guest_function_by_name("PrintOutput", message.to_string()) - .unwrap(); + let res: i32 = sandbox.call("PrintOutput", message.to_string()).unwrap(); assert_eq!(res, 5); - let res: String = sandbox - .call_guest_function_by_name("Echo", message2.to_string()) - .unwrap(); + let res: String = sandbox.call("Echo", message2.to_string()).unwrap(); assert_eq!(res, "world"); let buffer = [1u8, 2, 3, 4, 5, 6]; let res: Vec = sandbox - .call_guest_function_by_name("GetSizePrefixedBuffer", buffer.to_vec()) + .call("GetSizePrefixedBuffer", buffer.to_vec()) .unwrap(); assert_eq!(res, buffer); } @@ -300,13 +280,11 @@ fn simple_test_helper() -> Result<()> { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn simple_test() { simple_test_helper().unwrap(); } #[test] -#[cfg(target_os = "linux")] fn simple_test_parallel() { let handles: Vec<_> = (0..50) .map(|_| { @@ -322,7 +300,7 @@ fn simple_test_parallel() { } fn callback_test_helper() -> Result<()> { - for mut sandbox in get_callbackguest_uninit_sandboxes(None).into_iter() { + for mut sandbox in get_uninit_simpleguest_sandboxes(None).into_iter() { // create host function let (tx, rx) = channel(); sandbox.register("HostMethod1", move |msg: String| { @@ -332,9 +310,9 @@ fn callback_test_helper() -> Result<()> { })?; // call guest function that calls host function - let mut init_sandbox: MultiUseSandbox = sandbox.evolve(Noop::default())?; + let mut init_sandbox: MultiUseSandbox = sandbox.evolve()?; let msg = "Hello world"; - init_sandbox.call_guest_function_by_name::("GuestMethod1", msg.to_string())?; + init_sandbox.call::("GuestMethod1", msg.to_string())?; let messages = rx.try_iter().collect::>(); assert_eq!(messages, [format!("Hello from GuestFunction1, {msg}")]); @@ -343,13 +321,11 @@ fn callback_test_helper() -> Result<()> { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn callback_test() { callback_test_helper().unwrap(); } #[test] -#[cfg(target_os = "linux")] // windows can't run parallel with LoadLibrary fn callback_test_parallel() { let handles: Vec<_> = (0..100) .map(|_| { @@ -365,21 +341,27 @@ fn callback_test_parallel() { } #[test] -#[cfg_attr(target_os = "windows", serial)] // using LoadLibrary requires serial tests fn host_function_error() -> Result<()> { - for mut sandbox in get_callbackguest_uninit_sandboxes(None).into_iter() { + for mut sandbox in get_uninit_simpleguest_sandboxes(None).into_iter() { // create host function sandbox.register("HostMethod1", |_: String| -> Result { Err(new_error!("Host function error!")) })?; // call guest function that calls host function - let mut init_sandbox: MultiUseSandbox = sandbox.evolve(Noop::default())?; + let mut init_sandbox: MultiUseSandbox = sandbox.evolve()?; let msg = "Hello world"; - let res = init_sandbox - .call_guest_function_by_name::("GuestMethod1", msg.to_string()) - .unwrap_err(); - assert!(matches!(res, HyperlightError::Error(msg) if msg == "Host function error!")); + + for _ in 0..1000 { + let res = init_sandbox + .call::("GuestMethod1", msg.to_string()) + .unwrap_err(); + assert!( + matches!(&res, HyperlightError::GuestError(_, msg) if msg == "Host function error!") // rust guest + || matches!(&res, HyperlightError::GuestAborted(_, msg) if msg.contains("Host function error!")) // c guest + || matches!(&res, HyperlightError::StackOverflow()) // c guest. TODO fix this. C guest leaks when host func returns error guest panics. + ); + } } Ok(()) } diff --git a/src/hyperlight_host/tests/wit_test.rs b/src/hyperlight_host/tests/wit_test.rs index e42441dd8..e47faf6da 100644 --- a/src/hyperlight_host/tests/wit_test.rs +++ b/src/hyperlight_host/tests/wit_test.rs @@ -18,7 +18,7 @@ limitations under the License. use std::sync::{Arc, Mutex}; use hyperlight_common::resource::BorrowedResourceGuard; -use hyperlight_host::{GuestBinary, MultiUseGuestCallContext, UninitializedSandbox}; +use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox}; use hyperlight_testing::wit_guest_as_string; extern crate alloc; @@ -155,6 +155,48 @@ impl test::wit::Roundtrip for Host { ) -> test::wit::roundtrip::Testenum { x } + fn roundtrip_fix_list(&mut self, x: [u8; 4]) -> [u8; 4] { + x + } + fn roundtrip_fix_list_u32(&mut self, x: [u32; 4]) -> [u32; 4] { + x + } + fn roundtrip_fix_list_u64(&mut self, x: [u64; 4]) -> [u64; 4] { + x + } + fn roundtrip_fix_list_i8(&mut self, x: [i8; 4]) -> [i8; 4] { + x + } + fn roundtrip_fix_list_i16(&mut self, x: [i16; 4]) -> [i16; 4] { + x + } + fn roundtrip_fix_list_i32(&mut self, x: [i32; 4]) -> [i32; 4] { + x + } + fn roundtrip_fix_list_i64(&mut self, x: [i64; 4]) -> [i64; 4] { + x + } + fn roundtrip_fix_list_f32(&mut self, x: [f32; 4]) -> [f32; 4] { + x + } + fn roundtrip_fix_list_f64(&mut self, x: [f64; 4]) -> [f64; 4] { + x + } + fn roundtrip_fix_list_u8_size8(&mut self, x: [u8; 8]) -> [u8; 8] { + x + } + fn roundtrip_fix_list_u64_size2(&mut self, x: [u64; 2]) -> [u64; 2] { + x + } + fn roundtrip_fix_list_string(&mut self, x: [String; 4]) -> [String; 4] { + x + } + fn roundtrip_fix_array_of_lists(&mut self, x: [Vec; 3]) -> [Vec; 3] { + x + } + fn roundtrip_fix_array_of_string_lists(&mut self, x: [Vec; 2]) -> [Vec; 2] { + x + } fn roundtrip_no_result(&mut self, _x: u32) {} } @@ -164,12 +206,21 @@ struct TestResource { x: String, last: char, } +impl TestResource { + fn new(x: String, last: char) -> Arc> { + Arc::new(Mutex::new(TestResource { + n_calls: 0, + x, + last, + })) + } +} use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering::Relaxed; -// We only have 1 test that uses this, and it isn't a proptest or -// anything, so it should only run once. If multiple tests using this -// could run in parallel, there would be problems. +// We use some care below in the tests that use HAS_BEEN_DROPPED to +// synchronise on this mutex to avoid them stepping on each other +static SERIALIZE_TEST_RESOURCE_TESTS: Mutex<()> = Mutex::new(()); static HAS_BEEN_DROPPED: AtomicBool = AtomicBool::new(false); impl Drop for TestResource { @@ -183,11 +234,7 @@ impl Drop for TestResource { impl test::wit::host_resource::Testresource for Host { type T = Arc>; fn new(&mut self, x: String, last: char) -> Self::T { - Arc::new(Mutex::new(TestResource { - n_calls: 0, - x, - last, - })) + TestResource::new(x, last) } fn append_char(&mut self, self_: BorrowedResourceGuard<'_, Self::T>, c: char) { let mut self_ = self_.lock().unwrap(); @@ -238,7 +285,7 @@ impl test::wit::TestImports for Host { } } -fn sb() -> TestSandbox { +fn sb() -> TestSandbox { let path = wit_guest_as_string().unwrap(); let guest_path = GuestBinary::FilePath(path); let uninit = UninitializedSandbox::new(guest_path, None).unwrap(); @@ -331,18 +378,51 @@ mod wit_test { make_test! { roundtrip_flags_large, in arb_largeflags() } make_test! { roundtrip_variant, in arb_testvariant() } make_test! { roundtrip_enum, in arb_testenum() } + make_test! { roundtrip_fix_list, : [u8; 4] } + make_test! { roundtrip_fix_list_u32, : [u32; 4] } + make_test! { roundtrip_fix_list_u64, : [u64; 4] } + make_test! { roundtrip_fix_list_i8, : [i8; 4] } + make_test! { roundtrip_fix_list_i16, : [i16; 4] } + make_test! { roundtrip_fix_list_i32, : [i32; 4] } + make_test! { roundtrip_fix_list_i64, : [i64; 4] } + make_test! { roundtrip_fix_list_f32, : [f32; 4] } + make_test! { roundtrip_fix_list_f64, : [f64; 4] } + make_test! { roundtrip_fix_list_u8_size8, : [u8; 8] } + make_test! { roundtrip_fix_list_u64_size2, : [u64; 2] } + make_test! { roundtrip_fix_list_string, : [String; 4] } + make_test! { roundtrip_fix_array_of_lists, : [Vec; 3] } + make_test! { roundtrip_fix_array_of_string_lists, : [Vec; 2] } #[test] - fn test_simple_func() { + fn test_roundtrip_no_result() { sb().roundtrip().roundtrip_no_result(42); } + use std::sync::atomic::Ordering::Relaxed; + + #[test] + fn test_host_resource_uses_locally() { + let guard = crate::SERIALIZE_TEST_RESOURCE_TESTS.lock(); + crate::HAS_BEEN_DROPPED.store(false, Relaxed); + { + sb().test_host_resource().test_uses_locally(); + } + assert!(crate::HAS_BEEN_DROPPED.load(Relaxed)); + drop(guard); + } #[test] - fn test_host_resource() { + fn test_host_resource_passed_in_out() { + let guard = crate::SERIALIZE_TEST_RESOURCE_TESTS.lock(); + crate::HAS_BEEN_DROPPED.store(false, Relaxed); { - sb().test_host_resource().test(); + let mut sb = sb(); + let inst = sb.test_host_resource(); + let r = inst.test_makes(); + inst.test_accepts_borrow(&r); + inst.test_accepts_own(r); + inst.test_returns(); } - use std::sync::atomic::Ordering::Relaxed; assert!(crate::HAS_BEEN_DROPPED.load(Relaxed)); + drop(guard); } } diff --git a/src/hyperlight_testing/Cargo.toml b/src/hyperlight_testing/Cargo.toml index fca693d16..b88f97895 100644 --- a/src/hyperlight_testing/Cargo.toml +++ b/src/hyperlight_testing/Cargo.toml @@ -3,7 +3,7 @@ name = "hyperlight-testing" edition = "2021" [dependencies] -anyhow = "1.0.98" +anyhow = "1.0.100" log = "0.4" once_cell = "1.21" tracing = { version = "0.1.41", features = ["log"] } diff --git a/src/hyperlight_testing/src/lib.rs b/src/hyperlight_testing/src/lib.rs index eb48e203a..c47b6c797 100644 --- a/src/hyperlight_testing/src/lib.rs +++ b/src/hyperlight_testing/src/lib.rs @@ -72,20 +72,12 @@ pub fn simple_guest_as_string() -> Result { .ok_or_else(|| anyhow!("couldn't convert simple guest PathBuf to string")) } -/// Get a fully qualified OS-specific path to the callbackguest elf binary -pub fn callback_guest_as_string() -> Result { - let buf = rust_guest_as_pathbuf("callbackguest"); - buf.to_str() - .map(|s| s.to_string()) - .ok_or_else(|| anyhow!("couldn't convert callback guest PathBuf to string")) -} - /// Get a fully-qualified OS-specific path to the witguest elf binary pub fn wit_guest_as_string() -> Result { let buf = rust_guest_as_pathbuf("witguest"); buf.to_str() .map(|s| s.to_string()) - .ok_or_else(|| anyhow!("couldn't convert callback guest PathBuf to string")) + .ok_or_else(|| anyhow!("couldn't convert wit guest PathBuf to string")) } /// Get a fully qualified OS-specific path to the dummyguest elf binary @@ -116,13 +108,6 @@ pub fn c_simple_guest_as_string() -> Result { .ok_or_else(|| anyhow!("couldn't convert simple guest PathBuf to string")) } -pub fn c_callback_guest_as_string() -> Result { - let buf = c_guest_as_pathbuf("callbackguest"); - buf.to_str() - .map(|s| s.to_string()) - .ok_or_else(|| anyhow!("couldn't convert callback guest PathBuf to string")) -} - /// Get a fully qualified path to a simple guest binary preferring a binary /// in the same directory as the parent executable. This will be used in /// fuzzing scenarios where pre-built binaries will be built and submitted to diff --git a/src/schema/all.fbs b/src/schema/all.fbs new file mode 100644 index 000000000..9e70a50b8 --- /dev/null +++ b/src/schema/all.fbs @@ -0,0 +1,7 @@ +include "function_types.fbs"; +include "function_call_result.fbs"; +include "function_call.fbs"; +include "guest_error.fbs"; +include "guest_log_data.fbs"; +include "host_function_definition.fbs"; +include "host_function_details.fbs"; \ No newline at end of file diff --git a/src/schema/function_call_result.fbs b/src/schema/function_call_result.fbs index 28a5b6fe3..53ea52b25 100644 --- a/src/schema/function_call_result.fbs +++ b/src/schema/function_call_result.fbs @@ -1,9 +1,21 @@ include "function_types.fbs"; +include "guest_error.fbs"; namespace Hyperlight.Generated; +// Wrapper so ReturnValue (a union) can be a single union variant +table ReturnValueBox { + value: ReturnValue (required); +} + +// Result-like union +union FunctionCallResultType { + ReturnValueBox, + GuestError +} + table FunctionCallResult { - return_value:ReturnValue(required); + result: FunctionCallResultType (required); } root_type FunctionCallResult; \ No newline at end of file diff --git a/src/schema/guest_error.fbs b/src/schema/guest_error.fbs index fca2cc0b0..14dfa85e4 100644 --- a/src/schema/guest_error.fbs +++ b/src/schema/guest_error.fbs @@ -16,7 +16,8 @@ enum ErrorCode: ulong { MallocFailed = 13, // this error is set when malloc returns 0 bytes. GuestFunctionParameterTypeMismatch = 14, // The function call parameter type was not the expected type. GuestError = 15, // An error occurred in the guest Guest implementation should use this along with a message when calling setError. - ArrayLengthParamIsMissing = 16 // Expected a int parameter to follow a byte array + ArrayLengthParamIsMissing = 16, // Expected a int parameter to follow a byte array + HostError = 17 // Guest called Host Function, which errored. } table GuestError { diff --git a/src/tests/c_guests/c_callbackguest/main.c b/src/tests/c_guests/c_callbackguest/main.c deleted file mode 100644 index e4fa2a678..000000000 --- a/src/tests/c_guests/c_callbackguest/main.c +++ /dev/null @@ -1,51 +0,0 @@ -// Included from hyperlight_guest_capi/include -#include "hyperlight_guest.h" -// Included from hyperlight_guest_bin/third_party/libc -#include "stdint.h" -#include "stdio.h" -#include "string.h" -// Included from hyperlight_guest_bin/third_party/printf -#include "printf.h" - -int print_output(const char *message) { - int res = printf("%s", message); - - return res; -} - -int guest_function(const char *from_host) { - char guest_message[256] = "Hello from GuestFunction1, "; - int len = strlen(from_host); - strncat(guest_message, from_host, len); - - hl_Parameter params = {.tag = hl_ParameterType_String, - .value = {.String = guest_message}}; - const hl_FunctionCall host_call = {.function_name = "HostMethod1", - .parameters = ¶ms, - .parameters_len = 1, - .return_type = hl_ReturnType_Int}; - hl_call_host_function(&host_call); - hl_get_host_return_value_as_Int(); - - return 0; -} - -HYPERLIGHT_WRAP_FUNCTION(print_output, Int, 1, String); -HYPERLIGHT_WRAP_FUNCTION(guest_function, Int, 1, String); - -void hyperlight_main(void) { - HYPERLIGHT_REGISTER_FUNCTION("PrintOutput", print_output); - HYPERLIGHT_REGISTER_FUNCTION("GuestMethod1", guest_function); -} - -// This dispatch function is only used when the host dispatches a guest function -// call but there is no registered guest function with the given name. -hl_Vec *c_guest_dispatch_function(const hl_FunctionCall *function_call) { - const char *func_name = function_call->function_name; - if (strcmp(func_name, "ThisIsNotARealFunctionButTheNameIsImportant") == 0) { - // This is special case for test `iostack_is_working - return hl_flatbuffer_result_from_Int(99); - } - - return NULL; -} diff --git a/src/tests/c_guests/c_simpleguest/main.c b/src/tests/c_guests/c_simpleguest/main.c index 664b8441d..30caff590 100644 --- a/src/tests/c_guests/c_simpleguest/main.c +++ b/src/tests/c_guests/c_simpleguest/main.c @@ -4,6 +4,7 @@ #include "stdint.h" #include "string.h" #include "stdlib.h" +#include "assert.h" // Included from hyperlight_guest_bin/third_party/printf #include "printf.h" @@ -232,8 +233,106 @@ int log_message(const char *message, int64_t level) { return -1; } +hl_Vec *twenty_four_k_in_eight_k_out(const hl_FunctionCall* params) { + hl_Vec input = params->parameters[0].value.VecBytes; + assert(input.len == 24 * 1024); + return hl_flatbuffer_result_from_Bytes(input.data, 8 * 1024); +} + +int guest_function(const char *from_host) { + char guest_message[256] = "Hello from GuestFunction1, "; + int len = strlen(from_host); + strncat(guest_message, from_host, len); + + hl_Parameter params = {.tag = hl_ParameterType_String, + .value = {.String = guest_message}}; + const hl_FunctionCall host_call = {.function_name = "HostMethod1", + .parameters = ¶ms, + .parameters_len = 1, + .return_type = hl_ReturnType_Int}; + hl_call_host_function(&host_call); + hl_get_host_return_value_as_Int(); + + return 0; +} + +bool guest_fn_checks_if_host_returns_bool_value(int32_t a, int32_t b) { + hl_Parameter params[2]; + + params[0].tag = hl_ParameterType_Int; + params[0].value.Int = a; + + params[1].tag = hl_ParameterType_Int; + params[1].value.Int = b; + + const hl_FunctionCall host_call = {.function_name = "HostBool", + .parameters = params, + .parameters_len = 2, + .return_type = hl_ReturnType_Bool + }; + hl_call_host_function(&host_call); + return hl_get_host_return_value_as_Bool(); +} + +float guest_fn_checks_if_host_returns_float_value(float a, float b) { + hl_Parameter params[2]; + + params[0].tag = hl_ParameterType_Float; + params[0].value.Float = a; + + params[1].tag = hl_ParameterType_Float; + params[1].value.Float = b; + + const hl_FunctionCall host_call = {.function_name = "HostAddFloat", + .parameters = params, + .parameters_len = 2, + .return_type = hl_ReturnType_Float + }; + hl_call_host_function(&host_call); + return hl_get_host_return_value_as_Float(); +} + +double guest_fn_checks_if_host_returns_double_value(double a, double b) { + hl_Parameter params[2]; + + params[0].tag = hl_ParameterType_Double; + params[0].value.Double = a; + + params[1].tag = hl_ParameterType_Double; + params[1].value.Double = b; + + const hl_FunctionCall host_call = {.function_name = "HostAddDouble", + .parameters = params, + .parameters_len = 2, + .return_type = hl_ReturnType_Double + }; + hl_call_host_function(&host_call); + return hl_get_host_return_value_as_Double(); +} + +const char* guest_fn_checks_if_host_returns_string_value() { + char guest_message[256] = "Guest Function"; + hl_Parameter params; + + params.tag = hl_ParameterType_String; + params.value.String = guest_message; + + const hl_FunctionCall host_call = {.function_name = "HostAddStrings", + .parameters = ¶ms, + .parameters_len = 1, + .return_type = hl_ReturnType_String + }; + hl_call_host_function(&host_call); + return hl_get_host_return_value_as_String(); +} + +HYPERLIGHT_WRAP_FUNCTION(guest_fn_checks_if_host_returns_float_value, Float, 2, Float, Float) +HYPERLIGHT_WRAP_FUNCTION(guest_fn_checks_if_host_returns_double_value, Double, 2, Double, Double) +HYPERLIGHT_WRAP_FUNCTION(guest_fn_checks_if_host_returns_string_value, String, 0) +HYPERLIGHT_WRAP_FUNCTION(guest_fn_checks_if_host_returns_bool_value, Bool, 2, Int, Int) HYPERLIGHT_WRAP_FUNCTION(echo, String, 1, String) // HYPERLIGHT_WRAP_FUNCTION(set_byte_array_to_zero, 1, VecBytes) is not valid for functions that return VecBytes +HYPERLIGHT_WRAP_FUNCTION(guest_function, Int, 1, String) HYPERLIGHT_WRAP_FUNCTION(print_output, Int, 1, String) HYPERLIGHT_WRAP_FUNCTION(stack_allocate, Int, 1, Int) HYPERLIGHT_WRAP_FUNCTION(stack_overflow, Int, 1, Int) @@ -260,13 +359,19 @@ HYPERLIGHT_WRAP_FUNCTION(guest_abort_with_msg, Int, 2, Int, String) HYPERLIGHT_WRAP_FUNCTION(guest_abort_with_code, Int, 1, Int) HYPERLIGHT_WRAP_FUNCTION(execute_on_stack, Int, 0) HYPERLIGHT_WRAP_FUNCTION(log_message, Int, 2, String, Long) +// HYPERLIGHT_WRAP_FUNCTION(twenty_four_k_in_eight_k_out, VecBytes, 1, VecBytes) is not valid for functions that return VecBytes void hyperlight_main(void) { + HYPERLIGHT_REGISTER_FUNCTION("GuestRetrievesFloatValue", guest_fn_checks_if_host_returns_float_value); + HYPERLIGHT_REGISTER_FUNCTION("GuestRetrievesDoubleValue", guest_fn_checks_if_host_returns_double_value); + HYPERLIGHT_REGISTER_FUNCTION("GuestRetrievesStringValue", guest_fn_checks_if_host_returns_string_value); + HYPERLIGHT_REGISTER_FUNCTION("GuestRetrievesBoolValue", guest_fn_checks_if_host_returns_bool_value); HYPERLIGHT_REGISTER_FUNCTION("Echo", echo); // HYPERLIGHT_REGISTER_FUNCTION macro does not work for functions that return VecBytes, // so we use hl_register_function_definition directly hl_register_function_definition("SetByteArrayToZero", set_byte_array_to_zero, 1, (hl_ParameterType[]){hl_ParameterType_VecBytes}, hl_ReturnType_VecBytes); + HYPERLIGHT_REGISTER_FUNCTION("GuestMethod1", guest_function); HYPERLIGHT_REGISTER_FUNCTION("PrintOutput", print_output); HYPERLIGHT_REGISTER_FUNCTION("StackAllocate", stack_allocate); HYPERLIGHT_REGISTER_FUNCTION("StackOverflow", stack_overflow); @@ -295,6 +400,9 @@ void hyperlight_main(void) HYPERLIGHT_REGISTER_FUNCTION("GuestAbortWithMessage", guest_abort_with_msg); HYPERLIGHT_REGISTER_FUNCTION("ExecuteOnStack", execute_on_stack); HYPERLIGHT_REGISTER_FUNCTION("LogMessage", log_message); + // HYPERLIGHT_REGISTER_FUNCTION macro does not work for functions that return VecBytes, + // so we use hl_register_function_definition directly + hl_register_function_definition("24K_in_8K_out", twenty_four_k_in_eight_k_out, 1, (hl_ParameterType[]){hl_ParameterType_VecBytes}, hl_ReturnType_VecBytes); } // This dispatch function is only used when the host dispatches a guest function @@ -308,4 +416,4 @@ hl_Vec *c_guest_dispatch_function(const hl_FunctionCall *function_call) { } return NULL; -} +} \ No newline at end of file diff --git a/src/tests/rust_guests/callbackguest/.cargo/config.toml b/src/tests/rust_guests/callbackguest/.cargo/config.toml deleted file mode 100644 index f977e97ab..000000000 --- a/src/tests/rust_guests/callbackguest/.cargo/config.toml +++ /dev/null @@ -1,19 +0,0 @@ -[build] -target = "x86_64-unknown-none" - -[target.x86_64-unknown-none] -rustflags = [ - "-C", - "code-model=small", - "-C", - "link-args=-e entrypoint", -] -linker = "rust-lld" - -[profile.release] -opt-level = 0 -panic = "abort" - -[profile.dev] -opt-level = 0 -panic = "abort" \ No newline at end of file diff --git a/src/tests/rust_guests/callbackguest/Cargo.lock b/src/tests/rust_guests/callbackguest/Cargo.lock index 82979afeb..21ba7c47d 100644 --- a/src/tests/rust_guests/callbackguest/Cargo.lock +++ b/src/tests/rust_guests/callbackguest/Cargo.lock @@ -4,21 +4,21 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "buddy_system_allocator" @@ -36,22 +36,23 @@ dependencies = [ "hyperlight-common", "hyperlight-guest", "hyperlight-guest-bin", + "hyperlight-guest-tracing", ] [[package]] name = "cc" -version = "1.2.25" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "flatbuffers" @@ -65,13 +66,13 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hyperlight-common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "flatbuffers", @@ -81,16 +82,17 @@ dependencies = [ [[package]] name = "hyperlight-guest" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "hyperlight-common", + "hyperlight-guest-tracing", "serde_json", ] [[package]] name = "hyperlight-guest-bin" -version = "0.7.0" +version = "0.8.0" dependencies = [ "buddy_system_allocator", "cc", @@ -98,10 +100,29 @@ dependencies = [ "glob", "hyperlight-common", "hyperlight-guest", + "hyperlight-guest-tracing", "log", "spin 0.10.0", ] +[[package]] +name = "hyperlight-guest-tracing" +version = "0.8.0" +dependencies = [ + "hyperlight-common", + "hyperlight-guest-tracing-macro", + "spin 0.10.0", +] + +[[package]] +name = "hyperlight-guest-tracing-macro" +version = "0.8.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "itoa" version = "1.0.15" @@ -110,9 +131,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -126,15 +147,15 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -197,9 +218,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -233,9 +254,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", diff --git a/src/tests/rust_guests/callbackguest/Cargo.toml b/src/tests/rust_guests/callbackguest/Cargo.toml deleted file mode 100644 index ccbdf6a0a..000000000 --- a/src/tests/rust_guests/callbackguest/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "callbackguest" -version = "0.1.0" -edition = "2021" - -[dependencies] -hyperlight-guest = { path = "../../../hyperlight_guest" } -hyperlight-guest-bin = { path = "../../../hyperlight_guest_bin" } -hyperlight-common = { path = "../../../hyperlight_common", default-features = false } \ No newline at end of file diff --git a/src/tests/rust_guests/callbackguest/src/main.rs b/src/tests/rust_guests/callbackguest/src/main.rs deleted file mode 100644 index 0b79d578e..000000000 --- a/src/tests/rust_guests/callbackguest/src/main.rs +++ /dev/null @@ -1,266 +0,0 @@ -/* -Copyright 2025 The Hyperlight Authors. - -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. -*/ - -#![no_std] -#![no_main] - -extern crate alloc; -extern crate hyperlight_guest; - -use alloc::format; -use alloc::string::ToString; -use alloc::vec::Vec; - -use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall; -use hyperlight_common::flatbuffer_wrappers::function_types::{ - ParameterType, ParameterValue, ReturnType, -}; -use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; -use hyperlight_common::flatbuffer_wrappers::guest_log_level::LogLevel; -use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result; -use hyperlight_guest::error::{HyperlightGuestError, Result}; -use hyperlight_guest_bin::guest_function::definition::GuestFunctionDefinition; -use hyperlight_guest_bin::guest_function::register::register_function; -use hyperlight_guest_bin::guest_logger::log_message; -use hyperlight_guest_bin::host_comm::{call_host_function, print_output_with_host_print}; - -fn send_message_to_host_method( - method_name: &str, - guest_message: &str, - message: &str, -) -> Result> { - let message = format!("{}{}", guest_message, message); - let res = call_host_function::( - method_name, - Some(Vec::from(&[ParameterValue::String(message.to_string())])), - ReturnType::Int, - )?; - - Ok(get_flatbuffer_result(res)) -} - -fn guest_function(function_call: &FunctionCall) -> Result> { - if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { - send_message_to_host_method("HostMethod", "Hello from GuestFunction, ", message) - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to guest_function".to_string(), - )) - } -} - -fn guest_function1(function_call: &FunctionCall) -> Result> { - if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { - send_message_to_host_method("HostMethod1", "Hello from GuestFunction1, ", message) - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to guest_function1".to_string(), - )) - } -} - -fn guest_function2(function_call: &FunctionCall) -> Result> { - if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { - send_message_to_host_method("HostMethod1", "Hello from GuestFunction2, ", message) - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to guest_function2".to_string(), - )) - } -} - -fn guest_function3(function_call: &FunctionCall) -> Result> { - if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { - send_message_to_host_method("HostMethod1", "Hello from GuestFunction3, ", message) - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to guest_function3".to_string(), - )) - } -} - -fn guest_function4(_: &FunctionCall) -> Result> { - call_host_function::<()>( - "HostMethod4", - Some(Vec::from(&[ParameterValue::String( - "Hello from GuestFunction4".to_string(), - )])), - ReturnType::Void, - )?; - - Ok(get_flatbuffer_result(())) -} - -fn guest_log_message(function_call: &FunctionCall) -> Result> { - if let ( - ParameterValue::String(message), - ParameterValue::String(source), - ParameterValue::Int(level), - ) = ( - &function_call.parameters.as_ref().unwrap()[0], - &function_call.parameters.as_ref().unwrap()[1], - &function_call.parameters.as_ref().unwrap()[2], - ) { - let mut log_level = *level; - if !(0..=6).contains(&log_level) { - log_level = 0; - } - - log_message( - LogLevel::from(log_level as u8), - message, - source, - "guest_log_message", - file!(), - line!(), - ); - - Ok(get_flatbuffer_result(message.len() as i32)) - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to guest_log_message".to_string(), - )) - } -} - -fn call_error_method(function_call: &FunctionCall) -> Result> { - if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { - send_message_to_host_method("ErrorMethod", "Error From Host: ", message) - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to call_error_method".to_string(), - )) - } -} - -fn call_host_spin(_: &FunctionCall) -> Result> { - call_host_function::<()>("Spin", None, ReturnType::Void)?; - Ok(get_flatbuffer_result(())) -} - -fn host_call_loop(function_call: &FunctionCall) -> Result> { - if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { - loop { - call_host_function::<()>(message, None, ReturnType::Void).unwrap(); - } - } else { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to host_call_loop".to_string(), - )) - } -} - -#[no_mangle] -pub extern "C" fn hyperlight_main() { - let print_output_def = GuestFunctionDefinition::new( - "PrintOutput".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Int, - print_output_with_host_print as usize, - ); - register_function(print_output_def); - - let guest_function_def = GuestFunctionDefinition::new( - "GuestMethod".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Int, - guest_function as usize, - ); - register_function(guest_function_def); - - let guest_function1_def = GuestFunctionDefinition::new( - "GuestMethod1".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Int, - guest_function1 as usize, - ); - register_function(guest_function1_def); - - let guest_function2_def = GuestFunctionDefinition::new( - "GuestMethod2".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Int, - guest_function2 as usize, - ); - register_function(guest_function2_def); - - let guest_function3_def = GuestFunctionDefinition::new( - "GuestMethod3".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Int, - guest_function3 as usize, - ); - register_function(guest_function3_def); - - let guest_function4_def = GuestFunctionDefinition::new( - "GuestMethod4".to_string(), - Vec::new(), - ReturnType::Int, - guest_function4 as usize, - ); - register_function(guest_function4_def); - - let guest_log_message_def = GuestFunctionDefinition::new( - "LogMessage".to_string(), - Vec::from(&[ - ParameterType::String, - ParameterType::String, - ParameterType::Int, - ]), - ReturnType::Int, - guest_log_message as usize, - ); - register_function(guest_log_message_def); - - let call_error_method_def = GuestFunctionDefinition::new( - "CallErrorMethod".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Int, - call_error_method as usize, - ); - register_function(call_error_method_def); - - let call_host_spin_def = GuestFunctionDefinition::new( - "CallHostSpin".to_string(), - Vec::new(), - ReturnType::Int, - call_host_spin as usize, - ); - register_function(call_host_spin_def); - - let host_call_loop_def = GuestFunctionDefinition::new( - "HostCallLoop".to_string(), - Vec::from(&[ParameterType::String]), - ReturnType::Void, - host_call_loop as usize, - ); - register_function(host_call_loop_def); -} - -#[no_mangle] -pub fn guest_dispatch_function(function_call: FunctionCall) -> Result> { - Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionNotFound, - function_call.function_name.clone(), - )) -} diff --git a/src/tests/rust_guests/dummyguest/Cargo.lock b/src/tests/rust_guests/dummyguest/Cargo.lock index 3848de252..8844a2bb2 100644 --- a/src/tests/rust_guests/dummyguest/Cargo.lock +++ b/src/tests/rust_guests/dummyguest/Cargo.lock @@ -1,7 +1,331 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "buddy_system_allocator" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0108968a3a2dab95b089c0fc3f1afa7759aa5ebe6f1d86d206d6f7ba726eb" +dependencies = [ + "spin 0.9.8", +] + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "dummyguest" -version = "0.4.0" +version = "0.10.0" +dependencies = [ + "hyperlight-common", + "hyperlight-guest-bin", +] + +[[package]] +name = "flatbuffers" +version = "25.9.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "hyperlight-common" +version = "0.10.0" +dependencies = [ + "anyhow", + "flatbuffers", + "log", + "spin 0.10.0", +] + +[[package]] +name = "hyperlight-guest" +version = "0.10.0" +dependencies = [ + "anyhow", + "flatbuffers", + "hyperlight-common", + "hyperlight-guest-tracing", + "serde_json", + "tracing", +] + +[[package]] +name = "hyperlight-guest-bin" +version = "0.10.0" +dependencies = [ + "buddy_system_allocator", + "cc", + "cfg-if", + "flatbuffers", + "glob", + "hyperlight-common", + "hyperlight-guest", + "hyperlight-guest-tracing", + "log", + "spin 0.10.0", + "tracing", +] + +[[package]] +name = "hyperlight-guest-tracing" +version = "0.10.0" +dependencies = [ + "heapless", + "hyperlight-common", + "spin 0.10.0", + "tracing", + "tracing-core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/src/tests/rust_guests/dummyguest/Cargo.toml b/src/tests/rust_guests/dummyguest/Cargo.toml index 619031cae..ccf3a7010 100644 --- a/src/tests/rust_guests/dummyguest/Cargo.toml +++ b/src/tests/rust_guests/dummyguest/Cargo.toml @@ -1,4 +1,14 @@ [package] name = "dummyguest" -version = "0.4.0" -edition = "2021" \ No newline at end of file +version = "0.10.0" +edition = "2021" + + +[dependencies] +hyperlight-guest-bin = { path = "../../../hyperlight_guest_bin" } +hyperlight-common = { path = "../../../hyperlight_common", default-features = false } + +[features] +default = [] +trace_guest = ["hyperlight-guest-bin/trace_guest"] +mem_profile = ["hyperlight-common/mem_profile", "hyperlight-guest-bin/mem_profile"] diff --git a/src/tests/rust_guests/simpleguest/Cargo.lock b/src/tests/rust_guests/simpleguest/Cargo.lock index b30d4ef5e..5edd3cc30 100644 --- a/src/tests/rust_guests/simpleguest/Cargo.lock +++ b/src/tests/rust_guests/simpleguest/Cargo.lock @@ -4,21 +4,21 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "buddy_system_allocator" @@ -29,26 +29,32 @@ dependencies = [ "spin 0.9.8", ] +[[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.25" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "flatbuffers" -version = "25.2.10" +version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ "bitflags", "rustc_version", @@ -56,13 +62,33 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] [[package]] name = "hyperlight-common" -version = "0.7.0" +version = "0.10.0" dependencies = [ "anyhow", "flatbuffers", @@ -72,25 +98,42 @@ dependencies = [ [[package]] name = "hyperlight-guest" -version = "0.7.0" +version = "0.10.0" dependencies = [ "anyhow", + "flatbuffers", "hyperlight-common", + "hyperlight-guest-tracing", "serde_json", + "tracing", ] [[package]] name = "hyperlight-guest-bin" -version = "0.7.0" +version = "0.10.0" dependencies = [ "buddy_system_allocator", "cc", "cfg-if", + "flatbuffers", "glob", "hyperlight-common", "hyperlight-guest", + "hyperlight-guest-tracing", "log", "spin 0.10.0", + "tracing", +] + +[[package]] +name = "hyperlight-guest-tracing" +version = "0.10.0" +dependencies = [ + "heapless", + "hyperlight-common", + "spin 0.10.0", + "tracing", + "tracing-core", ] [[package]] @@ -101,9 +144,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -111,30 +154,36 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -188,9 +237,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -206,12 +255,14 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simpleguest" -version = "0.4.0" +version = "0.10.0" dependencies = [ "hyperlight-common", "hyperlight-guest", "hyperlight-guest-bin", + "hyperlight-guest-tracing", "log", + "tracing", ] [[package]] @@ -232,17 +283,51 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "syn" -version = "2.0.100" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/src/tests/rust_guests/simpleguest/Cargo.toml b/src/tests/rust_guests/simpleguest/Cargo.toml index f2e75ee1c..183698bf3 100644 --- a/src/tests/rust_guests/simpleguest/Cargo.toml +++ b/src/tests/rust_guests/simpleguest/Cargo.toml @@ -1,10 +1,18 @@ [package] name = "simpleguest" -version = "0.4.0" +version = "0.10.0" edition = "2021" [dependencies] hyperlight-guest = { path = "../../../hyperlight_guest" } hyperlight-guest-bin = { path = "../../../hyperlight_guest_bin" } hyperlight-common = { path = "../../../hyperlight_common", default-features = false } +hyperlight-guest-tracing = { path = "../../../hyperlight_guest_tracing" } log = {version = "0.4", default-features = false } +tracing = { version = "0.1.41", default-features = false, features = ["attributes"] } + +[features] +default = [] +trace_guest = ["hyperlight-guest-bin/trace_guest", "hyperlight-guest/trace_guest", "hyperlight-guest-tracing/trace"] +mem_profile = ["hyperlight-common/mem_profile", "hyperlight-guest-bin/mem_profile"] + diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 1cd276dd6..eda988f33 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -29,6 +29,7 @@ use alloc::boxed::Box; use alloc::string::ToString; use alloc::vec::Vec; use alloc::{format, vec}; +use core::alloc::Layout; use core::ffi::c_char; use core::hint::black_box; use core::ptr::write_volatile; @@ -46,11 +47,13 @@ use hyperlight_guest::exit::{abort_with_code, abort_with_code_and_message}; use hyperlight_guest_bin::guest_function::definition::GuestFunctionDefinition; use hyperlight_guest_bin::guest_function::register::register_function; use hyperlight_guest_bin::host_comm::{ - call_host_function, call_host_function_without_returning_result, read_n_bytes_from_user_memory, + call_host_function, call_host_function_without_returning_result, print_output_with_host_print, + read_n_bytes_from_user_memory, }; use hyperlight_guest_bin::memory::malloc; use hyperlight_guest_bin::{MIN_STACK_ADDRESS, guest_logger}; use log::{LevelFilter, error}; +use tracing::{Span, instrument}; extern crate hyperlight_guest; @@ -89,6 +92,7 @@ fn echo_float(function_call: &FunctionCall) -> Result> { } } +#[instrument(skip_all, parent = Span::current(), level= "Trace")] fn print_output(message: &str) -> Result> { let res = call_host_function::( "HostPrint", @@ -99,6 +103,7 @@ fn print_output(message: &str) -> Result> { Ok(get_flatbuffer_result(res)) } +#[instrument(skip_all, parent = Span::current(), level= "Trace")] fn simple_print_output(function_call: &FunctionCall) -> Result> { if let ParameterValue::String(message) = function_call.parameters.clone().unwrap()[0].clone() { print_output(&message) @@ -484,6 +489,22 @@ fn call_malloc(function_call: &FunctionCall) -> Result> { } } +unsafe fn exhaust_heap(_: &FunctionCall) -> ! { + let layout: Layout = Layout::new::(); + let mut ptr = alloc::alloc::alloc_zeroed(layout); + while !ptr.is_null() { + black_box(ptr); + ptr = alloc::alloc::alloc_zeroed(layout); + } + + // after alloc::alloc_zeroed failure (null return when called in loop above) + // allocate a Vec to ensure OOM panic + let vec = Vec::::with_capacity(1); + black_box(vec); + + panic!("function should have panicked before due to OOM") +} + fn malloc_and_free(function_call: &FunctionCall) -> Result> { if let ParameterValue::Int(size) = function_call.parameters.clone().unwrap()[0].clone() { let alloc_length = if size < DEFAULT_GUEST_STACK_SIZE { @@ -687,15 +708,14 @@ fn add_to_static_and_fail(_: &FunctionCall) -> Result> { )) } -fn violate_seccomp_filters(function_call: &FunctionCall) -> Result> { - if function_call.parameters.is_none() { - let res = call_host_function::("MakeGetpidSyscall", None, ReturnType::ULong)?; - - Ok(get_flatbuffer_result(res)) +fn twenty_four_k_in_eight_k_out(function_call: &FunctionCall) -> Result> { + if let ParameterValue::VecBytes(input) = &function_call.parameters.as_ref().unwrap()[0] { + assert!(input.len() == 24 * 1024, "Input must be 24K bytes"); + Ok(get_flatbuffer_result(&input[..8 * 1024])) } else { Err(HyperlightGuestError::new( ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to violate_seccomp_filters".to_string(), + "Invalid parameters passed to 24K_in_8K_out".to_string(), )) } } @@ -715,6 +735,14 @@ fn call_given_paramless_hostfunc_that_returns_i64(function_call: &FunctionCall) } } +fn use_sse2_registers(_: &FunctionCall) -> Result> { + unsafe { + let val: f32 = 1.2f32; + core::arch::asm!("movss xmm1, DWORD PTR [{0}]", in(reg) &val); + } + Ok(get_flatbuffer_result(())) +} + fn add(function_call: &FunctionCall) -> Result> { if let (ParameterValue::Int(a), ParameterValue::Int(b)) = ( function_call.parameters.clone().unwrap()[0].clone(), @@ -775,24 +803,168 @@ fn read_from_user_memory(function_call: &FunctionCall) -> Result> { } } +fn read_mapped_buffer(function_call: &FunctionCall) -> Result> { + if let (ParameterValue::ULong(base), ParameterValue::ULong(len)) = ( + function_call.parameters.clone().unwrap()[0].clone(), + function_call.parameters.clone().unwrap()[1].clone(), + ) { + let base = base as usize as *const u8; + let len = len as usize; + + unsafe { + hyperlight_guest_bin::paging::map_region(base as _, base as _, len as u64 + 4096) + }; + + let data = unsafe { core::slice::from_raw_parts(base, len) }; + + Ok(get_flatbuffer_result(data)) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to read_mapped_buffer".to_string(), + )) + } +} + +fn write_mapped_buffer(function_call: &FunctionCall) -> Result> { + if let (ParameterValue::ULong(base), ParameterValue::ULong(len)) = ( + function_call.parameters.clone().unwrap()[0].clone(), + function_call.parameters.clone().unwrap()[1].clone(), + ) { + let base = base as usize as *mut u8; + let len = len as usize; + + unsafe { + hyperlight_guest_bin::paging::map_region(base as _, base as _, len as u64 + 4096) + }; + + let data = unsafe { core::slice::from_raw_parts_mut(base, len) }; + + // should fail + data[0] = 0x42; + + // should never reach this + Ok(get_flatbuffer_result(true)) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to read_mapped_buffer".to_string(), + )) + } +} + +fn exec_mapped_buffer(function_call: &FunctionCall) -> Result> { + if let (ParameterValue::ULong(base), ParameterValue::ULong(len)) = ( + function_call.parameters.clone().unwrap()[0].clone(), + function_call.parameters.clone().unwrap()[1].clone(), + ) { + let base = base as usize as *mut u8; + let len = len as usize; + + unsafe { + hyperlight_guest_bin::paging::map_region(base as _, base as _, len as u64 + 4096) + }; + + let data = unsafe { core::slice::from_raw_parts(base, len) }; + + // Should be safe as long as data is something like a NOOP followed by a RET + let func: fn() = unsafe { core::mem::transmute(data.as_ptr()) }; + func(); + + Ok(get_flatbuffer_result(true)) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to read_mapped_buffer".to_string(), + )) + } +} + +fn call_host_expect_error(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(hostfuncname) = + function_call.parameters.clone().unwrap()[0].clone() + { + let res = call_host_function::(&hostfuncname, None, ReturnType::Int); + + match res { + Ok(_) => Err(HyperlightGuestError::new( + ErrorCode::GuestError, + "Expected host function to fail, but it succeeded".to_string(), + )), + Err(e) => { + assert_eq!(e.kind, ErrorCode::HostFunctionError); + assert_eq!( + e.message, + format!("HostFunction {} was not found", hostfuncname) + ); + Ok(get_flatbuffer_result(())) + } + } + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to call_host_expect_error".to_string(), + )) + } +} + #[no_mangle] +#[instrument(skip_all, parent = Span::current(), level= "Trace")] pub extern "C" fn hyperlight_main() { + let expect_error_def = GuestFunctionDefinition::new( + "CallHostExpectError".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Void, + call_host_expect_error as usize, + ); + register_function(expect_error_def); + + let twenty_four_k_in_def = GuestFunctionDefinition::new( + "24K_in_8K_out".to_string(), + Vec::from(&[ParameterType::VecBytes]), + ReturnType::VecBytes, + twenty_four_k_in_eight_k_out as usize, + ); + register_function(twenty_four_k_in_def); + let read_from_user_memory_def = GuestFunctionDefinition::new( "ReadFromUserMemory".to_string(), Vec::from(&[ParameterType::ULong, ParameterType::VecBytes]), ReturnType::VecBytes, read_from_user_memory as usize, ); - register_function(read_from_user_memory_def); + let read_mapped_buffer_def = GuestFunctionDefinition::new( + "ReadMappedBuffer".to_string(), + Vec::from(&[ParameterType::ULong, ParameterType::ULong]), + ReturnType::VecBytes, + read_mapped_buffer as usize, + ); + register_function(read_mapped_buffer_def); + + let write_mapped_buffer_def = GuestFunctionDefinition::new( + "WriteMappedBuffer".to_string(), + Vec::from(&[ParameterType::ULong, ParameterType::ULong]), + ReturnType::Bool, + write_mapped_buffer as usize, + ); + register_function(write_mapped_buffer_def); + + let exec_mapped_buffer_def = GuestFunctionDefinition::new( + "ExecMappedBuffer".to_string(), + Vec::from(&[ParameterType::ULong, ParameterType::ULong]), + ReturnType::Bool, + exec_mapped_buffer as usize, + ); + register_function(exec_mapped_buffer_def); + let set_static_def = GuestFunctionDefinition::new( "SetStatic".to_string(), Vec::new(), ReturnType::Int, set_static as usize, ); - register_function(set_static_def); let simple_print_output_def = GuestFunctionDefinition::new( @@ -803,6 +975,90 @@ pub extern "C" fn hyperlight_main() { ); register_function(simple_print_output_def); + let print_output_def = GuestFunctionDefinition::new( + "PrintOutputWithHostPrint".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Int, + print_output_with_host_print as usize, + ); + register_function(print_output_def); + + let guest_function_def = GuestFunctionDefinition::new( + "GuestMethod".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Int, + guest_function as usize, + ); + register_function(guest_function_def); + + let guest_function1_def = GuestFunctionDefinition::new( + "GuestMethod1".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Int, + guest_function1 as usize, + ); + register_function(guest_function1_def); + + let guest_function2_def = GuestFunctionDefinition::new( + "GuestMethod2".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Int, + guest_function2 as usize, + ); + register_function(guest_function2_def); + + let guest_function3_def = GuestFunctionDefinition::new( + "GuestMethod3".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Int, + guest_function3 as usize, + ); + register_function(guest_function3_def); + + let guest_function4_def = GuestFunctionDefinition::new( + "GuestMethod4".to_string(), + Vec::new(), + ReturnType::Int, + guest_function4 as usize, + ); + register_function(guest_function4_def); + + let guest_log_message_def = GuestFunctionDefinition::new( + "LogMessageWithSource".to_string(), + Vec::from(&[ + ParameterType::String, + ParameterType::String, + ParameterType::Int, + ]), + ReturnType::Int, + guest_log_message as usize, + ); + register_function(guest_log_message_def); + + let call_error_method_def = GuestFunctionDefinition::new( + "CallErrorMethod".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Int, + call_error_method as usize, + ); + register_function(call_error_method_def); + + let call_host_spin_def = GuestFunctionDefinition::new( + "CallHostSpin".to_string(), + Vec::new(), + ReturnType::Int, + call_host_spin as usize, + ); + register_function(call_host_spin_def); + + let host_call_loop_def = GuestFunctionDefinition::new( + "HostCallLoop".to_string(), + Vec::from(&[ParameterType::String]), + ReturnType::Void, + host_call_loop as usize, + ); + register_function(host_call_loop_def); + let print_using_printf_def = GuestFunctionDefinition::new( "PrintUsingPrintf".to_string(), Vec::from(&[ParameterType::String]), @@ -851,6 +1107,14 @@ pub extern "C" fn hyperlight_main() { ); register_function(call_malloc_def); + let exhaust_heap_def = GuestFunctionDefinition::new( + "ExhaustHeap".to_string(), + Vec::new(), + ReturnType::Int, + exhaust_heap as usize, + ); + register_function(exhaust_heap_def); + let malloc_and_free_def = GuestFunctionDefinition::new( "MallocAndFree".to_string(), Vec::from(&[ParameterType::Int]), @@ -1139,14 +1403,6 @@ pub extern "C" fn hyperlight_main() { ); register_function(add_to_static_and_fail_def); - let violate_seccomp_filters_def = GuestFunctionDefinition::new( - "ViolateSeccompFilters".to_string(), - Vec::new(), - ReturnType::ULong, - violate_seccomp_filters as usize, - ); - register_function(violate_seccomp_filters_def); - let echo_float_def = GuestFunctionDefinition::new( "EchoFloat".to_string(), Vec::from(&[ParameterType::Float]), @@ -1194,9 +1450,180 @@ pub extern "C" fn hyperlight_main() { call_given_paramless_hostfunc_that_returns_i64 as usize, ); register_function(call_given_hostfunc_def); + + let use_sse2_registers = GuestFunctionDefinition::new( + "UseSSE2Registers".to_string(), + Vec::new(), + ReturnType::Void, + use_sse2_registers as usize, + ); + register_function(use_sse2_registers); +} + +fn send_message_to_host_method( + method_name: &str, + guest_message: &str, + message: &str, +) -> Result> { + let message = format!("{}{}", guest_message, message); + let res = call_host_function::( + method_name, + Some(Vec::from(&[ParameterValue::String(message.to_string())])), + ReturnType::Int, + )?; + + Ok(get_flatbuffer_result(res)) +} + +fn guest_function(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { + send_message_to_host_method("HostMethod", "Hello from GuestFunction, ", message) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to guest_function".to_string(), + )) + } +} + +fn guest_function1(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { + send_message_to_host_method("HostMethod1", "Hello from GuestFunction1, ", message) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to guest_function1".to_string(), + )) + } +} + +fn guest_function2(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { + send_message_to_host_method("HostMethod1", "Hello from GuestFunction2, ", message) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to guest_function2".to_string(), + )) + } +} + +fn guest_function3(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { + send_message_to_host_method("HostMethod1", "Hello from GuestFunction3, ", message) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to guest_function3".to_string(), + )) + } +} + +fn guest_function4(_: &FunctionCall) -> Result> { + call_host_function::<()>( + "HostMethod4", + Some(Vec::from(&[ParameterValue::String( + "Hello from GuestFunction4".to_string(), + )])), + ReturnType::Void, + )?; + + Ok(get_flatbuffer_result(())) +} + +fn guest_log_message(function_call: &FunctionCall) -> Result> { + if let ( + ParameterValue::String(message), + ParameterValue::String(source), + ParameterValue::Int(level), + ) = ( + &function_call.parameters.as_ref().unwrap()[0], + &function_call.parameters.as_ref().unwrap()[1], + &function_call.parameters.as_ref().unwrap()[2], + ) { + let mut log_level = *level; + if !(0..=6).contains(&log_level) { + log_level = 0; + } + + guest_logger::log_message( + LogLevel::from(log_level as u8), + message, + source, + "guest_log_message", + file!(), + line!(), + ); + + Ok(get_flatbuffer_result(message.len() as i32)) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to guest_log_message".to_string(), + )) + } +} + +fn call_error_method(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { + send_message_to_host_method("ErrorMethod", "Error From Host: ", message) + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to call_error_method".to_string(), + )) + } +} + +fn call_host_spin(_: &FunctionCall) -> Result> { + call_host_function::<()>("Spin", None, ReturnType::Void)?; + Ok(get_flatbuffer_result(())) +} + +fn host_call_loop(function_call: &FunctionCall) -> Result> { + if let ParameterValue::String(message) = &function_call.parameters.as_ref().unwrap()[0] { + loop { + call_host_function::<()>(message, None, ReturnType::Void).unwrap(); + } + } else { + Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to host_call_loop".to_string(), + )) + } +} + +// Interprets the given guest function call as a host function call and dispatches it to the host. +fn fuzz_host_function(func: FunctionCall) -> Result> { + let mut params = func.parameters.unwrap(); + // first parameter must be string (the name of the host function to call) + let host_func_name = match params.remove(0) { + // TODO use `swap_remove` instead of `remove` if performance is an issue, but left out + // to avoid confusion for replicating failure cases + ParameterValue::String(name) => name, + _ => { + return Err(HyperlightGuestError::new( + ErrorCode::GuestFunctionParameterTypeMismatch, + "Invalid parameters passed to fuzz_host_function".to_string(), + )); + } + }; + + // Because we do not know at compile time the actual return type of the host function to be called + // we cannot use the `call_host_function` generic function. + // We need to use the `call_host_function_without_returning_result` function that does not retrieve the return + // value + call_host_function_without_returning_result( + &host_func_name, + Some(params), + func.expected_return_type, + ) + .expect("failed to call host function"); + Ok(get_flatbuffer_result(())) } #[no_mangle] +#[instrument(skip_all, parent = Span::current(), level= "Trace")] pub fn guest_dispatch_function(function_call: FunctionCall) -> Result> { // This test checks the stack behavior of the input/output buffer // by calling the host before serializing the function call. @@ -1240,32 +1667,3 @@ pub fn guest_dispatch_function(function_call: FunctionCall) -> Result> { Ok(get_flatbuffer_result(99)) } - -// Interprets the given guest function call as a host function call and dispatches it to the host. -fn fuzz_host_function(func: FunctionCall) -> Result> { - let mut params = func.parameters.unwrap(); - // first parameter must be string (the name of the host function to call) - let host_func_name = match params.remove(0) { - // TODO use `swap_remove` instead of `remove` if performance is an issue, but left out - // to avoid confusion for replicating failure cases - ParameterValue::String(name) => name, - _ => { - return Err(HyperlightGuestError::new( - ErrorCode::GuestFunctionParameterTypeMismatch, - "Invalid parameters passed to fuzz_host_function".to_string(), - )); - } - }; - - // Because we do not know at compile time the actual return type of the host function to be called - // we cannot use the `call_host_function` generic function. - // We need to use the `call_host_function_without_returning_result` function that does not retrieve the return - // value - call_host_function_without_returning_result( - &host_func_name, - Some(params), - func.expected_return_type, - ) - .expect("failed to call host function"); - Ok(get_flatbuffer_result(())) -} diff --git a/src/tests/rust_guests/witguest/Cargo.lock b/src/tests/rust_guests/witguest/Cargo.lock index 478740f35..6172a003c 100644 --- a/src/tests/rust_guests/witguest/Cargo.lock +++ b/src/tests/rust_guests/witguest/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -43,18 +43,18 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", @@ -63,21 +63,21 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "buddy_system_allocator" @@ -88,20 +88,26 @@ dependencies = [ "spin 0.9.8", ] +[[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.25" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "colorchoice" @@ -146,9 +152,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "flatbuffers" -version = "25.2.10" +version = "25.9.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "09b6620799e7340ebd9968d2e0708eb82cf1971e9a16821e2091b6d6e475eed5" dependencies = [ "bitflags", "rustc_version", @@ -162,23 +168,43 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", "serde", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] + [[package]] name = "hyperlight-common" -version = "0.6.1" +version = "0.10.0" dependencies = [ "anyhow", "flatbuffers", @@ -188,7 +214,7 @@ dependencies = [ [[package]] name = "hyperlight-component-macro" -version = "0.6.1" +version = "0.10.0" dependencies = [ "env_logger", "hyperlight-component-util", @@ -202,7 +228,7 @@ dependencies = [ [[package]] name = "hyperlight-component-util" -version = "0.6.1" +version = "0.10.0" dependencies = [ "itertools", "log", @@ -215,32 +241,49 @@ dependencies = [ [[package]] name = "hyperlight-guest" -version = "0.6.1" +version = "0.10.0" dependencies = [ "anyhow", + "flatbuffers", "hyperlight-common", + "hyperlight-guest-tracing", "serde_json", + "tracing", ] [[package]] name = "hyperlight-guest-bin" -version = "0.6.1" +version = "0.10.0" dependencies = [ "buddy_system_allocator", "cc", "cfg-if", + "flatbuffers", "glob", "hyperlight-common", "hyperlight-guest", + "hyperlight-guest-tracing", "log", "spin 0.10.0", + "tracing", +] + +[[package]] +name = "hyperlight-guest-tracing" +version = "0.10.0" +dependencies = [ + "heapless", + "hyperlight-common", + "spin 0.10.0", + "tracing", + "tracing-core", ] [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -270,9 +313,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -283,9 +326,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", @@ -294,9 +337,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -304,15 +347,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "once_cell_polyfill" @@ -320,11 +363,17 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -337,9 +386,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -347,27 +396,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -377,9 +426,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -388,9 +437,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rustc_version" @@ -441,9 +490,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -475,17 +524,51 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "syn" -version = "2.0.103" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -500,9 +583,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasmparser" -version = "0.224.1" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" dependencies = [ "bitflags", "hashbrown", @@ -511,21 +594,28 @@ dependencies = [ "serde", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -538,58 +628,59 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "witguest" -version = "0.1.0" +version = "0.10.0" dependencies = [ "hyperlight-common", "hyperlight-component-macro", "hyperlight-guest", "hyperlight-guest-bin", + "spin 0.10.0", ] diff --git a/src/tests/rust_guests/witguest/Cargo.toml b/src/tests/rust_guests/witguest/Cargo.toml index 63b38fc13..4db4984ab 100644 --- a/src/tests/rust_guests/witguest/Cargo.toml +++ b/src/tests/rust_guests/witguest/Cargo.toml @@ -1,10 +1,16 @@ [package] name = "witguest" -version = "0.1.0" +version = "0.10.0" edition = "2021" [dependencies] hyperlight-guest = { path = "../../../hyperlight_guest" } hyperlight-guest-bin = { path = "../../../hyperlight_guest_bin" } hyperlight-common = { path = "../../../hyperlight_common", default-features = false } -hyperlight-component-macro = { path = "../../../hyperlight_component_macro" } \ No newline at end of file +hyperlight-component-macro = { path = "../../../hyperlight_component_macro" } +spin = "0.10.0" + +[features] +default = [] +trace_guest = ["hyperlight-guest-bin/trace_guest", "hyperlight-guest/trace_guest"] +mem_profile = ["hyperlight-common/mem_profile", "hyperlight-guest-bin/mem_profile"] diff --git a/src/tests/rust_guests/witguest/guest.wit b/src/tests/rust_guests/witguest/guest.wit index 30b4969c5..9ed164b80 100644 --- a/src/tests/rust_guests/witguest/guest.wit +++ b/src/tests/rust_guests/witguest/guest.wit @@ -26,6 +26,20 @@ interface roundtrip { roundtrip-option: func(x: option) -> option; roundtrip-result: func(x: result) -> result; roundtrip-no-result: func(x: u32); + roundtrip-fix-list: func(x: list) -> list; + roundtrip-fix-list-u32: func(x: list) -> list; + roundtrip-fix-list-u64: func(x: list) -> list; + roundtrip-fix-list-i8: func(x: list) -> list; + roundtrip-fix-list-i16: func(x: list) -> list; + roundtrip-fix-list-i32: func(x: list) -> list; + roundtrip-fix-list-i64: func(x: list) -> list; + roundtrip-fix-list-f32: func(x: list) -> list; + roundtrip-fix-list-f64: func(x: list) -> list; + roundtrip-fix-list-u8-size8: func(x: list) -> list; + roundtrip-fix-list-u64-size2: func(x: list) -> list; + roundtrip-fix-list-string: func(x: list) -> list; + roundtrip-fix-array-of-lists: func(x: list, 3>) -> list, 3>; + roundtrip-fix-array-of-string-lists: func(x: list, 2>) -> list, 2>; record testrecord { contents: string, @@ -66,5 +80,13 @@ interface host-resource { } interface test-host-resource { - test: func() -> bool; + use host-resource.{testresource}; + + test-uses-locally: func() -> bool; + + test-makes: func() -> own; + + test-accepts-borrow: func(x: borrow); + test-accepts-own: func(x: own); + test-returns: func() -> own; } \ No newline at end of file diff --git a/src/tests/rust_guests/witguest/src/main.rs b/src/tests/rust_guests/witguest/src/main.rs index 1edf44ec1..307d9cca6 100644 --- a/src/tests/rust_guests/witguest/src/main.rs +++ b/src/tests/rust_guests/witguest/src/main.rs @@ -20,10 +20,16 @@ limitations under the License. extern crate alloc; extern crate hyperlight_guest; +use alloc::string::String; + +use spin::Mutex; + mod bindings; use bindings::*; -struct Guest {} +struct Guest { + host_resource: Option<::T>, +} impl test::wit::Roundtrip for Guest { fn roundtrip_bool(&mut self, x: bool) -> bool { @@ -113,16 +119,59 @@ impl test::wit::Roundtrip for Guest { ) -> test::wit::roundtrip::Testenum { (Host {}).roundtrip_enum(x) } + fn roundtrip_fix_list(&mut self, x: [u8; 4]) -> [u8; 4] { + (Host {}).roundtrip_fix_list(x) + } + fn roundtrip_fix_list_u32(&mut self, x: [u32; 4]) -> [u32; 4] { + (Host {}).roundtrip_fix_list_u32(x) + } + fn roundtrip_fix_list_u64(&mut self, x: [u64; 4]) -> [u64; 4] { + (Host {}).roundtrip_fix_list_u64(x) + } + fn roundtrip_fix_list_i8(&mut self, x: [i8; 4]) -> [i8; 4] { + (Host {}).roundtrip_fix_list_i8(x) + } + fn roundtrip_fix_list_i16(&mut self, x: [i16; 4]) -> [i16; 4] { + (Host {}).roundtrip_fix_list_i16(x) + } + fn roundtrip_fix_list_i32(&mut self, x: [i32; 4]) -> [i32; 4] { + (Host {}).roundtrip_fix_list_i32(x) + } + fn roundtrip_fix_list_i64(&mut self, x: [i64; 4]) -> [i64; 4] { + (Host {}).roundtrip_fix_list_i64(x) + } + fn roundtrip_fix_list_f32(&mut self, x: [f32; 4]) -> [f32; 4] { + (Host {}).roundtrip_fix_list_f32(x) + } + fn roundtrip_fix_list_f64(&mut self, x: [f64; 4]) -> [f64; 4] { + (Host {}).roundtrip_fix_list_f64(x) + } + fn roundtrip_fix_list_u8_size8(&mut self, x: [u8; 8]) -> [u8; 8] { + (Host {}).roundtrip_fix_list_u8_size8(x) + } + fn roundtrip_fix_list_u64_size2(&mut self, x: [u64; 2]) -> [u64; 2] { + (Host {}).roundtrip_fix_list_u64_size2(x) + } + fn roundtrip_fix_list_string(&mut self, x: [String; 4]) -> [String; 4] { + (Host {}).roundtrip_fix_list_string(x) + } + fn roundtrip_fix_array_of_lists(&mut self, x: [Vec; 3]) -> [Vec; 3] { + x + } + fn roundtrip_fix_array_of_string_lists(&mut self, x: [Vec; 2]) -> [Vec; 2] { + x + } fn roundtrip_no_result(&mut self, x: u32) { (Host {}).roundtrip_no_result(x) } } -impl test::wit::TestHostResource for Guest { - fn test(&mut self) -> bool { - use test::wit::host_resource::Testresource; +use alloc::string::ToString; + +use test::wit::host_resource::Testresource; +impl test::wit::TestHostResource<::T> for Guest { + fn test_uses_locally(&mut self) -> bool { let mut host = Host {}; - use alloc::string::ToString; let r = ::new(&mut host, "str".to_string(), 'z'); ::append_char(&mut host, &r, 'a'); ::append_char(&mut host, &r, 'b'); @@ -132,6 +181,27 @@ impl test::wit::TestHostResource for Guest { ::return_own(&mut host, r); true } + fn test_makes(&mut self) -> ::T { + let mut host = Host {}; + ::new(&mut host, "str".to_string(), 'z') + } + fn test_accepts_borrow(&mut self, r: &::T) { + let mut host = Host {}; + ::append_char(&mut host, r, 'a'); + } + fn test_accepts_own(&mut self, r: ::T) { + let mut host = Host {}; + // TODO: add test about the old contents of this being + // dropped, when #810 is fixed. + ::append_char(&mut host, &r, 'b'); + self.host_resource = Some(r); + } + fn test_returns(&mut self) -> ::T { + let mut host = Host {}; + let r = self.host_resource.take().unwrap(); + ::append_char(&mut host, &r, 'c'); + r + } } #[allow(refining_impl_trait)] @@ -146,9 +216,13 @@ impl test::wit::TestExports for Guest { } } +static GUEST_STATE: Mutex = Mutex::new(Guest { + host_resource: None, +}); + impl bindings::Guest for Guest { fn with_guest_state R>(f: F) -> R { - let mut g = Guest {}; + let mut g = GUEST_STATE.lock(); f(&mut g) } } diff --git a/src/trace_dump/Cargo.toml b/src/trace_dump/Cargo.toml new file mode 100644 index 000000000..d72631198 --- /dev/null +++ b/src/trace_dump/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "trace_dump" +version = "0.10.0" +publish = false +edition = "2021" + +[dependencies] +addr2line = "0.25.1" +piet-common = { version = "0.8.0", features = [ "png" ] } +blake3 = { version = "1.5.5" } + +[[bin]] +name = "trace_dump" +path = "main.rs" diff --git a/src/trace_dump/main.rs b/src/trace_dump/main.rs new file mode 100644 index 000000000..84f552735 --- /dev/null +++ b/src/trace_dump/main.rs @@ -0,0 +1,846 @@ +/* +Copyright 2025 The Hyperlight Authors. + +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. +*/ + +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Mutex; +use std::time::Duration; + +use piet_common::{Color, RenderContext, Text, TextLayout, TextLayoutBuilder, kurbo}; + +fn read_u128(inf: &mut File) -> Result { + let mut bytes: [u8; 16] = [0; 16]; + inf.read_exact(&mut bytes)?; + Ok(u128::from_ne_bytes(bytes)) +} +fn read_u64(inf: &mut File) -> Option { + let mut bytes: [u8; 8] = [0; 8]; + inf.read_exact(&mut bytes).ok()?; + Some(u64::from_ne_bytes(bytes)) +} +fn read_u64_vec(inf: &mut File) -> Option> { + let len = read_u64(inf)?; + let mut vec = Vec::with_capacity(len as usize); + for _ in 0..len { + vec.push(read_u64(inf)?); + } + Some(vec) +} +fn read_blake3_hash(inf: &mut File) -> Option { + let mut bytes: [u8; 32] = [0; 32]; + inf.read_exact(&mut bytes).ok()?; + Some(blake3::Hash::from_bytes(bytes)) +} + +fn dump_stack(state: &mut State, trace: Rc<[u64]>) { + for frame in &*trace { + println!(" {:x} {}", frame, state.symbol_cache.format_symbol(*frame)); + } +} + +fn dump_ident(_state: &mut State, _rs: &mut (), now: Duration, hash: blake3::Hash) -> Option<()> { + println!("\n[{:9?}] BLAKE3 hash of binary is {}", now, hash); + Some(()) +} + +fn dump_unwind(state: &mut State, _rs: &mut (), now: Duration, trace: Rc<[u64]>) -> Option<()> { + println!("\n[{:9?}] Guest requested stack trace", now); + dump_stack(state, trace); + Some(()) +} + +fn dump_alloc( + state: &mut State, + _rs: &mut (), + now: Duration, + ptr: u64, + amt: u64, + trace: Rc<[u64]>, +) -> Option<()> { + println!("\n[{:9?}] Allocated {} bytes at 0x{:x}", now, amt, ptr); + dump_stack(state, trace); + Some(()) +} + +fn dump_free( + state: &mut State, + _rs: &mut (), + now: Duration, + ptr: u64, + amt: u64, + trace: Rc<[u64]>, +) -> Option<()> { + println!("\n[{:9?}] Freed {} bytes at 0x{:x}", now, amt, ptr); + dump_stack(state, trace); + Some(()) +} + +// todo: this should use something more reasonable than a hash table +// for each node. let's measure the out-degree and see if a small +// array is better, to start. +struct TraceTrie { + value: T, + children: HashMap>, +} +impl TraceTrie { + fn new() -> Self { + Self { + value: Default::default(), + children: HashMap::new(), + } + } + fn apply_path<'a, 'i, F: Fn(&mut T), I: Iterator>( + &'a mut self, + trace: I, + f: F, + ) { + let mut node = self; + for frame in trace { + f(&mut node.value); + node = node.children.entry(*frame).or_insert(Self::new()) + } + f(&mut node.value); + } +} + +struct SymbolCache { + loader: addr2line::Loader, + symbol_cache: HashMap)>>, +} +impl SymbolCache { + fn resolve_symbol(&mut self, addr: u64) -> &Option<(String, Option)> { + self.symbol_cache.entry(addr).or_insert_with(|| { + let frame = &self.loader.find_frames(addr).ok()?.next().ok()??; + let function = frame.function.as_ref()?; + let demangled = + addr2line::demangle_auto(function.name.to_string_lossy(), function.language) + .to_string(); + Some((demangled, frame.location.as_ref()?.line)) + }) + } + fn format_symbol(&mut self, addr: u64) -> String { + match self.resolve_symbol(addr) { + None => format!("{}", addr), + Some((f, None)) => f.clone(), + Some((f, Some(l))) => format!("{}:{}", f, l), + } + } +} + +enum Visualisation { + Bar, + Flame, +} +impl std::fmt::Display for Visualisation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Visualisation::Bar => write!(f, "bar"), + Visualisation::Flame => write!(f, "flame"), + } + } +} + +struct State { + inf: File, + symbol_cache: SymbolCache, + out_dir: Option, + start_time: Option, + end_time: Option, + allocs: HashMap)>, + sites: HashMap, + traces: TraceTrie, + total: u64, + max_total: u64, + max_duration: Duration, + num_durations: u64, +} + +struct ViewParams { + margin: f64, + width: f64, + height: f64, + label_gap: f64, + amount_gap: f64, + bar_start: f64, + bar_height: f64, + bar_leading: f64, + bar_brush: R::Brush, +} + +fn draw_bg(render_context: &mut R, v: &ViewParams) { + let bg_brush = render_context.solid_brush(Color::rgb8(255, 255, 255)); + render_context.fill( + kurbo::Rect { + x0: 0.0, + y0: 0.0, + x1: v.width + v.margin * 2.0, + y1: v.height + v.margin * 2.0, + }, + &bg_brush, + ); +} +fn draw_bar( + render_context: &mut R, + v: &ViewParams, + stroke: bool, + n: u64, + label: String, + value: u64, + max_value: u64, +) -> Option<()> { + let left = v.margin + v.bar_start; + let top = v.margin + (n as f64) * (v.bar_height + v.bar_leading); + if stroke { + render_context.stroke( + kurbo::Rect { + x0: left, + y0: top, + //x1: v.margin + v.width, + x1: left + (v.width - v.bar_start), + y1: top + v.bar_height, + }, + &v.bar_brush, + 1.0, + ); + } + let right = left + (v.width - v.bar_start) * value as f64 / max_value as f64; + render_context.fill( + kurbo::Rect { + x0: left, + y0: top, + x1: right, + y1: top + v.bar_height, + }, + &v.bar_brush, + ); + let layout = render_context.text().new_text_layout(label).build().ok()?; + let metric = layout + .line_metric(0) + .expect("there must be at least one line metric"); + render_context.draw_text( + &layout, + kurbo::Point { + x: left - v.label_gap - layout.trailing_whitespace_width(), + y: top - metric.y_offset + (v.bar_height - metric.baseline) / 2.0, + }, + ); + let layout = render_context + .text() + .new_text_layout(format!("{}", value)) + .default_attribute(piet_common::TextAttribute::TextColor(Color::rgb8( + 255, 255, 255, + ))) + .build() + .ok()?; + let metric = layout + .line_metric(0) + .expect("there must be at least one line metric"); + if right - left > v.amount_gap + layout.trailing_whitespace_width() { + render_context.draw_text( + &layout, + kurbo::Point { + x: right - v.amount_gap - layout.trailing_whitespace_width(), + y: top + (v.bar_height - metric.height) / 2.0, + }, + ); + } else { + // hopefully the choice of colour doesn't affect the layout much + let layout = render_context + .text() + .new_text_layout(format!("{}", value)) + .build() + .ok()?; + let metric = layout + .line_metric(0) + .expect("there must be at least one line metric"); + render_context.draw_text( + &layout, + kurbo::Point { + x: right + v.amount_gap, + y: top + (v.bar_height - metric.height) / 2.0, + }, + ); + } + Some(()) +} + +trait RenderWrapper { + fn render(&mut self, ctx: &mut R, width: u64, height: u64) -> Option<()>; +} +fn render_bitmap( + mut out: O, + device: &mut piet_common::Device, + mut render: F, +) -> Option<()> { + let width = 1920; + let height = 1080; + let mut bitmap = device.bitmap_target(width, height, 1.0).ok()?; + { + let mut render_context = bitmap.render_context(); + render.render(&mut render_context, width as u64, height as u64)?; + render_context.finish().ok()?; + } + out.write_all( + bitmap + .to_image_buf(piet_common::ImageFormat::RgbaPremul) + .expect("unable to access image buffer") + .raw_pixels(), + ) + .expect("write to stdout"); + Some(()) +} + +impl ViewParams { + fn new(ctx: &mut R, ht: u64, wd: u64) -> Self { + let margin = 20.0; + let width = wd as f64 - margin * 2.0; + let height = ht as f64 - margin * 2.0; + let bar_brush = ctx.solid_brush(Color::rgb8(128, 0, 128)); + Self { + margin, + width, + height, + label_gap: 10.0, + amount_gap: 5.0, + bar_start: width / 4.0, + bar_height: 12.0, + bar_leading: 4.0, + bar_brush, + } + } +} + +struct BarRenderer<'r> { + state: &'r mut State, + now: Duration, +} +impl<'a, 's> RenderWrapper for BarRenderer<'_> { + fn render(&mut self, ctx: &mut R, wd: u64, ht: u64) -> Option<()> { + let v = ViewParams::new(ctx, ht, wd); + draw_bg(ctx, &v); + draw_bar( + ctx, + &v, + true, + 0, + "Execution time".to_string(), + self.now.as_micros() as u64, + self.state.max_duration.as_micros() as u64, + )?; + draw_bar( + ctx, + &v, + true, + 1, + "Total memory consumption".to_string(), + self.state.total, + self.state.max_total, + )?; + + let mut points: Vec<(&u64, &u64)> = self.state.sites.iter().collect(); + points.sort_by_key(|(_, size)| *size); + for (i, (site, size)) in points.iter().rev().enumerate() { + draw_bar( + ctx, + &v, + false, + (3 + i) as u64, + self.state.symbol_cache.format_symbol(**site), + **size, + self.state.total, + )?; + } + Some(()) + } +} + +struct FlameRenderer<'r> { + state: &'r mut State, + now: Duration, +} +#[derive(Clone, Copy)] +struct FlameView { + total_allocated: u64, + bottom: f64, + left: f64, + color: u8, +} +fn draw_flame( + ctx: &mut R, + v: &ViewParams, + fv: &FlameView, + sc: &mut SymbolCache, + t: &TraceTrie, + addr: Option, +) -> Option<()> { + let rect = kurbo::Rect { + x0: v.margin + fv.left, + y0: v.margin + fv.bottom - v.bar_height, + x1: v.margin + fv.left + (t.value as f64) * v.width / (fv.total_allocated as f64), + y1: v.margin + fv.bottom, + }; + ctx.fill(rect, &Color::rgb8(255, 0, fv.color)); + if let Some(addr) = addr { + ctx.save().ok()?; + ctx.clip(rect); + let layout = ctx + .text() + .new_text_layout(sc.format_symbol(addr)) + .default_attribute(piet_common::TextAttribute::FontSize(9.0)) + .build() + .ok()?; + ctx.draw_text( + &layout, + kurbo::Point { + x: v.margin + fv.left, + y: v.margin + fv.bottom - v.bar_height, + }, + ); + ctx.restore().ok()?; + } + let mut child_fv = FlameView { + total_allocated: fv.total_allocated, + bottom: fv.bottom - v.bar_height, + left: fv.left, + color: fv.color, + }; + for (addr, child) in &t.children { + draw_flame(ctx, v, &child_fv, sc, child, Some(*addr))?; + child_fv.left += (child.value as f64) * v.width / (fv.total_allocated as f64); + child_fv.color = child_fv.color.wrapping_add(85); + } + Some(()) +} +impl<'a, 's> RenderWrapper for FlameRenderer<'_> { + fn render(&mut self, ctx: &mut R, wd: u64, ht: u64) -> Option<()> { + let mut v = ViewParams::new(ctx, ht, wd); + v.bar_start = v.width / 8.0; + draw_bg(ctx, &v); + draw_bar( + ctx, + &v, + true, + 0, + "Execution time".to_string(), + self.now.as_micros() as u64, + self.state.max_duration.as_micros() as u64, + )?; + draw_bar( + ctx, + &v, + true, + 1, + "Total memory consumption".to_string(), + self.state.total, + self.state.max_total, + )?; + + let fv = FlameView { + total_allocated: self.state.total, + bottom: v.height, + left: 0.0, + color: 0, + }; + draw_flame( + ctx, + &v, + &fv, + &mut self.state.symbol_cache, + &self.state.traces, + None, + )?; + Some(()) + } +} + +struct RenderState<'a> { + device: &'a mut piet_common::Device, + bar_out: std::process::ChildStdin, + flame_out: std::process::ChildStdin, +} +fn render_state(state: &mut State, rs: &mut RenderState, now: Duration) -> Option<()> { + let late_enough = state.start_time.map(|t| now >= t).unwrap_or(true); + let early_enough = state.end_time.map(|t| now <= t).unwrap_or(true); + if late_enough && early_enough { + render_bitmap(&mut rs.bar_out, rs.device, BarRenderer { state, now })?; + render_bitmap(&mut rs.flame_out, rs.device, FlameRenderer { state, now })?; + } + Some(()) +} + +fn render_ident( + _state: &mut State, + _rs: &mut RenderState, + _now: Duration, + _hash: blake3::Hash, +) -> Option<()> { + Some(()) +} + +fn render_unwind( + _state: &mut State, + _rs: &mut RenderState, + _now: Duration, + _trace: Rc<[u64]>, +) -> Option<()> { + Some(()) +} + +fn render_alloc( + state: &mut State, + rs: &mut RenderState, + now: Duration, + _ptr: u64, + amt: u64, + trace: Rc<[u64]>, +) -> Option<()> { + for frame in trace.as_ref() { + *state.sites.entry(*frame).or_insert(0) += amt; + } + state.traces.apply_path(trace.iter().rev(), |t| *t += amt); + render_state(state, rs, now)?; + Some(()) +} + +fn render_free( + state: &mut State, + rs: &mut RenderState, + now: Duration, + ptr: u64, + _amt: u64, + _trace: Rc<[u64]>, +) -> Option<()> { + let (amt, trace) = state + .allocs + .get(&ptr) + .expect("free of un-allocated address"); + for frame in trace.as_ref() { + *state + .sites + .get_mut(frame) + .expect("free of un-allocated site") -= amt; + } + state.traces.apply_path(trace.iter().rev(), |t| *t -= amt); + render_state(state, rs, now)?; + Some(()) +} + +fn read_file( + state: &mut State, + mut handle_state: S, + handle_ident: I, + handle_unwind: U, + handle_alloc: A, + handle_free: F, +) -> Option<()> +where + I: Fn(&mut State, &mut S, Duration, blake3::Hash) -> Option<()>, + U: Fn(&mut State, &mut S, Duration, Rc<[u64]>) -> Option<()>, + A: Fn(&mut State, &mut S, Duration, u64, u64, Rc<[u64]>) -> Option<()>, + F: Fn(&mut State, &mut S, Duration, u64, u64, Rc<[u64]>) -> Option<()>, +{ + loop { + let time = match read_u128(&mut state.inf) { + Ok(t) => t, + Err(e) => { + if e.kind() == std::io::ErrorKind::UnexpectedEof { + break; + } else { + return None; + } + } + }; + let now = Duration::from_micros(time.try_into().expect("duration too large for u64")); + state.max_duration = std::cmp::max(state.max_duration, now); + state.num_durations += 1; + + let frame_id = read_u64(&mut state.inf)?; + + if frame_id == 0 { + let hash = read_blake3_hash(&mut state.inf)?; + handle_ident(state, &mut handle_state, now, hash)?; + } else if frame_id == 1 { + let trace: Rc<[u64]> = read_u64_vec(&mut state.inf)?.into(); + handle_unwind(state, &mut handle_state, now, trace)?; + } else if frame_id == 2 { + let ptr = read_u64(&mut state.inf)?; + let amt = read_u64(&mut state.inf)?; + let trace: Rc<[u64]> = read_u64_vec(&mut state.inf)?.into(); + state.allocs.insert(ptr, (amt, trace.clone())); + state.total += amt; + if state.total > state.max_total { + state.max_total = state.total; + } + handle_alloc(state, &mut handle_state, now, ptr, amt, trace)?; + } else if frame_id == 3 { + let ptr = read_u64(&mut state.inf)?; + let _ = read_u64_vec(&mut state.inf)?; + let amt_trace = state + .allocs + .get(&ptr) + .expect("free of un-allocated address"); + let amt = amt_trace.0; + let trace = amt_trace.1.clone(); + state.total -= amt; + handle_free(state, &mut handle_state, now, ptr, amt, trace)?; + } else { + return None; + } + } + Some(()) +} + +fn mkv_for(out_dir: &PathBuf, vis: Visualisation, start: Duration) -> PathBuf { + out_dir.join(format!("{:08}.{}.mkv", start.as_micros(), vis)) +} +fn ffmpeg_for( + out_dir: &PathBuf, + vis: Visualisation, + start: Duration, +) -> Option { + let out = std::fs::File::create(out_dir.join(format!("{:08}.{}.out", start.as_micros(), vis))) + .ok()?; + let err = std::fs::File::create(out_dir.join(format!("{:08}.{}.err", start.as_micros(), vis))) + .ok()?; + let mkv = mkv_for(out_dir, vis, start); + let _ = std::fs::remove_file(&mkv); + std::process::Command::new("ffmpeg") + .args([ + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + "-framerate", + "60", + "-video_size", + "1920x1080", + "-i", + "-", + "-c:v", + "libvpx-vp9", + "-crf", + "15", + "-b:v", + "0", + ]) + .arg(mkv) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::from(out)) + .stderr(std::process::Stdio::from(err)) + .spawn() + .ok() +} + +fn spawn_render_thread( + state: &mut State, + exe_file_name: String, + in_file_name: String, + interval: (Duration, Duration), +) -> std::thread::JoinHandle> { + let out_dir = state.out_dir.clone(); + let max_total = state.max_total; + let max_duration = state.max_duration; + std::thread::spawn(move || { + let out_dir = out_dir?; + eprintln!( + "> {:08} -- {:08}", + interval.0.as_micros(), + interval.1.as_micros() + ); + let loader = addr2line::Loader::new(exe_file_name).ok()?; + let inf = File::open(in_file_name).expect("could not open dump file"); + let mut bar_ffmpeg = ffmpeg_for(&out_dir, Visualisation::Bar, interval.0)?; + let mut flame_ffmpeg = ffmpeg_for(&out_dir, Visualisation::Flame, interval.0)?; + let mut job_state = State { + inf, + symbol_cache: SymbolCache { + loader, + symbol_cache: HashMap::new(), + }, + start_time: Some(interval.0), + end_time: Some(interval.1), + out_dir: Some(out_dir), + allocs: HashMap::new(), + sites: HashMap::new(), + traces: TraceTrie::new(), + total: 0, + max_total, + max_duration, + num_durations: 0, + }; + /* plot each individual frame */ + let mut device = piet_common::Device::new().expect("could not create Piet device"); + let rs = RenderState { + device: &mut device, + bar_out: bar_ffmpeg.stdin.take().expect("bar ffmpeg stdin"), + flame_out: flame_ffmpeg.stdin.take().expect("flame ffmpeg stdin"), + }; + read_file( + &mut job_state, + rs, + render_ident, + render_unwind, + render_alloc, + render_free, + )?; + bar_ffmpeg.wait().ok()?; + flame_ffmpeg.wait().ok()?; + Some(()) + }) +} + +fn main() { + let args: Vec = env::args().collect(); + let is_list = args.len() == 4 && args[3] == "list_frames"; + let is_plot = args.len() == 6 && args[3] == "plot_mem"; + if !is_list && !is_plot { + eprintln!("usage: {} list_frames", args[0]); + eprintln!( + "usage: {} plot_mem ", + args[0] + ); + return; + } + let Ok(loader) = addr2line::Loader::new(&args[1]) else { + eprintln!("could not load guest binary {}", args[1]); + return; + }; + let inf = File::open(args[2].clone()).expect("could not open trace file"); + let state = State { + inf, + symbol_cache: SymbolCache { + loader, + symbol_cache: HashMap::new(), + }, + start_time: None, + end_time: None, + out_dir: None, + allocs: HashMap::new(), + sites: HashMap::new(), + traces: TraceTrie::new(), + total: 0, + max_total: 0, + max_duration: Duration::ZERO, + num_durations: 0, + }; + if is_list { + dump_trace(state); + } else if is_plot { + plot_mem(args, state); + } +} + +fn dump_trace(mut state: State) { + read_file( + &mut state, + (), + dump_ident, + dump_unwind, + dump_alloc, + dump_free, + ); +} + +fn plot_mem(args: Vec, mut state: State) { + let out_dir = PathBuf::from(args[4].clone()); + state.out_dir = Some(out_dir.clone()); + std::fs::create_dir_all(&out_dir).expect("could not create output dir"); + + /* first pass: compute the maximum memory usage */ + match read_file( + &mut state, + (), + |_, _, _, _| Some(()), + |_, _, _, _| Some(()), + |_, _, _, _, _, _| Some(()), + |_, _, _, _, _, _| Some(()), + ) { + Some(()) => (), + None => { + eprintln!("i/o error encountered"); + } + } + eprintln!("max total memory used is {}", state.max_total); + state + .inf + .seek(SeekFrom::Start(0)) + .expect("couldn't seek back"); + state.allocs = HashMap::new(); + state.total = 0; + + /* second pass: compute fair durations so that each parallel job + * processes the same number of frames */ + let num_segments = str::parse::(&args[5]).expect("number of segments must be a number"); + let durations_per_segment = (state.num_durations - 1) / num_segments + 1; + state.num_durations = 0; + let jobs = Mutex::new(Vec::new()); + let start_duration = Mutex::new(Duration::ZERO); + let count_frame = |s: &mut State, _: &mut (), n: Duration, _, _, _| { + if s.num_durations == 1 { + *start_duration.lock().unwrap() = n; + } + if s.num_durations == durations_per_segment { + (*jobs.lock().unwrap()).push((*start_duration.lock().unwrap(), n)); + s.num_durations = 0; + } + Some(()) + }; + read_file( + &mut state, + (), + |_, _, _, _| Some(()), + |_, _, _, _| Some(()), + count_frame, + count_frame, + ); + if state.num_durations > 0 { + (*jobs.lock().unwrap()).push((*start_duration.lock().unwrap(), state.max_duration)); + } + + /* third pass: render in parallel */ + let mut handles = Vec::new(); + for job in &*jobs.lock().unwrap() { + handles.push(spawn_render_thread( + &mut state, + args[1].clone(), + args[2].clone(), + *job, + )); + } + for handle in handles { + handle.join().expect("thread died"); + } + + /* merge all the parallel rendered segments */ + let mut merge_bar = std::process::Command::new("mkvmerge"); + merge_bar.arg("-o").arg(out_dir.join("bar.mkv")); + let mut merge_flame = std::process::Command::new("mkvmerge"); + merge_flame.arg("-o").arg(out_dir.join("flame.mkv")); + for (n, job) in (*jobs.lock().unwrap()).iter().enumerate() { + if n > 0 { + merge_bar.arg("+"); + merge_flame.arg("+"); + } + merge_bar.arg(mkv_for(&out_dir, Visualisation::Bar, job.0)); + merge_flame.arg(mkv_for(&out_dir, Visualisation::Flame, job.0)); + } + merge_bar.status().unwrap(); + merge_flame.status().unwrap(); +} diff --git a/typos.toml b/typos.toml index bdf1e81f2..07ffa9dee 100644 --- a/typos.toml +++ b/typos.toml @@ -7,3 +7,5 @@ extend-exclude = ["**/*.patch", "src/hyperlight_guest_bin/third_party/**/*", "NO [default.extend-words] # typ is used for field name as type is a reserved keyword typ="typ" +mmaped="mmapped" +fpr="fpr"