godspeed_sdk/adversarial.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Adversarial test primitives for the §22 red-team / fuzz / chaos harness (the `probe` service).
3//!
4//! These are the one thing a test service legitimately needs that a safe wrapper cannot otherwise
5//! express: issuing a RAW syscall with arbitrary (fuzzed) number + arguments, and executing a
6//! DELIBERATE ring-3 fault to prove the kernel kills the faulting task rather than wedging the machine
7//! (invariant 12; §22 A14 / A15 / C2). Isolating that `unsafe` HERE - a designated, audited SDK module,
8//! every block carrying a `// SAFETY:` comment - keeps the test SERVICES themselves (`probe`)
9//! `unsafe`-free (§18.2), exactly as the SDK's ABI / MMIO / DMA modules keep the driver services
10//! `unsafe`-free (§18.1). This module is **test-only**: no production service should call it (a fault
11//! primitive merely kills the caller, which the kernel handles; the fuzz primitive is a raw trap the
12//! kernel validates). One production caller exists, by design: the SDK's own panic handler
13//! (`lib.rs`) calls [`fault_null_read`] so that a panicking service dies and is restarted instead of
14//! spinning.
15
16/// Issue a RAW syscall with an arbitrary number and arguments and return the raw `i64` result. This is
17/// the fuzz/adversarial entry point (F1/F2/A10/A15 ...): the kernel **validates every syscall**, so from
18/// the caller's side this is memory-safe - it never dereferences anything itself. A bad pointer passed
19/// as `a1` is the KERNEL's to validate and reject (or to attribute to this caller and kill it); it can
20/// never corrupt this task's memory. The `unsafe` is purely the inline-asm ABI trap, which is sound for
21/// any argument values, so it is isolated in [`crate::syscall::raw_syscall`] and wrapped safely here.
22#[inline]
23pub fn fuzz_syscall(nr: u64, a0: u64, a1: u64, a2: u64) -> i64 {
24 // SAFETY: `raw_syscall` is the ABI trap (`ud2` on x86-64, `svc #0` on ARMv7, the native trap on the
25 // other ports - `syscall.rs`). It is sound for ANY argument values -
26 // it just transitions to the kernel, which validates. Fuzzing arbitrary nr/args cannot violate this
27 // task's Rust memory safety; rejecting bad input is the kernel's responsibility (exactly what the
28 // fuzz/adversarial suite verifies). The worst outcome is that the kernel kills this task, not UB.
29 unsafe { crate::syscall::raw_syscall(nr, a0, a1, a2) }
30}
31
32/// Deliberately read through a NULL pointer, raising a ring-3 page fault (#PF) - §22 Chaos C2 / A14. A
33/// conforming kernel kills this task at the fault and continues; it must never halt the machine
34/// (invariant 12). Control does not return on a conforming kernel.
35#[inline]
36pub fn fault_null_read() {
37 // SAFETY: an intentional fault for the C2/A14 regression. A conforming kernel kills this task at the
38 // ring-3 #PF before the read yields a value, so nothing is truly observed; even if a (broken) kernel
39 // wrongly resumed us, `read_volatile` of address 0 is a defined faulting operation, not UB we rely on.
40 unsafe { core::ptr::read_volatile(core::ptr::null::<u8>()); }
41}
42
43/// Deliberately read a NON-CANONICAL address (bit 47 != bit 63), raising a ring-3 general-protection
44/// fault (#GP(0)) at CPL3 - §22 A14. The kernel must kill this task, not halt (kernel-audit C1).
45#[cfg(target_arch = "x86_64")]
46#[inline]
47pub fn fault_noncanonical_read() {
48 // SAFETY: an intentional fault for the A14 regression; the kernel kills this task at the ring-3 #GP.
49 // See `fault_null_read` - a conforming kernel never lets the read complete.
50 unsafe { let _ = core::ptr::read_volatile(0x8000_0000_0000_0000 as *const u8); }
51}
52
53/// ARMv7 has no "non-canonical" address form (that is an x86-64 48-bit-VA concept). The equivalent
54/// ring-3 CPU fault is a read of an **unmapped high address**, which takes a data abort the kernel
55/// must handle by killing the task, not halting - the same A14/C1 property.
56#[cfg(target_arch = "arm")]
57#[inline]
58pub fn fault_noncanonical_read() {
59 // SAFETY: an intentional data-abort for the A14 regression; a conforming kernel kills this task at
60 // the ring-3 abort. `0xFFFF_FFF0` is far above any mapped user or kernel page on this platform.
61 unsafe { let _ = core::ptr::read_volatile(0xFFFF_FFF0 as *const u8); }
62}
63
64/// AArch64 has a genuine analog of x86's non-canonical address: the **VA hole**. With a 39-bit range
65/// at each end (`T0SZ = T1SZ = 25`), addresses between the top of the `TTBR0` range and the bottom of
66/// the `TTBR1` range belong to neither table and fault on any access, from any EL. `0x0000_8000_0000_0000`
67/// sits squarely in that hole - the same "this address cannot possibly be valid" property #GP tests on
68/// x86, rather than merely an address that happens to be unmapped today.
69#[cfg(target_arch = "aarch64")]
70#[inline]
71pub fn fault_noncanonical_read() {
72 // SAFETY: an intentional data abort for the A14 regression; a conforming kernel kills this task at
73 // the EL0 abort. The address is in the architectural VA hole, so no mapping can ever make it valid.
74 unsafe {
75 let _ = core::ptr::read_volatile(0x0000_8000_0000_0000u64 as *const u8);
76 }
77}
78
79/// Deliberately divide by zero, raising a ring-3 divide-error (#DE, vector 0) at CPL3 - §22 A14. Uses
80/// raw asm because Rust inserts a divide guard that would PANIC (unwind) instead of faulting, and the
81/// threat model includes adversarial asm services. The kernel must kill this task, not halt.
82#[cfg(target_arch = "x86_64")]
83#[inline]
84pub fn fault_divide_by_zero() {
85 // SAFETY: an intentional fault for the A14 regression; the kernel kills this task at the ring-3 #DE.
86 // The asm touches no memory (nomem/nostack) and clobbers only the declared caller-saved registers.
87 unsafe {
88 core::arch::asm!(
89 "xor eax, eax",
90 "xor edx, edx",
91 "xor ecx, ecx",
92 "div ecx", // (EDX:EAX)=0 / ECX=0 -> #DE (divide error)
93 out("eax") _, out("edx") _, out("ecx") _,
94 options(nostack, nomem),
95 );
96 }
97}
98
99/// ARMv7 has no trapping integer divide-by-zero by default (`sdiv`/`udiv` return 0 unless
100/// `SCTLR.DZ` is set), so the equivalent ring-3 CPU fault is an **undefined instruction** (`udf`),
101/// which the kernel's UND vector must handle by killing the task, not halting - the same A14/C1
102/// property this primitive tests on x86.
103#[cfg(target_arch = "arm")]
104#[inline]
105pub fn fault_divide_by_zero() {
106 // SAFETY: an intentional undefined-instruction fault for the A14 regression. `udf #0` raises a
107 // ring-3 undefined-instruction exception; a conforming kernel kills this task and never resumes it.
108 unsafe {
109 core::arch::asm!("udf #0", options(nostack, nomem));
110 }
111}
112
113/// AArch64 has **no trapping integer divide** either - `sdiv`/`udiv` by zero return 0 rather than
114/// faulting, with no control bit to change that (unlike ARMv7's `SCTLR.DZ`). So, as on the 32-bit port,
115/// the equivalent EL0 CPU fault is an **undefined instruction**, which the kernel must handle by
116/// killing the task rather than halting - the same A14/C1 property the x86 `#DE` tests.
117///
118/// The name is kept across all three architectures so the test suite names one behaviour rather than
119/// three; what differs is only which instruction produces the fault.
120#[cfg(target_arch = "aarch64")]
121#[inline]
122pub fn fault_divide_by_zero() {
123 // SAFETY: an intentional undefined-instruction fault for the A14 regression. `udf #0` is the
124 // permanently-undefined encoding; a conforming kernel kills this task and never resumes it.
125 unsafe {
126 core::arch::asm!("udf #0", options(nostack, nomem));
127 }
128}
129
130/// A run of FRESH pages this task was just given by `AllocMem`, for the §22 P7 stale-translation test.
131///
132/// P7 asks whether a dead task's translations can outlive it (CLAUDE.md 10.5): a writer stamps its
133/// pages, is killed, and a checker spawned after it, given the SAME virtual addresses (every task's
134/// `AllocMem` grows from one base), must read the zeroes the kernel filled its new pages with. A
135/// pattern word there means the checker read through a translation that belonged to the dead writer.
136///
137/// Reading and writing that memory needs a raw pointer, which a test SERVICE must not hold (18.2), so
138/// it is wrapped here. The wrapper is sound because a `Region` can only be made from a successful
139/// `AllocMem`: its pages are mapped, writable, private to this task, and referenced by nothing else -
140/// the kernel has just created them, and no Rust object lives there.
141pub struct Region {
142 va: u64,
143 words: usize,
144}
145
146impl Region {
147 /// Ask the kernel for `pages` fresh 4 KiB pages. `None` if it refused.
148 pub fn alloc(ctx: &crate::service_context::ServiceContext, pages: usize) -> Option<Region> {
149 let len = pages.checked_mul(4096)?;
150 let va = ctx.alloc_mem(len).ok()?;
151 Some(Region { va, words: len / 4 })
152 }
153
154 /// The first virtual address of the region (the same in every task that asks first).
155 pub fn base(&self) -> u64 { self.va }
156
157 /// Write `word` into every 32-bit word of the region.
158 pub fn stamp(&self, word: u32) {
159 let p = self.va as *mut u32;
160 for i in 0..self.words {
161 // SAFETY: `p.add(i)` is inside the `words * 4` bytes `AllocMem` mapped for this task (the
162 // constructor is the only way to make a Region), 4-aligned because the base is
163 // page-aligned, and no Rust reference points there. Volatile, so every store is made.
164 unsafe { core::ptr::write_volatile(p.add(i), word) };
165 }
166 }
167
168 /// The byte offset and value of the first word that is not `want`, or `None` if every word is.
169 pub fn first_not(&self, want: u32) -> Option<(usize, u32)> {
170 let p = self.va as *const u32;
171 for i in 0..self.words {
172 // SAFETY: as `stamp`: inside the mapped region, aligned, aliased by no reference. Volatile,
173 // so each word is really read through this task's own translation.
174 let w = unsafe { core::ptr::read_volatile(p.add(i)) };
175 if w != want {
176 return Some((i * 4, w));
177 }
178 }
179 None
180 }
181}