Skip to main content

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}