Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
members = [
".",
"tests/isolation_test",
"tests/integration_test",
]

[package]
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ qemu: $(QEMU_BIN)
-append "ritm.boot_mode=el1"

test:
tests/isolation_test.py
tests/integration_test.py

clean:
cargo clean
Expand Down
91 changes: 91 additions & 0 deletions src/hvc_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use aarch64_rt::RegisterStateRef;
use core::fmt::{Debug, Formatter};
use log::debug;
use smccc::arch::Error::NotSupported;

/// The result of an HVC call handled by the platform.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum HvcResponse {
/// The HVC call was handled, and returns the provided values in x0-x3.
/// x4-x17 are preserved.
Success([u64; 4]),
/// The HVC call was handled, and returns the provided values in x0-x17.
SuccessLarge([u64; 18]),
}

impl Debug for HvcResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
let regs = match self {
HvcResponse::Success(regs) => regs.as_slice(),
HvcResponse::SuccessLarge(regs) => regs.as_slice(),
};

let mut d = f.debug_tuple("HvcResponse");
for reg in regs {
d.field(&format_args!("0x{reg:x}"));
}
d.finish()
}
}

/// The result of an HVC call.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HvcResult {
/// The HVC call was not handled.
Unhandled,
/// The HVC call was handled, and either succeeded or failed with an error code.
Handled(Result<HvcResponse, smccc::arch::Error>),
}

impl From<u64> for HvcResponse {
fn from(value: u64) -> Self {
HvcResponse::Success([value, 0, 0, 0])
}
}

impl From<[u64; 4]> for HvcResponse {
fn from(value: [u64; 4]) -> Self {
HvcResponse::Success(value)
}
}

impl From<[u64; 18]> for HvcResponse {
fn from(value: [u64; 18]) -> Self {
HvcResponse::SuccessLarge(value)
}
}

impl HvcResult {
pub(crate) fn modify_register_state(self, register_state: &mut RegisterStateRef) {
// SAFETY: We are just answering the guest call.
let regs = unsafe { register_state.get_mut() };
match self {
HvcResult::Handled(Ok(HvcResponse::Success(results))) => {
regs.registers[0..4].copy_from_slice(&results);
}
HvcResult::Handled(Ok(HvcResponse::SuccessLarge(results))) => {
regs.registers[0..18].copy_from_slice(&results);
}
HvcResult::Handled(Err(error)) => {
regs.registers[0] = error_to_u64(error);
}
HvcResult::Unhandled => {
debug!("HVC call not handled, returning NOT_SUPPORTED");
regs.registers[0] = error_to_u64(NotSupported);
}
}
}
}

