squib_arch/esr.rs
1//! ESR_EL2 syndrome decoder.
2//!
3//! ESR_EL2 (Exception Syndrome Register, EL2) is the synchronous-exception classifier the
4//! HVF backend reads on every `EXCEPTION` exit. Bits `[31:26]` are the exception class
5//! (`EC`); the meaning of the remaining bits depends on the EC value.
6//!
7//! The decoder never panics and never produces undefined output: any unknown EC is
8//! returned as [`EsrDecoded::Other`] for the caller to log + abort cleanly. This
9//! invariant is pinned by I-HV-4 (`12-hvf-backend.md § 9`).
10//!
11//! References: Arm ARM (Section D17.2 and D24), libkrun's `vendors/libkrun/src/hvf`.
12
13/// Exception class (`EC`) constants, raw values from Arm ARM § D17.2.16.
14pub mod ec {
15 /// Unknown reason.
16 pub const UNKNOWN: u8 = 0x00;
17 /// Trapped WFI/WFE.
18 pub const WFX: u8 = 0x01;
19 /// Trapped MCR/MRC access (cp15, AArch32).
20 pub const MCR_MRC_CP15: u8 = 0x03;
21 /// Trapped MCRR/MRRC access (cp15, AArch32).
22 pub const MCRR_MRRC_CP15: u8 = 0x04;
23 /// Trapped MCR/MRC access (cp14, AArch32).
24 pub const MCR_MRC_CP14: u8 = 0x05;
25 /// Trapped LDC/STC access.
26 pub const LDC_STC: u8 = 0x06;
27 /// FP/SIMD/SVE access trap.
28 pub const FP_ASIMD: u8 = 0x07;
29 /// Branch target identification mismatch.
30 pub const BTI: u8 = 0x0D;
31 /// Illegal execution state.
32 pub const ILLEGAL_STATE: u8 = 0x0E;
33 /// SVC instruction (AArch64).
34 pub const SVC: u8 = 0x15;
35 /// HVC instruction (AArch64).
36 pub const HVC: u8 = 0x16;
37 /// SMC instruction (AArch64).
38 pub const SMC: u8 = 0x17;
39 /// Trapped MSR / MRS / system instruction (AArch64).
40 pub const SYSTEM_REGISTER: u8 = 0x18;
41 /// Pointer-authentication failure.
42 pub const PAUTH: u8 = 0x1C;
43 /// Instruction abort, lower EL.
44 pub const INST_ABORT_LOWER_EL: u8 = 0x20;
45 /// Instruction abort, same EL.
46 pub const INST_ABORT_SAME_EL: u8 = 0x21;
47 /// PC alignment fault.
48 pub const PC_ALIGNMENT: u8 = 0x22;
49 /// Data abort, lower EL — the virtio/MMIO path.
50 pub const DATA_ABORT_LOWER_EL: u8 = 0x24;
51 /// Data abort, same EL.
52 pub const DATA_ABORT_SAME_EL: u8 = 0x25;
53 /// SP alignment fault.
54 pub const SP_ALIGNMENT: u8 = 0x26;
55 /// Trapped FP exception (AArch64).
56 pub const FPE_AARCH64: u8 = 0x2C;
57 /// SError interrupt.
58 pub const SERROR: u8 = 0x2F;
59 /// Breakpoint, lower EL.
60 pub const BREAKPOINT_LOWER_EL: u8 = 0x30;
61 /// Breakpoint, same EL.
62 pub const BREAKPOINT_SAME_EL: u8 = 0x31;
63 /// Software step, lower EL.
64 pub const SOFTWARE_STEP_LOWER_EL: u8 = 0x32;
65 /// Software step, same EL.
66 pub const SOFTWARE_STEP_SAME_EL: u8 = 0x33;
67 /// Watchpoint, lower EL.
68 pub const WATCHPOINT_LOWER_EL: u8 = 0x34;
69 /// Watchpoint, same EL.
70 pub const WATCHPOINT_SAME_EL: u8 = 0x35;
71 /// AArch32 BKPT.
72 pub const BKPT_AARCH32: u8 = 0x38;
73 /// AArch64 BRK.
74 pub const BRK_AARCH64: u8 = 0x3C;
75}
76
77/// A decoded ESR_EL2 value.
78#[derive(Debug, Clone, Copy, Eq, PartialEq)]
79pub enum EsrDecoded {
80 /// Data abort taken from a lower exception level. `is_write` distinguishes load vs
81 /// store; `sas` is the syndrome access size (`0` = byte, `1` = halfword, `2` = word,
82 /// `3` = doubleword). `srt` is the source/destination register index (0..=31). `sf`
83 /// indicates 64-bit register width.
84 DataAbort {
85 /// `true` for store, `false` for load.
86 is_write: bool,
87 /// Syndrome access size: byte/halfword/word/doubleword.
88 sas: u8,
89 /// Source/destination register (X0..X31, where X31 is XZR).
90 srt: u8,
91 /// 64-bit operation flag.
92 sf: bool,
93 },
94 /// HVC immediate. The argument register x[0..3] is read separately by the caller.
95 Hvc {
96 /// 16-bit immediate operand encoded in the instruction.
97 imm16: u16,
98 },
99 /// SMC immediate.
100 Smc {
101 /// 16-bit immediate operand encoded in the instruction.
102 imm16: u16,
103 },
104 /// Trapped MSR / MRS / system instruction.
105 SystemRegister {
106 /// `true` = MRS (read sysreg), `false` = MSR (write sysreg).
107 read: bool,
108 /// Op0 from the encoding.
109 op0: u8,
110 /// Op1.
111 op1: u8,
112 /// CRn.
113 crn: u8,
114 /// CRm.
115 crm: u8,
116 /// Op2.
117 op2: u8,
118 /// Source/destination register (X0..X30; X31 is XZR).
119 xt: u8,
120 },
121 /// `WFI`.
122 Wfi,
123 /// `WFE`.
124 Wfe,
125 /// `BRK` immediate (debugger breakpoint).
126 Brk {
127 /// 16-bit immediate.
128 imm16: u16,
129 },
130 /// Anything not specifically decoded above. The caller treats this as fatal and
131 /// returns `VmExit::InternalError`.
132 Other {
133 /// Exception class field (bits 31..26).
134 ec: u8,
135 /// The original raw ESR_EL2 register value, for logging.
136 raw: u64,
137 },
138}
139
140/// Decode an ESR_EL2 register value.
141///
142/// The decoder is total — every `u64` produces some [`EsrDecoded`] without panicking. It
143/// is read-only and zero-allocation, so it is cheap enough to call on every exception.
144#[must_use]
145pub fn decode(esr: u64) -> EsrDecoded {
146 let ec = ((esr >> 26) & 0x3F) as u8;
147 let iss = esr & 0x01FF_FFFF; // 25-bit ISS field
148 match ec {
149 ec::DATA_ABORT_LOWER_EL | ec::DATA_ABORT_SAME_EL => decode_data_abort(iss),
150 ec::HVC => EsrDecoded::Hvc {
151 imm16: (iss & 0xFFFF) as u16,
152 },
153 ec::SMC => EsrDecoded::Smc {
154 imm16: (iss & 0xFFFF) as u16,
155 },
156 ec::SYSTEM_REGISTER => decode_system_register(iss),
157 ec::WFX => decode_wfx(iss),
158 ec::BRK_AARCH64 => EsrDecoded::Brk {
159 imm16: (iss & 0xFFFF) as u16,
160 },
161 _ => EsrDecoded::Other { ec, raw: esr },
162 }
163}
164
165fn decode_data_abort(iss: u64) -> EsrDecoded {
166 // ISS layout for data abort (Arm ARM § D17.2.41):
167 // bit 24: ISV (instruction syndrome valid)
168 // bits 23..22: SAS
169 // bit 21: SSE (sign-extended)
170 // bits 20..16: SRT
171 // bit 15: SF (64-bit register width)
172 // bit 14: AR (acquire/release)
173 // bit 6: WnR (write not read)
174 let is_write = (iss & (1 << 6)) != 0;
175 let sas = ((iss >> 22) & 0x3) as u8;
176 let srt = ((iss >> 16) & 0x1F) as u8;
177 let sf = (iss & (1 << 15)) != 0;
178 EsrDecoded::DataAbort {
179 is_write,
180 sas,
181 srt,
182 sf,
183 }
184}
185
186fn decode_system_register(iss: u64) -> EsrDecoded {
187 // ISS layout for MSR/MRS trap (Arm ARM § D17.2.37):
188 // bits 21..20: Op0
189 // bits 19..17: Op2
190 // bits 16..14: Op1
191 // bits 13..10: CRn
192 // bits 9..5: Rt (xt)
193 // bits 4..1: CRm
194 // bit 0: Direction — 1 = read (MRS), 0 = write (MSR)
195 let op0 = ((iss >> 20) & 0x3) as u8;
196 let op2 = ((iss >> 17) & 0x7) as u8;
197 let op1 = ((iss >> 14) & 0x7) as u8;
198 let crn = ((iss >> 10) & 0xF) as u8;
199 let xt = ((iss >> 5) & 0x1F) as u8;
200 let crm = ((iss >> 1) & 0xF) as u8;
201 let read = (iss & 0x1) == 1;
202 EsrDecoded::SystemRegister {
203 read,
204 op0,
205 op1,
206 crn,
207 crm,
208 op2,
209 xt,
210 }
211}
212
213fn decode_wfx(iss: u64) -> EsrDecoded {
214 // ISS bit 0 distinguishes WFE (1) from WFI (0).
215 if (iss & 0x1) == 1 {
216 EsrDecoded::Wfe
217 } else {
218 EsrDecoded::Wfi
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 /// Build an ESR_EL2 value from `ec` (6 bits) and an ISS payload (25 bits).
227 const fn esr(ec: u8, iss: u64) -> u64 {
228 ((ec as u64) << 26) | (iss & 0x01FF_FFFF)
229 }
230
231 #[test]
232 fn decoder_is_total_for_every_ec() {
233 // Run every 6-bit EC and assert the function never panics.
234 for ec_value in 0u8..=0x3F {
235 let _ = decode(esr(ec_value, 0));
236 let _ = decode(esr(ec_value, 0x01FF_FFFF));
237 }
238 }
239
240 #[test]
241 fn unknown_ec_returns_other_with_raw_preserved() {
242 // EC 0x10 is unallocated.
243 let raw = esr(0x10, 0xDEAD_BEEF);
244 match decode(raw) {
245 EsrDecoded::Other { ec, raw: got } => {
246 assert_eq!(ec, 0x10);
247 assert_eq!(got, raw);
248 }
249 other => panic!("expected Other, got {other:?}"),
250 }
251 }
252
253 #[test]
254 fn data_abort_decodes_write_register_and_size() {
255 // SAS=2 (word), SRT=5, SF=1, WnR=1 (write).
256 let iss = (1u64 << 6) | (1u64 << 15) | (5u64 << 16) | (2u64 << 22);
257 let decoded = decode(esr(ec::DATA_ABORT_LOWER_EL, iss));
258 assert_eq!(
259 decoded,
260 EsrDecoded::DataAbort {
261 is_write: true,
262 sas: 2,
263 srt: 5,
264 sf: true,
265 }
266 );
267 }
268
269 #[test]
270 fn data_abort_distinguishes_load_vs_store() {
271 // WnR = 0 (read).
272 let iss = 0;
273 let decoded = decode(esr(ec::DATA_ABORT_LOWER_EL, iss));
274 let EsrDecoded::DataAbort { is_write, .. } = decoded else {
275 panic!("expected DataAbort, got {decoded:?}");
276 };
277 assert!(!is_write);
278 }
279
280 #[test]
281 fn hvc_extracts_imm16() {
282 let imm = 0xBEEF_u16;
283 let decoded = decode(esr(ec::HVC, u64::from(imm)));
284 assert_eq!(decoded, EsrDecoded::Hvc { imm16: imm });
285 }
286
287 #[test]
288 fn smc_extracts_imm16() {
289 let imm = 0x0042_u16;
290 let decoded = decode(esr(ec::SMC, u64::from(imm)));
291 assert_eq!(decoded, EsrDecoded::Smc { imm16: imm });
292 }
293
294 #[test]
295 fn brk_extracts_imm16() {
296 let decoded = decode(esr(ec::BRK_AARCH64, 0xF000));
297 assert_eq!(decoded, EsrDecoded::Brk { imm16: 0xF000 });
298 }
299
300 #[test]
301 fn wfi_vs_wfe_distinguished_by_iss_bit_0() {
302 let wfi = decode(esr(ec::WFX, 0));
303 let wfe = decode(esr(ec::WFX, 1));
304 assert_eq!(wfi, EsrDecoded::Wfi);
305 assert_eq!(wfe, EsrDecoded::Wfe);
306 }
307
308 #[test]
309 fn system_register_decodes_full_op_tuple() {
310 // Encode (op0=3, op1=0, crn=1, crm=2, op2=4, xt=5, read=1).
311 // op1 = 0 is a real value but the resulting `0u64 << 14` is dead-code from the
312 // identity-op linter's perspective; allow at the test scope.
313 #[allow(clippy::identity_op)]
314 let iss = (3u64 << 20) // op0
315 | (4u64 << 17) // op2
316 | (0u64 << 14) // op1
317 | (1u64 << 10) // crn
318 | (5u64 << 5) // xt
319 | (2u64 << 1) // crm
320 | 1u64; // read
321 let decoded = decode(esr(ec::SYSTEM_REGISTER, iss));
322 assert_eq!(
323 decoded,
324 EsrDecoded::SystemRegister {
325 read: true,
326 op0: 3,
327 op1: 0,
328 crn: 1,
329 crm: 2,
330 op2: 4,
331 xt: 5,
332 }
333 );
334 }
335
336 #[test]
337 fn property_test_random_inputs_never_panic() {
338 // Cheap deterministic sweep — proptest is not pulled in for squib-core.
339 // Exercise every multiple of 0x1000_0000 across the full u64 range.
340 let mut esr_value: u64 = 0;
341 for _ in 0..1000 {
342 let _ = decode(esr_value);
343 esr_value = esr_value.wrapping_add(0x0123_4567_89AB_CDEF);
344 }
345 }
346}