diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..2c48305b7e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + groups: + github-actions: + patterns: ["*"] + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb18c0e0e4..705dc01fd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,14 @@ jobs: RUSTDOCFLAGS: -Dwarnings steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - name: Set up Rust run: | rustup toolchain install $(awk -F'"' '/channel/{print $2}' rust-toolchain.toml) --profile minimal --no-self-update --component rustfmt,clippy --target x86_64-unknown-linux-gnu - name: Set up Nextest - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 with: tool: nextest@${{ env.NEXTEST_VERSION }} - name: Install iperf3 @@ -49,16 +49,16 @@ jobs: - name: Install AArch64 binutils run: | sudo apt install -y binutils-aarch64-linux-gnu - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Cache custom out directories - uses: actions/cache@v5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | target/*/build/litebox_runner_linux_userland-*/out key: custom-out-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/litebox_syscall_rewriter/**/*.rs') }} - run: ./.github/tools/github_actions_run_cargo fmt - run: | - ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_optee_on_linux_userland + ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_snp --exclude litebox_runner_optee_on_linux_userland ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features -p litebox_runner_optee_on_linux_userland # We exclude `litebox_runner_lvbs` because it requires a custom target and nightly # features. `build_and_test_lvbs` covers it. @@ -76,7 +76,7 @@ jobs: # aren't included in nextest at the moment. See relevant discussion at # https://github.com/nextest-rs/nextest/issues/16 - name: Build documentation (fail on warnings) - run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items --workspace --exclude litebox_runner_lvbs + run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_snp build_and_test_arm64: name: Build and Test (AArch64) @@ -100,12 +100,12 @@ jobs: -p litebox_runner_linux_userland steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Rust run: | rustup toolchain install $(awk -F'"' '/channel/{print $2}' rust-toolchain.toml) --profile minimal --no-self-update --component rustfmt,clippy - name: Set up Nextest - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 with: tool: nextest@${{ env.NEXTEST_VERSION }} - name: Install iperf3 @@ -114,7 +114,7 @@ jobs: - name: Install diod run: | sudo apt install -y diod - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Cache custom out directories uses: actions/cache@v5 with: @@ -145,7 +145,7 @@ jobs: RUSTFLAGS: -Dwarnings steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # LVBS requires a nightly toolchain because: # 1. It uses a custom target (x86_64_vtl1.json) for bare-metal VTL1 kernel development # 2. The custom target requires `-Z build-std` to build core/alloc from source @@ -165,12 +165,12 @@ jobs: rustup override set ${RUST_CHANNEL} rustup show - name: Set up Nextest - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 with: tool: nextest@${{ env.NEXTEST_VERSION }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Cache custom out directories - uses: actions/cache@v5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | target/*/build/litebox_runner_linux_userland-*/out @@ -218,15 +218,15 @@ jobs: -p litebox_runner_windows_userland steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Rust run: | rustup toolchain install $(awk -F'"' '/channel/{print $2}' rust-toolchain.toml) --profile minimal --no-self-update --component rustfmt,clippy --target x86_64-pc-windows-msvc - name: Set up Nextest - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 with: tool: nextest@${{ env.NEXTEST_VERSION }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - run: cargo clippy --locked --verbose --all-targets --all-features ($env:WINDOWS_CRATES -split ' ') - run: cargo build --locked --verbose ($env:WINDOWS_CRATES -split ' ') - run: cargo nextest run --locked --profile ci ($env:WINDOWS_CRATES -split ' ') @@ -238,16 +238,39 @@ jobs: - name: Build documentation (fail on warnings) run: cargo doc --locked --verbose --no-deps --all-features --document-private-items ($env:WINDOWS_CRATES -split ' ') + build_and_test_snp: + name: Build and Test SNP + runs-on: ubuntu-latest + env: + RUSTFLAGS: -Dwarnings + steps: + - name: Check out repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up Rust + run: | + RUST_CHANNEL=$(awk -F'"' '/channel/{print $2}' litebox_runner_snp/rust-toolchain.toml) + rustup toolchain install ${RUST_CHANNEL} --profile minimal --no-self-update --component rustfmt,clippy --target x86_64-unknown-none + rustup component add rust-src --toolchain ${RUST_CHANNEL}-x86_64-unknown-linux-gnu + rustup default ${RUST_CHANNEL} + rustup override set ${RUST_CHANNEL} + rustup show + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - run: ./.github/tools/github_actions_run_cargo clippy --all-features --target litebox_runner_snp/target.json --manifest-path=litebox_runner_snp/Cargo.toml -Zbuild-std=core,compiler_builtins,alloc + - run: | + ./.github/tools/github_actions_run_cargo build -Zbuild-std=core,compiler_builtins,alloc -Zbuild-std-features=compiler-builtins-mem --manifest-path=litebox_runner_snp/Cargo.toml --target litebox_runner_snp/target.json + - name: Build documentation (fail on warnings) + run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items + confirm_no_std: name: Confirm no_std runs-on: ubuntu-latest steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Rust run: | rustup toolchain install $(awk -F'"' '/channel/{print $2}' rust-toolchain.toml) --profile minimal --no-self-update --target x86_64-unknown-none - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Confirm that we haven't accidentally pulled in std into LiteBox run: | # Essentially, we run a build on a target that simply does NOT have @@ -333,10 +356,11 @@ jobs: # access since it needs to actually access the file-system, pull in # relevant files, and then actually trigger LiteBox itself. # - # - `litebox_shim_optee` is expected to work with - # `litebox_platform_lvbs` (`no_std`) and - # `litebox_platform_linux_userland` (for debugging) which - # depends on `litebox_platform_multiplex`. + # - `litebox_shim_optee` is `no_std`, but the builtin + # `x86_64-unknown-none` target is soft-float, which its crypto + # dependencies cannot lower. The shipped target, + # `litebox_runner_lvbs/x86_64_vtl1.json`, satisfies that + # requirement. # # - `litebox_shim_windows` itself is `no_std` but depends on # `litebox_platform_multiplex`. @@ -351,6 +375,9 @@ jobs: # - `litebox_runner_snp` is temporarily disabled until SNP networking # can use a kernel-mode broker # + # - `litebox_service_heki` is excluded for the same reason as + # `litebox_shim_optee` above; it also pulls in crypto dependencies. + # # - `dev_tests` is meant to only be used for tests, and thus can # safely use std. # @@ -379,6 +406,7 @@ jobs: -not -path './litebox_syscall_rewriter/Cargo.toml' \ -not -path './litebox_packager/Cargo.toml' \ -not -path './litebox_runner_snp/Cargo.toml' \ + -not -path './litebox_service_heki/Cargo.toml' \ -not -path './dev_tests/Cargo.toml' \ -not -path './dev_bench/Cargo.toml' \ -print0 | \ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f8d369aca1..87f992bfbe 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -27,7 +27,7 @@ jobs: # If you do not check out your code, Copilot will do this for you. steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Rust run: | rustup toolchain install $(awk -F'"' '/channel/{print $2}' rust-toolchain.toml) --profile minimal --no-self-update --component rustfmt,clippy diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml index 6ee599f588..82bb6d31ff 100644 --- a/.github/workflows/semver-checks.yml +++ b/.github/workflows/semver-checks.yml @@ -8,6 +8,11 @@ on: - synchronize - reopened - edited + issue_comment: + types: + - created + - edited + - deleted merge_group: # If a new commit is pushed to the branch before ongoing runs finish, cancel the ongoing runs @@ -24,12 +29,63 @@ env: CARGO_TERM_COLOR: always jobs: + rerun_fork_check: + name: Re-run fork semver check + if: >- + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + (github.event.action == 'deleted' || + contains(github.event.comment.body, ':robot: SemverChecks :robot:')) + permissions: + actions: write + contents: read + pull-requests: read + issues: read + runs-on: ubuntu-latest + steps: + - name: Re-run the pull request check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + API_URL: ${{ github.api_url }} + REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + set -euo pipefail + PR=$(curl --fail-with-body -sS \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "$API_URL/repos/$REPOSITORY/pulls/$PR_NUMBER") + IS_OPEN=$(jq -r '.state == "open"' <<<"$PR") + IS_FORK=$(jq -r '.head.repo.fork == true' <<<"$PR") + COMMENTER_IS_AUTHOR=$(jq -r --arg login "${{ github.event.comment.user.login }}" '.user.login == $login' <<<"$PR") + COMMENTER_IS_MAINTAINER=$(case '${{ github.event.comment.author_association }}' in OWNER|MEMBER|COLLABORATOR) echo true;; *) echo false;; esac) + if [ "$IS_OPEN" != true ] || [ "$IS_FORK" != true ] || \ + { [ "$COMMENTER_IS_AUTHOR" != true ] && [ "$COMMENTER_IS_MAINTAINER" != true ]; }; then + echo 'Ignoring comment from an unauthorized user or on a non-open fork PR.' + exit 0 + fi + HEAD_SHA=$(jq -r '.head.sha' <<<"$PR") + RUN_ID=$(curl --fail-with-body -sS \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "$API_URL/repos/$REPOSITORY/actions/workflows/semver-checks.yml/runs?event=pull_request&head_sha=$HEAD_SHA&per_page=10" | \ + jq -r --argjson pr_number "$PR_NUMBER" '[.workflow_runs[] | select(any(.pull_requests[]?; .number == $pr_number))] | first | .id // empty') + if [ -z "$RUN_ID" ]; then + echo 'No pull_request semver-checks run exists for this commit yet.' >&2 + exit 1 + fi + curl --fail-with-body -sS -X POST \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "$API_URL/repos/$REPOSITORY/actions/runs/$RUN_ID/rerun" + semver_checks: name: Check SemVer Correctness + if: ${{ github.event_name != 'issue_comment' }} runs-on: ubuntu-latest steps: - name: Check out repo - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Determine baseline ref id: baseline run: | @@ -101,7 +157,7 @@ jobs: # https://api.github.com/repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/issues/${{ github.event.number }}/reactions \ # -d '{"content":"${{ steps.semver_check.outputs.reaction }}"}' - name: Delete old semver checks comments, if any - if: github.event_name == 'pull_request' + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} run: | # Get the old comments COMMENT_IDS=$(curl -L \ @@ -120,7 +176,7 @@ jobs: https://api.github.com/repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/issues/comments/$ID done - name: Add a new issue comment if needed - if: github.event_name == 'pull_request' + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} run: | BASE_NOTE="" if [ "${{ steps.baseline.outputs.ref }}" != "main" ] && [ "${{ steps.baseline.outputs.ref }}" != "ulitebox" ]; then @@ -141,3 +197,44 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/issues/${{ github.event.number }}/comments \ -d "$(printf '%s' "$BODY" | jq -sR '{body: .}')" + - name: Require documentation from fork + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} + env: + API_URL: ${{ github.api_url }} + REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + BASE_NOTE="" + if [ "${{ steps.baseline.outputs.ref }}" != "main" ] && [ "${{ steps.baseline.outputs.ref }}" != "ulitebox" ]; then + BASE_NOTE=":information_source: **Note:** This semver check was run against the \`${{ steps.baseline.outputs.ref }}\` branch, not \`main\` or \`ulitebox\`.\n\n" + fi + if [ "${{ steps.packages.outputs.has_packages }}" != "true" ]; then + BODY="$(echo -e "${BASE_NOTE}"':robot: SemverChecks :robot: No semver-relevant crate changes detected; skipped cargo-semver-checks.')" + elif [ -s /tmp/semver-checks-stdout ]; then + BODY="$(echo -e "${BASE_NOTE}"':robot: SemverChecks :robot: :warning: Potential breaking API changes detected :warning:\n\n
Click for details\n\n```'"$(cat /tmp/semver-checks-stdout)"'\n```\n
')" + else + BODY="$(echo -e "${BASE_NOTE}"':robot: SemverChecks :robot: No breaking API changes detected\n\nNote: this does not mean API is unchanged, or even that there are no breaking changes; simply, none of the detections triggered.')" + fi + PREFIX='Documenting semver-checks CI text here:' + COMMENTS=$(curl --fail-with-body -sS \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H 'Accept: application/vnd.github+json' \ + "$API_URL/repos/$REPOSITORY/issues/$PR_NUMBER/comments?per_page=100&sort=created&direction=desc") + HAS_SEMVER_COMMENT=$(jq -r '[.[] | select(.body | contains(":robot: SemverChecks :robot:"))] | length > 0' <<<"$COMMENTS") + if [ "$HAS_SEMVER_COMMENT" = "false" ] && [ ! -s /tmp/semver-checks-stdout ]; then + echo 'No breaking changes and no prior semver-checks comment; documentation is unnecessary.' + exit 0 + fi + LAST=$(jq -r --arg prefix "$PREFIX" '[.[] | select(.body | startswith($prefix))] | last | if . then .body else "" end' <<<"$COMMENTS") + EXPECTED=$(printf '%s\n\n%s' "$PREFIX" "$BODY") + if [ "$LAST" != "$EXPECTED" ]; then + echo 'The latest semver documentation comment is missing or out of date.' >&2 + { + printf '## Semver-checks documentation required\n\n' + printf 'Copy and paste the following as a comment on the PR (not in the PR description):\n\n' + printf '````markdown\n%s\n````\n' "$EXPECTED" + } >> "$GITHUB_STEP_SUMMARY" + printf '\nCopy and paste this as a comment on the PR (not in the PR description):\n\n```markdown\n%s\n```\n' "$EXPECTED" + exit 1 + fi diff --git a/Cargo.lock b/Cargo.lock index b5b1df0879..fc9fa9d5fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -455,6 +461,18 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -609,6 +627,19 @@ dependencies = [ "syn", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "either" version = "1.15.0" @@ -621,6 +652,24 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -703,6 +752,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "filetime" version = "0.2.27" @@ -855,6 +914,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -916,6 +976,17 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1619,12 +1690,8 @@ version = "0.1.0" dependencies = [ "aligned-vec", "arrayvec", - "authenticode", "bitflags", - "cms", - "const-oid", "digest", - "elf", "hashbrown", "libc", "litebox", @@ -1633,15 +1700,12 @@ dependencies = [ "litebox_util_log", "modular-bitfield", "num_enum", - "object", "once_cell", "rangemap", "raw-cpuid", - "rsa", "sha2", "spin 0.10.0", "thiserror", - "x509-cert", "x86_64", "zerocopy", "zeroize", @@ -1726,6 +1790,7 @@ dependencies = [ "litebox_common_optee", "litebox_platform_lvbs", "litebox_platform_multiplex", + "litebox_service_heki", "litebox_shim_optee", "litebox_util_log", "log", @@ -1755,6 +1820,20 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "litebox_runner_snp" +version = "0.1.0" +dependencies = [ + "arrayvec", + "litebox", + "litebox_common_linux", + "litebox_platform_linux_kernel", + "litebox_shim_linux", + "litebox_util_log", + "log", + "once_cell", +] + [[package]] name = "litebox_runner_windows_on_linux_userland" version = "0.1.0" @@ -1789,6 +1868,32 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "litebox_service_heki" +version = "0.1.0" +dependencies = [ + "authenticode", + "cms", + "const-oid", + "digest", + "elf", + "hashbrown", + "litebox", + "litebox_common_linux", + "litebox_common_lvbs", + "log", + "object", + "once_cell", + "rangemap", + "rsa", + "sha2", + "spin 0.10.0", + "thiserror", + "x509-cert", + "x86_64", + "zerocopy", +] + [[package]] name = "litebox_shim_linux" version = "0.1.0" @@ -1834,6 +1939,8 @@ dependencies = [ "litebox_util_log", "num_enum", "once_cell", + "p384", + "rangemap", "sha2", "spin 0.10.0", "thiserror", @@ -2224,6 +2331,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "paste" version = "1.0.15" @@ -2347,6 +2466,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -2531,6 +2659,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ringbuf" version = "0.4.8" @@ -2625,6 +2763,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "seccompiler" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 270d544a75..fd0bb03a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,11 @@ members = [ "litebox_runner_windows_on_linux_userland", "litebox_runner_windows_userland", "litebox_runner_lvbs", + "litebox_runner_snp", "litebox_runner_optee_on_linux_userland", "litebox_shim_linux", "litebox_shim_windows", + "litebox_service_heki", "litebox_syscall_rewriter", "litebox_packager", "litebox_util_log", @@ -68,6 +70,7 @@ default-members = [ "litebox_shim_linux", "litebox_shim_windows", "litebox_shim_optee", + "litebox_service_heki", "litebox_syscall_rewriter", "litebox_packager", "litebox_util_log", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 87d578c5ff..084bacb3c9 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -39,13 +39,13 @@ fn ratchet_globals() -> Result<()> { ("litebox/", 9), ("litebox_platform_linux_kernel/", 5), ("litebox_platform_linux_userland/", 5), - ("litebox_platform_lvbs/", 23), + ("litebox_platform_lvbs/", 22), ("litebox_platform_multiplex/", 1), ("litebox_platform_windows_userland/", 8), - ("litebox_runner_lvbs/", 5), + ("litebox_runner_lvbs/", 6), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 2), - ("litebox_shim_optee/", 4), + ("litebox_shim_optee/", 6), ("litebox_shim_windows/", 1), ("litebox_runner_windows_userland/", 2), ], diff --git a/litebox/src/mm/allocator.rs b/litebox/src/mm/allocator.rs index 83026d52ba..64c84b1d1c 100644 --- a/litebox/src/mm/allocator.rs +++ b/litebox/src/mm/allocator.rs @@ -162,14 +162,18 @@ unsafe impl GlobalAlloc Self::BASE_PAGE_SIZE => { // Best to use the underlying backend directly to allocate pages // to avoid fragmentation - self.allocate_pages(Self::BASE_PAGE_SIZE_ORDER) - .expect("allocate page") + let Some(ptr) = self.allocate_pages(Self::BASE_PAGE_SIZE_ORDER) else { + return core::ptr::null_mut(); + }; + ptr } Self::LARGE_PAGE_SIZE => { // Best to use the underlying backend directly to allocate large pages // to avoid fragmentation - self.allocate_pages(Self::LARGE_PAGE_SIZE_ORDER) - .expect("allocate large page") + let Some(ptr) = self.allocate_pages(Self::LARGE_PAGE_SIZE_ORDER) else { + return core::ptr::null_mut(); + }; + ptr } 0..=ZoneAllocator::MAX_ALLOC_SIZE => { let mut zone_allocator = self.slab_allocator.lock(); diff --git a/litebox/src/platform/common_providers/userspace_pointers.rs b/litebox/src/platform/common_providers/userspace_pointers.rs index 25b61c1296..3dbef3cfc2 100644 --- a/litebox/src/platform/common_providers/userspace_pointers.rs +++ b/litebox/src/platform/common_providers/userspace_pointers.rs @@ -110,6 +110,27 @@ impl UserConstPtr { } } + /// Copy data from userspace to a raw pointer. + /// + /// # Safety + /// + /// `dst` must be either non-Rust memory or Rust memory with exclusive access. + pub unsafe fn copy_to_raw(self, dst: *mut T, len: usize) -> Option<()> + where + T: FromBytes, + { + if len == 0 { + return Some(()); + } + let byte_len = len.checked_mul(core::mem::size_of::())?; + self.inner.checked_add(byte_len)?; + let src = + V::validate_slice(core::ptr::slice_from_raw_parts(self.as_ptr(), len).cast_mut())? + .cast_const(); + V::with_user_memory_access(|| unsafe { memcpy_fallible(dst.cast(), src.cast(), byte_len) }) + .ok() + } + /// Explicitly-private function. This particular function exists because we /// store the `*const T` that would be stored in this struct instead as a /// `usize`. We store `inner` as a `usize` to support @@ -257,6 +278,25 @@ impl UserMutPtr { } } + /// Copy data from a raw pointer to userspace. + /// + /// # Safety + /// + /// `src` must be either non-Rust memory or Rust memory without concurrent modification. + pub unsafe fn copy_from_raw(self, src: *const T, len: usize) -> Option<()> + where + T: FromBytes + IntoBytes, + { + if len == 0 { + return Some(()); + } + let byte_len = len.checked_mul(core::mem::size_of::())?; + self.inner.checked_add(byte_len)?; + let dst = V::validate_slice(core::ptr::slice_from_raw_parts_mut(self.as_ptr(), len))?; + V::with_user_memory_access(|| unsafe { memcpy_fallible(dst.cast(), src.cast(), byte_len) }) + .ok() + } + /// Explicitly-private function. See equivalent [`UserConstPtr::as_ptr`] /// for more details. fn as_ptr(&self) -> *mut T { diff --git a/litebox_common_linux/Cargo.toml b/litebox_common_linux/Cargo.toml index b2061b8f5e..1606bad73b 100644 --- a/litebox_common_linux/Cargo.toml +++ b/litebox_common_linux/Cargo.toml @@ -11,7 +11,7 @@ litebox = { path = "../litebox/", version = "0.1.0" } thiserror = { version = "2.0.6", default-features = false } int-enum = "1.2.0" syscalls = { version = "0.6", default-features = false } -zerocopy = { version = "0.8", features = ["derive"] } +zerocopy = { version = "0.8", features = ["derive", "alloc"] } [lints] workspace = true diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 056a22b4ef..382a132a9d 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -226,6 +226,20 @@ impl ElfParsedFile { self.trampoline.is_some() } + /// The pages the trampoline occupies when this ELF is loaded at `base_addr`; + /// a zero `base_addr` yields the load-address-relative range. + /// + /// `None` if the binary has no trampoline or, like [`Self::has_trampoline`], + /// if [`Self::parse_trampoline`] has not run yet. + pub fn trampoline_page_range(&self, base_addr: usize) -> Option> { + let trampoline = self.trampoline.as_ref()?; + let start = base_addr.checked_add(trampoline.vaddr)?; + let end = start + .checked_add(trampoline.size)? + .checked_next_multiple_of(PAGE_SIZE)?; + Some(start..end) + } + /// Parse the LiteBox trampoline data, if any. /// /// The trampoline header is located at the end of the file (last 32/20 bytes). @@ -323,6 +337,16 @@ impl ElfParsedFile { return Err(ElfParseError::BadTrampoline); } + // Reject a vaddr whose range cannot be represented, so that later + // address arithmetic cannot wrap. + if vaddr + .checked_add(trampoline_size) + .and_then(|end| end.checked_next_multiple_of(PAGE_SIZE)) + .is_none() + { + return Err(ElfParseError::BadTrampoline); + } + self.trampoline = Some(TrampolineInfo { vaddr, size: trampoline_size, diff --git a/litebox_common_linux/src/physical_pointers.rs b/litebox_common_linux/src/physical_pointers.rs index 1bde38e6e5..a162cf7307 100644 --- a/litebox_common_linux/src/physical_pointers.rs +++ b/litebox_common_linux/src/physical_pointers.rs @@ -38,34 +38,15 @@ use crate::vmap::{ VmapManager, }; use core::marker::PhantomData; +use litebox::platform::common_providers::userspace_pointers::{ + UserConstPtr, UserMutPtr, ValidateAccess, +}; use zerocopy::{FromBytes, IntoBytes}; /// The concrete [`PhysPageMapInfo`] produced by the `VmapManager` behind a [`GlobalVmapManager`]. type MapInfoOf = <>::Manager as VmapManager>::MapInfo; -/// Allocate a zeroed `Box` on the heap. -/// -/// # Panics -/// -/// Panics if `T` is a zero-sized type, since `alloc_zeroed` with a zero-sized -/// layout is undefined behavior. -fn box_new_zeroed() -> alloc::boxed::Box { - assert!( - core::mem::size_of::() > 0, - "box_new_zeroed does not support zero-sized types" - ); - let layout = core::alloc::Layout::new::(); - // Safety: layout has a non-zero size and correct alignment for T. - let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }.cast::(); - if ptr.is_null() { - alloc::alloc::handle_alloc_error(layout); - } - // Safety: ptr is a valid, zeroed, properly aligned heap allocation for T. - // T: FromBytes guarantees all-zero is a valid bit pattern. - unsafe { alloc::boxed::Box::from_raw(ptr) } -} - #[inline] fn align_down(address: usize, align: usize) -> usize { address & !(align - 1) @@ -216,7 +197,13 @@ where core::mem::size_of::(), PhysPageMapPermissions::READ, )?; - let mut boxed = box_new_zeroed::(); + let mut boxed = ::new_box_zeroed().map_err( + |_err: zerocopy::AllocError| { + // zerocopy::AllocError is a ZST and carries no other information we + // could forward + PhysPointerError::AllocError + }, + )?; // SAFETY: `boxed` is a freshly allocated `T` and is thus valid for writes // of `size_of::()` bytes, which is the guard's mapped size. unsafe { guard.copy_out(core::ptr::from_mut::(boxed.as_mut()).cast::())? }; @@ -374,6 +361,47 @@ where } } +impl PhysMutPtr +where + V: GlobalVmapManager, +{ + /// Copy data to non-Rust userspace memory. + pub fn copy_to_user( + &self, + dst: UserMutPtr, + len: usize, + ) -> Result<(), PhysPointerError> { + if len > self.count { + return Err(PhysPointerError::IndexOutOfBounds(len, self.count)); + } + if len == 0 { + return Ok(()); + } + let guard = self.map_and_get_ptr_guard(0, len, PhysPageMapPermissions::READ)?; + guard.copy_to_user(dst) + } + + /// Copy data from non-Rust userspace memory. + pub fn copy_from_user( + &self, + src: UserConstPtr, + len: usize, + ) -> Result<(), PhysPointerError> { + if len > self.count { + return Err(PhysPointerError::IndexOutOfBounds(len, self.count)); + } + if len == 0 { + return Ok(()); + } + let guard = self.map_and_get_ptr_guard( + 0, + len, + PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE, + )?; + guard.copy_from_user(src) + } +} + /// RAII guard that unmaps physical pages when dropped. /// /// Created by `map_and_get_ptr_guard`. Its lifetime is tied to the parent @@ -393,6 +421,24 @@ struct MappedGuard<'a, T, const ALIGN: usize, V: GlobalVmapManager> { } impl> MappedGuard<'_, T, ALIGN, V> { + fn copy_to_user( + &self, + dst: UserMutPtr, + ) -> Result<(), PhysPointerError> { + // SAFETY: `PhysConstPtr`/`PhysMutPtr` only point to non-Rust memory. + unsafe { dst.copy_from_raw(self.ptr.cast::().cast_const(), self.size) } + .ok_or(PhysPointerError::CopyFailed) + } + + fn copy_from_user( + &self, + src: UserConstPtr, + ) -> Result<(), PhysPointerError> { + // SAFETY: `PhysMutPtr` only points to non-Rust memory. + unsafe { src.copy_to_raw(self.ptr.cast::(), self.size) } + .ok_or(PhysPointerError::CopyFailed) + } + /// Copy the `self.size` mapped bytes out into `dst`. /// /// This is the only path through which the raw mapped pointer is dereferenced. @@ -518,6 +564,20 @@ where } } +impl PhysConstPtr +where + V: GlobalVmapManager, +{ + /// Copy data to non-Rust userspace memory. + pub fn copy_to_user( + &self, + dst: UserMutPtr, + len: usize, + ) -> Result<(), PhysPointerError> { + self.inner.copy_to_user(dst, len) + } +} + impl> core::fmt::Debug for PhysConstPtr { diff --git a/litebox_common_linux/src/vmap.rs b/litebox_common_linux/src/vmap.rs index 4218ce6b7b..79cefd3b21 100644 --- a/litebox_common_linux/src/vmap.rs +++ b/litebox_common_linux/src/vmap.rs @@ -250,4 +250,6 @@ pub enum PhysPointerError { VaSpaceExhausted, #[error("Page-table frame allocation failed (out of memory)")] FrameAllocationFailed, + #[error("Rust object allocation failed (out of memory)")] + AllocError, } diff --git a/litebox_common_lvbs/src/lib.rs b/litebox_common_lvbs/src/lib.rs index daf0ef858a..916cd7f33d 100644 --- a/litebox_common_lvbs/src/lib.rs +++ b/litebox_common_lvbs/src/lib.rs @@ -8,14 +8,16 @@ extern crate alloc; +use alloc::vec::Vec; use core::mem; use litebox::utils::TruncateExt; use litebox_common_linux::errno::Errno; +use litebox_common_linux::vmap::PhysPageAddr; use num_enum::{IntoPrimitive, TryFromPrimitive}; use thiserror::Error; use x86_64::{ PhysAddr, VirtAddr, - structures::paging::{PageSize, Size4KiB}, + structures::paging::{PageSize, Size4KiB, frame::PhysFrameRange}, }; use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; @@ -49,6 +51,9 @@ pub const VSM_VTL_CALL_FUNC_ID_ALLOCATE_RINGBUFFER_MEMORY: u32 = 0x1_ffec; // This VSM function ID for setting the platform root key is subject to change pub const VSM_VTL_CALL_FUNC_ID_SET_PLATFORM_ROOT_KEY: u32 = 0x1_ffed; +// This VSM function ID for generating the identity signing key is subject to change +pub const VSM_VTL_CALL_FUNC_ID_GENERATE_IDENTITY_SIGNING_KEY: u32 = 0x1_ffee; + // This VSM function ID for OP-TEE messages is subject to change pub const VSM_VTL_CALL_FUNC_ID_OPTEE_MESSAGE: u32 = 0x1_fff0; @@ -72,6 +77,7 @@ pub enum VsmFunction { OpteeMessage = VSM_VTL_CALL_FUNC_ID_OPTEE_MESSAGE, AllocateRingbufferMemory = VSM_VTL_CALL_FUNC_ID_ALLOCATE_RINGBUFFER_MEMORY, SetPlatformRootKey = VSM_VTL_CALL_FUNC_ID_SET_PLATFORM_ROOT_KEY, + GenerateIdentitySigningKey = VSM_VTL_CALL_FUNC_ID_GENERATE_IDENTITY_SIGNING_KEY, } // `HV_STATUS_*` constants used as discriminants for `HypervCallError`. @@ -152,19 +158,15 @@ impl From for Errno { } /// Errors for Virtual Secure Mode (VSM) operations. +/// +/// TODO: split per layer, so the gates cannot name HEKI policy errors. #[derive(Debug, Error)] #[non_exhaustive] pub enum VsmError { // Boot/AP Initialization Errors - #[error("failed to copy boot signal page from VTL0")] - BootSignalPageCopyFailed, - #[error("failed to initialize AP: {0:?}")] ApInitFailed(HypervCallError), - #[error("failed to copy boot signal page to VTL0")] - BootSignalWriteFailed, - #[error("failed to copy cpu_online_mask from VTL0")] CpuOnlineMaskCopyFailed, @@ -275,9 +277,6 @@ pub enum VsmError { #[error("invalid virtual address")] InvalidVirtualAddress, - #[error("discontiguous memory range")] - DiscontiguousMemoryRange, - // Symbol Table Errors #[error("symbol table data empty")] SymbolTableEmpty, @@ -288,9 +287,6 @@ pub enum VsmError { #[error("symbol table length not aligned to symbol size")] SymbolTableLengthInvalid, - #[error("failed to parse symbol at offset {0:#x}")] - SymbolParseFailed(usize), - #[error("symbol name offset out of bounds")] SymbolNameOffsetInvalid, @@ -317,9 +313,6 @@ impl From for Errno { VsmError::InvalidInputAddress | VsmError::InvalidPhysicalAddress | VsmError::InvalidVirtualAddress - | VsmError::DiscontiguousMemoryRange - | VsmError::BootSignalPageCopyFailed - | VsmError::BootSignalWriteFailed | VsmError::CpuOnlineMaskCopyFailed | VsmError::HekiPagesCopyFailed | VsmError::Vtl0CopyFailed => Errno::EFAULT, @@ -364,7 +357,6 @@ impl From for Errno { | VsmError::KexecImageSegmentsInvalid | VsmError::SymbolTableEmpty | VsmError::SymbolTableLengthInvalid - | VsmError::SymbolParseFailed(_) | VsmError::SymbolNameOffsetInvalid | VsmError::SymbolNameInvalidUtf8 | VsmError::SymbolNameNoTerminator @@ -840,3 +832,171 @@ impl HekiKernelInfo { } } } + +/// The gate through which VTL1 acts on the untrusted VTL0. This is the +/// capability the HEKI service runs on. Every operation here targets +/// VTL0. VTL1's own setup operations live in [`Vtl1Gate`]. +/// +/// The platform owns the protected-frame registry (to deal with TOCTOU and +/// confused deputy) and rejects use of this interface against VTL1 frames and +/// protected VTL0 frames. +pub trait Vtl0Gate { + /// Copy `out.len()` bytes out of VTL0 physical memory, starting at `offset` + /// within the first page of `pages`, into `out`. The pages need not be + /// physically contiguous; use [`Self::read_vtl0_contiguous`] when the source + /// is a single contiguous span. + fn read_vtl0_pages( + &self, + pages: &[PhysPageAddr], + offset: usize, + out: &mut [u8], + ) -> Result<(), VsmError>; + + /// Directly set VTL0 protection on a frame range — no reservation, no + /// rollback. Use when the caller already trusts the frames, or is + /// re-protecting frames the registry already owns. + fn protect_frames( + &self, + range: PhysFrameRange, + attr: MemAttr, + ) -> Result<(), VsmError>; + + /// Release a frame range the registry currently protects, restoring VTL0 + /// read/write access — the standalone inverse of [`Self::protect_frames`]. + fn unprotect_frames(&self, range: PhysFrameRange) -> Result<(), VsmError>; + + /// Run a reserve-then-commit transaction: reserve `initial` (claiming the + /// frames so VTL0 cannot alter them while the caller inspects their contents), + /// run `f` — which reads/checks the frames and protects them via the + /// [`FrameTxn`] handle — then commit on `Ok` or roll back (release every + /// reserved range) on `Err`. Use when protection must be atomic with a check + /// of the frame contents (TOCTOU-safe). + fn protect_frames_transactionally( + &self, + initial: &[PhysFrameRange], + f: &mut dyn FnMut(&mut dyn FrameTxn) -> Result<(), VsmError>, + ) -> Result<(), VsmError>; + + /// Install a VTL0 physical buffer as the platform's log ring buffer. + fn install_ringbuffer(&self, pa: u64, size: u64); + + /// Whether VTL0 has signalled end of boot, i.e., whether VTL1's window of + /// trusting VTL0 has closed. Operations that are only legitimate while VTL0 + /// is still trusted must refuse once this returns `true`. + fn end_of_boot_reached(&self) -> bool; + + /// Lock VTL0's control registers by arming the hypervisor CR/MSR intercepts + /// and snapshotting their current values into VTL1 per-CPU state. + fn lock_control_registers(&self) -> Result<(), VsmError>; + + /// Read `out.len()` bytes from a contiguous VTL0 physical-memory span + /// starting at `phys_addr`, into `out`. The span may cross page boundaries; + /// the covered pages are required to be physically contiguous. Use + /// [`Self::read_vtl0_pages`] when they are not. + fn read_vtl0_contiguous(&self, phys_addr: u64, out: &mut [u8]) -> Result<(), VsmError> { + if out.is_empty() { + return Ok(()); + } + let page_size = PAGE_SIZE as u64; + let start_page = phys_addr & !(page_size - 1); + let offset: usize = (phys_addr - start_page).trunc(); + let end = phys_addr + .checked_add(out.len() as u64) + .ok_or(VsmError::IntegerOverflow)?; + let last_page = (end - 1) & !(page_size - 1); + + let page_count = ((last_page - start_page) / page_size + 1).trunc(); + let mut pages = Vec::with_capacity(page_count); + let mut p = start_page; + loop { + pages.push( + PhysPageAddr::::new(p.trunc()) + .ok_or(VsmError::InvalidPhysicalAddress)?, + ); + if p == last_page { + break; + } + p += page_size; + } + self.read_vtl0_pages(&pages, offset, out) + } + + /// Read a `FromBytes` value out of a contiguous VTL0 physical span starting + /// at `phys_addr`. + fn read_vtl0_val(&self, phys_addr: u64) -> Result { + let mut buf = alloc::vec![0u8; core::mem::size_of::()]; + self.read_vtl0_contiguous(phys_addr, &mut buf)?; + T::read_from_bytes(&buf).map_err(|_| VsmError::Vtl0CopyFailed) + } +} + +/// Authority to write VTL0 memory with the VTL0 protection masks **bypassed**. +/// +/// Deliberately not part of [`Vtl0Gate`]: it is strictly more dangerous than +/// everything there, so it is granted per-operation rather than held ambiently. +/// A holder of [`Vtl0Gate`] alone cannot bypass a protection mask. +/// +/// The primitive trusts its holder and knows nothing about what is being +/// written — whether the destination is legitimate is the grantee's business. +pub trait Vtl0PrivilegedWrite { + /// Copy `bytes` into VTL0 physical memory, starting at `offset` within the + /// first page of `pages`, bypassing VTL0 protection masks. The pages need + /// not be physically contiguous. + fn write_vtl0_pages( + &self, + pages: &[PhysPageAddr], + offset: usize, + bytes: &[u8], + ) -> Result<(), VsmError>; +} + +/// VTL1 setup steps that VTL0 requests over a VTL call. All mutate VTL1/platform +/// state rather than VTL0, so they are consumed by the runner and never by +/// the HEKI service, which is handed only [`Vtl0Gate`]. +/// +/// [`Self::signal_end_of_boot`] is self-protection: it closes VTL1's window of +/// trusting VTL0. The other half of VTL1 self-protection — locking VTL1's own +/// memory away from VTL0 — happens during platform bring-up, before any gate +/// exists, and so is not on this trait. +pub trait Vtl1Gate { + /// Enable VTL1 on the APs named in the VTL0 `cpu_present_mask` page at + /// `cpu_present_mask_pfn`, ahead of [`Self::boot_aps`]. + fn enable_aps_vtl(&self, cpu_present_mask_pfn: u64) -> Result<(), VsmError>; + + /// Bring VTL1 up on every online AP named in the VTL0 `cpu_online_mask` + /// page at `cpu_online_mask_pfn`. + fn boot_aps(&self, cpu_online_mask_pfn: u64) -> Result<(), VsmError>; + + /// Read the platform root key from VTL0 `key_pa` and store it in VTL1 state. + fn set_platform_root_key(&self, key_pa: u64) -> Result<(), VsmError>; + + /// Close VTL1's window of trusting VTL0, making + /// [`Vtl0Gate::end_of_boot_reached`] report `true` from here on. One-way: + /// the window never reopens. + fn signal_end_of_boot(&self); +} + +/// Outcome of reserving a physical frame range within a transaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReservationStatus { + /// The range was newly reserved by this transaction. + New, + /// The range was already owned by the protected-frame registry. + AlreadyOwned, +} + +/// Restricted handle for [`Vtl0Gate::protect_frames_transactionally`]. +/// +/// The only way to reserve/protect frames within a transaction; the concrete +/// reservation guard stays private in the platform. +pub trait FrameTxn { + /// Reserve the given physical frame ranges within this transaction, + /// returning the reservation status of each range. + fn reserve( + &mut self, + ranges: &[PhysFrameRange], + ) -> Result, VsmError>; + + /// Apply the given memory attributes to a reserved physical frame range. + fn protect(&mut self, range: PhysFrameRange, attr: MemAttr) -> Result<(), VsmError>; +} diff --git a/litebox_common_optee/src/lib.rs b/litebox_common_optee/src/lib.rs index 6e9f402bfd..976e5ab26c 100644 --- a/litebox_common_optee/src/lib.rs +++ b/litebox_common_optee/src/lib.rs @@ -582,12 +582,25 @@ impl UteeParams { #[derive(Clone)] pub enum UteeParamOwned { None, - ValueInput { value_a: u64, value_b: u64 }, + ValueInput { + value_a: u64, + value_b: u64, + }, ValueOutput, - ValueInout { value_a: u64, value_b: u64 }, - MemrefInput { data: Box<[u8]> }, - MemrefOutput { buffer_size: usize }, - MemrefInout { data: Box<[u8]>, buffer_size: usize }, + ValueInout { + value_a: u64, + value_b: u64, + }, + MemrefInput { + data: Option>, + }, + MemrefOutput { + buffer_size: usize, + }, + MemrefInout { + data: Option>, + buffer_size: usize, + }, } impl UteeParamOwned { @@ -1153,6 +1166,7 @@ impl From for TeeResult { Errno::EINVAL | Errno::EFAULT => Self::BadParameters, Errno::EPERM | Errno::EACCES => Self::AccessDenied, Errno::ENOMEM => Self::OutOfMemory, + Errno::EEXIST => Self::AccessConflict, Errno::EOVERFLOW => Self::Overflow, Errno::EBUSY => Self::Busy, _ => Self::GenericError, diff --git a/litebox_platform_linux_kernel/src/arch/x86/mm/paging.rs b/litebox_platform_linux_kernel/src/arch/x86/mm/paging.rs index ddc6d3e682..d9ab27f767 100644 --- a/litebox_platform_linux_kernel/src/arch/x86/mm/paging.rs +++ b/litebox_platform_linux_kernel/src/arch/x86/mm/paging.rs @@ -324,9 +324,14 @@ impl PageTableImpl for X64PageTabl let mut allocator = PageTableAllocator::::new(); // TODO: if it is file-backed, we need to read the page from file let frame = PageTableAllocator::::allocate_frame(true).unwrap(); + // ACCESSED and DIRTY are pre-set here (mirroring the Linux kernel's + // `_KERNPG_TABLE`) so the CPU's page-table walker doesn't need an + // atomic read-modify-write on this entry the first time it's traversed. let table_flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE - | PageTableFlags::USER_ACCESSIBLE; + | PageTableFlags::USER_ACCESSIBLE + | PageTableFlags::ACCESSED + | PageTableFlags::DIRTY; match unsafe { inner.map_to_with_table_flags( page, diff --git a/litebox_platform_lvbs/Cargo.toml b/litebox_platform_lvbs/Cargo.toml index b4f81aed23..07b85bf876 100644 --- a/litebox_platform_lvbs/Cargo.toml +++ b/litebox_platform_lvbs/Cargo.toml @@ -22,14 +22,7 @@ num_enum = { version = "0.7.3", default-features = false } once_cell = { version = "1.20.2", default-features = false, features = ["alloc", "race"] } modular-bitfield = { version = "0.12.0", default-features = false } hashbrown = "0.15.2" -elf = { version = "0.8.0", default-features = false } -cms = { version = "0.2.3", default-features = false, features = ["alloc"] } -rsa = { version = "0.9.10", default-features = false } sha2 = { version = "0.10.9", default-features = false, features = ["oid"] } -x509-cert = { version = "0.2.5", default-features = false } -const-oid = { version = "0.9.6", default-features = false, features = ["db"] } -authenticode = { version = "0.4.3", default-features = false, features = ["object"] } -object = { version = "0.36.7", default-features = false, features = ["pe"] } digest = { version = "0.10.7", default-features = false } aligned-vec = { version = "0.6.4", default-features = false } raw-cpuid = "11.6.0" diff --git a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs index 165f29584d..84686c777e 100644 --- a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs +++ b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs @@ -609,7 +609,14 @@ impl X64PageTable<'_, M, ALIGN> { flags }; // Parent entries use a stable permissive constant, not leaf-derived flags. - let table_flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; + // + // ACCESSED and DIRTY are pre-set here (mirroring the Linux kernel's + // `_KERNPG_TABLE`) so the CPU's page-table walker doesn't need an + // atomic read-modify-write on this entry the first time it's traversed. + let table_flags = PageTableFlags::PRESENT + | PageTableFlags::WRITABLE + | PageTableFlags::ACCESSED + | PageTableFlags::DIRTY; match unsafe { inner.map_to_with_table_flags( @@ -668,7 +675,13 @@ impl X64PageTable<'_, M, ALIGN> { .map_err(|_| MapToError::FrameAllocationFailed)?; let end_page = start_page + frames.len() as u64; - let table_flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; + // ACCESSED and DIRTY are pre-set here (mirroring the Linux kernel's + // `_KERNPG_TABLE`) so the CPU's page-table walker doesn't need an + // atomic read-modify-write on this entry the first time it's traversed. + let table_flags = PageTableFlags::PRESENT + | PageTableFlags::WRITABLE + | PageTableFlags::ACCESSED + | PageTableFlags::DIRTY; for (page, &target_frame) in Page::range(start_page, end_page).zip(frames.iter()) { // Note: Since we lock the entire page table for the duration of this function (`self.inner.lock()`), // there should be no concurrent modifications to the page table. If we allow concurrent mappings @@ -890,9 +903,14 @@ impl PageTableImpl for X64PageTabl let mut allocator = PageTableAllocator::::new(); // TODO: if it is file-backed, we need to read the page from file let frame = PageTableAllocator::::allocate_frame(true).unwrap(); + // ACCESSED and DIRTY are pre-set here (mirroring the Linux kernel's + // `_KERNPG_TABLE`) so the CPU's page-table walker doesn't need an + // atomic read-modify-write on this entry the first time it's traversed. let table_flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE - | PageTableFlags::USER_ACCESSIBLE; + | PageTableFlags::USER_ACCESSIBLE + | PageTableFlags::ACCESSED + | PageTableFlags::DIRTY; match unsafe { inner.map_to_with_table_flags( page, diff --git a/litebox_platform_lvbs/src/host/lvbs_impl.rs b/litebox_platform_lvbs/src/host/lvbs_impl.rs index f17c44aeee..c99b6cc49e 100644 --- a/litebox_platform_lvbs/src/host/lvbs_impl.rs +++ b/litebox_platform_lvbs/src/host/lvbs_impl.rs @@ -112,11 +112,7 @@ static PRK_ONCE: spin::Once<[u8; PRK_LEN]> = spin::Once::new(); /// /// This should be called once during platform initialization with a key derived /// from hardware or a boot nonce. -/// -/// # Panics -/// Panics if `key` length does not match `PRK_LEN`. -pub(crate) fn set_platform_root_key(key: &[u8]) { - assert_eq!(key.len(), PRK_LEN, "Platform Root Key length mismatch"); +pub(crate) fn set_platform_root_key(key: &[u8; PRK_LEN]) { PRK_ONCE.call_once(|| { let mut prk = Zeroizing::new([0u8; PRK_LEN]); prk.copy_from_slice(key); diff --git a/litebox_platform_lvbs/src/host/per_cpu_variables.rs b/litebox_platform_lvbs/src/host/per_cpu_variables.rs index e70af4aba0..aec492aeb3 100644 --- a/litebox_platform_lvbs/src/host/per_cpu_variables.rs +++ b/litebox_platform_lvbs/src/host/per_cpu_variables.rs @@ -11,7 +11,7 @@ use crate::{ }, }; use aligned_vec::avec; -use alloc::boxed::Box; +use alloc::{boxed::Box, sync::Arc}; use core::cell::{Cell, UnsafeCell}; use core::mem::offset_of; use litebox::utils::TruncateExt; @@ -64,6 +64,8 @@ pub struct PerCpuVariables { pub(crate) preemption_armed: Cell, /// Set when a preemption timer killed user-mode code. pub(crate) preemption_timeout_killed_user: Cell, + /// Reference to the currently loaded page table (`None`: the base page table). + active_page_table: UnsafeCell>)>>, } // These Hyper-V pages must be page-aligned. @@ -240,6 +242,31 @@ impl PerCpuVariables { pcv_asm.set_vtl1_user_xsave_area_addr(vtl1_user_xsave_area.as_ptr() as usize); pcv_asm.set_vtl1_xsave_mask(vtl1_xsave_mask); } + + /// Returns the active task page table matching `page_table_id`. + pub(crate) fn active_page_table( + &self, + page_table_id: usize, + ) -> Option>> { + // Safety: This field is private to the current core. + unsafe { &*self.active_page_table.get() } + .as_ref() + .filter(|(id, _)| *id == page_table_id) + .map(|(_, page_table)| Arc::clone(page_table)) + } + + /// # Safety + /// + /// CR3 must no longer reference the previous table. A new ID must match + /// CR3. Interrupts must be disabled, and this must not run in exception context. + pub(crate) unsafe fn set_active_page_table( + &self, + page_table: Option<(usize, Arc>)>, + ) { + // Safety: Only this core accesses the field, interrupts are disabled, + // and the update cannot fault. + unsafe { *self.active_page_table.get() = page_table } + } } /// Assembly-accessible per-CPU fields at the start of [`PerCpuVariables`]. @@ -493,6 +520,7 @@ pub fn allocate_per_cpu_variables() { let per_cpu_variables = unsafe { let ptr = per_cpu_variables.as_mut_ptr(); ptr.write_bytes(0, 1); + core::ptr::addr_of_mut!((*ptr).active_page_table).write(UnsafeCell::new(None)); // Set the "uninitialized" sentinel for vp_index (0 is a valid VP index). core::ptr::addr_of_mut!((*ptr).vp_index).write(Cell::new(u32::MAX)); per_cpu_variables.assume_init() diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 747dd15ba8..9f467044f6 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -6,7 +6,8 @@ #![cfg(target_arch = "x86_64")] #![no_std] -use crate::{host::per_cpu_variables::PerCpuVariablesAsm, mshv::vsm::Vtl0KernelInfo}; +use crate::host::per_cpu_variables::{PerCpuVariablesAsm, with_per_cpu_variables}; +use alloc::sync::Arc; use core::sync::atomic::AtomicU32; use hashbrown::HashMap; use litebox::platform::{ @@ -147,6 +148,38 @@ const USER_ADDR_MAX: usize = 0x0000_7FFF_FFFF_F000; /// const USER_ADDR_MIN: usize = 0x0000_0000_0001_0000; +/// Provide access to a page table +pub struct PageTableHandle<'a>(PageTableHandleInner<'a>); + +enum PageTableHandleInner<'a> { + Base(&'a mm::PageTable), + Task(Arc>), +} + +impl<'a> PageTableHandle<'a> { + #[inline] + fn base(page_table: &'a mm::PageTable) -> Self { + Self(PageTableHandleInner::Base(page_table)) + } + + #[inline] + fn task(page_table: Arc>) -> Self { + Self(PageTableHandleInner::Task(page_table)) + } +} + +impl core::ops::Deref for PageTableHandle<'_> { + type Target = mm::PageTable; + + #[inline] + fn deref(&self) -> &Self::Target { + match &self.0 { + PageTableHandleInner::Base(page_table) => page_table, + PageTableHandleInner::Task(page_table) => page_table, + } + } +} + /// Manages base and task page tables. /// /// This struct maintains: @@ -170,7 +203,7 @@ pub struct PageTableManager { /// Cached physical frame of the base page table (for fast CR3 comparison). base_page_table_frame: PhysFrame, /// Task page tables keyed by their P4 frame start address (the page table ID). - task_page_tables: spin::Mutex>>>, + task_page_tables: spin::RwLock>>>, } impl PageTableManager { @@ -185,46 +218,33 @@ impl PageTableManager { Self { base_page_table: base_pt, base_page_table_frame: base_frame, - task_page_tables: spin::Mutex::new(HashMap::new()), + task_page_tables: spin::RwLock::new(HashMap::new()), } } - /// Returns a reference to the current page table based on the CR3 register. + /// Returns a handle to the current page table. /// - /// This reads the current CR3 value and finds the matching page table. - /// If CR3 matches the base page table, returns that. Otherwise, it - /// looks up the task page table by physical frame. + /// This returns the base page table or the task page table retained by the + /// current core. /// /// # Panics /// - /// Panics if CR3 contains an unknown page table address (should never happen - /// in normal operation). + /// Panics if CR3 does not match the current core's retained page table. #[inline] - pub fn current_page_table(&self) -> &mm::PageTable { + pub fn current_page_table(&self) -> PageTableHandle<'_> { let (cr3_frame, _) = x86_64::registers::control::Cr3::read(); - // Fast path: check base page table first (most common case) if self.base_page_table_frame == cr3_frame { - return &self.base_page_table; + return PageTableHandle::base(&self.base_page_table); } let cr3_id: usize = cr3_frame.start_address().as_u64().trunc(); - let task_pts = self.task_page_tables.lock(); - if let Some(pt) = task_pts.get(&cr3_id) { - // SAFETY: Three invariants guarantee this reference remains valid: - // 1. The PageTable is Box-allocated, so HashMap rehashing does not - // move the PageTable itself (only the Box pointer moves). - // 2. This page table is the current CR3, so `delete_task_page_table` - // will refuse to remove it (returns EBUSY). - // 3. The PageTableManager is 'static, so neither it nor the HashMap - // will be deallocated. - let pt_ref: &mm::PageTable = pt; - return unsafe { &*core::ptr::from_ref(pt_ref) }; + if let Some(pt) = with_per_cpu_variables(|pcv| pcv.active_page_table(cr3_id)) { + return PageTableHandle::task(pt); } - // CR3 doesn't match any known page table - this shouldn't happen unreachable!( - "CR3 contains unknown page table: {:?}", + "CR3 does not match the per-CPU page table: {:?}", cr3_frame.start_address() ); } @@ -267,7 +287,15 @@ impl PageTableManager { /// after the switch (including the code being executed and stack) /// - No references to user-space memory are held across the switch pub unsafe fn load_base(&self) { - self.base_page_table.load(); + x86_64::instructions::interrupts::without_interrupts(|| { + // Ensure decreasing/dropping `Arc` for the previous page table (`set_active_page_table()`) + // only after switching CR3 (`mm::PageTable::load()`). + self.base_page_table.load(); + with_per_cpu_variables(|pcv| { + // Safety: CR3 now references the base page table and interrupts are disabled. + unsafe { pcv.set_active_page_table(None) } + }); + }); } /// Loads the specified task page table by updating CR3. @@ -290,13 +318,21 @@ impl PageTableManager { return Err(Errno::EINVAL); } - let task_pts = self.task_page_tables.lock(); - if let Some(pt) = task_pts.get(&task_pt_id) { + let pt = { + let task_pts = self.task_page_tables.read(); + Arc::clone(task_pts.get(&task_pt_id).ok_or(Errno::ENOENT)?) + }; + + x86_64::instructions::interrupts::without_interrupts(|| { + // Ensure decreasing/dropping `Arc` for the previous page table (`set_active_page_table()`) + // only after switching CR3 (`mm::PageTable::load()`). pt.load(); - Ok(()) - } else { - Err(Errno::ENOENT) - } + with_per_cpu_variables(|pcv| { + // Safety: CR3 now references `pt` and interrupts are disabled. + unsafe { pcv.set_active_page_table(Some((task_pt_id, pt))) } + }); + }); + Ok(()) } /// Creates a new task page table and returns its ID. @@ -318,10 +354,10 @@ impl PageTableManager { // fixed after boot; lower slots are not shared (see `copy_pml4_entries_from`). pt.copy_pml4_entries_from(&self.base_page_table); - let pt = alloc::boxed::Box::new(pt); + let pt = Arc::new(pt); let task_pt_id: usize = pt.get_physical_frame().start_address().as_u64().trunc(); - let mut task_pts = self.task_page_tables.lock(); + let mut task_pts = self.task_page_tables.write(); task_pts.insert(task_pt_id, pt); Ok(task_pt_id) @@ -348,15 +384,15 @@ impl PageTableManager { /// - `Ok(())` if the page table was successfully deleted /// - `Err(Errno::EINVAL)` if the page table ID is the base page table /// - `Err(Errno::ENOENT)` if the page table ID does not exist - /// - `Err(Errno::EBUSY)` if the page table is currently active (switch away first) + /// - `Err(Errno::EBUSY)` if the page table is active or has outstanding handles pub unsafe fn delete_task_page_table(&self, task_pt_id: usize) -> Result<(), Errno> { if task_pt_id == BASE_PAGE_TABLE_ID { return Err(Errno::EINVAL); } - let mut task_pts = self.task_page_tables.lock(); + let mut task_pts = self.task_page_tables.write(); - // Check CR3 under the same lock to avoid TOCTOU with the removal below. + // Fast path for the page table active on this core. let (cr3_frame, _) = x86_64::registers::control::Cr3::read(); let cr3_id: usize = cr3_frame.start_address().as_u64().trunc(); if cr3_id == task_pt_id { @@ -364,13 +400,18 @@ impl PageTableManager { } if let Some(pt) = task_pts.remove(&task_pt_id) { + // An active CR3 retains a per-CPU Arc. + let pt = match Arc::try_unwrap(pt) { + Ok(pt) => pt, + Err(pt) => { + task_pts.insert(task_pt_id, pt); + return Err(Errno::EBUSY); + } + }; drop(task_pts); - // Safety: We're about to delete this page table, so it's safe to - // free the task-owned intermediate page table frames (user, - // direct-map, and vmap slots). The kernel slots are shared with the - // base page table and are deliberately left untouched, so its - // P3/P2/P1 frames are not freed. + // Safety: successful unwrap proves the table is neither active nor + // borrowed. Kernel slots are base-owned and must not be freed. unsafe { pt.cleanup_page_table_frames(); } @@ -388,7 +429,7 @@ pub struct LinuxKernel { host_and_task: core::marker::PhantomData, page_table_manager: PageTableManager, vtl1_phys_frame_range: PhysFrameRange, - vtl0_kernel_info: Vtl0KernelInfo, + end_of_boot: core::sync::atomic::AtomicBool, } /// [`litebox::platform::common_providers::userspace_pointers::ValidateAccess`] @@ -621,10 +662,21 @@ impl LinuxKernel { host_and_task: core::marker::PhantomData, page_table_manager: PageTableManager::new(base_pt), vtl1_phys_frame_range: vtl1_range, - vtl0_kernel_info: Vtl0KernelInfo::new(), + end_of_boot: core::sync::atomic::AtomicBool::new(false), })) } + /// Whether VTL1's window of trusting VTL0 has closed. + pub(crate) fn end_of_boot_reached(&self) -> bool { + self.end_of_boot.load(core::sync::atomic::Ordering::SeqCst) + } + + /// Close VTL1's window of trusting VTL0. One-way. + pub(crate) fn signal_end_of_boot(&self) { + self.end_of_boot + .store(true, core::sync::atomic::Ordering::SeqCst); + } + /// Returns the physical frame range belonging to VTL1. pub fn vtl1_phys_frame_range(&self) -> PhysFrameRange { self.vtl1_phys_frame_range @@ -700,7 +752,7 @@ impl LinuxKernel { /// - `Ok(())` if successful /// - `Err(Errno::EINVAL)` if the page table is the base page table /// - `Err(Errno::ENOENT)` if the page table doesn't exist - /// - `Err(Errno::EBUSY)` if the page table is currently active + /// - `Err(Errno::EBUSY)` if the page table is active or has outstanding handles pub unsafe fn delete_task_page_table(&self, task_pt_id: usize) -> Result<(), Errno> { // Safety: caller guarantees no dangling references unsafe { self.page_table_manager.delete_task_page_table(task_pt_id) } @@ -1230,16 +1282,16 @@ unsafe impl VmapManager for Linu range_set.insert(start..end); } - let mem_attr = if perms.contains(PhysPageMapPermissions::WRITE) { + let page_prot = if perms.contains(PhysPageMapPermissions::WRITE) { // VTL1 needs writable access, so deny VTL0 all access. - litebox_common_lvbs::MemAttr::empty() + crate::mshv::HvPageProtFlags::HV_PAGE_ACCESS_NONE } else if perms.contains(PhysPageMapPermissions::READ) { // VTL1 wants to read data from the pages, preventing VTL0 from writing to the pages. - litebox_common_lvbs::MemAttr::MEM_ATTR_READ - | litebox_common_lvbs::MemAttr::MEM_ATTR_EXEC + crate::mshv::HvPageProtFlags::HV_PAGE_READABLE + | crate::mshv::HvPageProtFlags::HV_PAGE_EXECUTABLE } else { // VTL1 no longer protects the pages. - litebox_common_lvbs::MemAttr::all() + crate::mshv::HvPageProtFlags::HV_PAGE_FULL_ACCESS }; for range in range_set.iter() { @@ -1247,7 +1299,7 @@ unsafe impl VmapManager for Linu PhysFrame::::containing_address(x86_64::PhysAddr::new(range.start)), PhysFrame::::containing_address(x86_64::PhysAddr::new(range.end)), ); - crate::mshv::vsm::protect_physical_memory_range(frame_range, mem_attr) + crate::mshv::vsm::protect_physical_memory_range(frame_range, page_prot) .map_err(|_| PhysPointerError::UnsupportedPermissions(perms.bits()))?; } diff --git a/litebox_platform_lvbs/src/mshv/heki.rs b/litebox_platform_lvbs/src/mshv/heki.rs deleted file mode 100644 index c1615d9c1d..0000000000 --- a/litebox_platform_lvbs/src/mshv/heki.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Platform-coupled HEKI helpers. -//! -//! The wire types, enums, and constants have been hoisted into -//! [`litebox_common_lvbs`]. What remains here are helpers that depend on -//! platform-specific types (e.g. [`HvPageProtFlags`]). - -use crate::mshv::HvPageProtFlags; -use litebox_common_lvbs::MemAttr; - -pub(crate) fn mem_attr_to_hv_page_prot_flags(attr: MemAttr) -> HvPageProtFlags { - let mut flags = HvPageProtFlags::empty(); - - if attr.contains(MemAttr::MEM_ATTR_READ) { - flags.set(HvPageProtFlags::HV_PAGE_READABLE, true); - flags.set(HvPageProtFlags::HV_PAGE_USER_EXECUTABLE, true); - } - if attr.contains(MemAttr::MEM_ATTR_WRITE) { - flags.set(HvPageProtFlags::HV_PAGE_WRITABLE, true); - } - if attr.contains(MemAttr::MEM_ATTR_EXEC) { - flags.set(HvPageProtFlags::HV_PAGE_EXECUTABLE, true); - } - - flags -} diff --git a/litebox_platform_lvbs/src/mshv/mod.rs b/litebox_platform_lvbs/src/mshv/mod.rs index 55760aa130..cbb849f6bd 100644 --- a/litebox_platform_lvbs/src/mshv/mod.rs +++ b/litebox_platform_lvbs/src/mshv/mod.rs @@ -3,11 +3,9 @@ //! Hyper-V-specific code -pub(crate) mod heki; pub mod hvcall; pub(crate) mod hvcall_mm; mod hvcall_vp; -mod mem_integrity; pub(crate) mod ringbuffer; pub mod vsm; pub mod vsm_intercept; @@ -38,7 +36,7 @@ unsafe impl VmapManager for PrivilegedVmap { perms: PhysPageMapPermissions, ) -> Result { // SAFETY: callers uphold the raw mapping contract. This provider is used only for - // independently authorized HEKI patch and ring-buffer writes. + // writes whose destination the caller has independently authorized. unsafe { crate::platform_low().vmap_privileged(pages, perms) } } @@ -73,8 +71,8 @@ unsafe impl VmapManager for PrivilegedVmap { type Vtl0PhysConstPtr = litebox_common_linux::physical_pointers::PhysConstPtr; -/// Mutable VTL0 pointer reserved for validated HEKI text patching and the fixed-address log ring -/// buffer. It bypasses ordinary protected-frame access checks and synchronization. Do not use it for other +/// Mutable VTL0 pointer reserved for callers that have independently validated the destination. +/// It bypasses ordinary protected-frame access checks and synchronization. Do not use it for other /// VTL0 destinations that could enable confused-deputy writes. type PrivilegedVtl0PhysMutPtr = litebox_common_linux::physical_pointers::PhysMutPtr; @@ -192,7 +190,7 @@ pub const MSR_IA32_SYSENTER_EIP: u32 = 0x0000_0176; pub const DEFAULT_REG_PIN_MASK: u64 = u64::MAX; bitflags::bitflags! { - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Clone, Copy)] pub struct HvPageProtFlags: u8 { const HV_PAGE_ACCESS_NONE = 0x0; const HV_PAGE_READABLE = 0x1; diff --git a/litebox_platform_lvbs/src/mshv/vsm.rs b/litebox_platform_lvbs/src/mshv/vsm.rs index ba8c80f4f7..396413224d 100644 --- a/litebox_platform_lvbs/src/mshv/vsm.rs +++ b/litebox_platform_lvbs/src/mshv/vsm.rs @@ -1,18 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! VSM functions +//! Enabling Virtual Secure Mode (VSM) using Hyper-V hypercalls to +//! secure both VTL0 and VTL1. -#[cfg(debug_assertions)] -use crate::mshv::mem_integrity::parse_modinfo; -use crate::mshv::ringbuffer::set_ringbuffer; -use crate::mshv::{PrivilegedVtl0PhysMutPtr, Vtl0PhysConstPtr}; +use crate::host::linux::CpuMask; use crate::{ debug_serial_println, - host::{ - bootparam::get_vtl1_memory_info, linux::CpuMask, per_cpu_variables::with_per_cpu_variables, - set_platform_root_key, - }, + host::{bootparam::get_vtl1_memory_info, per_cpu_variables::with_per_cpu_variables}, mshv::{ HV_REGISTER_CR_INTERCEPT_CONTROL, HV_REGISTER_CR_INTERCEPT_CR0_MASK, HV_REGISTER_CR_INTERCEPT_CR4_MASK, HV_REGISTER_VSM_PARTITION_CONFIG, @@ -22,48 +17,33 @@ use crate::{ HV_X64_REGISTER_SYSENTER_EIP, HV_X64_REGISTER_SYSENTER_ESP, HvCrInterceptControlFlags, HvPageProtFlags, HvRegisterVsmPartitionConfig, HvRegisterVsmVpSecureVtlConfig, X86Cr0Flags, X86Cr4Flags, - heki::mem_attr_to_hv_page_prot_flags, hvcall_mm::hv_modify_vtl_protection_mask, - hvcall_vp::{hvcall_get_vp_vtl0_registers, hvcall_set_vp_registers, init_vtl_ap}, - mem_integrity::{ - validate_kernel_module_against_elf, validate_text_patch, - verify_kernel_module_signature, verify_kernel_pe_signature, - }, + hvcall_vp::{hvcall_get_vp_vtl0_registers, hvcall_set_vp_registers}, vtl_switch::mshv_vsm_get_code_page_offsets, - vtl1_mem_layout::{PAGE_SHIFT, PAGE_SIZE}, }, }; +use alloc::vec::Vec; +use core::ops::Range; +use litebox::utils::TruncateExt; +use litebox_common_linux::vmap::PhysPageAddr; use litebox_common_lvbs::{ - HekiKdataType, HekiKernelInfo, HekiKernelSymbol, HekiKexecType, HekiPage, HekiPatch, - HekiPatchInfo, HekiRange, HypervCallError, KEXEC_SEGMENT_MAX, Kimage, MemAttr, ModMemType, - PRK_LEN, VsmError, VsmFunction, mod_mem_type_to_mem_attr, -}; - -use alloc::{boxed::Box, ffi::CString, string::String, vec::Vec}; -use core::{ - mem, - ops::Range, - sync::atomic::{AtomicBool, AtomicI64, Ordering}, + FrameTxn, HypervCallError, MemAttr, PAGE_SHIFT, PAGE_SIZE, PRK_LEN, ReservationStatus, + VsmError, Vtl0Gate, Vtl0PrivilegedWrite, Vtl1Gate, }; -use hashbrown::{HashMap, HashSet}; -use litebox::utils::TruncateExt; -use litebox_common_linux::{errno::Errno, vmap::PhysPageAddr}; use rangemap::RangeSet; use spin::{Once, rwlock::RwLock as SpinRwLock}; -use thiserror::Error; use x86_64::{ - PhysAddr, VirtAddr, - structures::paging::{PageSize, PhysFrame, Size4KiB, frame::PhysFrameRange}, + PhysAddr, + structures::paging::{PhysFrame, Size4KiB, frame::PhysFrameRange}, }; -use x509_cert::{Certificate, der::Decode}; -use zerocopy::{FromBytes, FromZeros, IntoBytes}; +use zerocopy::FromBytes; use zeroize::Zeroizing; -// For now, we do not validate large kernel modules due to the VTL1's memory size limitation. -const MODULE_VALIDATION_MAX_SIZE: usize = 64 * 1024 * 1024; +use super::{PrivilegedVtl0PhysMutPtr, Vtl0PhysConstPtr}; -static CPU_ONLINE_MASK: Once> = Once::new(); +// --- VSM: Hyper-V partition/VP configuration and intercepts ----------------- +/// Bring VSM up on this CPU. The BSP also protects VTL1's own memory here. pub(crate) fn init(is_bsp: bool) { assert!( !(is_bsp && mshv_vsm_configure_partition().is_err()), @@ -106,62 +86,8 @@ pub(crate) fn init(is_bsp: bool) { } } -/// VSM function for enabling VTL of APs -/// Not supported in this implementation. -#[allow(clippy::unnecessary_wraps)] -pub fn mshv_vsm_enable_aps(_cpu_present_mask_pfn: u64) -> Result { - debug_serial_println!("mshv_vsm_enable_aps() not supported"); - Ok(0) -} - -/// VSM function for enabling VTL and booting APs -/// `cpu_online_mask_pfn` indicates the page containing the VTL0's CPU online mask. -pub fn mshv_vsm_boot_aps(cpu_online_mask_pfn: u64) -> Result { - debug_serial_println!("VSM: Boot APs"); - let cpu_online_mask_page_addr = cpu_online_mask_pfn - .checked_shl(PAGE_SHIFT.trunc()) - .and_then(|pa| PhysAddr::try_new(pa).ok()) - .ok_or(VsmError::InvalidPhysicalAddress)?; - - let cpu_mask_ptr = Vtl0PhysConstPtr::::with_usize( - cpu_online_mask_page_addr.as_u64().trunc(), - ) - .map_err(|_| VsmError::CpuOnlineMaskCopyFailed)?; - let cpu_mask = cpu_mask_ptr - .read_at_offset(0) - .map_err(|_| VsmError::CpuOnlineMaskCopyFailed)?; - - #[cfg(debug_assertions)] - { - crate::debug_serial_print!("cpu_online_mask: "); - cpu_mask.for_each_cpu(|cpu_id| { - crate::debug_serial_print!("{}, ", cpu_id); - }); - debug_serial_println!(""); - } - - let mut error = None; - - // Initialize VTL for each online CPU and update its boot signal byte - cpu_mask.for_each_cpu(|cpu_id| { - let cpu_id_u32: u32 = cpu_id.trunc(); - if let Err(e) = init_vtl_ap(cpu_id_u32) { - error = Some(e); - } - }); - - if let Some(e) = error { - return Err(VsmError::ApInitFailed(e)); - } - - // Store the cpu_online_mask for later use - CPU_ONLINE_MASK.call_once(|| cpu_mask); - - Ok(0) -} - -/// VSM function for enforcing certain security features of VTL0 -pub fn mshv_vsm_secure_config_vtl0() -> Result { +/// VSM function for enforcing certain security features of VTL0 to protect VTL1 +pub(crate) fn mshv_vsm_secure_config_vtl0() -> Result { debug_serial_println!("VSM: Secure VTL0 configuration"); let mut config = HvRegisterVsmVpSecureVtlConfig::new(); @@ -175,7 +101,7 @@ pub fn mshv_vsm_secure_config_vtl0() -> Result { } /// VSM function to configure a VSM partition for VTL1 -pub fn mshv_vsm_configure_partition() -> Result { +pub(crate) fn mshv_vsm_configure_partition() -> Result { debug_serial_println!("VSM: Configure partition"); let mut config = HvRegisterVsmPartitionConfig::new(); @@ -188,11 +114,12 @@ pub fn mshv_vsm_configure_partition() -> Result { Ok(0) } -/// VSM function for locking VTL0's control registers. -pub fn mshv_vsm_lock_regs() -> Result { +/// VSM function for locking VTL0's control registers, snapshotting their +/// current values into VTL1 per-CPU state. +pub(crate) fn mshv_vsm_lock_regs() -> Result { debug_serial_println!("VSM: Lock control registers"); - if crate::platform_low().vtl0_kernel_info.check_end_of_boot() { + if crate::platform_low().end_of_boot_reached() { return Err(VsmError::OperationAfterEndOfBoot( "control register locking", )); @@ -234,253 +161,134 @@ pub fn mshv_vsm_lock_regs() -> Result { Ok(0) } -/// VSM function for signaling the end of VTL0 boot process -pub fn mshv_vsm_end_of_boot() -> i64 { - debug_serial_println!("VSM: End of boot"); - crate::platform_low().vtl0_kernel_info.set_end_of_boot(); - 0 -} +pub const NUM_CONTROL_REGS: usize = 11; -/// VSM function for protecting certain memory ranges (e.g., kernel text, data, heap). -/// `pa` and `nranges` specify a memory area containing the information about the memory ranges to protect. -pub fn mshv_vsm_protect_memory(pa: u64, nranges: u64) -> Result { - if PhysAddr::try_new(pa) - .ok() - .as_ref() - .is_none_or(|p| !p.is_aligned(Size4KiB::SIZE)) - || nranges == 0 - { - return Err(VsmError::InvalidInputAddress); - } +/// Data structure for maintaining MSRs and control registers whose values are locked. +/// This structure is expected to be stored in per-core kernel context, so we do not protect it with a lock. +#[derive(Debug, Clone, Copy)] +pub struct ControlRegMap { + pub entries: [(u32, u64); NUM_CONTROL_REGS], +} - if crate::platform_low().vtl0_kernel_info.check_end_of_boot() { - return Err(VsmError::OperationAfterEndOfBoot( - "kernel memory protection", - )); +impl ControlRegMap { + pub fn init(&mut self) { + [ + HV_X64_REGISTER_CR0, + HV_X64_REGISTER_CR4, + HV_X64_REGISTER_LSTAR, + HV_X64_REGISTER_STAR, + HV_X64_REGISTER_CSTAR, + HV_X64_REGISTER_APIC_BASE, + HV_X64_REGISTER_EFER, + HV_X64_REGISTER_SYSENTER_CS, + HV_X64_REGISTER_SYSENTER_ESP, + HV_X64_REGISTER_SYSENTER_EIP, + HV_X64_REGISTER_SFMASK, + ] + .iter() + .enumerate() + .for_each(|(i, ®_name)| { + self.entries[i] = (reg_name, 0); + }); } - let heki_pages = copy_heki_pages_from_vtl0(pa, nranges).ok_or(VsmError::HekiPagesCopyFailed)?; - - for heki_page in heki_pages { - for heki_range in &heki_page { - let pa = heki_range.pa; - let epa = heki_range.epa; - let mem_attr = heki_range - .mem_attr() - .ok_or(VsmError::MemoryAttributeInvalid)?; - - if !heki_range.is_aligned(Size4KiB::SIZE) { - return Err(VsmError::AddressNotPageAligned); + pub fn get(&self, reg_name: u32) -> Option { + for entry in &self.entries { + if entry.0 == reg_name { + return Some(entry.1); } + } + None + } - #[cfg(debug_assertions)] - let va = heki_range.va; - debug_serial_println!( - "VSM: Protect memory: va {:#x} pa {:#x} epa {:#x} {:?} (size: {})", - va, - pa, - epa, - mem_attr, - epa - pa - ); - - if pa == epa { - continue; + pub fn set(&mut self, reg_name: u32, value: u64) { + for entry in &mut self.entries { + if entry.0 == reg_name { + entry.1 = value; + return; } - - protect_physical_memory_range( - PhysFrame::range( - // `HekiRange::is_valid` already validated both physical addresses. - PhysFrame::containing_address(PhysAddr::new(pa)), - PhysFrame::containing_address(PhysAddr::new(epa)), - ), - mem_attr, - )?; } } - Ok(0) -} - -fn parse_certs(mut buf: &[u8]) -> Result, VsmError> { - let mut certs = Vec::new(); - - while buf.len() >= 4 && buf[0] == 0x30 && buf[1] == 0x82 { - let der_len = ((buf[2] as usize) << 8) | (buf[3] as usize); - let total_len = der_len + 4; - if buf.len() < total_len { - return Err(VsmError::CertificateDerLengthInvalid { - expected: total_len, - actual: buf.len(), - }); + // consider implementing a mutable iterator (if we plan to lock many control registers) + pub fn reg_names(&self) -> [u32; NUM_CONTROL_REGS] { + let mut names = [0; NUM_CONTROL_REGS]; + for (i, entry) in self.entries.iter().enumerate() { + names[i] = entry.0; } - - let cert_bytes = &buf[..total_len]; - let cert = - Certificate::from_der(cert_bytes).map_err(|_| VsmError::CertificateParseFailed)?; - certs.push(cert); - buf = &buf[total_len..]; + names } - Ok(certs) } -/// VSM function for loading kernel data (e.g., certificates, blocklist, kernel symbols) into VTL1. -/// `pa` and `nranges` specify memory areas containing the information about the memory ranges to load. -pub fn mshv_vsm_load_kdata(pa: u64, nranges: u64) -> Result { - if PhysAddr::try_new(pa) - .ok() - .as_ref() - .is_none_or(|p| !p.is_aligned(Size4KiB::SIZE)) - || nranges == 0 - { - return Err(VsmError::InvalidInputAddress); - } - - if crate::platform_low().vtl0_kernel_info.check_end_of_boot() { - return Err(VsmError::OperationAfterEndOfBoot("loading kernel data")); - } - - let vtl0_info = &crate::platform_low().vtl0_kernel_info; - - let mut system_certs_mem = MemoryContainer::new(); - let mut kexec_trampoline_metadata = KexecMemoryMetadata::new(); - let mut kexec_trampoline_insert_failed = false; - let mut patch_info_mem = MemoryContainer::new(); - let mut kinfo_mem = MemoryContainer::new(); - let mut kdata_mem = MemoryContainer::new(); - - let heki_pages = copy_heki_pages_from_vtl0(pa, nranges).ok_or(VsmError::HekiPagesCopyFailed)?; - - for heki_page in &heki_pages { - for heki_range in heki_page { - debug_serial_println!("VSM: Load kernel data {heki_range:?}"); - match heki_range.heki_kdata_type() { - HekiKdataType::SystemCerts => system_certs_mem - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?, - HekiKdataType::KexecTrampoline => { - if let Err(e) = kexec_trampoline_metadata.insert_heki_range(heki_range) { - debug_serial_println!( - "VSM: KexecTrampoline insert_heki_range failed ({e:?}); skipping kexec trampoline protection" - ); - kexec_trampoline_insert_failed = true; - } - } - HekiKdataType::PatchInfo => patch_info_mem - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?, - HekiKdataType::KernelInfo => kinfo_mem - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?, - HekiKdataType::KernelData => kdata_mem - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?, - HekiKdataType::Unknown => { - return Err(VsmError::KernelDataTypeInvalid); - } - _ => { - debug_serial_println!("VSM: Unsupported kernel data not loaded {heki_range:?}"); - } - } +#[allow(clippy::unnecessary_wraps)] +fn save_vtl0_locked_regs() -> Result { + let reg_names = with_per_cpu_variables(|per_cpu_variables| { + let mut regs = per_cpu_variables.vtl0_locked_regs.get(); + regs.init(); + per_cpu_variables.vtl0_locked_regs.set(regs); + regs.reg_names() + }); + for reg_name in reg_names { + if let Ok(value) = hvcall_get_vp_vtl0_registers(reg_name) { + with_per_cpu_variables(|per_cpu_variables| { + let mut regs = per_cpu_variables.vtl0_locked_regs.get(); + regs.set(reg_name, value); + per_cpu_variables.vtl0_locked_regs.set(regs); + }); } } - system_certs_mem - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - patch_info_mem - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - kinfo_mem - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - kdata_mem - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - - if system_certs_mem.is_empty() { - return Err(VsmError::SystemCertificatesNotFound); - } - - let cert_buf = &system_certs_mem[..]; - let certs = parse_certs(cert_buf)?; - - if certs.is_empty() { - return Err(VsmError::SystemCertificatesInvalid); - } - - // The system certificate is loaded into VTL1 and locked down before `end_of_boot` is signaled. - // Its integrity depends on UEFI Secure Boot which ensures only trusted software is loaded during - // the boot process. - vtl0_info.set_system_certificates(certs.clone()); - debug_serial_println!("VSM: Loaded {} system certificate(s)", certs.len()); - - // ToDo: Remove kexec_trampoline_insert_failed and protect kexec_trampoline_metadata - // once we have a better solution to handle the non-page-aligned kexec trampoline metadata. - // The current solution is to skip protecting kexec trampoline metadata if its insert_heki_range - // fails, letting kdata load proceed so that heki is not broken. - if !kexec_trampoline_insert_failed { - for kexec_trampoline_range in &kexec_trampoline_metadata { - protect_physical_memory_range( - kexec_trampoline_range.phys_frame_range, - MemAttr::MEM_ATTR_READ, - )?; - } - } + Ok(0) +} - // pre-computed patch data for the kernel text - if !patch_info_mem.is_empty() { - let patch_info_buf = &patch_info_mem[..]; - vtl0_info - .precomputed_patches - .insert_patch_data_from_bytes(patch_info_buf, None) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - } +// --- VTL1 self-protection --------------------------------------------------- +// +// VTL1 locking down its own memory. Not HEKI: it runs during VTL1 setup, before +// any HEKI policy exists. - if kinfo_mem.is_empty() || kdata_mem.is_empty() { - return Err(VsmError::KernelSymbolTableNotFound); +/// This function protects a VTL1 physical memory range, securing VTL1's own pages. +/// VTL0 should never access VTL1 memory, so the memory attribute is always empty (no read, write, or execute). +/// +/// Note. This function doesn't check whether `phys_frame_range` belongs to VTL1 because it is called by BSP +/// before the kernel platform data structure is initialized. To this end, one might call this function with +/// a VTL0 physical memory range which only restricts access to the range. +#[inline] +pub(crate) fn protect_vtl1_physical_memory_range( + phys_frame_range: PhysFrameRange, +) -> Result<(), VsmError> { + let pa = phys_frame_range.start.start_address().as_u64(); + let num_pages = phys_frame_range.count() as u64; + if num_pages > 0 { + hv_modify_vtl_protection_mask(pa, num_pages, HvPageProtFlags::HV_PAGE_ACCESS_NONE) + .map_err(VsmError::HypercallFailed)?; } - - let kinfo_buf = &kinfo_mem[..]; - let kdata_buf = &kdata_mem[..]; - let kinfo = HekiKernelInfo::from_bytes(kinfo_buf)?; - - vtl0_info.gpl_symbols.build_from_container( - VirtAddr::from_ptr(kinfo.ksymtab_gpl_start), - VirtAddr::from_ptr(kinfo.ksymtab_gpl_end), - &kdata_mem, - kdata_buf, - )?; - - vtl0_info.symbols.build_from_container( - VirtAddr::from_ptr(kinfo.ksymtab_start), - VirtAddr::from_ptr(kinfo.ksymtab_end), - &kdata_mem, - kdata_buf, - )?; - - Ok(0) - // TODO: create blocklist keys - // TODO: save blocklist hashes + Ok(()) } -/// RAII reservation over VTL0 physical frames, shared by module load and kexec validation. -/// On drop without `commit`, every newly reserved range is restored to VTL0 read/write, -/// non-executable access. -struct FrameReservation { +// --- VTL0 frame protection -------------------------------------------------- +// +// VTL0 frame arbitration: the VTL1-wide record of which VTL0 frames are +// withheld from VTL0 or reserved by an in-flight validation. +// +// Although HEKI manages these frames (i.e., it is the only policy writer), we +// cannot maintain them in the HEKI service crate because there are other VTL1 +// readers which is unaware of HEKI (e.g., OP-TEE shim's normal-world pointers). +// Also, this is used by `protect_physical_memory_range` which invokes a hypercall. + +/// RAII reservation over VTL0 physical frames. On drop without `commit`, every +/// newly reserved range is restored to VTL0 read/write, non-executable access. +/// +/// VTL1 has to record this itself because there is no Hyper-V hypercall to get +/// a frame's current VTL protection mask. The reservation remembers which ranges +/// it changed, enabling reliable rollback. +pub(crate) struct FrameReservation { owned_ranges: Vec>, owned_frames: RangeSet, committed: bool, } -#[derive(Debug, PartialEq, Eq)] -enum ReservationStatus { - New, - AlreadyOwned, -} - impl FrameReservation { - fn new() -> Self { + pub(crate) fn new() -> Self { Self { owned_ranges: Vec::new(), owned_frames: RangeSet::new(), @@ -509,7 +317,7 @@ impl FrameReservation { /// /// Validation and insertion are atomic under exclusive registry access. On rejection, only /// claims added by this call are rolled back. - fn reserve( + pub(crate) fn reserve( &mut self, frames: impl IntoIterator>, ) -> Result, VsmError> { @@ -567,7 +375,7 @@ impl FrameReservation { } /// Mark the reserved frames as committed; drop becomes a no-op. - fn commit(&mut self) { + pub(crate) fn commit(&mut self) { self.committed = true; } } @@ -589,1143 +397,116 @@ impl Drop for FrameReservation { } } -/// VSM function for validating a guest kernel module and applying specified protection to its memory ranges after validation. -/// `pa` and `nranges` specify a memory area containing the information about the kernel module to validate or protect. -/// `flags` controls the validation process (unused for now). -/// This function returns a unique `token` to VTL0, which is used to identify the module in subsequent calls. -pub fn mshv_vsm_validate_guest_module(pa: u64, nranges: u64, _flags: u64) -> Result { - if PhysAddr::try_new(pa) - .ok() - .as_ref() - .is_none_or(|p| !p.is_aligned(Size4KiB::SIZE)) - || nranges == 0 - { - return Err(VsmError::InvalidInputAddress); - } - - debug_serial_println!( - "VSM: Validate kernel module: pa {:#x} nranges {}", - pa, - nranges, - ); +/// Registry of VTL0 frames that are non-writable to VTL0 or reserved by an +/// in-flight claim. Ordinary writable mappings retain shared access for their +/// lifetime; reservations and VTL0 protection updates use exclusive access. +/// Privileged mappings bypass the registry. +pub(crate) struct ProtectedFrameRegistry { + frames: SpinRwLock>, +} - let certs = crate::platform_low() - .vtl0_kernel_info - .get_system_certificates() - .ok_or(VsmError::SystemCertificatesNotLoaded)?; - - // collect and maintain the memory ranges of a module locally until the module is validated and its metadata is registered in the global map - // we don't maintain this content in the global map due to memory overhead. Instead, we could add its hash value to the global map to check the integrity. - let mut module_memory_metadata = ModuleMemoryMetadata::new(); - // a kernel module loaded in memory with relocations and patches - let mut module_in_memory = ModuleMemory::new(); - // the kernel module's original ELF binary which is signed by the kernel build pipeline - let mut module_as_elf = MemoryContainer::new(); - // patch info for the kernel module - let mut patch_info_for_module = MemoryContainer::new(); - - let heki_pages = copy_heki_pages_from_vtl0(pa, nranges).ok_or(VsmError::HekiPagesCopyFailed)?; - - for heki_page in &heki_pages { - for heki_range in heki_page { - match heki_range.mod_mem_type() { - ModMemType::Unknown => { - return Err(VsmError::ModuleMemoryTypeInvalid); - } - ModMemType::ElfBuffer => module_as_elf - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?, - ModMemType::Patch => patch_info_for_module - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?, - _ => { - // if input memory range's type is neither `Unknown` nor `ElfBuffer`, its addresses must be page-aligned - if !heki_range.is_aligned(Size4KiB::SIZE) { - return Err(VsmError::AddressNotPageAligned); - } - module_memory_metadata.insert_heki_range(heki_range); - module_in_memory - .extend_range(heki_range.mod_mem_type(), heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?; - } - } - } - } +/// Opaque guard that holds shared registry access for an ordinary writable mapping, blocking +/// exclusive protection and reservation updates until dropped. +pub(crate) struct ProtectedFrameAccessGuard<'a> { + _guard: spin::rwlock::RwLockReadGuard<'a, RangeSet>, +} - // Reject overlap and reserve this module's frames. Legitimate module frames are never shared. - let mut frame_guard = FrameReservation::new(); - let _ = frame_guard.reserve(module_memory_metadata.iter().map(|r| r.phys_frame_range))?; +struct ProtectedFrameUpdateGuard<'a> { + guard: spin::rwlock::RwLockWriteGuard<'a, RangeSet>, +} - // Freeze frames that require immutable copy/validation to avoid TOCTOU. - for mod_mem_range in &module_memory_metadata { - if !mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type).contains(MemAttr::MEM_ATTR_WRITE) { - protect_physical_memory_range(mod_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; - } +impl ProtectedFrameUpdateGuard<'_> { + fn overlaps(&self, range: &Range) -> bool { + self.guard.overlaps(range) } - module_as_elf - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - patch_info_for_module - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - module_in_memory - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - - let elf_size = (module_as_elf[..]).len(); - if elf_size > MODULE_VALIDATION_MAX_SIZE { - return Err(VsmError::ModuleElfSizeExceeded { - size: elf_size, - max: MODULE_VALIDATION_MAX_SIZE, - }); + fn insert(&mut self, range: Range) { + self.guard.insert(range); } - let original_elf_data = &module_as_elf[..]; - - #[cfg(debug_assertions)] - parse_modinfo(original_elf_data).map_err(|_| VsmError::Vtl0CopyFailed)?; - - verify_kernel_module_signature(original_elf_data, certs)?; - - if !validate_kernel_module_against_elf(&module_in_memory, original_elf_data) - .map_err(|_| VsmError::Vtl0CopyFailed)? - { - return Err(VsmError::ModuleRelocationInvalid); + fn remove(&mut self, range: Range) { + self.guard.remove(range); } - // Both read-only and executable frames have been frozen above. - // Thus, only promote executable frames to RX. - for mod_mem_range in &module_memory_metadata { - if matches!( - mod_mem_range.mod_mem_type, - ModMemType::Text | ModMemType::InitText - ) { - protect_physical_memory_range( - mod_mem_range.phys_frame_range, - mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type), - )?; + fn record_protection(&mut self, phys_frame_range: PhysFrameRange, protect: bool) { + let start = phys_frame_range.start.start_address().as_u64(); + let end = phys_frame_range.end.start_address().as_u64(); + if start >= end { + return; } - } - - // Commit the module's pre-computed patch data (transactional). - if !patch_info_for_module.is_empty() { - let patch_info_buf = &patch_info_for_module[..]; - crate::platform_low() - .vtl0_kernel_info - .precomputed_patches - .insert_patch_data_from_bytes(patch_info_buf, Some(&mut module_memory_metadata)) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - } - - // Fully validated and committed: disarm the guard and register the module. - frame_guard.commit(); - // register the module memory in the global map and obtain a unique token for it - let token = crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .register_module_memory_metadata(module_memory_metadata); - Ok(token) -} - -/// VSM function for supporting the initialization of a guest kernel module including -/// freeing the memory ranges that were used only for initialization and -/// write-protecting the memory ranges that should be read-only after initialization. -/// `token` is the unique identifier for the module. -pub fn mshv_vsm_free_guest_module_init(token: i64) -> Result { - debug_serial_println!("VSM: Free kernel module's init (token: {})", token); - - if !crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .contains_key(token) - { - return Err(VsmError::ModuleTokenInvalid); - } - - let mut result: Result<(), VsmError> = Ok(()); - if let Some(entry) = crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .iter_entry(token) - { - for mod_mem_range in entry.iter_mem_ranges() { - let range_result = match mod_mem_range.mod_mem_type { - ModMemType::InitText | ModMemType::InitData | ModMemType::InitRoData => { - unprotect_physical_memory_range(mod_mem_range.phys_frame_range) - } - ModMemType::RoAfterInit => { - // make this memory range read-only after initialization - protect_physical_memory_range( - mod_mem_range.phys_frame_range, - MemAttr::MEM_ATTR_READ, - ) - } - _ => Ok(()), - }; - if range_result.is_err() { - result = range_result; - break; - } + if protect { + self.insert(start..end); + } else { + self.remove(start..end); } } - - // Drop the init ranges from the module's metadata regardless of failures. This is intentional - // since hypercalls shouldn't fail and avoiding double release is more important. - let freed_init_patch_targets = crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .remove_init_ranges(token); - // Remove the precomputed patches targeting those freed init frames so a stale init patch cannot - // later be applied to recycled frames (no patch-after-free). - if !freed_init_patch_targets.is_empty() { - crate::platform_low() - .vtl0_kernel_info - .precomputed_patches - .remove_patch_data(&freed_init_patch_targets); - } - - result.map(|()| 0) } -/// VSM function for supporting the unloading of a guest kernel module. -/// `token` is the unique identifier for the module. -pub fn mshv_vsm_unload_guest_module(token: i64) -> Result { - debug_serial_println!("VSM: Unload kernel module (token: {})", token); - - if !crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .contains_key(token) - { - return Err(VsmError::ModuleTokenInvalid); +impl ProtectedFrameRegistry { + fn new() -> Self { + Self { + frames: SpinRwLock::new(RangeSet::new()), + } } - if let Some(entry) = crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .iter_entry(token) - { - for mod_mem_range in entry.iter_mem_ranges() { - unprotect_physical_memory_range(mod_mem_range.phys_frame_range)?; + /// Validates that no requested page is registered as protected or reserved and returns a shared + /// guard that prevents protection or reservation updates until dropped. + pub(crate) fn acquire_access_guard( + &self, + pages: &litebox_common_linux::vmap::PhysPageAddrArray, + ) -> Result, litebox_common_linux::vmap::PhysPointerError> { + let guard = self.frames.read(); + for page in pages { + let start = page.as_usize() as u64; + let end = start + .checked_add(ALIGN as u64) + .ok_or(litebox_common_linux::vmap::PhysPointerError::Overflow)?; + if guard.overlaps(&(start..end)) { + return Err( + litebox_common_linux::vmap::PhysPointerError::InvalidPhysicalAddress( + page.as_usize(), + ), + ); + } } + Ok(ProtectedFrameAccessGuard { _guard: guard }) } - if let Some(patch_targets) = crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .get_patch_targets(token) - { - crate::platform_low() - .vtl0_kernel_info - .precomputed_patches - .remove_patch_data(&patch_targets); + /// Runs `f` with exclusive registry access. + fn with_exclusive(&self, f: impl FnOnce(&mut ProtectedFrameUpdateGuard<'_>) -> R) -> R { + f(&mut ProtectedFrameUpdateGuard { + guard: self.frames.write(), + }) } - - crate::platform_low() - .vtl0_kernel_info - .module_memory_metadata - .remove(token); - Ok(0) } -/// VSM function for copying secondary key -#[allow(clippy::unnecessary_wraps)] -pub fn mshv_vsm_copy_secondary_key(_pa: u64, _nranges: u64) -> Result { - debug_serial_println!("VSM: Copy secondary key"); - // TODO: copy secondary key - Ok(0) +pub(crate) fn protected_frame_registry() -> &'static ProtectedFrameRegistry { + static REGISTRY: Once = Once::new(); + REGISTRY.call_once(ProtectedFrameRegistry::new) } -/// VSM function for write protecting the memory regions of a verified kernel image for kexec. -/// This function protects the kexec kernel blob (PE) only if it has a valid signature. -/// Note: this function does not make kexec kernel pages executable, which should be done by -/// another VTL1 method that can intercept the kexec/reset signal. -pub fn mshv_vsm_kexec_validate(pa: u64, nranges: u64, crash: u64) -> Result { - debug_serial_println!( - "VSM: Validate kexec pa {:#x} nranges {} crash {}", - pa, - nranges, - crash - ); +/// Protect a VTL0 physical memory range using VTL protection mask (e.g., kernel code integrity). +/// +/// The registry tracks non-writable VTL0 ranges and temporary validation reservations. +/// See [`protected_frame_registry`]. +/// +/// If the requested range overlaps with VTL1 working memory, the VTL1 portion is silently +/// skipped and only the remaining VTL0 portions are protected. If the range falls entirely +/// within VTL1, this function returns `Ok(())` without issuing a hypercall. +/// +/// `phys_frame_range` specifies the range whose VTL0 permissions are updated; VTL1 working-memory +/// portions are ignored. +/// `page_prot` specifies the hypervisor page-protection flags (VTL0's allowed access) to apply. +pub(crate) fn protect_physical_memory_range( + phys_frame_range: PhysFrameRange, + page_prot: HvPageProtFlags, +) -> Result<(), VsmError> { + let protect = !page_prot.contains(HvPageProtFlags::HV_PAGE_WRITABLE); + let vtl1_range = crate::platform_low().vtl1_phys_frame_range(); - let certs = crate::platform_low() - .vtl0_kernel_info - .get_system_certificates() - .ok_or(VsmError::SystemCertificatesNotLoaded)?; - - let is_crash = crash != 0; - let kexec_metadata_ref = if is_crash { - &crate::platform_low().vtl0_kernel_info.crash_kexec_metadata - } else { - &crate::platform_low().vtl0_kernel_info.kexec_metadata - }; - - // invalidate (i.e., remove protection and clear) the kexec memory ranges which were loaded in the past - for old_kexec_mem_range in kexec_metadata_ref.iter_guarded().iter_mem_ranges() { - unprotect_physical_memory_range(old_kexec_mem_range.phys_frame_range)?; - } - kexec_metadata_ref.clear_memory(); - - if pa == 0 { - // invalidation only - return Ok(0); - } - - let mut kexec_memory_metadata = KexecMemoryMetadata::new(); - let mut kexec_image = MemoryContainer::new(); - let mut kexec_kernel_blob = MemoryContainer::new(); - - let heki_pages = copy_heki_pages_from_vtl0(pa, nranges).ok_or(VsmError::HekiPagesCopyFailed)?; - - for heki_page in &heki_pages { - for heki_range in heki_page { - match heki_range.heki_kexec_type() { - HekiKexecType::KexecImage => { - kexec_memory_metadata.insert_heki_range(heki_range)?; - kexec_image - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?; - } - HekiKexecType::KexecKernelBlob => - // we do not protect kexec kernel blob memory - { - kexec_kernel_blob - .extend_range(heki_range) - .map_err(|_| VsmError::InvalidInputAddress)?; - } - - HekiKexecType::KexecPages => kexec_memory_metadata.insert_heki_range(heki_range)?, - HekiKexecType::Unknown => { - return Err(VsmError::KexecTypeInvalid); - } - } - } - } - - // Reserve then freeze the protected kexec frames, rejecting overlap with VTL1 or other - // protected frames. - let mut frame_guard = FrameReservation::new(); - let _ = frame_guard.reserve(kexec_memory_metadata.iter().map(|r| r.phys_frame_range))?; - for kexec_mem_range in &kexec_memory_metadata { - protect_physical_memory_range(kexec_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; - } - - kexec_image - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - kexec_kernel_blob - .write_bytes_from_heki_range() - .map_err(|_| VsmError::Vtl0CopyFailed)?; - - // If this function is called for crash kexec, we protect its kimage segments as well. - if is_crash { - let kimage = Kimage::read_from_bytes(&kexec_image[..core::mem::size_of::()]) - .map_err(|_| VsmError::KexecImageSegmentsInvalid)?; - if kimage.nr_segments > KEXEC_SEGMENT_MAX as u64 { - return Err(VsmError::KexecImageSegmentsInvalid); - } - let mut segment_ranges = Vec::new(); - for i in 0..usize::try_from(kimage.nr_segments).unwrap_or(0) { - let va = kimage.segment[i].buf; - let pa = kimage.segment[i].mem; - if let Some(epa) = pa.checked_add(kimage.segment[i].memsz) { - segment_ranges.push(KexecMemoryRange::new(va, pa, epa)?); - } else { - return Err(VsmError::KexecSegmentRangeInvalid); - } - } - let reservation_statuses = - frame_guard.reserve(segment_ranges.iter().map(|r| r.phys_frame_range))?; - for (segment_range, status) in segment_ranges.into_iter().zip(reservation_statuses) { - if status == ReservationStatus::New { - protect_physical_memory_range( - segment_range.phys_frame_range, - MemAttr::MEM_ATTR_READ, - )?; - kexec_memory_metadata.insert_memory_range(segment_range); - } - } - } - - // verify the signature of the kexec blob - if let Err(result) = verify_kernel_pe_signature(&kexec_kernel_blob[..], certs) { - return Err(VsmError::SignatureVerificationFailed(result)); - } - - frame_guard.commit(); - // register the protected kexec memory ranges to support possible invalidation in the future - kexec_metadata_ref.register_memory(kexec_memory_metadata); - - Ok(0) -} - -/// VSM function for patching kernel or module text. VTL0 kernel calls this function to patch certain kernel or module -/// text region (which it does not have a permission to modify). It passes `HekiPatch` structure which can be stored -/// within one or across two likely non-contiguous physical pages. -pub fn mshv_vsm_patch_text(patch_pa_0: u64, patch_pa_1: u64) -> Result { - let heki_patch = copy_heki_patch_from_vtl0(patch_pa_0, patch_pa_1)?; - debug_serial_println!("VSM: {:?}", heki_patch); - - let precomputed_patch = crate::platform_low() - .vtl0_kernel_info - .find_precomputed_patch(&heki_patch) - .ok_or(VsmError::PrecomputedPatchNotFound)?; - - if !validate_text_patch(&heki_patch, &precomputed_patch) { - return Err(VsmError::TextPatchSuspicious); - } - - apply_vtl0_text_patch(heki_patch)?; - Ok(0) -} - -/// This function copies patch data in `HekiPatch` structure from VTL0 to VTL1. This patch data can be -/// stored within a physical page or across two likely non-contiguous physical pages. -fn copy_heki_patch_from_vtl0(patch_pa_0: u64, patch_pa_1: u64) -> Result { - let patch_pa_0 = PhysAddr::try_new(patch_pa_0).map_err(|_| VsmError::InvalidPhysicalAddress)?; - let patch_pa_1 = PhysAddr::try_new(patch_pa_1).map_err(|_| VsmError::InvalidPhysicalAddress)?; - if patch_pa_0.is_null() || patch_pa_0 == patch_pa_1 || !patch_pa_1.is_aligned(Size4KiB::SIZE) { - return Err(VsmError::InvalidInputAddress); - } - let bytes_in_first_page = if patch_pa_0.is_aligned(Size4KiB::SIZE) { - core::cmp::min(PAGE_SIZE, core::mem::size_of::()) - } else { - core::cmp::min( - (patch_pa_0.align_up(Size4KiB::SIZE) - patch_pa_0).trunc(), - core::mem::size_of::(), - ) - }; - - if (bytes_in_first_page < core::mem::size_of::() && patch_pa_1.is_null()) - || (bytes_in_first_page == core::mem::size_of::() && !patch_pa_1.is_null()) - { - return Err(VsmError::InvalidInputAddress); - } - - let heki_patch = if patch_pa_1.is_null() - || (patch_pa_0.align_up(Size4KiB::SIZE) == patch_pa_1.align_down(Size4KiB::SIZE)) - { - let ptr = Vtl0PhysConstPtr::::with_usize(patch_pa_0.as_u64().trunc()) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - ptr.read_at_offset(0) - .map(|boxed| *boxed) - .map_err(|_| VsmError::Vtl0CopyFailed) - } else { - let mut heki_patch = HekiPatch::new_zeroed(); - let heki_patch_bytes = heki_patch.as_mut_bytes(); - let pages = [ - PhysPageAddr::::new(patch_pa_0.align_down(Size4KiB::SIZE).as_u64().trunc()) - .ok_or(VsmError::Vtl0CopyFailed)?, - PhysPageAddr::::new(patch_pa_1.as_u64().trunc()) - .ok_or(VsmError::Vtl0CopyFailed)?, - ]; - let ptr = Vtl0PhysConstPtr::::new( - &pages, - (patch_pa_0 - patch_pa_0.align_down(Size4KiB::SIZE)).trunc(), - ) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - ptr.read_slice_at_offset(0, heki_patch_bytes) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - Ok(heki_patch) - }?; - - if heki_patch.is_valid() { - Ok(heki_patch) - } else { - Err(VsmError::InvalidInputAddress) - } -} - -/// Apply a `HekiPatch` to VTL0 text after the caller has validated it against VTL1's precomputed -/// HEKI patch data. -fn apply_vtl0_text_patch(heki_patch: HekiPatch) -> Result<(), VsmError> { - // `HekiPatch::is_valid` already validated both physical addresses. - let heki_patch_pa_0 = PhysAddr::new(heki_patch.pa[0]); - let heki_patch_pa_1 = PhysAddr::new(heki_patch.pa[1]); - - let patch = &heki_patch.code[..usize::from(heki_patch.size)]; - if patch.is_empty() { - return Ok(()); - } - - if heki_patch_pa_1.is_null() - || (heki_patch_pa_0.align_up(Size4KiB::SIZE) == heki_patch_pa_1.align_down(Size4KiB::SIZE)) - { - // Single contiguous span: either fits in one page (pa_1 null) or pa_1 is the - // adjacent next page. `HekiPatch::is_valid` enforces this; assert in debug builds. - debug_assert!( - !heki_patch_pa_1.is_null() - || heki_patch_pa_0.as_u64() + patch.len() as u64 - <= heki_patch_pa_0.align_down(Size4KiB::SIZE).as_u64() + Size4KiB::SIZE, - "patch crosses page boundary but pa_1 is null" - ); - // The patch was validated against VTL1's precomputed HEKI patch data. - let ptr = PrivilegedVtl0PhysMutPtr::::with_contiguous_pages( - heki_patch_pa_0.as_u64().trunc(), - patch.len(), - ) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - ptr.write_slice_at_offset(0, patch) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - } else { - let pages = [ - PhysPageAddr::::new( - heki_patch_pa_0.align_down(Size4KiB::SIZE).as_u64().trunc(), - ) - .ok_or(VsmError::Vtl0CopyFailed)?, - PhysPageAddr::::new(heki_patch_pa_1.as_u64().trunc()) - .ok_or(VsmError::Vtl0CopyFailed)?, - ]; - // The patch was validated against VTL1's precomputed HEKI patch data. - let ptr = PrivilegedVtl0PhysMutPtr::::new( - &pages, - (heki_patch_pa_0 - heki_patch_pa_0.align_down(Size4KiB::SIZE)).trunc(), - ) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - ptr.write_slice_at_offset(0, patch) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - } - Ok(()) -} - -fn mshv_vsm_allocate_ringbuffer_memory(phys_addr: u64, size: usize) -> Result { - if crate::platform_low().vtl0_kernel_info.check_end_of_boot() { - return Err(VsmError::OperationAfterEndOfBoot("ring buffer allocation")); - } - - let end = phys_addr - .checked_add(size as u64) - .ok_or(VsmError::IntegerOverflow) - .and_then(|end| PhysAddr::try_new(end).map_err(|_| VsmError::InvalidPhysicalAddress))?; - let phys_addr = PhysAddr::new(phys_addr); - protect_physical_memory_range( - PhysFrame::range( - PhysFrame::from_start_address(phys_addr) - .map_err(|_| VsmError::AddressNotPageAligned)?, - PhysFrame::from_start_address(end).map_err(|_| VsmError::AddressNotPageAligned)?, - ), - MemAttr::MEM_ATTR_READ, - )?; - set_ringbuffer(phys_addr, size); - debug_serial_println!("VSM: Ring buffer allocated"); - Ok(0) -} - -/// This function sets the platform root key by copying key data from VTL0. -/// -/// - `key_pa`: Physical address (VTL0) that the platform root key is stored at. -/// -/// This function assumes that the caller stores key bytes in a single or -/// contiguous physical memory page(s), whose length is equal to `PRK_LEN`. -fn mshv_vsm_set_platform_root_key(key_pa: u64) -> Result { - if crate::platform_low().vtl0_kernel_info.check_end_of_boot() { - return Err(VsmError::OperationAfterEndOfBoot("set platform root key")); - } - - let key_pa = PhysAddr::try_new(key_pa).map_err(|_| VsmError::InvalidPhysicalAddress)?; - - let mut keybuf = Zeroizing::new([0u8; PRK_LEN]); - let key_ptr = - Vtl0PhysConstPtr::::with_contiguous_pages(key_pa.as_u64().trunc(), PRK_LEN) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - key_ptr - .read_slice_at_offset(0, &mut *keybuf) - .map_err(|_| VsmError::Vtl0CopyFailed)?; - set_platform_root_key(&*keybuf); - Ok(0) -} - -/// VSM function dispatcher -pub fn vsm_dispatch(func_id: VsmFunction, params: &[u64]) -> i64 { - let result: Result = match func_id { - VsmFunction::EnableAPsVtl => mshv_vsm_enable_aps(params[0]), - VsmFunction::BootAPs => mshv_vsm_boot_aps(params[0]), - VsmFunction::LockRegs => mshv_vsm_lock_regs(), - VsmFunction::SignalEndOfBoot => Ok(mshv_vsm_end_of_boot()), - VsmFunction::ProtectMemory => mshv_vsm_protect_memory(params[0], params[1]), - VsmFunction::LoadKData => mshv_vsm_load_kdata(params[0], params[1]), - VsmFunction::ValidateModule => { - mshv_vsm_validate_guest_module(params[0], params[1], params[2]) - } - #[allow(clippy::cast_possible_wrap)] - VsmFunction::FreeModuleInit => mshv_vsm_free_guest_module_init(params[0] as i64), - #[allow(clippy::cast_possible_wrap)] - VsmFunction::UnloadModule => mshv_vsm_unload_guest_module(params[0] as i64), - VsmFunction::CopySecondaryKey => mshv_vsm_copy_secondary_key(params[0], params[1]), - VsmFunction::KexecValidate => mshv_vsm_kexec_validate(params[0], params[1], params[2]), - VsmFunction::PatchText => mshv_vsm_patch_text(params[0], params[1]), - VsmFunction::AllocateRingbufferMemory => { - let size: usize = params[1].trunc(); - mshv_vsm_allocate_ringbuffer_memory(params[0], size) - } - VsmFunction::SetPlatformRootKey => mshv_vsm_set_platform_root_key(params[0]), - VsmFunction::OpteeMessage => Err(VsmError::OperationNotSupported("OP-TEE communication")), - }; - match result { - Ok(value) => value, - Err(e) => Errno::from(e).as_neg().into(), - } -} - -pub const NUM_CONTROL_REGS: usize = 11; - -/// Data structure for maintaining MSRs and control registers whose values are locked. -/// This structure is expected to be stored in per-core kernel context, so we do not protect it with a lock. -#[derive(Debug, Clone, Copy)] -pub struct ControlRegMap { - pub entries: [(u32, u64); NUM_CONTROL_REGS], -} - -impl ControlRegMap { - pub fn init(&mut self) { - [ - HV_X64_REGISTER_CR0, - HV_X64_REGISTER_CR4, - HV_X64_REGISTER_LSTAR, - HV_X64_REGISTER_STAR, - HV_X64_REGISTER_CSTAR, - HV_X64_REGISTER_APIC_BASE, - HV_X64_REGISTER_EFER, - HV_X64_REGISTER_SYSENTER_CS, - HV_X64_REGISTER_SYSENTER_ESP, - HV_X64_REGISTER_SYSENTER_EIP, - HV_X64_REGISTER_SFMASK, - ] - .iter() - .enumerate() - .for_each(|(i, ®_name)| { - self.entries[i] = (reg_name, 0); - }); - } - - pub fn get(&self, reg_name: u32) -> Option { - for entry in &self.entries { - if entry.0 == reg_name { - return Some(entry.1); - } - } - None - } - - pub fn set(&mut self, reg_name: u32, value: u64) { - for entry in &mut self.entries { - if entry.0 == reg_name { - entry.1 = value; - return; - } - } - } - - // consider implementing a mutable iterator (if we plan to lock many control registers) - pub fn reg_names(&self) -> [u32; NUM_CONTROL_REGS] { - let mut names = [0; NUM_CONTROL_REGS]; - for (i, entry) in self.entries.iter().enumerate() { - names[i] = entry.0; - } - names - } -} - -#[allow(clippy::unnecessary_wraps)] -fn save_vtl0_locked_regs() -> Result { - let reg_names = with_per_cpu_variables(|per_cpu_variables| { - let mut regs = per_cpu_variables.vtl0_locked_regs.get(); - regs.init(); - per_cpu_variables.vtl0_locked_regs.set(regs); - regs.reg_names() - }); - for reg_name in reg_names { - if let Ok(value) = hvcall_get_vp_vtl0_registers(reg_name) { - with_per_cpu_variables(|per_cpu_variables| { - let mut regs = per_cpu_variables.vtl0_locked_regs.get(); - regs.set(reg_name, value); - per_cpu_variables.vtl0_locked_regs.set(regs); - }); - } - } - - Ok(0) -} - -/// Data structure for maintaining the kernel information in VTL0. -/// It should be prepared by copying kernel data from VTL0 to VTL1 instead of -/// relying on shared memory access to VTL0 which suffers from security issues. -pub struct Vtl0KernelInfo { - module_memory_metadata: ModuleMemoryMetadataMap, - boot_done: AtomicBool, - system_certs: once_cell::race::OnceBox>, - kexec_metadata: KexecMemoryMetadataWrapper, - crash_kexec_metadata: KexecMemoryMetadataWrapper, - precomputed_patches: PatchDataMap, - symbols: SymbolTable, - gpl_symbols: SymbolTable, - // TODO: revocation cert, blocklist, etc. -} - -impl Default for Vtl0KernelInfo { - fn default() -> Self { - Self::new() - } -} - -impl Vtl0KernelInfo { - pub fn new() -> Self { - Self { - module_memory_metadata: ModuleMemoryMetadataMap::new(), - boot_done: AtomicBool::new(false), - system_certs: once_cell::race::OnceBox::new(), - kexec_metadata: KexecMemoryMetadataWrapper::new(), - crash_kexec_metadata: KexecMemoryMetadataWrapper::new(), - precomputed_patches: PatchDataMap::new(), - symbols: SymbolTable::new(), - gpl_symbols: SymbolTable::new(), - } - } - - /// This function records the end of the VTL0 boot process. - pub(crate) fn set_end_of_boot(&self) { - self.boot_done - .store(true, core::sync::atomic::Ordering::SeqCst); - } - - /// This function checks whether the VTL0 boot process is done. VTL1 kernel relies on this function - /// to lock down certain security-critical VSM functions. - pub fn check_end_of_boot(&self) -> bool { - self.boot_done.load(core::sync::atomic::Ordering::SeqCst) - } - - pub fn set_system_certificates(&self, certs: Vec) { - let boxed_slice = certs.into_boxed_slice(); - let _ = self.system_certs.set(boxed_slice.into()); - } - - pub fn get_system_certificates(&self) -> Option<&[Certificate]> { - self.system_certs.get().map(|b| &**b) - } - - /// This function finds the precomputed patch data corresponding to the input patch data. - /// - /// Each step of `text_poke_bp_batch` only exposes a portion of the target's address range, - /// so we look up in the precomputed map by two keys derived from `patch_data.pa[0]`: - /// - `pa[0]` matches step 1 or 3 (target's first byte) and, for a precomputed patch that - /// straddles at offset 1, step 2. - /// - `pa[0] - 1` matches step 2 where `patch.pa[0] == precomputed.pa[0] + 1`. - /// - /// No legitimate step requires looking up by `patch.pa[1]`. - pub fn find_precomputed_patch(&self, patch_data: &HekiPatch) -> Option { - // `HekiPatch::is_valid` already validated both physical addresses. - let patch_pa_0 = PhysAddr::new(patch_data.pa[0]); - let patch_pa_0_prev = patch_data.pa[0].checked_sub(1).map(PhysAddr::new); - - self.precomputed_patches - .get(patch_pa_0) - .or_else(|| patch_pa_0_prev.and_then(|pa| self.precomputed_patches.get(pa))) - .or(None) - } -} - -/// Data structure for maintaining the memory ranges of each VTL0 kernel module and their types -pub struct ModuleMemoryMetadataMap { - inner: spin::mutex::SpinMutex>, - key_gen: AtomicI64, -} - -pub struct ModuleMemoryMetadata { - ranges: Vec, - patch_targets: Vec, -} - -impl ModuleMemoryMetadata { - pub fn new() -> Self { - Self { - ranges: Vec::new(), - patch_targets: Vec::new(), - } - } - - #[inline] - pub(crate) fn insert_heki_range(&mut self, heki_range: &HekiRange) { - // `HekiRange::is_valid` already validated these addresses. - let va = heki_range.va; - let pa = heki_range.pa; - let epa = heki_range.epa; - self.insert_memory_range(ModuleMemoryRange::new_checked( - va, - pa, - epa, - heki_range.mod_mem_type(), - )); - } - - #[inline] - pub(crate) fn insert_memory_range(&mut self, mem_range: ModuleMemoryRange) { - self.ranges.push(mem_range); - } - - #[inline] - pub(crate) fn insert_patch_target(&mut self, patch_target: PhysAddr) { - self.patch_targets.push(patch_target); - } - - // This function returns patch targets belonging to this module to remove them - // from the precomputed patch data map when the module is unloaded. - #[inline] - pub(crate) fn get_patch_targets(&self) -> &Vec { - &self.patch_targets - } -} - -impl Default for ModuleMemoryMetadata { - fn default() -> Self { - Self::new() - } -} - -impl ModuleMemoryMetadata { - /// Returns an iterator over the memory ranges. - pub fn iter(&self) -> core::slice::Iter<'_, ModuleMemoryRange> { - self.ranges.iter() - } -} - -impl<'a> IntoIterator for &'a ModuleMemoryMetadata { - type Item = &'a ModuleMemoryRange; - type IntoIter = core::slice::Iter<'a, ModuleMemoryRange>; - - fn into_iter(self) -> Self::IntoIter { - self.ranges.iter() - } -} - -#[derive(Clone, Copy)] -pub struct ModuleMemoryRange { - pub virt_addr: VirtAddr, - pub phys_frame_range: PhysFrameRange, - pub mod_mem_type: ModMemType, -} - -impl ModuleMemoryRange { - /// Create a memory range from values which are already validated. - pub(crate) fn new_checked( - virt_addr: u64, - phys_start: u64, - phys_end: u64, - mod_mem_type: ModMemType, - ) -> Self { - let phys_start = PhysAddr::new(phys_start); - let phys_end = PhysAddr::new(phys_end); - Self { - virt_addr: VirtAddr::new(virt_addr), - phys_frame_range: PhysFrame::range( - PhysFrame::containing_address(phys_start), - PhysFrame::containing_address(phys_end), - ), - mod_mem_type, - } - } - - pub fn new( - virt_addr: u64, - phys_start: u64, - phys_end: u64, - mod_mem_type: ModMemType, - ) -> Result { - Ok(Self { - virt_addr: VirtAddr::try_new(virt_addr).map_err(|_| VsmError::InvalidVirtualAddress)?, - phys_frame_range: PhysFrame::range( - PhysFrame::containing_address( - PhysAddr::try_new(phys_start).map_err(|_| VsmError::InvalidPhysicalAddress)?, - ), - PhysFrame::containing_address( - PhysAddr::try_new(phys_end).map_err(|_| VsmError::InvalidPhysicalAddress)?, - ), - ), - mod_mem_type, - }) - } -} - -impl Default for ModuleMemoryRange { - fn default() -> Self { - Self { - virt_addr: VirtAddr::zero(), - phys_frame_range: PhysFrame::range( - PhysFrame::containing_address(PhysAddr::zero()), - PhysFrame::containing_address(PhysAddr::zero()), - ), - mod_mem_type: ModMemType::Unknown, - } - } -} - -impl ModuleMemoryMetadataMap { - pub fn new() -> Self { - Self { - inner: spin::mutex::SpinMutex::new(HashMap::new()), - key_gen: AtomicI64::new(0), - } - } - - /// Generate a unique key for representing each loaded kernel module. - /// It assumes a 64-bit atomic counter is sufficient and there is no run out of keys. - fn gen_unique_key(&self) -> i64 { - self.key_gen.fetch_add(1, Ordering::Relaxed) - } - - pub fn contains_key(&self, key: i64) -> bool { - self.inner.lock().contains_key(&key) - } - - /// Register a new module memory metadata structure in the map and return a unique key/token for it. - pub(crate) fn register_module_memory_metadata( - &self, - module_memory: ModuleMemoryMetadata, - ) -> i64 { - let key = self.gen_unique_key(); - - let mut map = self.inner.lock(); - assert!( - !map.contains_key(&key), - "VSM: Key {key} already exists in the module memory map", - ); - let _ = map.insert(key, module_memory); - - key - } - - pub(crate) fn remove(&self, key: i64) -> bool { - let mut map = self.inner.lock(); - map.remove(&key).is_some() - } - - /// Drop a module's freed init ranges from its metadata after [`mshv_vsm_free_guest_module_init`] - /// hands them back to VTL0, so a later free/unload does not re-release them. - /// - /// It also returns patch targets that fell within this freed init frames. These patch targets - /// are no longer valid (i.e., potential patch-after-free) and thus their corresponding - /// precomputed patches should be removed (we can't remove them here due to locks). - fn remove_init_ranges(&self, key: i64) -> Vec { - let is_init = |t| { - matches!( - t, - ModMemType::InitText | ModMemType::InitData | ModMemType::InitRoData - ) - }; - let mut map = self.inner.lock(); - let Some(metadata) = map.get_mut(&key) else { - return Vec::new(); - }; - let init_ranges: Vec> = metadata - .ranges - .iter() - .filter(|r| is_init(r.mod_mem_type)) - .map(|r| r.phys_frame_range) - .collect(); - metadata.ranges.retain(|r| !is_init(r.mod_mem_type)); - let mut freed_patch_targets = Vec::new(); - metadata.patch_targets.retain(|&pa| { - let freed = init_ranges - .iter() - .any(|fr| fr.start.start_address() <= pa && fr.end.start_address() > pa); - if freed { - freed_patch_targets.push(pa); - false - } else { - true - } - }); - freed_patch_targets - } - - /// Return the addresses of patch targets belonging to a module identified by `key` - pub(crate) fn get_patch_targets(&self, key: i64) -> Option> { - let guard = self.inner.lock(); - guard - .get(&key) - .map(|metadata| metadata.get_patch_targets().clone()) - } - - pub fn iter_entry(&self, key: i64) -> Option> { - let guard = self.inner.lock(); - if guard.contains_key(&key) { - Some(ModuleMemoryMetadataIters { - guard, - key, - phantom: core::marker::PhantomData, - }) - } else { - None - } - } -} - -impl Default for ModuleMemoryMetadataMap { - fn default() -> Self { - Self::new() - } -} - -pub struct ModuleMemoryMetadataIters<'a> { - guard: spin::mutex::SpinMutexGuard<'a, HashMap>, - key: i64, - phantom: core::marker::PhantomData<&'a PhysFrameRange>, -} - -impl<'a> ModuleMemoryMetadataIters<'a> { - /// Returns an iterator over the memory ranges. - /// - /// # Panics - /// - /// Panics if the key is not found in the guard. - pub fn iter_mem_ranges(&'a self) -> impl Iterator { - self.guard.get(&self.key).unwrap().ranges.iter() - } -} - -/// This function copies `HekiPage` structures from VTL0 and returns a vector of them. -/// `pa` and `nranges` specify the physical address range containing one or more than one `HekiPage` structures. -fn copy_heki_pages_from_vtl0(pa: u64, nranges: u64) -> Option> { - let mut heki_pages = Vec::with_capacity(nranges.trunc()); - let mut visited_pages = HashSet::new(); - let mut range: u64 = 0; - - let mut cur_pa = PhysAddr::try_new(pa).ok()?; - while range < nranges { - if visited_pages.contains(&cur_pa.as_u64()) { - return None; - } - let ptr = - Vtl0PhysConstPtr::::with_usize(cur_pa.as_u64().trunc()).ok()?; - let heki_page = ptr.read_at_offset(0).ok()?; - if !heki_page.is_valid() { - return None; - } - visited_pages.insert(cur_pa.as_u64()); - - range = range.checked_add(heki_page.nranges)?; - if range < nranges && (heki_page.next_pa == 0 || visited_pages.contains(&heki_page.next_pa)) - { - return None; - } - // `HekiPage::is_valid` already validated `next_pa`. - cur_pa = PhysAddr::new(heki_page.next_pa); - heki_pages.push(*heki_page); - } - - Some(heki_pages) -} - -/// Registry of VTL0 frames that are non-writable to VTL0 or reserved by in-flight module or kexec -/// validation. Ordinary writable mappings retain shared access for their lifetime; reservations and -/// VTL0 protection updates use exclusive access. Privileged HEKI and ring-buffer mappings bypass -/// the registry. -pub(crate) struct ProtectedFrameRegistry { - frames: SpinRwLock>, -} - -/// Opaque guard that holds shared registry access for an ordinary writable mapping, blocking -/// exclusive protection and reservation updates until dropped. -pub(crate) struct ProtectedFrameAccessGuard<'a> { - _guard: spin::rwlock::RwLockReadGuard<'a, RangeSet>, -} - -struct ProtectedFrameUpdateGuard<'a> { - guard: spin::rwlock::RwLockWriteGuard<'a, RangeSet>, -} - -impl ProtectedFrameUpdateGuard<'_> { - fn overlaps(&self, range: &Range) -> bool { - self.guard.overlaps(range) - } - - fn insert(&mut self, range: Range) { - self.guard.insert(range); - } - - fn remove(&mut self, range: Range) { - self.guard.remove(range); - } - - fn record_protection(&mut self, phys_frame_range: PhysFrameRange, protect: bool) { - let start = phys_frame_range.start.start_address().as_u64(); - let end = phys_frame_range.end.start_address().as_u64(); - if start >= end { - return; - } - if protect { - self.insert(start..end); - } else { - self.remove(start..end); - } - } -} - -impl ProtectedFrameRegistry { - fn new() -> Self { - Self { - frames: SpinRwLock::new(RangeSet::new()), - } - } - - /// Validates that no requested page is registered as protected or reserved and returns a shared - /// guard that prevents protection or reservation updates until dropped. - pub(crate) fn acquire_access_guard( - &self, - pages: &litebox_common_linux::vmap::PhysPageAddrArray, - ) -> Result, litebox_common_linux::vmap::PhysPointerError> { - let guard = self.frames.read(); - for page in pages { - let start = page.as_usize() as u64; - let end = start - .checked_add(ALIGN as u64) - .ok_or(litebox_common_linux::vmap::PhysPointerError::Overflow)?; - if guard.overlaps(&(start..end)) { - return Err( - litebox_common_linux::vmap::PhysPointerError::InvalidPhysicalAddress( - page.as_usize(), - ), - ); - } - } - Ok(ProtectedFrameAccessGuard { _guard: guard }) - } - - /// Runs `f` with exclusive registry access. - fn with_exclusive(&self, f: impl FnOnce(&mut ProtectedFrameUpdateGuard<'_>) -> R) -> R { - f(&mut ProtectedFrameUpdateGuard { - guard: self.frames.write(), - }) - } -} - -pub(crate) fn protected_frame_registry() -> &'static ProtectedFrameRegistry { - static REGISTRY: Once = Once::new(); - REGISTRY.call_once(ProtectedFrameRegistry::new) -} - -/// Protect a VTL0 physical memory range using VTL protection mask (e.g., kernel code integrity). -/// -/// The registry tracks non-writable VTL0 ranges and temporary validation reservations. -/// See [`protected_frame_registry`]. -/// -/// If the requested range overlaps with VTL1 working memory, the VTL1 portion is silently -/// skipped and only the remaining VTL0 portions are protected. If the range falls entirely -/// within VTL1, this function returns `Ok(())` without issuing a hypercall. -/// -/// `phys_frame_range` specifies the range whose VTL0 permissions are updated; VTL1 working-memory -/// portions are ignored. -/// `mem_attr` specifies the memory attributes (VTL0's allowed access) to be applied. -pub(crate) fn protect_physical_memory_range( - phys_frame_range: PhysFrameRange, - mem_attr: MemAttr, -) -> Result<(), VsmError> { - let protect = !mem_attr.contains(MemAttr::MEM_ATTR_WRITE); - let vtl1_range = crate::platform_low().vtl1_phys_frame_range(); - - // Range fully within VTL1 — nothing to protect for VTL0. - if phys_frame_range.start >= vtl1_range.start && phys_frame_range.end <= vtl1_range.end { - return Ok(()); + // Range fully within VTL1 — nothing to protect for VTL0. + if phys_frame_range.start >= vtl1_range.start && phys_frame_range.end <= vtl1_range.end { + return Ok(()); } // Fast path: no overlap with VTL1 — protect the entire range directly. @@ -1736,7 +517,7 @@ pub(crate) fn protect_physical_memory_range( if !overlaps_vtl1 { let pa = phys_frame_range.start.start_address().as_u64(); let num_pages = phys_frame_range.count() as u64; - hv_modify_vtl_protection_mask(pa, num_pages, mem_attr_to_hv_page_prot_flags(mem_attr)) + hv_modify_vtl_protection_mask(pa, num_pages, page_prot) .map_err(VsmError::HypercallFailed)?; protected.record_protection(phys_frame_range, protect); return Ok(()); @@ -1761,7 +542,7 @@ pub(crate) fn protect_physical_memory_range( } let pa = sub_range.start.start_address().as_u64(); let num_pages = sub_range.count() as u64; - hv_modify_vtl_protection_mask(pa, num_pages, mem_attr_to_hv_page_prot_flags(mem_attr)) + hv_modify_vtl_protection_mask(pa, num_pages, page_prot) .map_err(VsmError::HypercallFailed)?; protected.record_protection(sub_range, protect); } @@ -1769,651 +550,244 @@ pub(crate) fn protect_physical_memory_range( }) } -/// Restore VTL0 read/write access while leaving execution disabled, and removes the registry entry. -fn unprotect_physical_memory_range( +/// Restore VTL0 read/write access and remove the registry entry. +/// +/// This is `MEM_ATTR_READ | MEM_ATTR_WRITE` expressed in hypervisor flags, so +/// it also restores user-mode execute — see [`mem_attr_to_hv_page_prot_flags`] +/// for why that rides along with read. +pub(crate) fn unprotect_physical_memory_range( phys_frame_range: PhysFrameRange, ) -> Result<(), VsmError> { protect_physical_memory_range( phys_frame_range, - MemAttr::MEM_ATTR_READ | MemAttr::MEM_ATTR_WRITE, + HvPageProtFlags::HV_PAGE_READABLE + | HvPageProtFlags::HV_PAGE_USER_EXECUTABLE + | HvPageProtFlags::HV_PAGE_WRITABLE, ) } -/// This function is a variant of [`protect_physical_memory_range`] to protect a VTL1 physical memory range. -/// Unlike [`protect_physical_memory_range`], this is intended exclusively for securing VTL1's own pages. -/// VTL0 should never access VTL1 memory, so the memory attribute is always empty (no read, write, or execute). -/// -/// Note. This function doesn't check whether `phys_frame_range` belongs to VTL1 because it is called by BSP -/// before the kernel platform data structure is initialized. To this end, one might call this function with -/// a VTL0 physical memory range which only restricts access to the range. -#[inline] -fn protect_vtl1_physical_memory_range( - phys_frame_range: PhysFrameRange, -) -> Result<(), VsmError> { - let pa = phys_frame_range.start.start_address().as_u64(); - let num_pages = phys_frame_range.count() as u64; - if num_pages > 0 { - hv_modify_vtl_protection_mask( - pa, - num_pages, - mem_attr_to_hv_page_prot_flags(MemAttr::empty()), - ) - .map_err(VsmError::HypercallFailed)?; - } - Ok(()) -} - -/// Data structure for maintaining the memory content of a kernel module by its sections. Currently, it only maintains -/// certain sections like `.text` and `.init.text` which are needed for module validation. -pub struct ModuleMemory { - text: MemoryContainer, - init_text: MemoryContainer, - init_rodata: MemoryContainer, -} - -impl Default for ModuleMemory { - fn default() -> Self { - Self::new() - } -} - -impl ModuleMemory { - pub fn new() -> Self { - Self { - text: MemoryContainer::new(), - init_text: MemoryContainer::new(), - init_rodata: MemoryContainer::new(), - } - } - - /// Return a memory container for a section of the module memory by its name - pub fn find_section_by_name(&self, name: &str) -> Option<&MemoryContainer> { - match name { - ".text" => Some(&self.text), - ".init.text" => Some(&self.init_text), - ".init.rodata" => Some(&self.init_rodata), - _ => None, - } - } - - /// Write physical memory bytes from VTL0 specified in `HekiRange` at the specified virtual address of - /// a certain memory container based on the memory/section type. - #[inline] - pub(crate) fn write_bytes_from_heki_range(&mut self) -> Result<(), MemoryContainerError> { - self.text.write_bytes_from_heki_range()?; - self.init_text.write_bytes_from_heki_range()?; - self.init_rodata.write_bytes_from_heki_range()?; - Ok(()) - } - - pub(crate) fn extend_range( - &mut self, - mod_mem_type: ModMemType, - heki_range: &HekiRange, - ) -> Result<(), VsmError> { - match mod_mem_type { - ModMemType::Text => self.text.extend_range(heki_range)?, - ModMemType::InitText => self.init_text.extend_range(heki_range)?, - ModMemType::InitRoData => self.init_rodata.extend_range(heki_range)?, - _ => {} - } - Ok(()) - } -} +// --- The gates: platform implementation of the capability traits ----------- -/// Data structure for abstracting addressable paged memory. Unlike `ModuleMemoryMetadataMap` which maintains -/// physical/virtual address ranges and their access permissions, this structure stores actual data in memory pages. -/// This structure allows us to handle data copied from VTL0 (e.g., for virtual-address-based page sorting) without -/// explicit page mappings at VTL1. -/// This structure is expected to be used locally and temporarily, so we do not protect it with a lock. -#[derive(Clone, Copy)] -struct MemoryRange { - addr: VirtAddr, - phys_addr: PhysAddr, - len: u64, +/// Zero-sized capability implementing [`Vtl0Gate`]: mediated access to the +/// untrusted VTL0. Held by the HEKI service. +pub struct LvbsVtl0Gate { + /// Private, so the capability is built only via [`LvbsVtl0Gate::mint`], + /// never a bare literal. + _private: (), } -pub struct MemoryContainer { - range: Vec, - buf: Vec, +/// Zero-sized capability implementing [`Vtl1Gate`]: the VTL1 setup steps VTL0 +/// may request. Held by the runner. +pub struct LvbsVtl1Gate { + /// Private, so the capability is built only via [`LvbsVtl1Gate::mint`], + /// never a bare literal. + _private: (), } -impl Default for MemoryContainer { - fn default() -> Self { - Self::new() - } +/// Zero-sized capability implementing [`Vtl0PrivilegedWrite`]: VTL0 writes with +/// the protection masks bypassed. +/// +/// Deliberately its own type rather than a method on [`LvbsVtl0Gate`], so this +/// authority is granted per-operation and nothing holds it incidentally. Like a +/// `PunchthroughToken`, it is an auditability aid rather than a boundary: it +/// funnels every protection-bypassing write through one greppable mint point. +pub struct LvbsVtl0PrivilegedWriter { + /// Private, so the capability is built only via + /// [`LvbsVtl0PrivilegedWriter::mint`], never a bare literal. + _private: (), } -impl MemoryContainer { - pub fn new() -> Self { - Self { - range: Vec::new(), - buf: Vec::new(), - } - } - - /// Return the byte length of the memory container - pub fn len(&self) -> usize { - self.buf.len() - } - - /// Check if the memory container is empty - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn get_range(&self) -> Option> { - let start_range = self.range.first()?; - let end_range = self.range.last()?; - let end = end_range.addr.as_u64().checked_add(end_range.len)?; - Some(Range { - start: start_range.addr, - end: VirtAddr::try_new(end).ok()?, - }) - } - - pub(crate) fn extend_range(&mut self, heki_range: &HekiRange) -> Result<(), VsmError> { - // `HekiRange::is_valid` already validated the addresses and `pa <= epa`. - let addr = VirtAddr::new(heki_range.va); - let phys_addr = PhysAddr::new(heki_range.pa); - let len = heki_range.epa - heki_range.pa; - if let Some(last_range) = self.range.last() - && VirtAddr::try_new( - last_range - .addr - .as_u64() - .checked_add(last_range.len) - .ok_or(VsmError::IntegerOverflow)?, - ) - .map_err(|_| VsmError::InvalidVirtualAddress)? - != addr - { - debug_serial_println!("Discontiguous address found {heki_range:?}"); - // NOTE: Intentionally not returning an error here. - // TODO: This should be an error once patch_info is fixed from VTL0 - // It will simplify patch_info and heki_range parsing as well - } - self.range.push(MemoryRange { - addr, - phys_addr, - len, - }); - Ok(()) - } - - /// Write physical memory bytes from VTL0 specified in `HekiRange` at the specified virtual address - #[inline] - pub(crate) fn write_bytes_from_heki_range(&mut self) -> Result<(), MemoryContainerError> { - let mut len: usize = 0; - if self.buf.is_empty() { - for range in &self.range { - let range_len: usize = range.len.trunc(); - len = len - .checked_add(range_len) - .ok_or(MemoryContainerError::Overflow)?; - } - self.buf.reserve_exact(len); - } - - let range = self.range.clone(); - for range in range { - let phys_end = range - .phys_addr - .as_u64() - .checked_add(range.len) - .and_then(|end| PhysAddr::try_new(end).ok()) - .ok_or(MemoryContainerError::Overflow)?; - self.write_vtl0_phys_bytes(range.phys_addr, phys_end)?; - } - Ok(()) - } - - /// Write physical memory bytes from VTL0 at the specified physical address - pub(crate) fn write_vtl0_phys_bytes( - &mut self, - phys_start: PhysAddr, - phys_end: PhysAddr, - ) -> Result<(), MemoryContainerError> { - let bytes_to_copy: usize = (phys_end - phys_start).trunc(); - if bytes_to_copy == 0 { - return Ok(()); - } - - let ptr = Vtl0PhysConstPtr::::with_contiguous_pages( - phys_start.as_u64().trunc(), - bytes_to_copy, - ) - .map_err(|_| MemoryContainerError::CopyFromVtl0Failed)?; - - let old_len = self.buf.len(); - self.buf.resize(old_len + bytes_to_copy, 0); - if ptr - .read_slice_at_offset(0, &mut self.buf[old_len..]) - .is_err() - { - self.buf.truncate(old_len); - return Err(MemoryContainerError::CopyFromVtl0Failed); - } - Ok(()) +impl LvbsVtl0Gate { + /// Mint the VTL0 mediation capability. Reserved for VTL1-trusted + /// composition-root code (the runner). + #[must_use] + pub fn mint() -> Self { + Self { _private: () } } } -impl core::ops::Deref for MemoryContainer { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.buf +impl LvbsVtl1Gate { + /// Mint the VTL1 setup capability. Reserved for VTL1-trusted + /// composition-root code (the runner). + #[must_use] + pub fn mint() -> Self { + Self { _private: () } } } -/// Errors for memory container operations. -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum MemoryContainerError { - #[error("failed to copy data from VTL0")] - CopyFromVtl0Failed, - #[error("integer overflow while processing VTL0 memory")] - Overflow, -} - -pub struct KexecMemoryMetadataWrapper { - inner: spin::mutex::SpinMutex, -} - -impl Default for KexecMemoryMetadataWrapper { - fn default() -> Self { - Self::new() +impl LvbsVtl0PrivilegedWriter { + /// Mint the protection-mask-bypassing write capability. The audit point for + /// every privileged VTL0 write. + #[must_use] + pub fn mint() -> Self { + Self { _private: () } } } -impl KexecMemoryMetadataWrapper { - pub fn new() -> Self { - Self { - inner: spin::mutex::SpinMutex::new(KexecMemoryMetadata::new()), - } - } - - pub(crate) fn clear_memory(&self) { - let mut inner = self.inner.lock(); - inner.clear(); +/// Maps a [`MemAttr`] permission set (the Vtl0Gate permission type) to the +/// corresponding Hyper-V page-protection flags. +/// Maps a [`MemAttr`] permission set to hypervisor page-protection flags. +/// +/// `HV_PAGE_USER_EXECUTABLE` accompanies read rather than exec: Hyper-V +/// requires it for compatibility, so a VTL0 frame that HEKI marks read-only is +/// still user-executable. Intentional. +/// [`MemAttr::MEM_ATTR_EXEC`]. +pub(crate) fn mem_attr_to_hv_page_prot_flags(attr: MemAttr) -> HvPageProtFlags { + let mut flags = HvPageProtFlags::empty(); + if attr.contains(MemAttr::MEM_ATTR_READ) { + flags.set(HvPageProtFlags::HV_PAGE_READABLE, true); + flags.set(HvPageProtFlags::HV_PAGE_USER_EXECUTABLE, true); } - - pub(crate) fn register_memory(&self, kexec_memory: KexecMemoryMetadata) { - let mut inner = self.inner.lock(); - inner.ranges = kexec_memory.ranges; + if attr.contains(MemAttr::MEM_ATTR_WRITE) { + flags.set(HvPageProtFlags::HV_PAGE_WRITABLE, true); } - - pub fn iter_guarded(&self) -> KexecMemoryMetadataIters<'_> { - KexecMemoryMetadataIters { - guard: self.inner.lock(), - phantom: core::marker::PhantomData, - } + if attr.contains(MemAttr::MEM_ATTR_EXEC) { + flags.set(HvPageProtFlags::HV_PAGE_EXECUTABLE, true); } + flags } -// TODO: `ModuleMemoryMetadata` and `KexecMemoryMetadata` are similar. consider merging them into a single structure if possible. -pub struct KexecMemoryMetadata { - ranges: Vec, +/// Restricted transaction handle for a `protect_frames_transactionally` closure. +/// Wraps the private platform [`FrameReservation`] guard so the service can +/// never hold or leak a reservation across the trait boundary. +struct PlatformFrameTxn<'a> { + guard: &'a mut FrameReservation, } -impl KexecMemoryMetadata { - pub fn new() -> Self { - Self { ranges: Vec::new() } - } - - #[inline] - pub(crate) fn insert_heki_range(&mut self, heki_range: &HekiRange) -> Result<(), VsmError> { - // `HekiRange::is_valid` already validated these addresses. - if !heki_range.is_aligned(Size4KiB::SIZE) { - return Err(VsmError::AddressNotPageAligned); - } - let va = heki_range.va; - let pa = heki_range.pa; - let epa = heki_range.epa; - self.insert_memory_range(KexecMemoryRange::new_checked(va, pa, epa)); - Ok(()) - } - - #[inline] - pub(crate) fn insert_memory_range(&mut self, mem_range: KexecMemoryRange) { - self.ranges.push(mem_range); - } - - #[inline] - pub(crate) fn clear(&mut self) { - self.ranges.clear(); +impl FrameTxn for PlatformFrameTxn<'_> { + fn reserve( + &mut self, + ranges: &[PhysFrameRange], + ) -> Result, VsmError> { + self.guard.reserve(ranges.iter().copied()) } -} -impl Default for KexecMemoryMetadata { - fn default() -> Self { - Self::new() + fn protect(&mut self, range: PhysFrameRange, attr: MemAttr) -> Result<(), VsmError> { + protect_physical_memory_range(range, mem_attr_to_hv_page_prot_flags(attr)) } } -impl KexecMemoryMetadata { - /// Returns an iterator over the memory ranges. - pub fn iter(&self) -> core::slice::Iter<'_, KexecMemoryRange> { - self.ranges.iter() +impl Vtl0Gate for LvbsVtl0Gate { + fn read_vtl0_pages( + &self, + pages: &[PhysPageAddr], + offset: usize, + out: &mut [u8], + ) -> Result<(), VsmError> { + let ptr = Vtl0PhysConstPtr::::new(pages, offset) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + ptr.read_slice_at_offset(0, out) + .map_err(|_| VsmError::Vtl0CopyFailed) } -} - -impl<'a> IntoIterator for &'a KexecMemoryMetadata { - type Item = &'a KexecMemoryRange; - type IntoIter = core::slice::Iter<'a, KexecMemoryRange>; - fn into_iter(self) -> Self::IntoIter { - self.ranges.iter() + fn protect_frames( + &self, + range: PhysFrameRange, + attr: MemAttr, + ) -> Result<(), VsmError> { + protect_physical_memory_range(range, mem_attr_to_hv_page_prot_flags(attr)) } -} - -pub struct KexecMemoryMetadataIters<'a> { - guard: spin::mutex::SpinMutexGuard<'a, KexecMemoryMetadata>, - phantom: core::marker::PhantomData<&'a PhysFrameRange>, -} -impl<'a> KexecMemoryMetadataIters<'a> { - pub fn iter_mem_ranges(&'a self) -> impl Iterator { - self.guard.ranges.iter() + fn unprotect_frames(&self, range: PhysFrameRange) -> Result<(), VsmError> { + unprotect_physical_memory_range(range) } -} -#[derive(Clone, Copy)] -pub struct KexecMemoryRange { - pub virt_addr: VirtAddr, - pub phys_frame_range: PhysFrameRange, -} - -impl KexecMemoryRange { - /// Create a memory range from values which are already validated. - pub(crate) fn new_checked(virt_addr: u64, phys_start: u64, phys_end: u64) -> Self { - let phys_start = PhysAddr::new(phys_start); - let phys_end = PhysAddr::new(phys_end); - Self { - virt_addr: VirtAddr::new(virt_addr), - phys_frame_range: PhysFrame::range( - PhysFrame::from_start_address(phys_start) - .expect("kexec memory start address is not page-aligned"), - PhysFrame::from_start_address(phys_end) - .expect("kexec memory end address is not page-aligned"), - ), + fn protect_frames_transactionally( + &self, + initial: &[PhysFrameRange], + f: &mut dyn FnMut(&mut dyn FrameTxn) -> Result<(), VsmError>, + ) -> Result<(), VsmError> { + let mut guard = FrameReservation::new(); + guard.reserve(initial.iter().copied())?; + let mut txn = PlatformFrameTxn { guard: &mut guard }; + let result = f(&mut txn); + if result.is_ok() { + txn.guard.commit(); } + // On `Err`, `guard` drops uncommitted, rolling back every reserved range. + result } - pub fn new(virt_addr: u64, phys_start: u64, phys_end: u64) -> Result { - let phys_start = - PhysAddr::try_new(phys_start).map_err(|_| VsmError::InvalidPhysicalAddress)?; - let phys_end = PhysAddr::try_new(phys_end).map_err(|_| VsmError::InvalidPhysicalAddress)?; - Ok(Self { - virt_addr: VirtAddr::try_new(virt_addr).map_err(|_| VsmError::InvalidVirtualAddress)?, - phys_frame_range: PhysFrame::range( - PhysFrame::from_start_address(phys_start) - .map_err(|_| VsmError::AddressNotPageAligned)?, - PhysFrame::from_start_address(phys_end) - .map_err(|_| VsmError::AddressNotPageAligned)?, - ), - }) + fn install_ringbuffer(&self, pa: u64, size: u64) { + let _ = crate::mshv::ringbuffer::set_ringbuffer(PhysAddr::new(pa), size.trunc()); } -} -impl Default for KexecMemoryRange { - fn default() -> Self { - Self { - virt_addr: VirtAddr::zero(), - phys_frame_range: PhysFrame::range( - PhysFrame::containing_address(PhysAddr::zero()), - PhysFrame::containing_address(PhysAddr::zero()), - ), - } + fn end_of_boot_reached(&self) -> bool { + crate::platform_low().end_of_boot_reached() } -} - -pub struct PatchDataMap { - inner: spin::rwlock::RwLock>, -} -impl Default for PatchDataMap { - fn default() -> Self { - Self::new() + fn lock_control_registers(&self) -> Result<(), VsmError> { + mshv_vsm_lock_regs().map(|_| ()) } } -impl PatchDataMap { - pub fn new() -> Self { - Self { - inner: spin::rwlock::RwLock::new(HashMap::new()), - } - } - - #[inline] - pub fn remove_patch_data(&self, patch_targets: &Vec) { - let mut inner = self.inner.write(); - for key in patch_targets { - inner.remove(key); - } - } - - #[inline] - pub fn get(&self, addr: PhysAddr) -> Option { - let inner = self.inner.read(); - inner.get(&addr).copied() - } - - /// Add patch data from a buffer containing `HekiPatchInfo` and `HekiPatch` structures. - /// If this patch data is from a module (`module_memory_metadata` is `Some`), this function - /// denies any patch target addresses not within the module's executable memory ranges. - pub fn insert_patch_data_from_bytes( +impl Vtl0PrivilegedWrite for LvbsVtl0PrivilegedWriter { + fn write_vtl0_pages( &self, - patch_info_buf: &[u8], - mut module_memory_metadata: Option<&mut ModuleMemoryMetadata>, - ) -> Result<(), PatchDataMapError> { - if patch_info_buf.len() < core::mem::size_of::() { - return Err(PatchDataMapError::InvalidHekiPatchInfo); - } - - let mut parsed: Vec<(PhysAddr, HekiPatch)> = Vec::new(); - - // the buffer looks like below: - // [`HekiPatchInfo`, [`HekiPatch`, ...], `HekiPatchInfo`, [`HekiPatch`, ...], ...] - // Each `HekiPatchInfo`'s `patch_index` field specifies the number of `HekiPatch` entries that follow it. - // The buffer may have trailing bytes (from page-aligned VTL0 ranges) that don't form a valid record. - let mut index: usize = 0; - while index + core::mem::size_of::() <= patch_info_buf.len() { - let Some(patch_info) = HekiPatchInfo::try_from_bytes( - &patch_info_buf[index..index + core::mem::size_of::()], - ) else { - // Remaining bytes don't form a valid header. End of meaningful patch data. - break; - }; - - let patch_index: usize = patch_info.patch_index.trunc(); - let total_patch_size = core::mem::size_of::() - .checked_mul(patch_index) - .ok_or(PatchDataMapError::InvalidHekiPatchInfo)?; - let patches_start = index - .checked_add(core::mem::size_of::()) - .ok_or(PatchDataMapError::InvalidHekiPatchInfo)?; - let patches_end = patches_start - .checked_add(total_patch_size) - .filter(|&end| end <= patch_info_buf.len()) - .ok_or(PatchDataMapError::InvalidHekiPatchInfo)?; - - for patch in patch_info_buf[patches_start..patches_end] - .chunks(core::mem::size_of::()) - .map(HekiPatch::try_from_bytes) - { - let patch = patch.ok_or(PatchDataMapError::InvalidHekiPatch)?; - // `HekiPatch::try_from_bytes` already validated both physical addresses. - let patch_target_pa_0 = PhysAddr::new(patch.pa[0]); - let patch_target_pa_1 = PhysAddr::new(patch.pa[1]); - - // The second page is used as an additional key when a patch straddles two physical - // pages (see `validate_text_poke_bp_batch`). - let straddles_second_page = !patch_target_pa_1.is_null() - && patch_target_pa_0 - .as_u64() - .checked_add(1) - .and_then(|next| PhysAddr::try_new(next).ok()) - .is_some_and(|next| next.is_aligned(Size4KiB::SIZE)); - - if let Some(ref mod_mem_meta) = module_memory_metadata { - // Only accept patch targets within the module's executable ranges. - let in_executable_range = mod_mem_meta.iter().any(|mod_mem_range| { - let in_range = |pa: PhysAddr| { - mod_mem_range.phys_frame_range.start.start_address() <= pa - && mod_mem_range.phys_frame_range.end.start_address() > pa - }; - matches!( - mod_mem_range.mod_mem_type, - ModMemType::Text | ModMemType::InitText - ) && in_range(patch_target_pa_0) - && (patch_target_pa_1.is_null() || in_range(patch_target_pa_1)) - }); - if !in_executable_range { - continue; - } - } - - parsed.push((patch_target_pa_0, patch)); - if straddles_second_page { - parsed.push((patch_target_pa_1, patch)); - } - } - index = patches_end; - } - - // Commit every parsed patch and record its targets for later unload cleanup. - let mut inner = self.inner.write(); - for (target, patch) in parsed { - inner.insert(target, patch); - if let Some(ref mut mod_mem_meta) = module_memory_metadata { - mod_mem_meta.insert_patch_target(target); - } - } - - Ok(()) - } -} - -/// Errors for patch data map operations. -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum PatchDataMapError { - #[error("invalid HEKI patch info")] - InvalidHekiPatchInfo, - #[error("invalid HEKI patch")] - InvalidHekiPatch, -} - -// TODO: Use this to resolve symbols in modules -pub struct Symbol { - _value: u64, -} - -impl Symbol { - /// Parse a symbol from a byte buffer. - pub fn from_bytes( - kinfo_start: usize, - start: VirtAddr, + pages: &[PhysPageAddr], + offset: usize, bytes: &[u8], - ) -> Result<(String, Self), VsmError> { - let kinfo_bytes = &bytes[kinfo_start..]; - let ksym = HekiKernelSymbol::from_bytes(kinfo_bytes)?; - - let value_addr = start + mem::offset_of!(HekiKernelSymbol, value_offset) as u64; - let value = value_addr - .as_u64() - .wrapping_add_signed(i64::from(ksym.value_offset)); - - let name_offset = kinfo_start - + mem::offset_of!(HekiKernelSymbol, name_offset) - + usize::try_from(ksym.name_offset).map_err(|_| VsmError::SymbolNameOffsetInvalid)?; - - if name_offset >= bytes.len() { - return Err(VsmError::SymbolNameOffsetInvalid); - } - let name_len = bytes[name_offset..] - .iter() - .position(|&b| b == 0) - .ok_or(VsmError::SymbolNameNoTerminator)?; - if name_len >= HekiKernelSymbol::KSY_NAME_LEN { - return Err(VsmError::SymbolNameTooLong); - } - - // SAFETY: - // - offset is within bytes (checked above) - // - there is a NUL terminator within bytes[offset..] (checked above) - // - Length of name string is within spec range (checked above) - // - bytes is still valid for the duration of this function - let name_str = unsafe { - let name_ptr = bytes.as_ptr().add(name_offset).cast::(); - CStr::from_ptr(name_ptr) - }; - let name = CString::new( - name_str - .to_str() - .map_err(|_| VsmError::SymbolNameInvalidUtf8)?, - ) - .map_err(|_| VsmError::SymbolNameInvalidUtf8)?; - let name = name - .into_string() - .map_err(|_| VsmError::SymbolNameInvalidUtf8)?; - Ok((name, Symbol { _value: value })) + ) -> Result<(), VsmError> { + let ptr = PrivilegedVtl0PhysMutPtr::::new(pages, offset) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + ptr.write_slice_at_offset(0, bytes) + .map_err(|_| VsmError::Vtl0CopyFailed) } } -pub struct SymbolTable { - inner: spin::rwlock::RwLock>, -} -use core::ffi::{CStr, c_char}; -impl Default for SymbolTable { - fn default() -> Self { - Self::new() +impl Vtl1Gate for LvbsVtl1Gate { + fn enable_aps_vtl(&self, _cpu_present_mask_pfn: u64) -> Result<(), VsmError> { + // APs enter VTL1 via `boot_aps`; no separate enablement step is needed. + debug_serial_println!("VSM: Enable APs' VTL is not supported"); + Ok(()) } -} -impl SymbolTable { - pub fn new() -> Self { - Self { - inner: spin::rwlock::RwLock::new(HashMap::new()), + fn boot_aps(&self, cpu_online_mask_pfn: u64) -> Result<(), VsmError> { + let mask_pa = cpu_online_mask_pfn + .checked_shl(PAGE_SHIFT.trunc()) + .and_then(|pa| PhysAddr::try_new(pa).ok()) + .ok_or(VsmError::InvalidPhysicalAddress)?; + + // Read exactly the fixed-size cpu_online_mask (MAX_CORES bits); bits + // beyond MAX_CORES are outside the ABI and cannot drive AP boots. + let mut mask_bytes = [0u8; core::mem::size_of::()]; + // Reading the argument out of VTL0 needs the VTL0 gate; the platform + // implements both capabilities, so it mints its own. + LvbsVtl0Gate::mint() + .read_vtl0_contiguous(mask_pa.as_u64(), &mut mask_bytes) + .map_err(|_| VsmError::CpuOnlineMaskCopyFailed)?; + let cpu_online_mask = + CpuMask::read_from_bytes(&mask_bytes).map_err(|_| VsmError::CpuOnlineMaskCopyFailed)?; + + // Best-effort: attempt every online CPU, surfacing the last init failure. + let mut error = None; + cpu_online_mask.for_each_cpu(|cpu_id| { + if let Err(e) = crate::mshv::hvcall_vp::init_vtl_ap(TruncateExt::::trunc(cpu_id)) { + error = Some(e); + } + }); + match error { + Some(e) => Err(VsmError::ApInitFailed(e)), + None => Ok(()), } } - /// Build a symbol table from a memory container. - pub fn build_from_container( - &self, - start: VirtAddr, - end: VirtAddr, - mem: &MemoryContainer, - buf: &[u8], - ) -> Result { - if mem.is_empty() { - return Err(VsmError::SymbolTableEmpty); - } - let Some(range) = mem.get_range() else { - return Err(VsmError::SymbolTableEmpty); - }; - if start < range.start || end > range.end { - return Err(VsmError::SymbolTableOutOfRange); - } + fn signal_end_of_boot(&self) { + debug_serial_println!("VSM: End of boot; VTL0 is no longer trusted"); + crate::platform_low().signal_end_of_boot(); + } - let kinfo_len: usize = (end - start).trunc(); - if !kinfo_len.is_multiple_of(HekiKernelSymbol::KSYM_LEN) { - return Err(VsmError::SymbolTableLengthInvalid); + fn set_platform_root_key(&self, key_pa: u64) -> Result<(), VsmError> { + if crate::platform_low().end_of_boot_reached() { + return Err(VsmError::OperationAfterEndOfBoot("set platform root key")); } - let mut kinfo_offset: usize = (start - range.start).trunc(); - let mut kinfo_addr = start; - let ksym_count = kinfo_len / HekiKernelSymbol::KSYM_LEN; - let mut inner = self.inner.write(); - inner.reserve(ksym_count); - - for _ in 0..ksym_count { - let (name, sym) = Symbol::from_bytes(kinfo_offset, kinfo_addr, buf)?; - inner.insert(name, sym); - kinfo_offset += HekiKernelSymbol::KSYM_LEN; - kinfo_addr += HekiKernelSymbol::KSYM_LEN as u64; - } - Ok(0) + let key_pa = PhysAddr::try_new(key_pa).map_err(|_| VsmError::InvalidPhysicalAddress)?; + let mut keybuf = Zeroizing::new([0u8; PRK_LEN]); + LvbsVtl0Gate::mint() + .read_vtl0_contiguous(key_pa.as_u64(), &mut *keybuf) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + crate::host::set_platform_root_key(&keybuf); + Ok(()) } } diff --git a/litebox_runner_lvbs/Cargo.toml b/litebox_runner_lvbs/Cargo.toml index 08d32f5a47..a612b73860 100644 --- a/litebox_runner_lvbs/Cargo.toml +++ b/litebox_runner_lvbs/Cargo.toml @@ -7,10 +7,11 @@ edition = "2024" arrayvec = { version = "0.7.6", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_platform_lvbs = { version = "0.1.0", path = "../litebox_platform_lvbs", default-features = false } -litebox_common_lvbs = { version = "0.1.0", path = "../litebox_common_lvbs" } litebox_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_lvbs"] } litebox_common_optee = { path = "../litebox_common_optee/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } +litebox_common_lvbs = { path = "../litebox_common_lvbs/", version = "0.1.0" } +litebox_service_heki = { path = "../litebox_service_heki/", version = "0.1.0" } litebox_shim_optee = { path = "../litebox_shim_optee/", version = "0.1.0" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } log = { version = "0.4", default-features = false } diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index eaf33d5d31..a4a25e37fc 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -13,11 +13,12 @@ use litebox::{ utils::{ReinterpretSignedExt, TruncateExt}, }; use litebox_common_linux::errno::Errno; -use litebox_common_lvbs::{NUM_VTLCALL_PARAMS, VsmFunction}; +use litebox_common_lvbs::{NUM_VTLCALL_PARAMS, VsmError, VsmFunction}; use litebox_common_optee::{ OpteeMessageCommand, OpteeMsgArgs, OpteeRpcArgs, OpteeSmcArgs, OpteeSmcResult, OpteeSmcReturnCode, TeeOrigin, TeeResult, UteeEntryFunc, UteeParams, optee_msg_args_total_size, }; +use litebox_platform_lvbs::mshv::vsm::{LvbsVtl0Gate, LvbsVtl0PrivilegedWriter, LvbsVtl1Gate}; use litebox_platform_lvbs::{ arch::{gdt, instrs::hlt_loop, interrupts, timer}, debug_serial_println, @@ -25,7 +26,6 @@ use litebox_platform_lvbs::{ mm::MemoryProvider, mshv::{ hvcall, - vsm::vsm_dispatch, vsm_intercept::raise_vtl0_gp_fault, vtl_switch::{vtl_switch, vtl_switch_init}, vtl1_mem_layout::{ @@ -43,7 +43,7 @@ use litebox_shim_optee::msg_handler::{ decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, update_optee_msg_args, }; use litebox_shim_optee::session::{OpenSessionTarget, TaInstance, session_manager}; -use litebox_shim_optee::{NormalWorldConstPtr, NormalWorldMutPtr, UserConstPtr}; +use litebox_shim_optee::{NormalWorldConstPtr, NormalWorldMutPtr, TaMemrefAddresses, UserConstPtr}; /// Seed the initial heap regions so the global allocator has enough memory /// for slab-backed allocations (the slab needs >= 2 MB backing pages). @@ -224,6 +224,11 @@ pub fn init(is_bsp: bool) -> Option<&'static Platform> { // Per-CPU; safe to call on BSP and APs. timer::init(); + if is_bsp { + let shim = litebox_shim_optee::OpteeShimBuilder::new().build(); + register_embedded_tas(&shim); + } + ret } @@ -256,10 +261,72 @@ fn vtlcall_dispatch(params: &[u64; NUM_VTLCALL_PARAMS]) -> i64 { let smc_args_pfn = params[1]; optee_smc_handler_entry(smc_args_pfn) } + VsmFunction::GenerateIdentitySigningKey => { + let public_key_pa = params[1]; + let key_alg = params[2]; + litebox_shim_optee::idk::generate_identity_signing_key(public_key_pa, key_alg) + } _ => vsm_dispatch(func_id, ¶ms[1..]), } } +/// Returns this VTL1 kernel's HEKI service: a single long-lived instance owned +/// by the runner (the VSM composition root), initialized on first access. +/// +/// This is where the abstract service is bound to the concrete platform gate; +/// the service holds it for its lifetime, so handlers need no gate argument. +fn heki() -> &'static litebox_service_heki::Heki { + static HEKI: spin::Once> = spin::Once::new(); + HEKI.call_once(|| litebox_service_heki::Heki::new(LvbsVtl0Gate::mint())) +} + +/// Dispatch a VSM function to its handler and return the result. +/// +/// Routes each call to the subsystem that owns it: HEKI (VTL0 protection) to +/// the service, which only gets `Vtl0Gate`, and VTL1 setup `Vtl1Gate`. +/// The Hyper-V mechanics behind both stay inside the platform, so nothing +/// here talks to the hypervisor. As the VSM composition root, the runner +/// mints the gate and owns the HEKI service. +fn vsm_dispatch(func_id: VsmFunction, params: &[u64]) -> i64 { + use litebox_common_lvbs::Vtl1Gate as _; + + let vtl1 = LvbsVtl1Gate::mint(); + let heki = heki(); + let result: Result = match func_id { + VsmFunction::EnableAPsVtl => vtl1.enable_aps_vtl(params[0]).map(|()| 0), + VsmFunction::BootAPs => vtl1.boot_aps(params[0]).map(|()| 0), + VsmFunction::LockRegs => heki.lock_regs(), + VsmFunction::SignalEndOfBoot => { + vtl1.signal_end_of_boot(); + Ok(0) + } + VsmFunction::ProtectMemory => heki.protect_memory(params[0], params[1]), + VsmFunction::LoadKData => heki.load_kdata(params[0], params[1]), + VsmFunction::ValidateModule => heki.validate_guest_module(params[0], params[1], params[2]), + VsmFunction::FreeModuleInit => { + heki.free_guest_module_init(params[0].reinterpret_as_signed()) + } + VsmFunction::UnloadModule => heki.unload_guest_module(params[0].reinterpret_as_signed()), + VsmFunction::CopySecondaryKey => heki.copy_secondary_key(params[0], params[1]), + VsmFunction::KexecValidate => heki.kexec_validate(params[0], params[1], params[2]), + VsmFunction::PatchText => { + heki.patch_text(&LvbsVtl0PrivilegedWriter::mint(), params[0], params[1]) + } + VsmFunction::AllocateRingbufferMemory => { + heki.allocate_ringbuffer_memory(params[0], params[1]) + } + VsmFunction::SetPlatformRootKey => vtl1.set_platform_root_key(params[0]).map(|()| 0), + VsmFunction::GenerateIdentitySigningKey => { + Err(VsmError::OperationNotSupported("Identity key generation")) + } + VsmFunction::OpteeMessage => Err(VsmError::OperationNotSupported("OP-TEE communication")), + }; + match result { + Ok(value) => value, + Err(e) => Errno::from(e).as_neg().into(), + } +} + /// An entry point function to handle OP-TEE SMC call. fn optee_smc_handler_entry(smc_args_pfn: u64) -> i64 { match optee_smc_handler_entry_inner(smc_args_pfn) { @@ -578,13 +645,14 @@ fn open_session_single_instance( let _task_pt_guard = TaskPageTableGuard::enter(task_pt_id)?; // Load TA context with parameters for OpenSession - pass actual session_id - instance + let memref_addresses = instance .loaded_program() .entrypoints .as_ref() .ok_or(OpteeSmcReturnCode::EBadCmd)? - .load_ta_context( + .load_ta_context_with_shm( params, + &ta_req_info.shm_info, runner_session_id, UteeEntryFunc::OpenSession as u32, None, @@ -631,6 +699,7 @@ fn open_session_single_instance( None, // No session ID on failure Some(&ta_params), Some(ta_req_info), + Some(&memref_addresses), ); // For single-instance TAs, only clean up on TARGET_DEAD (panic). @@ -663,6 +732,7 @@ fn open_session_single_instance( Some(runner_session_id), Some(&ta_params), Some(ta_req_info), + Some(&memref_addresses), ); // Write-back failure: OpenSession succeeded inside the TA, but we cannot @@ -716,13 +786,14 @@ fn open_session_new_instance( client_identity: Option, ta_req_info: &litebox_shim_optee::msg_handler::TaRequestInfo, ) -> Result<(), OpteeSmcReturnCode> { - let Some(ta_bin) = find_ta_binary(ta_uuid) else { + let shim = litebox_shim_optee::OpteeShimBuilder::new().build(); + if shim.get_ta_bin(&ta_uuid).is_none() { msg_args.session = 0; msg_args.ret = TeeResult::ItemNotFound; msg_args.ret_origin = TeeOrigin::Tee; write_non_ta_msg_args_to_normal_world(msg_args, msg_args_phys_addr)?; return Ok(()); - }; + } // Token is declared before `task_pt_guard` so it drops AFTER it. // Marker only releases once CR3 is back to base. See @@ -739,16 +810,12 @@ fn open_session_new_instance( })?; // Load ldelf and TA - Box immediately to keep at fixed heap address - let shim = litebox_shim_optee::OpteeShimBuilder::new().build(); - let loaded_program = Box::new( - shim.load_ldelf(LDELF_BINARY, ta_uuid, Some(ta_bin)) - .map_err(|_| { - // Safety: We are about to tear down this TA instance; - // no references to user-space memory will be held afterwards. - unsafe { teardown_ta_page_table(&shim, task_pt_id) }; - OpteeSmcReturnCode::ENomem - })?, - ); + let loaded_program = Box::new(shim.load_ldelf(LDELF_BINARY, ta_uuid).map_err(|_| { + // Safety: We are about to tear down this TA instance; + // no references to user-space memory will be held afterwards. + unsafe { teardown_ta_page_table(&shim, task_pt_id) }; + OpteeSmcReturnCode::ENomem + })?); let ta_flags = loaded_program.ta_flags; @@ -785,6 +852,7 @@ fn open_session_new_instance( None, // No session ID on failure None, Some(ta_req_info), + None, ); // Safety: We are about to tear down this TA instance; @@ -805,12 +873,13 @@ fn open_session_new_instance( unsafe { teardown_ta_page_table(&shim, task_pt_id) }; OpteeSmcReturnCode::EBadCmd })?; - loaded_program + let memref_addresses = loaded_program .entrypoints .as_ref() .unwrap() - .load_ta_context( + .load_ta_context_with_shm( params, + &ta_req_info.shm_info, runner_session_id, UteeEntryFunc::OpenSession as u32, None, @@ -867,6 +936,7 @@ fn open_session_new_instance( None, // No session ID on failure Some(&ta_params), Some(ta_req_info), + Some(&memref_addresses), ); // Safety: We are about to tear down this TA instance; @@ -887,6 +957,7 @@ fn open_session_new_instance( Some(runner_session_id), Some(&ta_params), Some(ta_req_info), + Some(&memref_addresses), ) .inspect_err(|_| { // Safety: We are about to tear down this TA instance; @@ -977,9 +1048,10 @@ fn handle_invoke_command( // Set up the entry-point parameters for InvokeCommand. let entrypoints_ref = instance.loaded_program().entrypoints.as_ref().unwrap(); - entrypoints_ref - .load_ta_context( + let memref_addresses = entrypoints_ref + .load_ta_context_with_shm( params.as_slice(), + &ta_req_info.shm_info, session_id, UteeEntryFunc::InvokeCommand as u32, Some(cmd_id), @@ -1016,6 +1088,7 @@ fn handle_invoke_command( None, Some(&ta_params), Some(&ta_req_info), + Some(&memref_addresses), ); // Per OP-TEE OS: if TA panics (TARGET_DEAD), the TA context is @@ -1115,6 +1188,7 @@ fn handle_close_session( None, None, None, + None, ); let removed_flags = session_manager().unregister_session(session_id); @@ -1187,6 +1261,7 @@ fn write_msg_args_to_normal_world( session_id: Option, ta_params: Option<&UteeParams>, ta_req_info: Option<&litebox_shim_optee::msg_handler::TaRequestInfo>, + memref_addresses: Option<&TaMemrefAddresses>, ) -> Result<(), OpteeSmcReturnCode> { // Ensure we're on a task page table, not the base page table. // Accessing TA userspace memory requires the TA's page table to be active. @@ -1209,6 +1284,7 @@ fn write_msg_args_to_normal_world( session_id, ta_params, ta_req_info, + memref_addresses, msg_args, )?; @@ -1275,24 +1351,26 @@ fn write_rpc_args_to_normal_world( Ok(()) } -// use include_bytes! to include ldelf and (KMPP) TA binaries +// use include_bytes! to include ldelf const LDELF_BINARY: &[u8] = &[0u8; 0]; const TA_BINARY: &[u8] = &[0u8; 0]; const TA_BINARIES: &[&[u8]] = &[TA_BINARY]; -/// Look up TA binary by UUID. -/// TODO: Handle PTA UUIDs -fn find_ta_binary(ta_uuid: litebox_common_optee::TeeUuid) -> Option<&'static [u8]> { - use litebox_common_optee::parse_ta_head; +/// Register a TA binary embedded in the runner image. +fn register_embedded_ta(shim: &litebox_shim_optee::OpteeShim, ta_binary: &'static [u8]) -> bool { + let Some(ta_head) = litebox_common_optee::parse_ta_head(ta_binary) else { + return false; + }; + shim.store_ta_bin(&ta_head.uuid, ta_binary) +} +/// Register all TA binaries embedded in the runner image. +fn register_embedded_tas(shim: &litebox_shim_optee::OpteeShim) { for ta_binary in TA_BINARIES { - if let Some(ta_head) = parse_ta_head(ta_binary) - && ta_head.uuid == ta_uuid - { - return Some(ta_binary); + if !ta_binary.is_empty() { + assert!(register_embedded_ta(shim, ta_binary)); } } - None } #[panic_handler] diff --git a/litebox_runner_lvbs/src/main.rs b/litebox_runner_lvbs/src/main.rs index abf33655ff..282dd5eef0 100644 --- a/litebox_runner_lvbs/src/main.rs +++ b/litebox_runner_lvbs/src/main.rs @@ -8,7 +8,7 @@ use core::arch::{asm, naked_asm}; use core::sync::atomic::{AtomicBool, Ordering}; use litebox_platform_lvbs::{ - arch::{enable_extended_states, enable_fsgsbase, enable_smep_smap, instrs::hlt_loop}, + arch::{enable_extended_states, enable_fsgsbase, enable_smep_smap}, host::{ bootparam::save_boot_info, per_cpu_variables::{ @@ -48,13 +48,6 @@ static HOST_LOGGER: HostLogger = HostLogger; /// releases it after switching to its own heap-allocated per-CPU kernel stack. static AP_BOOT_STACK_LOCK: AtomicBool = AtomicBool::new(false); -/// Release the AP boot stack spinlock. -/// -/// Called after the current core has switched RSP to its per-CPU kernel stack. -extern "C" fn release_boot_stack_lock() { - AP_BOOT_STACK_LOCK.store(false, Ordering::Release); -} - /// ELF64 relocation entry #[repr(C)] struct Elf64Rela { @@ -69,7 +62,14 @@ const R_X86_64_RELATIVE: u64 = 8; const KERNEL_OFFSET: u64 = litebox_platform_lvbs::KERNEL_OFFSET; /// Page table entry flags for Phase 1 mappings (present + writable). -const PTE_TABLE_FLAGS: u64 = PageTableFlags::PRESENT.bits() | PageTableFlags::WRITABLE.bits(); +/// +/// ACCESSED and DIRTY are pre-set here too (mirroring the Linux kernel's +/// `_KERNPG_TABLE`) so the CPU's page-table walker doesn't need an atomic +/// read-modify-write on these entries the first time they're traversed. +const PTE_TABLE_FLAGS: u64 = PageTableFlags::PRESENT.bits() + | PageTableFlags::WRITABLE.bits() + | PageTableFlags::ACCESSED.bits() + | PageTableFlags::DIRTY.bits(); /// x86-64 page table structure constants const ENTRIES_PER_PT_PAGE: usize = 512; @@ -329,23 +329,19 @@ unsafe fn remap_to_high_canonical() -> ! { } /// Trampoline executed at the high-canonical address after Phase 1 remap. -/// -/// Adjusts RSP from low-canonical (PA-based) to high-canonical, re-applies -/// ELF relocations for the final link address, and tail-jumps to -/// `common_start` with `is_bsp = true`. #[unsafe(naked)] unsafe extern "C" fn high_canonical_trampoline() -> ! { // 1. Adjust RSP from low-canonical (PA-based) to high-canonical. // 2. Phase 1b: Re-apply ELF relocations so every GOT slot now points to // high-canonical VAs (addend + memory_base + KERNEL_OFFSET). - // 3. Set edi = 1 (is_bsp = true) and tail-jump to common_start. + // 3. Set edi = 1 (is_bsp = true) and call common_start. naked_asm!( "mov rax, {offset}", "add rsp, rax", "and rsp, -16", "call {apply_reloc}", "mov edi, 1", - "jmp {common_start}", + "call {common_start}", offset = const KERNEL_OFFSET, apply_reloc = sym apply_relocations, common_start = sym common_start, @@ -377,12 +373,18 @@ pub unsafe extern "C" fn _ap_start() -> ! { "3:", // This AP has acquired the lock and exclusively owns the boot stack. "xor edi, edi", // is_bsp = false - "jmp {common_start}", + "call {common_start}", lock = sym AP_BOOT_STACK_LOCK, common_start = sym common_start, ); } +#[inline(never)] +unsafe extern "C" fn finalize_stack_switch_and_start_kernel(is_bsp: bool) -> ! { + AP_BOOT_STACK_LOCK.store(false, Ordering::Release); + unsafe { kernel_main(is_bsp) } +} + /// Shared boot path for BSP and AP cores. /// /// When `is_bsp` is `true`, seeds the initial heap. @@ -400,27 +402,19 @@ unsafe extern "C" fn common_start(is_bsp: bool) -> ! { init_per_cpu_variables(); - // Switch to the kernel stack and tail-call kernel_main with is_bsp + // Switch to the per-CPU kernel stack and continue startup with is_bsp. let is_bsp_u32 = u32::from(is_bsp); unsafe { asm!( // Now use this core's heap-allocated kernel stack. "mov rsp, gs:[{kernel_sp_off}]", - // The boot stack is no longer in use. Release the AP boot stack - // spinlock so the next AP can proceed. For the BSP this is a - // harmless no-op (the lock was never held). - "push rdi", - "call {release_lock}", - "pop rdi", - "call {kernel_main}", + "call {switch_stack_and_start_kernel}", kernel_sp_off = const { PerCpuVariablesAsm::kernel_stack_ptr_offset() }, in("edi") is_bsp_u32, - release_lock = sym release_boot_stack_lock, - kernel_main = sym kernel_main, + switch_stack_and_start_kernel = sym finalize_stack_switch_and_start_kernel, + options(noreturn), ); } - - hlt_loop() } /// BSP-only entry point. diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index 97a1a2551e..c1a3161727 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, Result}; use clap::Parser; use litebox_broker_local_userland as broker; -use litebox_common_optee::{TeeUuid, UteeEntryFunc, UteeParamOwned}; +use litebox_common_optee::{UteeEntryFunc, UteeParamOwned}; use litebox_platform_multiplex::Platform; use litebox_shim_optee::session::session_manager; use std::path::PathBuf; @@ -132,6 +132,10 @@ fn run_ta_with_default_commands( ldelf_bin: &[u8], ta_bin: &[u8], ) { + let ta_uuid = litebox_common_optee::parse_ta_head(ta_bin) + .expect("Failed to parse TA header from ta_bin") + .uuid; + assert!(shim.store_ta_bin(&ta_uuid, ta_bin)); for func_id in [UteeEntryFunc::OpenSession, UteeEntryFunc::CloseSession] { let params = [const { UteeParamOwned::None }; UteeParamOwned::TEE_NUM_PARAMS]; @@ -139,7 +143,7 @@ fn run_ta_with_default_commands( let session_token = session_manager().try_acquire_open_session_token().unwrap(); let session_id = session_token.session_id().unwrap(); let loaded_program = shim - .load_ldelf(ldelf_bin, TeeUuid::default(), Some(ta_bin)) + .load_ldelf(ldelf_bin, ta_uuid) .map_err(|_| { panic!("Failed to load ldelf"); }) diff --git a/litebox_runner_optee_on_linux_userland/src/tests.rs b/litebox_runner_optee_on_linux_userland/src/tests.rs index 645055431e..4b3b288296 100644 --- a/litebox_runner_optee_on_linux_userland/src/tests.rs +++ b/litebox_runner_optee_on_linux_userland/src/tests.rs @@ -27,6 +27,9 @@ pub fn run_ta_with_test_commands( let json_str = std::fs::read_to_string(json_path).unwrap(); serde_json::from_str(&json_str).unwrap() }; + let ta_head = + litebox_common_optee::parse_ta_head(ta_bin).expect("Failed to parse TA header from ta_bin"); + assert!(shim.store_ta_bin(&ta_head.uuid, ta_bin)); let mut ta_info: Option = None; // The active session id for the TA. Set at OpenSession and reused for the // subsequent InvokeCommand entries on the same persistent session. @@ -52,8 +55,6 @@ pub fn run_ta_with_test_commands( continue; } if func_id == UteeEntryFunc::OpenSession { - let ta_head = litebox_common_optee::parse_ta_head(ta_bin) - .expect("Failed to parse TA header from ta_bin"); let mut session_token = session_manager().try_acquire_open_session_token().unwrap(); let open_session_id = session_token.session_id().unwrap(); session_id = Some(open_session_id); @@ -67,7 +68,7 @@ pub fn run_ta_with_test_commands( ); session_manager().set_session_client_identity(open_session_id, Some(client_identity)); let loaded = shim - .load_ldelf(ldelf_bin, ta_head.uuid, Some(ta_bin)) + .load_ldelf(ldelf_bin, ta_head.uuid) .map_err(|_| { panic!("Failed to load TA"); }) @@ -310,7 +311,7 @@ impl TaCommandParamsBase64 { value_b: *value_b, }, TaCommandParamsBase64::MemrefInput { data_base64 } => UteeParamOwned::MemrefInput { - data: Self::decode_base64(data_base64).into_boxed_slice(), + data: Some(Self::decode_base64(data_base64).into_boxed_slice()), }, TaCommandParamsBase64::MemrefOutput { buffer_size } => UteeParamOwned::MemrefOutput { buffer_size: usize::try_from(*buffer_size).unwrap(), @@ -326,7 +327,7 @@ impl TaCommandParamsBase64 { "Buffer size is smaller than input data size" ); UteeParamOwned::MemrefInout { - data: decoded_data.into_boxed_slice(), + data: Some(decoded_data.into_boxed_slice()), buffer_size, } } diff --git a/litebox_runner_optee_on_linux_userland/tests/hello3seg-ta-cmds.json b/litebox_runner_optee_on_linux_userland/tests/hello3seg-ta-cmds.json new file mode 100644 index 0000000000..9d87bf1087 --- /dev/null +++ b/litebox_runner_optee_on_linux_userland/tests/hello3seg-ta-cmds.json @@ -0,0 +1,34 @@ +[ + { + "func_id": "open_session", + "client_identity": { + "login": "user" + } + }, + { + "func_id": "invoke_command", + "cmd_id": 0, + "args": [ + { + "param_type": "value_inout", + "value_a": 100, + "value_b": 0 + } + ] + }, + { + "func_id": "invoke_command", + "cmd_id": 1, + "args": [ + { + "param_type": "value_inout", + "value_a": 200, + "value_b": 0 + } + ] + }, + { + "func_id": "close_session" + } +] + diff --git a/litebox_runner_optee_on_linux_userland/tests/hello3seg-ta.elf b/litebox_runner_optee_on_linux_userland/tests/hello3seg-ta.elf new file mode 100755 index 0000000000..b4e59bd294 Binary files /dev/null and b/litebox_runner_optee_on_linux_userland/tests/hello3seg-ta.elf differ diff --git a/litebox_runner_optee_on_linux_userland/tests/run.rs b/litebox_runner_optee_on_linux_userland/tests/run.rs index 6a88118c45..4f5cc4aff3 100644 --- a/litebox_runner_optee_on_linux_userland/tests/run.rs +++ b/litebox_runner_optee_on_linux_userland/tests/run.rs @@ -83,6 +83,17 @@ fn test_runner_hello_ta() { run("hello-ta"); } +/// Same TA as [`test_runner_hello_ta`], but built with three `PT_LOAD` +/// segments instead of two. +/// +/// A third segment produces a middle segment mapped at a *fixed* address with +/// non-zero `pad_end`, which is the case that collides with the LiteBox +/// trampoline pages. +#[test] +fn test_runner_hello_3seg_ta() { + run("hello3seg-ta"); +} + #[test] fn test_runner_random_ta() { run("random-ta"); diff --git a/litebox_service_heki/Cargo.toml b/litebox_service_heki/Cargo.toml new file mode 100644 index 0000000000..18409e64da --- /dev/null +++ b/litebox_service_heki/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "litebox_service_heki" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox = { path = "../litebox/", version = "0.1.0" } +litebox_common_lvbs = { path = "../litebox_common_lvbs/", version = "0.1.0" } +litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } +x86_64 = { version = "0.15.2", default-features = false, features = ["instructions"] } +log = { version = "0.4", default-features = false } +spin = { version = "0.10.0", default-features = false, features = [ + "spin_mutex", + "once", + "rwlock", +] } +hashbrown = "0.15.2" +rangemap = { version = "1.5.1", features = ["const_fn"] } +thiserror = { version = "2.0.6", default-features = false } +once_cell = { version = "1.20.2", default-features = false, features = ["alloc", "race"] } +elf = { version = "0.8.0", default-features = false } +cms = { version = "0.2.3", default-features = false, features = ["alloc"] } +rsa = { version = "0.9.10", default-features = false } +sha2 = { version = "0.10.9", default-features = false, features = ["oid"] } +x509-cert = { version = "0.2.5", default-features = false } +const-oid = { version = "0.9.6", default-features = false, features = ["db"] } +authenticode = { version = "0.4.3", default-features = false, features = ["object"] } +object = { version = "0.36.7", default-features = false, features = ["pe"] } +digest = { version = "0.10.7", default-features = false } + +[lints] +workspace = true diff --git a/litebox_service_heki/src/handlers.rs b/litebox_service_heki/src/handlers.rs new file mode 100644 index 0000000000..67619cec2e --- /dev/null +++ b/litebox_service_heki/src/handlers.rs @@ -0,0 +1,819 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! The VTL call entry points — one [`Heki`] method per HEKI function the runner +//! dispatches — and the helpers they are built from. + +#[cfg(debug_assertions)] +use crate::mem_integrity::parse_modinfo; +use crate::mem_integrity::{ + validate_kernel_module_against_elf, validate_text_patch, verify_kernel_module_signature, + verify_kernel_pe_signature, +}; +use crate::{ + Heki, KexecMemoryMetadata, KexecMemoryRange, MemoryContainer, ModuleMemory, + ModuleMemoryMetadata, +}; + +use alloc::vec::Vec; +use hashbrown::HashSet; +use litebox::utils::TruncateExt; +use litebox_common_linux::vmap::PhysPageAddr; +use litebox_common_lvbs::{ + HekiKdataType, HekiKernelInfo, HekiKexecType, HekiPage, HekiPatch, KEXEC_SEGMENT_MAX, Kimage, + MemAttr, ModMemType, PAGE_SIZE, ReservationStatus, VsmError, Vtl0Gate, Vtl0PrivilegedWrite, + mod_mem_type_to_mem_attr, +}; +use x86_64::{ + PhysAddr, VirtAddr, + structures::paging::{PageSize, PhysFrame, Size4KiB, frame::PhysFrameRange}, +}; +use x509_cert::{Certificate, der::Decode}; +use zerocopy::{FromBytes, FromZeros, IntoBytes}; + +// For now, we do not validate large kernel modules due to the VTL1's memory size limitation. +const MODULE_VALIDATION_MAX_SIZE: usize = 64 * 1024 * 1024; + +/// HEKI handlers for individual VTL call entry points. +impl Heki

