//! Narrow two-CPU boot checks; K471 adds a bounded release/acquire message lane. use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; /// One producer and one consumer, with an external acknowledgement preventing /// overwrite until the consumer has finished reading the previous sequence. pub struct SmpMessageLane { data: AtomicU64, sequence: AtomicUsize, } /// K472: FIFO admission for a bounded two-CPU contention experiment. pub struct SmpTicketLock { next: AtomicUsize, serving: AtomicUsize, wait_events: AtomicUsize, max_wait_spins: AtomicUsize, } #[must_use = "dropping the guard advances the FIFO ticket queue"] pub struct SmpTicketGuard<'a> { lock: &'a SmpTicketLock, ticket: usize, wait_spins: usize, _not_send: core::marker::PhantomData<*mut ()>, } impl SmpTicketLock { pub const fn new() -> Self { Self { next: AtomicUsize::new(0), serving: AtomicUsize::new(0), wait_events: AtomicUsize::new(0), max_wait_spins: AtomicUsize::new(0), } } pub fn lock(&self) -> SmpTicketGuard<'_> { let ticket = self.next.fetch_add(1, Ordering::Relaxed); let mut wait_spins = 0usize; while self.serving.load(Ordering::Acquire) != ticket { if wait_spins == 0 { self.wait_events.fetch_add(1, Ordering::Relaxed); } wait_spins = wait_spins.saturating_add(1); core::hint::spin_loop(); } self.max_wait_spins.fetch_max(wait_spins, Ordering::Relaxed); SmpTicketGuard { lock: self, ticket, wait_spins, _not_send: core::marker::PhantomData, } } pub fn next_ticket(&self) -> usize { self.next.load(Ordering::Acquire) } pub fn serving(&self) -> usize { self.serving.load(Ordering::Acquire) } pub fn wait_events(&self) -> usize { self.wait_events.load(Ordering::Acquire) } pub fn max_wait_spins(&self) -> usize { self.max_wait_spins.load(Ordering::Acquire) } } impl Default for SmpTicketLock { fn default() -> Self { Self::new() } } impl SmpTicketGuard<'_> { pub fn ticket(&self) -> usize { self.ticket } pub fn wait_spins(&self) -> usize { self.wait_spins } } impl Drop for SmpTicketGuard<'_> { fn drop(&mut self) { let previous = self.lock.serving.fetch_add(1, Ordering::Release); assert_eq!( previous, self.ticket, "ticket owner must release in FIFO order" ); } } /// K482: a finite, ticket-indexed ownership trace, not a general race detector. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TwoCpuTraceSummary { pub cpu0: usize, pub cpu1: usize, pub switches: usize, pub longest_run: usize, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TwoCpuTraceError { InvalidOwner, ForcedOrderMissing, WrongCounts, } pub fn summarize_two_cpu_trace(owners: &[u8; 256]) -> Result { if owners.iter().any(|owner| *owner > 1) { return Err(TwoCpuTraceError::InvalidOwner); } if owners[0] != 0 || owners[1] != 1 { return Err(TwoCpuTraceError::ForcedOrderMissing); } let mut cpu0 = 0; let mut cpu1 = 0; let mut switches = 0; let mut run = 0; let mut longest_run = 0; let mut previous = owners[0]; for owner in owners { if *owner == 0 { cpu0 += 1; } else { cpu1 += 1; } if *owner != previous { switches += 1; run = 0; previous = *owner; } run += 1; longest_run = longest_run.max(run); } if cpu0 != 128 || cpu1 != 128 { return Err(TwoCpuTraceError::WrongCounts); } Ok(TwoCpuTraceSummary { cpu0, cpu1, switches, longest_run, }) } /// Returns a strictly positive interval without silently wrapping a counter. pub fn positive_counter_delta(start: u64, end: u64) -> Option { end.checked_sub(start).filter(|delta| *delta > 0) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CounterStatsError { Reversed, Overflow, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CounterStats { pub samples: usize, pub minimum: u64, pub maximum: u64, pub total: u64, } impl CounterStats { pub const fn new() -> Self { Self { samples: 0, minimum: u64::MAX, maximum: 0, total: 0, } } pub fn record(&mut self, start: u64, end: u64) -> Result<(), CounterStatsError> { let delta = end.checked_sub(start).ok_or(CounterStatsError::Reversed)?; let total = self .total .checked_add(delta) .ok_or(CounterStatsError::Overflow)?; let samples = self .samples .checked_add(1) .ok_or(CounterStatsError::Overflow)?; self.minimum = self.minimum.min(delta); self.maximum = self.maximum.max(delta); self.total = total; self.samples = samples; Ok(()) } } impl Default for CounterStats { fn default() -> Self { Self::new() } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WaitClassStats { pub zero: usize, pub contended: usize, pub total_spins: usize, pub maximum_spins: usize, } impl WaitClassStats { pub const fn new() -> Self { Self { zero: 0, contended: 0, total_spins: 0, maximum_spins: 0, } } pub fn record(&mut self, spins: usize) -> Result<(), CounterStatsError> { let zero = self .zero .checked_add((spins == 0) as usize) .ok_or(CounterStatsError::Overflow)?; let contended = self .contended .checked_add((spins != 0) as usize) .ok_or(CounterStatsError::Overflow)?; zero.checked_add(contended) .ok_or(CounterStatsError::Overflow)?; let total_spins = self .total_spins .checked_add(spins) .ok_or(CounterStatsError::Overflow)?; self.zero = zero; self.contended = contended; self.total_spins = total_spins; self.maximum_spins = self.maximum_spins.max(spins); Ok(()) } } impl Default for WaitClassStats { fn default() -> Self { Self::new() } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ClassifiedCounterStats { pub zero_spin: CounterStats, pub contended: CounterStats, } impl ClassifiedCounterStats { pub const fn new() -> Self { Self { zero_spin: CounterStats::new(), contended: CounterStats::new(), } } pub fn record(&mut self, start: u64, end: u64, spins: usize) -> Result<(), CounterStatsError> { if spins == 0 { self.zero_spin.record(start, end) } else { self.contended.record(start, end) } } } impl Default for ClassifiedCounterStats { fn default() -> Self { Self::new() } } impl SmpMessageLane { pub const fn new() -> Self { Self { data: AtomicU64::new(0), sequence: AtomicUsize::new(0), } } pub fn publish(&self, sequence: usize, data: u64) { assert!(sequence != 0); self.data.store(data, Ordering::Relaxed); self.sequence.store(sequence, Ordering::Release); } pub fn try_receive(&self, sequence: usize) -> Option { (self.sequence.load(Ordering::Acquire) == sequence) .then(|| self.data.load(Ordering::Relaxed)) } pub fn published_sequence(&self) -> usize { self.sequence.load(Ordering::Acquire) } } impl Default for SmpMessageLane { fn default() -> Self { Self::new() } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CpuId { pub mpidr: u64, pub affinity: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct StackSpan { pub start: u64, pub end: u64, pub sp: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TopologyError { DuplicateAffinity, InvalidStack, OverlappingStacks, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct VectorFault { pub esr: u64, pub elr: u64, pub mpidr: u64, pub sp: u64, pub vbar: u64, pub seen: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum VectorError { NotPublished, WrongCpu, WrongException, WrongInstruction, WrongVector, WrongStack, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TimerSample { pub mpidr: u64, pub frequency: u64, pub start: u64, pub deadline0: u64, pub tick0: u64, pub deadline1: u64, pub tick1: u64, pub seen: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TimerError { Publication, CpuIdentity, Frequency, Deadline, TickOrder, } pub fn validate_timer_pair(primary: TimerSample, secondary: TimerSample) -> Result<(), TimerError> { if primary.seen != 1 || secondary.seen != 1 { return Err(TimerError::Publication); } if affinity(primary.mpidr) != 0 || affinity(secondary.mpidr) != 1 { return Err(TimerError::CpuIdentity); } if primary.frequency < 100 || primary.frequency != secondary.frequency { return Err(TimerError::Frequency); } for sample in [primary, secondary] { let period = sample.frequency / 100; if sample.start.checked_add(period) != Some(sample.deadline0) || sample.tick0.checked_add(period) != Some(sample.deadline1) { return Err(TimerError::Deadline); } if sample.tick0 < sample.deadline0 || sample.tick1 < sample.deadline1 { return Err(TimerError::TickOrder); } } Ok(()) } pub fn validate_secondary_brk( fault: VectorFault, expected_mpidr: u64, expected_brk_pc: u64, expected_vbar: u64, stack: StackSpan, ) -> Result<(), VectorError> { if fault.seen != 1 { return Err(VectorError::NotPublished); } if affinity(fault.mpidr) != affinity(expected_mpidr) || affinity(fault.mpidr) != 1 { return Err(VectorError::WrongCpu); } if fault.esr >> 26 != 0x3c || fault.esr & 0xffff != 0x464 { return Err(VectorError::WrongException); } if fault.elr != expected_brk_pc { return Err(VectorError::WrongInstruction); } if fault.vbar != expected_vbar || fault.vbar & 2047 != 0 { return Err(VectorError::WrongVector); } if stack.sp != fault.sp || stack.start >= stack.end || stack.start & 4095 != 0 || stack.end & 4095 != 0 || fault.sp <= stack.start || fault.sp > stack.end || fault.sp & 15 != 0 { return Err(VectorError::WrongStack); } Ok(()) } pub const fn affinity(mpidr: u64) -> u32 { // Aff0–Aff2 occupy bits 0–23 and Aff3 occupies bits 32–39. U/MT and // other non-affinity bits must never make one physical CPU look distinct. ((mpidr & 0x00ff_ffff) | (((mpidr >> 32) & 0xff) << 24)) as u32 } pub fn distinct_pair(primary: u64, secondary: u64) -> Result<[CpuId; 2], TopologyError> { let first = CpuId { mpidr: primary, affinity: affinity(primary), }; let second = CpuId { mpidr: secondary, affinity: affinity(secondary), }; if first.affinity == second.affinity { return Err(TopologyError::DuplicateAffinity); } Ok([first, second]) } pub fn disjoint_stacks(primary: StackSpan, secondary: StackSpan) -> Result<(), TopologyError> { let valid = |stack: StackSpan| { stack.start < stack.end && stack.start & 4095 == 0 && stack.end & 4095 == 0 && stack.sp > stack.start && stack.sp <= stack.end && stack.sp & 15 == 0 }; if !valid(primary) || !valid(secondary) { return Err(TopologyError::InvalidStack); } if primary.start < secondary.end && secondary.start < primary.end { return Err(TopologyError::OverlappingStacks); } Ok(()) } #[cfg(test)] mod tests { use super::*; extern crate std; use std::{sync::Arc, thread}; #[test] fn k482_ticket_owner_trace_summarizes_order_and_rejects_corruption() { let mut owners = [0u8; 256]; for (ticket, owner) in owners.iter_mut().enumerate() { *owner = (ticket & 1) as u8; } assert_eq!( summarize_two_cpu_trace(&owners), Ok(TwoCpuTraceSummary { cpu0: 128, cpu1: 128, switches: 255, longest_run: 1 }) ); owners[0] = 1; assert_eq!( summarize_two_cpu_trace(&owners), Err(TwoCpuTraceError::ForcedOrderMissing) ); owners[0] = 0; owners[1] = 0; assert_eq!( summarize_two_cpu_trace(&owners), Err(TwoCpuTraceError::ForcedOrderMissing) ); owners[1] = 1; owners[100] = 2; assert_eq!( summarize_two_cpu_trace(&owners), Err(TwoCpuTraceError::InvalidOwner) ); owners[100] = 0; owners[101] = 0; assert_eq!( summarize_two_cpu_trace(&owners), Err(TwoCpuTraceError::WrongCounts) ); } #[test] fn k472_ticket_fifo_orders_two_contending_threads() { let lock = Arc::new(SmpTicketLock::new()); let order = Arc::new(AtomicUsize::new(0)); let first = lock.lock(); assert_eq!(first.ticket(), 0); let worker_lock = lock.clone(); let worker_order = order.clone(); let worker = thread::spawn(move || { for _ in 0..128 { let guard = worker_lock.lock(); assert_eq!(worker_order.fetch_add(1, Ordering::Relaxed), guard.ticket()); } }); while lock.next_ticket() < 2 { thread::yield_now(); } assert_eq!(lock.serving(), 0); assert_eq!(order.fetch_add(1, Ordering::Relaxed), first.ticket()); drop(first); for _ in 1..128 { let guard = lock.lock(); assert_eq!(order.fetch_add(1, Ordering::Relaxed), guard.ticket()); } worker.join().unwrap(); assert_eq!(order.load(Ordering::Acquire), 256); assert_eq!(lock.next_ticket(), lock.serving()); } #[test] fn k473_wait_witness_is_recorded_before_owner_releases() { let lock = Arc::new(SmpTicketLock::new()); let first = lock.lock(); assert_eq!(first.ticket(), 0); assert_eq!(first.wait_spins(), 0); let worker_lock = lock.clone(); let worker = thread::spawn(move || { let second = worker_lock.lock(); assert_eq!(second.ticket(), 1); second.wait_spins() }); while lock.wait_events() == 0 { thread::yield_now(); } assert_eq!(lock.serving(), 0); drop(first); let observed = worker.join().unwrap(); assert!(observed > 0); assert!(lock.max_wait_spins() >= observed); assert_eq!(lock.wait_events(), 1); assert_eq!(lock.serving(), 2); } #[test] fn k474_counter_delta_rejects_zero_and_wrapped_samples() { assert_eq!(positive_counter_delta(100, 125), Some(25)); assert_eq!(positive_counter_delta(100, 100), None); assert_eq!(positive_counter_delta(100, 99), None); assert_eq!(positive_counter_delta(u64::MAX, 0), None); } #[test] fn k475_counter_stats_allow_zero_but_reject_reversal_and_overflow_without_mutation() { let mut stats = CounterStats::new(); assert_eq!(stats.record(10, 10), Ok(())); assert_eq!(stats.record(10, 15), Ok(())); assert_eq!( (stats.samples, stats.minimum, stats.maximum, stats.total), (2, 0, 5, 5) ); let unchanged = stats; assert_eq!(stats.record(20, 19), Err(CounterStatsError::Reversed)); assert_eq!(stats, unchanged); let mut edge = CounterStats::new(); assert_eq!(edge.record(0, u64::MAX), Ok(())); let unchanged = edge; assert_eq!(edge.record(0, 1), Err(CounterStatsError::Overflow)); assert_eq!(edge, unchanged); } #[test] fn k476_wait_classes_count_zero_and_contended_admissions_without_overflow() { let mut stats = WaitClassStats::new(); assert_eq!(stats.record(0), Ok(())); assert_eq!(stats.record(7), Ok(())); assert_eq!(stats.record(3), Ok(())); assert_eq!( ( stats.zero, stats.contended, stats.total_spins, stats.maximum_spins ), (1, 2, 10, 7) ); let mut full = WaitClassStats { zero: usize::MAX, ..WaitClassStats::new() }; let unchanged = full; assert_eq!(full.record(0), Err(CounterStatsError::Overflow)); assert_eq!(full, unchanged); let mut total_full = WaitClassStats { total_spins: usize::MAX, ..WaitClassStats::new() }; let unchanged = total_full; assert_eq!(total_full.record(1), Err(CounterStatsError::Overflow)); assert_eq!(total_full, unchanged); } #[test] fn k477_hold_samples_include_forced_first_section_and_reject_reversed_end() { let mut holds = CounterStats::new(); let first = positive_counter_delta(10, 15).unwrap(); assert_eq!(holds.record(10, 15), Ok(())); assert_eq!(holds.record(20, 22), Ok(())); assert_eq!(holds.record(30, 30), Ok(())); assert_eq!( (holds.samples, holds.minimum, holds.maximum, holds.total), (3, 0, 5, 7) ); assert!(holds.maximum >= first); let unchanged = holds; assert_eq!(holds.record(42, 41), Err(CounterStatsError::Reversed)); assert_eq!(holds, unchanged); } #[test] fn k478_counter_pair_baseline_keeps_raw_hold_samples_unchanged() { let mut baseline = CounterStats::new(); for (start, end) in [(10, 10), (20, 21), (30, 32), (40, 40)] { baseline.record(start, end).unwrap(); } assert_eq!( ( baseline.samples, baseline.minimum, baseline.maximum, baseline.total ), (4, 0, 2, 3) ); let mut hold = CounterStats::new(); hold.record(100, 109).unwrap(); assert_eq!( (hold.samples, hold.minimum, hold.maximum, hold.total), (1, 9, 9, 9) ); let unchanged = baseline; assert_eq!(baseline.record(51, 50), Err(CounterStatsError::Reversed)); assert_eq!(baseline, unchanged); assert_eq!( hold.total, 9, "baseline is observed, not subtracted from the raw hold interval" ); } #[test] fn k479_classified_admission_intervals_partition_raw_stats_without_mutating_on_error() { let mut raw = CounterStats::new(); let mut classes = ClassifiedCounterStats::new(); for (start, end, spins) in [(10, 10, 0), (20, 27, 3), (30, 32, 0)] { raw.record(start, end).unwrap(); classes.record(start, end, spins).unwrap(); } assert_eq!(classes.zero_spin.samples, 2); assert_eq!(classes.contended.samples, 1); assert_eq!(classes.zero_spin.total + classes.contended.total, raw.total); assert_eq!( classes.zero_spin.maximum.max(classes.contended.maximum), raw.maximum ); assert_eq!(classes.contended.total, 7); let unchanged = classes; assert_eq!(classes.record(40, 39, 2), Err(CounterStatsError::Reversed)); assert_eq!(classes, unchanged); } #[test] fn k471_release_acquire_exposes_payload_before_sequence() { let lane = Arc::new(SmpMessageLane::new()); let ack = Arc::new(AtomicUsize::new(0)); let receiver = lane.clone(); let received = ack.clone(); let worker = thread::spawn(move || { for sequence in 1..=256 { let expected = (sequence as u64) ^ 0x4b34_3731_0000_0000; loop { if let Some(data) = receiver.try_receive(sequence) { assert_eq!(data, expected); received.store(sequence, Ordering::Release); break; } thread::yield_now(); } } }); assert_eq!(lane.try_receive(1), None); for sequence in 1..=256 { lane.publish(sequence, (sequence as u64) ^ 0x4b34_3731_0000_0000); while ack.load(Ordering::Acquire) != sequence { thread::yield_now(); } } worker.join().unwrap(); assert_eq!(lane.published_sequence(), 256); } #[test] fn k462_affinity_is_unique_across_two_real_qemu_mpidr_shapes() { let ids = distinct_pair(0x8000_0000, 0x8000_0001).unwrap(); assert_eq!(ids[0].affinity, 0); assert_eq!(ids[1].affinity, 1); assert_eq!(affinity(0x0000_00ff_ffff), 0x00ff_ffff); assert_eq!(affinity(0x0000_00fe_0000_0000), 0xfe00_0000); } #[test] fn k462_non_affinity_bits_cannot_disguise_duplicate_identity() { assert_eq!( distinct_pair(0x8000_0001, 0x0000_0001), Err(TopologyError::DuplicateAffinity) ); assert_eq!( distinct_pair(0x8000_0000, 0x8000_0000), Err(TopologyError::DuplicateAffinity) ); } #[test] fn k463_primary_and_secondary_stack_pages_are_disjoint() { let primary = StackSpan { start: 0x4008_0000, end: 0x4008_4000, sp: 0x4008_3ff0, }; let secondary = StackSpan { start: 0x4008_4000, end: 0x4008_5000, sp: 0x4008_5000, }; assert_eq!(disjoint_stacks(primary, secondary), Ok(())); assert_eq!(disjoint_stacks(secondary, primary), Ok(())); assert_eq!( disjoint_stacks(primary, primary), Err(TopologyError::OverlappingStacks) ); } #[test] fn k463_bad_stack_alignment_or_pointer_is_rejected() { let primary = StackSpan { start: 0x4008_0000, end: 0x4008_4000, sp: 0x4008_3ff0, }; for bad in [ StackSpan { start: 0x4008_4001, end: 0x4008_5000, sp: 0x4008_5000, }, StackSpan { start: 0x4008_4000, end: 0x4008_5000, sp: 0x4008_4000, }, StackSpan { start: 0x4008_4000, end: 0x4008_5000, sp: 0x4008_5010, }, StackSpan { start: 0x4008_4000, end: 0x4008_5000, sp: 0x4008_4ff8, }, StackSpan { start: 0x4008_5000, end: 0x4008_5000, sp: 0x4008_5000, }, ] { assert_eq!( disjoint_stacks(primary, bad), Err(TopologyError::InvalidStack) ); } } #[test] fn k463_partial_overlap_is_rejected() { let primary = StackSpan { start: 0x4008_0000, end: 0x4008_4000, sp: 0x4008_3ff0, }; let overlap = StackSpan { start: 0x4008_3000, end: 0x4008_5000, sp: 0x4008_5000, }; assert_eq!( disjoint_stacks(primary, overlap), Err(TopologyError::OverlappingStacks) ); } #[test] fn k464_secondary_brk_contract_accepts_exact_fault() { let fault = VectorFault { esr: 0xf200_0464, elr: 0x4000_0464, mpidr: 0x8000_0001, sp: 0x4009_0000, vbar: 0x4008_0000, seen: 1, }; let stack = StackSpan { start: 0x4008_f000, end: 0x4009_0000, sp: fault.sp, }; assert_eq!( validate_secondary_brk(fault, fault.mpidr, fault.elr, fault.vbar, stack), Ok(()) ); assert_eq!( validate_secondary_brk( VectorFault { seen: 2, ..fault }, fault.mpidr, fault.elr, fault.vbar, stack ), Err(VectorError::NotPublished) ); assert_eq!( validate_secondary_brk( VectorFault { mpidr: 0x8000_0000, ..fault }, fault.mpidr, fault.elr, fault.vbar, stack ), Err(VectorError::WrongCpu) ); assert_eq!( validate_secondary_brk( VectorFault { esr: 0, ..fault }, fault.mpidr, fault.elr, fault.vbar, stack ), Err(VectorError::WrongException) ); assert_eq!( validate_secondary_brk( VectorFault { elr: fault.elr + 4, ..fault }, fault.mpidr, fault.elr, fault.vbar, stack ), Err(VectorError::WrongInstruction) ); assert_eq!( validate_secondary_brk( VectorFault { vbar: fault.vbar + 2048, ..fault }, fault.mpidr, fault.elr, fault.vbar, stack ), Err(VectorError::WrongVector) ); assert_eq!( validate_secondary_brk( VectorFault { sp: stack.start, ..fault }, fault.mpidr, fault.elr, fault.vbar, stack ), Err(VectorError::WrongStack) ); } #[test] fn k465_two_cpu_timer_samples_require_distinct_identity_and_two_expiries() { let primary = TimerSample { mpidr: 0x8000_0000, frequency: 24_000_000, start: 1000, deadline0: 241_000, tick0: 241_001, deadline1: 481_001, tick1: 481_002, seen: 1, }; let secondary = TimerSample { mpidr: 0x8000_0001, start: 2000, deadline0: 242_000, tick0: 242_001, deadline1: 482_001, tick1: 482_002, ..primary }; assert_eq!(validate_timer_pair(primary, secondary), Ok(())); assert_eq!( validate_timer_pair( primary, TimerSample { mpidr: primary.mpidr, ..secondary } ), Err(TimerError::CpuIdentity) ); assert_eq!( validate_timer_pair( primary, TimerSample { seen: 0, ..secondary } ), Err(TimerError::Publication) ); assert_eq!( validate_timer_pair( primary, TimerSample { frequency: 23_000_000, ..secondary } ), Err(TimerError::Frequency) ); assert_eq!( validate_timer_pair( primary, TimerSample { deadline0: secondary.deadline0 + 1, ..secondary } ), Err(TimerError::Deadline) ); assert_eq!( validate_timer_pair( primary, TimerSample { tick0: secondary.deadline0 - 1, ..secondary } ), Err(TimerError::Deadline) ); assert_eq!( validate_timer_pair( primary, TimerSample { tick1: secondary.deadline1 - 1, ..secondary } ), Err(TimerError::TickOrder) ); } }