Skip to content

Commit 2f599cf

Browse files
authored
Handle custom HVCs in platform code (#30)
Add `handle_hvc` to the `Platform` trait, enabling custom EL2 HVC calls for specific platforms. Unhandled HVCs are properly trapped and routed to this method. If a platform doesn't handle the HVC, it now returns SMCCC_NOT_SUPPORTED (-1) to the guest instead of panicking. Added a dummy HVC (0xFF00_0000) to the QEMU platform implementation for testing purposes. The `integration_test` (formerly called `isolation_test`) validates both successful dummy HVC execution and expected failure for unknown HVCs.
1 parent d854d83 commit 2f599cf

File tree

13 files changed

+212
-45
lines changed

13 files changed

+212
-45
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[workspace]
22
members = [
33
".",
4-
"tests/isolation_test",
4+
"tests/integration_test",
55
]
66

77
[package]

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ qemu: $(QEMU_BIN)
4242
-append "ritm.boot_mode=el1"
4343

4444
test:
45-
tests/isolation_test.py
45+
tests/integration_test.py
4646

4747
clean:
4848
cargo clean

src/hvc_response.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
// option. This file may not be copied, modified, or distributed
7+
// except according to those terms.
8+
9+
use aarch64_rt::RegisterStateRef;
10+
use core::fmt::{Debug, Formatter};
11+
use log::debug;
12+
use smccc::arch::Error::NotSupported;
13+
14+
/// The result of an HVC call handled by the platform.
15+
#[derive(Copy, Clone, PartialEq, Eq)]
16+
pub enum HvcResponse {
17+
/// The HVC call was handled, and returns the provided values in x0-x3.
18+
/// x4-x17 are preserved.
19+
Success([u64; 4]),
20+
/// The HVC call was handled, and returns the provided values in x0-x17.
21+
SuccessLarge([u64; 18]),
22+
}
23+
24+
impl Debug for HvcResponse {
25+
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
26+
let regs = match self {
27+
HvcResponse::Success(regs) => regs.as_slice(),
28+
HvcResponse::SuccessLarge(regs) => regs.as_slice(),
29+
};
30+
31+
let mut d = f.debug_tuple("HvcResponse");
32+
for reg in regs {
33+
d.field(&format_args!("0x{reg:x}"));
34+
}
35+
d.finish()
36+
}
37+
}
38+
39+
/// The result of an HVC call.
40+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
41+
pub enum HvcResult {
42+
/// The HVC call was not handled.
43+
Unhandled,
44+
/// The HVC call was handled, and either succeeded or failed with an error code.
45+
Handled(Result<HvcResponse, smccc::arch::Error>),
46+
}
47+
48+
impl From<u64> for HvcResponse {
49+
fn from(value: u64) -> Self {
50+
HvcResponse::Success([value, 0, 0, 0])
51+
}
52+
}
53+
54+
impl From<[u64; 4]> for HvcResponse {
55+
fn from(value: [u64; 4]) -> Self {
56+
HvcResponse::Success(value)
57+
}
58+
}
59+
60+
impl From<[u64; 18]> for HvcResponse {
61+
fn from(value: [u64; 18]) -> Self {
62+
HvcResponse::SuccessLarge(value)
63+
}
64+
}
65+
66+
impl HvcResult {
67+
pub(crate) fn modify_register_state(self, register_state: &mut RegisterStateRef) {
68+
// SAFETY: We are just answering the guest call.
69+
let regs = unsafe { register_state.get_mut() };
70+
match self {
71+
HvcResult::Handled(Ok(HvcResponse::Success(results))) => {
72+
regs.registers[0..4].copy_from_slice(&results);
73+
}
74+
HvcResult::Handled(Ok(HvcResponse::SuccessLarge(results))) => {
75+
regs.registers[0..18].copy_from_slice(&results);
76+
}
77+
HvcResult::Handled(Err(error)) => {
78+
regs.registers[0] = error_to_u64(error);
79+
}
80+
HvcResult::Unhandled => {
81+
debug!("HVC call not handled, returning NOT_SUPPORTED");
82+
regs.registers[0] = error_to_u64(NotSupported);
83+
}
84+
}
85+
}
86+
}
87+
88+
#[must_use]
89+
fn error_to_u64(error: smccc::arch::Error) -> u64 {
90+
i64::from(i32::from(error)).cast_unsigned()
91+
}

src/hypervisor.rs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// option. This file may not be copied, modified, or distributed
77
// except according to those terms.
88