#[must_use]
fn error_to_u64(error: smccc::arch::Error) -> u64 {
i64::from(i32::from(error)).cast_unsigned()
}
45 changes: 24 additions & 21 deletions src/hypervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use crate::hvc_response::{HvcResponse, HvcResult};
use crate::{
arch,
platform::{Platform, PlatformImpl},
Expand Down Expand Up @@ -210,17 +211,19 @@ pub fn handle_sync_lower(mut register_state: RegisterStateRef) {

match ec {
ExceptionClass::HvcTrappedInAArch64 | ExceptionClass::SmcTrappedInAArch64 => {
let function_id = register_state.registers[0];
let [function_id, args @ .., _] = register_state.registers;
debug!("HVC/SMC call: function_id={function_id:#x}");

match function_id {
let result = match function_id {
0x8400_0000..=0x8400_001F | 0xC400_0000..=0xC400_001F => {
try_handle_psci(&mut register_state)
.expect("Unknown PSCI call: {register_state:?}");
HvcResult::Handled(try_handle_psci(function_id, args[0], args[1], args[2]))
}
_ => {
panic!("Unknown HVC/SMC call: function_id={function_id:x}; {register_state:?}");
debug!("Forwarding HVC call to platform");
PlatformImpl::handle_hvc(function_id, args)
}
}
};
result.modify_register_state(&mut register_state);
}
ExceptionClass::DataAbortLowerEL => {
inject_data_abort(&mut register_state);
Expand Down Expand Up @@ -302,26 +305,26 @@ fn inject_data_abort(register_state: &mut RegisterStateRef) {
regs.spsr = spsr.bits();
}

fn try_handle_psci(register_state: &mut RegisterStateRef) -> Result<(), arm_psci::Error> {
let [fn_id, arg0, arg1, arg2, ..] = register_state.registers;
fn try_handle_psci(
fn_id: u64,
arg0: u64,
arg1: u64,
arg2: u64,
) -> Result<HvcResponse, smccc::arch::Error> {
debug!(
"Forwarding the PSCI call: fn_id={fn_id:#x}, arg0={arg0:#x}, arg1={arg1:#x}, arg2={arg2:#x}"
);

let out = handle_psci(fn_id, arg0, arg1, arg2)?;
debug!("PSCI call output: out={out:#x}");

// SAFETY: This is an answer to the guest calling HVC/SMC, so it expects x0..3 will
// get overwritten.
unsafe {
let regs = register_state.get_mut();
regs.registers[0] = out;
regs.registers[1] = 0;
regs.registers[2] = 0;
regs.registers[3] = 0;
}
let out = match handle_psci(fn_id, arg0, arg1, arg2) {
Ok(val) => Ok(HvcResponse::from(val)),
Err(error) => {
let error_code = arm_psci::ErrorCode::from(error);
Err(smccc::arch::Error::from(i32::from(error_code)))
}
};
debug!("PSCI call output: out={out:?}");

Ok(())
out
}

/// Handles a PSCI call.
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ extern crate alloc;
mod arch;
mod console;
mod exceptions;
mod hvc_response;
mod hypervisor;
mod logger;
mod pagetable;
Expand Down
11 changes: 11 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#[cfg(platform = "qemu")]
mod qemu;

use crate::hvc_response::HvcResult;
use aarch64_paging::idmap::IdMap;
use aarch64_paging::paging::{PAGE_SIZE, Stage2};
use dtoolkit::fdt::Fdt;
Expand Down Expand Up @@ -63,6 +64,16 @@ pub trait Platform {
/// The page table should typically unmap the part of the memory where RITM resides, so that
/// the guest cannot interact with it in any way.
fn make_stage2_pagetable() -> IdMap<Stage2>;

/// Handles a custom HVC call.
///
/// The default implementation returns `HvcResult::Unhandled`, indicating the call was not handled.
/// If handled, it should return the corresponding `HvcResult` which specifies how the registers
/// should be updated.
fn handle_hvc(function_id: u64, args: [u64; 17]) -> HvcResult {
let _ = (function_id, args);
HvcResult::Unhandled
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand Down
10 changes: 10 additions & 0 deletions src/platform/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/// The QEMU aarch64 virt platform.
use super::{FDT_ALIGNMENT, Platform, PlatformParts};
use crate::hvc_response::HvcResult;
use crate::pagetable::{STAGE2_DEVICE_ATTRIBUTES, STAGE2_MEMORY_ATTRIBUTES};
use crate::{
pagetable::{DEVICE_ATTRIBUTES, MEMORY_ATTRIBUTES},
Expand Down Expand Up @@ -178,4 +179,13 @@ impl Platform for Qemu {

idmap
}

fn handle_hvc(function_id: u64, _args: [u64; 17]) -> HvcResult {
// Dummy HVC for testing
if function_id == 0xFF00_0000 {
return HvcResult::Handled(Ok(0x1234_5678_9ABC_DEF0.into()));
}

HvcResult::Unhandled
}
}
17 changes: 9 additions & 8 deletions tests/isolation_test.py → tests/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from pathlib import Path

PROJECT_ROOT = Path(__file__).parent.parent.resolve()
TEST_DIR = PROJECT_ROOT / "tests" / "isolation_test"
PAYLOAD_ELF = PROJECT_ROOT / "target" / "aarch64-unknown-none" / "release" / "isolation_test"
PAYLOAD_BIN = PROJECT_ROOT / "target" / "aarch64-unknown-none" / "release" / "isolation_test.bin"
TEST_DIR = PROJECT_ROOT / "tests" / "integration_test"
PAYLOAD_ELF = PROJECT_ROOT / "target" / "aarch64-unknown-none" / "release" / "integration_test"
PAYLOAD_BIN = PROJECT_ROOT / "target" / "aarch64-unknown-none" / "release" / "integration_test.bin"

def run_command(cmd, cwd=None, env=None, check=True):
print(f"Running: {' '.join(str(c) for c in cmd)}")
Expand All @@ -28,17 +28,17 @@ def run_command(cmd, cwd=None, env=None, check=True):
return result

def main():
print("Building isolation_test payload...")
print("Building integration_test payload...")
env = os.environ.copy()
run_command(
["cargo", "build", "--release", "--locked", "--target", "aarch64-unknown-none", "-p", "isolation_test"],
["cargo", "build", "--release", "--locked", "--target", "aarch64-unknown-none", "-p", "integration_test"],
cwd=TEST_DIR,
env=env
)

print("Creating payload binary...")
run_command(
["cargo", "objcopy", "--target", "aarch64-unknown-none", "-p", "isolation_test", "--", "-O", "binary", str(PAYLOAD_BIN)],
["cargo", "objcopy", "--target", "aarch64-unknown-none", "-p", "integration_test", "--", "-O", "binary", str(PAYLOAD_BIN)],
cwd=PROJECT_ROOT
)

Expand All @@ -61,8 +61,9 @@ def main():
env=env
)

success_string = "Caught expected Data Abort! Isolation test passed."
failure_string = "FAILED"
success_string = "TEST: All tests passed!"
failure_string = "PANIC"

start_time = time.time()
timeout = 30 # seconds

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "isolation_test"
name = "integration_test"
version = "0.1.0"
edition = "2024"
publish = false
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading