//! EL1 return-frame ABI used by isolated SVC, GICv3 SGI and timer probes. //! The default Image still keeps IRQ masked; EL0 context is not active. use core::mem::{align_of, offset_of, size_of}; #[repr(C, align(16))] #[derive(Clone, Copy)] pub struct IrqContext { pub x: [u64; 31], pub interrupted_sp: u64, pub sp_el0: u64, pub elr: u64, pub spsr: u64, pub _padding: u64, pub q: [[u64; 2]; 32], pub fpcr: u32, pub fpsr: u32, pub vector_slot: u64, } impl IrqContext { pub const EMPTY: Self = Self { x: [0; 31], interrupted_sp: 0, sp_el0: 0, elr: 0, spsr: 0, _padding: 0, q: [[0; 2]; 32], fpcr: 0, fpsr: 0, vector_slot: 0, }; } /// A timer-preempted EL1h task must return to executable text on its own /// aligned stack with IRQ unmasked. This is checked before replacing the /// shared IRQ-stack frame or accepting a newly interrupted task. pub fn preempt_task_frame_valid( frame: &IrqContext, stack_start: usize, stack_top: usize, text_start: u64, text_end: u64, ) -> bool { stack_start & 4095 == 0 && stack_top.checked_sub(stack_start) == Some(16 * 1024) && frame.vector_slot == 5 && frame.spsr & 0x8f == 5 && frame.interrupted_sp >= (stack_start + 16) as u64 && frame.interrupted_sp <= stack_top as u64 && frame.interrupted_sp & 15 == 0 && text_start < text_end && frame.elr >= text_start && frame.elr < text_end && frame.elr & 3 == 0 } pub const IRQ_STACK_SIZE: usize = 16 * 1024; pub const IRQ_PROBE_NZCV: u64 = 0xa000_0000; pub fn nzcv_matches(value: u64, expected: u64) -> bool { value & 0xf000_0000 == expected & 0xf000_0000 } /// The EL1h vector group starts at slot 4: synchronous and IRQ are distinct. pub fn return_slot_matches(vector_slot: u64, is_irq: bool) -> bool { vector_slot == if is_irq { 5 } else { 4 } } /// GICD/GICR_PIDR2.ArchRev is bits [7:4]; only GICv3/v4 provide this /// system-register CPU interface and redistributor programming model. pub fn gic_arch_revision(pidr2: u32) -> Option { let revision = ((pidr2 >> 4) & 0xf) as u8; matches!(revision, 3 | 4).then_some(revision) } /// GICR_TYPER.AffinityValue is AF3:AF2:AF1:AF0 in its top word. pub fn redistributor_matches_mpidr(typer: u64, mpidr: u64) -> bool { let affinity = (((mpidr >> 32) & 0xff) << 24) | (mpidr & 0x00ff_ffff); (typer >> 32) == affinity } /// A leaf handler can leave SP at the frame base; a non-leaf handler moves it /// downward, but may never escape the dedicated stack. pub fn handler_sp_in_stack(stack_start: usize, frame_base: usize, current_sp: usize) -> bool { current_sp >= stack_start && current_sp <= frame_base } /// The one-CPU SGI probe must run its handler on the private IRQ stack while /// the interrupted SP stays on the boot stack and its low canary remains live. pub fn main_stack_intact( main_start: usize, main_top: usize, irq_start: usize, irq_top: usize, interrupted_sp: usize, observed_guard: u64, expected_guard: u64, ) -> bool { main_top.checked_sub(main_start) == Some(IRQ_STACK_SIZE) && irq_top.checked_sub(irq_start) == Some(IRQ_STACK_SIZE) && main_start & 0xfff == 0 && irq_start & 15 == 0 && (main_top <= irq_start || irq_top <= main_start) && interrupted_sp >= main_start + 16 && interrupted_sp <= main_top && interrupted_sp & 15 == 0 && observed_guard == expected_guard } #[derive(Debug, PartialEq, Eq)] pub enum IarClass { Expected, Unexpected(u32), Special(u32), } /// GIC special INTIDs 1020–1023 are not acknowledged and must not be EOI'd. pub fn classify_iar(iar: u64, expected: u32) -> IarClass { let intid = (iar & 0x00ff_ffff) as u32; if (1020..=1023).contains(&intid) { IarClass::Special(intid) } else if intid == expected { IarClass::Expected } else { IarClass::Unexpected(intid) } } /// Only SGI IDs occupy bits 0–15 in GICR_ICENABLER0. Never turn a PPI, /// external SPI, or special IAR value into an SGI-disable write. pub fn sgi_disable_bit(intid: u32) -> Option { (intid < 16).then(|| 1u32 << intid) } /// A bounded interrupt burst is useful only if the interrupted main loop /// actually executed between at least half of its 100 timer deliveries. pub fn storm_main_progress_ok(interleaved: usize, deliveries: usize, progress: usize) -> bool { deliveries == 100 && interleaved >= 50 && interleaved <= 99 && progress > 500 } pub const SERVICE_TICKS: usize = 6032; pub const SERVICE_QUEUE_CAPACITY: usize = 8; pub const SERVICE_LATENCY_US: [u64; 5] = [50, 250, 1000, 5000, 20000]; /// Quantize observed deadline lateness, not an end-to-end real-time promise. pub fn service_latency_bucket(now: u64, deadline: u64, frequency: u64) -> Option { if frequency == 0 { return None; } let late = now.checked_sub(deadline)?; for (index, limit) in SERVICE_LATENCY_US.iter().enumerate() { let threshold = frequency.checked_mul(*limit)?.checked_div(1_000_000)?; if late <= threshold { return Some(index); } } Some(SERVICE_LATENCY_US.len()) } pub fn service_queue_admit(head: u64, tail: u64) -> Option { let occupied = head.checked_sub(tail)?; (occupied <= SERVICE_QUEUE_CAPACITY as u64).then_some(occupied < SERVICE_QUEUE_CAPACITY as u64) } /// Advance an absolute physical-timer deadline without scheduling in the /// past. The second value counts whole periods skipped after a late handler. /// Overflow is an explicit error, never a wrapped compare value. pub fn next_timer_deadline(previous: u64, period: u64, now: u64) -> Option<(u64, u64)> { if period == 0 { return None; } let candidate = previous.checked_add(period)?; if candidate > now { return Some((candidate, 0)); } let skipped = (now - candidate).checked_div(period)?.checked_add(1)?; let next = candidate.checked_add(skipped.checked_mul(period)?)?; Some((next, skipped)) } /// Convert a duration to physical-counter ticks without silently wrapping the /// frequency × duration product. A sub-tick or zero request is not armable. pub fn timer_ticks_for_ns(frequency_hz: u64, interval_ns: u64) -> Option { if frequency_hz == 0 || interval_ns == 0 { return None; } let ticks = frequency_hz.checked_mul(interval_ns)? / 1_000_000_000; (ticks != 0).then_some(ticks) } /// Encode a Group 1 SGI for one target PE. Aff0 selects a bit in the 16-bit /// target list; Aff1/2/3 are copied from MPIDR_EL1. No "all but self" mode. pub fn sgi1r_for_mpidr(mpidr: u64, intid: u8) -> Option { let aff0 = (mpidr & 0xff) as u32; if aff0 >= 16 || intid >= 16 { return None; } let aff1 = (mpidr >> 8) & 0xff; let aff2 = (mpidr >> 16) & 0xff; let aff3 = (mpidr >> 32) & 0xff; Some((aff3 << 48) | (aff2 << 32) | ((intid as u64) << 24) | (aff1 << 16) | (1 << aff0)) } #[repr(align(16))] pub struct IrqStack(pub [u8; IRQ_STACK_SIZE]); impl Default for IrqStack { fn default() -> Self { Self([0; IRQ_STACK_SIZE]) } } const _: () = { assert!(align_of::() == 16); assert!(size_of::() == 816); assert!(offset_of!(IrqContext, x) == 0); assert!(offset_of!(IrqContext, interrupted_sp) == 248); assert!(offset_of!(IrqContext, sp_el0) == 256); assert!(offset_of!(IrqContext, elr) == 264); assert!(offset_of!(IrqContext, spsr) == 272); assert!(offset_of!(IrqContext, q) == 288); assert!(offset_of!(IrqContext, fpcr) == 800); assert!(offset_of!(IrqContext, fpsr) == 804); assert!(offset_of!(IrqContext, vector_slot) == 808); assert!(align_of::() == 16); assert!(size_of::() == IRQ_STACK_SIZE); }; #[cfg(test)] mod tests { use super::*; #[test] fn p07_k538_latency_buckets_count_lateness_without_realtime_guarantees() { for (late, bucket) in [ (0, 0), (50, 0), (51, 1), (250, 1), (251, 2), (1000, 2), (1001, 3), (5000, 3), (5001, 4), (20000, 4), (20001, 5), ] { assert_eq!( service_latency_bucket(100_000 + late, 100_000, 1_000_000), Some(bucket) ); } assert_eq!(service_latency_bucket(9, 10, 1_000_000), None); assert_eq!(service_latency_bucket(10, 10, 0), None); assert_eq!(service_latency_bucket(u64::MAX, 0, u64::MAX), None); } #[test] fn p07_k534_full_queue_never_overwrites_unconsumed_tickets() { for occupied in 0..8 { assert_eq!(service_queue_admit(100 + occupied, 100), Some(true)); } assert_eq!(service_queue_admit(108, 100), Some(false)); assert_eq!(service_queue_admit(109, 100), None); assert_eq!(service_queue_admit(99, 100), None); assert_eq!(service_queue_admit(u64::MAX, u64::MAX), Some(true)); } #[test] fn preempt_frame_rejects_foreign_stack_pc_irq_mask_and_wrong_slot() { let mut frame = IrqContext::EMPTY; frame.interrupted_sp = 0x4000_8000; frame.elr = 0x4000_2000; frame.spsr = 0x345; frame.vector_slot = 5; let valid = |frame: &IrqContext| { preempt_task_frame_valid(frame, 0x4000_4000, 0x4000_8000, 0x4000_1000, 0x4000_3000) }; assert!(valid(&frame)); frame.interrupted_sp = 0x4000_8008; assert!(!valid(&frame)); frame.interrupted_sp = 0x4000_3000; assert!(!valid(&frame)); frame.interrupted_sp = 0x4000_8000; frame.elr = 0x4000_3000; assert!(!valid(&frame)); frame.elr = 0x4000_2002; assert!(!valid(&frame)); frame.elr = 0x4000_2000; frame.spsr |= 0x80; assert!(!valid(&frame)); frame.spsr = 0x345; frame.vector_slot = 4; assert!(!valid(&frame)); frame.vector_slot = 5; assert!(!preempt_task_frame_valid( &frame, 0x4000_4001, 0x4000_8001, 0x4000_1000, 0x4000_3000 )); } #[test] fn gic_id_accepts_v3_v4_and_rejects_older_or_reserved_revisions() { assert_eq!(gic_arch_revision(0x3b), Some(3)); assert_eq!(gic_arch_revision(0x40), Some(4)); for pidr2 in [0, 0x10, 0x20, 0x50, 0xf0] { assert_eq!(gic_arch_revision(pidr2), None); } } #[test] fn redistributor_affinity_must_match_current_mpidr() { let mpidr = 0x0000_0004_0003_0205; let typer = 0x0403_0205_0000_0010; assert!(redistributor_matches_mpidr(typer, mpidr)); assert!(!redistributor_matches_mpidr(typer ^ (1 << 40), mpidr)); assert!(!redistributor_matches_mpidr(typer, mpidr ^ 1)); } #[test] fn k457_wrong_irq_mask_targets_only_sgi_bits() { assert_eq!(sgi_disable_bit(2), Some(4)); assert_eq!(sgi_disable_bit(0), Some(1)); assert_eq!(sgi_disable_bit(15), Some(1 << 15)); for intid in [16, 30, 31, 32, 1020, 1023, u32::MAX] { assert_eq!(sgi_disable_bit(intid), None); } } #[test] fn k458_timer_storm_requires_interleaved_main_progress() { assert!(storm_main_progress_ok(99, 100, 501)); assert!(storm_main_progress_ok(50, 100, 501)); for (interleaved, deliveries, progress) in [ (49, 100, 501), (50, 99, 501), (100, 100, 501), (50, 100, 500), ] { assert!(!storm_main_progress_ok(interleaved, deliveries, progress)); } } #[test] fn irq_context_reserves_all_general_and_simd_register_slots() { assert_eq!(size_of::(), 816); assert_eq!(offset_of!(IrqContext, x) + 30 * 8, 240); assert_eq!(offset_of!(IrqContext, q) + 31 * 16, 784); assert_eq!(offset_of!(IrqContext, vector_slot), 808); } #[test] fn return_vector_slot_distinguishes_el1_sync_and_irq() { assert!(return_slot_matches(4, false)); assert!(return_slot_matches(5, true)); for slot in [0, 1, 2, 3, 6, 7, 12, 13] { assert!(!return_slot_matches(slot, false)); assert!(!return_slot_matches(slot, true)); } assert!(!return_slot_matches(5, false)); assert!(!return_slot_matches(4, true)); } #[test] fn condition_flags_reject_each_corrupted_nzcv_bit() { let spsr = IRQ_PROBE_NZCV | 5; assert!(nzcv_matches(spsr, IRQ_PROBE_NZCV)); for bit in [28, 29, 30, 31] { assert!(!nzcv_matches(spsr ^ (1 << bit), IRQ_PROBE_NZCV)); } assert!(nzcv_matches(spsr ^ (1 << 9), IRQ_PROBE_NZCV)); } #[test] fn independent_irq_stack_contract_is_aligned_and_bounded() { assert_eq!(align_of::(), 16); assert_eq!(size_of::(), IRQ_STACK_SIZE); } #[test] fn handler_stack_accepts_leaf_sp_and_rejects_escape() { assert!(handler_sp_in_stack(0x1000, 0x4ce0, 0x4ce0)); assert!(handler_sp_in_stack(0x1000, 0x4ce0, 0x4cd0)); assert!(!handler_sp_in_stack(0x1000, 0x4ce0, 0x4cf0)); assert!(!handler_sp_in_stack(0x1000, 0x4ce0, 0x0ff0)); } #[test] fn main_stack_guard_and_irq_stack_must_remain_disjoint() { let check = |main_start, main_top, irq_start, irq_top, interrupted_sp, guard| { main_stack_intact( main_start, main_top, irq_start, irq_top, interrupted_sp, guard, 0x55, ) }; assert!(check(0x1000, 0x5000, 0x6010, 0xa010, 0x4fe0, 0x55)); assert!(!check(0x1000, 0x5000, 0x6010, 0xa010, 0x4fe0, 0x54)); assert!(!check(0x1000, 0x5000, 0x4010, 0x8010, 0x4fe0, 0x55)); assert!(!check(0x1000, 0x5000, 0x6010, 0xa010, 0x6000, 0x55)); assert!(!check(0x1000, 0x5000, 0x6010, 0xa010, 0x4fe8, 0x55)); assert!(!check(0x1000, 0x4000, 0x6010, 0xa010, 0x3fe0, 0x55)); } #[test] fn iar_classifies_expected_unexpected_and_non_acknowledging_special_ids() { assert_eq!(classify_iar(30, 30), IarClass::Expected); assert_eq!(classify_iar(2, 30), IarClass::Unexpected(2)); for intid in 1020..=1023 { assert_eq!(classify_iar(intid, 30), IarClass::Special(intid as u32)); } } #[test] fn sgi_target_list_uses_mpidr_affinities() { assert_eq!(sgi1r_for_mpidr(0, 1), Some(0x0100_0001)); assert_eq!( sgi1r_for_mpidr(0x0000_0004_0003_0205, 7), Some(0x0004_0003_0702_0020) ); } #[test] fn sgi_rejects_unrepresentable_target_or_id() { assert_eq!(sgi1r_for_mpidr(16, 1), None); assert_eq!(sgi1r_for_mpidr(0, 16), None); } #[test] fn absolute_timer_deadline_does_not_drift_or_schedule_in_past() { assert_eq!(next_timer_deadline(1_000, 100, 1_050), Some((1_100, 0))); assert_eq!(next_timer_deadline(1_000, 100, 1_100), Some((1_200, 1))); assert_eq!(next_timer_deadline(1_000, 100, 1_450), Some((1_500, 4))); } #[test] fn absolute_timer_deadline_rejects_zero_and_overflow() { assert_eq!(next_timer_deadline(1, 0, 1), None); assert_eq!(next_timer_deadline(u64::MAX - 9, 10, 0), None); assert_eq!(next_timer_deadline(u64::MAX - 99, 1, u64::MAX), None); } #[test] fn timer_duration_conversion_matches_ten_milliseconds() { assert_eq!(timer_ticks_for_ns(24_000_000, 10_000_000), Some(240_000)); assert_eq!(timer_ticks_for_ns(1_000_000_000, 1), Some(1)); } #[test] fn timer_duration_conversion_rejects_overflow_and_subtick() { assert_eq!(timer_ticks_for_ns(u64::MAX, 10_000_000), None); assert_eq!(timer_ticks_for_ns(24_000_000, u64::MAX), None); assert_eq!(timer_ticks_for_ns(24_000_000, 1), None); assert_eq!(timer_ticks_for_ns(0, 10_000_000), None); } }