9+
use crate::hvc_response::{HvcResponse, HvcResult};
910
use crate::{
1011
arch,
1112
platform::{Platform, PlatformImpl},
@@ -210,17 +211,19 @@ pub fn handle_sync_lower(mut register_state: RegisterStateRef) {
210211

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

215-
match function_id {
217+
let result = match function_id {
216218
0x8400_0000..=0x8400_001F | 0xC400_0000..=0xC400_001F => {
217-
try_handle_psci(&mut register_state)
218-
.expect("Unknown PSCI call: {register_state:?}");
219+
HvcResult::Handled(try_handle_psci(function_id, args[0], args[1], args[2]))
219220
}
220221
_ => {
221-
panic!("Unknown HVC/SMC call: function_id={function_id:x}; {register_state:?}");
222+
debug!("Forwarding HVC call to platform");
223+
PlatformImpl::handle_hvc(function_id, args)
222224
}
223-
}
225+
};
226+
result.modify_register_state(&mut register_state);
224227
}
225228
ExceptionClass::DataAbortLowerEL => {
226229
inject_data_abort(&mut register_state);
@@ -302,26 +305,26 @@ fn inject_data_abort(register_state: &mut RegisterStateRef) {
302305
regs.spsr = spsr.bits();
303306
}
304307

305-
fn try_handle_psci(register_state: &mut RegisterStateRef) -> Result<(), arm_psci::Error> {
306-
let [fn_id, arg0, arg1, arg2, ..] = register_state.registers;
308+
fn try_handle_psci(
309+
fn_id: u64,
310+
arg0: u64,
311+
arg1: u64,
312+
arg2: u64,
313+
) -> Result<HvcResponse, smccc::arch::Error> {
307314
debug!(
308315
"Forwarding the PSCI call: fn_id={fn_id:#x}, arg0={arg0:#x}, arg1={arg1:#x}, arg2={arg2:#x}"
309316
);
310317

311-
let out = handle_psci(fn_id, arg0, arg1, arg2)?;
312-
debug!("PSCI call output: out={out:#x}");
313-
314-
// SAFETY: This is an answer to the guest calling HVC/SMC, so it expects x0..3 will
315-
// get overwritten.
316-
unsafe {
317-
let regs = register_state.get_mut();
318-
regs.registers[0] = out;
319-
regs.registers[1] = 0;
320-
regs.registers[2] = 0;
321-
regs.registers[3] = 0;
322-
}
318+
let out = match handle_psci(fn_id, arg0, arg1, arg2) {
319+
Ok(val) => Ok(HvcResponse::from(val)),
320+
Err(error) => {
321+
let error_code = arm_psci::ErrorCode::from(error);
322+
Err(smccc::arch::Error::from(i32::from(error_code)))
323+
}
324+
};
325+
debug!("PSCI call output: out={out:?}");
323326

324-
Ok(())
327+
out
325328
}
326329

327330
/// Handles a PSCI call.

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ extern crate alloc;
1414
mod arch;
1515
mod console;
1616
mod exceptions;
17+
mod hvc_response;
1718
mod hypervisor;
1819
mod logger;
1920
mod pagetable;

src/platform.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#[cfg(platform = "qemu")]
1010
mod qemu;
1111

12+
use crate::hvc_response::HvcResult;
1213
use aarch64_paging::idmap::IdMap;
1314
use aarch64_paging::paging::{PAGE_SIZE, Stage2};
1415
use dtoolkit::fdt::Fdt;
@@ -63,6 +64,16 @@ pub trait Platform {
6364
/// The page table should typically unmap the part of the memory where RITM resides, so that
6465
/// the guest cannot interact with it in any way.
6566
fn make_stage2_pagetable() -> IdMap<Stage2>;
67+
68+
/// Handles a custom HVC call.
69+
///
70+
/// The default implementation returns `HvcResult::Unhandled`, indicating the call was not handled.
71+
/// If handled, it should return the corresponding `HvcResult` which specifies how the registers
72+
/// should be updated.
73+
fn handle_hvc(function_id: u64, args: [u64; 17]) -> HvcResult {
74+
let _ = (function_id, args);
75+
HvcResult::Unhandled
76+
}
6677
}
6778

6879
#[derive(Debug, Copy, Clone, PartialEq, Eq)]

src/platform/qemu.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/// The QEMU aarch64 virt platform.
1010
use super::{FDT_ALIGNMENT, Platform, PlatformParts};
11+
use crate::hvc_response::HvcResult;
1112
use crate::pagetable::{STAGE2_DEVICE_ATTRIBUTES, STAGE2_MEMORY_ATTRIBUTES};
1213
use crate::{
1314
pagetable::{DEVICE_ATTRIBUTES, MEMORY_ATTRIBUTES},
@@ -178,4 +179,13 @@ impl Platform for Qemu {
178179

179180
idmap
180181
}
182+
183+
fn handle_hvc(function_id: u64, _args: [u64; 17]) -> HvcResult {
184+
// Dummy HVC for testing
185+
if function_id == 0xFF00_0000 {
186+
return HvcResult::Handled(Ok(0x1234_5678_9ABC_DEF0.into()));
187+
}
188+
189+
HvcResult::Unhandled
190+
}
181191
}
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
from pathlib import Path
1616

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

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

3030
def main():
31-
print("Building isolation_test payload...")
31+
print("Building integration_test payload...")
3232
env = os.environ.copy()
3333
run_command(
34-
["cargo", "build", "--release", "--locked", "--target", "aarch64-unknown-none", "-p", "isolation_test"],
34+
["cargo", "build", "--release", "--locked", "--target", "aarch64-unknown-none", "-p", "integration_test"],
3535
cwd=TEST_DIR,
3636
env=env
3737
)
3838

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

@@ -61,8 +61,9 @@ def main():
6161
env=env
6262
)
6363

64-
success_string = "Caught expected Data Abort! Isolation test passed."
65-
failure_string = "FAILED"
64+
success_string = "TEST: All tests passed!"
65+
failure_string = "PANIC"
66+
6667
start_time = time.time()
6768
timeout = 30 # seconds
6869

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "isolation_test"
2+
name = "integration_test"
33
version = "0.1.0"
44
edition = "2024"
55
publish = false

0 commit comments

Comments
 (0)