diff --git a/Cargo.lock b/Cargo.lock index cf3463c39d..29dc7250db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3062,6 +3062,7 @@ dependencies = [ name = "hcl" version = "0.0.0" dependencies = [ + "aarch64defs", "anyhow", "arrayvec", "bitfield-struct 0.11.0", @@ -3079,6 +3080,7 @@ dependencies = [ "open_enum", "pal", "parking_lot", + "rsi", "safe_intrinsics", "sidecar_client", "signal-hook", @@ -3224,6 +3226,7 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" name = "hv1_emulator" version = "0.0.0" dependencies = [ + "aarch64defs", "build_rs_guest_arch", "guestmem", "hv1_structs", @@ -6580,6 +6583,13 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsi" +version = "0.0.0" +dependencies = [ + "zerocopy", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -7816,6 +7826,7 @@ dependencies = [ "guestmem", "hvdef", "loader", + "memory_range", "mesh", "object 0.37.3", "page_table", @@ -7826,6 +7837,7 @@ dependencies = [ "tracing-subscriber", "tracing_helpers", "underhill_mem", + "user_driver", "virt", "virt_hvf", "virt_kvm", @@ -8567,6 +8579,7 @@ dependencies = [ "memory_range", "pal_async", "parking_lot", + "rsi", "sparse_mmap", "thiserror 2.0.16", "tracelimit", @@ -9049,6 +9062,7 @@ dependencies = [ "pal_uring", "parking_lot", "pci_core", + "rsi", "safe_intrinsics", "safeatomic", "sidecar_client", diff --git a/Cargo.toml b/Cargo.toml index e6997a7f01..499512e981 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -219,6 +219,7 @@ tmk_tests = { path = "tmk/tmk_tests" } aarch64defs = { path = "vm/aarch64/aarch64defs" } aarch64emu = { path = "vm/aarch64/aarch64emu" } +rsi = { path = "vm/aarch64/rsi" } acpi = { path = "vm/acpi" } acpi_spec = { path = "vm/acpi_spec" } chipset_arc_mutex_device = { path = "vm/chipset_arc_mutex_device" } diff --git a/Guide/src/dev_guide/dev_tools/xflowey.md b/Guide/src/dev_guide/dev_tools/xflowey.md index 4e1202ea95..b9d78dd3bc 100644 --- a/Guide/src/dev_guide/dev_tools/xflowey.md +++ b/Guide/src/dev_guide/dev_tools/xflowey.md @@ -12,6 +12,7 @@ Some particularly notable pipelines: - `cargo xflowey build-igvm` - primarily dev-tool used to build OpenHCL IGVM files locally - `cargo xflowey restore-packages` - restores external packages needed to compile and run OpenVMM / OpenHCL - `cargo xflowey vmm-tests-run` - build and run VMM tests with automatic artifact discovery. Use `--filter "test(name)"` to run specific tests +- `cargo xflowey cca-tests` - build and run ARM64 CCA tests using software emulator ## `xflowey` vs `xtask` diff --git a/flowey/flowey_hvlite/src/pipelines/cca_tests.rs b/flowey/flowey_hvlite/src/pipelines/cca_tests.rs new file mode 100644 index 0000000000..d988382056 --- /dev/null +++ b/flowey/flowey_hvlite/src/pipelines/cca_tests.rs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use flowey::node::prelude::ReadVar; +use flowey::pipeline::prelude::*; +use std::path::PathBuf; + +/// CCA test flows, including installing, updating CCA emulation environment and run OpenVMM tests +#[derive(clap::Args)] +pub struct CcaTestsCli { + /// Root directory for holding all CCA test related stuff + #[clap(long, default_value = "target/cca-test")] + pub test_root: PathBuf, + + /// Install CCA emulation environment, including downloading emulator and building all needed firmware + #[clap(long)] + pub install_emu: bool, + + /// Update CCA emulation environment by rebuilding firmwares, support a few sub-commands + #[clap(long)] + pub update_emu: bool, + + /// Verbose pipeline output + #[clap(long)] + pub verbose: bool, + + #[clap(flatten)] + pub update_emu_subcmds: CcaTestsUpdateEmuSubCmds, +} + +#[derive(clap::Args)] +#[clap(next_help_heading = "--update_emu subcommands")] +pub struct CcaTestsUpdateEmuSubCmds { + /// Rebuild the plane0 Linux image from the existing source tree. + #[clap(long)] + pub rebuild_plane0_linux: bool, + + /// Rebuild the shrinkwrap-generated rootfs image. + #[clap(long)] + pub rebuild_rootfs: bool, + + /// Update TF-A to specified revision and rebuild. + #[clap(long)] + pub tfa_rev: Option, + + /// Update TF-RMM to specified revision and rebuild. + #[clap(long)] + pub tfrmm_rev: Option, + + /// Update plane0 Linux to specified revision and rebuild. + #[clap(long)] + pub plane0_linux_rev: Option, +} + +impl IntoPipeline for CcaTestsCli { + fn into_pipeline(self, backend_hint: PipelineBackendHint) -> anyhow::Result { + let Self { + test_root, + install_emu, + update_emu, + verbose, + update_emu_subcmds: + CcaTestsUpdateEmuSubCmds { + rebuild_plane0_linux, + rebuild_rootfs, + tfa_rev, + tfrmm_rev, + plane0_linux_rev, + }, + } = self; + + let openvmm_repo = flowey_lib_common::git_checkout::RepoSource::ExistingClone( + ReadVar::from_static(crate::repo_root()), + ); + + // Absolute path is expected across cca_tests infrastructure. Relative + // paths are resolved from repo root. + let test_root = if test_root.is_absolute() { + test_root + } else { + crate::repo_root().join(test_root) + }; + + let mut pipeline = Pipeline::new(); + + if install_emu { + let check_job = pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-tests: check existence of emulation environment needed tools", + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: true, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + no_incremental: false, + }) + .dep_on( + |ctx| flowey_lib_hvlite::_jobs::local_check_cca_emu_prereq::Params { + done: ctx.new_done_handle(), + }, + ) + .finish(); + + let install_job = pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-tests: install emulation environment", + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init) + .dep_on( + |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { + hvlite_repo_source: openvmm_repo.clone(), + }, + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: true, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + no_incremental: false, + }) + .dep_on( + |ctx| flowey_lib_hvlite::_jobs::local_install_cca_emu::Params { + test_root: test_root.clone(), + done: ctx.new_done_handle(), + }, + ) + .finish(); + + pipeline.non_artifact_dep(&install_job, &check_job); + return Ok(pipeline); + } + + let update_job = if update_emu { + Some( + pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-tests: update emulation environment", + ) + .dep_on( + |ctx| flowey_lib_hvlite::_jobs::local_update_cca_emu::Params { + test_root: test_root.clone(), + sub_cmds: flowey_lib_hvlite::_jobs::local_update_cca_emu::SubCmds { + rebuild_plane0_linux, + rebuild_rootfs, + tfa_rev, + tfrmm_rev, + plane0_linux_rev, + }, + done: ctx.new_done_handle(), + }, + ) + .finish(), + ) + } else { + None + }; + + let test_job = pipeline + .new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "cca-tests: run cca tests", + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init) + .dep_on( + |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { + hvlite_repo_source: openvmm_repo.clone(), + }, + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: true, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + no_incremental: false, + }) + .dep_on(|ctx| flowey_lib_hvlite::_jobs::local_run_cca_test::Params { + test_root: test_root.clone(), + done: ctx.new_done_handle(), + }) + .finish(); + + // Only add dependency if update_job exists + if let Some(update_job) = &update_job { + pipeline.non_artifact_dep(&test_job, update_job); + } + + Ok(pipeline) + } +} diff --git a/flowey/flowey_hvlite/src/pipelines/mod.rs b/flowey/flowey_hvlite/src/pipelines/mod.rs index f04d9e2ff1..101f7adf9e 100644 --- a/flowey/flowey_hvlite/src/pipelines/mod.rs +++ b/flowey/flowey_hvlite/src/pipelines/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use cca_tests::CcaTestsCli; use flowey::pipeline::prelude::*; use restore_packages::RestorePackagesCli; use vmm_tests_run::VmmTestsRunCli; @@ -8,6 +9,7 @@ use vmm_tests_run::VmmTestsRunCli; pub mod build_docs; pub mod build_igvm; pub mod build_reproducible; +pub mod cca_tests; pub mod checkin_gates; pub mod custom_vmfirmwareigvm_dll; pub mod restore_packages; @@ -36,6 +38,9 @@ pub enum OpenvmmPipelines { /// Build and run VMM tests with automatic artifact discovery VmmTestsRun(VmmTestsRunCli), + + /// Build and run CCA tests with installation of emulation environment supported + CcaTests(CcaTestsCli), } #[derive(clap::Subcommand)] @@ -64,6 +69,7 @@ impl IntoPipeline for OpenvmmPipelines { }, OpenvmmPipelines::RestorePackages(cmd) => cmd.into_pipeline(pipeline_hint), OpenvmmPipelines::VmmTestsRun(cmd) => cmd.into_pipeline(pipeline_hint), + OpenvmmPipelines::CcaTests(cmd) => cmd.into_pipeline(pipeline_hint), } } } diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_check_cca_emu_prereq.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_check_cca_emu_prereq.rs new file mode 100644 index 0000000000..058f4b2e7d --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_check_cca_emu_prereq.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! To install CCA emulation environment, we need a few tools. This job checks +//! their existence. +use flowey::node::prelude::*; +use std::fs; + +flowey_request! { + pub struct Params { + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(ctx: &mut ImportCtx<'_>) { + ctx.import::(); + ctx.import::(); + } + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { done } = request; + + let required_packages_installed = ctx.reqv(|v| { + flowey_lib_common::install_dist_pkg::Request::Install { + package_names: vec![ + "netcat-openbsd".into(), + "python3".into(), + "python3-pip".into(), + "telnet".into(), + "docker.io".into(), + "gcc-aarch64-linux-gnu".into(), + // flex and bison are needed when building linux kernel kconfig parser + "flex".into(), + "bison".into(), + "libssl-dev".into(), + "python3-venv".into(), + ], + done: v, + } + }); + + ctx.emit_rust_step("check prerequisite of arm64 emulation environment", |ctx| { + done.claim(ctx); + required_packages_installed.claim(ctx); + move |rt| { + // Check if docker is setup + let group_name = "docker"; + let group_file = fs::read_to_string("/etc/group").expect("Failed to read /etc/group"); + let docker_group = group_file + .lines() + .find(|line| line.starts_with(&format!("{group_name}:"))); + + if docker_group.is_none() { + anyhow::bail!("Group '{group_name}' does not exist, please add it using 'sudo groupadd docker'"); + } + + // Check if current user is in the group + let output = flowey::shell_cmd!(rt, "id -nG").output()?; + let output = String::from_utf8(output.stdout)?; + let is_member = output.split_whitespace().any(|g| g == group_name); + if !is_member { + anyhow::bail!("Current user does NOT belong to the '{group_name}' group, please add it using 'sudo usermod -aG docker $USER', and restart the shell!"); + } + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_install_cca_emu.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_install_cca_emu.rs new file mode 100644 index 0000000000..84794e0cbf --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_install_cca_emu.rs @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Install CCA emulation environment. Now we only support using ARM's Fixed +//! Virtual Platform (FVP) as the emulator. The environment also contains a +//! few essential firmwares in CCA stack, for example TF-A and TF-RMM. ARM has +//! published an python based tool, 'shrinkwrap', to simply the deployment +//! process, this installation use it as well. +use flowey::node::prelude::RustRuntimeServices; +use flowey::node::prelude::*; +use std::env; +use std::path::Path; +use std::path::PathBuf; +use std::thread; + +const SHRINKWRAP_REPO: &str = "https://git.gitlab.arm.com/tooling/shrinkwrap.git"; +// The guest Linux kernel (with cca/plane driver) hasn't been upstreamed yet, fetch it from our private repo +const PLANE0_LINUX_REPO: &str = "https://github.com/jiong-microsoft/OHCL-Linux-Kernel.git"; +const PLANE0_LINUX_BRANCH: &str = "cca-dev"; +// A few config information needed when building Linux kernel +const CCA_CONFIGS: &[&str] = &["CONFIG_VIRT_DRIVERS", "CONFIG_ARM_CCA_GUEST"]; +const NINEP_CONFIGS: &[&str] = &[ + "CONFIG_NET_9P", + "CONFIG_NET_9P_FD", + "CONFIG_NET_9P_VIRTIO", + "CONFIG_NET_9P_FS", +]; +const HYPERV_CONFIGS: &[&str] = &[ + "CONFIG_HYPERV", + "CONFIG_HYPERV_MSHV", + "CONFIG_MSHV", + "CONFIG_MSHV_VTL", + "CONFIG_HYPERV_VTL_MODE", +]; + +flowey_request! { + pub struct Params { + /// The CCA test root directory, defaults to target/cca-test. + pub test_root: PathBuf, + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +fn clone_repo( + rt: &RustRuntimeServices<'_>, + repo_url: &str, + target_dir: &Path, + branch: Option<&str>, + repo_name: &str, +) -> anyhow::Result<()> { + if target_dir.exists() { + log::info!( + "{} has been installed at {}", + repo_name, + target_dir.display() + ); + return Ok(()); + } + + log::info!("Cloning {} to {}", repo_name, target_dir.display()); + + if let Some(b) = branch { + flowey::shell_cmd!(rt, "git clone --branch {b} {repo_url} {target_dir}").run()?; + } else { + flowey::shell_cmd!(rt, "git clone {repo_url} {target_dir}").run()?; + } + + log::info!("{} has been cloned successfully", repo_name); + + Ok(()) +} + +fn enable_kernel_configs( + rt: &RustRuntimeServices<'_>, + group: &str, + configs: &[&str], +) -> anyhow::Result<()> { + // Enable each config one at a time to avoid shell argument parsing issues + for config in configs { + flowey::shell_cmd!(rt, "./scripts/config --file .config --enable {config}") + .run() + .with_context(|| format!("Failed to enable {} kernel config {}", group, config))?; + } + + Ok(()) +} + +fn make_target( + rt: &RustRuntimeServices<'_>, + arch: &str, + target: &str, + jobs: &str, +) -> anyhow::Result<()> { + flowey::shell_cmd!( + rt, + "make ARCH={arch} CROSS_COMPILE=aarch64-linux-gnu- {target} -j{jobs}" + ) + .run() + .with_context(|| format!("Failed to run `make {}`", target))?; + Ok(()) +} + +pub(crate) fn build_plane0_linux( + rt: &RustRuntimeServices<'_>, + plane0_linux: &Path, + plane0_image: &Path, +) -> anyhow::Result<()> { + log::info!("Compiling Plane0 Linux kernel..."); + rt.sh.change_dir(plane0_linux); + + const ARCH: &str = "arm64"; + const SINGLE_JOB: &str = "1"; + + log::info!("Running make defconfig..."); + make_target(rt, ARCH, "defconfig", SINGLE_JOB)?; + + log::info!("Enabling required kernel configurations..."); + for (name, configs) in [ + ("CCA", CCA_CONFIGS), + ("9P", NINEP_CONFIGS), + ("Hyper-V", HYPERV_CONFIGS), + ] { + enable_kernel_configs(rt, name, configs)?; + } + + log::info!("Running make olddefconfig..."); + make_target(rt, ARCH, "olddefconfig", SINGLE_JOB)?; + + let jobs = + thread::available_parallelism().map_or_else(|_| "1".to_string(), |n| n.get().to_string()); + + log::info!("Building plane0 kernel image..."); + make_target(rt, ARCH, "Image", &jobs)?; + + anyhow::ensure!( + plane0_image.exists(), + "Plane0 kernel compilation appeared to succeed but image file was not found at {}", + plane0_image.display() + ); + + log::info!("Plane0 Linux kernel compiled successfully"); + log::info!("Kernel image at: {}", plane0_image.display()); + Ok(()) +} + +fn sync_shrinkwrap_overlay_assets( + openvmm_root: &Path, + shrinkwrap_dir: &Path, +) -> anyhow::Result<()> { + let overlay_assets = [ + ( + openvmm_root.join("vmm_tests/vmm_tests/test_data/cca_planes.yaml"), + shrinkwrap_dir.join("config/cca_planes.yaml"), + "planes.yaml", + ), + ( + openvmm_root.join("vmm_tests/vmm_tests/test_data/cca_realm_overlay.yaml"), + shrinkwrap_dir.join("config/cca_realm_overlay.yaml"), + "realm overlay config", + ), + ]; + + fs_err::create_dir_all(shrinkwrap_dir.join("config"))?; + + for (src, dest, label) in overlay_assets { + if dest.is_file() { + if fs_err::read(&src)? == fs_err::read(&dest)? { + log::info!( + "{label} already exists at {} and matches source", + dest.display() + ); + continue; + } + + log::info!( + "{label} already exists at {} but differs from source; replacing it", + dest.display() + ); + } + + log::info!( + "Copying {label} from {} to {}", + src.display(), + dest.display() + ); + + fs_err::copy(&src, &dest)?; + } + + Ok(()) +} + +pub(crate) fn build_cca_rootfs( + rt: &RustRuntimeServices<'_>, + test_root: &Path, + shrinkwrap_dir: &Path, + venv_dir: &Path, +) -> anyhow::Result<()> { + let shrinkwrap_exe = shrinkwrap_dir.join("shrinkwrap/shrinkwrap"); + anyhow::ensure!( + shrinkwrap_exe.exists(), + "shrinkwrap installation is missing or broken at {}, try --install-emu first", + shrinkwrap_exe.display() + ); + anyhow::ensure!( + venv_dir.exists(), + "shrinkwrap venv is missing at {}, try --install-emu first", + venv_dir.display() + ); + + let log_dir = test_root.join("logs"); + fs_err::create_dir_all(&log_dir)?; + let log_file = log_dir.join("shrinkwrap.build.log"); + + let path = format!( + "{}:{}", + venv_dir.join("bin").display(), + env::var("PATH").unwrap_or_default() + ); + + let rootfs = "${artifact:BUILDROOT}"; + let tfa_revision = "8dae0862c502e08568a61a1050091fa9357f1240"; + let cmd = format!( + "{} build cca-3world.yaml \ + --overlay buildroot.yaml \ + --overlay cca_realm_overlay.yaml \ + --overlay cca_planes.yaml \ + --btvar GUEST_ROOTFS={rootfs} \ + --btvar TFA_REVISION={tfa_revision} \ + 2>&1 | tee {}", + shrinkwrap_exe.display(), + log_file.display() + ); + + flowey::shell_cmd!(rt, "bash -c {cmd}") + .env("VIRTUAL_ENV", venv_dir) + .env("PATH", path) + .run() + .with_context(|| "failed to do shrinkwrap build")?; + + log::info!("shrinkwrap build finished, emulation env have been setup"); + Ok(()) +} + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(ctx: &mut ImportCtx<'_>) { + ctx.import::(); + } + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { test_root, done } = request; + + let openvmm_root = ctx.reqv(crate::git_checkout_openvmm_repo::req::GetRepoDir); + + ctx.emit_rust_step("install cca emulation environment", |ctx| { + done.claim(ctx); + let openvmm_root = openvmm_root.claim(ctx); + move |rt| { + // emulation environment is under 'test_root' + fs_err::create_dir_all(&test_root)?; + + // 'shrinkwrap' only build host Linux kernel, plane0 Linux kernel + // needs to be downloaded and built separately. + let plane0_linux = test_root.join("plane0-linux"); + let plane0_image = plane0_linux + .join("arch") + .join("arm64") + .join("boot") + .join("Image"); + if plane0_linux.exists() { + log::info!( + "plane0 Linux source tree is already installed at: {}", + plane0_linux.display() + ); + } else { + clone_repo( + rt, + PLANE0_LINUX_REPO, + &plane0_linux, + Some(PLANE0_LINUX_BRANCH), + "plane0 Linux", + )?; + } + + // Now check if image has been built + if plane0_image.exists() { + log::info!( + "plane0 Linux image also has been built and found at: {}", + plane0_image.display() + ); + } else { + build_plane0_linux(rt, &plane0_linux, &plane0_image)?; + } + + // Install the remaining emulation environment components + // using 'shrinkwrap', which leverages YAML to define all required + // components. This significantly reduces manual effort and the risk of errors. + let shrinkwrap_dir = test_root.join("shrinkwrap"); + let venv_dir = shrinkwrap_dir.join("venv"); + if shrinkwrap_dir.exists() { + log::info!( + "'shrinkwrap' source tree is already installed at: {}", + shrinkwrap_dir.display() + ); + } else { + clone_repo(rt, SHRINKWRAP_REPO, &shrinkwrap_dir, None, "shrinkwrap")?; + + // Create venv + if !venv_dir.exists() { + log::info!( + "Creating Python virtual environment at {}", + venv_dir.display() + ); + flowey::shell_cmd!(rt, "python3 -m venv") + .arg(&venv_dir) + .run()?; + } + + // Install packages + log::info!("Installing Python dependencies..."); + let pip = venv_dir.join("bin/pip"); + + flowey::shell_cmd!(rt, "{pip} install --upgrade pip").run()?; + flowey::shell_cmd!(rt, "{pip} install pyyaml termcolor tuxmake").run()?; + } + + let openvmm_root = rt.read(openvmm_root); + sync_shrinkwrap_overlay_assets(&openvmm_root, &shrinkwrap_dir)?; + + let home_dir = env::var("HOME").map(PathBuf::from).expect("HOME not set"); + let rootfs_file = home_dir.join(".shrinkwrap/package/cca-3world/rootfs.ext2"); + if rootfs_file.exists() { + log::info!( + "cca emulation rootfs is already generated at: {}", + rootfs_file.display() + ); + } else { + build_cca_rootfs(rt, &test_root, &shrinkwrap_dir, &venv_dir)?; + } + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_run_cca_test.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_run_cca_test.rs new file mode 100644 index 0000000000..267e5a33ac --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_run_cca_test.rs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Run OpenVMM CCA tests. Now we run them using emulator, code can be tweaked +//! to support running tests on native hardware platform. +use crate::common::CommonArch; +use crate::common::CommonPlatform; +use crate::common::CommonProfile; +use crate::common::CommonTriple; +use flowey::node::prelude::*; +use std::env; +use std::ffi::OsStr; +use std::thread; +use std::time::Duration; + +flowey_request! { + pub struct Params { + pub test_root: PathBuf, + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(ctx: &mut ImportCtx<'_>) { + ctx.import::(); + ctx.import::(); + } + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { test_root, done } = request; + + let shrinkwrap_dir = test_root.join("shrinkwrap"); + let venv_dir = shrinkwrap_dir.join("venv"); + let shrinkwrap_exe = shrinkwrap_dir.join("shrinkwrap/shrinkwrap"); + if !shrinkwrap_exe.exists() { + anyhow::bail!( + "shrinkwrap installation is missing or broken, try --install-emu or --update-emu --rebuild" + ); + } + + if !venv_dir.exists() { + anyhow::bail!( + "can't find shrinkwrap venv, try --install-emu or --update-emu --rebuild" + ); + } + + let plane0_linux_image = test_root.join("plane0-linux/arch/arm64/boot/Image"); + if !plane0_linux_image.exists() { + anyhow::bail!( + "can't find plane0 linux image at {}, try --install-emu or --update-emu --rebuild-plane0-linux", + plane0_linux_image.display() + ); + } + + let home_dir = env::var("HOME").map(PathBuf::from).expect("HOME not set"); + let firmware_dir = home_dir.join(".shrinkwrap/package/cca-3world"); + let rootfs_file = firmware_dir.join("rootfs.ext2"); + if !rootfs_file.exists() { + anyhow::bail!( + "can't find cca emulation rootfs at {}, try --install-emu or --update-emu --rebuild-rootfs", + rootfs_file.display() + ); + } + + let e2fsck_bin = + home_dir.join(".shrinkwrap/build/build/cca-3world/buildroot/host/sbin/e2fsck"); + if !e2fsck_bin.exists() { + anyhow::bail!( + "can't find host e2fsck binary at {}, try --install-emu or --update-emu --rebuild", + e2fsck_bin.display() + ); + } + + let resize2fs_bin = + home_dir.join(".shrinkwrap/build/build/cca-3world/buildroot/host/sbin/resize2fs"); + if !resize2fs_bin.exists() { + anyhow::bail!( + "can't find host resize2fs binary at {}, try --install-emu or --update-emu --rebuild", + resize2fs_bin.display() + ); + } + + // Generate request to build tmk_vmm + let tmk_vmm_output = ctx.reqv(|v| crate::build_tmk_vmm::Request { + target: CommonTriple::Common { + arch: CommonArch::Aarch64, + platform: CommonPlatform::LinuxGnu, + }, + unstable_whp: false, + profile: CommonProfile::Debug, + tmk_vmm: v, + }); + + // Generate request to build simple_tmk + let simple_tmk_output = ctx.reqv(|v| crate::build_tmks::Request { + arch: CommonArch::Aarch64, + profile: CommonProfile::Debug, + tmks: v, + }); + + ctx.emit_rust_step("running cca tests", |ctx| { + done.claim(ctx); + let tmk_vmm_output = tmk_vmm_output.claim(ctx); + let simple_tmk_output = simple_tmk_output.claim(ctx); + move |rt| { + let tmk_vmm_output = rt.read(tmk_vmm_output); + let crate::build_tmk_vmm::TmkVmmOutput::LinuxBin { bin: tmk_vmm_bin, .. } = + tmk_vmm_output + else { + anyhow::bail!("expect Linux tmk_vmm only"); + }; + + let simple_tmk_output = rt.read(simple_tmk_output); + let simple_tmk_bin = simple_tmk_output.bin; + + // fsck has the following exit_code, if we start the FVP and + // then kill it by force, the rootfs will left in 'dirty' status, + // but fsck will just clean it and finish with exit code 1, this + // is not an error. + // + // 0 No errors + // 1 Errors found and corrected (common after journal replay) + // (full exit code see https://man7.org/linux/man-pages/man8/e2fsck.8.html) + let fsck_cmd = format!( + r#" + {e2fsck_bin} -fp {rootfs_file} || rc=$? + [ "${{rc:-0}}" -le 1 ] || exit "$rc" + "#, + e2fsck_bin = e2fsck_bin.display(), + rootfs_file = rootfs_file.display(), + ); + flowey::shell_cmd!(rt, "bash -c {fsck_cmd}").run()?; + log::info!("e2fsck finished"); + + flowey::shell_cmd!(rt, "{resize2fs_bin} {rootfs_file} 1024M").run()?; + log::info!("resize rootfs to 1024M finished"); + + let guest_disk = firmware_dir.join("guest-disk.img"); + let kvmtool_efi = firmware_dir.join("KVMTOOL_EFI.fd"); + let lkvm = firmware_dir.join("lkvm"); + + let mnt_dir = PathBuf::from("mnt"); + let cca_dir = mnt_dir.join("cca"); + + let run_sudo = |description: &str, args: &[&OsStr]| -> anyhow::Result<()> { + let status = std::process::Command::new("sudo") + .args(args) + .status() + .with_context(|| format!("failed to execute sudo command to {description}"))?; + + if !status.success() { + anyhow::bail!("failed to {description}: exit status {status}"); + } + + Ok(()) + }; + + let mut mounted = false; + let inject_result = (|| -> anyhow::Result<()> { + run_sudo( + "create guest rootfs mount directory", + &[OsStr::new("mkdir"), OsStr::new("-p"), mnt_dir.as_os_str()], + )?; + run_sudo( + "mount guest rootfs", + &[OsStr::new("mount"), rootfs_file.as_os_str(), mnt_dir.as_os_str()], + )?; + mounted = true; + + run_sudo( + "create cca directory in guest rootfs", + &[OsStr::new("mkdir"), OsStr::new("-p"), cca_dir.as_os_str()], + )?; + + for file in [ + &simple_tmk_bin, + &tmk_vmm_bin, + &guest_disk, + &plane0_linux_image, + &kvmtool_efi, + &lkvm, + ] { + run_sudo( + &format!("copy {} into guest rootfs", file.display()), + &[OsStr::new("cp"), file.as_os_str(), cca_dir.as_os_str()], + )?; + } + + run_sudo("sync guest rootfs writes", &[OsStr::new("sync")])?; + + Ok(()) + })(); + + if mounted { + if let Err(err) = run_sudo( + "unmount guest rootfs", + &[OsStr::new("umount"), mnt_dir.as_os_str()], + ) + .or_else(|_| { + run_sudo( + "lazy unmount guest rootfs", + &[OsStr::new("umount"), OsStr::new("-l"), mnt_dir.as_os_str()], + ) + }) { + log::warn!("{err:#}"); + } + } + + if let Err(err) = run_sudo("sync host writes", &[OsStr::new("sync")]) { + log::warn!("{err:#}"); + } + + thread::sleep(Duration::from_secs(1)); + for _ in 0..5 { + if !mnt_dir.is_dir() { + break; + } + + if run_sudo( + "remove guest rootfs mount directory", + &[OsStr::new("rmdir"), mnt_dir.as_os_str()], + ) + .is_ok() + { + break; + } + + thread::sleep(Duration::from_millis(500)); + } + + if mnt_dir.is_dir() { + if let Err(err) = run_sudo( + "force remove guest rootfs mount directory", + &[OsStr::new("rm"), OsStr::new("-rf"), mnt_dir.as_os_str()], + ) { + log::warn!("{err:#}"); + } + } + + inject_result.with_context(|| "failed to mount or inject files into guest rootfs")?; + + log::info!("rootfs.ext2 updated successfully with cca firmwares, paravisor, and tests injected"); + log::info!("launching openvmm cca tests..."); + + let venv_bin_path = format!("{}:{}", venv_dir.join("bin").display(), env::var("PATH").unwrap_or_default()); + flowey::shell_cmd!(rt, "{shrinkwrap_exe} run cca-3world.yaml --rtvar ROOTFS={rootfs_file}") + .env("VIRTUAL_ENV", &venv_dir) + .env("PATH", &venv_bin_path) + .run() + .with_context(|| "failed to launch guest using shrinkwrap")?; + + log::info!("openvmm cca tests finished"); + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_update_cca_emu.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_update_cca_emu.rs new file mode 100644 index 0000000000..839e63e3f5 --- /dev/null +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_update_cca_emu.rs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Update components in CCA emulation environment according to options specified. +use crate::_jobs::local_install_cca_emu::build_cca_rootfs; +use crate::_jobs::local_install_cca_emu::build_plane0_linux; +use flowey::node::prelude::*; + +#[derive(Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct SubCmds { + pub rebuild_plane0_linux: bool, + pub rebuild_rootfs: bool, + pub tfa_rev: Option, + pub tfrmm_rev: Option, + pub plane0_linux_rev: Option, +} + +flowey_request! { + pub struct Params { + pub test_root: PathBuf, + pub sub_cmds: SubCmds, + pub done: WriteVar, + } +} + +new_simple_flow_node!(struct Node); + +impl SimpleFlowNode for Node { + type Request = Params; + + fn imports(_ctx: &mut ImportCtx<'_>) {} + + fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { + let Params { + test_root, + sub_cmds, + done, + } = request; + + let SubCmds { + rebuild_plane0_linux, + rebuild_rootfs, + tfa_rev: _, + tfrmm_rev: _, + plane0_linux_rev: _, + } = sub_cmds; + + ctx.emit_rust_step("update cca emulation environment", |ctx| { + done.claim(ctx); + move |rt| { + if rebuild_plane0_linux { + let plane0_linux = test_root.join("plane0-linux"); + let plane0_image = plane0_linux + .join("arch") + .join("arm64") + .join("boot") + .join("Image"); + + anyhow::ensure!( + plane0_linux.exists(), + "plane0 Linux source tree is missing at {}, try --install-emu first", + plane0_linux.display() + ); + + build_plane0_linux(rt, &plane0_linux, &plane0_image)?; + } + + if rebuild_rootfs { + let shrinkwrap_dir = test_root.join("shrinkwrap"); + let venv_dir = shrinkwrap_dir.join("venv"); + build_cca_rootfs(rt, &test_root, &shrinkwrap_dir, &venv_dir)?; + } + + Ok(()) + } + }); + + Ok(()) + } +} diff --git a/flowey/flowey_lib_hvlite/src/_jobs/mod.rs b/flowey/flowey_lib_hvlite/src/_jobs/mod.rs index 97dded08a0..be79bee33d 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/mod.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/mod.rs @@ -24,7 +24,11 @@ pub mod consume_and_test_nextest_unit_tests_archive; pub mod consume_and_test_nextest_vmm_tests_archive; pub mod local_build_and_run_nextest_vmm_tests; pub mod local_build_igvm; +pub mod local_check_cca_emu_prereq; pub mod local_custom_vmfirmwareigvm_dll; +pub mod local_install_cca_emu; pub mod local_restore_packages; +pub mod local_run_cca_test; +pub mod local_update_cca_emu; pub mod publish_vmgstool_gh_release; pub mod test_local_flowey_build_igvm; diff --git a/openhcl/hcl/Cargo.toml b/openhcl/hcl/Cargo.toml index 83c09df290..7952236571 100644 --- a/openhcl/hcl/Cargo.toml +++ b/openhcl/hcl/Cargo.toml @@ -17,6 +17,8 @@ tdcall = { workspace = true, features = ["tracing"] } x86defs.workspace = true inspect.workspace = true user_driver.workspace = true +aarch64defs.workspace = true +rsi.workspace = true anyhow.workspace = true arrayvec.workspace = true diff --git a/openhcl/hcl/src/ioctl.rs b/openhcl/hcl/src/ioctl.rs index 8e56b9269c..4c1df11a17 100755 --- a/openhcl/hcl/src/ioctl.rs +++ b/openhcl/hcl/src/ioctl.rs @@ -7,6 +7,7 @@ mod deferred; pub mod register; pub mod aarch64; +pub mod cca; pub mod snp; pub mod tdx; pub mod x64; @@ -15,6 +16,7 @@ use self::deferred::DeferredActionSlots; use self::ioctls::*; use crate::GuestVtl; use crate::ioctl::deferred::DeferredAction; +use crate::ioctl::register::SetRegError; use crate::mapped_page::MappedPage; use crate::protocol; use crate::protocol::EnterModes; @@ -130,6 +132,11 @@ pub enum Error { MapRedirectedDeviceInterrupt(#[source] nix::Error), #[error("failed to restore partition time")] RestorePartitionTime(#[source] nix::Error), + // TODO: added for CCA for now. could separate into own enum + #[error("failed to set registers using set_vp_registers hypercall")] + SetRegisters(#[source] SetRegError), + #[error("Invalid register value")] + InvalidRegisterValue, } /// Error for IOCTL errors specifically. @@ -204,6 +211,8 @@ pub enum ApplyVtlProtectionsError { permissions: x86defs::tdx::TdgMemPageGpaAttr, vtl: HvInputVtl, }, + #[error("cca failed when protecting pages {range} with permissions for vtl {vtl:?}")] + Cca { range: MemoryRange, vtl: HvInputVtl }, #[error("no valid protections for vtl {0:?}")] InvalidVtl(Vtl), } @@ -330,6 +339,7 @@ enum HvcallRepInput<'a, T> { } pub(crate) mod ioctls { + use super::cca; use crate::protocol; use hvdef::hypercall::HvRegisterAssoc; use nix::ioctl_none; @@ -361,6 +371,9 @@ pub(crate) mod ioctls { const MSHV_KICKCPUS: u16 = 0x38; const MSHV_MAP_REDIRECTED_DEVICE_INTERRUPT: u16 = 0x39; const MSHV_VTL_SECURE_AVIC_VTL0_PFN: u16 = 0x40; + const MSHV_VTL_REALM_CONFIG: u16 = 0x41; + const MSHV_VTL_RSI_SYSREG_WRITE: u16 = 0x42; + const MSHV_VTL_RSI_SET_MEM_PERM: u16 = 0x43; #[repr(C)] #[derive(Copy, Clone)] @@ -578,6 +591,36 @@ pub(crate) mod ioctls { ioctl_read!(mshv_create_vtl, MSHV_IOCTL, MSHV_CREATE_VTL, u8); + // CCA: Gets the RSI Realm Config value from the kernel + ioctl_read!( + hcl_realm_config, + MSHV_IOCTL, + MSHV_VTL_REALM_CONFIG, + cca::mshv_realm_config + ); + + // CCA: Set the value of a system register + ioctl_write_ptr!( + hcl_rsi_sysreg_write, + MSHV_IOCTL, + MSHV_VTL_RSI_SYSREG_WRITE, + cca::mshv_rsi_sysreg_write + ); + + // CCA: Assign the address described by `mshv_rsi_set_mem_perm` + // to a plane. + // Note: This is a simplification of the memory access configuration. + // The kernel driver does some stuff under the hood, making two RSI calls + // as part of this ioctl: RSI_MEM_SET_PERM_VALUE and RSI_MEM_SET_PERM_INDEX. + // Will need to decide how to design this interface and who maps the + // memory of a plane to the RSI calls needed to set it up. + ioctl_write_ptr!( + hcl_rsi_set_mem_perm, + MSHV_IOCTL, + MSHV_VTL_RSI_SET_MEM_PERM, + cca::mshv_rsi_set_mem_perm + ); + #[repr(C)] pub struct mshv_invlpgb { pub rax: u64, @@ -1359,6 +1402,8 @@ pub enum IsolationType { Snp, /// Intel TDX. Tdx, + /// ARM CCA. + Cca, } impl IsolationType { @@ -1369,7 +1414,7 @@ impl IsolationType { /// Returns whether the isolation type is hardware-backed. pub fn is_hardware_isolated(&self) -> bool { - matches!(self, Self::Snp | Self::Tdx) + matches!(self, Self::Snp | Self::Tdx | Self::Cca) } } @@ -1410,6 +1455,9 @@ enum BackingState { vtl0_apic_page: MappedPage, vtl1_apic_page: MemoryBlock, }, + Cca { + plane_run: MemoryBlock, + }, } #[derive(Debug)] @@ -1487,6 +1535,12 @@ impl HclVp { .allocate_dma_buffer(HV_PAGE_SIZE as usize) .map_err(Error::AllocVp)?, }, + IsolationType::Cca => BackingState::Cca { + plane_run: private_dma_client + .ok_or(Error::MissingPrivateMemory)? + .allocate_dma_buffer(HV_PAGE_SIZE as usize) + .map_err(Error::AllocVp)?, + }, }; Ok(Self { @@ -1747,10 +1801,22 @@ impl Hcl { // // FUTURE: the kernel driver should probably tell us this, especially // since the kernel ABI is different for different isolation types. - let supported_isolation = if cfg!(guest_arch = "x86_64") { + let validate_isolation = |supported_isolation| { + if isolation != supported_isolation { + Err(Error::MismatchedIsolation { + supported: supported_isolation, + requested: isolation, + }) + } else { + Ok(()) + } + }; + + #[cfg(guest_arch = "x86_64")] + { // xtask-fmt allow-target-arch cpu-intrinsic #[cfg(target_arch = "x86_64")] - { + let supported_isolation = { let result = safe_intrinsics::cpuid( hvdef::HV_CPUID_FUNCTION_MS_HV_ISOLATION_CONFIGURATION, 0, @@ -1762,21 +1828,12 @@ impl Hcl { 3 => IsolationType::Tdx, ty => panic!("unknown isolation type {ty:#x}"), } - } + }; // xtask-fmt allow-target-arch cpu-intrinsic #[cfg(not(target_arch = "x86_64"))] - { - unreachable!() - } - } else { - IsolationType::None - }; + let supported_isolation = unreachable!(); - if isolation != supported_isolation { - return Err(Error::MismatchedIsolation { - supported: supported_isolation, - requested: isolation, - }); + validate_isolation(supported_isolation)?; } let supports_vtl_ret_action = mshv_fd.check_extension(HCL_CAP_VTL_RETURN_ACTION)?; @@ -1793,6 +1850,31 @@ impl Hcl { let vtl_fd = mshv_fd.create_vtl()?; + #[cfg(guest_arch = "aarch64")] + { + let supported_isolation = match isolation { + IsolationType::Cca => { + // Realm-visible ID registers can be sanitized, so use the + // HCL/RMM path to validate that CCA is actually available. + if vtl_fd.get_realm_config().is_ok() { + IsolationType::Cca + } else { + IsolationType::None + } + } + _ => IsolationType::None, + }; + + validate_isolation(supported_isolation)?; + } + + #[cfg(not(any(guest_arch = "x86_64", guest_arch = "aarch64")))] + { + let supported_isolation = IsolationType::None; + + validate_isolation(supported_isolation)?; + } + // Open the hypercall pseudo-device let mshv_hvcall = MshvHvcall::new()?; diff --git a/openhcl/hcl/src/ioctl/cca.rs b/openhcl/hcl/src/ioctl/cca.rs new file mode 100755 index 0000000000..de4e1bcf3a --- /dev/null +++ b/openhcl/hcl/src/ioctl/cca.rs @@ -0,0 +1,457 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Backing for CCA partitions. + +use std::os::fd::AsRawFd; + +use super::Hcl; +use super::HclVp; +use super::MshvVtl; +use super::NoRunner; +use super::ProcessorRunner; +use crate::GuestVtl; +use crate::ioctl::Error; +use crate::ioctl::HvError; +use crate::ioctl::SetRegError; +use crate::ioctl::ioctls::hcl_realm_config; +use crate::ioctl::ioctls::hcl_rsi_set_mem_perm; +use crate::ioctl::ioctls::hcl_rsi_sysreg_write; +use aarch64defs::SystemReg; +use hvdef::HV_PAGE_SIZE; +use hvdef::HvArm64RegisterName; +use hvdef::HvRegisterName; +use hvdef::HvRegisterValue; +use memory_range::MemoryRange; +use rsi::RSI_PLANE_ENTER_FLAGS_TRAP_SIMD; +use rsi::RSI_PLANE_GIC_NUM_LRS; +use rsi::RSI_PLANE_NR_GPRS; +use rsi::cca_rsi_plane_entry; +use rsi::cca_rsi_plane_exit; +use rsi::cca_rsi_plane_run; +use sidecar_client::SidecarVp; +use user_driver::memory::MemoryBlock; + +/// CCA: Structure mirroring the data returned by RMM in the RSI_REALM_CONFIG call. +#[repr(C, align(0x1000))] +#[derive(Clone, Copy, Default)] +#[expect(missing_docs)] +pub struct mshv_realm_config { + pub ipa_width: u64, + pub algorithm: u64, + pub num_aux_planes: u64, + pub gicv3_vtr: u64, +} + +/// CCA: Structure (mostly) mirroring the data taken by RMM in the RSI_PLANE_SYSREG_WRITE. +/// `vtl` is converted into plane number in kernel driver. +#[repr(C)] +#[derive(Clone, Copy, Default)] +#[expect(missing_docs)] +pub struct mshv_rsi_sysreg_write { + pub vtl: u8, + pub _pad: [u8; 7], + pub sysreg: u64, + pub value: u64, +} + +/// CCA: Structure mirroring the data taken by RMM in the RSI_SET_MEM_PERM. +/// NOTE: we hand over the plane number here, we should probably stay consistent with +/// `sysreg_write`. +#[repr(C)] +#[derive(Clone, Copy, Default)] +#[expect(missing_docs)] +pub struct mshv_rsi_set_mem_perm { + pub plane: u8, + pub _pad: [u8; 7], + pub base_addr: u64, + pub top_addr: u64, +} + +/// SystemReg is following encoding used by MSR/MRS which is different with +/// the encoding RSI is using. The latter doesn't left shift the register +/// number. +const fn encode_rsi_sysreg(sysreg: SystemReg) -> u64 { + ((sysreg.0.op0() as u64) << 14) + | ((sysreg.0.op1() as u64) << 11) + | ((sysreg.0.crn() as u64) << 7) + | ((sysreg.0.crm() as u64) << 3) + | (sysreg.0.op2() as u64) +} + +/// Runner backing for CCA partitions. +pub struct Cca { + plane_run: MemoryBlock, +} + +impl Cca { + /// Create new CCA runner backing. + pub fn new(plane_run: &MemoryBlock) -> Self { + assert_eq!(plane_run.offset_in_page(), 0); + assert!(plane_run.len() >= size_of::()); + + Self { + plane_run: plane_run.clone(), + } + } + + fn plane_run_ref(&self) -> &cca_rsi_plane_run { + // SAFETY: the DMA allocation remains mapped for the lifetime of the backing + // and is page-aligned, so it can be viewed as a `cca_rsi_plane_run`. Also, + // 'new' validates that the allocation size is >= sizeof cca_rsi_plane_run. + unsafe { &*self.plane_run.base().cast::() } + } + + fn plane_run_mut(&mut self) -> &mut cca_rsi_plane_run { + // SAFETY: the DMA allocation remains mapped for the lifetime of the backing + // and `&mut self` guarantees exclusive access to the mapped page contents. + unsafe { &mut *self.plane_run.base().cast_mut().cast::() } + } + + fn plane_run_phys(&self) -> u64 { + self.plane_run.pfns()[0] * HV_PAGE_SIZE + } +} + +impl ProcessorRunner<'_, Cca> { + /// Returns a reference to the current VTL's CPU context. + pub fn cpu_context(&self) -> &u64 { + // SAFETY: the cpu context will not be concurrently accessed by the + // hypervisor while this VP is in VTL2. + unsafe { &*(&raw mut (*self.run.get()).context).cast() } + } + + /// Returns a mutable reference to the current VTL's CPU context. + pub fn cpu_context_mut(&mut self) -> &mut u64 { + // SAFETY: the cpu context will not be concurrently accessed by the + // hypervisor while this VP is in VTL2. + unsafe { &mut *(&raw mut (*self.run.get()).context).cast() } + } + + /// Returns a mutable reference to the current VTL's CCA RSI plane run structure. + pub fn cca_rsi_plane_run_mut(&mut self) -> &mut cca_rsi_plane_run { + self.state.plane_run_mut() + } + + /// Returns a mutable reference to the current VTL's plane entry structure. + pub fn cca_rsi_plane_entry(&mut self) -> &mut cca_rsi_plane_entry { + &mut self.state.plane_run_mut().entry + } + + /// Returns a mutable reference to the current VTL's plane exit structure. + pub fn cca_rsi_plane_exit(&self) -> &cca_rsi_plane_exit { + &self.state.plane_run_ref().exit + } + + /// Set the value of the plane entry flags. + pub fn cca_set_entry_flags(&mut self, value: u64) { + self.cca_rsi_plane_entry().flags = value; + } + + /// Set the value of the plane entry PC. + pub fn cca_set_entry_pc(&mut self, value: u64) { + self.cca_rsi_plane_entry().pc = value; + } + + /// Set the value of the plane entry GPRs. + pub fn cca_set_entry_gprs(&mut self, values: [u64; RSI_PLANE_NR_GPRS]) { + self.cca_rsi_plane_entry().gprs = values; + } + + /// Set the value of the plane entry gicv3_hcr register. + pub fn cca_set_entry_gicv3_hcr(&mut self, value: u64) { + self.cca_rsi_plane_entry().gicv3_hcr = value; + } + + /// Set the value of the plane entry GIC v3 LRs. + pub fn cca_set_entry_gicv3_lrs(&mut self, values: [u64; RSI_PLANE_GIC_NUM_LRS]) { + self.cca_rsi_plane_entry().gicv3_lrs = values; + } + + /// Set the value of a single plane entry GPR. + fn cca_set_entry_gpr(&mut self, register: usize, value: u64) { + assert!(register < RSI_PLANE_NR_GPRS); + self.cca_rsi_plane_entry().gprs[register] = value; + } + + /// Get the value of a single plane entry GPR. + fn cca_get_entry_gpr(&self, register: usize) -> u64 { + assert!(register < RSI_PLANE_NR_GPRS); + self.cca_rsi_plane_exit().gprs[register] + } + + /// Flush the given value for a system register to the RMM. + pub fn cca_sysreg_write( + &mut self, + vtl: GuestVtl, + name: SystemReg, + value: u64, + ) -> Result<(), SetRegError> { + self.hcl + .rsi_sysreg_write(vtl, encode_rsi_sysreg(name), value) + } + + /// Update the address of the `plane_run` structure in `mshv_vtl_run.context`. + pub fn cca_set_plane_enter(&mut self) { + // SAFETY: the run page is exclusively accessed through `&mut self` while + // this VP is in VTL2, and the CCA runner uses `context` as a u64 + // physical address slot for the plane run page. + let plane_run: &mut u64 = unsafe { &mut *(&raw mut (*self.run.get()).context).cast() }; + *plane_run = self.state.plane_run_phys(); + } + + /// Set flag to enable trapping of SIMD operations in the lower VTL. + pub fn cca_plane_trap_simd(&mut self) { + let plane_run: &mut cca_rsi_plane_run = self.state.plane_run_mut(); + plane_run.entry.flags |= RSI_PLANE_ENTER_FLAGS_TRAP_SIMD; + } + + /// Unset flag that enables trapping of SIMD operations in lower VTL + /// (i.e., SIMD operations are not trapped). + pub fn cca_plane_no_trap_simd(&mut self) { + let plane_run: &mut cca_rsi_plane_run = self.state.plane_run_mut(); + plane_run.entry.flags &= !RSI_PLANE_ENTER_FLAGS_TRAP_SIMD; + } + + /// Set the default value for PSTATE for the lower VTL. + pub fn cca_set_default_pstate(&mut self) { + // SPSR_EL2_MODE_EL1h | SPSR_EL2_nRW_AARCH64 | SPSR_EL2_F_BIT | SPSR_EL2_I_BIT | SPSR_EL2_A_BIT | SPSR_EL2_D_BIT + self.cca_rsi_plane_entry().pstate = 0x3c5; + } +} + +// TODO CCA: this implementation is lifted from the aarch64 VBS implementation +// and might need more work to make it CCA-aligned. +impl<'a> super::BackingPrivate<'a> for Cca { + fn new(vp: &HclVp, sidecar: Option<&SidecarVp<'_>>, _hcl: &Hcl) -> Result { + assert!(sidecar.is_none()); + let super::BackingState::Cca { plane_run } = &vp.backing else { + unreachable!() + }; + let cca = Cca::new(plane_run); + + Ok(cca) + } + + fn try_set_reg( + runner: &mut ProcessorRunner<'a, Self>, + _vtl: GuestVtl, + name: HvRegisterName, + value: HvRegisterValue, + ) -> bool { + // Try to set the register in the CPU context, the fastest path. Only + // VTL-shared registers can be set this way: the CPU context only + // exposes the last VTL, and if we entered VTL2 on an interrupt, + // OpenHCL doesn't know what the last VTL is. + match name.into() { + HvArm64RegisterName::X0 + | HvArm64RegisterName::X1 + | HvArm64RegisterName::X2 + | HvArm64RegisterName::X3 + | HvArm64RegisterName::X4 + | HvArm64RegisterName::X5 + | HvArm64RegisterName::X6 + | HvArm64RegisterName::X7 + | HvArm64RegisterName::X8 + | HvArm64RegisterName::X9 + | HvArm64RegisterName::X10 + | HvArm64RegisterName::X11 + | HvArm64RegisterName::X12 + | HvArm64RegisterName::X13 + | HvArm64RegisterName::X14 + | HvArm64RegisterName::X15 + | HvArm64RegisterName::X16 + | HvArm64RegisterName::X17 + | HvArm64RegisterName::X18 + | HvArm64RegisterName::X19 + | HvArm64RegisterName::X20 + | HvArm64RegisterName::X21 + | HvArm64RegisterName::X22 + | HvArm64RegisterName::X23 + | HvArm64RegisterName::X24 + | HvArm64RegisterName::X25 + | HvArm64RegisterName::X26 + | HvArm64RegisterName::X27 + | HvArm64RegisterName::X28 + | HvArm64RegisterName::XFp + | HvArm64RegisterName::XLr => { + runner.cca_set_entry_gpr( + (name.0 - HvArm64RegisterName::X0.0) as usize, + value.as_u64(), + ); + true + } + _ => false, + } + } + + fn must_flush_regs_on(_runner: &ProcessorRunner<'a, Self>, _name: HvRegisterName) -> bool { + false + } + + fn try_get_reg( + runner: &ProcessorRunner<'a, Self>, + _vtl: GuestVtl, + name: HvRegisterName, + ) -> Option { + // Try to get the register from the CPU context, the fastest path. + // NOTE: for VBS x18 is omitted here as it is managed by the hypervisor, + // do we need to do the same here? + match name.into() { + HvArm64RegisterName::X0 + | HvArm64RegisterName::X1 + | HvArm64RegisterName::X2 + | HvArm64RegisterName::X3 + | HvArm64RegisterName::X4 + | HvArm64RegisterName::X5 + | HvArm64RegisterName::X6 + | HvArm64RegisterName::X7 + | HvArm64RegisterName::X8 + | HvArm64RegisterName::X9 + | HvArm64RegisterName::X10 + | HvArm64RegisterName::X11 + | HvArm64RegisterName::X12 + | HvArm64RegisterName::X13 + | HvArm64RegisterName::X14 + | HvArm64RegisterName::X15 + | HvArm64RegisterName::X16 + | HvArm64RegisterName::X17 + | HvArm64RegisterName::X18 + | HvArm64RegisterName::X19 + | HvArm64RegisterName::X20 + | HvArm64RegisterName::X21 + | HvArm64RegisterName::X22 + | HvArm64RegisterName::X23 + | HvArm64RegisterName::X24 + | HvArm64RegisterName::X25 + | HvArm64RegisterName::X26 + | HvArm64RegisterName::X27 + | HvArm64RegisterName::X28 + | HvArm64RegisterName::XFp + | HvArm64RegisterName::XLr => Some( + runner + .cca_get_entry_gpr((name.0 - HvArm64RegisterName::X0.0) as usize) + .into(), + ), + _ => None, + } + } + + fn flush_register_page(_runner: &mut ProcessorRunner<'a, Self>) {} +} + +/// Representation of the Realm config data available to Plane 0. +/// +/// * ipa_width is the size of the realm protected memory space +/// * hash_algo is the hash alg used for measurements +/// * num_aux_planes indicates how many low-privilege planes exist +/// * gicv3_vtr shows part of the GICv3 configuration for the realm, +/// needed for GIC virtualisation +#[derive(Debug, Clone, Copy)] +pub struct RsiRealmConfig { + ipa_width: u64, + #[expect(unused)] + hash_algo: u64, + #[expect(unused)] + num_aux_planes: u64, + #[expect(unused)] + gicv3_vtr: u64, +} + +impl RsiRealmConfig { + /// Get the IPA width of the realm + pub fn ipa_width(&self) -> u64 { + self.ipa_width + } +} + +impl From for RsiRealmConfig { + fn from(value: mshv_realm_config) -> Self { + RsiRealmConfig { + ipa_width: value.ipa_width, + hash_algo: value.algorithm, + num_aux_planes: value.num_aux_planes, + gicv3_vtr: value.gicv3_vtr, + } + } +} + +impl MshvVtl { + /// Get the realm-specific parameters from the RMM + pub fn get_realm_config(&self) -> Result { + let mut config = mshv_realm_config::default(); + + // SAFETY: Calling hcl_realm_config ioctl with the correct arguments. + unsafe { + hcl_realm_config(self.file.as_raw_fd(), &mut config) + .map_err(|_| Error::InvalidRegisterValue)?; + } + + Ok(config.into()) + } + + /// Write the value of a system register for the given VTL + pub fn rsi_sysreg_write( + &self, + vtl: GuestVtl, + sysreg: u64, + value: u64, + ) -> Result<(), SetRegError> { + let sysreg_write = mshv_rsi_sysreg_write { + vtl: vtl.into(), + sysreg, + value, + ..Default::default() + }; + + // SAFETY: Calling hcl_rsi_sysreg_write ioctl with the correct arguments. + unsafe { + hcl_rsi_sysreg_write(self.file.as_raw_fd(), &sysreg_write) + .map_err(SetRegError::Ioctl)?; + } + Ok(()) + } + + /// Assign given memory range to the VTL. + pub fn rsi_set_mem_perm(&self, vtl: GuestVtl, range: &MemoryRange) -> Result<(), HvError> { + let set_mem_perm = mshv_rsi_set_mem_perm { + plane: if vtl == GuestVtl::Vtl0 { + 1 + } else { + panic!("Invalid VTL") + }, + _pad: [0; 7], + base_addr: range.start(), + top_addr: range.end(), + }; + + // SAFETY: Calling hcl_rsi_set_mem_perm ioctl with the correct arguments. + unsafe { + hcl_rsi_set_mem_perm(self.file.as_raw_fd(), &set_mem_perm) + .map_err(|_| HvError::InvalidRegisterValue)?; + } + Ok(()) + } +} + +impl Hcl { + /// Gets Realm config + pub fn get_realm_config(&self) -> Result { + self.mshv_vtl.get_realm_config() + } + + /// sets system registers through rsi calls + pub fn rsi_sysreg_write( + &self, + vtl: GuestVtl, + sysreg: u64, + value: u64, + ) -> Result<(), SetRegError> { + self.mshv_vtl.rsi_sysreg_write(vtl, sysreg, value) + } + + /// setting memory permissions + pub fn rsi_set_mem_perm(&self, vtl: GuestVtl, range: MemoryRange) -> Result<(), HvError> { + self.mshv_vtl.rsi_set_mem_perm(vtl, &range) + } +} diff --git a/openhcl/hcl/src/ioctl/register.rs b/openhcl/hcl/src/ioctl/register.rs index ed963bb1c1..38b2477d19 100644 --- a/openhcl/hcl/src/ioctl/register.rs +++ b/openhcl/hcl/src/ioctl/register.rs @@ -358,6 +358,14 @@ impl Hcl { .with_intercept_page_available(caps.intercept_page_available()) .with_dr6_shared(true) .with_proxy_interrupt_redirect_available(caps.proxy_interrupt_redirect_available()), + // TODO: CCA: figure out what capabilities to enable here? + // TMK seems to work without any. + IsolationType::Cca => { + tracing::info!( + "cca: get_vsm_capabilities is not implemented and returning empty set now" + ); + hvdef::HvRegisterVsmCapabilities::new() + } }; assert_eq!(caps.dr6_shared(), self.dr6_shared()); @@ -399,6 +407,10 @@ impl Hcl { #[cfg(guest_arch = "aarch64")] { + if self.isolation.is_hardware_isolated() { + return Ok(hvdef::HvPartitionPrivilege::default()); + } + Ok(hvdef::HvPartitionPrivilege::from( self.get_partition_vtl2_register(HvArchRegisterName::PrivilegesAndFeaturesInfo)? .as_u64(), diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 08f6ab0169..b54f183346 100644 --- a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs +++ b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs @@ -99,6 +99,8 @@ open_enum! { TVM_REPORT = 3, /// TDX report TDX_VM_REPORT = 4, + /// CCA report + CCA_VM_REPORT = 5, } } diff --git a/openhcl/openhcl_boot/src/arch/x86_64/memory.rs b/openhcl/openhcl_boot/src/arch/x86_64/memory.rs index fbe9fe5bee..2a964372a2 100644 --- a/openhcl/openhcl_boot/src/arch/x86_64/memory.rs +++ b/openhcl/openhcl_boot/src/arch/x86_64/memory.rs @@ -87,7 +87,7 @@ pub fn setup_vtl2_memory( IsolationType::Snp | IsolationType::Tdx => Some(init_local_map( loader_defs::paravisor::PARAVISOR_LOCAL_MAP_VA, )), - IsolationType::None | IsolationType::Vbs => None, + _ => None, }; // Make sure imported regions are in increasing order. diff --git a/openhcl/openhcl_boot/src/dt.rs b/openhcl/openhcl_boot/src/dt.rs index 9baa6d014f..5262d245a5 100644 --- a/openhcl/openhcl_boot/src/dt.rs +++ b/openhcl/openhcl_boot/src/dt.rs @@ -516,6 +516,7 @@ pub fn write_dt( IsolationType::Vbs => "vbs", IsolationType::Snp => "snp", IsolationType::Tdx => "tdx", + IsolationType::Cca => "cca", }; openhcl_builder = openhcl_builder.add_str(p_isolation_type, isolation_type)?; diff --git a/openhcl/openhcl_boot/src/host_params/shim_params.rs b/openhcl/openhcl_boot/src/host_params/shim_params.rs index 5db8419688..851df5ca6b 100644 --- a/openhcl/openhcl_boot/src/host_params/shim_params.rs +++ b/openhcl/openhcl_boot/src/host_params/shim_params.rs @@ -19,6 +19,9 @@ pub enum IsolationType { Snp, #[cfg_attr(target_arch = "aarch64", expect(dead_code))] Tdx, + // There is no usage of this yet + #[expect(dead_code)] + Cca, } impl IsolationType { @@ -28,6 +31,7 @@ impl IsolationType { IsolationType::Vbs => false, IsolationType::Snp => true, IsolationType::Tdx => true, + IsolationType::Cca => true, } } } diff --git a/openhcl/tee_call/src/lib.rs b/openhcl/tee_call/src/lib.rs index 61a272fa95..543d47e4b3 100644 --- a/openhcl/tee_call/src/lib.rs +++ b/openhcl/tee_call/src/lib.rs @@ -53,6 +53,8 @@ pub enum TeeType { Snp, /// Intel TDX Tdx, + /// ARM CCA + Cca, /// Virtualization-based Security (VBS) Vbs, } diff --git a/openhcl/underhill_attestation/src/igvm_attest/mod.rs b/openhcl/underhill_attestation/src/igvm_attest/mod.rs index bf5b46dea2..ec7fc3b37c 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/mod.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/mod.rs @@ -72,6 +72,8 @@ pub enum ReportType { Snp, /// TDX report Tdx, + /// CCA report + Cca, /// Trusted VM report Tvm, } @@ -83,6 +85,7 @@ impl ReportType { Self::Vbs => IgvmAttestReportType::VBS_VM_REPORT, Self::Snp => IgvmAttestReportType::SNP_VM_REPORT, Self::Tdx => IgvmAttestReportType::TDX_VM_REPORT, + Self::Cca => IgvmAttestReportType::CCA_VM_REPORT, Self::Tvm => IgvmAttestReportType::TVM_REPORT, } } @@ -115,6 +118,7 @@ impl IgvmAttestRequestHelper { let report_type = match tee_type { TeeType::Snp => ReportType::Snp, TeeType::Tdx => ReportType::Tdx, + TeeType::Cca => ReportType::Cca, TeeType::Vbs => ReportType::Vbs, }; @@ -151,6 +155,7 @@ impl IgvmAttestRequestHelper { let report_type = match tee_type { Some(TeeType::Snp) => ReportType::Snp, Some(TeeType::Tdx) => ReportType::Tdx, + Some(TeeType::Cca) => ReportType::Cca, Some(TeeType::Vbs) => ReportType::Vbs, None => ReportType::Tvm, }; @@ -330,6 +335,7 @@ fn get_report_size(report_type: &ReportType) -> usize { ReportType::Snp => openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE, ReportType::Tdx => openhcl_attestation_protocol::igvm_attest::get::TDX_VM_REPORT_SIZE, ReportType::Tvm => openhcl_attestation_protocol::igvm_attest::get::TVM_REPORT_SIZE, + ReportType::Cca => todo!(), } } diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index dbb1205083..cab8157432 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -246,6 +246,8 @@ pub enum AttestationType { Tdx, /// Use the VBS TEE for attestation. Vbs, + /// Use the CCA TEE for attestation, + Cca, /// Use trusted host-based attestation. Host, } diff --git a/openhcl/underhill_core/src/emuplat/tpm.rs b/openhcl/underhill_core/src/emuplat/tpm.rs index 23ff174470..a34d4bdf64 100644 --- a/openhcl/underhill_core/src/emuplat/tpm.rs +++ b/openhcl/underhill_core/src/emuplat/tpm.rs @@ -63,6 +63,7 @@ impl RequestAkCert for TpmRequestAkCertHelper { let tee_type = match self.attestation_type { AttestationType::Snp => Some(tee_call::TeeType::Snp), AttestationType::Tdx => Some(tee_call::TeeType::Tdx), + AttestationType::Cca => Some(tee_call::TeeType::Cca), AttestationType::Vbs => Some(tee_call::TeeType::Vbs), AttestationType::Host => None, }; @@ -228,6 +229,7 @@ pub mod resources { AttestationType::Snp => Some(Arc::new(tee_call::SnpCall)), AttestationType::Tdx => Some(Arc::new(tee_call::TdxCall)), AttestationType::Vbs => Some(Arc::new(tee_call::VbsCall)), + AttestationType::Cca => todo!(), AttestationType::Host => None, }; diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 9eb32a722c..4b3bd34b55 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -1858,7 +1858,7 @@ async fn new_underhill_vm( // Construct the underhill partition instance. This contains much of the configuration of the guest deposited by // the host, along with additional device configuration and transports. - let params = UhPartitionNewParams { + let mut params = UhPartitionNewParams { lower_vtl_memory_layout: &mem_layout, isolation, topology: &processor_topology, @@ -1874,7 +1874,7 @@ async fn new_underhill_vm( disable_lower_vtl_timer_virt: env_cfg.disable_lower_vtl_timer_virt, }; - let proto_partition = UhProtoPartition::new(params, |cpu| tp.driver(cpu).clone()) + let proto_partition = UhProtoPartition::new(&mut params, |cpu| tp.driver(cpu).clone()) .context("failed to create prototype partition")?; let gm = underhill_mem::init(&underhill_mem::Init { @@ -2002,6 +2002,7 @@ async fn new_underhill_vm( virt::IsolationType::Snp => Some(Box::new(tee_call::SnpCall)), virt::IsolationType::Tdx => Some(Box::new(tee_call::TdxCall)), virt::IsolationType::Vbs => Some(Box::new(tee_call::VbsCall)), + virt::IsolationType::Cca => todo!(), virt::IsolationType::None => None, }; @@ -2877,6 +2878,7 @@ async fn new_underhill_vm( virt::IsolationType::Snp => AttestationType::Snp, virt::IsolationType::Tdx => AttestationType::Tdx, virt::IsolationType::Vbs => AttestationType::Vbs, + virt::IsolationType::Cca => AttestationType::Cca, virt::IsolationType::None => AttestationType::Host, }; @@ -2904,6 +2906,7 @@ async fn new_underhill_vm( .attempt_ak_cert_callback() }), ), + AttestationType::Cca => todo!(), } }; diff --git a/openhcl/underhill_mem/Cargo.toml b/openhcl/underhill_mem/Cargo.toml index aed2f632ea..317d2b8bf9 100644 --- a/openhcl/underhill_mem/Cargo.toml +++ b/openhcl/underhill_mem/Cargo.toml @@ -17,6 +17,7 @@ virt.workspace = true virt_mshv_vtl.workspace = true vm_topology.workspace = true x86defs.workspace = true +rsi.workspace = true cvm_tracing.workspace = true inspect.workspace = true diff --git a/openhcl/underhill_mem/src/init.rs b/openhcl/underhill_mem/src/init.rs index 214a8aa738..0b0a866c8c 100644 --- a/openhcl/underhill_mem/src/init.rs +++ b/openhcl/underhill_mem/src/init.rs @@ -290,9 +290,9 @@ pub async fn init(params: &Init<'_>) -> anyhow::Result { // accessors, or something. 0 } - IsolationType::Snp => { - // SNP has two mappings for each shared page: one below and one - // above VTOM. So, unlike for TDX, for SNP we could choose to + IsolationType::Snp | IsolationType::Cca => { + // SNP and CCA have two mappings for each shared page: one below + // and one above VTOM. So, unlike for TDX, we could choose to // register memory twice, allowing the kernel to operate on // either shared or encrypted memory. But, for consistency with // TDX, just register the shared mapping. diff --git a/openhcl/underhill_mem/src/lib.rs b/openhcl/underhill_mem/src/lib.rs index d49166c7c1..6e3c19efe9 100644 --- a/openhcl/underhill_mem/src/lib.rs +++ b/openhcl/underhill_mem/src/lib.rs @@ -42,6 +42,7 @@ use memory_range::MemoryRange; use parking_lot::Mutex; use parking_lot::MutexGuard; use registrar::RegisterMemory; +use rsi::CcaMemPermIndex; use std::collections::VecDeque; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -121,6 +122,9 @@ enum GpaVtlPermissions { Vbs(HvMapGpaFlags), Snp(SevRmpAdjust), Tdx(TdgMemPageGpaAttr, TdgMemPageAttrWriteR8), + // TODO: CCA: we need to use the 'vtl' and 'protections' below to get the correct index + // This implies that we've set up the index list properly, and we just select the right one here + Cca(CcaMemPermIndex), } impl GpaVtlPermissions { @@ -139,6 +143,11 @@ impl GpaVtlPermissions { vtl_permissions.set(vtl, protections); vtl_permissions } + IsolationType::Cca => { + let mut vtl_permissions = GpaVtlPermissions::Cca(CcaMemPermIndex::default()); + vtl_permissions.set(vtl, protections); + vtl_permissions + } } } @@ -182,6 +191,9 @@ impl GpaVtlPermissions { *attributes = new_attributes; *mask = new_mask; } + GpaVtlPermissions::Cca(_index) => { + tracing::warn!("cca: GpaVtlPermissions::set is doing nothing now"); + } } } } @@ -249,6 +261,10 @@ impl MemoryAcceptor { .tdx_accept_pages(range, Some((attributes, mask))) .map_err(|err| AcceptPagesError::Tdx { error: err, range }) } + IsolationType::Cca => { + // TODO: CCA: do we need to set RIPAS here? + Ok(()) + } } } @@ -275,6 +291,9 @@ impl MemoryAcceptor { IsolationType::Tdx => { // Nothing to do for TDX. } + IsolationType::Cca => { + // TODO: CCA: anything to do here? + } } } @@ -343,6 +362,13 @@ impl MemoryAcceptor { vtl: vtl.into(), }) } + GpaVtlPermissions::Cca(_index) => self + .mshv_vtl + .rsi_set_mem_perm(GuestVtl::Vtl0, &range) + .map_err(|_err| ApplyVtlProtectionsError::Cca { + range, + vtl: vtl.into(), + }), } } } diff --git a/openhcl/virt_mshv_vtl/Cargo.toml b/openhcl/virt_mshv_vtl/Cargo.toml index 0be8fcf3af..2b64204291 100644 --- a/openhcl/virt_mshv_vtl/Cargo.toml +++ b/openhcl/virt_mshv_vtl/Cargo.toml @@ -26,6 +26,7 @@ sidecar_client.workspace = true vmcore.workspace = true x86defs.workspace = true x86emu.workspace = true +rsi.workspace = true atomic_ringbuf.workspace = true cvm_tracing.workspace = true diff --git a/openhcl/virt_mshv_vtl/src/lib.rs b/openhcl/virt_mshv_vtl/src/lib.rs index 9edf06717e..af512b76e4 100755 --- a/openhcl/virt_mshv_vtl/src/lib.rs +++ b/openhcl/virt_mshv_vtl/src/lib.rs @@ -12,35 +12,45 @@ mod devmsr; cfg_if::cfg_if!( if #[cfg(guest_arch = "x86_64")] { mod cvm_cpuid; + + pub use crate::processor::mshv::x64::HypervisorBackedX86 as HypervisorBacked; pub use processor::snp::SnpBacked; pub use processor::tdx::TdxBacked; - use crate::processor::HardwareIsolatedBacking; - pub use crate::processor::mshv::x64::HypervisorBackedX86 as HypervisorBacked; - use crate::processor::mshv::x64::HypervisorBackedX86Shared as HypervisorBackedShared; + use bitvec::prelude::BitArray; use bitvec::prelude::Lsb0; + use crate::processor::mshv::x64::HypervisorBackedX86Shared as HypervisorBackedShared; use devmsr::MsrDevice; - use hv1_emulator::hv::ProcessorVtlHv; use processor::LapicState; use processor::snp::SnpBackedShared; use processor::tdx::TdxBackedShared; use std::arch::x86_64::CpuidResult; use virt::CpuidLeaf; + use virt::X86Partition; use virt::state::StateElement; use virt::vp::MpState; + use virt_support_apic::LocalApicSet; + /// Bitarray type for representing IRR bits in a x86-64 APIC /// Each bit represent the 256 possible vectors. type IrrBitmap = BitArray<[u32; 8], Lsb0>; } else if #[cfg(guest_arch = "aarch64")] { pub use crate::processor::mshv::arm64::HypervisorBackedArm64 as HypervisorBacked; + pub use processor::cca::CcaBacked; + + use aarch64defs::Vendor; use crate::processor::mshv::arm64::HypervisorBackedArm64Shared as HypervisorBackedShared; + use processor::cca::CcaBackedShared; + use safe_intrinsics::read_cntfrq_el0; } ); mod processor; +use hv1_emulator::hv::ProcessorVtlHv; pub use processor::Backing; pub use processor::UhProcessor; +use crate::processor::HardwareIsolatedBacking; use anyhow::Context as AnyhowContext; use bitfield_struct::bitfield; use bitvec::boxed::BitBox; @@ -105,11 +115,9 @@ use user_driver::DmaClient; use virt::IsolationType; use virt::PartitionCapabilities; use virt::VpIndex; -use virt::X86Partition; use virt::irqcon::IoApicRouting; use virt::irqcon::MsiRequest; use virt::x86::apic_software_device::ApicSoftwareDevices; -use virt_support_apic::LocalApicSet; use vm_topology::memory::MemoryLayout; use vm_topology::processor::ProcessorTopology; use vm_topology::processor::TargetVpInfo; @@ -255,6 +263,8 @@ enum BackingShared { Snp(#[inspect(flatten)] SnpBackedShared), #[cfg(guest_arch = "x86_64")] Tdx(#[inspect(flatten)] TdxBackedShared), + #[cfg(guest_arch = "aarch64")] + Cca(#[inspect(flatten)] Box), } impl BackingShared { @@ -281,7 +291,10 @@ impl BackingShared { partition_params, backing_shared_params, )?), - #[cfg(not(guest_arch = "x86_64"))] + #[cfg(guest_arch = "aarch64")] + IsolationType::Cca => { + BackingShared::Cca(Box::new(CcaBackedShared::new(backing_shared_params)?)) + } _ => unreachable!(), }) } @@ -292,6 +305,8 @@ impl BackingShared { #[cfg(guest_arch = "x86_64")] BackingShared::Snp(SnpBackedShared { cvm, .. }) | BackingShared::Tdx(TdxBackedShared { cvm, .. }) => Some(cvm), + #[cfg(guest_arch = "aarch64")] + BackingShared::Cca(s) => Some(&s.cvm), } } @@ -302,6 +317,8 @@ impl BackingShared { BackingShared::Snp(_) => None, #[cfg(guest_arch = "x86_64")] BackingShared::Tdx(s) => s.untrusted_synic.as_ref(), + #[cfg(guest_arch = "aarch64")] + BackingShared::Cca(_) => None, } } } @@ -368,7 +385,6 @@ impl GuestVsmVpState { } } -#[cfg(guest_arch = "x86_64")] #[derive(Inspect)] /// VP state for CVMs. struct UhCvmVpState { @@ -381,17 +397,18 @@ struct UhCvmVpState { hv: VtlArray, /// LAPIC state. #[inspect(safe)] + #[cfg(guest_arch = "x86_64")] lapics: VtlArray, /// Guest VSM state for this vp. Some when VTL 1 is enabled. + #[cfg(guest_arch = "x86_64")] vtl1: Option, } -#[cfg(guest_arch = "x86_64")] impl UhCvmVpState { /// Creates a new CVM VP state. pub(crate) fn new( cvm_partition: &UhCvmPartitionState, - inner: &UhPartitionInner, + #[cfg_attr(guest_arch = "aarch64", expect(unused_variables))] inner: &UhPartitionInner, vp_info: &TargetVpInfo, overlay_pages_required: usize, ) -> Result { @@ -400,8 +417,9 @@ impl UhCvmVpState { .allocate_dma_buffer(overlay_pages_required * HV_PAGE_SIZE as usize) .map_err(Error::AllocateSharedVisOverlay)?; - let apic_base = virt::vp::Apic::at_reset(&inner.caps, vp_info).apic_base; + #[cfg(guest_arch = "x86_64")] let lapics = VtlArray::from_fn(|vtl| { + let apic_base = virt::vp::Apic::at_reset(&inner.caps, vp_info).apic_base; let apic_set = &cvm_partition.lapic[vtl]; // The APIC is software-enabled after reset for secure VTLs, to @@ -424,13 +442,14 @@ impl UhCvmVpState { direct_overlay_handle, exit_vtl: GuestVtl::Vtl0, hv, + #[cfg(guest_arch = "x86_64")] lapics, + #[cfg(guest_arch = "x86_64")] vtl1: None, }) } } -#[cfg(guest_arch = "x86_64")] #[derive(Inspect, Default)] #[inspect(hex)] /// Configuration of VTL 1 registration for intercepts on certain registers @@ -467,9 +486,9 @@ struct UhCvmPartitionState { #[inspect(with = "inspect::iter_by_index")] vps: Vec, shared_memory: GuestMemory, - #[cfg_attr(guest_arch = "aarch64", expect(dead_code))] #[inspect(skip)] isolated_memory_protector: Arc, + #[cfg(guest_arch = "x86_64")] /// The emulated local APIC set. lapic: VtlArray, /// The emulated hypervisor state. @@ -581,7 +600,6 @@ struct TscReferenceTimeSource { tsc_scale: u64, } -#[cfg_attr(guest_arch = "aarch64", expect(dead_code))] impl TscReferenceTimeSource { fn new(tsc_frequency: u64) -> Self { TscReferenceTimeSource { @@ -767,7 +785,6 @@ struct TlbLockInfo { sleeping: AtomicBool, } -#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))] impl TlbLockInfo { fn new(vp_count: usize) -> Self { Self { @@ -793,10 +810,12 @@ struct WakeReason { impl WakeReason { // Convenient constants. + #[cfg(guest_arch = "x86_64")] const EXTINT: Self = Self::new().with_extint(true); const MESSAGE_QUEUES: Self = Self::new().with_message_queues(true); #[cfg(guest_arch = "x86_64")] const HV_START_ENABLE_VP_VTL: Self = Self::new().with_hv_start_enable_vtl_vp(true); // StartVp/EnableVpVtl handling + #[cfg(guest_arch = "x86_64")] const INTCON: Self = Self::new().with_intcon(true); #[cfg(guest_arch = "x86_64")] const UPDATE_PROXY_IRR_FILTER: Self = Self::new().with_update_proxy_irr_filter(true); @@ -835,6 +854,10 @@ impl UhPartition { | BackingShared::Tdx(TdxBackedShared { cvm, .. }) => { revoke(&mut *cvm.guest_vsm.write())?; } + #[cfg(guest_arch = "aarch64")] + BackingShared::Cca(s) => { + revoke(&mut *s.cvm.guest_vsm.write())?; + } }; Ok(()) @@ -872,6 +895,7 @@ impl virt::Partition for UhPartition { } } +#[cfg(guest_arch = "x86_64")] impl X86Partition for UhPartition { fn ioapic_routing(&self) -> Arc { self.inner.clone() @@ -902,6 +926,7 @@ impl UhPartitionInner { self.vps.get(index.index() as usize) } + #[cfg(guest_arch = "x86_64")] fn lapic(&self, vtl: GuestVtl) -> Option<&LocalApicSet> { self.backing_shared.cvm_state().map(|x| &x.lapic[vtl]) } @@ -1322,6 +1347,7 @@ impl pci_core::msi::SignalMsi for UhInterruptTarget { } impl UhPartitionInner { + #[cfg(guest_arch = "x86_64")] fn request_msi(&self, vtl: GuestVtl, request: MsiRequest) { if let Some(lapic) = self.lapic(vtl) { tracing::trace!(?request, "interrupt"); @@ -1346,6 +1372,20 @@ impl UhPartitionInner { } } } + + #[cfg(guest_arch = "aarch64")] + fn request_msi(&self, vtl: GuestVtl, request: MsiRequest) { + match self.isolation { + IsolationType::Cca => { + tracelimit::warn_ratelimited!( + ?vtl, + ?request, + "ignoring MSI request for CCA on aarch64: MSI routing is not implemented" + ); + } + _ => unimplemented!("MSI routing is not implemented on aarch64"), + } + } } impl IoApicRouting for UhPartitionInner { @@ -1383,6 +1423,10 @@ fn is_restore_partition_time_available() -> bool { /// Configure the [`hvdef::HvRegisterVsmPartitionConfig`] register with the /// values used by underhill. fn set_vtl2_vsm_partition_config(hcl: &Hcl) -> Result<(), Error> { + if hcl.isolation() == hcl::ioctl::IsolationType::Cca { + tracing::warn!("cca: set_vtl2_vsm_partition_config: do nothing now"); + return Ok(()); + } // Read available capabilities to determine what to enable. let caps = hcl.get_vsm_capabilities().map_err(Error::GetReg)?; let hardware_isolated = hcl.isolation().is_hardware_isolated(); @@ -1407,6 +1451,7 @@ fn set_vtl2_vsm_partition_config(hcl: &Hcl) -> Result<(), Error> { /// Configuration parameters supplied to [`UhProtoPartition::new`]. /// /// These do not include runtime resources. +#[derive(Clone, Copy)] pub struct UhPartitionNewParams<'a> { /// The isolation type for the partition. pub isolation: IsolationType, @@ -1598,7 +1643,7 @@ impl<'a> UhProtoPartition<'a> { /// `driver(cpu)` returns the driver to use for polling the sidecar device /// whose base CPU is `cpu`. pub fn new( - params: UhPartitionNewParams<'a>, + params: &mut UhPartitionNewParams<'a>, driver: impl FnMut(u32) -> T, ) -> Result { let hcl_isolation = match params.isolation { @@ -1606,6 +1651,7 @@ impl<'a> UhProtoPartition<'a> { IsolationType::Vbs => hcl::ioctl::IsolationType::Vbs, IsolationType::Snp => hcl::ioctl::IsolationType::Snp, IsolationType::Tdx => hcl::ioctl::IsolationType::Tdx, + IsolationType::Cca => hcl::ioctl::IsolationType::Cca, }; // Try to open the sidecar device, if it is present. @@ -1681,12 +1727,22 @@ impl<'a> UhProtoPartition<'a> { } .build() .map_err(Error::CvmCpuid)?, - IsolationType::Vbs | IsolationType::None => Default::default(), + IsolationType::Vbs | IsolationType::None => virt::CpuidLeafSet::new(Vec::new()), + // Eliminate 'non-exhaustive patterns' compilation warning, we shouldn't reach here for + // any arm64 types. + IsolationType::Cca => unreachable!(), }; + #[cfg(guest_arch = "aarch64")] + if params.isolation == IsolationType::Cca && params.vtom.is_none() { + // Query vtom from realm config. + let realm_config = hcl.get_realm_config().map_err(Error::Hcl)?; + params.vtom = Some(1_u64 << (realm_config.ipa_width() - 1)); + } + Ok(UhProtoPartition { hcl, - params, + params: *params, guest_vsm_available, create_partition_available: privs.create_partitions(), #[cfg(guest_arch = "x86_64")] @@ -1845,12 +1901,13 @@ impl<'a> UhProtoPartition<'a> { } }; + // TODO: CCA: will probably need to add some support here like above #[cfg(guest_arch = "aarch64")] let software_devices = None; #[cfg(guest_arch = "aarch64")] let caps = virt::aarch64::Aarch64PartitionCapabilities { - // TODO: query aarch32 support from the hypervisor. + vendor: Vendor::ARM, supports_aarch32_el0: false, }; @@ -1888,24 +1945,29 @@ impl<'a> UhProtoPartition<'a> { .expect("registering synic intercept cannot fail"); } - #[cfg(guest_arch = "x86_64")] - let cvm_state = if is_hardware_isolated { - let vsm_caps = hcl.get_vsm_capabilities().map_err(Error::GetReg)?; - let proxy_interrupt_redirect_available = - vsm_caps.proxy_interrupt_redirect_available() && !params.disable_proxy_redirect; - - Some(Self::construct_cvm_state( + let cvm_state = match isolation { + IsolationType::Snp | IsolationType::Tdx => { + let vsm_caps = hcl.get_vsm_capabilities().map_err(Error::GetReg)?; + let proxy_interrupt_redirect_available = + vsm_caps.proxy_interrupt_redirect_available() && !params.disable_proxy_redirect; + + Some(Self::construct_cvm_state( + ¶ms, + late_params.cvm_params.unwrap(), + &caps, + guest_vsm_available, + proxy_interrupt_redirect_available, + )?) + } + IsolationType::Cca => Some(Self::construct_cvm_state( ¶ms, late_params.cvm_params.unwrap(), &caps, guest_vsm_available, - proxy_interrupt_redirect_available, - )?) - } else { - None + false, + )?), + IsolationType::Vbs | IsolationType::None => None, }; - #[cfg(guest_arch = "aarch64")] - let cvm_state = None; let lower_vtl_timer_virt_available = hcl.supports_lower_vtl_timer_virt() && !params.disable_lower_vtl_timer_virt; @@ -2083,6 +2145,19 @@ impl UhPartitionInner { &mut TdxBacked::tlb_flush_lock_access(None, self, tdx_backed_shared), ) .map_err(|e| anyhow::anyhow!(e)), + #[cfg(guest_arch = "aarch64")] + BackingShared::Cca(cca_backed_shared) => cca_backed_shared + .cvm + .isolated_memory_protector + .register_overlay_page( + vtl, + gpn, + GpnSource::Dma, + HvMapGpaFlags::new(), + Some(new_perms), + &mut CcaBacked::tlb_flush_lock_access(None, self, cca_backed_shared), + ) + .map_err(|e| anyhow::anyhow!(e)), BackingShared::Hypervisor(_) => { let _ = (vtl, gpn, new_perms); unreachable!() @@ -2118,6 +2193,16 @@ impl UhPartitionInner { let _ = (vtl, gpn); unreachable!() } + #[cfg(guest_arch = "aarch64")] + BackingShared::Cca(cca_backed_shared) => cca_backed_shared + .cvm + .isolated_memory_protector + .unregister_overlay_page( + vtl, + gpn, + &mut CcaBacked::tlb_flush_lock_access(None, self, cca_backed_shared), + ) + .map_err(|e| anyhow::anyhow!(e)), } } } @@ -2136,7 +2221,6 @@ impl UhProtoPartition<'_> { Ok(guest_vsm_config.maximum_vtl() >= u8::from(GuestVtl::Vtl1)) } - #[cfg(guest_arch = "x86_64")] /// Constructs partition-wide CVM state. fn construct_cvm_state( params: &UhPartitionNewParams<'_>, @@ -2160,6 +2244,7 @@ impl UhProtoPartition<'_> { let tlb_locked_vps = VtlArray::from_fn(|_| BitVec::repeat(false, vp_count).into_boxed_bitslice()); + #[cfg(guest_arch = "x86_64")] let lapic = VtlArray::from_fn(|_| { LocalApicSet::builder() .x2apic_capable(caps.x2apic) @@ -2185,11 +2270,13 @@ impl UhProtoPartition<'_> { }); Ok(UhCvmPartitionState { + #[cfg(guest_arch = "x86_64")] vps_per_socket: params.topology.reserved_vps_per_socket(), tlb_locked_vps, vps, shared_memory: late_params.shared_gm, isolated_memory_protector: late_params.isolated_memory_protector, + #[cfg(guest_arch = "x86_64")] lapic, hv, guest_vsm: RwLock::new(GuestVsmState::from_availability(guest_vsm_available)), @@ -2296,19 +2383,23 @@ impl UhPartition { } } -#[cfg(guest_arch = "x86_64")] /// Gets the TSC frequency for the current platform. fn get_tsc_frequency(isolation: IsolationType) -> Result { // Always get the frequency from the hypervisor. It's believed that, as long // as the hypervisor is behaving, it will provide the most precise and accurate frequency. - let msr = MsrDevice::new(0).map_err(Error::OpenMsr)?; - let hv_frequency = msr - .read_msr(hvdef::HV_X64_MSR_TSC_FREQUENCY) - .map_err(Error::ReadTscFrequency)?; + #[cfg(guest_arch = "x86_64")] + let hv_frequency = { + let msr = MsrDevice::new(0).map_err(Error::OpenMsr)?; + msr.read_msr(hvdef::HV_X64_MSR_TSC_FREQUENCY) + .map_err(Error::ReadTscFrequency)? + }; + #[cfg(guest_arch = "aarch64")] + let hv_frequency = read_cntfrq_el0(); // Get the hardware-advertised frequency and validate that the // hypervisor frequency is not too far off. - let hw_info = match isolation { + let hw_info: Option<(u64, u64)> = match isolation { + #[cfg(guest_arch = "x86_64")] IsolationType::Tdx => { // TDX provides the TSC frequency via cpuid. let max_function = @@ -2336,11 +2427,17 @@ fn get_tsc_frequency(isolation: IsolationType) -> Result { allowed_error, )) } + #[cfg(not(guest_arch = "x86_64"))] + IsolationType::Tdx => None, IsolationType::Snp => { // SNP currently does not provide the frequency. None } IsolationType::Vbs | IsolationType::None => None, + IsolationType::Cca => { + // CCA currently does not provide the frequency. + None + } }; if let Some((hw_frequency, allowed_error)) = hw_info { diff --git a/openhcl/virt_mshv_vtl/src/processor/cca/mod.rs b/openhcl/virt_mshv_vtl/src/processor/cca/mod.rs new file mode 100644 index 0000000000..8269eb561c --- /dev/null +++ b/openhcl/virt_mshv_vtl/src/processor/cca/mod.rs @@ -0,0 +1,675 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Processor support for CCA Planes. + +use super::BackingSharedParams; +use super::HardwareIsolatedBacking; +use super::UhProcessor; +use super::private::BackingPrivate; +use super::vp_state; +use super::vp_state::UhVpStateAccess; +use crate::BackingShared; +use crate::Error; +use crate::TlbFlushLockAccess; +use crate::UhCvmPartitionState; +use crate::UhCvmVpState; +use crate::UhPartitionInner; +use crate::processor::InterceptMessageState; +use aarch64defs::EsrEl2; +use aarch64defs::SystemReg; +use hcl::GuestVtl; +use hcl::ioctl::cca::Cca; +use hcl::ioctl::register; +use hv1_emulator::hv::ProcessorVtlHv; +use hv1_emulator::synic::ProcessorSynic; +use hv1_structs::VtlArray; +use hvdef::HvRegisterCrInterceptControl; +use inspect::Inspect; +use inspect::InspectMut; +use rsi::cca_rsi_plane_exit; +use virt::VpHaltReason; +use virt::VpIndex; +use virt::aarch64::vp; +use virt::aarch64::vp::AccessVpState; +use virt::io::CpuIo; +use virt_support_aarch64emu::translate::TranslationRegisters; +use zerocopy::FromZeros; + +#[derive(Debug, Error)] +#[error("failed to run")] +struct CcaRunVpError(#[source] hcl::ioctl::Error); + +// For use with Hyper-V synthetic interrupt controller allocated by paravisor. +enum UhDirectOverlay { + #[expect(unused)] + Sipp, + #[expect(unused)] + Sifp, + Count, +} + +/// Backing for CCA planes. +#[derive(InspectMut)] +pub struct CcaBacked { + vtls: VtlArray, + cvm: UhCvmVpState, +} + +#[derive(Clone, Copy, InspectMut, Inspect)] +struct CcaVtl { + // TODO: CCA: potentially needed fields, based on TDX implementation: + // * values of control registers + // * interrupt information + // * exception error code + // * TLB flush state + // * PMU stats + sp_el0: u64, + sp_el1: u64, + cpsr: u64, +} + +impl CcaVtl { + pub(crate) fn new() -> Self { + Self { + sp_el0: 0, + sp_el1: 0, + cpsr: 0, + } + } +} + +#[derive(Inspect)] +pub struct CcaBackedShared { + pub(crate) cvm: UhCvmPartitionState, +} + +impl CcaBackedShared { + pub(crate) fn new(params: BackingSharedParams<'_>) -> Result { + Ok(Self { + cvm: params.cvm_state.unwrap(), + }) + } +} + +/// Types of exceptions that can occur in the CCA plane, +/// and get reported back to use from the RMM. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +enum ExceptionClass { + DataAbort, + InstructionAbort, + SimdAccess, + SmcError, +} + +impl From for ExceptionClass { + fn from(value: u8) -> Self { + match value { + 0b0010_0100 => ExceptionClass::DataAbort, + 0b0010_0000 => ExceptionClass::InstructionAbort, + 0b0000_0111 => ExceptionClass::SimdAccess, + 0b0001_0111 => ExceptionClass::SmcError, + _ => panic!("Unknown exception class: {value}"), + } + } +} + +/// The reason for a CCA plane exit, which can be either a synchronous event +/// (like an MMIO access or an exception) or an IRQ. +#[derive(Debug, Clone, Copy)] +enum PlaneExitReason { + Sync, + Irq, +} + +impl From for PlaneExitReason { + fn from(value: u64) -> Self { + match value { + 0 => PlaneExitReason::Sync, + 1 => PlaneExitReason::Irq, + _ => panic!("Unknown CCA plane exit reason: {value}"), + } + } +} + +/// A wrapper around the CCA RSI plane exit structure, providing methods to +/// access information regarding the exit of the plane. +struct CcaExit<'a>(&'a cca_rsi_plane_exit); + +impl<'a> CcaExit<'a> { + fn exit_reason(&self) -> PlaneExitReason { + self.0.exit_reason.into() + } + + fn esr_el2(&self) -> EsrEl2 { + self.0.esr_el2.into() + } + + fn esr_el2_class(&self) -> ExceptionClass { + ExceptionClass::from(EsrEl2::from_bits(self.0.esr_el2).ec()) + } + + fn far_el2(&self) -> u64 { + self.0.far_el2 + } +} + +/// Stub, just so we have a type to implement the `BackingPrivate` trait. +#[derive(Default)] +pub struct CcaEmulationCache; + +#[expect(private_interfaces)] +impl BackingPrivate for CcaBacked { + type HclBacking<'cca> = Cca; + type Shared = CcaBackedShared; + type EmulationCache = CcaEmulationCache; + + fn shared(shared: &BackingShared) -> &Self::Shared { + let BackingShared::Cca(shared) = shared else { + unreachable!() + }; + shared + } + + fn new( + params: super::BackingParams<'_, '_, Self>, + shared: &CcaBackedShared, + ) -> Result { + // TODO: CCA: do we need a "flush_page" here (?) + // TODO: CCA: initialize untrusted synic (?) + Ok(Self { + vtls: VtlArray::new(CcaVtl::new()), + cvm: UhCvmVpState::new( + &shared.cvm, + params.partition, + params.vp_info, + UhDirectOverlay::Count as usize, + )?, + }) + } + + type StateAccess<'p, 'a> + = UhVpStateAccess<'a, 'p, Self> + where + Self: 'a + 'p, + 'p: 'a; + + fn access_vp_state<'a, 'p>( + this: &'a mut UhProcessor<'p, Self>, + vtl: GuestVtl, + ) -> Self::StateAccess<'p, 'a> { + UhVpStateAccess::new(this, vtl) + } + + fn init(vp: &mut UhProcessor<'_, Self>) { + // initialise non-zero registers for plane + // TODO: CCA: SIMD regs? + const SCTLR_EL1_DEFAULT: u64 = 0xC50878; + const PMCR_EL0_DEFAULT: u64 = 1 << 6; + const MDSCR_EL1_DEFAULT: u64 = 1 << 11; + + vp.sysreg_write(GuestVtl::Vtl0, SystemReg::SCTLR, SCTLR_EL1_DEFAULT) + .map_err(vp_state::Error::SetRegisters) + .unwrap(); + + vp.sysreg_write(GuestVtl::Vtl0, SystemReg::PMCR_EL0, PMCR_EL0_DEFAULT) + .map_err(vp_state::Error::SetRegisters) + .unwrap(); + + vp.sysreg_write(GuestVtl::Vtl0, SystemReg::MDSCR_EL1, MDSCR_EL1_DEFAULT) + .map_err(vp_state::Error::SetRegisters) + .unwrap() + } + + async fn run_vp( + this: &mut UhProcessor<'_, Self>, + dev: &impl CpuIo, + _stop: &mut virt::StopVp<'_>, + ) -> Result<(), VpHaltReason> { + // TODO: CCA: TDX implementation handled "deliverability events/interrupts" here, + // no clue what they're about, potentially some VBS stuff? + + // TODO: CCA: NEXT: move this to `init`? + this.set_plane_enter(); + + // Run the CCA plane. + // This will return when the plane exits. + let intercepted = this + .runner + .run() + .map_err(|e| dev.fatal_error(CcaRunVpError(e).into()))?; + + // Preserve the plane context, so we can restore it later. + this.preserve_plane_context(); + + if intercepted { + // CCA: note, this is a very simplified version of the exit handling, + // just enough to get the TMK running. + // TODO: CCA: NEXT: document how we integrate with the wider emulation + // system. + let cca_exit = CcaExit(this.runner.cca_rsi_plane_exit()); + let exit_reason = cca_exit.exit_reason(); + let esr_el2 = cca_exit.esr_el2(); + match exit_reason { + PlaneExitReason::Sync => { + match cca_exit.esr_el2_class() { + ExceptionClass::DataAbort => { + // get the address that caused the data abort + let address = cca_exit.far_el2(); + // Based on the CpuIo impl in tmk_vmm/src/run.rs, dev.is_mmio(address) + // always returns false, so we handle MMIO access here. + + if esr_el2.is_write() { + // Handle MMIO write + if let Some(srt) = esr_el2.srt() { + dev.write_mmio( + this.vp_index(), + address, + &this.runner.cca_rsi_plane_exit().gprs[srt as usize] + .to_ne_bytes(), + ) + .await; + } else { + tracing::warn!( + "MMIO write not handled, srt does have a valid value" + ); + } + } else { + // Handle MMIO read + todo!(); + } + this.runner.cca_rsi_plane_entry().pc += 4; // Advance PC + } + ExceptionClass::InstructionAbort => { + // Handle instruction abort + todo!(); + } + ExceptionClass::SimdAccess => { + this.runner.cca_plane_no_trap_simd(); + } + ExceptionClass::SmcError => { + tracing::warn!("SmcError exception triggered, but not handled"); + } + } + } + PlaneExitReason::Irq => { + // Handle IRQ exit + tracing::warn!("IRQ triggered, but not handled"); + } + } + } + Ok(()) + } + + fn process_interrupts( + _this: &mut UhProcessor<'_, Self>, + _scan_irr: VtlArray, + _first_scan_irr: &mut bool, + _dev: &impl CpuIo, + ) -> bool { + false + } + + fn poll_apic(_this: &mut UhProcessor<'_, Self>, _vtl: GuestVtl, _scan_irr: bool) { + // TODO: CCA: poll GIC? + } + + fn request_extint_readiness(_this: &mut UhProcessor<'_, Self>) { + unreachable!("extint managed through software apic") + } + + fn request_untrusted_sint_readiness(_this: &mut UhProcessor<'_, Self>, _sints: u16) { + // TODO: CCA: handle this for CCA untrusted synic + unimplemented!(); + } + + // fn handle_cross_vtl_interrupts( + // _this: &mut UhProcessor<'_, Self>, + // _dev: &impl CpuIo, + // ) -> Result { + // // TODO: CCA: handle cross VTL interrupts when GIC support is added + // Ok(false) + // } + + fn hv(&self, _vtl: GuestVtl) -> Option<&ProcessorVtlHv> { + None + } + + fn hv_mut(&mut self, _vtl: GuestVtl) -> Option<&mut ProcessorVtlHv> { + None + } + + fn handle_vp_start_enable_vtl_wake(_this: &mut UhProcessor<'_, Self>, _vtl: GuestVtl) { + todo!() + } + + fn vtl1_inspectable(_this: &UhProcessor<'_, Self>) -> bool { + todo!() + } +} + +impl UhProcessor<'_, CcaBacked> { + fn sysreg_write( + &mut self, + vtl: GuestVtl, + reg: SystemReg, + val: u64, + ) -> Result<(), register::SetRegError> { + self.runner.cca_sysreg_write(vtl, reg, val) + } + + fn set_plane_enter(&mut self) { + self.runner.cca_set_plane_enter(); + } + + // Copy the exit context to the entry context. + fn preserve_plane_context(&mut self) { + let plane_run = self.runner.cca_rsi_plane_run_mut(); + + // Copy GPRs across. + plane_run + .entry + .gprs + .copy_from_slice(&plane_run.exit.gprs[..]); + + // Set the PC to the ELR_EL2 value from the exit context. + plane_run.entry.pc = plane_run.exit.elr_el2; + + // Set GICv3 HCR to the value from the exit context. + plane_run.entry.gicv3_hcr = plane_run.exit.gicv3_hcr; + } + + // TODO: CCA: lots of stuff might be needed based on the TDX implementation, something akin to: + // async fn run_vp_cca(&mut self, dev: &impl CpuIo) -> Result<(), VpHaltReason> +} + +impl AccessVpState for UhVpStateAccess<'_, '_, CcaBacked> { + type Error = vp_state::Error; + + fn caps(&self) -> &virt::PartitionCapabilities { + &self.vp.partition.caps + } + + fn commit(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn registers(&mut self) -> Result { + let mut reg: vp::Registers = vp::Registers::default(); + + let plane_enter = self.vp.runner.cca_rsi_plane_entry(); + + reg.x0 = plane_enter.gprs[0]; + reg.x1 = plane_enter.gprs[1]; + reg.x2 = plane_enter.gprs[2]; + reg.x3 = plane_enter.gprs[3]; + reg.x4 = plane_enter.gprs[4]; + reg.x5 = plane_enter.gprs[5]; + reg.x6 = plane_enter.gprs[6]; + reg.x7 = plane_enter.gprs[7]; + reg.x8 = plane_enter.gprs[8]; + reg.x9 = plane_enter.gprs[9]; + reg.x10 = plane_enter.gprs[10]; + reg.x11 = plane_enter.gprs[11]; + reg.x12 = plane_enter.gprs[12]; + reg.x13 = plane_enter.gprs[13]; + reg.x14 = plane_enter.gprs[14]; + reg.x15 = plane_enter.gprs[15]; + reg.x16 = plane_enter.gprs[16]; + reg.x17 = plane_enter.gprs[17]; + reg.x18 = plane_enter.gprs[18]; + reg.x19 = plane_enter.gprs[19]; + reg.x20 = plane_enter.gprs[20]; + reg.x21 = plane_enter.gprs[21]; + reg.x22 = plane_enter.gprs[22]; + reg.x23 = plane_enter.gprs[23]; + reg.x24 = plane_enter.gprs[24]; + reg.x25 = plane_enter.gprs[25]; + reg.x26 = plane_enter.gprs[26]; + reg.x27 = plane_enter.gprs[27]; + reg.x28 = plane_enter.gprs[28]; + reg.fp = plane_enter.gprs[29]; + reg.lr = plane_enter.gprs[30]; + reg.pc = plane_enter.pc; + + Ok(reg) + } + + fn set_registers(&mut self, value: &vp::Registers) -> Result<(), Self::Error> { + self.vp.runner.cca_plane_trap_simd(); + self.vp.runner.cca_set_default_pstate(); + + let vp::Registers { + x0, + x1, + x2, + x3, + x4, + x5, + x6, + x7, + x8, + x9, + x10, + x11, + x12, + x13, + x14, + x15, + x16, + x17, + x18, + x19, + x20, + x21, + x22, + x23, + x24, + x25, + x26, + x27, + x28, + fp, + lr, + pc, + .. + } = value; + + let plane_enter = self.vp.runner.cca_rsi_plane_entry(); + plane_enter.gprs[0] = *x0; + plane_enter.gprs[1] = *x1; + plane_enter.gprs[2] = *x2; + plane_enter.gprs[3] = *x3; + plane_enter.gprs[4] = *x4; + plane_enter.gprs[5] = *x5; + plane_enter.gprs[6] = *x6; + plane_enter.gprs[7] = *x7; + plane_enter.gprs[8] = *x8; + plane_enter.gprs[9] = *x9; + plane_enter.gprs[10] = *x10; + plane_enter.gprs[11] = *x11; + plane_enter.gprs[12] = *x12; + plane_enter.gprs[13] = *x13; + plane_enter.gprs[14] = *x14; + plane_enter.gprs[15] = *x15; + plane_enter.gprs[16] = *x16; + plane_enter.gprs[17] = *x17; + plane_enter.gprs[18] = *x18; + plane_enter.gprs[19] = *x19; + plane_enter.gprs[20] = *x20; + plane_enter.gprs[21] = *x21; + plane_enter.gprs[22] = *x22; + plane_enter.gprs[23] = *x23; + plane_enter.gprs[24] = *x24; + plane_enter.gprs[25] = *x25; + plane_enter.gprs[26] = *x26; + plane_enter.gprs[27] = *x27; + plane_enter.gprs[28] = *x28; + plane_enter.gprs[29] = *fp; + plane_enter.gprs[30] = *lr; + plane_enter.pc = *pc; + + Ok(()) + } + + fn system_registers(&mut self) -> Result { + todo!() + } + + fn set_system_registers(&mut self, _value: &vp::SystemRegisters) -> Result<(), Self::Error> { + todo!() + } +} + +impl HardwareIsolatedBacking for CcaBacked { + fn cvm_state(&self) -> &UhCvmVpState { + &self.cvm + } + + fn cvm_state_mut(&mut self) -> &mut UhCvmVpState { + &mut self.cvm + } + + fn cvm_partition_state(shared: &Self::Shared) -> &UhCvmPartitionState { + &shared.cvm + } + + fn switch_vtl(this: &mut UhProcessor<'_, Self>, _source_vtl: GuestVtl, target_vtl: GuestVtl) { + // TODO: CCA: This might need more work when multiple VTLs are supported. + + this.backing.cvm_state_mut().exit_vtl = target_vtl; + } + + fn translation_registers( + &self, + _this: &UhProcessor<'_, Self>, + _vtl: GuestVtl, + ) -> TranslationRegisters { + unimplemented!() + } + + fn tlb_flush_lock_access<'a>( + vp_index: Option, + partition: &'a UhPartitionInner, + shared: &'a Self::Shared, + ) -> impl TlbFlushLockAccess + 'a { + let vp_index_t = vp_index.unwrap_or_else(|| VpIndex::new(0)); + + CcaTlbLockFlushAccess { + vp_index: vp_index_t, + partition, + shared, + } + } + + fn pending_event_vector(_this: &UhProcessor<'_, Self>, _vtl: GuestVtl) -> Option { + None + } + + fn is_interrupt_pending( + _this: &mut UhProcessor<'_, Self>, + _vtl: GuestVtl, + _check_rflags: bool, + _dev: &impl CpuIo, + ) -> bool { + false + } + + fn set_pending_exception( + _this: &mut UhProcessor<'_, Self>, + _vtl: GuestVtl, + _event: hvdef::HvX64PendingExceptionEvent, + ) { + } + + ///TODO Place holder. Not implemented for arm64. + fn intercept_message_state( + _this: &UhProcessor<'_, Self>, + _vtl: GuestVtl, + _include_optional_state: bool, + ) -> InterceptMessageState { + InterceptMessageState { + instruction_length_and_cr8: 0, + cpl: 0, + efer_lma: false, + cs: hvdef::HvX64SegmentRegister::new_zeroed(), + rip: 0, + rflags: 0, + rax: 0, + rdx: 0, + rcx: 0, + rsi: 0, + rdi: 0, + optional: None, + } + } + + fn cr0(_this: &UhProcessor<'_, Self>, _vtl: GuestVtl) -> u64 { + 0 + } + + fn cr4(_this: &UhProcessor<'_, Self>, _vtl: GuestVtl) -> u64 { + 0 + } + + fn cr_intercept_registration( + _this: &mut UhProcessor<'_, Self>, + _intercept_control: HvRegisterCrInterceptControl, + ) { + } + + fn untrusted_synic_mut(&mut self) -> Option<&mut ProcessorSynic> { + None + } + + fn update_deadline(_this: &mut UhProcessor<'_, Self>, _ref_time_now: u64, _next_ref_time: u64) { + unimplemented!() + } + + fn clear_deadline(_this: &mut UhProcessor<'_, Self>) { + unimplemented!() + } +} + +#[expect(unused)] +struct CcaTlbLockFlushAccess<'a> { + vp_index: VpIndex, + partition: &'a UhPartitionInner, + shared: &'a CcaBackedShared, +} + +impl TlbFlushLockAccess for CcaTlbLockFlushAccess<'_> { + fn flush(&mut self, _vtl: GuestVtl) { + unimplemented!() + } + + fn flush_entire(&mut self) { + unimplemented!() + } + + fn set_wait_for_tlb_locks(&mut self, _vtl: GuestVtl) { + unimplemented!() + } +} + +mod save_restore { + use super::CcaBacked; + use super::UhProcessor; + use vmcore::save_restore::RestoreError; + use vmcore::save_restore::SaveError; + use vmcore::save_restore::SaveRestore; + use vmcore::save_restore::SavedStateNotSupported; + + impl SaveRestore for UhProcessor<'_, CcaBacked> { + type SavedState = SavedStateNotSupported; + + fn save(&mut self) -> Result { + Err(SaveError::NotSupported) + } + + fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> { + match state {} + } + } +} diff --git a/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs b/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs index c4a969571d..6633e04ed7 100644 --- a/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/hardware_cvm/mod.rs @@ -1576,6 +1576,7 @@ impl hv1_hypercall::EnableVpVtl hvdef::hypercall::InitialVpContextX64::new_zeroed(), + virt::IsolationType::Cca => todo!(), }; // Tell the hypervisor to enable VTL 1, and register any needed state diff --git a/openhcl/virt_mshv_vtl/src/processor/mod.rs b/openhcl/virt_mshv_vtl/src/processor/mod.rs index 8aa07bd6b0..e8051c65cf 100644 --- a/openhcl/virt_mshv_vtl/src/processor/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/mod.rs @@ -14,7 +14,6 @@ cfg_if::cfg_if! { pub mod snp; pub mod tdx; - use crate::TlbFlushLockAccess; use crate::VtlCrash; use bitvec::prelude::BitArray; use bitvec::prelude::Lsb0; @@ -28,8 +27,12 @@ cfg_if::cfg_if! { use virt::vp::AccessVpState; use zerocopy::IntoBytes; } else if #[cfg(guest_arch = "aarch64")] { + pub mod cca; use hv1_hypercall::Arm64RegisterState; use hvdef::HvArm64RegisterName; + use virt_support_aarch64emu::translate::TranslationRegisters; + use hvdef::HvRegisterCrInterceptControl; + use hv1_emulator::synic::ProcessorSynic; } else { compile_error!("unsupported guest architecture"); } @@ -40,6 +43,7 @@ use super::UhPartitionInner; use super::UhVpInner; use crate::ExitActivity; use crate::GuestVtl; +use crate::TlbFlushLockAccess; use crate::WakeReason; use cvm_tracing::CVM_ALLOWED; use cvm_tracing::CVM_CONFIDENTIAL; @@ -416,7 +420,7 @@ impl InterceptMessageType { } /// Trait for processor backings that have hardware isolation support. -#[cfg(guest_arch = "x86_64")] +#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))] pub(crate) trait HardwareIsolatedBacking: Backing { /// Gets CVM specific VP state. fn cvm_state(&self) -> &crate::UhCvmVpState; diff --git a/openhcl/virt_mshv_vtl/src/processor/mshv/arm64.rs b/openhcl/virt_mshv_vtl/src/processor/mshv/arm64.rs index 25b31de569..5b1b6ce5c4 100644 --- a/openhcl/virt_mshv_vtl/src/processor/mshv/arm64.rs +++ b/openhcl/virt_mshv_vtl/src/processor/mshv/arm64.rs @@ -108,7 +108,9 @@ impl BackingPrivate for HypervisorBackedArm64 { type Shared = HypervisorBackedArm64Shared; fn shared(shared: &BackingShared) -> &Self::Shared { - let BackingShared::Hypervisor(shared) = shared; + let BackingShared::Hypervisor(shared) = shared else { + unreachable!() + }; shared } diff --git a/support/safe_intrinsics/src/lib.rs b/support/safe_intrinsics/src/lib.rs index b53a8a7057..27e2142875 100644 --- a/support/safe_intrinsics/src/lib.rs +++ b/support/safe_intrinsics/src/lib.rs @@ -45,3 +45,20 @@ pub fn store_fence() { // Make the compiler aware. core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::Release); } + +/// Read the CNTFRQ_EL0 system register, which contains the frequency of the +/// system timer in Hz. This is used to determine the frequency of the +/// system timer for the current execution level (EL0). +#[cfg(target_arch = "aarch64")] +pub fn read_cntfrq_el0() -> u64 { + let freq: u64; + // SAFETY: no safety requirements, just reading an EL0 sysreg + unsafe { + core::arch::asm!( + "mrs {cntfrq}, cntfrq_el0", + cntfrq = out(reg) freq, + options(nomem, nostack, preserves_flags) + ); + }; + freq +} diff --git a/tmk/tmk_vmm/Cargo.toml b/tmk/tmk_vmm/Cargo.toml index 439afe90c7..e7347293f2 100644 --- a/tmk/tmk_vmm/Cargo.toml +++ b/tmk/tmk_vmm/Cargo.toml @@ -21,6 +21,7 @@ vm_topology.workspace = true vmcore.workspace = true vm_loader.workspace = true x86defs.workspace = true +memory_range.workspace = true pal_async.workspace = true mesh.workspace = true @@ -44,6 +45,7 @@ underhill_mem.workspace = true virt_kvm.workspace = true virt_mshv.workspace = true virt_mshv_vtl.workspace = true +user_driver.workspace = true [target.'cfg(target_os = "macos")'.dependencies] virt_hvf.workspace = true diff --git a/tmk/tmk_vmm/src/load.rs b/tmk/tmk_vmm/src/load.rs index 26d242b764..3a4440b8d3 100644 --- a/tmk/tmk_vmm/src/load.rs +++ b/tmk/tmk_vmm/src/load.rs @@ -9,6 +9,7 @@ use guestmem::GuestMemory; use hvdef::Vtl; use loader::importer::GuestArch; use loader::importer::ImageLoad; +#[cfg(guest_arch = "x86_64")] use loader::importer::X86Register; use object::Endianness; use object::Object; @@ -19,14 +20,17 @@ use std::sync::Arc; use virt::VpIndex; use vm_topology::memory::MemoryLayout; use vm_topology::processor::ProcessorTopology; +#[cfg(guest_arch = "aarch64")] use vm_topology::processor::aarch64::Aarch64Topology; +#[cfg(guest_arch = "x86_64")] use vm_topology::processor::x86::X86Topology; use zerocopy::FromBytes as _; +#[cfg(guest_arch = "x86_64")] use zerocopy::FromZeros; use zerocopy::IntoBytes; /// Loads a TMK, returning the initial registers for the BSP. -#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))] +#[cfg(guest_arch = "x86_64")] pub fn load_x86( memory_layout: &MemoryLayout, guest_memory: &GuestMemory, @@ -36,7 +40,7 @@ pub fn load_x86( test: &TestInfo, ) -> anyhow::Result> { let mut loader = vm_loader::Loader::new(guest_memory.clone(), memory_layout, Vtl::Vtl0); - let load_info = load_common(&mut loader, tmk, test)?; + let load_info = load_common(None, &mut loader, tmk, test)?; let page_table_base = load_info.next_available_address; let mut page_table_work_buffer: Vec = @@ -88,7 +92,7 @@ pub fn load_x86( Ok(regs) } -#[cfg_attr(not(guest_arch = "aarch64"), expect(dead_code))] +#[cfg(guest_arch = "aarch64")] pub fn load_aarch64( memory_layout: &MemoryLayout, guest_memory: &GuestMemory, @@ -98,7 +102,12 @@ pub fn load_aarch64( test: &TestInfo, ) -> anyhow::Result> { let mut loader = vm_loader::Loader::new(guest_memory.clone(), memory_layout, Vtl::Vtl0); - let load_info = load_common(&mut loader, tmk, test)?; + let load_info = load_common( + Some(memory_layout.ram()[0].range.start()), + &mut loader, + tmk, + test, + )?; let mut import_reg = |reg| { loader @@ -118,6 +127,7 @@ pub fn load_aarch64( } fn load_common( + offset_addr: Option, loader: &mut vm_loader::Loader<'_, R>, tmk: &File, test: &TestInfo, @@ -126,7 +136,7 @@ fn load_common( loader, &mut &*tmk, 0, - 0x200000, + offset_addr.unwrap_or(0x200000), false, loader::importer::BootPageAcceptance::Exclusive, "tmk", @@ -158,6 +168,7 @@ fn load_common( struct LoadInfo { entrypoint: u64, param: u64, + #[cfg_attr(guest_arch = "aarch64", expect(dead_code))] next_available_address: u64, } diff --git a/tmk/tmk_vmm/src/main.rs b/tmk/tmk_vmm/src/main.rs index 9c2a883664..dd58e2029a 100644 --- a/tmk/tmk_vmm/src/main.rs +++ b/tmk/tmk_vmm/src/main.rs @@ -13,6 +13,7 @@ mod paravisor_vmm; mod run; use anyhow::Context; +use anyhow::Result; use clap::Parser; use pal_async::DefaultDriver; use pal_async::DefaultPool; @@ -23,7 +24,7 @@ use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -fn main() -> anyhow::Result<()> { +fn main() -> Result<()> { tracing_subscriber::registry() .with( tracing_subscriber::fmt::layer() @@ -87,10 +88,26 @@ enum HypervisorOpt { /// Use Hypervisor.Framework to run the TMK. #[cfg(target_os = "macos")] Hvf, + /// Use mshv-vtl to run the TMK inside a CCA realm. + #[cfg(all(target_os = "linux", guest_arch = "aarch64"))] + Cca, } -async fn do_main(driver: DefaultDriver) -> anyhow::Result<()> { - let opts = Options::parse(); +impl Options { + fn finalize(mut self) -> Result { + let hv = match self.hv { + Some(hv) => hv, + None => choose_hypervisor()?, + }; + + self.hv = Some(hv); + + Ok(self) + } +} + +async fn do_main(driver: DefaultDriver) -> Result<()> { + let opts = Options::parse().finalize()?; if opts.list { let tmk = fs_err::File::open(&opts.tmk).context("failed to open TMK")?; @@ -100,10 +117,7 @@ async fn do_main(driver: DefaultDriver) -> anyhow::Result<()> { } Ok(()) } else { - let hv = match opts.hv { - Some(hv) => hv, - None => choose_hypervisor()?, - }; + let hv = opts.hv.expect("hv must have an finalized value"); let mut state = CommonState::new(driver, opts).await?; state @@ -118,6 +132,12 @@ async fn do_main(driver: DefaultDriver) -> anyhow::Result<()> { .run_paravisor_vmm(virt::IsolationType::None, test) .await } + #[cfg(all(target_os = "linux", guest_arch = "aarch64"))] + HypervisorOpt::Cca => { + state + .run_paravisor_vmm(virt::IsolationType::Cca, test) + .await + } #[cfg(windows)] HypervisorOpt::Whp => { state @@ -137,7 +157,7 @@ async fn do_main(driver: DefaultDriver) -> anyhow::Result<()> { } } -fn choose_hypervisor() -> anyhow::Result { +fn choose_hypervisor() -> Result { #[cfg(all(target_os = "linux", guest_arch = "x86_64"))] { if virt_mshv::is_available()? { diff --git a/tmk/tmk_vmm/src/paravisor_vmm.rs b/tmk/tmk_vmm/src/paravisor_vmm.rs index b235efeb09..1b3d3c975b 100644 --- a/tmk/tmk_vmm/src/paravisor_vmm.rs +++ b/tmk/tmk_vmm/src/paravisor_vmm.rs @@ -21,7 +21,7 @@ impl RunContext<'_> { isolation: virt::IsolationType, test: &crate::load::TestInfo, ) -> anyhow::Result { - let params = UhPartitionNewParams { + let mut params = UhPartitionNewParams { isolation, hide_isolation: false, lower_vtl_memory_layout: &self.state.memory_layout, @@ -37,13 +37,13 @@ impl RunContext<'_> { // TODO: match openhcl defaults when TDX is supported. disable_lower_vtl_timer_virt: true, }; - let p = virt_mshv_vtl::UhProtoPartition::new(params, |_| self.state.driver.clone())?; + let p = virt_mshv_vtl::UhProtoPartition::new(&mut params, |_| self.state.driver.clone())?; let m = underhill_mem::init(&underhill_mem::Init { processor_topology: &self.state.processor_topology, isolation, vtl0_alias_map_bit: None, - vtom: None, + vtom: params.vtom, mem_layout: &self.state.memory_layout, complete_memory_layout: &self.state.memory_layout, boot_init: None, @@ -52,6 +52,18 @@ impl RunContext<'_> { }) .await?; + let cvm_params = if isolation == virt::IsolationType::Cca { + let cvm_memory = m.cvm_memory().unwrap(); + Some(virt_mshv_vtl::CvmLateParams { + shared_gm: cvm_memory.shared_gm.clone(), + isolated_memory_protector: cvm_memory.protector.clone(), + shared_dma_client: Arc::new(user_driver::lockmem::LockedMemorySpawner), + private_dma_client: self.state.cca_private_dma_client(), + }) + } else { + None + }; + let (partition, vps) = p .build(UhLateParams { gm: [ @@ -65,7 +77,7 @@ impl RunContext<'_> { cpuid: Vec::new(), crash_notification_send: mesh::channel().0, vmtime: self.vmtime_source, - cvm_params: None, + cvm_params, vmbus_relay: false, }) .await?; @@ -76,7 +88,7 @@ impl RunContext<'_> { let r = self .run(m.vtl0(), partition.caps(), test, async |_this, runner| { let [vp] = vps.try_into().ok().unwrap(); - threads.push(start_vp(vp, runner).await?); + threads.push(start_vp(vp, runner, isolation).await?); Ok(()) }) .await?; @@ -95,17 +107,26 @@ impl RunContext<'_> { async fn start_vp( mut vp: UhProcessorBox, mut runner: RunnerBuilder, + isolation: virt::IsolationType, ) -> anyhow::Result> { let vp_thread = std::thread::spawn(move || { let pool = pal_uring::IoUringPool::new("vp", 256).unwrap(); let driver = pool.client().initiator().clone(); - pool.client().set_idle_task(async move |mut control| { - let vp = vp - .bind_processor::(&driver, Some(&mut control)) - .unwrap(); - - runner.build(vp).unwrap().run_vp().await; - }); + match isolation { + #[cfg(guest_arch = "aarch64")] + virt::IsolationType::Cca => pool.client().set_idle_task(async move |mut control| { + let vp = vp + .bind_processor::(&driver, Some(&mut control)) + .unwrap(); + runner.build(vp).unwrap().run_vp().await; + }), + _ => pool.client().set_idle_task(async move |mut control| { + let vp = vp + .bind_processor::(&driver, Some(&mut control)) + .unwrap(); + runner.build(vp).unwrap().run_vp().await; + }), + } pool.run() }); Ok(vp_thread) diff --git a/tmk/tmk_vmm/src/run.rs b/tmk/tmk_vmm/src/run.rs index 5576e18e0e..2e464ec5fd 100644 --- a/tmk/tmk_vmm/src/run.rs +++ b/tmk/tmk_vmm/src/run.rs @@ -11,6 +11,8 @@ use guestmem::GuestMemory; use hvdef::Vtl; use pal_async::DefaultDriver; use std::sync::Arc; +#[cfg(target_os = "linux")] +use user_driver::DmaClient; use virt::PartitionCapabilities; use virt::Processor; use virt::StopVpSource; @@ -27,11 +29,152 @@ use zerocopy::TryFromBytes as _; pub const COMMAND_ADDRESS: u64 = 0xffff_0000; +#[cfg(all(target_os = "linux", guest_arch = "aarch64"))] +mod cca { + use super::DmaClient; + use super::MemoryLayout; + use super::Options; + use crate::HypervisorOpt; + use anyhow::Context as _; + use core::ops::Range; + use memory_range::MemoryRange; + use std::sync::Arc; + use underhill_mem::MemoryAcceptor; + use user_driver::lockmem::LockedMemorySpawner; + use user_driver::memory::MemoryBlock; + use user_driver::memory::PAGE_SIZE; + use user_driver::memory::PAGE_SIZE64; + use virt::IsolationType; + use vm_topology::memory::MemoryRangeWithNode; + + pub(super) struct CcaState { + pub(super) private_dma_client: Arc, + pub(super) _guest_ram_backing: MemoryBlock, + } + + pub(super) fn build( + opts: &Options, + memory_layout: &mut MemoryLayout, + ram_size: u64, + ) -> anyhow::Result> { + let hv = opts.hv.expect("hv must have an finalized value"); + match hv { + HypervisorOpt::Cca => { + let mut map_size = ram_size as usize; + let private_dma_client: Arc = Arc::new(LockedMemorySpawner); + + let (private_memory, private_ram_pfn) = { + const BITMAP_ALIGNMENT: u64 = PAGE_SIZE64 * 8; + const MAX_ALLOC_ATTEMPTS: usize = 4; + let mut selected = None; + + for _attempt in 0..MAX_ALLOC_ATTEMPTS { + let private_memory = private_dma_client + .allocate_dma_buffer(map_size) + .with_context(|| { + format!( + "failed to allocate private CCA RAM buffer of size {map_size}" + ) + })?; + + let asking_size = ram_size + .checked_add(BITMAP_ALIGNMENT - PAGE_SIZE64) + .context("private CCA RAM search size overflowed")?; + if let Some(pfns) = + contiguous_subpfns(&private_memory, asking_size as usize) + { + let page_count = (ram_size as usize).div_ceil(PAGE_SIZE); + if let Some(start_index) = pfns + .iter() + .position(|pfn| { + (pfn * PAGE_SIZE64).is_multiple_of(BITMAP_ALIGNMENT) + }) + .filter(|&start_index| pfns.len() - start_index >= page_count) + { + selected = Some((private_memory, pfns[start_index])); + break; + } + } + + map_size = map_size + .checked_mul(2) + .context("private CCA RAM allocation size overflowed while retrying")?; + } + + selected.with_context(|| { + format!( + "failed to allocate private CCA RAM with {ram_size} contiguous bytes after {MAX_ALLOC_ATTEMPTS} attempts" + ) + })? + }; + + private_memory.write_zeros(0, private_memory.len()); + + let pa = private_ram_pfn * PAGE_SIZE64; + let start = pa; + let end = pa + .checked_add(ram_size) + .context("private CCA RAM range overflowed")?; + + *memory_layout = MemoryLayout::new_from_ranges( + &[MemoryRangeWithNode { + range: MemoryRange::new(Range { start, end }), + vnode: 0, + }], + &[], + ) + .context("bad memory layout")?; + + // Grant GPA to Plane1 (eqv. VTL0) + let ram = memory_layout.ram().iter().map(|r| r.range); + let acceptor = MemoryAcceptor::new(IsolationType::Cca)?; + for range in ram { + acceptor.apply_initial_lower_vtl_protections(range)?; + } + + Ok(Some(CcaState { + private_dma_client, + _guest_ram_backing: private_memory, + })) + } + _ => Ok(None), + } + } + + /// Returns a sorted contiguous subset of PFNs large enough for `asking_size` bytes. + fn contiguous_subpfns(memory: &MemoryBlock, asking_size: usize) -> Option> { + let page_count = asking_size.div_ceil(PAGE_SIZE); + if page_count == 0 { + return Some(Vec::new()); + } + + let mut pfns = memory.pfns().to_vec(); + pfns.sort_unstable(); + + let mut run_start = 0; + for i in 1..=pfns.len() { + let run_ended = i == pfns.len() || pfns[i - 1] + 1 != pfns[i]; + if run_ended { + if i - run_start >= page_count { + pfns.truncate(run_start + page_count); + pfns.drain(..run_start); + return Some(pfns); + } + run_start = i; + } + } + + None + } +} + pub struct CommonState { pub driver: DefaultDriver, pub opts: Options, pub processor_topology: ProcessorTopology, pub memory_layout: MemoryLayout, + #[cfg(all(target_os = "linux", guest_arch = "aarch64"))] + cca: Option, } pub struct RunContext<'a> { @@ -51,6 +194,20 @@ pub enum TestResult { } impl CommonState { + #[cfg(all(target_os = "linux", guest_arch = "aarch64"))] + pub fn cca_private_dma_client(&self) -> Arc { + self.cca + .as_ref() + .expect("CCA private DMA client is only available when running with --hv cca") + .private_dma_client + .clone() + } + + #[cfg(all(target_os = "linux", not(guest_arch = "aarch64")))] + pub fn cca_private_dma_client(&self) -> Arc { + panic!("CCA private DMA client is only available on aarch64") + } + pub async fn new(driver: DefaultDriver, opts: Options) -> anyhow::Result { #[cfg(guest_arch = "x86_64")] let processor_topology = TopologyBuilder::new_x86() @@ -74,14 +231,23 @@ impl CommonState { .context("failed to build processor topology")?; let ram_size = 0x400000; - let memory_layout = + + #[cfg_attr( + not(all(target_os = "linux", guest_arch = "aarch64")), + expect(unused_mut) + )] + let mut memory_layout = MemoryLayout::new(ram_size, &[], &[], &[], None).context("bad memory layout")?; + #[cfg(all(target_os = "linux", guest_arch = "aarch64"))] + let cca = cca::build(&opts, &mut memory_layout, ram_size)?; Ok(Self { driver, opts, processor_topology, memory_layout, + #[cfg(all(target_os = "linux", guest_arch = "aarch64"))] + cca, }) } @@ -392,10 +558,9 @@ impl RunnerBuilder { { let virt::aarch64::Aarch64InitialRegs { registers, - system_registers, + system_registers: _, } = self.regs.as_ref(); state.set_registers(registers)?; - state.set_system_registers(system_registers)?; } state.commit()?; } diff --git a/vm/aarch64/aarch64defs/src/lib.rs b/vm/aarch64/aarch64defs/src/lib.rs index 9587158470..8cfb4a7a82 100644 --- a/vm/aarch64/aarch64defs/src/lib.rs +++ b/vm/aarch64/aarch64defs/src/lib.rs @@ -11,6 +11,7 @@ pub mod gic; pub mod smccc; use bitfield_struct::bitfield; +use core::fmt::Display; use open_enum::open_enum; use zerocopy::FromBytes; use zerocopy::Immutable; @@ -59,8 +60,17 @@ pub struct Cpsr64 { #[bitfield(u64)] #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] pub struct EsrEl2 { - #[bits(25)] - pub iss: u32, + #[bits(6)] + pub lower_iss: u8, + pub wnr: bool, + #[bits(9)] + pub mid_iss: u16, + #[bits(5)] + pub b_srt: u8, + pub a: bool, + pub b: bool, + pub c: bool, + pub d: bool, pub il: bool, #[bits(6)] pub ec: u8, @@ -70,6 +80,38 @@ pub struct EsrEl2 { _rsvd: u32, } +impl EsrEl2 { + pub fn is_write(&self) -> bool { + // The WNR bit is set for writes, not reads. + self.wnr() + } + + pub fn is_read(&self) -> bool { + // The WNR bit is set for writes, not reads. + !self.wnr() + } + + pub fn iss(&self) -> u32 { + u32::from(self.lower_iss()) + | ((self.wnr() as u32) << 6) + | (u32::from(self.mid_iss()) << 7) + | (u32::from(self.b_srt()) << 16) + | ((self.a() as u32) << 21) + | ((self.b() as u32) << 22) + | ((self.c() as u32) << 23) + | ((self.d() as u32) << 24) + } + + pub fn srt(&self) -> Option { + // The SRT field is only valid for data aborts. + if (ExceptionClass::DATA_ABORT_LOWER.0..ExceptionClass::DATA_ABORT.0).contains(&self.ec()) { + Some(self.b_srt()) + } else { + None + } + } +} + /// aarch64 SCTRL_EL1 #[bitfield(u64)] #[derive(PartialEq, Eq)] @@ -222,9 +264,17 @@ pub struct IssDataAbort { impl From for EsrEl2 { fn from(abort_code: IssDataAbort) -> Self { let val: u32 = abort_code.into(); + let iss = val & 0x07ff_ffff; EsrEl2::new() .with_ec(ExceptionClass::DATA_ABORT.0) - .with_iss(val & 0x07ffffff) + .with_lower_iss((iss & 0x3f) as u8) + .with_wnr(((iss >> 6) & 1) != 0) + .with_mid_iss(((iss >> 7) & 0x1ff) as u16) + .with_b_srt(((iss >> 16) & 0x1F) as u8) + .with_a(((iss >> 21) & 0x1) != 0) + .with_b(((iss >> 22) & 0x1) != 0) + .with_c(((iss >> 23) & 0x1) != 0) + .with_d(((iss >> 24) & 0x1) != 0) .with_iss2((val >> 27) as u8) } } @@ -315,9 +365,18 @@ pub struct IssInstructionAbort { impl From for EsrEl2 { fn from(instruction_code: IssInstructionAbort) -> Self { let val: u32 = instruction_code.into(); + let iss = val & 0x07ff_ffff; + EsrEl2::new() .with_ec(ExceptionClass::INSTRUCTION_ABORT.0) - .with_iss(val & 0x07ffffff) + .with_lower_iss((iss & 0x3f) as u8) + .with_wnr(((iss >> 6) & 1) != 0) + .with_mid_iss(((iss >> 7) & 0x1ff) as u16) + .with_b_srt(((iss >> 16) & 0x1F) as u8) + .with_a(((iss >> 21) & 0x1) != 0) + .with_b(((iss >> 22) & 0x1) != 0) + .with_c(((iss >> 23) & 0x1) != 0) + .with_d(((iss >> 24) & 0x1) != 0) .with_iss2((val >> 27) as u8) } } @@ -856,3 +915,24 @@ open_enum! { HIBERNATE_OFF = 1, } } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Vendor(pub u32); + +impl Vendor { + pub const ARM: Self = Self(0x0010); + + pub fn is_arm_compatible(&self) -> bool { + *self == Self::ARM + } +} + +impl Display for Vendor { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if self.is_arm_compatible() { + f.pad("Arm") + } else { + write!(f, "{:#x}", self.0) + } + } +} diff --git a/vm/aarch64/aarch64emu/src/emulator.rs b/vm/aarch64/aarch64emu/src/emulator.rs index b5125c5a4f..c042ef6738 100644 --- a/vm/aarch64/aarch64emu/src/emulator.rs +++ b/vm/aarch64/aarch64emu/src/emulator.rs @@ -182,7 +182,16 @@ impl<'a, T: Cpu> Emulator<'a, T> { ) { return Ok(false); } - let iss = aarch64defs::IssDataAbort::from(syndrome.iss()); + + let iss: u32 = (syndrome.lower_iss() as u32) + | ((syndrome.wnr() as u32) << 6) + | ((syndrome.mid_iss() as u32) << 7) + | ((syndrome.b_srt() as u32) << 16) + | ((syndrome.a() as u32) << 21) + | ((syndrome.b() as u32) << 22) + | ((syndrome.c() as u32) << 23) + | ((syndrome.d() as u32) << 24); + let iss = aarch64defs::IssDataAbort::from(iss); if !iss.isv() { return Ok(false); } diff --git a/vm/aarch64/rsi/Cargo.toml b/vm/aarch64/rsi/Cargo.toml new file mode 100644 index 0000000000..f59415de5c --- /dev/null +++ b/vm/aarch64/rsi/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "rsi" +edition.workspace = true +rust-version.workspace = true + +[features] +default = [] + +[dependencies] +zerocopy.workspace = true + +[lints] +workspace = true diff --git a/vm/aarch64/rsi/src/lib.rs b/vm/aarch64/rsi/src/lib.rs new file mode 100644 index 0000000000..206d94d9bf --- /dev/null +++ b/vm/aarch64/rsi/src/lib.rs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Arm CCA specific definitions, including for the Realm Service Interface (RSI). +#![allow(non_camel_case_types)] +#![expect(missing_docs)] + +// TODO: CCA: A lot of the code in this module depends on who gets to package the RSI calls. +// If OpenVMM is the one that packages the RSI calls, then this module should be +// responsible for defining the RSI calls and their parameters. If the kernel driver is the one +// that packages the RSI calls, then this module should only define the data structures used +// to communicate with the kernel driver, and the RSI calls should be defined in the kernel driver. + +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +/// CCA memory permission index, used to set and get Stage 2 memory access permissions +/// via the RSI interface. +#[expect(missing_docs)] +#[repr(u64)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum CcaMemPermIndex { + Index0, + Index1, + Index2, + Index3, + Index4, + Index5, + Index6, + Index7, + Index8, + Index9, + Index10, + Index11, + Index12, + Index13, + #[default] + Index14, +} + +pub const RSI_PLANE_NR_GPRS: usize = 31; +pub const RSI_PLANE_GIC_NUM_LRS: usize = 16; +pub const RSI_PLANE_ENTER_FLAGS_TRAP_SIMD: u64 = 1 << 4; + +/// Layout for the realm configuration page shared with the kernel driver. +#[repr(C, align(0x1000))] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct cca_realm_config { + pub ipa_width: u64, + pub algorithm: u64, + pub num_aux_planes: u64, + pub gicv3_vtr: u64, + /// 0x1000 − (4 × 8) = 0x1000 − 32 = 0xFE0 + pub pad1: [u8; 0x1000 - 4 * 8], +} + +/// Flattened RSI plane entry buffer layout. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct cca_rsi_plane_entry { + pub flags: u64, + pub pc: u64, + pub pstate: u64, + pub pad0: [u8; 0x100 - 3 * 8], + pub gprs: [u64; RSI_PLANE_NR_GPRS], + pub pad2: [u8; 0x100 - RSI_PLANE_NR_GPRS * 8], + pub gicv3_hcr: u64, + pub gicv3_lrs: [u64; RSI_PLANE_GIC_NUM_LRS], + pub pad3: [u8; 0x100 - (1 + RSI_PLANE_GIC_NUM_LRS) * 8], +} + +/// Flattened RSI plane exit buffer layout. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Debug)] +pub struct cca_rsi_plane_exit { + pub exit_reason: u64, + pub pad1: [u8; 0x100 - 8], + pub elr_el2: u64, + pub esr_el2: u64, + pub far_el2: u64, + pub hpfar_el2: u64, + pub pstate: u64, + pub pad2: [u8; 0x100 - 5 * 8], + pub gprs: [u64; RSI_PLANE_NR_GPRS], + pub pad3: [u8; 0x100 - RSI_PLANE_NR_GPRS * 8], + pub gicv3_hcr: u64, + pub gicv3_lrs: [u64; RSI_PLANE_GIC_NUM_LRS], + pub gicv3_misr: u64, + pub gicv3_vmcr: u64, + pub cntp_ctl_el0: u64, + pub cntp_cval_el0: u64, + pub cntv_ctl_el0: u64, + pub cntv_cval_el0: u64, + pub pad4: [u8; 0x100 - (7 + RSI_PLANE_GIC_NUM_LRS) * 8], +} + +/// Combined RSI plane run page layout. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct cca_rsi_plane_run { + pub entry: cca_rsi_plane_entry, + pub pad4: [u8; 0x800 - size_of::()], + pub exit: cca_rsi_plane_exit, + pub pad9: [u8; 0x800 - size_of::()], +} + +const _: () = assert!(size_of::() == 0x1000); +const _: () = assert!(size_of::() == 0x300); +const _: () = assert!(size_of::() == 0x400); +const _: () = assert!(size_of::() == 0x1000); diff --git a/vm/hv1/hv1_emulator/Cargo.toml b/vm/hv1/hv1_emulator/Cargo.toml index 1bd8ff9f11..795effcb73 100644 --- a/vm/hv1/hv1_emulator/Cargo.toml +++ b/vm/hv1/hv1_emulator/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +aarch64defs.workspace = true hv1_structs.workspace = true hvdef.workspace = true vm_topology.workspace = true diff --git a/vm/hv1/hv1_emulator/src/hv.rs b/vm/hv1/hv1_emulator/src/hv.rs index bfd8e44871..d6ebe368ae 100644 --- a/vm/hv1/hv1_emulator/src/hv.rs +++ b/vm/hv1/hv1_emulator/src/hv.rs @@ -8,6 +8,8 @@ use super::synic::ProcessorSynic; use crate::VtlProtectAccess; use crate::pages::LockedPage; use crate::pages::OverlayPage; +#[cfg(guest_arch = "aarch64")] +use aarch64defs::Vendor; use hv1_structs::VtlArray; use hvdef::HV_REFERENCE_TSC_SEQUENCE_INVALID; use hvdef::HvError; @@ -25,6 +27,7 @@ use std::sync::atomic::Ordering; use virt::x86::MsrError; use vm_topology::processor::VpIndex; use vmcore::reference_time::ReferenceTimeSource; +#[cfg(guest_arch = "x86_64")] use x86defs::cpuid::Vendor; use zerocopy::FromZeros; @@ -246,10 +249,12 @@ impl ProcessorVtlHv { if hc.enable() && (!mutable.hypercall_reg.enable() || hc.gpn() != mutable.hypercall_reg.gpn()) { + #[cfg(guest_arch = "x86_64")] let new_page = mutable .hypercall_page .remap(hc.gpn(), prot_access, true) .map_err(|_| MsrError::InvalidAccess)?; + #[cfg(guest_arch = "x86_64")] self.write_hypercall_page(new_page); } else if !hc.enable() { mutable.hypercall_page.unmap(prot_access); @@ -330,6 +335,7 @@ impl ProcessorVtlHv { Ok(()) } + #[cfg(guest_arch = "x86_64")] fn write_hypercall_page(&self, page: &LockedPage) { // Fill the page with int3 to catch invalid jumps into the page. let int3 = 0xcc; @@ -348,6 +354,7 @@ impl ProcessorVtlHv { /// Gets the VSM code page offset register that corresponds to the hypercall /// page generated by this emulator. + #[cfg(guest_arch = "x86_64")] pub fn vsm_code_page_offsets(&self, bit64: bool) -> hvdef::HvRegisterVsmCodePageOffsets { // The code page offsets are the same for all VTLs. let page = if self.partition_state.vendor.is_amd_compatible() { @@ -458,12 +465,14 @@ impl ProcessorVtlHv { } } +#[cfg(guest_arch = "x86_64")] struct HypercallPage { page: [u8; 50], offsets32: hvdef::HvRegisterVsmCodePageOffsets, offsets64: hvdef::HvRegisterVsmCodePageOffsets, } +#[cfg(guest_arch = "x86_64")] const fn hypercall_page(use_vmmcall: bool) -> HypercallPage { let [hc0, hc1, hc2] = if use_vmmcall { [0x0f, 0x01, 0xd9] // vmmcall @@ -509,7 +518,9 @@ const fn hypercall_page(use_vmmcall: bool) -> HypercallPage { } } +#[cfg(guest_arch = "x86_64")] const AMD_HYPERCALL_PAGE: HypercallPage = hypercall_page(true); +#[cfg(guest_arch = "x86_64")] const INTEL_HYPERCALL_PAGE: HypercallPage = hypercall_page(false); #[derive(Default, Inspect)] diff --git a/vm/hv1/hvdef/src/lib.rs b/vm/hv1/hvdef/src/lib.rs index 8e3f1ab21b..a0d35ef59a 100755 --- a/vm/hv1/hvdef/src/lib.rs +++ b/vm/hv1/hvdef/src/lib.rs @@ -352,6 +352,7 @@ open_enum! { VBS = 1, SNP = 2, TDX = 3, + CCA = 4, } } diff --git a/vmm_core/virt/src/aarch64/mod.rs b/vmm_core/virt/src/aarch64/mod.rs index dd6d52e7e4..9c631b863f 100644 --- a/vmm_core/virt/src/aarch64/mod.rs +++ b/vmm_core/virt/src/aarch64/mod.rs @@ -7,6 +7,7 @@ pub mod vm; pub mod vp; use crate::state::StateElement; +use aarch64defs::Vendor; use inspect::Inspect; use mesh_protobuf::Protobuf; use thiserror::Error; @@ -34,6 +35,8 @@ impl Aarch64InitialRegs { pub struct Aarch64PartitionCapabilities { /// Whether the processor supports aarch32 execution at EL0. pub supports_aarch32_el0: bool, + #[inspect(display)] + pub vendor: Vendor, } #[derive(Error, Debug)] diff --git a/vmm_core/virt/src/generic.rs b/vmm_core/virt/src/generic.rs index e31204174a..748d3f790a 100644 --- a/vmm_core/virt/src/generic.rs +++ b/vmm_core/virt/src/generic.rs @@ -97,6 +97,8 @@ pub enum IsolationType { Snp, /// Trust domain extensions (Intel TDX) - hardware based isolation. Tdx, + /// Confidential Compute Architecture (ARM CCA) - hardware based isolation. + Cca, } impl IsolationType { @@ -107,7 +109,7 @@ impl IsolationType { /// Returns whether the isolation type is hardware-backed. pub fn is_hardware_isolated(&self) -> bool { - matches!(self, Self::Snp | Self::Tdx) + matches!(self, Self::Snp | Self::Tdx | Self::Cca) } } @@ -124,6 +126,7 @@ impl IsolationType { hvdef::HvPartitionIsolationType::VBS => Ok(IsolationType::Vbs), hvdef::HvPartitionIsolationType::SNP => Ok(IsolationType::Snp), hvdef::HvPartitionIsolationType::TDX => Ok(IsolationType::Tdx), + hvdef::HvPartitionIsolationType::CCA => Ok(IsolationType::Cca), _ => Err(UnexpectedIsolationType), } } @@ -134,6 +137,7 @@ impl IsolationType { IsolationType::Vbs => hvdef::HvPartitionIsolationType::VBS, IsolationType::Snp => hvdef::HvPartitionIsolationType::SNP, IsolationType::Tdx => hvdef::HvPartitionIsolationType::TDX, + IsolationType::Cca => hvdef::HvPartitionIsolationType::CCA, } } } diff --git a/vmm_core/virt_hvf/src/lib.rs b/vmm_core/virt_hvf/src/lib.rs index ae32d12165..3cf5c7e0fe 100644 --- a/vmm_core/virt_hvf/src/lib.rs +++ b/vmm_core/virt_hvf/src/lib.rs @@ -19,6 +19,7 @@ use aarch64defs::ExceptionClass; use aarch64defs::IssDataAbort; use aarch64defs::IssSystem; use aarch64defs::MpidrEl1; +use aarch64defs::Vendor; use aarch64defs::smccc::FastCall; use aarch64defs::smccc::PsciError; use aarch64defs::smccc::SmcCall; @@ -166,6 +167,7 @@ impl virt::ProtoPartition for HvfProtoPartition<'_> { caps: Aarch64PartitionCapabilities { // Apple Silicon does not support aarch32. supports_aarch32_el0: false, + vendor: Vendor::ARM, }, virt_timer_ppi: self.config.processor_topology.virt_timer_ppi(), vps: self diff --git a/vmm_core/virt_kvm/src/arch/aarch64/mod.rs b/vmm_core/virt_kvm/src/arch/aarch64/mod.rs index 890a99b13a..e7ac0a6277 100644 --- a/vmm_core/virt_kvm/src/arch/aarch64/mod.rs +++ b/vmm_core/virt_kvm/src/arch/aarch64/mod.rs @@ -18,6 +18,7 @@ use crate::gsi::GsiRouting; use crate::gsi::KvmIrqFdState; use crate::gsi::MsiRouteBuilder; use aarch64defs::SystemReg; +use aarch64defs::Vendor; use aarch64defs::gic::GicV2mRegister; use bitfield_struct::bitfield; use core::panic; @@ -824,6 +825,7 @@ impl virt::ProtoPartition for KvmProtoPartition<'_> { pfr0 & 0xf == 2 }; PartitionCapabilities { + vendor: Vendor::ARM, supports_aarch32_el0, } }; diff --git a/vmm_core/virt_mshv/src/aarch64/mod.rs b/vmm_core/virt_mshv/src/aarch64/mod.rs index 9e95dfbe91..98512c7cb6 100644 --- a/vmm_core/virt_mshv/src/aarch64/mod.rs +++ b/vmm_core/virt_mshv/src/aarch64/mod.rs @@ -19,6 +19,7 @@ use crate::create_vm_with_retry; use aarch64defs::EsrEl2; use aarch64defs::ExceptionClass; use aarch64defs::IssDataAbort; +use aarch64defs::Vendor; use guestmem::DoorbellRegistration; use hvdef::HvArm64RegisterName; use hvdef::HvDeliverabilityNotificationsRegister; @@ -138,6 +139,7 @@ impl ProtoPartition for MshvProtoPartition<'_> { ) -> Result<(Self::Partition, Vec), Self::Error> { let caps = Aarch64PartitionCapabilities { supports_aarch32_el0: false, + vendor: Vendor::ARM, }; let inner = Arc::new(MshvPartitionInner { @@ -362,7 +364,12 @@ impl MshvProcessor<'_> { match ec { ExceptionClass::DATA_ABORT_LOWER => { - let iss = IssDataAbort::from(syndrome.iss()); + let iss = IssDataAbort::from( + u32::from(syndrome.lower_iss()) + | (u32::from(syndrome.wnr()) << 6) + | (u32::from(syndrome.mid_iss()) << 7) + | (u32::from(syndrome.b_srt()) << 16), + ); if !iss.isv() { return Err(dev.fatal_error( "data abort with no valid ISS (instruction syndrome not valid)" diff --git a/vmm_core/virt_whp/src/lib.rs b/vmm_core/virt_whp/src/lib.rs index 7611128055..50f8bd449b 100644 --- a/vmm_core/virt_whp/src/lib.rs +++ b/vmm_core/virt_whp/src/lib.rs @@ -23,6 +23,8 @@ mod vtl2; use crate::memory::vtl2_mapper::MappingState; use crate::memory::vtl2_mapper::ResetMappingState; +#[cfg(guest_arch = "aarch64")] +use aarch64defs::Vendor; use guestmem::DoorbellRegistration; use guestmem::GuestMemory; use hv1_emulator::hv::GlobalHv; @@ -72,6 +74,7 @@ use vmcore::reference_time::ReferenceTimeSource; use vmcore::vmtime::VmTimeAccess; use vmcore::vmtime::VmTimeSource; use vp_state::WhpVpStateAccess; +#[cfg(guest_arch = "x86_64")] use x86defs::cpuid::Vendor; #[cfg(guest_arch = "aarch64")] @@ -1089,6 +1092,18 @@ impl WhpPartitionInner { caps.dr6_tsx_broken = true; caps }; + let vendor = match whp::capabilities::processor_vendor().for_op("get processor vendor")? { + #[cfg(guest_arch = "x86_64")] + whp::abi::WHvProcessorVendorIntel => Vendor::INTEL, + #[cfg(guest_arch = "x86_64")] + whp::abi::WHvProcessorVendorAmd => Vendor::AMD, + #[cfg(guest_arch = "x86_64")] + whp::abi::WHvProcessorVendorHygon => Vendor::HYGON, + #[cfg(guest_arch = "aarch64")] + whp::abi::WHvProcessorVendorArm => Vendor::ARM, + _ => panic!("unsupported processor vendor"), + }; + #[cfg(guest_arch = "aarch64")] let caps = { let features = @@ -1097,20 +1112,10 @@ impl WhpPartitionInner { supports_aarch32_el0: features .bank0 .is_set(whp::abi::WHV_PROCESSOR_FEATURES::El0Aarch32), + vendor, } }; - let vendor = match whp::capabilities::processor_vendor().for_op("get processor vendor")? { - whp::abi::WHvProcessorVendorIntel => Vendor::INTEL, - #[cfg(guest_arch = "x86_64")] - whp::abi::WHvProcessorVendorAmd => Vendor::AMD, - #[cfg(guest_arch = "x86_64")] - whp::abi::WHvProcessorVendorHygon => Vendor::HYGON, - #[cfg(guest_arch = "aarch64")] - whp::abi::WHvProcessorVendorArm => Vendor([0; 12]), - _ => panic!("unsupported processor vendor"), - }; - let hvstate = if proto_config.hv_config.is_some() { if vtl0.hypervisor_enlightened { Hv1State::Offloaded diff --git a/vmm_tests/vmm_tests/test_data/cca_planes.yaml b/vmm_tests/vmm_tests/test_data/cca_planes.yaml new file mode 100644 index 0000000000..b0dc7c1fd7 --- /dev/null +++ b/vmm_tests/vmm_tests/test_data/cca_planes.yaml @@ -0,0 +1,83 @@ +# +# SPDX-License-Identifier: BSD-3-Clause +# SPDX-FileCopyrightText: Copyright TF-RMM Contributors. +# +# This file is consumed by Arm software emulator FVP (Fixed Virtual Platform) +# when running OpenVMM CCA tests +%YAML 1.2 +--- +description: >- + Overlay to build TF-RMM with standard build settings. + In order to use this overlay, the RMM_SRC btvar must be defined containing + the absolute path for the local TF-RMM repository. + + The overlay allows to overwrite the default log level for TF-RMM, by + using ``RMM_LOG_LEVEL`` btvar as well as to define the SHA/Branch of TF-A + to be used for the particular instance of RMM to build through the + ``TFA_REVISION`` btvar. It also allows to define the host platform through + the ``RMM_CONFIG`` btvar. + +buildex: + btvars: + # Determines the RMM Log level + RMM_LOG_LEVEL: + type: string + value: '10' + + # Determines the RMM Configuration (a.k.a. platform) to build + RMM_CONFIG: + type: string + value: 'fvp_defcfg' + + # Determines the branch or SHA to pull for TF-A which is suitable + # for the current RMM + TFA_REVISION: + type: string + value: master + +build: + rmm: + params: + -DLOG_LEVEL: ${btvar:RMM_LOG_LEVEL} + -DRMM_CONFIG: ${btvar:RMM_CONFIG} + -DRMM_V1_1: 1 + -DPLAT_CMN_CTX_MAX_XLAT_TABLES: 13 + repo: + remote: https://github.com/TF-RMM/tf-rmm.git + revision: 9a98e8fcb1645b9917b2abd79212e6e3062e09fd + tfa: + params: + GIC_ENABLE_V4_EXTN: 1 + repo: + revision: ${btvar:TFA_REVISION} + linux: + repo: + remote: https://gitlab.arm.com/linux-arm/linux-cca.git + revision: cca/planes/rfc-v1 + prebuild: + # Use source dir modification time as timestamp (for locally reproducible build) + - export KBUILD_BUILD_TIMESTAMP="@$$(stat -c '%Y' ${param:sourcedir})" + - ./scripts/config --file ${param:builddir}/.config --enable CONFIG_VIRT_DRIVERS + # Reduce the number of timer exits from the guest + - ./scripts/config --file ${param:builddir}/.config --disable CONFIG_HZ_250 --enable CONFIG_HZ_100 + - ./scripts/config --file ${param:builddir}/.config --disable CONFIG_RANDOMIZE_BASE + kvmtool: + repo: + kvmtool: + remote: https://gitlab.arm.com/linux-arm/kvmtool-cca.git + revision: cca/planes/rfc-v1 + + +run: + params: + # Enable Permission Indirection + -C cluster0.has_permission_indirection_s1: 2 + -C cluster1.has_permission_indirection_s1: 2 + -C cluster0.has_permission_indirection_s2: 2 + -C cluster1.has_permission_indirection_s2: 2 + + # Enable Permission Overlay + -C cluster0.has_permission_overlay_s1: 2 + -C cluster1.has_permission_overlay_s1: 2 + -C cluster0.has_permission_overlay_s2: 2 + -C cluster1.has_permission_overlay_s2: 2 diff --git a/vmm_tests/vmm_tests/test_data/cca_realm_overlay.yaml b/vmm_tests/vmm_tests/test_data/cca_realm_overlay.yaml new file mode 100644 index 0000000000..8d9592a3bd --- /dev/null +++ b/vmm_tests/vmm_tests/test_data/cca_realm_overlay.yaml @@ -0,0 +1,157 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +%YAML 1.2 +--- +description: >- + Buildroot overlay that installs realm auto-launch and injects + Plane0 start-tmk.sh via lkvm 9P root. + +build: + buildroot: + prebuild: + - mkdir -p ${param:builddir}/overlay/usr/local/bin + - mkdir -p ${param:builddir}/overlay/etc/init.d + + # ========================= + # run_realm_test.sh + # ========================= + - | + cat > ${param:builddir}/overlay/usr/local/bin/run_realm_test.sh <<'SH' + #!/usr/bin/env bash + set -euo pipefail + + echo "[realm-launch] Starting..." + date + + CCA_DIR="$${CCA_DIR:-/cca}" + LKVM="$${CCA_DIR}/lkvm" + KERNEL="$${KERNEL:-/cca/Image}" + GUESTROOT="/.lkvm/default" + + # Wait for artifacts + for i in $$(seq 1 30); do + echo "[realm-launch] waiting for artifacts: $$i" + if [ -x "$$LKVM" ] && [ -f "$$KERNEL" ]; then + break + fi + sleep 1 + done + + if [ ! -x "$$LKVM" ]; then + echo "[realm-launch][ERROR] Missing $$LKVM" + exit 1 + fi + + if [ ! -f "$$KERNEL" ]; then + echo "[realm-launch][ERROR] Missing $$KERNEL" + exit 1 + fi + + # ========================= + # Background watcher + # ========================= + prepare_plane0_hook() { + echo "[realm-launch] watcher waiting for Plane0 root tree..." + + for i in $$(seq 1 300); do + if [ -d "$$GUESTROOT/root" ] && [ -e "$$GUESTROOT/virt/init" ]; then + echo "[realm-launch] Plane0 root tree found" + break + fi + echo "[realm-launch] waiting for Plane0 root tree: $$i/300" + sleep 1 + done + + if [ ! -d "$$GUESTROOT/root" ]; then + echo "[realm-launch][ERROR] Plane0 root tree did not appear" + return 1 + fi + + + echo "[realm-launch] installing busybox..." + + cp /bin/busybox "$$GUESTROOT/root/busybox" + chmod 755 "$$GUESTROOT/root/busybox" + + echo "[realm-launch] installing start-tmk.sh..." + + cat > "$$GUESTROOT/root/start-tmk.sh" <<'EOF' + #!/root/busybox sh + set -e + + echo "[plane0] start-tmk.sh reached" + + mkdir -p /root/mount + mount -t 9p -o trans=virtio cca_mount /root/mount + + cd /root/mount + export RUST_BACKTRACE=1 + + echo "[plane0] Launching tmk_vmm..." + exec ./tmk_vmm --hv cca --tmk ./simple_tmk + EOF + + chmod 755 "$$GUESTROOT/root/start-tmk.sh" + + # Debug to verify correct shebang + interpreter + od -An -tx1 -N16 "$$GUESTROOT/root/start-tmk.sh" >/dev/console 2>&1 || true + head -1 "$$GUESTROOT/root/start-tmk.sh" >/dev/console 2>&1 || true + + ls -l "$$GUESTROOT/root/busybox" >/dev/console 2>&1 || true + head -1 "$$GUESTROOT/root/start-tmk.sh" >/dev/console 2>&1 || true + + echo "[realm-launch] Installed $$GUESTROOT/root/start-tmk.sh" + } + + # Start watcher in background + prepare_plane0_hook & + + cd "$$CCA_DIR" + + echo "[realm-launch] Launching lkvm..." + + exec ./lkvm run --realm --disable-sve --irqchip=gicv3-its \ + -c 1 -m 512 \ + --no-pvtime --force-pci \ + --console virtio \ + --kernel "$$KERNEL" \ + --9p /cca/,cca_mount \ + -p "console=hvc0 root=/dev/vda2" \ + --measurement-algo=sha256 \ + --restricted_mem + SH + + # ========================= + # S99realm-launch + # ========================= + - | + cat > ${param:builddir}/overlay/etc/init.d/S99realm-launch <<'SH' + #!/bin/sh + + case "$$1" in + start|"") + echo "[realm-launch] starting on /dev/console" >/dev/console + if [ -x /usr/local/bin/run_realm_test.sh ]; then + exec /dev/console 2>&1 /usr/local/bin/run_realm_test.sh + fi + ;; + stop) + ;; + restart) + ;; + *) + echo "Usage: $$0 {start|stop|restart}" + exit 1 + ;; + esac + + exit 0 + SH + + # Permissions + - chmod 0755 ${param:builddir}/overlay/usr/local/bin/run_realm_test.sh + - chmod 0755 ${param:builddir}/overlay/etc/init.d/S99realm-launch + + # Enable overlay + - ${param:sourcedir}/utils/config --file ${param:builddir}/.config --set-val BR2_ROOTFS_OVERLAY "${param:builddir}/overlay"