Skip to content

Commit d91e3dc

Browse files
committed
Add stage-2 pagetables
This makes the RITM memory invisible from the guest.
1 parent 9a4eddc commit d91e3dc

File tree

15 files changed

+463
-20
lines changed

15 files changed

+463
-20
lines changed

.github/workflows/rust.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,26 @@ jobs:
3131
- uses: actions/checkout@v6
3232
- name: Format Rust code
3333
run: cargo fmt --all -- --check
34+
35+
test:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v6
39+
- name: Install aarch64 toolchain
40+
uses: dtolnay/rust-toolchain@v1
41+
with:
42+
toolchain: stable
43+
targets: aarch64-unknown-none
44+
components: llvm-tools
45+
- name: Install QEMU
46+
run: |
47+
sudo apt-get update && \
48+
sudo apt-get install -y --no-install-recommends qemu-system-arm ipxe-qemu seabios
49+
- name: Install cargo-binutils
50+
uses: taiki-e/install-action@v2
51+
with:
52+
tool: cargo-binutils
53+
- name: Prepare QEMU
54+
run: sudo chown $(whoami):$(whoami) /dev/vhost-vsock && sudo chmod g+rw /dev/vhost-vsock
55+
- name: Run the tests
56+
run: make test

Cargo.lock

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
[workspace]
2+
members = [
3+
".",
4+
"tests/isolation_test",
5+
]
6+
17
[package]
28
name = "ritm"
39
version = "0.1.0"
@@ -10,7 +16,7 @@ keywords = ["arm", "aarch64", "cortex-a", "osdev"]
1016
categories = ["embedded", "no-std"]
1117

