Skip to main content

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}