{ + /// HEKI handler for locking VTL0's control registers + pub fn lock_regs(&self) -> Result { + self.gate.lock_control_registers()?; + Ok(0) + } + + /// HEKI handler for protecting certain memory ranges (e.g., kernel text, data, heap). + /// `pa` and `nranges` specify a memory area containing the information about the memory ranges to protect. + pub fn protect_memory(&self, pa: u64, nranges: u64) -> Result { + if PhysAddr::try_new(pa) + .ok() + .as_ref() + .is_none_or(|p| !p.is_aligned(Size4KiB::SIZE)) + || nranges == 0 + { + return Err(VsmError::InvalidInputAddress); + } + + if self.gate.end_of_boot_reached() { + return Err(VsmError::OperationAfterEndOfBoot( + "kernel memory protection", + )); + } + + let heki_pages = copy_heki_pages_from_vtl0(&self.gate, pa, nranges) + .ok_or(VsmError::HekiPagesCopyFailed)?; + + for heki_page in heki_pages { + for heki_range in &heki_page { + let pa = heki_range.pa; + let epa = heki_range.epa; + let mem_attr = heki_range + .mem_attr() + .ok_or(VsmError::MemoryAttributeInvalid)?; + + if !heki_range.is_aligned(Size4KiB::SIZE) { + return Err(VsmError::AddressNotPageAligned); + } + + let va = heki_range.va; + log::debug!( + "HEKI: Protect memory: va {:#x} pa {:#x} epa {:#x} {:?} (size: {})", + va, + pa, + epa, + mem_attr, + epa - pa + ); + + if pa == epa { + continue; + } + + self.gate.protect_frames( + PhysFrame::range( + // `HekiRange::is_valid` already validated both physical addresses. + PhysFrame::containing_address(PhysAddr::new(pa)), + PhysFrame::containing_address(PhysAddr::new(epa)), + ), + mem_attr, + )?; + } + } + Ok(0) + } + + /// HEKI handler for loading kernel data (e.g., certificates, blocklist, kernel symbols) into VTL1. + /// `pa` and `nranges` specify memory areas containing the information about the memory ranges to load. + pub fn load_kdata(&self, pa: u64, nranges: u64) -> Result { + if PhysAddr::try_new(pa) + .ok() + .as_ref() + .is_none_or(|p| !p.is_aligned(Size4KiB::SIZE)) + || nranges == 0 + { + return Err(VsmError::InvalidInputAddress); + } + + if self.gate.end_of_boot_reached() { + return Err(VsmError::OperationAfterEndOfBoot("loading kernel data")); + } + + let mut system_certs_mem = MemoryContainer::new(); + let mut kexec_trampoline_metadata = KexecMemoryMetadata::new(); + let mut kexec_trampoline_insert_failed = false; + let mut patch_info_mem = MemoryContainer::new(); + let mut kinfo_mem = MemoryContainer::new(); + let mut kdata_mem = MemoryContainer::new(); + + let heki_pages = copy_heki_pages_from_vtl0(&self.gate, pa, nranges) + .ok_or(VsmError::HekiPagesCopyFailed)?; + + for heki_page in &heki_pages { + for heki_range in heki_page { + log::debug!("HEKI: Load kernel data {heki_range:?}"); + match heki_range.heki_kdata_type() { + HekiKdataType::SystemCerts => system_certs_mem + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?, + HekiKdataType::KexecTrampoline => { + if let Err(e) = kexec_trampoline_metadata.insert_heki_range(heki_range) { + log::debug!( + "HEKI: KexecTrampoline insert_heki_range failed ({e:?}); skipping kexec trampoline protection" + ); + kexec_trampoline_insert_failed = true; + } + } + HekiKdataType::PatchInfo => patch_info_mem + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?, + HekiKdataType::KernelInfo => kinfo_mem + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?, + HekiKdataType::KernelData => kdata_mem + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?, + HekiKdataType::Unknown => { + return Err(VsmError::KernelDataTypeInvalid); + } + _ => { + log::debug!("HEKI: Unsupported kernel data not loaded {heki_range:?}"); + } + } + } + } + + system_certs_mem + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + patch_info_mem + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + kinfo_mem + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + kdata_mem + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + + if system_certs_mem.is_empty() { + return Err(VsmError::SystemCertificatesNotFound); + } + + let cert_buf = &system_certs_mem[..]; + let certs = parse_certs(cert_buf)?; + + if certs.is_empty() { + return Err(VsmError::SystemCertificatesInvalid); + } + + // The system certificate is loaded into VTL1 and locked down before `end_of_boot` is signaled. + // Its integrity depends on UEFI Secure Boot which ensures only trusted software is loaded during + // the boot process. + self.set_system_certificates(certs.clone()); + log::debug!("HEKI: Loaded {} system certificate(s)", certs.len()); + + // ToDo: Remove kexec_trampoline_insert_failed and protect kexec_trampoline_metadata + // once we have a better solution to handle the non-page-aligned kexec trampoline metadata. + // The current solution is to skip protecting kexec trampoline metadata if its insert_heki_range + // fails, letting kdata load proceed so that heki is not broken. + if !kexec_trampoline_insert_failed { + for kexec_trampoline_range in &kexec_trampoline_metadata { + self.gate.protect_frames( + kexec_trampoline_range.phys_frame_range, + MemAttr::MEM_ATTR_READ, + )?; + } + } + + // pre-computed patch data for the kernel text + if !patch_info_mem.is_empty() { + let patch_info_buf = &patch_info_mem[..]; + self.precomputed_patches + .insert_patch_data_from_bytes(patch_info_buf, None) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + } + + if kinfo_mem.is_empty() || kdata_mem.is_empty() { + return Err(VsmError::KernelSymbolTableNotFound); + } + + let kinfo_buf = &kinfo_mem[..]; + let kdata_buf = &kdata_mem[..]; + let kinfo = HekiKernelInfo::from_bytes(kinfo_buf)?; + + self.gpl_symbols.build_from_container( + VirtAddr::from_ptr(kinfo.ksymtab_gpl_start), + VirtAddr::from_ptr(kinfo.ksymtab_gpl_end), + &kdata_mem, + kdata_buf, + )?; + + self.symbols.build_from_container( + VirtAddr::from_ptr(kinfo.ksymtab_start), + VirtAddr::from_ptr(kinfo.ksymtab_end), + &kdata_mem, + kdata_buf, + )?; + + Ok(0) + // TODO: create blocklist keys + // TODO: save blocklist hashes + } + + /// HEKI handler for validating a guest kernel module and applying specified protection to its memory ranges after validation. + /// `pa` and `nranges` specify a memory area containing the information about the kernel module to validate or protect. + /// `flags` controls the validation process (unused for now). + /// This function returns a unique `token` to VTL0, which is used to identify the module in subsequent calls. + pub fn validate_guest_module( + &self, + pa: u64, + nranges: u64, + _flags: u64, + ) -> Result { + if PhysAddr::try_new(pa) + .ok() + .as_ref() + .is_none_or(|p| !p.is_aligned(Size4KiB::SIZE)) + || nranges == 0 + { + return Err(VsmError::InvalidInputAddress); + } + + log::debug!("HEKI: Validate kernel module: pa {pa:#x} nranges {nranges}"); + + let certs = self + .get_system_certificates() + .ok_or(VsmError::SystemCertificatesNotLoaded)?; + + // collect and maintain the memory ranges of a module locally until the module is validated and its metadata is registered in the global map + // we don't maintain this content in the global map due to memory overhead. Instead, we could add its hash value to the global map to check the integrity. + let mut module_memory_metadata = ModuleMemoryMetadata::new(); + // a kernel module loaded in memory with relocations and patches + let mut module_in_memory = ModuleMemory::new(); + // the kernel module's original ELF binary which is signed by the kernel build pipeline + let mut module_as_elf = MemoryContainer::new(); + // patch info for the kernel module + let mut patch_info_for_module = MemoryContainer::new(); + + let heki_pages = copy_heki_pages_from_vtl0(&self.gate, pa, nranges) + .ok_or(VsmError::HekiPagesCopyFailed)?; + + for heki_page in &heki_pages { + for heki_range in heki_page { + match heki_range.mod_mem_type() { + ModMemType::Unknown => { + return Err(VsmError::ModuleMemoryTypeInvalid); + } + ModMemType::ElfBuffer => module_as_elf + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?, + ModMemType::Patch => patch_info_for_module + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?, + _ => { + // if input memory range's type is neither `Unknown` nor `ElfBuffer`, its addresses must be page-aligned + if !heki_range.is_aligned(Size4KiB::SIZE) { + return Err(VsmError::AddressNotPageAligned); + } + module_memory_metadata.insert_heki_range(heki_range); + module_in_memory + .extend_range(heki_range.mod_mem_type(), heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?; + } + } + } + } + + // Reject overlap and reserve this module's frames. Legitimate module frames are never shared. + // The reserve + freeze + validate + promote + patch-commit sequence runs transactionally: the + // gate reserves `initial`, commits on `Ok`, and rolls back (unprotecting every newly + // reserved range) on `Err`. + let initial: Vec> = module_memory_metadata + .iter() + .map(|r| r.phys_frame_range) + .collect(); + self.gate + .protect_frames_transactionally(&initial, &mut |txn| { + // Freeze frames that require immutable copy/validation to avoid TOCTOU. + for mod_mem_range in &module_memory_metadata { + if !mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type) + .contains(MemAttr::MEM_ATTR_WRITE) + { + txn.protect(mod_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; + } + } + + module_as_elf + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + patch_info_for_module + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + module_in_memory + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + + let elf_size = (module_as_elf[..]).len(); + if elf_size > MODULE_VALIDATION_MAX_SIZE { + return Err(VsmError::ModuleElfSizeExceeded { + size: elf_size, + max: MODULE_VALIDATION_MAX_SIZE, + }); + } + + let original_elf_data = &module_as_elf[..]; + + #[cfg(debug_assertions)] + parse_modinfo(original_elf_data).map_err(|_| VsmError::Vtl0CopyFailed)?; + + verify_kernel_module_signature(original_elf_data, certs)?; + + if !validate_kernel_module_against_elf(&module_in_memory, original_elf_data) + .map_err(|_| VsmError::Vtl0CopyFailed)? + { + return Err(VsmError::ModuleRelocationInvalid); + } + + // Both read-only and executable frames have been frozen above. + // Thus, only promote executable frames to RX. + for mod_mem_range in &module_memory_metadata { + if matches!( + mod_mem_range.mod_mem_type, + ModMemType::Text | ModMemType::InitText + ) { + txn.protect( + mod_mem_range.phys_frame_range, + mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type), + )?; + } + } + + // Commit the module's pre-computed patch data (transactional). + if !patch_info_for_module.is_empty() { + let patch_info_buf = &patch_info_for_module[..]; + self.precomputed_patches + .insert_patch_data_from_bytes( + patch_info_buf, + Some(&mut module_memory_metadata), + ) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + } + Ok(()) + })?; + + // Fully validated and committed: register the module. + // register the module memory in the global map and obtain a unique token for it + let token = self + .module_memory_metadata + .register_module_memory_metadata(module_memory_metadata); + Ok(token) + } + + /// HEKI handler for supporting the initialization of a guest kernel module including + /// freeing the memory ranges that were used only for initialization and + /// write-protecting the memory ranges that should be read-only after initialization. + /// `token` is the unique identifier for the module. + pub fn free_guest_module_init(&self, token: i64) -> Result { + log::debug!("HEKI: Free kernel module's init (token: {token})"); + + if !self.module_memory_metadata.contains_key(token) { + return Err(VsmError::ModuleTokenInvalid); + } + + let mut result: Result<(), VsmError> = Ok(()); + if let Some(entry) = self.module_memory_metadata.iter_entry(token) { + for mod_mem_range in entry.iter_mem_ranges() { + let range_result = match mod_mem_range.mod_mem_type { + ModMemType::InitText | ModMemType::InitData | ModMemType::InitRoData => { + self.gate.unprotect_frames(mod_mem_range.phys_frame_range) + } + ModMemType::RoAfterInit => { + // make this memory range read-only after initialization + self.gate + .protect_frames(mod_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ) + } + _ => Ok(()), + }; + if range_result.is_err() { + result = range_result; + break; + } + } + } + + // Drop the init ranges from the module's metadata regardless of failures. This is intentional + // since hypercalls shouldn't fail and avoiding double release is more important. + let freed_init_patch_targets = self.module_memory_metadata.remove_init_ranges(token); + // Remove the precomputed patches targeting those freed init frames so a stale init patch cannot + // later be applied to recycled frames (no patch-after-free). + if !freed_init_patch_targets.is_empty() { + self.precomputed_patches + .remove_patch_data(&freed_init_patch_targets); + } + + result.map(|()| 0) + } + + /// HEKI handler for supporting the unloading of a guest kernel module. + /// `token` is the unique identifier for the module. + pub fn unload_guest_module(&self, token: i64) -> Result { + log::debug!("HEKI: Unload kernel module (token: {token})"); + + if !self.module_memory_metadata.contains_key(token) { + return Err(VsmError::ModuleTokenInvalid); + } + + if let Some(entry) = self.module_memory_metadata.iter_entry(token) { + for mod_mem_range in entry.iter_mem_ranges() { + self.gate.unprotect_frames(mod_mem_range.phys_frame_range)?; + } + } + + if let Some(patch_targets) = self.module_memory_metadata.get_patch_targets(token) { + self.precomputed_patches.remove_patch_data(&patch_targets); + } + + self.module_memory_metadata.remove(token); + Ok(0) + } + + /// HEKI handler for copying secondary key + #[allow(clippy::unnecessary_wraps)] + pub fn copy_secondary_key(&self, _pa: u64, _nranges: u64) -> Result { + log::debug!("HEKI: Copy secondary key"); + // TODO: copy secondary key + Ok(0) + } + + /// HEKI handler for write protecting the memory regions of a verified kernel image for kexec. + /// This function protects the kexec kernel blob (PE) only if it has a valid signature. + /// Note: this function does not make kexec kernel pages executable, which should be done by + /// another VTL1 method that can intercept the kexec/reset signal. + pub fn kexec_validate(&self, pa: u64, nranges: u64, crash: u64) -> Result { + log::debug!("HEKI: Validate kexec pa {pa:#x} nranges {nranges} crash {crash}"); + + let certs = self + .get_system_certificates() + .ok_or(VsmError::SystemCertificatesNotLoaded)?; + + let is_crash = crash != 0; + let kexec_metadata_ref = if is_crash { + &self.crash_kexec_metadata + } else { + &self.kexec_metadata + }; + + // invalidate (i.e., remove protection and clear) the kexec memory ranges which were loaded in the past + for old_kexec_mem_range in kexec_metadata_ref.iter_guarded().iter_mem_ranges() { + self.gate + .unprotect_frames(old_kexec_mem_range.phys_frame_range)?; + } + kexec_metadata_ref.clear_memory(); + + if pa == 0 { + // invalidation only + return Ok(0); + } + + let mut kexec_memory_metadata = KexecMemoryMetadata::new(); + let mut kexec_image = MemoryContainer::new(); + let mut kexec_kernel_blob = MemoryContainer::new(); + + let heki_pages = copy_heki_pages_from_vtl0(&self.gate, pa, nranges) + .ok_or(VsmError::HekiPagesCopyFailed)?; + + for heki_page in &heki_pages { + for heki_range in heki_page { + match heki_range.heki_kexec_type() { + HekiKexecType::KexecImage => { + kexec_memory_metadata.insert_heki_range(heki_range)?; + kexec_image + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?; + } + HekiKexecType::KexecKernelBlob => + // we do not protect kexec kernel blob memory + { + kexec_kernel_blob + .extend_range(heki_range) + .map_err(|_| VsmError::InvalidInputAddress)?; + } + + HekiKexecType::KexecPages => { + kexec_memory_metadata.insert_heki_range(heki_range)?; + } + HekiKexecType::Unknown => { + return Err(VsmError::KexecTypeInvalid); + } + } + } + } + + // Reserve then freeze the protected kexec frames, rejecting overlap with VTL1 or other + // protected frames. The reserve/protect (incl. the mid-flow segment reserve for crash kexec), + // blob copy, and signature check run transactionally: commit on `Ok`, rollback on `Err`. + let initial: Vec> = kexec_memory_metadata + .iter() + .map(|r| r.phys_frame_range) + .collect(); + self.gate + .protect_frames_transactionally(&initial, &mut |txn| { + for kexec_mem_range in &kexec_memory_metadata { + txn.protect(kexec_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; + } + + kexec_image + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + kexec_kernel_blob + .write_bytes_from_heki_range(&self.gate) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + + // If this function is called for crash kexec, we protect its kimage segments as well. + if is_crash { + let kimage = + Kimage::read_from_bytes(&kexec_image[..core::mem::size_of::()]) + .map_err(|_| VsmError::KexecImageSegmentsInvalid)?; + if kimage.nr_segments > KEXEC_SEGMENT_MAX as u64 { + return Err(VsmError::KexecImageSegmentsInvalid); + } + let mut segment_ranges = Vec::new(); + for i in 0..usize::try_from(kimage.nr_segments).unwrap_or(0) { + VirtAddr::try_new(kimage.segment[i].buf) + .map_err(|_| VsmError::InvalidVirtualAddress)?; + let pa = kimage.segment[i].mem; + if let Some(epa) = pa.checked_add(kimage.segment[i].memsz) { + segment_ranges.push(KexecMemoryRange::new(pa, epa)?); + } else { + return Err(VsmError::KexecSegmentRangeInvalid); + } + } + let segment_frame_ranges: Vec> = + segment_ranges.iter().map(|r| r.phys_frame_range).collect(); + let reservation_statuses = txn.reserve(&segment_frame_ranges)?; + for (segment_range, status) in + segment_ranges.into_iter().zip(reservation_statuses) + { + if status == ReservationStatus::New { + txn.protect(segment_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; + kexec_memory_metadata.insert_memory_range(segment_range); + } + } + } + + // verify the signature of the kexec blob + if let Err(result) = verify_kernel_pe_signature(&kexec_kernel_blob[..], certs) { + return Err(VsmError::SignatureVerificationFailed(result)); + } + Ok(()) + })?; + + // register the protected kexec memory ranges to support possible invalidation in the future + kexec_metadata_ref.register_memory(kexec_memory_metadata); + + Ok(0) + } + + /// HEKI handler for patching kernel or module text. VTL0 kernel calls this function to patch certain kernel or module + /// text region (which it does not have a permission to modify). It passes `HekiPatch` structure which can be stored + /// within one or across two likely non-contiguous physical pages. + pub fn patch_text( + &self, + writer: &W, + patch_pa_0: u64, + patch_pa_1: u64, + ) -> Result { + let heki_patch = copy_heki_patch_from_vtl0(&self.gate, patch_pa_0, patch_pa_1)?; + log::debug!("HEKI: {heki_patch:?}"); + + let precomputed_patch = self + .find_precomputed_patch(&heki_patch) + .ok_or(VsmError::PrecomputedPatchNotFound)?; + + if let Some(validated) = ValidatedTextPatch::prepare(&heki_patch, &precomputed_patch)? { + validated.apply(writer)?; + } + Ok(0) + } + + pub fn allocate_ringbuffer_memory(&self, phys_addr: u64, size: u64) -> Result { + if self.gate.end_of_boot_reached() { + return Err(VsmError::OperationAfterEndOfBoot("ring buffer allocation")); + } + + let end = phys_addr + .checked_add(size) + .ok_or(VsmError::IntegerOverflow) + .and_then(|end| PhysAddr::try_new(end).map_err(|_| VsmError::InvalidPhysicalAddress))?; + let phys_addr = PhysAddr::new(phys_addr); + let frame_range = PhysFrame::range( + PhysFrame::from_start_address(phys_addr) + .map_err(|_| VsmError::AddressNotPageAligned)?, + PhysFrame::from_start_address(end).map_err(|_| VsmError::AddressNotPageAligned)?, + ); + self.gate + .protect_frames(frame_range, MemAttr::MEM_ATTR_READ)?; + self.gate.install_ringbuffer(phys_addr.as_u64(), size); + log::debug!("HEKI: Ring buffer allocated"); + Ok(0) + } +} // impl Heki + +/// Copies patch data in a `HekiPatch` structure from VTL0 to VTL1. The patch +/// data can live within one physical page or across two likely +/// non-contiguous physical pages. +fn copy_heki_patch_from_vtl0( + gate: &P, + patch_pa_0: u64, + patch_pa_1: u64, +) -> Result { + let patch_pa_0 = PhysAddr::try_new(patch_pa_0).map_err(|_| VsmError::InvalidPhysicalAddress)?; + let patch_pa_1 = PhysAddr::try_new(patch_pa_1).map_err(|_| VsmError::InvalidPhysicalAddress)?; + if patch_pa_0.is_null() || patch_pa_0 == patch_pa_1 || !patch_pa_1.is_aligned(Size4KiB::SIZE) { + return Err(VsmError::InvalidInputAddress); + } + let bytes_in_first_page = if patch_pa_0.is_aligned(Size4KiB::SIZE) { + core::cmp::min(PAGE_SIZE, core::mem::size_of::()) + } else { + core::cmp::min( + (patch_pa_0.align_up(Size4KiB::SIZE) - patch_pa_0).trunc(), + core::mem::size_of::(), + ) + }; + + if (bytes_in_first_page < core::mem::size_of::() && patch_pa_1.is_null()) + || (bytes_in_first_page == core::mem::size_of::() && !patch_pa_1.is_null()) + { + return Err(VsmError::InvalidInputAddress); + } + + let heki_patch = if patch_pa_1.is_null() + || (patch_pa_0.align_up(Size4KiB::SIZE) == patch_pa_1.align_down(Size4KiB::SIZE)) + { + gate.read_vtl0_val::(patch_pa_0.as_u64()) + } else { + let mut heki_patch = HekiPatch::new_zeroed(); + let heki_patch_bytes = heki_patch.as_mut_bytes(); + let pages = [ + PhysPageAddr::::new(patch_pa_0.align_down(Size4KiB::SIZE).as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?, + PhysPageAddr::::new(patch_pa_1.as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?, + ]; + gate.read_vtl0_pages( + &pages, + (patch_pa_0 - patch_pa_0.align_down(Size4KiB::SIZE)).trunc(), + heki_patch_bytes, + ) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + Ok(heki_patch) + }?; + + if heki_patch.is_valid() { + Ok(heki_patch) + } else { + Err(VsmError::InvalidInputAddress) + } +} +/// Copies `HekiPage` structures from VTL0 and returns a vector of them. `pa` and +/// `nranges` specify the physical address range holding one or more `HekiPage`s. +fn copy_heki_pages_from_vtl0( + gate: &P, + pa: u64, + nranges: u64, +) -> Option> { + let mut heki_pages = Vec::new(); + heki_pages.try_reserve(nranges.trunc()).ok()?; + let mut visited_pages = HashSet::new(); + let mut range: u64 = 0; + + let mut cur_pa = PhysAddr::try_new(pa).ok()?; + while range < nranges { + if visited_pages.contains(&cur_pa.as_u64()) { + return None; + } + let heki_page = gate.read_vtl0_val::(cur_pa.as_u64()).ok()?; + if !heki_page.is_valid() { + return None; + } + visited_pages.insert(cur_pa.as_u64()); + + range = range.checked_add(heki_page.nranges)?; + if range < nranges && (heki_page.next_pa == 0 || visited_pages.contains(&heki_page.next_pa)) + { + return None; + } + // `HekiPage::is_valid` already validated `next_pa`. + cur_pa = PhysAddr::new(heki_page.next_pa); + heki_pages.push(heki_page); + } + + Some(heki_pages) +} + +/// Parse a concatenated run of DER-encoded X.509 certificates. +fn parse_certs(mut buf: &[u8]) -> Result, VsmError> { + let mut certs = Vec::new(); + + while buf.len() >= 4 && buf[0] == 0x30 && buf[1] == 0x82 { + let der_len = ((buf[2] as usize) << 8) | (buf[3] as usize); + let total_len = der_len + 4; + + if buf.len() < total_len { + return Err(VsmError::CertificateDerLengthInvalid { + expected: total_len, + actual: buf.len(), + }); + } + + let cert_bytes = &buf[..total_len]; + let cert = + Certificate::from_der(cert_bytes).map_err(|_| VsmError::CertificateParseFailed)?; + certs.push(cert); + buf = &buf[total_len..]; + } + Ok(certs) +} + +/// A text patch that matched VTL1's precomputed patch data, bundled with the +/// VTL0 write parameters it authorizes. +/// +/// Its fields are private and it is constructible only via `prepare`, which +/// performs the `validate_text_patch` check, so this answers *what* may be +/// written. *Whether* the caller may bypass protection masks at all is a +/// separate question, answered by holding a [`Vtl0PrivilegedWrite`]. +pub(crate) struct ValidatedTextPatch<'a> { + pages: [PhysPageAddr; 2], + page_count: usize, + offset: usize, + bytes: &'a [u8], +} + +impl<'a> ValidatedTextPatch<'a> { + /// Validate `patch` against `precomputed`; on success, compute the target + /// page(s), in-page offset, and bytes. Returns `Err(TextPatchSuspicious)` if + /// validation fails, or `Ok(None)` if the patch is empty (nothing to write). + fn prepare(patch: &'a HekiPatch, precomputed: &HekiPatch) -> Result, VsmError> { + if !validate_text_patch(patch, precomputed) { + return Err(VsmError::TextPatchSuspicious); + } + let bytes = &patch.code[..usize::from(patch.size)]; + if bytes.is_empty() { + return Ok(None); + } + + let pa_0 = PhysAddr::new(patch.pa[0]); + let pa_0_page = pa_0.align_down(Size4KiB::SIZE); + let offset = (pa_0 - pa_0_page).trunc(); + let page0 = PhysPageAddr::::new(pa_0_page.as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?; + + // Page count comes from the write extent, not `pa[1]`, so prepare stays + // self-contained instead of depending on the `HekiPatch::is_valid` + // coupling (`pa[1].is_null()` iff the patch fits one page). A straddling + // write's second page is `pa[1]`, which step-2 validation pins. + let (target_pages, page_count) = if offset + bytes.len() > PAGE_SIZE { + let pa_1_page = PhysAddr::new(patch.pa[1]).align_down(Size4KiB::SIZE); + let page1 = PhysPageAddr::::new(pa_1_page.as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?; + ([page0, page1], 2) + } else { + ([page0, page0], 1) + }; + + Ok(Some(Self { + pages: target_pages, + page_count, + offset, + bytes, + })) + } + + /// Perform the write this patch authorizes, consuming the proof. + /// + /// The write parameters are never handed out separately, so a validated + /// page list cannot be paired with some other patch's offset or bytes. + fn apply(self, writer: &impl Vtl0PrivilegedWrite) -> Result<(), VsmError> { + writer.write_vtl0_pages(&self.pages[..self.page_count], self.offset, self.bytes) + } +} diff --git a/litebox_service_heki/src/lib.rs b/litebox_service_heki/src/lib.rs new file mode 100644 index 0000000000..9b9667d266 --- /dev/null +++ b/litebox_service_heki/src/lib.rs @@ -0,0 +1,939 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#![cfg(target_arch = "x86_64")] +#![no_std] + +//! HEKI service: VTL0 protection policy (kernel/module/kexec integrity, text +//! patching) expressed purely over [`litebox_common_lvbs::Vtl0Gate`], with no +//! knowledge of Hyper-V or of VTL1's own setup. +//! +//! This module holds [`Heki`] — the service itself, which owns the gate — and +//! the data types its handlers work in. The handlers live in `handlers`. + +extern crate alloc; + +mod handlers; +mod mem_integrity; + +use alloc::{boxed::Box, ffi::CString, string::String, vec::Vec}; +use core::ffi::{CStr, c_char}; +use core::{ + mem, + ops::Range, + sync::atomic::{AtomicI64, Ordering}, +}; +use hashbrown::HashMap; +use litebox::utils::TruncateExt; +use litebox_common_lvbs::{ + HekiKernelSymbol, HekiPatch, HekiPatchInfo, HekiRange, ModMemType, VsmError, Vtl0Gate, +}; +use thiserror::Error; +use x86_64::{ + PhysAddr, VirtAddr, + structures::paging::{PageSize, PhysFrame, Size4KiB, frame::PhysFrameRange}, +}; +use x509_cert::Certificate; + +/// The HEKI service: the [`Vtl0Gate`] it acts through, plus VTL1's own record +/// of what it has protected in VTL0 — module and kexec memory, precomputed +/// patches, certificates, and the kernel symbol tables. +/// +/// Everything here is copied into VTL1 rather than read from VTL0 on demand, so +/// policy decisions cannot be raced by VTL0 mutating the data behind them. +pub struct Heki { + /// The VTL0 capability every handler acts through. Owned rather than + /// borrowed: gate types are zero-sized, so this costs nothing and keeps the + /// gate out of every handler signature. + pub(crate) gate: P, + pub(crate) module_memory_metadata: ModuleMemoryMetadataMap, + system_certs: once_cell::race::OnceBox>, + pub(crate) kexec_metadata: KexecMemoryMetadataWrapper, + pub(crate) crash_kexec_metadata: KexecMemoryMetadataWrapper, + pub(crate) precomputed_patches: PatchDataMap, + pub(crate) symbols: SymbolTable, + pub(crate) gpl_symbols: SymbolTable, + // TODO: revocation cert, blocklist, etc. +} + +/// Construction and state access. +/// +/// The impl is split by role: this block is the service's own bookkeeping, and +/// nothing in it is a VTL call entry point. Those live in `handlers`. +impl Heki

{ + pub fn new(gate: P) -> Self { + Self { + gate, + module_memory_metadata: ModuleMemoryMetadataMap::new(), + system_certs: once_cell::race::OnceBox::new(), + kexec_metadata: KexecMemoryMetadataWrapper::new(), + crash_kexec_metadata: KexecMemoryMetadataWrapper::new(), + precomputed_patches: PatchDataMap::new(), + symbols: SymbolTable::new(), + gpl_symbols: SymbolTable::new(), + } + } + + pub(crate) fn set_system_certificates(&self, certs: Vec) { + let boxed_slice = certs.into_boxed_slice(); + let _ = self.system_certs.set(boxed_slice.into()); + } + + pub(crate) fn get_system_certificates(&self) -> Option<&[Certificate]> { + self.system_certs.get().map(|b| &**b) + } + + /// This function finds the precomputed patch data corresponding to the input patch data. + /// + /// Each step of `text_poke_bp_batch` only exposes a portion of the target's address range, + /// so we look up in the precomputed map by two keys derived from `patch_data.pa[0]`: + /// - `pa[0]` matches step 1 or 3 (target's first byte) and, for a precomputed patch that + /// straddles at offset 1, step 2. + /// - `pa[0] - 1` matches step 2 where `patch.pa[0] == precomputed.pa[0] + 1`. + /// + /// No legitimate step requires looking up by `patch.pa[1]`. + pub(crate) fn find_precomputed_patch(&self, patch_data: &HekiPatch) -> Option { + // `HekiPatch::is_valid` already validated both physical addresses. + let patch_pa_0 = PhysAddr::new(patch_data.pa[0]); + let patch_pa_0_prev = patch_data.pa[0].checked_sub(1).map(PhysAddr::new); + + self.precomputed_patches + .get(patch_pa_0) + .or_else(|| patch_pa_0_prev.and_then(|pa| self.precomputed_patches.get(pa))) + } +} + +/// Data structure for maintaining the memory ranges of each VTL0 kernel module and their types +pub(crate) struct ModuleMemoryMetadataMap { + inner: spin::mutex::SpinMutex>, + key_gen: AtomicI64, +} + +pub(crate) struct ModuleMemoryMetadata { + ranges: Vec, + patch_targets: Vec, +} + +impl ModuleMemoryMetadata { + pub fn new() -> Self { + Self { + ranges: Vec::new(), + patch_targets: Vec::new(), + } + } + + #[inline] + pub(crate) fn insert_heki_range(&mut self, heki_range: &HekiRange) { + // `HekiRange::is_valid` already validated these addresses. + let pa = heki_range.pa; + let epa = heki_range.epa; + self.insert_memory_range(ModuleMemoryRange::new_checked( + pa, + epa, + heki_range.mod_mem_type(), + )); + } + + #[inline] + pub(crate) fn insert_memory_range(&mut self, mem_range: ModuleMemoryRange) { + self.ranges.push(mem_range); + } + + #[inline] + pub(crate) fn insert_patch_target(&mut self, patch_target: PhysAddr) { + self.patch_targets.push(patch_target); + } + + // This function returns patch targets belonging to this module to remove them + // from the precomputed patch data map when the module is unloaded. + #[inline] + pub(crate) fn get_patch_targets(&self) -> &Vec { + &self.patch_targets + } + + /// Returns an iterator over the memory ranges. + pub fn iter(&self) -> core::slice::Iter<'_, ModuleMemoryRange> { + self.ranges.iter() + } +} + +impl Default for ModuleMemoryMetadata { + fn default() -> Self { + Self::new() + } +} + +impl<'a> IntoIterator for &'a ModuleMemoryMetadata { + type Item = &'a ModuleMemoryRange; + type IntoIter = core::slice::Iter<'a, ModuleMemoryRange>; + + fn into_iter(self) -> Self::IntoIter { + self.ranges.iter() + } +} + +#[derive(Clone, Copy)] +pub(crate) struct ModuleMemoryRange { + pub phys_frame_range: PhysFrameRange, + pub mod_mem_type: ModMemType, +} + +impl ModuleMemoryRange { + /// Create a memory range from values which are already validated. + pub(crate) fn new_checked(phys_start: u64, phys_end: u64, mod_mem_type: ModMemType) -> Self { + let phys_start = PhysAddr::new(phys_start); + let phys_end = PhysAddr::new(phys_end); + Self { + phys_frame_range: PhysFrame::range( + PhysFrame::containing_address(phys_start), + PhysFrame::containing_address(phys_end), + ), + mod_mem_type, + } + } +} + +impl Default for ModuleMemoryRange { + fn default() -> Self { + Self { + phys_frame_range: PhysFrame::range( + PhysFrame::containing_address(PhysAddr::zero()), + PhysFrame::containing_address(PhysAddr::zero()), + ), + mod_mem_type: ModMemType::Unknown, + } + } +} + +impl ModuleMemoryMetadataMap { + pub(crate) fn new() -> Self { + Self { + inner: spin::mutex::SpinMutex::new(HashMap::new()), + key_gen: AtomicI64::new(0), + } + } + + /// Generate a unique key for representing each loaded kernel module. + /// It assumes a 64-bit atomic counter is sufficient and there is no run out of keys. + fn gen_unique_key(&self) -> i64 { + self.key_gen.fetch_add(1, Ordering::Relaxed) + } + + pub(crate) fn contains_key(&self, key: i64) -> bool { + self.inner.lock().contains_key(&key) + } + + /// Register a new module memory metadata structure in the map and return a unique key/token for it. + pub(crate) fn register_module_memory_metadata( + &self, + module_memory: ModuleMemoryMetadata, + ) -> i64 { + let key = self.gen_unique_key(); + + let mut map = self.inner.lock(); + assert!( + !map.contains_key(&key), + "HEKI: Key {key} already exists in the module memory map", + ); + let _ = map.insert(key, module_memory); + + key + } + + pub(crate) fn remove(&self, key: i64) -> bool { + let mut map = self.inner.lock(); + map.remove(&key).is_some() + } + + /// Drop a module's freed init ranges from its metadata after [`crate::Heki::free_guest_module_init`] + /// hands them back to VTL0, so a later free/unload does not re-release them. + /// + /// It also returns patch targets that fell within this freed init frames. These patch targets + /// are no longer valid (i.e., potential patch-after-free) and thus their corresponding + /// precomputed patches should be removed (we can't remove them here due to locks). + pub(crate) fn remove_init_ranges(&self, key: i64) -> Vec { + let is_init = |t| { + matches!( + t, + ModMemType::InitText | ModMemType::InitData | ModMemType::InitRoData + ) + }; + let mut map = self.inner.lock(); + let Some(metadata) = map.get_mut(&key) else { + return Vec::new(); + }; + let init_ranges: Vec> = metadata + .ranges + .iter() + .filter(|r| is_init(r.mod_mem_type)) + .map(|r| r.phys_frame_range) + .collect(); + metadata.ranges.retain(|r| !is_init(r.mod_mem_type)); + let mut freed_patch_targets = Vec::new(); + metadata.patch_targets.retain(|&pa| { + let freed = init_ranges + .iter() + .any(|fr| fr.start.start_address() <= pa && fr.end.start_address() > pa); + if freed { + freed_patch_targets.push(pa); + false + } else { + true + } + }); + freed_patch_targets + } + + /// Return the addresses of patch targets belonging to a module identified by `key` + pub(crate) fn get_patch_targets(&self, key: i64) -> Option> { + let guard = self.inner.lock(); + guard + .get(&key) + .map(|metadata| metadata.get_patch_targets().clone()) + } + + pub(crate) fn iter_entry(&self, key: i64) -> Option> { + let guard = self.inner.lock(); + if guard.contains_key(&key) { + Some(ModuleMemoryMetadataIters { + guard, + key, + phantom: core::marker::PhantomData, + }) + } else { + None + } + } +} + +impl Default for ModuleMemoryMetadataMap { + fn default() -> Self { + Self::new() + } +} + +pub(crate) struct ModuleMemoryMetadataIters<'a> { + guard: spin::mutex::SpinMutexGuard<'a, HashMap>, + key: i64, + phantom: core::marker::PhantomData<&'a PhysFrameRange>, +} + +impl<'a> ModuleMemoryMetadataIters<'a> { + /// Returns an iterator over the memory ranges. + /// + /// # Panics + /// + /// Panics if the key is not found in the guard. + pub(crate) fn iter_mem_ranges(&'a self) -> impl Iterator { + self.guard.get(&self.key).unwrap().ranges.iter() + } +} + +/// Data structure for maintaining the memory content of a kernel module by its sections. Currently, it only maintains +/// certain sections like `.text` and `.init.text` which are needed for module validation. +pub(crate) struct ModuleMemory { + text: MemoryContainer, + init_text: MemoryContainer, + init_rodata: MemoryContainer, +} + +impl Default for ModuleMemory { + fn default() -> Self { + Self::new() + } +} + +impl ModuleMemory { + pub(crate) fn new() -> Self { + Self { + text: MemoryContainer::new(), + init_text: MemoryContainer::new(), + init_rodata: MemoryContainer::new(), + } + } + + /// Return a memory container for a section of the module memory by its name + pub(crate) fn find_section_by_name(&self, name: &str) -> Option<&MemoryContainer> { + match name { + ".text" => Some(&self.text), + ".init.text" => Some(&self.init_text), + ".init.rodata" => Some(&self.init_rodata), + _ => None, + } + } + + /// Write physical memory bytes from VTL0 specified in `HekiRange` at the specified virtual address of + /// a certain memory container based on the memory/section type. + #[inline] + pub(crate) fn write_bytes_from_heki_range( + &mut self, + gate: &P, + ) -> Result<(), MemoryContainerError> { + self.text.write_bytes_from_heki_range(gate)?; + self.init_text.write_bytes_from_heki_range(gate)?; + self.init_rodata.write_bytes_from_heki_range(gate)?; + Ok(()) + } + + pub(crate) fn extend_range( + &mut self, + mod_mem_type: ModMemType, + heki_range: &HekiRange, + ) -> Result<(), VsmError> { + match mod_mem_type { + ModMemType::Text => self.text.extend_range(heki_range)?, + ModMemType::InitText => self.init_text.extend_range(heki_range)?, + ModMemType::InitRoData => self.init_rodata.extend_range(heki_range)?, + _ => {} + } + Ok(()) + } +} + +/// Data structure for abstracting addressable paged memory. Unlike `ModuleMemoryMetadataMap` which maintains +/// physical/virtual address ranges and their access permissions, this structure stores actual data in memory pages. +/// This structure allows us to handle data copied from VTL0 (e.g., for virtual-address-based page sorting) without +/// explicit page mappings at VTL1. +/// This structure is expected to be used locally and temporarily, so we do not protect it with a lock. +#[derive(Clone, Copy)] +struct MemoryRange { + addr: VirtAddr, + phys_addr: PhysAddr, + len: u64, +} + +pub(crate) struct MemoryContainer { + range: Vec, + buf: Vec, +} + +impl Default for MemoryContainer { + fn default() -> Self { + Self::new() + } +} + +impl MemoryContainer { + pub(crate) fn new() -> Self { + Self { + range: Vec::new(), + buf: Vec::new(), + } + } + + /// Return the byte length of the memory container + pub(crate) fn len(&self) -> usize { + self.buf.len() + } + + /// Check if the memory container is empty + pub(crate) fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub(crate) fn get_range(&self) -> Option> { + let start_range = self.range.first()?; + let end_range = self.range.last()?; + let end = end_range.addr.as_u64().checked_add(end_range.len)?; + Some(Range { + start: start_range.addr, + end: VirtAddr::try_new(end).ok()?, + }) + } + + pub(crate) fn extend_range(&mut self, heki_range: &HekiRange) -> Result<(), VsmError> { + // `HekiRange::is_valid` already validated the addresses and `pa <= epa`. + let addr = VirtAddr::new(heki_range.va); + let phys_addr = PhysAddr::new(heki_range.pa); + let len = heki_range.epa - heki_range.pa; + if let Some(last_range) = self.range.last() + && VirtAddr::try_new( + last_range + .addr + .as_u64() + .checked_add(last_range.len) + .ok_or(VsmError::IntegerOverflow)?, + ) + .map_err(|_| VsmError::InvalidVirtualAddress)? + != addr + { + log::debug!("Discontiguous address found {heki_range:?}"); + // NOTE: Intentionally not returning an error here. + // TODO: This should be an error once patch_info is fixed from VTL0 + // It will simplify patch_info and heki_range parsing as well + } + self.range.push(MemoryRange { + addr, + phys_addr, + len, + }); + Ok(()) + } + + /// Write physical memory bytes from VTL0 specified in `HekiRange` at the specified virtual address + #[inline] + pub(crate) fn write_bytes_from_heki_range( + &mut self, + gate: &P, + ) -> Result<(), MemoryContainerError> { + let mut len: usize = 0; + if self.buf.is_empty() { + for range in &self.range { + let range_len: usize = range.len.trunc(); + len = len + .checked_add(range_len) + .ok_or(MemoryContainerError::Overflow)?; + } + self.buf.reserve_exact(len); + } + + let range = self.range.clone(); + for range in range { + let phys_end = range + .phys_addr + .as_u64() + .checked_add(range.len) + .and_then(|end| PhysAddr::try_new(end).ok()) + .ok_or(MemoryContainerError::Overflow)?; + self.write_vtl0_phys_bytes(gate, range.phys_addr, phys_end)?; + } + Ok(()) + } + + /// Write physical memory bytes from VTL0 at the specified physical address + pub(crate) fn write_vtl0_phys_bytes( + &mut self, + gate: &P, + phys_start: PhysAddr, + phys_end: PhysAddr, + ) -> Result<(), MemoryContainerError> { + let bytes_to_copy: usize = (phys_end - phys_start).trunc(); + if bytes_to_copy == 0 { + return Ok(()); + } + + let old_len = self.buf.len(); + self.buf.resize(old_len + bytes_to_copy, 0); + if gate + .read_vtl0_contiguous(phys_start.as_u64(), &mut self.buf[old_len..]) + .is_err() + { + self.buf.truncate(old_len); + return Err(MemoryContainerError::CopyFromVtl0Failed); + } + Ok(()) + } +} + +impl core::ops::Deref for MemoryContainer { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.buf + } +} + +/// Errors for memory container operations. +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +pub(crate) enum MemoryContainerError { + #[error("failed to copy data from VTL0")] + CopyFromVtl0Failed, + #[error("integer overflow while processing VTL0 memory")] + Overflow, +} + +pub(crate) struct KexecMemoryMetadataWrapper { + inner: spin::mutex::SpinMutex, +} + +impl Default for KexecMemoryMetadataWrapper { + fn default() -> Self { + Self::new() + } +} + +impl KexecMemoryMetadataWrapper { + pub(crate) fn new() -> Self { + Self { + inner: spin::mutex::SpinMutex::new(KexecMemoryMetadata::new()), + } + } + + pub(crate) fn clear_memory(&self) { + let mut inner = self.inner.lock(); + inner.clear(); + } + + pub(crate) fn register_memory(&self, kexec_memory: KexecMemoryMetadata) { + let mut inner = self.inner.lock(); + inner.ranges = kexec_memory.ranges; + } + + pub(crate) fn iter_guarded(&self) -> KexecMemoryMetadataIters<'_> { + KexecMemoryMetadataIters { + guard: self.inner.lock(), + phantom: core::marker::PhantomData, + } + } +} + +// TODO: `ModuleMemoryMetadata` and `KexecMemoryMetadata` are similar. consider merging them into a single structure if possible. +pub(crate) struct KexecMemoryMetadata { + ranges: Vec, +} + +impl KexecMemoryMetadata { + pub fn new() -> Self { + Self { ranges: Vec::new() } + } + + #[inline] + pub(crate) fn insert_heki_range(&mut self, heki_range: &HekiRange) -> Result<(), VsmError> { + // `HekiRange::is_valid` already validated these addresses. + if !heki_range.is_aligned(Size4KiB::SIZE) { + return Err(VsmError::AddressNotPageAligned); + } + let pa = heki_range.pa; + let epa = heki_range.epa; + self.insert_memory_range(KexecMemoryRange::new_checked(pa, epa)); + Ok(()) + } + + #[inline] + pub(crate) fn insert_memory_range(&mut self, mem_range: KexecMemoryRange) { + self.ranges.push(mem_range); + } + + #[inline] + pub(crate) fn clear(&mut self) { + self.ranges.clear(); + } + + /// Returns an iterator over the memory ranges. + pub fn iter(&self) -> core::slice::Iter<'_, KexecMemoryRange> { + self.ranges.iter() + } +} + +impl Default for KexecMemoryMetadata { + fn default() -> Self { + Self::new() + } +} + +impl<'a> IntoIterator for &'a KexecMemoryMetadata { + type Item = &'a KexecMemoryRange; + type IntoIter = core::slice::Iter<'a, KexecMemoryRange>; + + fn into_iter(self) -> Self::IntoIter { + self.ranges.iter() + } +} + +pub(crate) struct KexecMemoryMetadataIters<'a> { + guard: spin::mutex::SpinMutexGuard<'a, KexecMemoryMetadata>, + phantom: core::marker::PhantomData<&'a PhysFrameRange>, +} + +impl<'a> KexecMemoryMetadataIters<'a> { + pub(crate) fn iter_mem_ranges(&'a self) -> impl Iterator { + self.guard.ranges.iter() + } +} + +#[derive(Clone, Copy)] +pub(crate) struct KexecMemoryRange { + pub phys_frame_range: PhysFrameRange, +} + +impl KexecMemoryRange { + /// Create a memory range from values which are already validated. + pub(crate) fn new_checked(phys_start: u64, phys_end: u64) -> Self { + let phys_start = PhysAddr::new(phys_start); + let phys_end = PhysAddr::new(phys_end); + Self { + phys_frame_range: PhysFrame::range( + PhysFrame::from_start_address(phys_start) + .expect("kexec memory start address is not page-aligned"), + PhysFrame::from_start_address(phys_end) + .expect("kexec memory end address is not page-aligned"), + ), + } + } + + pub(crate) fn new(phys_start: u64, phys_end: u64) -> Result { + let phys_start = + PhysAddr::try_new(phys_start).map_err(|_| VsmError::InvalidPhysicalAddress)?; + let phys_end = PhysAddr::try_new(phys_end).map_err(|_| VsmError::InvalidPhysicalAddress)?; + Ok(Self { + phys_frame_range: PhysFrame::range( + PhysFrame::from_start_address(phys_start) + .map_err(|_| VsmError::AddressNotPageAligned)?, + PhysFrame::from_start_address(phys_end) + .map_err(|_| VsmError::AddressNotPageAligned)?, + ), + }) + } +} + +impl Default for KexecMemoryRange { + fn default() -> Self { + Self { + phys_frame_range: PhysFrame::range( + PhysFrame::containing_address(PhysAddr::zero()), + PhysFrame::containing_address(PhysAddr::zero()), + ), + } + } +} + +pub(crate) struct PatchDataMap { + inner: spin::rwlock::RwLock>, +} + +impl Default for PatchDataMap { + fn default() -> Self { + Self::new() + } +} + +impl PatchDataMap { + pub(crate) fn new() -> Self { + Self { + inner: spin::rwlock::RwLock::new(HashMap::new()), + } + } + + #[inline] + pub(crate) fn remove_patch_data(&self, patch_targets: &Vec) { + let mut inner = self.inner.write(); + for key in patch_targets { + inner.remove(key); + } + } + + #[inline] + pub(crate) fn get(&self, addr: PhysAddr) -> Option { + let inner = self.inner.read(); + inner.get(&addr).copied() + } + + /// Add patch data from a buffer containing `HekiPatchInfo` and `HekiPatch` structures. + /// If this patch data is from a module (`module_memory_metadata` is `Some`), this function + /// denies any patch target addresses not within the module's executable memory ranges. + pub(crate) fn insert_patch_data_from_bytes( + &self, + patch_info_buf: &[u8], + mut module_memory_metadata: Option<&mut ModuleMemoryMetadata>, + ) -> Result<(), PatchDataMapError> { + if patch_info_buf.len() < core::mem::size_of::() { + return Err(PatchDataMapError::InvalidHekiPatchInfo); + } + + let mut parsed: Vec<(PhysAddr, HekiPatch)> = Vec::new(); + + // the buffer looks like below: + // [`HekiPatchInfo`, [`HekiPatch`, ...], `HekiPatchInfo`, [`HekiPatch`, ...], ...] + // Each `HekiPatchInfo`'s `patch_index` field specifies the number of `HekiPatch` entries that follow it. + // The buffer may have trailing bytes (from page-aligned VTL0 ranges) that don't form a valid record. + let mut index: usize = 0; + while index + core::mem::size_of::() <= patch_info_buf.len() { + let Some(patch_info) = HekiPatchInfo::try_from_bytes( + &patch_info_buf[index..index + core::mem::size_of::()], + ) else { + // Remaining bytes don't form a valid header. End of meaningful patch data. + break; + }; + + let patch_index: usize = patch_info.patch_index.trunc(); + let total_patch_size = core::mem::size_of::() + .checked_mul(patch_index) + .ok_or(PatchDataMapError::InvalidHekiPatchInfo)?; + let patches_start = index + .checked_add(core::mem::size_of::()) + .ok_or(PatchDataMapError::InvalidHekiPatchInfo)?; + let patches_end = patches_start + .checked_add(total_patch_size) + .filter(|&end| end <= patch_info_buf.len()) + .ok_or(PatchDataMapError::InvalidHekiPatchInfo)?; + + for patch in patch_info_buf[patches_start..patches_end] + .chunks(core::mem::size_of::()) + .map(HekiPatch::try_from_bytes) + { + let patch = patch.ok_or(PatchDataMapError::InvalidHekiPatch)?; + // `HekiPatch::try_from_bytes` already validated both physical addresses. + let patch_target_pa_0 = PhysAddr::new(patch.pa[0]); + let patch_target_pa_1 = PhysAddr::new(patch.pa[1]); + + // The second page is used as an additional key when a patch straddles two physical + // pages (see `validate_text_poke_bp_batch`). + let straddles_second_page = !patch_target_pa_1.is_null() + && patch_target_pa_0 + .as_u64() + .checked_add(1) + .and_then(|next| PhysAddr::try_new(next).ok()) + .is_some_and(|next| next.is_aligned(Size4KiB::SIZE)); + + if let Some(ref mod_mem_meta) = module_memory_metadata { + // Only accept patch targets within the module's executable ranges. + let in_executable_range = mod_mem_meta.iter().any(|mod_mem_range| { + let in_range = |pa: PhysAddr| { + mod_mem_range.phys_frame_range.start.start_address() <= pa + && mod_mem_range.phys_frame_range.end.start_address() > pa + }; + matches!( + mod_mem_range.mod_mem_type, + ModMemType::Text | ModMemType::InitText + ) && in_range(patch_target_pa_0) + && (patch_target_pa_1.is_null() || in_range(patch_target_pa_1)) + }); + if !in_executable_range { + continue; + } + } + + parsed.push((patch_target_pa_0, patch)); + if straddles_second_page { + parsed.push((patch_target_pa_1, patch)); + } + } + index = patches_end; + } + + // Commit every parsed patch and record its targets for later unload cleanup. + let mut inner = self.inner.write(); + for (target, patch) in parsed { + inner.insert(target, patch); + if let Some(ref mut mod_mem_meta) = module_memory_metadata { + mod_mem_meta.insert_patch_target(target); + } + } + + Ok(()) + } +} + +/// Errors for patch data map operations. +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +pub(crate) enum PatchDataMapError { + #[error("invalid HEKI patch info")] + InvalidHekiPatchInfo, + #[error("invalid HEKI patch")] + InvalidHekiPatch, +} + +// TODO: Use this to resolve symbols in modules +pub(crate) struct Symbol { + _value: u64, +} + +impl Symbol { + /// Parse a symbol from a byte buffer. + pub(crate) fn from_bytes( + kinfo_start: usize, + start: VirtAddr, + bytes: &[u8], + ) -> Result<(String, Self), VsmError> { + let kinfo_bytes = &bytes[kinfo_start..]; + let ksym = HekiKernelSymbol::from_bytes(kinfo_bytes)?; + + let value_addr = start + mem::offset_of!(HekiKernelSymbol, value_offset) as u64; + let value = value_addr + .as_u64() + .wrapping_add_signed(i64::from(ksym.value_offset)); + + let name_offset = kinfo_start + + mem::offset_of!(HekiKernelSymbol, name_offset) + + usize::try_from(ksym.name_offset).map_err(|_| VsmError::SymbolNameOffsetInvalid)?; + + if name_offset >= bytes.len() { + return Err(VsmError::SymbolNameOffsetInvalid); + } + let name_len = bytes[name_offset..] + .iter() + .position(|&b| b == 0) + .ok_or(VsmError::SymbolNameNoTerminator)?; + if name_len >= HekiKernelSymbol::KSY_NAME_LEN { + return Err(VsmError::SymbolNameTooLong); + } + + // SAFETY: + // - offset is within bytes (checked above) + // - there is a NUL terminator within bytes[offset..] (checked above) + // - Length of name string is within spec range (checked above) + // - bytes is still valid for the duration of this function + let name_str = unsafe { + let name_ptr = bytes.as_ptr().add(name_offset).cast::(); + CStr::from_ptr(name_ptr) + }; + let name = CString::new( + name_str + .to_str() + .map_err(|_| VsmError::SymbolNameInvalidUtf8)?, + ) + .map_err(|_| VsmError::SymbolNameInvalidUtf8)?; + let name = name + .into_string() + .map_err(|_| VsmError::SymbolNameInvalidUtf8)?; + Ok((name, Symbol { _value: value })) + } +} + +pub(crate) struct SymbolTable { + inner: spin::rwlock::RwLock>, +} + +impl Default for SymbolTable { + fn default() -> Self { + Self::new() + } +} + +impl SymbolTable { + pub(crate) fn new() -> Self { + Self { + inner: spin::rwlock::RwLock::new(HashMap::new()), + } + } + + /// Build a symbol table from a memory container. + pub(crate) fn build_from_container( + &self, + start: VirtAddr, + end: VirtAddr, + mem: &MemoryContainer, + buf: &[u8], + ) -> Result { + if mem.is_empty() { + return Err(VsmError::SymbolTableEmpty); + } + let Some(range) = mem.get_range() else { + return Err(VsmError::SymbolTableEmpty); + }; + if start < range.start || end > range.end { + return Err(VsmError::SymbolTableOutOfRange); + } + + let kinfo_len: usize = (end - start).trunc(); + if !kinfo_len.is_multiple_of(HekiKernelSymbol::KSYM_LEN) { + return Err(VsmError::SymbolTableLengthInvalid); + } + + let mut kinfo_offset: usize = (start - range.start).trunc(); + let mut kinfo_addr = start; + let ksym_count = kinfo_len / HekiKernelSymbol::KSYM_LEN; + let mut inner = self.inner.write(); + inner.reserve(ksym_count); + + for _ in 0..ksym_count { + let (name, sym) = Symbol::from_bytes(kinfo_offset, kinfo_addr, buf)?; + inner.insert(name, sym); + kinfo_offset += HekiKernelSymbol::KSYM_LEN; + kinfo_addr += HekiKernelSymbol::KSYM_LEN as u64; + } + Ok(0) + } +} diff --git a/litebox_platform_lvbs/src/mshv/mem_integrity.rs b/litebox_service_heki/src/mem_integrity.rs similarity index 95% rename from litebox_platform_lvbs/src/mshv/mem_integrity.rs rename to litebox_service_heki/src/mem_integrity.rs index 323b97d52a..8114bb6f6e 100644 --- a/litebox_platform_lvbs/src/mshv/mem_integrity.rs +++ b/litebox_service_heki/src/mem_integrity.rs @@ -3,7 +3,7 @@ //! Functions for checking the memory integrity of VTL0 kernel image and modules -use crate::mshv::vsm::ModuleMemory; +use crate::ModuleMemory; use alloc::{vec, vec::Vec}; use authenticode::{AttributeCertificateIterator, AuthenticodeSignature, authenticode_digest}; use cms::{content_info::ContentInfo, signed_data::SignedData}; @@ -19,7 +19,8 @@ use elf::{ string_table::StringTable, symbol::Symbol, }; -use litebox_common_lvbs::{HekiPatch, ModuleSignature, POKE_MAX_OPCODE_SIZE, VerificationError}; +pub(crate) use litebox_common_lvbs::VerificationError; +use litebox_common_lvbs::{HekiPatch, ModuleSignature, POKE_MAX_OPCODE_SIZE}; use object::read::pe::PeFile64; use rangemap::set::RangeSet; use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs1v15::Signature, signature::Verifier}; @@ -32,9 +33,6 @@ use x509_cert::{ }; use zerocopy::FromBytes; -#[cfg(debug_assertions)] -use crate::debug_serial_println; - /// This function validates the memory content of a loaded kernel module against the original ELF file. /// In particular, it checks whether the non-relocatable/patchable bytes of certain sections /// (e.g., `.text`, `.init.text`) of the module are tampered with. @@ -43,7 +41,7 @@ use crate::debug_serial_println; /// Note that this is mainly for defense-in-depth. Even without this code and data tampering, the compromised /// module loader could still leverage other attack mechanisms like return-oriented programming (ROP). /// In the future, we can add more checks to harden the validation. -pub fn validate_kernel_module_against_elf( +pub(crate) fn validate_kernel_module_against_elf( module_memory: &ModuleMemory, original_elf_data: &[u8], ) -> Result { @@ -130,10 +128,7 @@ pub fn validate_kernel_module_against_elf( section_from_elf[reloc.clone()].copy_from_slice(§ion_in_memory[reloc.clone()]); } if section_from_elf != section_in_memory { - crate::serial_println!( - "Found {} mismatches in {target_section_name}", - target_section_name - ); + log::warn!("Found mismatches in {target_section_name}"); result = false; } } @@ -148,7 +143,7 @@ pub fn validate_kernel_module_against_elf( } } if !diffs.is_empty() { - debug_serial_println!( + log::debug!( "Found {} mismatches in {target_section_name} at {:?}", diffs.len(), diffs @@ -210,7 +205,7 @@ fn identify_direct_relocations( todo!("Unsupported relocation type {:?}", rela.r_type); #[cfg(not(debug_assertions))] { - crate::serial_println!("Unsupported relocation type {:?}", rela.r_type); + log::warn!("Unsupported relocation type {:?}", rela.r_type); return Err(KernelElfError::UnsupportedRelocation); } } @@ -304,7 +299,7 @@ fn identify_indirect_relocations( todo!("Unsupported relocation type {:?}", rela.r_type); #[cfg(not(debug_assertions))] { - crate::serial_println!("Unsupported relocation type {:?}", rela.r_type); + log::warn!("Unsupported relocation type {:?}", rela.r_type); return Err(KernelElfError::UnsupportedRelocation); } } @@ -342,7 +337,7 @@ fn identify_indirect_relocations( /// This function parses the `.modinfo` section of a kernel module ELF #[cfg(debug_assertions)] -pub fn parse_modinfo(original_elf_data: &[u8]) -> Result<(), KernelElfError> { +pub(crate) fn parse_modinfo(original_elf_data: &[u8]) -> Result<(), KernelElfError> { let elf = ElfBytes::::minimal_parse(original_elf_data) .map_err(|_| KernelElfError::ElfParseFailed)?; @@ -370,7 +365,7 @@ pub fn parse_modinfo(original_elf_data: &[u8]) -> Result<(), KernelElfError> { && let Some((k, v)) = s.split_once('=') && k == "name" { - debug_serial_println!("Modinfo: {} = {}", k, v); + log::debug!("Modinfo: {k} = {v}"); } } } @@ -384,7 +379,7 @@ pub fn parse_modinfo(original_elf_data: &[u8]) -> Result<(), KernelElfError> { /// /// Currently, this function is slow because it uses the `sha2` crate with the `force-soft` feature. /// We should consider using HW-accelerated SHA-512 in the future (need to save/restore vector registers). -pub fn verify_kernel_module_signature( +pub(crate) fn verify_kernel_module_signature( signed_module: &[u8], certs: &[Certificate], ) -> Result<(), VerificationError> { @@ -402,7 +397,7 @@ pub fn verify_kernel_module_signature( ); #[cfg(not(debug_assertions))] { - crate::serial_println!( + log::warn!( "Unsupported digest or signature algorithm: {:?}, {:?}", digest_alg, signature_alg @@ -532,7 +527,7 @@ fn decode_signature( /// [EFI boot stub](https://docs.kernel.org/admin-guide/efi-stub.html). This PE header embeds /// [Authenticode signature](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) for UEFI /// Secure Boot. The Authenticode signature is computed over the PE image digest and other attributes. -pub fn verify_kernel_pe_signature( +pub(crate) fn verify_kernel_pe_signature( kernel_blob: &[u8], certs: &[Certificate], ) -> Result<(), VerificationError> { @@ -553,7 +548,7 @@ pub fn verify_kernel_pe_signature( todo!("Unsupported digest algorithm: {:?}", digest_algorithm_oid); #[cfg(not(debug_assertions))] { - crate::serial_println!("Unsupported digest algorithm: {:?}", digest_algorithm_oid); + log::warn!("Unsupported digest algorithm: {:?}", digest_algorithm_oid); return Err(VerificationError::Unsupported); } } @@ -658,7 +653,10 @@ const JMP32_INSN_SIZE: u8 = 5; /// Each invocation of `text_poke_bp_batch` does one of the steps with a portion of the code (1 or n-1 bytes), /// so there are up to three invocations for each target target address. /// Refer [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/arch/x86/kernel/alternative.c#L2164) -pub fn validate_text_poke_bp_batch(patch_data: &HekiPatch, precomputed_patch: &HekiPatch) -> bool { +pub(crate) fn validate_text_poke_bp_batch( + patch_data: &HekiPatch, + precomputed_patch: &HekiPatch, +) -> bool { // step 1 if patch_data.size == 1 && patch_data.code[0] == INT3_INSN_OPCODE @@ -683,7 +681,7 @@ pub fn validate_text_poke_bp_batch(patch_data: &HekiPatch, precomputed_patch: &H return false; } - // step 2. `apply_vtl0_text_patch` uses `patch_data.pa[1]` only when + // step 2. `ValidatedTextPatch` uses `patch_data.pa[1]` only when // `patch_data.pa[0]` leaves the remainder of the patch on the next page. // For a legitimate step 2, that next page is the precomputed patch's pa[1]. if !precomputed_patch_second_byte_pa_aligned && patch_data.pa[1] != precomputed_patch.pa[1] @@ -714,11 +712,24 @@ pub fn validate_text_poke_bp_batch(patch_data: &HekiPatch, precomputed_patch: &H } /// This function checks whether the patch data is valid for a given target -pub fn validate_text_patch(patch_data: &HekiPatch, precomputed_patch: &HekiPatch) -> bool { +pub(crate) fn validate_text_patch(patch_data: &HekiPatch, precomputed_patch: &HekiPatch) -> bool { validate_text_poke_bp_batch(patch_data, precomputed_patch) // TODO: support other patching methods } +/// Errors for kernel ELF validation and relocation. +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +pub(crate) enum KernelElfError { + #[error("failed to parse ELF file")] + ElfParseFailed, + #[error("required section not found")] + SectionNotFound, + #[cfg_attr(debug_assertions, allow(dead_code))] + #[error("unsupported relocation type")] + UnsupportedRelocation, +} + #[cfg(test)] mod tests { use super::*; @@ -747,16 +758,3 @@ mod tests { assert!(!validate_text_poke_bp_batch(&patch_data, &precomputed)); } } - -/// Errors for kernel ELF validation and relocation. -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum KernelElfError { - #[error("failed to parse ELF file")] - ElfParseFailed, - #[error("required section not found")] - SectionNotFound, - #[cfg_attr(debug_assertions, allow(dead_code))] - #[error("unsupported relocation type")] - UnsupportedRelocation, -} diff --git a/litebox_shim_optee/Cargo.toml b/litebox_shim_optee/Cargo.toml index 36dcbaf05a..459c3d7299 100644 --- a/litebox_shim_optee/Cargo.toml +++ b/litebox_shim_optee/Cargo.toml @@ -16,12 +16,14 @@ litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } hmac = { version = "0.12", default-features = false } num_enum = { version = "0.7.3", default-features = false } +rangemap = { version = "1.5.1", features = ["const_fn"] } once_cell = { version = "1.20.2", default-features = false, features = ["alloc", "race"] } sha2 = { version = "0.10", default-features = false } -spin = { version = "0.10.0", default-features = false, features = ["spin_mutex", "once"] } +spin = { version = "0.10.0", default-features = false, features = ["spin_mutex", "rwlock", "once"] } thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } zeroize = { version = "1.8", default-features = false, features = ["alloc"] } +p384 = { version = "0.13.1", default-features = false, features = ["arithmetic", "ecdsa"] } [features] default = ["platform_lvbs"] diff --git a/litebox_shim_optee/src/idk.rs b/litebox_shim_optee/src/idk.rs new file mode 100644 index 0000000000..9205227003 --- /dev/null +++ b/litebox_shim_optee/src/idk.rs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::NormalWorldMutPtr; +use litebox::{LiteBox, mm::linux::PAGE_SIZE, utils::TruncateExt}; +use litebox_common_linux::errno::Errno; +use num_enum::TryFromPrimitive; +use p384::{NonZeroScalar, elliptic_curve::sec1::ToEncodedPoint}; +use spin::Once; +use zeroize::Zeroizing; + +const IDENTITY_SIGNING_PRIVATE_KEY_LEN: usize = 48; +const IDENTITY_SIGNING_PUBLIC_KEY_LEN: usize = 97; +const KEY_ALGORITHM_MASK: u64 = 0xff00; +const KEY_VARIANT_MASK: u64 = 0xff; +const KEY_ALGORITHM_VALUE_MASK: u64 = KEY_ALGORITHM_MASK | KEY_VARIANT_MASK; +const MAX_KEYGEN_ATTEMPT: usize = 256; + +static IDENTITY_SIGNING_KEY_PAIR: Once = Once::new(); + +struct IdentitySigningKeyPair { + #[allow(dead_code, reason = "retained for future IDK_S signing operations")] + private_key: Zeroizing<[u8; IDENTITY_SIGNING_PRIVATE_KEY_LEN]>, + public_key: [u8; IDENTITY_SIGNING_PUBLIC_KEY_LEN], +} + +#[derive(TryFromPrimitive)] +#[repr(u8)] +enum KeyAlgorithm { + Rsa = 0x01, + Ecdsa = 0x02, + Pqc = 0x04, +} + +#[derive(TryFromPrimitive)] +#[repr(u8)] +enum EcdsaCurve { + P256 = 0x01, + P384 = 0x02, + P521 = 0x03, +} + +pub fn generate_identity_signing_key(public_key_pa: u64, key_alg: u64) -> i64 { + match generate_identity_signing_key_inner(public_key_pa, key_alg) { + Ok(res) => res, + Err(e) => e.as_neg().into(), + } +} + +/// This function generates an identity signing key pair (IDK_S) and returns the public +/// portion of it. +/// +/// - `public_key_pa`: VTL0/Normal-world physical address where an uncompressed SEC1 P-384 +/// public key will be written. The corresponding private key is generated by the platform +/// CRNG, retained for the boot cycle, and never leaves VTL1/secure-world. +/// - `key_alg`: Key algorithm namespace and variant. Only ECDSA P-384 is supported. +/// +/// We intentially uses the raw format. Any DER/SPKI wrapping or TCG event‑log construction +/// is the VTL0's responsibility, allowing VTL1 ABI to be independent of verifier's format. +/// +/// This function assumes that the caller prepares a buffer at the given physical +/// address (in a single or contiguous physical memory page(s)) whose length is equal to +/// or greater than `IDENTITY_SIGNING_PUBLIC_KEY_LEN`. +fn generate_identity_signing_key_inner(public_key_pa: u64, key_alg: u64) -> Result { + validate_key_algorithm(key_alg)?; + + let pubkey_ptr = + NormalWorldMutPtr::<[u8; IDENTITY_SIGNING_PUBLIC_KEY_LEN], PAGE_SIZE>::with_usize( + public_key_pa.trunc(), + ) + .map_err(|_| Errno::EINVAL)?; + + let key_pair = get_identity_signing_key_pair()?; + pubkey_ptr + .write_at_offset(0, key_pair.public_key) + .map_err(|_| Errno::EFAULT)?; + Ok(0) +} + +fn validate_key_algorithm(key_alg: u64) -> Result<(), Errno> { + if key_alg & !KEY_ALGORITHM_VALUE_MASK != 0 { + return Err(Errno::EINVAL); + } + + let algorithm = u8::try_from((key_alg & KEY_ALGORITHM_MASK) >> 8) + .ok() + .and_then(|value| KeyAlgorithm::try_from(value).ok()) + .ok_or(Errno::EINVAL)?; + let variant = u8::try_from(key_alg & KEY_VARIANT_MASK).map_err(|_| Errno::EINVAL)?; + if variant == 0 { + return Err(Errno::EINVAL); + } + + match algorithm { + KeyAlgorithm::Ecdsa => match EcdsaCurve::try_from(variant).map_err(|_| Errno::EINVAL)? { + EcdsaCurve::P384 => Ok(()), + EcdsaCurve::P256 | EcdsaCurve::P521 => Err(Errno::EOPNOTSUPP), + }, + KeyAlgorithm::Rsa | KeyAlgorithm::Pqc => Err(Errno::EOPNOTSUPP), + } +} + +fn get_identity_signing_key_pair() -> Result<&'static IdentitySigningKeyPair, Errno> { + IDENTITY_SIGNING_KEY_PAIR.try_call_once(|| { + let private_key = generate_identity_signing_private_key()?; + let public_key = identity_signing_public_key_from_private_key(&private_key)?; + Ok(IdentitySigningKeyPair { + private_key, + public_key, + }) + }) +} + +fn generate_identity_signing_private_key() +-> Result, Errno> { + let mut private_key_bytes = Zeroizing::new([0u8; IDENTITY_SIGNING_PRIVATE_KEY_LEN]); + + for _ in 0..MAX_KEYGEN_ATTEMPT { + LiteBox::new(litebox_platform_multiplex::platform()) + .fill_random(&mut private_key_bytes[..]) + .map_err(|_| Errno::EIO)?; + if is_valid_identity_signing_private_key(&private_key_bytes) { + return Ok(private_key_bytes); + } + } + + Err(Errno::EIO) +} + +#[inline] +fn is_valid_identity_signing_private_key( + private_key: &[u8; IDENTITY_SIGNING_PRIVATE_KEY_LEN], +) -> bool { + // P-384 private keys must be valid non-zero scalars smaller than the curve order. + NonZeroScalar::try_from(&private_key[..]).is_ok() +} + +fn identity_signing_public_key_from_private_key( + private_key: &[u8; IDENTITY_SIGNING_PRIVATE_KEY_LEN], +) -> Result<[u8; IDENTITY_SIGNING_PUBLIC_KEY_LEN], Errno> { + let private_key_scalar = + Zeroizing::new(NonZeroScalar::try_from(&private_key[..]).map_err(|_| Errno::EINVAL)?); + let public_key = p384::PublicKey::from_secret_scalar(&private_key_scalar); + let encoded_point = public_key.to_encoded_point(false); + let mut public_key_bytes = [0u8; IDENTITY_SIGNING_PUBLIC_KEY_LEN]; + public_key_bytes.copy_from_slice(encoded_point.as_bytes()); + Ok(public_key_bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identity_signing_private_key_signs_and_verifies_message() { + use crate::syscalls::tests::init_platform; + use p384::ecdsa::{ + Signature, SigningKey, VerifyingKey, + signature::{Signer, Verifier}, + }; + + let message = b"IDK_S signing test message"; + + let _task = init_platform(); + // XXX: Use a fixed valid scalar so this unit test only exercises signing and + // verification; key generation requires the broker-backed platform RNG. + let mut private_key = [0u8; IDENTITY_SIGNING_PRIVATE_KEY_LEN]; + private_key[IDENTITY_SIGNING_PRIVATE_KEY_LEN - 1] = 1; + assert!(is_valid_identity_signing_private_key(&private_key)); + let signing_key = SigningKey::from_slice(&private_key[..]).unwrap(); + let public_key = identity_signing_public_key_from_private_key(&private_key).unwrap(); + let verifying_key = VerifyingKey::from_sec1_bytes(&public_key).unwrap(); + + let signature: Signature = signing_key.sign(message); + + verifying_key.verify(message, &signature).unwrap(); + } +} diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index 6684e82dba..4088f4d910 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -36,6 +36,9 @@ pub(crate) mod syscalls; pub mod msg_handler; +#[cfg(feature = "platform_lvbs")] +pub mod idk; + // Re-export session management types for convenience pub use session::{OpenSessionTarget, SessionManager, SessionToken, TaInstance}; @@ -149,7 +152,7 @@ impl OpteeShimBuilder { boot_instant: TimeProvider::now(self.platform), pm: PageManager::new(&self.litebox), litebox: self.litebox, - ta_uuid_map: TaUuidMap::new(), + ta_uuid_map: ta_uuid_map(), pta_busy: spin::mutex::SpinMutex::new(HashSet::new()), }); OpteeShim(global) @@ -169,7 +172,7 @@ struct GlobalState { /// The LiteBox instance used throughout the shim. litebox: litebox::LiteBox, /// The TA UUID to binary map for TA loading. - ta_uuid_map: TaUuidMap, + ta_uuid_map: &'static TaUuidMap, /// Tracks which non-concurrent PTAs (i.e., PTAs w/o `TaFlags::CONCURRENT`) /// are currently busy. A busy PTA is *rejected* with `TeeResult::Busy` /// rather than queued. @@ -190,7 +193,7 @@ impl GlobalState { } /// Get the TA binary associated with the given TA UUID. - pub(crate) fn get_ta_bin(&self, ta_uuid: &TeeUuid) -> Option> { + pub(crate) fn get_ta_bin(&self, ta_uuid: &TeeUuid) -> Option> { if let Some(ta_bin) = self.ta_uuid_map.get(ta_uuid) { Some(ta_bin) } else { @@ -222,21 +225,20 @@ impl GlobalState { /// to avoid repeated RPCs and memory transfers. We remove it lazily if there is /// a memory pressure. /// - /// TODO: Use something like `Arc` to to ensure no active ldelf/TA holds a handle to - /// this TA binary #[expect(dead_code)] pub(crate) fn remove_ta_bin(&self, ta_uuid: &TeeUuid) { let _ = self.ta_uuid_map.remove(ta_uuid); } /// RPC to get the TA binary associated with the given TA UUID. Placeholder for now. - fn rpc_get_ta_bin(_ta_uuid: &TeeUuid) -> Option> { + fn rpc_get_ta_bin(_ta_uuid: &TeeUuid) -> Option> { None } } type UserMutPtr = ::RawMutPointer; pub type UserConstPtr = ::RawConstPointer; +pub type TaMemrefAddresses = [Option; litebox_common_optee::UteeParams::TEE_NUM_PARAMS]; type MutPtr = ::RawMutPointer; @@ -256,7 +258,6 @@ impl OpteeShim { &self, ldelf_bin: &[u8], ta_uuid: TeeUuid, - ta_bin: Option<&[u8]>, ) -> Result { let entrypoints = crate::OpteeShimEntrypoints { _not_send: core::marker::PhantomData, @@ -271,15 +272,11 @@ impl OpteeShim { ta_entry_point: Cell::new(0), ta_stack_base_addr: Cell::new(0), ta_prepared: Cell::new(false), + ta_trampoline_page_range: Cell::new(None), #[cfg(target_arch = "x86_64")] tls_base_addr: Cell::new(0), }, }; - if let Some(ta_bin) = ta_bin - && !entrypoints.task.global.store_ta_bin(&ta_uuid, ta_bin) - { - return Err(loader::elf::ElfLoaderError::InvalidUuid); - } let elf_loader = loader::elf::ElfLoader::new(&entrypoints.task, ldelf_bin, true)?; entrypoints.task.load_ldelf(elf_loader, ta_uuid)?; let params_address = if entrypoints.task.get_ta_stack_base_addr().is_some() { @@ -308,6 +305,19 @@ impl OpteeShim { &self.0.pm } + /// Store a TA binary associated with the given TA UUID. + /// + /// Returns `true` if the binary was successfully stored, `false` if the binary's + /// UUID (from `.ta_head` section) doesn't match the provided UUID or parsing failed. + pub fn store_ta_bin(&self, ta_uuid: &TeeUuid, ta_bin: &[u8]) -> bool { + self.0.store_ta_bin(ta_uuid, ta_bin) + } + + /// Get the TA binary associated with the given TA UUID. + pub fn get_ta_bin(&self, ta_uuid: &TeeUuid) -> Option> { + self.0.get_ta_bin(ta_uuid) + } + /// Release all user-space memory mappings owned by this shim instance. /// /// This must be called before switching to the base page table and deleting @@ -334,11 +344,30 @@ impl OpteeShimEntrypoints { func_id: u32, cmd_id: Option, ) -> Result<(), loader::elf::ElfLoaderError> { + self.load_ta_context_with_shm(params, &[], session_id, func_id, cmd_id) + .map(|_| ()) + } + + /// Load the TA context with shared-memory sources for its input buffers. + pub fn load_ta_context_with_shm( + &self, + params: &[litebox_common_optee::UteeParamOwned], + shm_info: &[Option>], + session_id: u32, + func_id: u32, + cmd_id: Option, + ) -> Result { let init_state = self .task - .load_ta_context(params, session_id, func_id, cmd_id)?; + .load_ta_context(params, shm_info, session_id, func_id, cmd_id)?; + let ThreadInitState::Ta { + memref_addresses, .. + } = init_state + else { + return Err(loader::elf::ElfLoaderError::InvalidStackAddr); + }; self.task.thread.init_state.set(init_state); - Ok(()) + Ok(memref_addresses) } } @@ -651,6 +680,7 @@ impl Task { func_id, entry_point, stack_top, + .. } => { #[cfg(target_arch = "x86_64")] { @@ -785,6 +815,7 @@ impl Task { fn load_ta_context( &self, params: &[litebox_common_optee::UteeParamOwned], + shm_info: &[Option>], session_id: u32, func_id: u32, cmd_id: Option, @@ -812,8 +843,8 @@ impl Task { )?; let mut stack_canary = [0; 16]; self.global.litebox.fill_random(&mut stack_canary)?; - ta_stack - .init(params, stack_canary) + let memref_addresses = ta_stack + .init(params, shm_info, stack_canary) .ok_or(ElfLoaderError::InvalidStackAddr)?; Ok(ThreadInitState::Ta { @@ -823,6 +854,7 @@ impl Task { func_id: func_id as usize, entry_point: self.get_ta_entry_point(), stack_top: ta_stack.get_cur_stack_top(), + memref_addresses, }) } @@ -1315,24 +1347,24 @@ impl TaHandleMap { /// Entry in the TA UUID map containing binary data and parsed flags. struct TaInfo { /// The raw TA binary - binary: alloc::boxed::Box<[u8]>, + binary: Arc<[u8]>, /// Parsed TA flags from .ta_head section flags: TaFlags, } /// Data structure to maintain a mapping from TA UUIDs to their binary data and flags. pub(crate) struct TaUuidMap { - inner: spin::mutex::SpinMutex>, + inner: spin::rwlock::RwLock>, } impl TaUuidMap { pub(crate) fn new() -> Self { Self { - inner: spin::mutex::SpinMutex::new(HashMap::new()), + inner: spin::rwlock::RwLock::new(HashMap::new()), } } - pub(crate) fn insert(&self, uuid: TeeUuid, ta_bin: alloc::boxed::Box<[u8]>) -> bool { + pub(crate) fn insert(&self, uuid: TeeUuid, ta_bin: Arc<[u8]>) -> bool { // Parse TA head from the binary's .ta_head section let Some(ta_head) = litebox_common_optee::parse_ta_head(&ta_bin) else { return false; @@ -1343,8 +1375,7 @@ impl TaUuidMap { return false; } - let mut inner = self.inner.lock(); - inner.insert( + let _replaced = self.inner.write().insert( uuid, TaInfo { binary: ta_bin, @@ -1354,21 +1385,27 @@ impl TaUuidMap { true } - pub(crate) fn get(&self, uuid: &TeeUuid) -> Option> { - self.inner.lock().get(uuid).map(|info| info.binary.clone()) + pub(crate) fn get(&self, uuid: &TeeUuid) -> Option> { + self.inner.read().get(uuid).map(|info| info.binary.clone()) } /// Get the TA flags for a given UUID. pub(crate) fn get_flags(&self, uuid: &TeeUuid) -> Option { - self.inner.lock().get(uuid).map(|info| info.flags) + self.inner.read().get(uuid).map(|info| info.flags) } // Lazy removal of TA binaries when they are no longer needed. - pub(crate) fn remove(&self, uuid: &TeeUuid) -> Option> { - self.inner.lock().remove(uuid).map(|info| info.binary) + pub(crate) fn remove(&self, uuid: &TeeUuid) -> Option> { + self.inner.write().remove(uuid).map(|info| info.binary) } } +/// Get the global TA UUID map. +fn ta_uuid_map() -> &'static TaUuidMap { + static TA_UUID_MAP: once_cell::race::OnceBox = once_cell::race::OnceBox::new(); + TA_UUID_MAP.get_or_init(|| alloc::boxed::Box::new(TaUuidMap::new())) +} + /// Per-instance TA state which can be shared between sessions if it is /// a single-instance multi-session TA. The active session id is carried /// per entry (see [`Task::current_session_id`]). @@ -1391,6 +1428,8 @@ struct Task { ta_stack_base_addr: Cell, /// Whether the TA has been prepared ta_prepared: Cell, + /// Pages left mapped for the TA's syscall trampoline, if any + ta_trampoline_page_range: Cell>, /// TLS base address for x86_64 (stored to restore FS before each TA entry) #[cfg(target_arch = "x86_64")] tls_base_addr: Cell, @@ -1435,6 +1474,7 @@ pub(crate) enum ThreadInitState { func_id: usize, entry_point: usize, stack_top: usize, + memref_addresses: TaMemrefAddresses, }, } @@ -1558,6 +1598,7 @@ mod test_utils { ta_entry_point: Cell::new(0), ta_stack_base_addr: Cell::new(0), ta_prepared: Cell::new(false), + ta_trampoline_page_range: Cell::new(None), #[cfg(target_arch = "x86_64")] tls_base_addr: Cell::new(0), } diff --git a/litebox_shim_optee/src/loader/elf.rs b/litebox_shim_optee/src/loader/elf.rs index 79e38c3bb2..5a70a7c548 100644 --- a/litebox_shim_optee/src/loader/elf.rs +++ b/litebox_shim_optee/src/loader/elf.rs @@ -28,7 +28,7 @@ use litebox_common_linux::{ errno::Errno, loader::{ElfParseError, ElfParsedFile}, }; -use litebox_common_optee::LdelfArg; +use litebox_common_optee::{LdelfArg, TeeUuid}; use thiserror::Error; /// An ELF file loaded in memory @@ -223,6 +223,26 @@ impl<'a> ElfLoader<'a> { Ok(Self { main, is_ldelf }) } + /// The pages the TA's trampoline occupies, relative to the TA's load + /// address, or `None` if the TA has no trampoline. + /// + /// Callers anchor the result at the address `ldelf` maps the first segment + /// at, whereas [`Self::load_ta_trampoline`] anchors at + /// `entry_point - e_entry`. The two agree as long as the first `PT_LOAD` + /// starts at vaddr 0, which `ldelf` assumes too. + pub(crate) fn ta_trampoline_relative_page_range( + task: &'a Task, + ta_uuid: &TeeUuid, + ) -> Result>, ElfLoaderError> { + let ta_bin = task + .global + .get_ta_bin(ta_uuid) + .ok_or(ElfLoaderError::OpenError(Errno::ENOENT))?; + // Constructing the loader only parses headers; it maps nothing. + let loader = Self::new(task, &ta_bin, false)?; + Ok(loader.main.parsed.trampoline_page_range(0)) + } + /// Load `ldelf` and prepare the stack and CPU context for it with the given TA UUID. pub fn load_ldelf(&mut self, ldelf_arg: &LdelfArg) -> Result { if !self.is_ldelf { diff --git a/litebox_shim_optee/src/loader/ta_stack.rs b/litebox_shim_optee/src/loader/ta_stack.rs index 54747d9d84..ed70c75e1b 100644 --- a/litebox_shim_optee/src/loader/ta_stack.rs +++ b/litebox_shim_optee/src/loader/ta_stack.rs @@ -10,7 +10,7 @@ use litebox::{ use litebox_common_optee::{LdelfArg, TeeParamType, UteeParamOwned, UteeParams}; use zerocopy::IntoBytes; -use crate::UserMutPtr; +use crate::{TaMemrefAddresses, UserMutPtr, msg_handler::ShmInfo}; #[inline] fn align_down(addr: usize, align: usize) -> usize { @@ -185,34 +185,41 @@ impl TaStack { param_type: TeeParamType, bytes: Option<&[u8]>, len: usize, - ) -> Option<()> { + ) -> Option { if self.num_params >= UteeParams::TEE_NUM_PARAMS { return None; } match param_type { TeeParamType::MemrefInput | TeeParamType::MemrefInout => { - let bytes = bytes?; + let bytes = bytes.unwrap_or(&[]); if len > bytes.len() { self.pos = self.pos.checked_sub(len - bytes.len())?; } self.push_bytes(bytes)?; - self.params - .set_values(self.num_params, self.get_cur_stack_top() as u64, len as u64) - .ok()?; - } - TeeParamType::MemrefOutput => { - self.pos = self.pos.checked_sub(len)?; - self.params - .set_values(self.num_params, self.get_cur_stack_top() as u64, len as u64) - .ok()?; - } - _ => { - return None; } + TeeParamType::MemrefOutput => self.pos = self.pos.checked_sub(len)?, + _ => return None, } + let address = self.get_cur_stack_top(); + self.params + .set_values(self.num_params, address as u64, len as u64) + .ok()?; self.params.set_type(self.num_params, param_type).ok()?; self.num_params += 1; - Some(()) + Some(address) + } + + fn push_param_memref_from_shm( + &mut self, + param_type: TeeParamType, + shm_info: &ShmInfo, + len: usize, + ) -> Option { + let address = self.push_param_memref(param_type, None, len)?; + shm_info + .copy_to_user(UserMutPtr::from_usize(address), len) + .ok()?; + Some(address) } /// Set `UteeParams` on the stack. @@ -223,14 +230,20 @@ impl TaStack { Some(()) } - pub(crate) fn init(&mut self, params: &[UteeParamOwned], stack_canary: [u8; 16]) -> Option<()> { + pub(crate) fn init( + &mut self, + params: &[UteeParamOwned], + shm_info: &[Option>], + stack_canary: [u8; 16], + ) -> Option { if params.len() > UteeParams::TEE_NUM_PARAMS { return None; } self.scrub()?; - for param in params { + let mut memref_addresses = [None; UteeParams::TEE_NUM_PARAMS]; + for (index, param) in params.iter().enumerate() { match param { UteeParamOwned::ValueInput { value_a, value_b } => { self.push_param_values(TeeParamType::ValueInput, Some((*value_a, *value_b)))?; @@ -242,13 +255,37 @@ impl TaStack { self.push_param_values(TeeParamType::ValueInout, Some((*value_a, *value_b)))?; } UteeParamOwned::MemrefInput { data } => { - self.push_param_memref(TeeParamType::MemrefInput, Some(data), data.len())?; + if let Some(shm_info) = shm_info.get(index).and_then(Option::as_ref) { + let len = shm_info.len(); + self.push_param_memref_from_shm(TeeParamType::MemrefInput, shm_info, len)?; + } else { + let data = data.as_deref()?; + self.push_param_memref(TeeParamType::MemrefInput, Some(data), data.len())?; + } } UteeParamOwned::MemrefInout { data, buffer_size } => { - self.push_param_memref(TeeParamType::MemrefInout, Some(data), *buffer_size)?; + let address = + if let Some(shm_info) = shm_info.get(index).and_then(Option::as_ref) { + self.push_param_memref_from_shm( + TeeParamType::MemrefInout, + shm_info, + *buffer_size, + )? + } else { + self.push_param_memref( + TeeParamType::MemrefInout, + Some(data.as_deref()?), + *buffer_size, + )? + }; + memref_addresses[index] = Some(address); } UteeParamOwned::MemrefOutput { buffer_size } => { - self.push_param_memref(TeeParamType::MemrefOutput, None, *buffer_size)?; + memref_addresses[index] = Some(self.push_param_memref( + TeeParamType::MemrefOutput, + None, + *buffer_size, + )?); } UteeParamOwned::None => self.push_param_none()?, } @@ -266,7 +303,7 @@ impl TaStack { self.pos % Self::STACK_ALIGNMENT, core::mem::size_of::() ); - Some(()) + Some(memref_addresses) } pub(crate) fn init_with_ldelf_arg(&mut self, ldelf_arg: &LdelfArg) -> Option<()> { diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index 3f73043b5b..b61672a9d6 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -54,11 +54,10 @@ const OPTEE_MSG_OS_OPTEE_UUID_3: u32 = 0xa5d5_c51b; // We do not support notification for now const MAX_NOTIF_VALUE: usize = 0; -/// Maximum secure-world heap copy for a single OP-TEE memref parameter. +/// Maximum TA buffer size for a single OP-TEE memref parameter. /// /// OP-TEE OS validates memref sizes against their backing shared-memory -/// objects, but it does not define a universal ABI maximum. OP-TEE shim -/// copies input/inout memrefs into owned buffers, so this is a local +/// objects, but it does not define a universal ABI maximum. This is a local /// resource policy to keep one normal-world request from consuming a large /// fraction of the default 128 MiB memory budget. /// @@ -384,8 +383,8 @@ pub fn handle_optee_msg_args(msg_args: &OpteeMsgArgs) -> Result<(), OpteeSmcRetu /// TA request information extracted from an OP-TEE message. /// /// In addition to standard TA information (i.e., TA UUID, session ID, command ID, -/// and parameters), it contains shared memory information (`out_shm_info`) to -/// write back output data to the normal world once the TA execution is done. +/// and parameters), it contains shared memory information (`shm_info`) to +/// transfer data between the normal world and the TA. pub struct TaRequestInfo { pub uuid: Option, pub client_identity: Option, @@ -393,14 +392,12 @@ pub struct TaRequestInfo { pub entry_func: UteeEntryFunc, pub cmd_id: u32, pub params: [UteeParamOwned; UteeParamOwned::TEE_NUM_PARAMS], - pub out_shm_info: [Option>; UteeParamOwned::TEE_NUM_PARAMS], + pub shm_info: [Option>; UteeParamOwned::TEE_NUM_PARAMS], } /// This function decodes a TA request contained in `OpteeMsgArgs`. /// -/// It copies the entire parameter data from the normal world shared memory into the secure world's -/// memory to create `UteeParamOwned` structures to avoid potential data corruption during TA -/// execution. +/// Memref payload copies are deferred until their TA buffers are allocated. pub fn decode_ta_request( msg_args: &OpteeMsgArgs, ) -> Result, OpteeSmcReturnCode> { @@ -468,7 +465,7 @@ pub fn decode_ta_request( entry_func: ta_entry_func, cmd_id: msg_args.func, params: [const { UteeParamOwned::None }; UteeParamOwned::TEE_NUM_PARAMS], - out_shm_info: [const { None }; UteeParamOwned::TEE_NUM_PARAMS], + shm_info: [const { None }; UteeParamOwned::TEE_NUM_PARAMS], }; if num_params @@ -511,20 +508,28 @@ pub fn decode_ta_request( let tmem = param.get_param_tmem().ok_or(OpteeSmcReturnCode::EBadCmd)?; let data_size = checked_memref_size(tmem.size)?; let shm_info = get_shm_info_from_optee_msg_param_tmem(tmem)?; - build_memref_input(&shm_info, data_size)? + if data_size != shm_info.len() { + return Err(OpteeSmcReturnCode::EBadAddr); + } + ta_req_info.shm_info[i] = Some(shm_info); + UteeParamOwned::MemrefInput { data: None } } OpteeMsgAttrType::RmemInput => { let rmem = param.get_param_rmem().ok_or(OpteeSmcReturnCode::EBadCmd)?; let data_size = checked_memref_size(rmem.size)?; let shm_info = get_shm_info_from_optee_msg_param_rmem(rmem)?; - build_memref_input(&shm_info, data_size)? + if data_size != shm_info.len() { + return Err(OpteeSmcReturnCode::EBadAddr); + } + ta_req_info.shm_info[i] = Some(shm_info); + UteeParamOwned::MemrefInput { data: None } } OpteeMsgAttrType::TmemOutput => { let tmem = param.get_param_tmem().ok_or(OpteeSmcReturnCode::EBadCmd)?; let buffer_size = checked_memref_size(tmem.size)?; let shm_info = get_shm_info_from_optee_msg_param_tmem(tmem)?; - ta_req_info.out_shm_info[i] = Some(shm_info); + ta_req_info.shm_info[i] = Some(shm_info); UteeParamOwned::MemrefOutput { buffer_size } } OpteeMsgAttrType::RmemOutput => { @@ -532,7 +537,7 @@ pub fn decode_ta_request( let buffer_size = checked_memref_size(rmem.size)?; let shm_info = get_shm_info_from_optee_msg_param_rmem(rmem)?; - ta_req_info.out_shm_info[i] = Some(shm_info); + ta_req_info.shm_info[i] = Some(shm_info); UteeParamOwned::MemrefOutput { buffer_size } } OpteeMsgAttrType::TmemInout => { @@ -540,16 +545,22 @@ pub fn decode_ta_request( let buffer_size = checked_memref_size(tmem.size)?; let shm_info = get_shm_info_from_optee_msg_param_tmem(tmem)?; - ta_req_info.out_shm_info[i] = Some(shm_info.clone()); - build_memref_inout(&shm_info, buffer_size)? + ta_req_info.shm_info[i] = Some(shm_info); + UteeParamOwned::MemrefInout { + data: None, + buffer_size, + } } OpteeMsgAttrType::RmemInout => { let rmem = param.get_param_rmem().ok_or(OpteeSmcReturnCode::EBadCmd)?; let buffer_size = checked_memref_size(rmem.size)?; let shm_info = get_shm_info_from_optee_msg_param_rmem(rmem)?; - ta_req_info.out_shm_info[i] = Some(shm_info.clone()); - build_memref_inout(&shm_info, buffer_size)? + ta_req_info.shm_info[i] = Some(shm_info); + UteeParamOwned::MemrefInout { + data: None, + buffer_size, + } } _ => return Err(OpteeSmcReturnCode::EBadCmd), }; @@ -558,29 +569,6 @@ pub fn decode_ta_request( Ok(ta_req_info) } -#[inline] -fn build_memref_input( - shm_info: &ShmInfo, - data_size: usize, -) -> Result { - let mut data = alloc::vec![0u8; data_size]; - shm_info.read_at(0, &mut data)?; - Ok(UteeParamOwned::MemrefInput { data: data.into() }) -} - -#[inline] -fn build_memref_inout( - shm_info: &ShmInfo, - buffer_size: usize, -) -> Result { - let mut buffer = alloc::vec![0u8; buffer_size]; - shm_info.read_at(0, &mut buffer)?; - Ok(UteeParamOwned::MemrefInout { - data: buffer.into(), - buffer_size, - }) -} - /// This function updates the OP-TEE message arguments for returning from the secure world to the normal world. /// /// It writes back TA execution outputs associated with shared memory references and updates @@ -596,6 +584,7 @@ pub fn update_optee_msg_args( session_id: Option, ta_params: Option<&UteeParams>, ta_req_info: Option<&TaRequestInfo>, + memref_addresses: Option<&crate::TaMemrefAddresses>, msg_args: &mut OpteeMsgArgs, ) -> Result<(), OpteeSmcReturnCode> { msg_args.ret = return_code; @@ -610,7 +599,13 @@ pub fn update_optee_msg_args( let Some(ta_req_info) = ta_req_info else { return Ok(()); }; + let wire_param_offset = if ta_req_info.entry_func == UteeEntryFunc::OpenSession { + 2 + } else { + 0 + }; for index in 0..UteeParams::TEE_NUM_PARAMS { + let wire_index = index + wire_param_offset; let param_type = ta_params .get_type(index) .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; @@ -618,7 +613,7 @@ pub fn update_optee_msg_args( TeeParamType::ValueOutput | TeeParamType::ValueInout => { if let Ok(Some((value_a, value_b))) = ta_params.get_values(index) { msg_args.set_param_value( - index, + wire_index, OpteeMsgParamValue { a: value_a, b: value_b, @@ -628,33 +623,32 @@ pub fn update_optee_msg_args( } } TeeParamType::MemrefOutput | TeeParamType::MemrefInout => { - if let Ok(Some((addr, len))) = ta_params.get_values(index) { + if let Ok(Some((_addr, len))) = ta_params.get_values(index) { let len = checked_memref_size(len)?; - let Some(out_shm_info) = &ta_req_info.out_shm_info[index] else { + if !matches!( + &ta_req_info.params[index], + UteeParamOwned::MemrefOutput { .. } | UteeParamOwned::MemrefInout { .. } + ) { + continue; + } + let Some(shm_info) = &ta_req_info.shm_info[index] else { continue; }; - if len > out_shm_info.len() { + if len > shm_info.len() { if return_code != TeeResult::ShortBuffer { return Err(OpteeSmcReturnCode::EBadAddr); } // For short-buffer returns, report the required size without copying data. - msg_args.set_param_memref_size(index, len as u64)?; + msg_args.set_param_memref_size(wire_index, len as u64)?; continue; } // Update the output size in msg_args before attempting any copy-out. - msg_args.set_param_memref_size(index, len as u64)?; - // SAFETY - // `addr` is expected to be a valid address of a TA and `addr + len` does not - // exceed the TA's memory region. - let ptr = crate::UserConstPtr::::from_usize(addr.trunc()); - let slice = ptr - .to_owned_slice(len) + msg_args.set_param_memref_size(wire_index, len as u64)?; + let address = memref_addresses + .and_then(|addresses| addresses[index]) .ok_or(OpteeSmcReturnCode::EBadAddr)?; - - if slice.is_empty() { - continue; - } - out_shm_info.write(slice.as_ref())?; + let ptr = crate::UserConstPtr::::from_usize(address); + shm_info.copy_from_user(ptr, len)?; } } _ => {} @@ -718,7 +712,7 @@ impl ShmInfo { }) } - fn len(&self) -> usize { + pub(crate) fn len(&self) -> usize { self.len } @@ -732,20 +726,45 @@ impl ShmInfo { { return Err(OpteeSmcReturnCode::EBadAddr); } + if buffer.is_empty() { + return Ok(()); + } let ptr = NormalWorldConstPtr::::new(&self.page_addrs, self.page_offset)?; ptr.read_slice_at_offset(offset, buffer)?; Ok(()) } - /// Write `buffer` to the normal-world shared memory pages referenced by `self`, - /// starting at the beginning of the view. - /// Returns `EBadAddr` if `buffer` does not fit within the view. - fn write(&self, buffer: &[u8]) -> Result<(), OpteeSmcReturnCode> { - if buffer.len() > self.len { + /// Copy from this normal-world shared memory into TA userspace. + pub(crate) fn copy_to_user( + &self, + dst: crate::UserMutPtr, + len: usize, + ) -> Result<(), OpteeSmcReturnCode> { + if len > self.len { return Err(OpteeSmcReturnCode::EBadAddr); } + if len == 0 { + return Ok(()); + } + let ptr = NormalWorldConstPtr::::new(&self.page_addrs, self.page_offset)?; + ptr.copy_to_user(dst, len)?; + Ok(()) + } + + /// Copy from TA userspace into this normal-world shared memory. + fn copy_from_user( + &self, + src: crate::UserConstPtr, + len: usize, + ) -> Result<(), OpteeSmcReturnCode> { + if len > self.len { + return Err(OpteeSmcReturnCode::EBadAddr); + } + if len == 0 { + return Ok(()); + } let ptr = NormalWorldMutPtr::::new(&self.page_addrs, self.page_offset)?; - ptr.write_slice_at_offset(0, buffer)?; + ptr.copy_from_user(src, len)?; Ok(()) } } @@ -923,6 +942,9 @@ fn get_shm_info_from_optee_msg_param_rmem( if view_end > shm_info.len() { return Err(OpteeSmcReturnCode::EBadAddr); } + if rmem.size == 0 { + return ShmInfo::new(Box::new([]), 0, 0); + } let start = page_offset .checked_add(rmem_offs) .ok_or(OpteeSmcReturnCode::EBadAddr)?; diff --git a/litebox_shim_optee/src/session.rs b/litebox_shim_optee/src/session.rs index 17fdc4edeb..d63ab0aa0d 100644 --- a/litebox_shim_optee/src/session.rs +++ b/litebox_shim_optee/src/session.rs @@ -449,6 +449,8 @@ pub struct SessionManager { /// pass the limit before either registers. pending_count: SpinMutex, /// Cached TA flags by UUID, populated on first successful session registration. + /// New entries are published under `ta_load_lock` and are never updated + /// or removed; the post-acquire recheck relies on this. /// /// TODO: a TA's flags (in particular single- vs multi-instance) can /// change across a version update of the same UUID. Key this map by @@ -607,11 +609,8 @@ impl SessionManager { /// /// - **Known single-instance**: per-UUID lock flag. /// - **Known multi-instance**: no lock (each session is independent). - /// - **Unknown**: the global `ta_load_lock`. This serializes first-loads - /// of all not-yet-known UUIDs together, but avoids minting a per-UUID - /// lock entry until the TA has been confirmed single-instance. A failed - /// or multi-instance load therefore leaves no stale entry in - /// `single_instance_locks`. + /// - **Unknown**: get the global `ta_load_lock`, recheck flags, and transition + /// to the per-UUID lock or no lock if the flags is published. /// /// Returns `Err(EThreadLimit)` on contention. fn try_acquire_for_open(&self, uuid: TeeUuid) -> Result, OpteeSmcReturnCode> { @@ -621,10 +620,26 @@ impl SessionManager { .ok_or(OpteeSmcReturnCode::EThreadLimit)?, ), Some(_) => None, - None => Some( - self.try_acquire_ta_load_lock() - .ok_or(OpteeSmcReturnCode::EThreadLimit)?, - ), + None => { + let load_lock = self + .try_acquire_ta_load_lock() + .ok_or(OpteeSmcReturnCode::EThreadLimit)?; + + // Another concurrent opener might have loaded this TA and published flags. + // Recheck to change the lock domain. + if let Some(flags) = self.get_known_flags(&uuid) { + if flags.is_single_instance() { + let uuid_lock = self.try_acquire_uuid_lock(uuid); + self.release_uuid_lock(load_lock); + Some(uuid_lock.ok_or(OpteeSmcReturnCode::EThreadLimit)?) + } else { + self.release_uuid_lock(load_lock); + None + } + } else { + Some(load_lock) + } + } }; Ok(SessionToken { manager: self, diff --git a/litebox_shim_optee/src/syscalls/ldelf.rs b/litebox_shim_optee/src/syscalls/ldelf.rs index bcc3f7b5f0..5f46d67c90 100644 --- a/litebox_shim_optee/src/syscalls/ldelf.rs +++ b/litebox_shim_optee/src/syscalls/ldelf.rs @@ -2,9 +2,10 @@ // Licensed under the MIT license. use crate::syscalls::Cleanup; -use crate::{Task, UserMutPtr}; +use crate::{Platform, Task, UserMutPtr}; use litebox::mm::linux::PAGE_SIZE; -use litebox::platform::{RawConstPointer, RawMutPointer, SystemInfoProvider as _}; +use litebox::platform::page_mgmt::PageManagementProvider; +use litebox::platform::{RawConstPointer, RawMutPointer}; use litebox_common_linux::{MapFlags, ProtFlags}; use litebox_common_optee::{LdelfMapFlags, TeeResult, TeeUuid}; @@ -45,7 +46,7 @@ impl Drop for MmapGuard<'_> { impl Task { #[inline] - fn checked_map_size( + fn checked_map_len( num_bytes: usize, pad_begin: usize, pad_end: usize, @@ -59,26 +60,98 @@ impl Task { #[inline] fn get_aligned_start_of_pad_end( - padded_start: usize, + usable_start_addr: usize, num_bytes: usize, ) -> Result { - padded_start + usable_start_addr .checked_add(num_bytes) .and_then(|end| end.checked_next_multiple_of(PAGE_SIZE)) .ok_or(TeeResult::BadParameters) } + /// Check that the `pad_begin` bytes below `usable_start_addr` and the + /// `pad_end` bytes above `usable_start_addr + segment_len` are free, as + /// OP-TEE's `select_va_in_range` does for a caller-named address. + /// + /// Mapping the segment alone would only validate `ROUNDUP(num_bytes)`. The + /// TA's trampoline pages are excluded, since OP-TEE believes that address + /// space is unmapped. + /// + /// Under the userland runner, LiteBox's own mappings (its binary, libc, the + /// heap) share this address space; they are tracked as of start-up, but not + /// what it allocates afterwards. + fn ensure_pads_are_unmapped( + &self, + usable_start_addr: usize, + segment_len: usize, + pad_begin: usize, + pad_end: usize, + ) -> Result<(), TeeResult> { + if pad_begin == 0 && pad_end == 0 { + return Ok(()); + } + let usable_end_addr = usable_start_addr + .checked_add(segment_len) + .ok_or(TeeResult::BadParameters)?; + // Either gap can be empty, which `RangeSet::insert` rejects. + let mut pads = rangemap::RangeSet::new(); + if pad_begin != 0 { + pads.insert( + usable_start_addr + .checked_sub(pad_begin) + .ok_or(TeeResult::BadParameters)?..usable_start_addr, + ); + } + if pad_end != 0 { + pads.insert( + usable_end_addr + ..usable_end_addr + .checked_add(pad_end) + .ok_or(TeeResult::BadParameters)?, + ); + } + // The gaps must fall inside the task's address range; padding that runs + // past either end is an access conflict rather than a fit. + if pads.iter().any(|pad| { + pad.start < >::TASK_ADDR_MIN + || pad.end > >::TASK_ADDR_MAX + }) { + return Err(TeeResult::AccessConflict); + } + // Cut the trampoline pages out of the gaps rather than skipping the + // mappings inside them: `RangeMap` coalesces adjacent ranges that share + // flags, so they are routinely merged into a segment's VMA. + if let Some((start, end)) = self.ta_trampoline_page_range.get() { + pads.remove(start..end); + } + + if self + .global + .pm + .mappings() + .iter() + .any(|(range, _flags)| pads.overlaps(range)) + { + return Err(TeeResult::AccessConflict); + } + Ok(()) + } + /// OP-TEE's syscall to map zero-initialized memory with padding. /// - /// Maps `pad_begin + num_bytes + pad_end` bytes (rounded up to a page) and - /// zero-initializes the `num_bytes` usable region. `va` is a page-aligned - /// hint for the *base of the whole mapping* (`0` means no hint). The usable - /// region thus starts at `start = va + pad_begin`; the `pad_begin`/`pad_end` - /// regions are reserved and must not be accessed. + /// `va` is either `0` (OP-TEE picks the address) or a fixed address. Padding + /// only steers that choice: OP-TEE maps and records just + /// `ROUNDUP(num_bytes)` and leaves the padding unmapped (see `vm_map_pad()` + /// in `core/mm/vm.c`). /// - /// On success, returns `start` plus a `Cleanup` that unmaps the usable - /// region. The caller communicates the address back to userspace and must - /// run the cleanup if that write-back fails. + /// The usable region is `num_bytes` long and zero-initialized. With `va == + /// 0` it starts `pad_begin` bytes into the span OP-TEE picked; with a fixed + /// `va` it starts at `va` itself and `pad_begin` merely demands that many + /// free bytes below it, matching `vm_map_pad`'s in/out `va`. + /// + /// On success, returns that usable start address plus a [`Cleanup`] that + /// unmaps the usable region. The caller communicates the address back to + /// userspace and must run the cleanup if that write-back fails. pub fn sys_map_zi( &self, va: usize, @@ -91,6 +164,8 @@ impl Task { litebox_util_log::debug!( va:% = format_args!("{:#x}", va), num_bytes:% = num_bytes, + pad_begin:% = pad_begin, + pad_end:% = pad_end, flags:% = format_args!("{:#x}", flags); "sys_map_zi" ); @@ -109,54 +184,63 @@ impl Task { return Err(TeeResult::AccessConflict); } - let total_size = Self::checked_map_size(num_bytes, pad_begin, pad_end)?; - if va.checked_add(total_size).is_none() { + let padded_len = Self::checked_map_len(num_bytes, pad_begin, pad_end)?; + if va.checked_add(padded_len).is_none() { return Err(TeeResult::BadParameters); } + let segment_len = Self::checked_map_len(num_bytes, 0, 0)?; + // `sys_map_zi` always creates read/writeable mapping. // - // We map with PROT_READ_WRITE first, then mprotect padding regions to PROT_NONE. + // With `va == 0`, map the padded span so LiteBox's allocator picks a gap + // wide enough for it, mirroring `select_va_in_range`, then unmap the + // padding. With a fixed `va` there is no placement to influence, so map + // only the segment and check the gaps instead. let mut flags = MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS; - if va != 0 { - flags |= MapFlags::MAP_FIXED; - } + let (map_len, map_pad_begin_len) = if va == 0 { + (padded_len, pad_begin) + } else { + self.ensure_pads_are_unmapped(va, segment_len, pad_begin, pad_end)?; + flags |= MapFlags::MAP_FIXED_NOREPLACE; + (segment_len, 0) + }; let addr = self - .sys_mmap(va, total_size, ProtFlags::PROT_READ_WRITE, flags, -1, 0) - .map_err(|_| TeeResult::OutOfMemory)?; - let guard = MmapGuard::new(self, addr, total_size); + .sys_mmap(va, map_len, ProtFlags::PROT_READ_WRITE, flags, -1, 0) + .map_err(TeeResult::from)?; + let guard = MmapGuard::new(self, addr, map_len); - let padded_start = addr + let usable_start_addr = addr .as_usize() - .checked_add(pad_begin) + .checked_add(map_pad_begin_len) .ok_or(TeeResult::BadParameters)?; // Unmap the padding regions to free physical memory. // Using munmap instead of mprotect(PROT_NONE) actually deallocates the frames. - // pad_begin region: [addr, align_down(padded_start, PAGE_SIZE)) - let pad_begin_end = align_down(padded_start, PAGE_SIZE); - if addr.as_usize() < pad_begin_end { - let _ = self.sys_munmap(addr, pad_begin_end - addr.as_usize()); + // pad_begin region: [addr, align_down(usable_start_addr, PAGE_SIZE)) + let pad_begin_end_addr = align_down(usable_start_addr, PAGE_SIZE); + if addr.as_usize() < pad_begin_end_addr { + let _ = self.sys_munmap(addr, pad_begin_end_addr - addr.as_usize()); } - // pad_end region: [align_up(padded_start + num_bytes, PAGE_SIZE), addr + total_size) - let pad_end_start = Self::get_aligned_start_of_pad_end(padded_start, num_bytes)?; - let region_end = addr + // pad_end region: [align_up(usable_start_addr + num_bytes, PAGE_SIZE), addr + map_len) + let pad_end_start_addr = Self::get_aligned_start_of_pad_end(usable_start_addr, num_bytes)?; + let map_end_addr = addr .as_usize() - .checked_add(total_size) + .checked_add(map_len) .ok_or(TeeResult::BadParameters)?; - if pad_end_start < region_end { + if pad_end_start_addr < map_end_addr { let _ = self.sys_munmap( - UserMutPtr::from_usize(pad_end_start), - region_end - pad_end_start, + UserMutPtr::from_usize(pad_end_start_addr), + map_end_addr - pad_end_start_addr, ); } guard.disarm(); let cleanup = Cleanup::Unmap { - addr: padded_start, - len: pad_end_start - padded_start, + addr: usable_start_addr, + len: pad_end_start_addr - usable_start_addr, }; - Ok((padded_start, cleanup)) + Ok((usable_start_addr, cleanup)) } /// OP-TEE's syscall to open a TA binary. @@ -249,69 +333,98 @@ impl Task { return Err(TeeResult::BadParameters); } - let total_size = Self::checked_map_size(num_bytes, pad_begin, pad_end)?; - if addr.checked_add(total_size).is_none() { + let padded_len = Self::checked_map_len(num_bytes, pad_begin, pad_end)?; + if addr.checked_add(padded_len).is_none() { return Err(TeeResult::BadParameters); } + let segment_len = Self::checked_map_len(num_bytes, 0, 0)?; + // We map with PROT_READ_WRITE first, then mprotect padding regions to PROT_NONE as // explained in `sys_map_zi`. let mut flags_internal = MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS; - if addr != 0 { - flags_internal |= MapFlags::MAP_FIXED; - } // TODO: on Arm, check whether flags contains `LDELF_MAP_FLAG_SHAREABLE` to control cache behaviors - // Avoiding TA trampoline address conflict based on heuristics. - // Grow the underlying mmap by one page but keep trimming based on - // the original total_size so the extra page survives unseen by - // ldelf. ldelf reserves the address space for TA ELF via the main - // `sys_map_bin` call: addr=0 (PM picks the base), at least one of - // pad_begin/pad_end > 0 (reservation room around the first - // segment; ASLR-enabled builds put it in pad_begin, ASLR-disabled - // may put it entirely in pad_end), and LDELF_MAP_FLAG_EXECUTABLE - // (the first segment is .text). Skip on kernel-mode platforms - // which don't use a syscall trampoline. - // - // TODO: consider a reliable solution. - let should_extend_ta_reservation = addr == 0 - && (pad_begin > 0 || pad_end > 0) - && flags.contains(LdelfMapFlags::LDELF_MAP_FLAG_EXECUTABLE) - && self.global.platform.get_syscall_entry_point() != 0; - let mmap_size = if should_extend_ta_reservation { - // The size of OP-TEE TA trampoline is 0x3f8, so one page is enough. - total_size - .checked_add(PAGE_SIZE) - .ok_or(TeeResult::OutOfMemory)? + // `pad_begin` is the ASLR offset `ldelf` puts before the image; `pad_end` + // covers the segments that follow. Neither is mapped, so every segment's + // `pad_end` overlaps the rest of the image and only the last has none. + let (map_len, map_pad_begin_len, trampoline_relative_page_range) = if addr == 0 { + // The call that establishes the load address is the one that must + // also keep the trampoline pages: it has padding and an executable + // segment, unlike the bare one-page map `ldelf` makes to read the + // ELF header. Only the first such call counts, so that the pages + // already kept stay tracked. + // + // TODO: consider a reliable solution. + let trampoline_page_range = if (pad_begin > 0 || pad_end > 0) + && flags.contains(LdelfMapFlags::LDELF_MAP_FLAG_EXECUTABLE) + && self.ta_trampoline_page_range.get().is_none() + { + let ta_uuid = self + .ta_handle_map + .get(handle) + .ok_or(TeeResult::BadParameters)?; + // Fail here rather than let `ldelf` allocate over the + // trampoline and report something unrelated later. + crate::loader::elf::ElfLoader::ta_trampoline_relative_page_range(self, &ta_uuid) + .map_err(|_| TeeResult::BadFormat)? + } else { + None + }; + (padded_len, pad_begin, trampoline_page_range) } else { - total_size + // `NOREPLACE` so a segment cannot silently replace an existing + // mapping: the padding is unmapped, so nothing guards the span. + // + // The trampoline pages are cut out of the padding checks but not + // out of this map, so a segment overlapping them fails here where + // OP-TEE would succeed. `ldelf` never gets that far: they start at + // `roundup(max p_vaddr + p_memsz)`, exactly where the last segment + // ends. A TA naming such an address via `PTA_SYSTEM_MAP_ZI` would be + // mapping over its own trampoline, so refuse this. + self.ensure_pads_are_unmapped(addr, segment_len, pad_begin, pad_end)?; + flags_internal |= MapFlags::MAP_FIXED_NOREPLACE; + (segment_len, 0, None) + }; + // `map_len` is the span `ldelf` knows about; `alloc_len` is what we + // actually request. The trampoline sits past the image, so it falls + // inside `pad_end` or just beyond: widen the request to cover it, so the + // allocator finds a gap that fits both. The trim below releases the + // padding but leaves the trampoline pages mapped, so they stay free + // until `load_ta_context` loads the trampoline into them. + let alloc_len = match &trampoline_relative_page_range { + Some(range) => map_len.max( + pad_begin + .checked_add(range.end) + .ok_or(TeeResult::OutOfMemory)?, + ), + None => map_len, }; - // Currently, we do not support TA binary mapping. So, we create an anonymous mapping and copy // the content of the TA binary into it. - let addr = self + let map_base_addr = self .sys_mmap( addr, - mmap_size, + alloc_len, ProtFlags::PROT_READ_WRITE, flags_internal, -1, 0, ) - .map_err(|_| TeeResult::OutOfMemory)?; - let guard = MmapGuard::new(self, addr, mmap_size); + .map_err(TeeResult::from)?; + let guard = MmapGuard::new(self, map_base_addr, alloc_len); - let padded_start = addr + let usable_start_addr = map_base_addr .as_usize() - .checked_add(pad_begin) + .checked_add(map_pad_begin_len) .ok_or(TeeResult::BadParameters)?; - if padded_start == 0 { + if usable_start_addr == 0 { return Err(TeeResult::BadFormat); } if self .read_ta_bin( handle, - UserMutPtr::from_usize(padded_start), + UserMutPtr::from_usize(usable_start_addr), offs, num_bytes, ) @@ -327,14 +440,14 @@ impl Task { } else if flags.contains(LdelfMapFlags::LDELF_MAP_FLAG_EXECUTABLE) { prot |= ProtFlags::PROT_EXEC; } - let prot_start = align_down(padded_start, PAGE_SIZE); - let prot_len = padded_start - .checked_sub(prot_start) + let prot_start_addr = align_down(usable_start_addr, PAGE_SIZE); + let prot_len = usable_start_addr + .checked_sub(prot_start_addr) .and_then(|offset| offset.checked_add(num_bytes)) .and_then(|len| len.checked_next_multiple_of(PAGE_SIZE)) .ok_or(TeeResult::BadParameters)?; if self - .sys_mprotect(UserMutPtr::from_usize(prot_start), prot_len, prot) + .sys_mprotect(UserMutPtr::from_usize(prot_start_addr), prot_len, prot) .is_err() { return Err(TeeResult::AccessDenied); @@ -342,26 +455,48 @@ impl Task { // Unmap the padding regions to free physical memory. // Using munmap instead of mprotect(PROT_NONE) actually deallocates the frames. - // pad_begin region: [addr, align_down(padded_start, PAGE_SIZE)) - let pad_begin_end = align_down(padded_start, PAGE_SIZE); - if addr.as_usize() < pad_begin_end { - let _ = self.sys_munmap(addr, pad_begin_end - addr.as_usize()); + // pad_begin region: [map_base_addr, align_down(usable_start_addr, PAGE_SIZE)) + let pad_begin_end_addr = align_down(usable_start_addr, PAGE_SIZE); + if map_base_addr.as_usize() < pad_begin_end_addr { + let _ = self.sys_munmap(map_base_addr, pad_begin_end_addr - map_base_addr.as_usize()); } - // pad_end region: [align_up(padded_start + num_bytes, PAGE_SIZE), addr + total_size) - let pad_end_start = Self::get_aligned_start_of_pad_end(padded_start, num_bytes)?; - let region_end = addr + // pad_end region: [align_up(usable_start_addr + num_bytes, PAGE_SIZE), map_base_addr + alloc_len), + // except the trampoline pages, which stay mapped. + let pad_end_start_addr = Self::get_aligned_start_of_pad_end(usable_start_addr, num_bytes)?; + let alloc_end_addr = map_base_addr .as_usize() - .checked_add(total_size) + .checked_add(alloc_len) .ok_or(TeeResult::BadParameters)?; - if pad_end_start < region_end { - let _ = self.sys_munmap( - UserMutPtr::from_usize(pad_end_start), - region_end - pad_end_start, - ); + let trampoline_page_range = match trampoline_relative_page_range { + Some(range) => Some( + usable_start_addr + .checked_add(range.start) + .ok_or(TeeResult::BadParameters)? + ..usable_start_addr + .checked_add(range.end) + .ok_or(TeeResult::BadParameters)?, + ), + None => None, + }; + if pad_end_start_addr < alloc_end_addr { + let mut to_release = rangemap::RangeSet::new(); + to_release.insert(pad_end_start_addr..alloc_end_addr); + if let Some(range) = &trampoline_page_range { + to_release.remove(range.clone()); + } + for range in to_release.iter() { + let _ = + self.sys_munmap(UserMutPtr::from_usize(range.start), range.end - range.start); + } } - let _ = va.write_at_offset(0, padded_start); + let _ = va.write_at_offset(0, usable_start_addr); guard.disarm(); + // Record the trampoline pages so the padding checks treat them as unmapped. + if let Some(range) = trampoline_page_range { + self.ta_trampoline_page_range + .set(Some((range.start, range.end))); + } Ok(()) } diff --git a/litebox_shim_optee/src/syscalls/mm.rs b/litebox_shim_optee/src/syscalls/mm.rs index 2263e68c6b..baacbefc70 100644 --- a/litebox_shim_optee/src/syscalls/mm.rs +++ b/litebox_shim_optee/src/syscalls/mm.rs @@ -3,11 +3,15 @@ //! Implementation of memory management related syscalls, eg., `mmap`, `munmap`, etc. -use litebox::mm::linux::{MappingError, PAGE_SIZE}; +use litebox::mm::linux::{MappingError, PAGE_SIZE, VmFlags}; +use litebox::platform::page_mgmt::PageManagementProvider; use litebox_common_linux::{MapFlags, ProtFlags, errno::Errno, user_pointers::UserPtrMut}; use crate::{Platform, Task, UserMutPtr}; +// Keep bottom-up placement consistent with LiteBox's private Vmem stack policy. +const STACK_GUARD_GAP: usize = 256 << 12; + #[inline] fn align_up(addr: usize, align: usize) -> Option { debug_assert!(align.is_power_of_two()); @@ -15,6 +19,39 @@ fn align_up(addr: usize, align: usize) -> Option { } impl Task { + /// Finds the first address-space gap from low to high. + /// + /// OP-TEE chooses user VAs bottom-up. Matching that order matters because + /// `ldelf`'s sequential segment allocations with padding rely on it, while + /// LiteBox's `get_unmmaped_area` searches for free VAs top-down by default. + fn find_bottom_up_gap(&self, len: usize) -> Option { + debug_assert!(len.is_multiple_of(PAGE_SIZE)); + let task_addr_min = >::TASK_ADDR_MIN; + let task_addr_max = >::TASK_ADDR_MAX; + let mut candidate = task_addr_min..task_addr_min.checked_add(len)?; + if candidate.end > task_addr_max { + return None; + } + // `PageManager::mappings()` returns mappings ordered by ascending start address. + for (range, flags) in self.global.pm.mappings() { + let protected_range = if flags.contains(VmFlags::VM_GROWSDOWN) { + range.start.saturating_sub(STACK_GUARD_GAP << 1) + } else { + range.start + }..range.end; + if candidate.end <= protected_range.start { + return Some(candidate.start); + } + if candidate.start < protected_range.end { + candidate = protected_range.end..protected_range.end.checked_add(len)?; + if candidate.end > task_addr_max { + return None; + } + } + } + Some(candidate.start) + } + #[inline] fn do_mmap_anonymous( &self, @@ -75,7 +112,28 @@ impl Task { return Err(Errno::EOVERFLOW); } - let suggested_addr = if addr == 0 { None } else { Some(addr) }; + let (suggested_addr, flags) = if addr == 0 + && !flags.intersects(MapFlags::MAP_FIXED | MapFlags::MAP_FIXED_NOREPLACE) + { + debug_assert_ne!( + >::TASK_ADDR_MIN, + 0, + "sys_mmap treats address zero as no hint" + ); + // The mapping snapshot and fixed-address claim are separate operations. + // Since OP-TEE OS doesn't support multithreading and we serialize each TA + // instance's execution, this address space cannot change between them. + // We can use a bounded retry loop for the search and claim on EEXIST + // if we need to consider multithreaded TAs in the future. + ( + Some(self.find_bottom_up_gap(aligned_len).ok_or(Errno::ENOMEM)?), + flags | MapFlags::MAP_FIXED_NOREPLACE, + ) + } else if addr == 0 { + (None, flags) + } else { + (Some(addr), flags) + }; let result = if flags.contains(MapFlags::MAP_ANONYMOUS) { self.do_mmap_anonymous(suggested_addr, aligned_len, prot, flags) } else { diff --git a/litebox_shim_optee/src/syscalls/tests.rs b/litebox_shim_optee/src/syscalls/tests.rs index 694f6bd76a..4145799f29 100644 --- a/litebox_shim_optee/src/syscalls/tests.rs +++ b/litebox_shim_optee/src/syscalls/tests.rs @@ -70,3 +70,27 @@ fn test_sys_get_time_system_is_monotonic() { let second_ms = u64::from(second.seconds) * 1000 + u64::from(second.millis); assert!(second_ms >= first_ms, "system time went backwards"); } + +#[test] +fn test_sys_map_zi_uses_bottom_up_placement() { + use litebox::mm::linux::PAGE_SIZE; + use litebox_common_optee::LdelfMapFlags; + + let task = init_platform(); + let (header, header_cleanup) = task + .sys_map_zi(0, PAGE_SIZE, 0, 0, LdelfMapFlags::empty()) + .expect("header mapping should succeed"); + let (image, image_cleanup) = task + .sys_map_zi( + 0, + PAGE_SIZE, + PAGE_SIZE, + 2 * PAGE_SIZE, + LdelfMapFlags::empty(), + ) + .expect("padded image mapping should succeed"); + + assert!(header < image, "OP-TEE-chosen mappings must grow upward"); + image_cleanup.run(&task); + header_cleanup.run(&task); +}