1218
[dependencies]
13-
aarch64-paging = { version = "0.11", default-features = false }
19+
aarch64-paging = "0.11"
1420
aarch64-rt = { version = "0.4", features = ["el2", "exceptions", "initial-pagetable", "psci"], default-features = false }
1521
arm-pl011-uart = "0.4"
1622
arm-psci = "0.2"
@@ -25,6 +31,7 @@ spin = { version = "0.10", features = ["lazy", "once", "spin_mutex"], default-fe
2531

2632
[patch.crates-io]
2733
aarch64-rt = { git = "https://github.com/m4tx/aarch64-rt.git", rev = "b5783edd5e5d5555105946c462646572d3454d99" }
34+
aarch64-paging = { git = "https://github.com/m4tx/aarch64-paging.git", rev = "6e28cdee945701821b7b5721b500e8e93f1a99b5" }
2835

2936
[lints.rust]
3037
deprecated-safe = "warn"

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ PAYLOAD ?= payload.bin
1313
QEMU_BIN := target/ritm.qemu.bin
1414
QEMU_RUSTFLAGS := "--cfg platform=\"qemu\""
1515

16-
.PHONY: all build.qemu clean clippy qemu
16+
.PHONY: all build.qemu clean clippy qemu test
1717

1818
all: $(QEMU_BIN)
1919

@@ -37,6 +37,9 @@ qemu: $(QEMU_BIN)
3737
-device virtconsole,chardev=char0 \
3838
-device vhost-vsock-device,id=virtiosocket0,guest-cid=102
3939

40+
test:
41+
tests/isolation_test.py
42+
4043
clean:
4144
cargo clean
4245
rm -f target/*.bin

src/arch.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ pub fn isb() {
3434
}
3535
}
3636

37+
/// TLBI VMALLS12E1 - VMID-based Stage-1/Stage-2 combined invalidation for the EL1&0 regime.
38+
pub fn tlbi_vmalls12e1() {
39+
// SAFETY: TLBI VMALLS12E1 is always safe.
40+
unsafe {
41+
asm!("tlbi vmalls12e1", options(nostack, preserves_flags));
42+
}
43+
}
44+
3745
pub fn esr() -> u64 {
3846
let mut esr: u64;
3947
// SAFETY: Reading esr is always safe.
@@ -106,7 +114,8 @@ sys_reg!(ccsidr_el1);
106114
sys_reg!(hcr_el2, {
107115
RW: 1 << 31,
108116
TSC: 1 << 19,
109-
IMO: 1 << 4
117+
IMO: 1 << 4,
118+
VM: 1 << 0
110119
});
111120
sys_reg!(cntvoff_el2);
112121
sys_reg!(cnthctl_el2, {
@@ -120,6 +129,20 @@ sys_reg!(spsr_el2, {
120129
sys_reg!(elr_el2);
121130
sys_reg!(sp_el1);
122131
sys_reg!(mpidr_el1);
132+
sys_reg!(vtcr_el2, {
133+
PS_40BIT: 2 << 16,
134+
TG0_4KB: 0 << 14,
135+
SH0_INNER: 3 << 12,
136+
ORGN0_WB_RA_WA: 1 << 10,
137+
IRGN0_WB_RA_WA: 1 << 8,
138+
SL0_L0: 2 << 6,
139+
T0SZ_40BIT: 24
140+
});
141+
sys_reg!(vbar_el1);
142+
sys_reg!(elr_el1);
143+
sys_reg!(spsr_el1);
144+
sys_reg!(esr_el1);
145+
sys_reg!(far_el1);
123146

124147
/// Disables MMU and caches.
125148
///

src/hypervisor.rs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ pub unsafe fn entry_point_el1(arg0: u64, arg1: u64, arg2: u64, arg3: u64, entry_
3333
// Setup EL1
3434
// SAFETY: We are configuring HCR_EL2 to allow EL1 execution.
3535
unsafe {
36+
setup_stage2();
37+
3638
let mut hcr = arch::hcr_el2::read();
3739
hcr |= arch::hcr_el2::RW;
3840
hcr |= arch::hcr_el2::TSC;
41+
hcr |= arch::hcr_el2::VM;
3942
hcr &= !arch::hcr_el2::IMO;
4043
arch::hcr_el2::write(hcr);
4144
}
@@ -81,6 +84,37 @@ pub unsafe fn entry_point_el1(arg0: u64, arg1: u64, arg2: u64, arg3: u64, entry_
8184
}
8285
}
8386

87+
fn setup_stage2() {
88+
debug!("Setting up stage 2 page table");
89+
let idmap = Box::new(PlatformImpl::make_stage2_pagetable());
90+
91+
let root_pa = idmap.root_address().0;
92+
debug!("Root PA: {root_pa:#x}");
93+
let idmap = Box::leak(idmap);
94+
95+
// Activate the page table
96+
// SAFETY: We are initializing the Stage 2 translation. The guest is not running yet.
97+
unsafe {
98+
let ttbr = idmap.activate();
99+
debug!("idmap.activate() returned ttbr={ttbr:#x}");
100+
101+
let vtcr = arch::vtcr_el2::PS_40BIT
102+
| arch::vtcr_el2::TG0_4KB
103+
| arch::vtcr_el2::SH0_INNER
104+
| arch::vtcr_el2::ORGN0_WB_RA_WA
105+
| arch::vtcr_el2::IRGN0_WB_RA_WA
106+
| arch::vtcr_el2::SL0_L0
107+
| arch::vtcr_el2::T0SZ_40BIT;
108+
debug!("Writing VTCR_EL2={vtcr:#x}...");
109+
arch::vtcr_el2::write(vtcr);
110+
111+
arch::tlbi_vmalls12e1();
112+
arch::dsb();
113+
arch::isb();
114+
debug!("Stage 2 activation complete.");
115+
}
116+
}
117+
84118
/// Returns to EL1.
85119
///
86120
/// This function executes the `eret` instruction to return to EL1 with the provided arguments.
@@ -122,13 +156,53 @@ pub fn handle_sync_lower(mut register_state: RegisterStateRef) {
122156
}
123157
}
124158
}
125-
ExceptionClass::Unknown(_) => {
159+
ExceptionClass::Unknown(val) => {
126160
panic!(
127-
"Unexpected sync_lower, far={:#x}, register_state={register_state:?}",
161+
"Unexpected sync_lower, esr={esr:#x}, ec={val:#x}, far={:#x}, register_state={register_state:?}",
128162
far(),
129163
);
130164
}
165+
ExceptionClass::DataAbortLowerEL => {
166+
inject_data_abort(&mut register_state);
167+
}
168+
}
169+
}
170+
171+
fn inject_data_abort(register_state: &mut RegisterStateRef) {
172+
// SAFETY: We are modifying the saved register state to redirect execution.
173+
let regs = unsafe { register_state.get_mut() };
174+
let fault_addr = far();
175+
let syndrome = esr();
176+
177+
debug!("Injecting data abort to guest: fault_addr={fault_addr:#x}, syndrome={syndrome:#x}");
178+
179+
// Read guest VBAR
180+
let vbar = arch::vbar_el1::read();
181+
assert!(
182+
vbar != 0,
183+
"Guest VBAR_EL1 is 0, cannot inject data abort. Fault addr: {fault_addr:#x}"
184+
);
185+
let handler = vbar + 0x200; // Current EL with SPx Sync
186+
187+
// Save current context to guest EL1 regs
188+
// SAFETY: We are accessing EL1 system registers to inject exception.
189+
unsafe {
190+
arch::elr_el1::write(regs.elr as u64);
191+
arch::spsr_el1::write(regs.spsr);
192+
arch::esr_el1::write(syndrome);
193+
arch::far_el1::write(fault_addr);
194+
}
195+
196+
// Redirect execution
197+
#[expect(
198+
clippy::cast_possible_truncation,
199+
reason = "only 64-bit target is supported"
200+
)]
201+
{
202+
regs.elr = handler as usize;
131203
}
204+
// Mask all interrupts (DAIF) and set mode to EL1h (0x5)
205+
regs.spsr = 0x3C5;
132206
}
133207

134208
const AARCH64_INSTRUCTION_LENGTH: usize = 4;
@@ -311,6 +385,8 @@ enum ExceptionClass {
311385
HvcTrappedInAArch64,
312386
/// SMC instruction execution in `AArch64` state.
313387
SmcTrappedInAArch64,
388+
/// Data Abort taken without a change in Exception Level.
389+
DataAbortLowerEL,
314390
#[allow(unused)]
315391
/// Unknown exception class.
316392
Unknown(u8),
@@ -321,6 +397,7 @@ impl ExceptionClass {
321397
match value {
322398
0x16 => Self::HvcTrappedInAArch64,
323399
0x17 => Self::SmcTrappedInAArch64,
400+
0x24 => Self::DataAbortLowerEL,
324401
_ => Self::Unknown(value),
325402
}
326403
}

src/main.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,15 @@ const LOG_LEVEL: LevelFilter = LevelFilter::Info;
4343
const HEAP_SIZE: usize = 40 * PAGE_SIZE;
4444
static HEAP: SpinMutex<[u8; HEAP_SIZE]> = SpinMutex::new([0; HEAP_SIZE]);
4545

46+
const SHARED_HEAP_SIZE: usize = 16 * PAGE_SIZE;
47+
static SHARED_HEAP: SpinMutex<[u8; SHARED_HEAP_SIZE]> = SpinMutex::new([0; SHARED_HEAP_SIZE]);
48+
4649
#[global_allocator]
4750
static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::new();
4851

52+
/// Heap allocator for data that needs to be shared between RITM and the guest running in EL1.
53+
pub static SHARED_HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::new();
54+
4955
#[repr(align(0x200000))] // Linux requires 2MB alignment
5056
struct AlignImage<T>(T);
5157

@@ -73,6 +79,12 @@ fn main(x0: u64, x1: u64, x2: u64, x3: u64) -> ! {
7379
SpinMutexGuard::leak(HEAP.try_lock().expect("failed to lock heap")).as_mut_slice(),
7480
);
7581

82+
add_to_heap(
83+
SHARED_HEAP_ALLOCATOR.lock().deref_mut(),
84+
SpinMutexGuard::leak(SHARED_HEAP.try_lock().expect("failed to lock shared heap"))
85+
.as_mut_slice(),
86+
);
87+
7688
let fdt_address = x0 as *const u8;
7789
// SAFETY: We trust that the FDT pointer we were given is valid, and this is the only time we
7890
// use it.
@@ -138,3 +150,19 @@ unsafe fn run_payload_el1(x0: u64, x1: u64, x2: u64, x3: u64) -> ! {
138150
hypervisor::entry_point_el1(x0, x1, x2, x3, &raw const NEXT_IMAGE.0 as u64);
139151
}
140152
}
153+
154+
/// Allocates a buffer from the shared heap.
155+
///
156+
/// # Panics
157+
///
158+
/// Panics if the requested size is invalid or if the allocation fails.
159+
pub fn shared_alloc(size: usize) -> &'static mut [u8] {
160+
use core::alloc::Layout;
161+
let layout = Layout::from_size_align(size, PAGE_SIZE).expect("invalid layout");
162+
let ptr = SHARED_HEAP_ALLOCATOR
163+
.lock()
164+
.alloc(layout)
165+
.expect("failed to allocate from shared heap");
166+
// SAFETY: The pointer is valid and represents the requested size.
167+
unsafe { core::slice::from_raw_parts_mut(ptr.as_ptr(), size) }
168+
}

src/pagetable.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@
77
// except according to those terms.
88

99
use crate::platform::PlatformImpl;
10-
use aarch64_paging::descriptor::Attributes;
10+
use aarch64_paging::descriptor::Stage1Attributes;
1111
use aarch64_rt::initial_pagetable;
1212

1313
/// Attributes to use for device memory in the initial identity map.
14-
pub const DEVICE_ATTRIBUTES: Attributes = Attributes::VALID
15-
.union(Attributes::ATTRIBUTE_INDEX_0)
16-
.union(Attributes::ACCESSED)
17-
.union(Attributes::UXN);
14+
pub const DEVICE_ATTRIBUTES: Stage1Attributes = Stage1Attributes::VALID
15+
.union(Stage1Attributes::ATTRIBUTE_INDEX_0)
16+
.union(Stage1Attributes::ACCESSED)
17+
.union(Stage1Attributes::UXN);
1818

1919
/// Attributes to use for normal memory in the initial identity map.
20-
pub const MEMORY_ATTRIBUTES: Attributes = Attributes::VALID
21-
.union(Attributes::ATTRIBUTE_INDEX_1)
22-
.union(Attributes::INNER_SHAREABLE)
23-
.union(Attributes::ACCESSED)
24-
.union(Attributes::NON_GLOBAL);
20+
pub const MEMORY_ATTRIBUTES: Stage1Attributes = Stage1Attributes::VALID
21+
.union(Stage1Attributes::ATTRIBUTE_INDEX_1)
22+
.union(Stage1Attributes::INNER_SHAREABLE)
23+
.union(Stage1Attributes::ACCESSED)
24+
.union(Stage1Attributes::NON_GLOBAL);
2525

2626
// The initial hardcoded page table used before the Rust code starts and activates the main page
2727
// table.

src/platform.rs

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

12+
use aarch64_paging::descriptor::Stage2Attributes;
13+
use aarch64_paging::idmap::IdMap;
1214
use dtoolkit::fdt::Fdt;
1315
use embedded_io::{Write, WriteReady};
1416
#[cfg(platform = "qemu")]
@@ -45,6 +47,12 @@ pub trait Platform {
4547
fn modify_dt(&self, fdt: Fdt<'static>) -> Fdt<'static> {
4648
fdt
4749
}
50+
51+
/// Create stage-2 page table for use by the guest for use when booting the payload at EL1.
52+
///
53+
/// The page table should typically unmap the part of the memory where RITM resides, so that
54+
/// the guest cannot interact with it in any way.
55+
fn make_stage2_pagetable() -> IdMap<Stage2Attributes>;
4856
}
4957

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

0 commit comments

Comments
 (0)