Skip to main content

lab_proxy_core/
executor.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Read-path executor: policy check, hardware access, and audit for scalar
6//! reads and bounded snapshots.
7//!
8//! Every attempted operation is audited, including rejections, which never
9//! touch hardware. A snapshot validates every item before the first read
10//! and is an ordered series of reads, not an atomic hardware snapshot.
11
12use crate::access_policy::{AccessClass, AccessPolicy, Denial, ResourceId, WritePrecondition};
13use crate::audit_ring::{AuditRecord, AuditRing, Decision, OpStatus};
14use crate::hardware_backend::{BackendError, Clock, Timer};
15use crate::protocol_resource_adapter::ResourceBackend;
16use std::collections::BTreeMap;
17
18/// Instance-wide execution limits, derived from the target ceiling.
19#[derive(Clone, Copy, Debug)]
20pub struct ExecLimits {
21    /// Maximum number of items in one snapshot.
22    pub max_snapshot_items: usize,
23    /// Maximum number of items in one sequence.
24    pub max_sequence_items: usize,
25    /// Maximum single delay in nanoseconds.
26    pub max_delay_ns: i64,
27    /// Maximum duration of an entire sequence in nanoseconds.
28    pub max_sequence_duration_ns: i64,
29}
30
31impl Default for ExecLimits {
32    fn default() -> Self {
33        Self {
34            max_snapshot_items: 64,
35            max_sequence_items: 64,
36            max_delay_ns: 5_000_000_000,             // 5 seconds
37            max_sequence_duration_ns: 1_000_000_000, // 1 second
38        }
39    }
40}
41
42/// One requested snapshot read.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct SnapshotItem {
45    /// Resource to read.
46    pub resource: ResourceId,
47    /// Byte offset from the start of the logical resource.
48    pub offset: u64,
49}
50
51/// A completed scalar read.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct ReadOutcome {
54    /// The value read.
55    pub value: u32,
56    /// Audit sequence assigned to the read.
57    pub audit_seq: u64,
58    /// Timestamp of the read.
59    pub timestamp_ns: i64,
60}
61
62/// Why a scalar read did not complete.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum ReadError {
65    /// Policy rejected the read; hardware was not touched.
66    Denied {
67        /// The rejecting policy layer's reason.
68        denial: Denial,
69        /// Audit sequence of the rejection record.
70        audit_seq: u64,
71    },
72    /// The backend failed after policy allowed the read.
73    Backend {
74        /// The backend error.
75        error: BackendError,
76        /// Audit sequence of the failure record.
77        audit_seq: u64,
78    },
79}
80
81/// The outcome of a completed 32-bit write.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct WriteOutcome {
84    /// Value read back if readback was configured or requested.
85    pub readback_value: u32,
86    /// Audit sequence assigned to the write.
87    pub audit_seq: u64,
88    /// Timestamp of the write.
89    pub timestamp_ns: i64,
90}
91
92/// Why a 32-bit write did not complete.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum WriteError {
95    /// Policy rejected the write; hardware was not touched.
96    Denied {
97        /// The rejecting policy layer's reason.
98        denial: Denial,
99        /// Audit sequence of the rejection record.
100        audit_seq: u64,
101    },
102    /// The backend failed after policy allowed the write.
103    Backend {
104        /// The backend error.
105        error: BackendError,
106        /// Audit sequence of the failure record.
107        audit_seq: u64,
108    },
109}
110
111/// The outcome of a completed 32-bit poll.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub struct PollOutcome {
114    /// Final value read from the register.
115    pub value: u32,
116    /// Audit sequence assigned to the matching read.
117    pub audit_seq: u64,
118    /// Timestamp of the matching read.
119    pub timestamp_ns: i64,
120}
121
122/// Why a 32-bit poll did not complete.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum PollError {
125    /// Policy rejected the poll or the poll timed out.
126    Denied {
127        /// The rejection or timeout reason.
128        denial: Denial,
129        /// Audit sequence of the rejection record.
130        audit_seq: u64,
131    },
132    /// The backend failed during polling.
133    Backend {
134        /// The backend error.
135        error: BackendError,
136        /// Audit sequence of the failure record.
137        audit_seq: u64,
138    },
139}
140
141/// The outcome of a completed GPIO read.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct GpioReadOutcome {
144    pub value: bool,
145    pub audit_seq: u64,
146    pub timestamp_ns: i64,
147}
148
149/// Why a GPIO read did not complete.
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum GpioReadError {
152    Denied { denial: Denial, audit_seq: u64 },
153    Backend { error: BackendError, audit_seq: u64 },
154}
155
156/// The outcome of a completed GPIO write.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct GpioWriteOutcome {
159    pub audit_seq: u64,
160    pub timestamp_ns: i64,
161}
162
163/// Why a GPIO write did not complete.
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165pub enum GpioWriteError {
166    Denied { denial: Denial, audit_seq: u64 },
167    Backend { error: BackendError, audit_seq: u64 },
168}
169
170/// The outcome of a completed I2C transfer.
171#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct I2cTransferOutcome {
173    pub read_data: Vec<u8>,
174    pub audit_seq: u64,
175    pub timestamp_ns: i64,
176}
177
178/// Why an I2C transfer did not complete.
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub enum I2cTransferError {
181    Denied { denial: Denial, audit_seq: u64 },
182    Backend { error: BackendError, audit_seq: u64 },
183}
184
185/// The outcome of a completed SPI transmit.
186#[derive(Clone, Debug, PartialEq, Eq)]
187pub struct SpiTransmitOutcome {
188    pub rx_data: Vec<u8>,
189    pub audit_seq: u64,
190    pub timestamp_ns: i64,
191}
192
193/// Why a SPI transmit did not complete.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub enum SpiTransmitError {
196    Denied { denial: Denial, audit_seq: u64 },
197    Backend { error: BackendError, audit_seq: u64 },
198}
199
200/// The outcome of a completed Clock enable.
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub struct ClockEnableOutcome {
203    pub audit_seq: u64,
204    pub timestamp_ns: i64,
205}
206
207/// The outcome of a completed Clock disable.
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub struct ClockDisableOutcome {
210    pub audit_seq: u64,
211    pub timestamp_ns: i64,
212}
213
214/// The outcome of a completed Clock is_enabled query.
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct ClockIsEnabledOutcome {
217    pub enabled: bool,
218    pub audit_seq: u64,
219    pub timestamp_ns: i64,
220}
221
222/// The outcome of a completed Clock set_rate.
223#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct ClockSetRateOutcome {
225    pub audit_seq: u64,
226    pub timestamp_ns: i64,
227}
228
229/// The outcome of a completed Clock query_rate.
230#[derive(Clone, Debug, PartialEq, Eq)]
231pub struct ClockQueryRateOutcome {
232    pub hz_out: u64,
233    pub audit_seq: u64,
234    pub timestamp_ns: i64,
235}
236
237/// The outcome of a completed Clock get_rate.
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct ClockGetRateOutcome {
240    pub hz: u64,
241    pub audit_seq: u64,
242    pub timestamp_ns: i64,
243}
244
245/// Why a Clock operation did not complete.
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub enum ClockOpError {
248    Denied { denial: Denial, audit_seq: u64 },
249    Backend { error: BackendError, audit_seq: u64 },
250}
251
252/// The outcome of a completed Reset assert.
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct ResetAssertOutcome {
255    pub audit_seq: u64,
256    pub timestamp_ns: i64,
257}
258
259/// The outcome of a completed Reset deassert.
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct ResetDeassertOutcome {
262    pub audit_seq: u64,
263    pub timestamp_ns: i64,
264}
265
266/// The outcome of a completed Reset toggle.
267#[derive(Clone, Debug, PartialEq, Eq)]
268pub struct ResetToggleOutcome {
269    pub audit_seq: u64,
270    pub timestamp_ns: i64,
271}
272
273/// The outcome of a completed Reset status query.
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub struct ResetStatusOutcome {
276    pub asserted: bool,
277    pub audit_seq: u64,
278    pub timestamp_ns: i64,
279}
280
281/// Why a Reset operation did not complete.
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
283pub enum ResetOpError {
284    Denied { denial: Denial, audit_seq: u64 },
285    Backend { error: BackendError, audit_seq: u64 },
286}
287
288/// The outcome of a completed Serial read.
289#[derive(Clone, Debug, PartialEq, Eq)]
290pub struct SerialReadOutcome {
291    pub data: Vec<u8>,
292    pub audit_seq: u64,
293    pub timestamp_ns: i64,
294}
295
296/// The outcome of a completed Serial write.
297#[derive(Clone, Debug, PartialEq, Eq)]
298pub struct SerialWriteOutcome {
299    pub audit_seq: u64,
300    pub timestamp_ns: i64,
301}
302
303/// Why a Serial operation did not complete.
304#[derive(Clone, Copy, Debug, PartialEq, Eq)]
305pub enum SerialOpError {
306    Denied { denial: Denial, audit_seq: u64 },
307    Backend { error: BackendError, audit_seq: u64 },
308}
309
310/// An operation within a bounded sequence.
311#[derive(Clone, Debug, PartialEq, Eq)]
312pub enum SequenceItem {
313    Read32 {
314        resource: ResourceId,
315        offset: u64,
316    },
317    Write32 {
318        resource: ResourceId,
319        offset: u64,
320        value: u32,
321        write_mask: u32,
322        precondition: Option<WritePrecondition>,
323        readback: bool,
324    },
325    Poll32 {
326        resource: ResourceId,
327        offset: u64,
328        expected: u32,
329        mask: u32,
330        interval_ns: i64,
331        timeout_ns: i64,
332    },
333    DelayNs(i64),
334    Barrier,
335    GpioRead {
336        resource: ResourceId,
337    },
338    GpioWrite {
339        resource: ResourceId,
340        value: bool,
341    },
342    I2cTransfer {
343        resource: ResourceId,
344        write_data: Vec<u8>,
345        read_length: u32,
346    },
347    SpiTransmit {
348        resource: ResourceId,
349        tx_data: Vec<u8>,
350    },
351}
352
353/// The outcome of one sequence item that completed.
354#[derive(Clone, Debug, PartialEq, Eq)]
355pub enum SequenceItemOutcome {
356    Read32(ReadOutcome),
357    Write32(WriteOutcome),
358    Poll32(PollOutcome),
359    DelayNs,
360    Barrier,
361    Error(Denial),
362    Backend(BackendError),
363    GpioRead(GpioReadOutcome),
364    GpioWrite(GpioWriteOutcome),
365    I2cTransfer(I2cTransferOutcome),
366    SpiTransmit(SpiTransmitOutcome),
367}
368
369/// Result of one sequence item that began execution.
370#[derive(Clone, Debug, PartialEq, Eq)]
371pub struct SequenceItemResult {
372    pub index: u32,
373    pub ok: bool,
374    pub outcome: SequenceItemOutcome,
375}
376
377/// Outcome of a sequence whose prevalidation succeeded.
378#[derive(Clone, Debug, PartialEq, Eq)]
379pub struct SequenceOutcome {
380    pub results: Vec<SequenceItemResult>,
381    pub complete: bool,
382}
383
384/// Why a sequence was rejected before any hardware access.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub enum SequenceError {
387    TooManyItems { max: usize, audit_seq: u64 },
388    Rejected { index: usize, denial: Denial, audit_seq: u64 },
389}
390
391/// The per-item result of a snapshot read that began execution.
392#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub struct SnapshotItemResult {
394    /// The requested item.
395    pub item: SnapshotItem,
396    /// The value read, or the backend error.
397    pub value: Result<u32, BackendError>,
398    /// Audit sequence assigned to this item.
399    pub audit_seq: u64,
400    /// Timestamp of this item's read.
401    pub timestamp_ns: i64,
402}
403
404/// Outcome of a snapshot whose prevalidation succeeded.
405#[derive(Clone, Debug, PartialEq, Eq)]
406pub struct SnapshotOutcome {
407    /// One result per item that began execution, in request order.
408    pub results: Vec<SnapshotItemResult>,
409    /// Whether every requested item completed successfully. When false the
410    /// snapshot stopped at the first backend failure and later items never
411    /// began.
412    pub complete: bool,
413}
414
415/// Why a snapshot was rejected before any hardware access.
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417pub enum SnapshotError {
418    /// More items than the execution limit permits.
419    TooManyItems {
420        /// The enforced limit.
421        max: usize,
422        /// Audit sequence of the rejection record.
423        audit_seq: u64,
424    },
425    /// An item failed prevalidation; no item was executed.
426    Rejected {
427        /// Index of the first offending item.
428        index: usize,
429        /// The rejecting policy layer's reason.
430        denial: Denial,
431        /// Audit sequence of the rejection record.
432        audit_seq: u64,
433    },
434}
435
436/// Read-path and sequence executor shared by all sessions. Each call takes the
437/// calling session's validated [`AccessPolicy`].
438#[derive(Debug)]
439pub struct Executor<B, C> {
440    backends: BTreeMap<ResourceId, B>,
441    clock: C,
442    limits: ExecLimits,
443}
444
445impl<B: ResourceBackend, C: Clock> Executor<B, C> {
446    /// Creates an executor over the instance's backends.
447    pub fn new(backends: BTreeMap<ResourceId, B>, clock: C, limits: ExecLimits) -> Self {
448        Self { backends, clock, limits }
449    }
450
451    /// The execution limits, for capability reporting.
452    pub fn limits(&self) -> ExecLimits {
453        self.limits
454    }
455
456    /// The backend for `resource`, for diagnostics and tests.
457    pub fn backend(&self, resource: ResourceId) -> Option<&B> {
458        self.backends.get(&resource)
459    }
460
461    /// Reads a register once directly from the backend without policy or audit.
462    pub fn poll_read_once(
463        &mut self,
464        resource: ResourceId,
465        offset: u64,
466    ) -> Result<u32, BackendError> {
467        let backend = self.backends.get_mut(&resource).ok_or(BackendError::Fault)?;
468        backend.read32(offset)
469    }
470
471    /// Invokes hardware MMIO barriers across all backends.
472    pub fn barrier(&mut self) {
473        for backend in self.backends.values_mut() {
474            backend.barrier();
475        }
476    }
477
478    /// Performs one policy-checked, audited 32-bit read.
479    pub fn read32(
480        &mut self,
481        policy: &AccessPolicy,
482        audit: &mut AuditRing,
483        session: u64,
484        resource: ResourceId,
485        offset: u64,
486    ) -> Result<ReadOutcome, ReadError> {
487        self.read32_internal(policy, audit, session, resource, offset, None)
488    }
489
490    /// Internal 32-bit read implementation supporting sequence item indexing.
491    pub fn read32_internal(
492        &mut self,
493        policy: &AccessPolicy,
494        audit: &mut AuditRing,
495        session: u64,
496        resource: ResourceId,
497        offset: u64,
498        item_index: Option<u32>,
499    ) -> Result<ReadOutcome, ReadError> {
500        let timestamp_ns = self.clock.now_ns();
501        let access_class =
502            if item_index.is_some() { AccessClass::Sequence } else { AccessClass::ReadOnce };
503        if let Err(denial) = policy.check_read32(resource, offset, access_class) {
504            let mut record = op_record(
505                session,
506                resource,
507                "read32",
508                offset,
509                Decision::Denied(denial),
510                OpStatus::Rejected,
511                None,
512                timestamp_ns,
513            );
514            record.item_index = item_index;
515            let audit_seq = audit.append(record);
516            return Err(ReadError::Denied { denial, audit_seq });
517        }
518        let Some(backend) = self.backends.get_mut(&resource) else {
519            let mut record = op_record(
520                session,
521                resource,
522                "read32",
523                offset,
524                Decision::Allowed,
525                OpStatus::BackendFault,
526                None,
527                timestamp_ns,
528            );
529            record.item_index = item_index;
530            let audit_seq = audit.append(record);
531            return Err(ReadError::Backend { error: BackendError::Fault, audit_seq });
532        };
533        match backend.read32(offset) {
534            Ok(value) => {
535                let mut record = op_record(
536                    session,
537                    resource,
538                    "read32",
539                    offset,
540                    Decision::Allowed,
541                    OpStatus::Ok,
542                    Some(value),
543                    timestamp_ns,
544                );
545                record.item_index = item_index;
546                let audit_seq = audit.append(record);
547                Ok(ReadOutcome { value, audit_seq, timestamp_ns })
548            }
549            Err(error) => {
550                let mut record = op_record(
551                    session,
552                    resource,
553                    "read32",
554                    offset,
555                    Decision::Allowed,
556                    OpStatus::BackendFault,
557                    None,
558                    timestamp_ns,
559                );
560                record.item_index = item_index;
561                let audit_seq = audit.append(record);
562                Err(ReadError::Backend { error, audit_seq })
563            }
564        }
565    }
566
567    /// Performs one policy-checked, audited 32-bit masked write.
568    pub fn write32(
569        &mut self,
570        policy: &AccessPolicy,
571        audit: &mut AuditRing,
572        session: u64,
573        resource: ResourceId,
574        offset: u64,
575        value: u32,
576        write_mask: u32,
577        precondition: Option<WritePrecondition>,
578        readback: bool,
579    ) -> Result<WriteOutcome, WriteError> {
580        self.write32_internal(
581            policy,
582            audit,
583            session,
584            resource,
585            offset,
586            value,
587            write_mask,
588            precondition,
589            readback,
590            None,
591        )
592    }
593
594    /// Internal 32-bit write implementation supporting sequence item indexing.
595    pub fn write32_internal(
596        &mut self,
597        policy: &AccessPolicy,
598        audit: &mut AuditRing,
599        session: u64,
600        resource: ResourceId,
601        offset: u64,
602        value: u32,
603        write_mask: u32,
604        precondition: Option<WritePrecondition>,
605        readback: bool,
606        item_index: Option<u32>,
607    ) -> Result<WriteOutcome, WriteError> {
608        let timestamp_ns = self.clock.now_ns();
609        let reg = match policy.check_write32(resource, offset, write_mask, precondition.as_ref()) {
610            Ok(r) => *r,
611            Err(denial) => {
612                let mut record = op_record(
613                    session,
614                    resource,
615                    "write32",
616                    offset,
617                    Decision::Denied(denial),
618                    OpStatus::Rejected,
619                    None,
620                    timestamp_ns,
621                );
622                record.item_index = item_index;
623                let audit_seq = audit.append(record);
624                return Err(WriteError::Denied { denial, audit_seq });
625            }
626        };
627
628        let Some(backend) = self.backends.get_mut(&resource) else {
629            let mut record = op_record(
630                session,
631                resource,
632                "write32",
633                offset,
634                Decision::Allowed,
635                OpStatus::BackendFault,
636                None,
637                timestamp_ns,
638            );
639            record.item_index = item_index;
640            let audit_seq = audit.append(record);
641            return Err(WriteError::Backend { error: BackendError::Fault, audit_seq });
642        };
643
644        let needs_before_read =
645            write_mask != 0xFFFF_FFFF || precondition.is_some() || reg.require_precondition;
646        let mut before_val = 0u32;
647        if needs_before_read {
648            let read_ts = self.clock.now_ns();
649            match backend.read32(offset) {
650                Ok(val) => {
651                    before_val = val;
652                    let mut record = op_record(
653                        session,
654                        resource,
655                        "write32_before_read",
656                        offset,
657                        Decision::Allowed,
658                        OpStatus::Ok,
659                        Some(val),
660                        read_ts,
661                    );
662                    record.item_index = item_index;
663                    audit.append(record);
664                }
665                Err(error) => {
666                    let mut record = op_record(
667                        session,
668                        resource,
669                        "write32_before_read",
670                        offset,
671                        Decision::Allowed,
672                        OpStatus::BackendFault,
673                        None,
674                        read_ts,
675                    );
676                    record.item_index = item_index;
677                    let audit_seq = audit.append(record);
678                    return Err(WriteError::Backend { error, audit_seq });
679                }
680            }
681        }
682
683        if let Some(pre) = precondition {
684            if (before_val & pre.mask) != (pre.expected & pre.mask) {
685                let fail_ts = self.clock.now_ns();
686                let mut record = op_record(
687                    session,
688                    resource,
689                    "write32",
690                    offset,
691                    Decision::Denied(Denial::PreconditionFailed),
692                    OpStatus::Rejected,
693                    Some(before_val),
694                    fail_ts,
695                );
696                record.item_index = item_index;
697                let audit_seq = audit.append(record);
698                return Err(WriteError::Denied { denial: Denial::PreconditionFailed, audit_seq });
699            }
700        }
701
702        let final_val = if write_mask == 0xFFFF_FFFF {
703            value
704        } else {
705            (before_val & !write_mask) | (value & write_mask)
706        };
707
708        let write_ts = self.clock.now_ns();
709        if let Err(error) = backend.write32(offset, final_val) {
710            let mut record = op_record(
711                session,
712                resource,
713                "write32",
714                offset,
715                Decision::Allowed,
716                OpStatus::BackendFault,
717                Some(final_val),
718                write_ts,
719            );
720            record.item_index = item_index;
721            let audit_seq = audit.append(record);
722            return Err(WriteError::Backend { error, audit_seq });
723        }
724
725        let mut record = op_record(
726            session,
727            resource,
728            "write32",
729            offset,
730            Decision::Allowed,
731            OpStatus::Ok,
732            Some(final_val),
733            write_ts,
734        );
735        record.item_index = item_index;
736        let audit_seq = audit.append(record);
737
738        backend.barrier();
739
740        let do_readback = readback || reg.readback;
741        let readback_val = if do_readback {
742            let rb_ts = self.clock.now_ns();
743            match backend.read32(offset) {
744                Ok(rb) => {
745                    let mut rb_record = op_record(
746                        session,
747                        resource,
748                        "write32_readback",
749                        offset,
750                        Decision::Allowed,
751                        OpStatus::Ok,
752                        Some(rb),
753                        rb_ts,
754                    );
755                    rb_record.item_index = item_index;
756                    audit.append(rb_record);
757                    rb
758                }
759                Err(error) => {
760                    let mut rb_record = op_record(
761                        session,
762                        resource,
763                        "write32_readback",
764                        offset,
765                        Decision::Allowed,
766                        OpStatus::BackendFault,
767                        None,
768                        rb_ts,
769                    );
770                    rb_record.item_index = item_index;
771                    let rb_audit_seq = audit.append(rb_record);
772                    return Err(WriteError::Backend { error, audit_seq: rb_audit_seq });
773                }
774            }
775        } else {
776            0
777        };
778
779        Ok(WriteOutcome { readback_value: readback_val, audit_seq, timestamp_ns: write_ts })
780    }
781
782    /// Evaluates one polling read step against expected value and mask.
783    pub fn poll32_read_step(
784        &mut self,
785        audit: &mut AuditRing,
786        session: u64,
787        resource: ResourceId,
788        offset: u64,
789        expected: u32,
790        mask: u32,
791        item_index: Option<u32>,
792    ) -> Result<Result<PollOutcome, u32>, PollError> {
793        let timestamp_ns = self.clock.now_ns();
794        let val = match self.poll_read_once(resource, offset) {
795            Ok(v) => v,
796            Err(error) => {
797                let mut record = op_record(
798                    session,
799                    resource,
800                    "poll32",
801                    offset,
802                    Decision::Allowed,
803                    OpStatus::BackendFault,
804                    None,
805                    timestamp_ns,
806                );
807                record.item_index = item_index;
808                let audit_seq = audit.append(record);
809                return Err(PollError::Backend { error, audit_seq });
810            }
811        };
812        if (val & mask) == (expected & mask) {
813            let mut record = op_record(
814                session,
815                resource,
816                "poll32",
817                offset,
818                Decision::Allowed,
819                OpStatus::Ok,
820                Some(val),
821                timestamp_ns,
822            );
823            record.item_index = item_index;
824            let audit_seq = audit.append(record);
825            Ok(Ok(PollOutcome { value: val, audit_seq, timestamp_ns }))
826        } else {
827            Ok(Err(val))
828        }
829    }
830
831    /// Records an audit rejection when polling times out.
832    pub fn poll32_timeout(
833        &mut self,
834        audit: &mut AuditRing,
835        session: u64,
836        resource: ResourceId,
837        offset: u64,
838        last_val: Option<u32>,
839        item_index: Option<u32>,
840    ) -> PollError {
841        let timestamp_ns = self.clock.now_ns();
842        let mut record = op_record(
843            session,
844            resource,
845            "poll32",
846            offset,
847            Decision::Denied(Denial::Timeout),
848            OpStatus::Rejected,
849            last_val,
850            timestamp_ns,
851        );
852        record.item_index = item_index;
853        let audit_seq = audit.append(record);
854        PollError::Denied { denial: Denial::Timeout, audit_seq }
855    }
856
857    /// Validates whole sequence before any hardware execution.
858    pub fn prevalidate_sequence(
859        &self,
860        policy: &AccessPolicy,
861        items: &[SequenceItem],
862    ) -> Result<(), (usize, Denial)> {
863        if items.len() > self.limits.max_sequence_items {
864            return Err((0, Denial::LimitExceeded));
865        }
866        for (index, item) in items.iter().enumerate() {
867            match item {
868                SequenceItem::Read32 { resource, offset } => {
869                    policy
870                        .check_read32(*resource, *offset, AccessClass::Sequence)
871                        .map_err(|d| (index, d))?;
872                }
873                SequenceItem::Write32 { resource, offset, write_mask, precondition, .. } => {
874                    policy
875                        .check_write32(*resource, *offset, *write_mask, precondition.as_ref())
876                        .map_err(|d| (index, d))?;
877                }
878                SequenceItem::Poll32 {
879                    resource, offset, mask, interval_ns, timeout_ns, ..
880                } => {
881                    policy
882                        .check_poll32(*resource, *offset, *mask, *interval_ns, *timeout_ns)
883                        .map_err(|d| (index, d))?;
884                    if *timeout_ns > self.limits.max_delay_ns {
885                        return Err((index, Denial::LimitExceeded));
886                    }
887                }
888                SequenceItem::DelayNs(delay_ns) => {
889                    if *delay_ns < 0 || *delay_ns > self.limits.max_delay_ns {
890                        return Err((index, Denial::LimitExceeded));
891                    }
892                }
893                SequenceItem::Barrier => {}
894                SequenceItem::GpioRead { resource } => {
895                    policy.check_gpio_read(*resource).map_err(|d| (index, d))?;
896                }
897                SequenceItem::GpioWrite { resource, .. } => {
898                    policy.check_gpio_write(*resource).map_err(|d| (index, d))?;
899                }
900                SequenceItem::I2cTransfer { resource, write_data, read_length } => {
901                    policy
902                        .check_i2c_transfer(*resource, write_data.len(), *read_length as usize)
903                        .map_err(|d| (index, d))?;
904                }
905                SequenceItem::SpiTransmit { resource, tx_data } => {
906                    policy.check_spi_transmit(*resource, tx_data.len()).map_err(|d| (index, d))?;
907                }
908            }
909        }
910        Ok(())
911    }
912
913    /// Performs a bounded snapshot: every item is validated before the
914    /// first read, then items execute in order, stopping at the first
915    /// backend failure.
916    pub fn snapshot(
917        &mut self,
918        policy: &AccessPolicy,
919        audit: &mut AuditRing,
920        session: u64,
921        items: &[SnapshotItem],
922    ) -> Result<SnapshotOutcome, SnapshotError> {
923        if items.len() > self.limits.max_snapshot_items {
924            let timestamp_ns = self.clock.now_ns();
925            let audit_seq = audit.append(AuditRecord {
926                session: Some(session),
927                resource: None,
928                operation: "snapshot",
929                offset: None,
930                decision: Decision::Denied(Denial::LimitExceeded),
931                status: OpStatus::Rejected,
932                value: None,
933                timestamp_ns,
934                run_id: None,
935                item_index: None,
936            });
937            return Err(SnapshotError::TooManyItems {
938                max: self.limits.max_snapshot_items,
939                audit_seq,
940            });
941        }
942        for (index, item) in items.iter().enumerate() {
943            if let Err(denial) =
944                policy.check_read32(item.resource, item.offset, AccessClass::Snapshot)
945            {
946                let timestamp_ns = self.clock.now_ns();
947                let mut record = op_record(
948                    session,
949                    item.resource,
950                    "snapshot",
951                    item.offset,
952                    Decision::Denied(denial),
953                    OpStatus::Rejected,
954                    None,
955                    timestamp_ns,
956                );
957                record.item_index = Some(index as u32);
958                let audit_seq = audit.append(record);
959                return Err(SnapshotError::Rejected { index, denial, audit_seq });
960            }
961        }
962        let mut results = Vec::with_capacity(items.len());
963        let mut complete = true;
964        for (index, item) in items.iter().enumerate() {
965            let timestamp_ns = self.clock.now_ns();
966            let value = match self.backends.get_mut(&item.resource) {
967                Some(backend) => backend.read32(item.offset),
968                None => Err(BackendError::Fault),
969            };
970            let (status, audited_value) = match value {
971                Ok(read) => (OpStatus::Ok, Some(read)),
972                Err(_) => (OpStatus::BackendFault, None),
973            };
974            let mut record = op_record(
975                session,
976                item.resource,
977                "snapshot_read32",
978                item.offset,
979                Decision::Allowed,
980                status,
981                audited_value,
982                timestamp_ns,
983            );
984            record.item_index = Some(index as u32);
985            let audit_seq = audit.append(record);
986            results.push(SnapshotItemResult { item: *item, value, audit_seq, timestamp_ns });
987            if value.is_err() {
988                complete = false;
989                break;
990            }
991        }
992        Ok(SnapshotOutcome { results, complete })
993    }
994
995    /// Performs one policy-checked, audited GPIO read.
996    pub fn gpio_read(
997        &mut self,
998        policy: &AccessPolicy,
999        audit: &mut AuditRing,
1000        session: u64,
1001        resource: ResourceId,
1002    ) -> Result<GpioReadOutcome, GpioReadError> {
1003        self.gpio_read_internal(policy, audit, session, resource, None)
1004    }
1005
1006    /// Internal GPIO read implementation supporting sequence item indexing.
1007    pub fn gpio_read_internal(
1008        &mut self,
1009        policy: &AccessPolicy,
1010        audit: &mut AuditRing,
1011        session: u64,
1012        resource: ResourceId,
1013        item_index: Option<u32>,
1014    ) -> Result<GpioReadOutcome, GpioReadError> {
1015        let timestamp_ns = self.clock.now_ns();
1016        if let Err(denial) = policy.check_gpio_read(resource) {
1017            let mut record = op_record(
1018                session,
1019                resource,
1020                "gpio_read",
1021                0,
1022                Decision::Denied(denial),
1023                OpStatus::Rejected,
1024                None,
1025                timestamp_ns,
1026            );
1027            record.item_index = item_index;
1028            let audit_seq = audit.append(record);
1029            return Err(GpioReadError::Denied { denial, audit_seq });
1030        }
1031        let Some(backend) = self.backends.get_mut(&resource) else {
1032            let mut record = op_record(
1033                session,
1034                resource,
1035                "gpio_read",
1036                0,
1037                Decision::Allowed,
1038                OpStatus::BackendFault,
1039                None,
1040                timestamp_ns,
1041            );
1042            record.item_index = item_index;
1043            let audit_seq = audit.append(record);
1044            return Err(GpioReadError::Backend { error: BackendError::Fault, audit_seq });
1045        };
1046        match backend.gpio_read() {
1047            Ok(value) => {
1048                let mut record = op_record(
1049                    session,
1050                    resource,
1051                    "gpio_read",
1052                    0,
1053                    Decision::Allowed,
1054                    OpStatus::Ok,
1055                    Some(value as u32),
1056                    timestamp_ns,
1057                );
1058                record.item_index = item_index;
1059                let audit_seq = audit.append(record);
1060                Ok(GpioReadOutcome { value, audit_seq, timestamp_ns })
1061            }
1062            Err(error) => {
1063                let mut record = op_record(
1064                    session,
1065                    resource,
1066                    "gpio_read",
1067                    0,
1068                    Decision::Allowed,
1069                    OpStatus::BackendFault,
1070                    None,
1071                    timestamp_ns,
1072                );
1073                record.item_index = item_index;
1074                let audit_seq = audit.append(record);
1075                Err(GpioReadError::Backend { error, audit_seq })
1076            }
1077        }
1078    }
1079
1080    /// Performs one policy-checked, audited GPIO write.
1081    pub fn gpio_write(
1082        &mut self,
1083        policy: &AccessPolicy,
1084        audit: &mut AuditRing,
1085        session: u64,
1086        resource: ResourceId,
1087        value: bool,
1088    ) -> Result<GpioWriteOutcome, GpioWriteError> {
1089        self.gpio_write_internal(policy, audit, session, resource, value, None)
1090    }
1091
1092    /// Internal GPIO write implementation supporting sequence item indexing.
1093    pub fn gpio_write_internal(
1094        &mut self,
1095        policy: &AccessPolicy,
1096        audit: &mut AuditRing,
1097        session: u64,
1098        resource: ResourceId,
1099        value: bool,
1100        item_index: Option<u32>,
1101    ) -> Result<GpioWriteOutcome, GpioWriteError> {
1102        let timestamp_ns = self.clock.now_ns();
1103        if let Err(denial) = policy.check_gpio_write(resource) {
1104            let mut record = op_record(
1105                session,
1106                resource,
1107                "gpio_write",
1108                0,
1109                Decision::Denied(denial),
1110                OpStatus::Rejected,
1111                Some(value as u32),
1112                timestamp_ns,
1113            );
1114            record.item_index = item_index;
1115            let audit_seq = audit.append(record);
1116            return Err(GpioWriteError::Denied { denial, audit_seq });
1117        }
1118        let Some(backend) = self.backends.get_mut(&resource) else {
1119            let mut record = op_record(
1120                session,
1121                resource,
1122                "gpio_write",
1123                0,
1124                Decision::Allowed,
1125                OpStatus::BackendFault,
1126                Some(value as u32),
1127                timestamp_ns,
1128            );
1129            record.item_index = item_index;
1130            let audit_seq = audit.append(record);
1131            return Err(GpioWriteError::Backend { error: BackendError::Fault, audit_seq });
1132        };
1133        match backend.gpio_write(value) {
1134            Ok(()) => {
1135                let mut record = op_record(
1136                    session,
1137                    resource,
1138                    "gpio_write",
1139                    0,
1140                    Decision::Allowed,
1141                    OpStatus::Ok,
1142                    Some(value as u32),
1143                    timestamp_ns,
1144                );
1145                record.item_index = item_index;
1146                let audit_seq = audit.append(record);
1147                Ok(GpioWriteOutcome { audit_seq, timestamp_ns })
1148            }
1149            Err(error) => {
1150                let mut record = op_record(
1151                    session,
1152                    resource,
1153                    "gpio_write",
1154                    0,
1155                    Decision::Allowed,
1156                    OpStatus::BackendFault,
1157                    Some(value as u32),
1158                    timestamp_ns,
1159                );
1160                record.item_index = item_index;
1161                let audit_seq = audit.append(record);
1162                Err(GpioWriteError::Backend { error, audit_seq })
1163            }
1164        }
1165    }
1166
1167    /// Performs one policy-checked, audited I2C transfer.
1168    pub fn i2c_transfer(
1169        &mut self,
1170        policy: &AccessPolicy,
1171        audit: &mut AuditRing,
1172        session: u64,
1173        resource: ResourceId,
1174        write_data: &[u8],
1175        read_length: u32,
1176    ) -> Result<I2cTransferOutcome, I2cTransferError> {
1177        self.i2c_transfer_internal(policy, audit, session, resource, write_data, read_length, None)
1178    }
1179
1180    /// Internal I2C transfer implementation supporting sequence item indexing.
1181    pub fn i2c_transfer_internal(
1182        &mut self,
1183        policy: &AccessPolicy,
1184        audit: &mut AuditRing,
1185        session: u64,
1186        resource: ResourceId,
1187        write_data: &[u8],
1188        read_length: u32,
1189        item_index: Option<u32>,
1190    ) -> Result<I2cTransferOutcome, I2cTransferError> {
1191        let timestamp_ns = self.clock.now_ns();
1192        if let Err(denial) =
1193            policy.check_i2c_transfer(resource, write_data.len(), read_length as usize)
1194        {
1195            let mut record = op_record(
1196                session,
1197                resource,
1198                "i2c_transfer",
1199                0,
1200                Decision::Denied(denial),
1201                OpStatus::Rejected,
1202                None,
1203                timestamp_ns,
1204            );
1205            record.item_index = item_index;
1206            let audit_seq = audit.append(record);
1207            return Err(I2cTransferError::Denied { denial, audit_seq });
1208        }
1209        let Some(backend) = self.backends.get_mut(&resource) else {
1210            let mut record = op_record(
1211                session,
1212                resource,
1213                "i2c_transfer",
1214                0,
1215                Decision::Allowed,
1216                OpStatus::BackendFault,
1217                None,
1218                timestamp_ns,
1219            );
1220            record.item_index = item_index;
1221            let audit_seq = audit.append(record);
1222            return Err(I2cTransferError::Backend { error: BackendError::Fault, audit_seq });
1223        };
1224        match backend.i2c_transfer(write_data, read_length as usize) {
1225            Ok(read_data) => {
1226                let mut record = op_record(
1227                    session,
1228                    resource,
1229                    "i2c_transfer",
1230                    0,
1231                    Decision::Allowed,
1232                    OpStatus::Ok,
1233                    Some(read_data.len() as u32),
1234                    timestamp_ns,
1235                );
1236                record.item_index = item_index;
1237                let audit_seq = audit.append(record);
1238                Ok(I2cTransferOutcome { read_data, audit_seq, timestamp_ns })
1239            }
1240            Err(error) => {
1241                let mut record = op_record(
1242                    session,
1243                    resource,
1244                    "i2c_transfer",
1245                    0,
1246                    Decision::Allowed,
1247                    OpStatus::BackendFault,
1248                    None,
1249                    timestamp_ns,
1250                );
1251                record.item_index = item_index;
1252                let audit_seq = audit.append(record);
1253                Err(I2cTransferError::Backend { error, audit_seq })
1254            }
1255        }
1256    }
1257
1258    /// Performs one policy-checked, audited SPI transmit.
1259    pub fn spi_transmit(
1260        &mut self,
1261        policy: &AccessPolicy,
1262        audit: &mut AuditRing,
1263        session: u64,
1264        resource: ResourceId,
1265        tx_data: &[u8],
1266    ) -> Result<SpiTransmitOutcome, SpiTransmitError> {
1267        self.spi_transmit_internal(policy, audit, session, resource, tx_data, None)
1268    }
1269
1270    /// Internal SPI transmit implementation supporting sequence item indexing.
1271    pub fn spi_transmit_internal(
1272        &mut self,
1273        policy: &AccessPolicy,
1274        audit: &mut AuditRing,
1275        session: u64,
1276        resource: ResourceId,
1277        tx_data: &[u8],
1278        item_index: Option<u32>,
1279    ) -> Result<SpiTransmitOutcome, SpiTransmitError> {
1280        let timestamp_ns = self.clock.now_ns();
1281        if let Err(denial) = policy.check_spi_transmit(resource, tx_data.len()) {
1282            let mut record = op_record(
1283                session,
1284                resource,
1285                "spi_transmit",
1286                0,
1287                Decision::Denied(denial),
1288                OpStatus::Rejected,
1289                None,
1290                timestamp_ns,
1291            );
1292            record.item_index = item_index;
1293            let audit_seq = audit.append(record);
1294            return Err(SpiTransmitError::Denied { denial, audit_seq });
1295        }
1296        let Some(backend) = self.backends.get_mut(&resource) else {
1297            let mut record = op_record(
1298                session,
1299                resource,
1300                "spi_transmit",
1301                0,
1302                Decision::Allowed,
1303                OpStatus::BackendFault,
1304                None,
1305                timestamp_ns,
1306            );
1307            record.item_index = item_index;
1308            let audit_seq = audit.append(record);
1309            return Err(SpiTransmitError::Backend { error: BackendError::Fault, audit_seq });
1310        };
1311        match backend.spi_transmit(tx_data) {
1312            Ok(rx_data) => {
1313                let mut record = op_record(
1314                    session,
1315                    resource,
1316                    "spi_transmit",
1317                    0,
1318                    Decision::Allowed,
1319                    OpStatus::Ok,
1320                    Some(rx_data.len() as u32),
1321                    timestamp_ns,
1322                );
1323                record.item_index = item_index;
1324                let audit_seq = audit.append(record);
1325                Ok(SpiTransmitOutcome { rx_data, audit_seq, timestamp_ns })
1326            }
1327            Err(error) => {
1328                let mut record = op_record(
1329                    session,
1330                    resource,
1331                    "spi_transmit",
1332                    0,
1333                    Decision::Allowed,
1334                    OpStatus::BackendFault,
1335                    None,
1336                    timestamp_ns,
1337                );
1338                record.item_index = item_index;
1339                let audit_seq = audit.append(record);
1340                Err(SpiTransmitError::Backend { error, audit_seq })
1341            }
1342        }
1343    }
1344
1345    pub fn clock_enable(
1346        &mut self,
1347        policy: &AccessPolicy,
1348        audit: &mut AuditRing,
1349        session: u64,
1350        resource: ResourceId,
1351    ) -> Result<ClockEnableOutcome, ClockOpError> {
1352        let timestamp_ns = self.clock.now_ns();
1353        if let Err(denial) = policy.check_clock_enable(resource) {
1354            let record = op_record(
1355                session,
1356                resource,
1357                "clock_enable",
1358                0,
1359                Decision::Denied(denial),
1360                OpStatus::Rejected,
1361                None,
1362                timestamp_ns,
1363            );
1364            let audit_seq = audit.append(record);
1365            return Err(ClockOpError::Denied { denial, audit_seq });
1366        }
1367        let Some(backend) = self.backends.get_mut(&resource) else {
1368            let record = op_record(
1369                session,
1370                resource,
1371                "clock_enable",
1372                0,
1373                Decision::Allowed,
1374                OpStatus::BackendFault,
1375                None,
1376                timestamp_ns,
1377            );
1378            let audit_seq = audit.append(record);
1379            return Err(ClockOpError::Backend { error: BackendError::Fault, audit_seq });
1380        };
1381        match backend.clock_enable() {
1382            Ok(()) => {
1383                let record = op_record(
1384                    session,
1385                    resource,
1386                    "clock_enable",
1387                    0,
1388                    Decision::Allowed,
1389                    OpStatus::Ok,
1390                    None,
1391                    timestamp_ns,
1392                );
1393                let audit_seq = audit.append(record);
1394                Ok(ClockEnableOutcome { audit_seq, timestamp_ns })
1395            }
1396            Err(error) => {
1397                let record = op_record(
1398                    session,
1399                    resource,
1400                    "clock_enable",
1401                    0,
1402                    Decision::Allowed,
1403                    OpStatus::BackendFault,
1404                    None,
1405                    timestamp_ns,
1406                );
1407                let audit_seq = audit.append(record);
1408                Err(ClockOpError::Backend { error, audit_seq })
1409            }
1410        }
1411    }
1412
1413    pub fn clock_disable(
1414        &mut self,
1415        policy: &AccessPolicy,
1416        audit: &mut AuditRing,
1417        session: u64,
1418        resource: ResourceId,
1419    ) -> Result<ClockDisableOutcome, ClockOpError> {
1420        let timestamp_ns = self.clock.now_ns();
1421        if let Err(denial) = policy.check_clock_disable(resource) {
1422            let record = op_record(
1423                session,
1424                resource,
1425                "clock_disable",
1426                0,
1427                Decision::Denied(denial),
1428                OpStatus::Rejected,
1429                None,
1430                timestamp_ns,
1431            );
1432            let audit_seq = audit.append(record);
1433            return Err(ClockOpError::Denied { denial, audit_seq });
1434        }
1435        let Some(backend) = self.backends.get_mut(&resource) else {
1436            let record = op_record(
1437                session,
1438                resource,
1439                "clock_disable",
1440                0,
1441                Decision::Allowed,
1442                OpStatus::BackendFault,
1443                None,
1444                timestamp_ns,
1445            );
1446            let audit_seq = audit.append(record);
1447            return Err(ClockOpError::Backend { error: BackendError::Fault, audit_seq });
1448        };
1449        match backend.clock_disable() {
1450            Ok(()) => {
1451                let record = op_record(
1452                    session,
1453                    resource,
1454                    "clock_disable",
1455                    0,
1456                    Decision::Allowed,
1457                    OpStatus::Ok,
1458                    None,
1459                    timestamp_ns,
1460                );
1461                let audit_seq = audit.append(record);
1462                Ok(ClockDisableOutcome { audit_seq, timestamp_ns })
1463            }
1464            Err(error) => {
1465                let record = op_record(
1466                    session,
1467                    resource,
1468                    "clock_disable",
1469                    0,
1470                    Decision::Allowed,
1471                    OpStatus::BackendFault,
1472                    None,
1473                    timestamp_ns,
1474                );
1475                let audit_seq = audit.append(record);
1476                Err(ClockOpError::Backend { error, audit_seq })
1477            }
1478        }
1479    }
1480
1481    pub fn clock_is_enabled(
1482        &mut self,
1483        policy: &AccessPolicy,
1484        audit: &mut AuditRing,
1485        session: u64,
1486        resource: ResourceId,
1487    ) -> Result<ClockIsEnabledOutcome, ClockOpError> {
1488        let timestamp_ns = self.clock.now_ns();
1489        if let Err(denial) = policy.check_clock_is_enabled(resource) {
1490            let record = op_record(
1491                session,
1492                resource,
1493                "clock_is_enabled",
1494                0,
1495                Decision::Denied(denial),
1496                OpStatus::Rejected,
1497                None,
1498                timestamp_ns,
1499            );
1500            let audit_seq = audit.append(record);
1501            return Err(ClockOpError::Denied { denial, audit_seq });
1502        }
1503        let Some(backend) = self.backends.get_mut(&resource) else {
1504            let record = op_record(
1505                session,
1506                resource,
1507                "clock_is_enabled",
1508                0,
1509                Decision::Allowed,
1510                OpStatus::BackendFault,
1511                None,
1512                timestamp_ns,
1513            );
1514            let audit_seq = audit.append(record);
1515            return Err(ClockOpError::Backend { error: BackendError::Fault, audit_seq });
1516        };
1517        match backend.clock_is_enabled() {
1518            Ok(enabled) => {
1519                let record = op_record(
1520                    session,
1521                    resource,
1522                    "clock_is_enabled",
1523                    0,
1524                    Decision::Allowed,
1525                    OpStatus::Ok,
1526                    Some(enabled as u32),
1527                    timestamp_ns,
1528                );
1529                let audit_seq = audit.append(record);
1530                Ok(ClockIsEnabledOutcome { enabled, audit_seq, timestamp_ns })
1531            }
1532            Err(error) => {
1533                let record = op_record(
1534                    session,
1535                    resource,
1536                    "clock_is_enabled",
1537                    0,
1538                    Decision::Allowed,
1539                    OpStatus::BackendFault,
1540                    None,
1541                    timestamp_ns,
1542                );
1543                let audit_seq = audit.append(record);
1544                Err(ClockOpError::Backend { error, audit_seq })
1545            }
1546        }
1547    }
1548
1549    pub fn clock_set_rate(
1550        &mut self,
1551        policy: &AccessPolicy,
1552        audit: &mut AuditRing,
1553        session: u64,
1554        resource: ResourceId,
1555        hz: u64,
1556    ) -> Result<ClockSetRateOutcome, ClockOpError> {
1557        let timestamp_ns = self.clock.now_ns();
1558        if let Err(denial) = policy.check_clock_set_rate(resource) {
1559            let record = op_record(
1560                session,
1561                resource,
1562                "clock_set_rate",
1563                0,
1564                Decision::Denied(denial),
1565                OpStatus::Rejected,
1566                None,
1567                timestamp_ns,
1568            );
1569            let audit_seq = audit.append(record);
1570            return Err(ClockOpError::Denied { denial, audit_seq });
1571        }
1572        let Some(backend) = self.backends.get_mut(&resource) else {
1573            let record = op_record(
1574                session,
1575                resource,
1576                "clock_set_rate",
1577                0,
1578                Decision::Allowed,
1579                OpStatus::BackendFault,
1580                None,
1581                timestamp_ns,
1582            );
1583            let audit_seq = audit.append(record);
1584            return Err(ClockOpError::Backend { error: BackendError::Fault, audit_seq });
1585        };
1586        match backend.clock_set_rate(hz) {
1587            Ok(()) => {
1588                let record = op_record(
1589                    session,
1590                    resource,
1591                    "clock_set_rate",
1592                    0,
1593                    Decision::Allowed,
1594                    OpStatus::Ok,
1595                    Some((hz & 0xffffffff) as u32),
1596                    timestamp_ns,
1597                );
1598                let audit_seq = audit.append(record);
1599                Ok(ClockSetRateOutcome { audit_seq, timestamp_ns })
1600            }
1601            Err(error) => {
1602                let record = op_record(
1603                    session,
1604                    resource,
1605                    "clock_set_rate",
1606                    0,
1607                    Decision::Allowed,
1608                    OpStatus::BackendFault,
1609                    None,
1610                    timestamp_ns,
1611                );
1612                let audit_seq = audit.append(record);
1613                Err(ClockOpError::Backend { error, audit_seq })
1614            }
1615        }
1616    }
1617
1618    pub fn clock_query_rate(
1619        &mut self,
1620        policy: &AccessPolicy,
1621        audit: &mut AuditRing,
1622        session: u64,
1623        resource: ResourceId,
1624        hz_in: u64,
1625    ) -> Result<ClockQueryRateOutcome, ClockOpError> {
1626        let timestamp_ns = self.clock.now_ns();
1627        if let Err(denial) = policy.check_clock_query_rate(resource) {
1628            let record = op_record(
1629                session,
1630                resource,
1631                "clock_query_rate",
1632                0,
1633                Decision::Denied(denial),
1634                OpStatus::Rejected,
1635                None,
1636                timestamp_ns,
1637            );
1638            let audit_seq = audit.append(record);
1639            return Err(ClockOpError::Denied { denial, audit_seq });
1640        }
1641        let Some(backend) = self.backends.get_mut(&resource) else {
1642            let record = op_record(
1643                session,
1644                resource,
1645                "clock_query_rate",
1646                0,
1647                Decision::Allowed,
1648                OpStatus::BackendFault,
1649                None,
1650                timestamp_ns,
1651            );
1652            let audit_seq = audit.append(record);
1653            return Err(ClockOpError::Backend { error: BackendError::Fault, audit_seq });
1654        };
1655        match backend.clock_query_rate(hz_in) {
1656            Ok(hz_out) => {
1657                let record = op_record(
1658                    session,
1659                    resource,
1660                    "clock_query_rate",
1661                    0,
1662                    Decision::Allowed,
1663                    OpStatus::Ok,
1664                    Some((hz_out & 0xffffffff) as u32),
1665                    timestamp_ns,
1666                );
1667                let audit_seq = audit.append(record);
1668                Ok(ClockQueryRateOutcome { hz_out, audit_seq, timestamp_ns })
1669            }
1670            Err(error) => {
1671                let record = op_record(
1672                    session,
1673                    resource,
1674                    "clock_query_rate",
1675                    0,
1676                    Decision::Allowed,
1677                    OpStatus::BackendFault,
1678                    None,
1679                    timestamp_ns,
1680                );
1681                let audit_seq = audit.append(record);
1682                Err(ClockOpError::Backend { error, audit_seq })
1683            }
1684        }
1685    }
1686
1687    pub fn clock_get_rate(
1688        &mut self,
1689        policy: &AccessPolicy,
1690        audit: &mut AuditRing,
1691        session: u64,
1692        resource: ResourceId,
1693    ) -> Result<ClockGetRateOutcome, ClockOpError> {
1694        let timestamp_ns = self.clock.now_ns();
1695        if let Err(denial) = policy.check_clock_get_rate(resource) {
1696            let record = op_record(
1697                session,
1698                resource,
1699                "clock_get_rate",
1700                0,
1701                Decision::Denied(denial),
1702                OpStatus::Rejected,
1703                None,
1704                timestamp_ns,
1705            );
1706            let audit_seq = audit.append(record);
1707            return Err(ClockOpError::Denied { denial, audit_seq });
1708        }
1709        let Some(backend) = self.backends.get_mut(&resource) else {
1710            let record = op_record(
1711                session,
1712                resource,
1713                "clock_get_rate",
1714                0,
1715                Decision::Allowed,
1716                OpStatus::BackendFault,
1717                None,
1718                timestamp_ns,
1719            );
1720            let audit_seq = audit.append(record);
1721            return Err(ClockOpError::Backend { error: BackendError::Fault, audit_seq });
1722        };
1723        match backend.clock_get_rate() {
1724            Ok(hz) => {
1725                let record = op_record(
1726                    session,
1727                    resource,
1728                    "clock_get_rate",
1729                    0,
1730                    Decision::Allowed,
1731                    OpStatus::Ok,
1732                    Some((hz & 0xffffffff) as u32),
1733                    timestamp_ns,
1734                );
1735                let audit_seq = audit.append(record);
1736                Ok(ClockGetRateOutcome { hz, audit_seq, timestamp_ns })
1737            }
1738            Err(error) => {
1739                let record = op_record(
1740                    session,
1741                    resource,
1742                    "clock_get_rate",
1743                    0,
1744                    Decision::Allowed,
1745                    OpStatus::BackendFault,
1746                    None,
1747                    timestamp_ns,
1748                );
1749                let audit_seq = audit.append(record);
1750                Err(ClockOpError::Backend { error, audit_seq })
1751            }
1752        }
1753    }
1754
1755    pub fn reset_assert(
1756        &mut self,
1757        policy: &AccessPolicy,
1758        audit: &mut AuditRing,
1759        session: u64,
1760        resource: ResourceId,
1761    ) -> Result<ResetAssertOutcome, ResetOpError> {
1762        let timestamp_ns = self.clock.now_ns();
1763        if let Err(denial) = policy.check_reset_assert(resource) {
1764            let record = op_record(
1765                session,
1766                resource,
1767                "reset_assert",
1768                0,
1769                Decision::Denied(denial),
1770                OpStatus::Rejected,
1771                None,
1772                timestamp_ns,
1773            );
1774            let audit_seq = audit.append(record);
1775            return Err(ResetOpError::Denied { denial, audit_seq });
1776        }
1777        let Some(backend) = self.backends.get_mut(&resource) else {
1778            let record = op_record(
1779                session,
1780                resource,
1781                "reset_assert",
1782                0,
1783                Decision::Allowed,
1784                OpStatus::BackendFault,
1785                None,
1786                timestamp_ns,
1787            );
1788            let audit_seq = audit.append(record);
1789            return Err(ResetOpError::Backend { error: BackendError::Fault, audit_seq });
1790        };
1791        match backend.reset_assert() {
1792            Ok(()) => {
1793                let record = op_record(
1794                    session,
1795                    resource,
1796                    "reset_assert",
1797                    0,
1798                    Decision::Allowed,
1799                    OpStatus::Ok,
1800                    None,
1801                    timestamp_ns,
1802                );
1803                let audit_seq = audit.append(record);
1804                Ok(ResetAssertOutcome { audit_seq, timestamp_ns })
1805            }
1806            Err(error) => {
1807                let record = op_record(
1808                    session,
1809                    resource,
1810                    "reset_assert",
1811                    0,
1812                    Decision::Allowed,
1813                    OpStatus::BackendFault,
1814                    None,
1815                    timestamp_ns,
1816                );
1817                let audit_seq = audit.append(record);
1818                Err(ResetOpError::Backend { error, audit_seq })
1819            }
1820        }
1821    }
1822
1823    pub fn reset_deassert(
1824        &mut self,
1825        policy: &AccessPolicy,
1826        audit: &mut AuditRing,
1827        session: u64,
1828        resource: ResourceId,
1829    ) -> Result<ResetDeassertOutcome, ResetOpError> {
1830        let timestamp_ns = self.clock.now_ns();
1831        if let Err(denial) = policy.check_reset_deassert(resource) {
1832            let record = op_record(
1833                session,
1834                resource,
1835                "reset_deassert",
1836                0,
1837                Decision::Denied(denial),
1838                OpStatus::Rejected,
1839                None,
1840                timestamp_ns,
1841            );
1842            let audit_seq = audit.append(record);
1843            return Err(ResetOpError::Denied { denial, audit_seq });
1844        }
1845        let Some(backend) = self.backends.get_mut(&resource) else {
1846            let record = op_record(
1847                session,
1848                resource,
1849                "reset_deassert",
1850                0,
1851                Decision::Allowed,
1852                OpStatus::BackendFault,
1853                None,
1854                timestamp_ns,
1855            );
1856            let audit_seq = audit.append(record);
1857            return Err(ResetOpError::Backend { error: BackendError::Fault, audit_seq });
1858        };
1859        match backend.reset_deassert() {
1860            Ok(()) => {
1861                let record = op_record(
1862                    session,
1863                    resource,
1864                    "reset_deassert",
1865                    0,
1866                    Decision::Allowed,
1867                    OpStatus::Ok,
1868                    None,
1869                    timestamp_ns,
1870                );
1871                let audit_seq = audit.append(record);
1872                Ok(ResetDeassertOutcome { audit_seq, timestamp_ns })
1873            }
1874            Err(error) => {
1875                let record = op_record(
1876                    session,
1877                    resource,
1878                    "reset_deassert",
1879                    0,
1880                    Decision::Allowed,
1881                    OpStatus::BackendFault,
1882                    None,
1883                    timestamp_ns,
1884                );
1885                let audit_seq = audit.append(record);
1886                Err(ResetOpError::Backend { error, audit_seq })
1887            }
1888        }
1889    }
1890
1891    pub fn reset_toggle(
1892        &mut self,
1893        policy: &AccessPolicy,
1894        audit: &mut AuditRing,
1895        session: u64,
1896        resource: ResourceId,
1897    ) -> Result<ResetToggleOutcome, ResetOpError> {
1898        let timestamp_ns = self.clock.now_ns();
1899        if let Err(denial) = policy.check_reset_toggle(resource) {
1900            let record = op_record(
1901                session,
1902                resource,
1903                "reset_toggle",
1904                0,
1905                Decision::Denied(denial),
1906                OpStatus::Rejected,
1907                None,
1908                timestamp_ns,
1909            );
1910            let audit_seq = audit.append(record);
1911            return Err(ResetOpError::Denied { denial, audit_seq });
1912        }
1913        let Some(backend) = self.backends.get_mut(&resource) else {
1914            let record = op_record(
1915                session,
1916                resource,
1917                "reset_toggle",
1918                0,
1919                Decision::Allowed,
1920                OpStatus::BackendFault,
1921                None,
1922                timestamp_ns,
1923            );
1924            let audit_seq = audit.append(record);
1925            return Err(ResetOpError::Backend { error: BackendError::Fault, audit_seq });
1926        };
1927        match backend.reset_toggle() {
1928            Ok(()) => {
1929                let record = op_record(
1930                    session,
1931                    resource,
1932                    "reset_toggle",
1933                    0,
1934                    Decision::Allowed,
1935                    OpStatus::Ok,
1936                    None,
1937                    timestamp_ns,
1938                );
1939                let audit_seq = audit.append(record);
1940                Ok(ResetToggleOutcome { audit_seq, timestamp_ns })
1941            }
1942            Err(error) => {
1943                let record = op_record(
1944                    session,
1945                    resource,
1946                    "reset_toggle",
1947                    0,
1948                    Decision::Allowed,
1949                    OpStatus::BackendFault,
1950                    None,
1951                    timestamp_ns,
1952                );
1953                let audit_seq = audit.append(record);
1954                Err(ResetOpError::Backend { error, audit_seq })
1955            }
1956        }
1957    }
1958
1959    pub fn reset_status(
1960        &mut self,
1961        policy: &AccessPolicy,
1962        audit: &mut AuditRing,
1963        session: u64,
1964        resource: ResourceId,
1965    ) -> Result<ResetStatusOutcome, ResetOpError> {
1966        let timestamp_ns = self.clock.now_ns();
1967        if let Err(denial) = policy.check_reset_status(resource) {
1968            let record = op_record(
1969                session,
1970                resource,
1971                "reset_status",
1972                0,
1973                Decision::Denied(denial),
1974                OpStatus::Rejected,
1975                None,
1976                timestamp_ns,
1977            );
1978            let audit_seq = audit.append(record);
1979            return Err(ResetOpError::Denied { denial, audit_seq });
1980        }
1981        let Some(backend) = self.backends.get_mut(&resource) else {
1982            let record = op_record(
1983                session,
1984                resource,
1985                "reset_status",
1986                0,
1987                Decision::Allowed,
1988                OpStatus::BackendFault,
1989                None,
1990                timestamp_ns,
1991            );
1992            let audit_seq = audit.append(record);
1993            return Err(ResetOpError::Backend { error: BackendError::Fault, audit_seq });
1994        };
1995        match backend.reset_status() {
1996            Ok(asserted) => {
1997                let record = op_record(
1998                    session,
1999                    resource,
2000                    "reset_status",
2001                    0,
2002                    Decision::Allowed,
2003                    OpStatus::Ok,
2004                    Some(asserted as u32),
2005                    timestamp_ns,
2006                );
2007                let audit_seq = audit.append(record);
2008                Ok(ResetStatusOutcome { asserted, audit_seq, timestamp_ns })
2009            }
2010            Err(error) => {
2011                let record = op_record(
2012                    session,
2013                    resource,
2014                    "reset_status",
2015                    0,
2016                    Decision::Allowed,
2017                    OpStatus::BackendFault,
2018                    None,
2019                    timestamp_ns,
2020                );
2021                let audit_seq = audit.append(record);
2022                Err(ResetOpError::Backend { error, audit_seq })
2023            }
2024        }
2025    }
2026
2027    pub fn serial_read(
2028        &mut self,
2029        policy: &AccessPolicy,
2030        audit: &mut AuditRing,
2031        session: u64,
2032        resource: ResourceId,
2033    ) -> Result<SerialReadOutcome, SerialOpError> {
2034        let timestamp_ns = self.clock.now_ns();
2035        if let Err(denial) = policy.check_serial_read(resource) {
2036            let record = op_record(
2037                session,
2038                resource,
2039                "serial_read",
2040                0,
2041                Decision::Denied(denial),
2042                OpStatus::Rejected,
2043                None,
2044                timestamp_ns,
2045            );
2046            let audit_seq = audit.append(record);
2047            return Err(SerialOpError::Denied { denial, audit_seq });
2048        }
2049        let Some(backend) = self.backends.get_mut(&resource) else {
2050            let record = op_record(
2051                session,
2052                resource,
2053                "serial_read",
2054                0,
2055                Decision::Allowed,
2056                OpStatus::BackendFault,
2057                None,
2058                timestamp_ns,
2059            );
2060            let audit_seq = audit.append(record);
2061            return Err(SerialOpError::Backend { error: BackendError::Fault, audit_seq });
2062        };
2063        match backend.serial_read() {
2064            Ok(data) => {
2065                let record = op_record(
2066                    session,
2067                    resource,
2068                    "serial_read",
2069                    0,
2070                    Decision::Allowed,
2071                    OpStatus::Ok,
2072                    Some(data.len() as u32),
2073                    timestamp_ns,
2074                );
2075                let audit_seq = audit.append(record);
2076                Ok(SerialReadOutcome { data, audit_seq, timestamp_ns })
2077            }
2078            Err(error) => {
2079                let record = op_record(
2080                    session,
2081                    resource,
2082                    "serial_read",
2083                    0,
2084                    Decision::Allowed,
2085                    OpStatus::BackendFault,
2086                    None,
2087                    timestamp_ns,
2088                );
2089                let audit_seq = audit.append(record);
2090                Err(SerialOpError::Backend { error, audit_seq })
2091            }
2092        }
2093    }
2094
2095    pub fn serial_write(
2096        &mut self,
2097        policy: &AccessPolicy,
2098        audit: &mut AuditRing,
2099        session: u64,
2100        resource: ResourceId,
2101        data: &[u8],
2102    ) -> Result<SerialWriteOutcome, SerialOpError> {
2103        let timestamp_ns = self.clock.now_ns();
2104        if let Err(denial) = policy.check_serial_write(resource, data.len()) {
2105            let record = op_record(
2106                session,
2107                resource,
2108                "serial_write",
2109                0,
2110                Decision::Denied(denial),
2111                OpStatus::Rejected,
2112                None,
2113                timestamp_ns,
2114            );
2115            let audit_seq = audit.append(record);
2116            return Err(SerialOpError::Denied { denial, audit_seq });
2117        }
2118        let Some(backend) = self.backends.get_mut(&resource) else {
2119            let record = op_record(
2120                session,
2121                resource,
2122                "serial_write",
2123                0,
2124                Decision::Allowed,
2125                OpStatus::BackendFault,
2126                None,
2127                timestamp_ns,
2128            );
2129            let audit_seq = audit.append(record);
2130            return Err(SerialOpError::Backend { error: BackendError::Fault, audit_seq });
2131        };
2132        match backend.serial_write(data) {
2133            Ok(()) => {
2134                let record = op_record(
2135                    session,
2136                    resource,
2137                    "serial_write",
2138                    0,
2139                    Decision::Allowed,
2140                    OpStatus::Ok,
2141                    Some(data.len() as u32),
2142                    timestamp_ns,
2143                );
2144                let audit_seq = audit.append(record);
2145                Ok(SerialWriteOutcome { audit_seq, timestamp_ns })
2146            }
2147            Err(error) => {
2148                let record = op_record(
2149                    session,
2150                    resource,
2151                    "serial_write",
2152                    0,
2153                    Decision::Allowed,
2154                    OpStatus::BackendFault,
2155                    None,
2156                    timestamp_ns,
2157                );
2158                let audit_seq = audit.append(record);
2159                Err(SerialOpError::Backend { error, audit_seq })
2160            }
2161        }
2162    }
2163}
2164
2165impl<B: ResourceBackend, C: Timer> Executor<B, C> {
2166    /// Performs an asynchronous 32-bit poll with an optional abort token.
2167    pub async fn poll32_with_abort(
2168        &mut self,
2169        policy: &AccessPolicy,
2170        audit: &mut AuditRing,
2171        session: u64,
2172        resource: ResourceId,
2173        offset: u64,
2174        expected: u32,
2175        mask: u32,
2176        interval_ns: i64,
2177        timeout_ns: i64,
2178        abort: Option<&std::sync::atomic::AtomicBool>,
2179    ) -> Result<PollOutcome, PollError> {
2180        self.poll32_internal(
2181            policy,
2182            audit,
2183            session,
2184            resource,
2185            offset,
2186            expected,
2187            mask,
2188            interval_ns,
2189            timeout_ns,
2190            None,
2191            abort,
2192        )
2193        .await
2194    }
2195
2196    /// Performs an asynchronous 32-bit poll.
2197    pub async fn poll32(
2198        &mut self,
2199        policy: &AccessPolicy,
2200        audit: &mut AuditRing,
2201        session: u64,
2202        resource: ResourceId,
2203        offset: u64,
2204        expected: u32,
2205        mask: u32,
2206        interval_ns: i64,
2207        timeout_ns: i64,
2208    ) -> Result<PollOutcome, PollError> {
2209        self.poll32_with_abort(
2210            policy,
2211            audit,
2212            session,
2213            resource,
2214            offset,
2215            expected,
2216            mask,
2217            interval_ns,
2218            timeout_ns,
2219            None,
2220        )
2221        .await
2222    }
2223
2224    /// Internal 32-bit poll implementation supporting sequence item indexing.
2225    pub async fn poll32_internal(
2226        &mut self,
2227        policy: &AccessPolicy,
2228        audit: &mut AuditRing,
2229        session: u64,
2230        resource: ResourceId,
2231        offset: u64,
2232        expected: u32,
2233        mask: u32,
2234        interval_ns: i64,
2235        timeout_ns: i64,
2236        item_index: Option<u32>,
2237        abort: Option<&std::sync::atomic::AtomicBool>,
2238    ) -> Result<PollOutcome, PollError> {
2239        let timestamp_ns = self.clock.now_ns();
2240        if let Some(token) = abort {
2241            if token.load(std::sync::atomic::Ordering::Relaxed) {
2242                let mut record = op_record(
2243                    session,
2244                    resource,
2245                    "poll32",
2246                    offset,
2247                    Decision::Denied(Denial::NotAccepting),
2248                    OpStatus::Rejected,
2249                    None,
2250                    timestamp_ns,
2251                );
2252                record.item_index = item_index;
2253                let audit_seq = audit.append(record);
2254                return Err(PollError::Denied { denial: Denial::NotAccepting, audit_seq });
2255            }
2256        }
2257        if let Err(denial) = policy.check_poll32(resource, offset, mask, interval_ns, timeout_ns) {
2258            let mut record = op_record(
2259                session,
2260                resource,
2261                "poll32",
2262                offset,
2263                Decision::Denied(denial),
2264                OpStatus::Rejected,
2265                None,
2266                timestamp_ns,
2267            );
2268            record.item_index = item_index;
2269            let audit_seq = audit.append(record);
2270            return Err(PollError::Denied { denial, audit_seq });
2271        }
2272        if timeout_ns > self.limits.max_delay_ns {
2273            let mut record = op_record(
2274                session,
2275                resource,
2276                "poll32",
2277                offset,
2278                Decision::Denied(Denial::LimitExceeded),
2279                OpStatus::Rejected,
2280                None,
2281                timestamp_ns,
2282            );
2283            record.item_index = item_index;
2284            let audit_seq = audit.append(record);
2285            return Err(PollError::Denied { denial: Denial::LimitExceeded, audit_seq });
2286        }
2287
2288        let start_ns = self.clock.now_ns();
2289        let deadline_ns = start_ns.saturating_add(timeout_ns);
2290        let mut last_val: Option<u32> = None;
2291        loop {
2292            if let Some(token) = abort {
2293                if token.load(std::sync::atomic::Ordering::Relaxed) {
2294                    let mut record = op_record(
2295                        session,
2296                        resource,
2297                        "poll32",
2298                        offset,
2299                        Decision::Denied(Denial::NotAccepting),
2300                        OpStatus::Rejected,
2301                        last_val,
2302                        self.clock.now_ns(),
2303                    );
2304                    record.item_index = item_index;
2305                    let audit_seq = audit.append(record);
2306                    return Err(PollError::Denied { denial: Denial::NotAccepting, audit_seq });
2307                }
2308            }
2309            match self
2310                .poll32_read_step(audit, session, resource, offset, expected, mask, item_index)?
2311            {
2312                Ok(outcome) => return Ok(outcome),
2313                Err(val) => {
2314                    last_val = Some(val);
2315                    let now = self.clock.now_ns();
2316                    if now >= deadline_ns {
2317                        return Err(self.poll32_timeout(
2318                            audit, session, resource, offset, last_val, item_index,
2319                        ));
2320                    }
2321                    let sleep_ns = interval_ns.min(deadline_ns - now);
2322                    if sleep_ns > 0 {
2323                        self.clock.sleep(sleep_ns).await;
2324                    }
2325                    if let Some(token) = abort {
2326                        if token.load(std::sync::atomic::Ordering::Relaxed) {
2327                            let mut record = op_record(
2328                                session,
2329                                resource,
2330                                "poll32",
2331                                offset,
2332                                Decision::Denied(Denial::NotAccepting),
2333                                OpStatus::Rejected,
2334                                last_val,
2335                                self.clock.now_ns(),
2336                            );
2337                            record.item_index = item_index;
2338                            let audit_seq = audit.append(record);
2339                            return Err(PollError::Denied {
2340                                denial: Denial::NotAccepting,
2341                                audit_seq,
2342                            });
2343                        }
2344                    }
2345                    if self.clock.now_ns() >= deadline_ns {
2346                        match self.poll32_read_step(
2347                            audit, session, resource, offset, expected, mask, item_index,
2348                        )? {
2349                            Ok(outcome) => return Ok(outcome),
2350                            Err(final_val) => {
2351                                return Err(self.poll32_timeout(
2352                                    audit,
2353                                    session,
2354                                    resource,
2355                                    offset,
2356                                    Some(final_val),
2357                                    item_index,
2358                                ));
2359                            }
2360                        }
2361                    }
2362                }
2363            }
2364        }
2365    }
2366
2367    /// Executes a bounded ordered sequence.
2368    pub async fn execute_sequence(
2369        &mut self,
2370        policy: &AccessPolicy,
2371        audit: &mut AuditRing,
2372        session: u64,
2373        items: &[SequenceItem],
2374    ) -> Result<SequenceOutcome, SequenceError> {
2375        self.execute_sequence_with_abort(policy, audit, session, items, None).await
2376    }
2377
2378    /// Executes a bounded ordered sequence with an optional abort token.
2379    pub async fn execute_sequence_with_abort(
2380        &mut self,
2381        policy: &AccessPolicy,
2382        audit: &mut AuditRing,
2383        session: u64,
2384        items: &[SequenceItem],
2385        abort: Option<&std::sync::atomic::AtomicBool>,
2386    ) -> Result<SequenceOutcome, SequenceError> {
2387        if items.len() > self.limits.max_sequence_items {
2388            let timestamp_ns = self.clock.now_ns();
2389            let record = AuditRecord {
2390                session: Some(session),
2391                resource: None,
2392                operation: "sequence",
2393                offset: None,
2394                decision: Decision::Denied(Denial::LimitExceeded),
2395                status: OpStatus::Rejected,
2396                value: None,
2397                timestamp_ns,
2398                run_id: None,
2399                item_index: None,
2400            };
2401            let audit_seq = audit.append(record);
2402            return Err(SequenceError::TooManyItems {
2403                max: self.limits.max_sequence_items,
2404                audit_seq,
2405            });
2406        }
2407        if let Err((index, denial)) = self.prevalidate_sequence(policy, items) {
2408            let timestamp_ns = self.clock.now_ns();
2409            let record = AuditRecord {
2410                session: Some(session),
2411                resource: None,
2412                operation: "sequence",
2413                offset: None,
2414                decision: Decision::Denied(denial),
2415                status: OpStatus::Rejected,
2416                value: None,
2417                timestamp_ns,
2418                run_id: None,
2419                item_index: Some(index as u32),
2420            };
2421            let audit_seq = audit.append(record);
2422            return Err(SequenceError::Rejected { index, denial, audit_seq });
2423        }
2424        if let Some(token) = abort {
2425            if token.load(std::sync::atomic::Ordering::Relaxed) {
2426                let timestamp_ns = self.clock.now_ns();
2427                let record = AuditRecord {
2428                    session: Some(session),
2429                    resource: None,
2430                    operation: "sequence",
2431                    offset: None,
2432                    decision: Decision::Denied(Denial::NotAccepting),
2433                    status: OpStatus::Rejected,
2434                    value: None,
2435                    timestamp_ns,
2436                    run_id: None,
2437                    item_index: None,
2438                };
2439                let audit_seq = audit.append(record);
2440                return Err(SequenceError::Rejected {
2441                    index: 0,
2442                    denial: Denial::NotAccepting,
2443                    audit_seq,
2444                });
2445            }
2446        }
2447
2448        let mut results = Vec::with_capacity(items.len());
2449        let mut complete = true;
2450        let start_ts = self.clock.now_ns();
2451
2452        for (index, item) in items.iter().enumerate() {
2453            let idx = index as u32;
2454            let now = self.clock.now_ns();
2455
2456            if let Some(token) = abort {
2457                if token.load(std::sync::atomic::Ordering::Relaxed) {
2458                    let record = AuditRecord {
2459                        session: Some(session),
2460                        resource: None,
2461                        operation: "sequence",
2462                        offset: None,
2463                        decision: Decision::Denied(Denial::NotAccepting),
2464                        status: OpStatus::Rejected,
2465                        value: None,
2466                        timestamp_ns: now,
2467                        run_id: None,
2468                        item_index: Some(idx),
2469                    };
2470                    audit.append(record);
2471                    results.push(SequenceItemResult {
2472                        index: idx,
2473                        ok: false,
2474                        outcome: SequenceItemOutcome::Error(Denial::NotAccepting),
2475                    });
2476                    complete = false;
2477                    break;
2478                }
2479            }
2480
2481            if now.saturating_sub(start_ts) > self.limits.max_sequence_duration_ns {
2482                let record = AuditRecord {
2483                    session: Some(session),
2484                    resource: None,
2485                    operation: "sequence",
2486                    offset: None,
2487                    decision: Decision::Denied(Denial::Timeout),
2488                    status: OpStatus::Rejected,
2489                    value: None,
2490                    timestamp_ns: now,
2491                    run_id: None,
2492                    item_index: Some(idx),
2493                };
2494                audit.append(record);
2495                results.push(SequenceItemResult {
2496                    index: idx,
2497                    ok: false,
2498                    outcome: SequenceItemOutcome::Error(Denial::Timeout),
2499                });
2500                complete = false;
2501                break;
2502            }
2503
2504            match item {
2505                SequenceItem::Read32 { resource, offset } => {
2506                    match self.read32_internal(
2507                        policy,
2508                        audit,
2509                        session,
2510                        *resource,
2511                        *offset,
2512                        Some(idx),
2513                    ) {
2514                        Ok(outcome) => {
2515                            results.push(SequenceItemResult {
2516                                index: idx,
2517                                ok: true,
2518                                outcome: SequenceItemOutcome::Read32(outcome),
2519                            });
2520                        }
2521                        Err(ReadError::Denied { denial, .. }) => {
2522                            results.push(SequenceItemResult {
2523                                index: idx,
2524                                ok: false,
2525                                outcome: SequenceItemOutcome::Error(denial),
2526                            });
2527                            complete = false;
2528                            break;
2529                        }
2530                        Err(ReadError::Backend { error, .. }) => {
2531                            results.push(SequenceItemResult {
2532                                index: idx,
2533                                ok: false,
2534                                outcome: SequenceItemOutcome::Backend(error),
2535                            });
2536                            complete = false;
2537                            break;
2538                        }
2539                    }
2540                }
2541                SequenceItem::Write32 {
2542                    resource,
2543                    offset,
2544                    value,
2545                    write_mask,
2546                    precondition,
2547                    readback,
2548                } => {
2549                    match self.write32_internal(
2550                        policy,
2551                        audit,
2552                        session,
2553                        *resource,
2554                        *offset,
2555                        *value,
2556                        *write_mask,
2557                        *precondition,
2558                        *readback,
2559                        Some(idx),
2560                    ) {
2561                        Ok(outcome) => {
2562                            results.push(SequenceItemResult {
2563                                index: idx,
2564                                ok: true,
2565                                outcome: SequenceItemOutcome::Write32(outcome),
2566                            });
2567                        }
2568                        Err(WriteError::Denied { denial, .. }) => {
2569                            results.push(SequenceItemResult {
2570                                index: idx,
2571                                ok: false,
2572                                outcome: SequenceItemOutcome::Error(denial),
2573                            });
2574                            complete = false;
2575                            break;
2576                        }
2577                        Err(WriteError::Backend { error, .. }) => {
2578                            results.push(SequenceItemResult {
2579                                index: idx,
2580                                ok: false,
2581                                outcome: SequenceItemOutcome::Backend(error),
2582                            });
2583                            complete = false;
2584                            break;
2585                        }
2586                    }
2587                }
2588                SequenceItem::Poll32 {
2589                    resource,
2590                    offset,
2591                    expected,
2592                    mask,
2593                    interval_ns,
2594                    timeout_ns,
2595                } => {
2596                    match self
2597                        .poll32_internal(
2598                            policy,
2599                            audit,
2600                            session,
2601                            *resource,
2602                            *offset,
2603                            *expected,
2604                            *mask,
2605                            *interval_ns,
2606                            *timeout_ns,
2607                            Some(idx),
2608                            abort,
2609                        )
2610                        .await
2611                    {
2612                        Ok(outcome) => {
2613                            results.push(SequenceItemResult {
2614                                index: idx,
2615                                ok: true,
2616                                outcome: SequenceItemOutcome::Poll32(outcome),
2617                            });
2618                        }
2619                        Err(PollError::Denied { denial, .. }) => {
2620                            results.push(SequenceItemResult {
2621                                index: idx,
2622                                ok: false,
2623                                outcome: SequenceItemOutcome::Error(denial),
2624                            });
2625                            complete = false;
2626                            break;
2627                        }
2628                        Err(PollError::Backend { error, .. }) => {
2629                            results.push(SequenceItemResult {
2630                                index: idx,
2631                                ok: false,
2632                                outcome: SequenceItemOutcome::Backend(error),
2633                            });
2634                            complete = false;
2635                            break;
2636                        }
2637                    }
2638                }
2639                SequenceItem::DelayNs(delay_ns) => {
2640                    let ts = self.clock.now_ns();
2641                    if *delay_ns > 0 {
2642                        self.clock.sleep(*delay_ns).await;
2643                    }
2644                    let record = AuditRecord {
2645                        session: Some(session),
2646                        resource: None,
2647                        operation: "delay",
2648                        offset: None,
2649                        decision: Decision::Allowed,
2650                        status: OpStatus::Ok,
2651                        value: None,
2652                        timestamp_ns: ts,
2653                        run_id: None,
2654                        item_index: Some(idx),
2655                    };
2656                    audit.append(record);
2657                    results.push(SequenceItemResult {
2658                        index: idx,
2659                        ok: true,
2660                        outcome: SequenceItemOutcome::DelayNs,
2661                    });
2662                }
2663                SequenceItem::Barrier => {
2664                    let ts = self.clock.now_ns();
2665                    self.barrier();
2666                    let record = AuditRecord {
2667                        session: Some(session),
2668                        resource: None,
2669                        operation: "barrier",
2670                        offset: None,
2671                        decision: Decision::Allowed,
2672                        status: OpStatus::Ok,
2673                        value: None,
2674                        timestamp_ns: ts,
2675                        run_id: None,
2676                        item_index: Some(idx),
2677                    };
2678                    audit.append(record);
2679                    results.push(SequenceItemResult {
2680                        index: idx,
2681                        ok: true,
2682                        outcome: SequenceItemOutcome::Barrier,
2683                    });
2684                }
2685                SequenceItem::GpioRead { resource } => {
2686                    match self.gpio_read_internal(policy, audit, session, *resource, Some(idx)) {
2687                        Ok(outcome) => {
2688                            results.push(SequenceItemResult {
2689                                index: idx,
2690                                ok: true,
2691                                outcome: SequenceItemOutcome::GpioRead(outcome),
2692                            });
2693                        }
2694                        Err(GpioReadError::Denied { denial, .. }) => {
2695                            results.push(SequenceItemResult {
2696                                index: idx,
2697                                ok: false,
2698                                outcome: SequenceItemOutcome::Error(denial),
2699                            });
2700                            complete = false;
2701                            break;
2702                        }
2703                        Err(GpioReadError::Backend { error, .. }) => {
2704                            results.push(SequenceItemResult {
2705                                index: idx,
2706                                ok: false,
2707                                outcome: SequenceItemOutcome::Backend(error),
2708                            });
2709                            complete = false;
2710                            break;
2711                        }
2712                    }
2713                }
2714                SequenceItem::GpioWrite { resource, value } => {
2715                    match self.gpio_write_internal(
2716                        policy,
2717                        audit,
2718                        session,
2719                        *resource,
2720                        *value,
2721                        Some(idx),
2722                    ) {
2723                        Ok(outcome) => {
2724                            results.push(SequenceItemResult {
2725                                index: idx,
2726                                ok: true,
2727                                outcome: SequenceItemOutcome::GpioWrite(outcome),
2728                            });
2729                        }
2730                        Err(GpioWriteError::Denied { denial, .. }) => {
2731                            results.push(SequenceItemResult {
2732                                index: idx,
2733                                ok: false,
2734                                outcome: SequenceItemOutcome::Error(denial),
2735                            });
2736                            complete = false;
2737                            break;
2738                        }
2739                        Err(GpioWriteError::Backend { error, .. }) => {
2740                            results.push(SequenceItemResult {
2741                                index: idx,
2742                                ok: false,
2743                                outcome: SequenceItemOutcome::Backend(error),
2744                            });
2745                            complete = false;
2746                            break;
2747                        }
2748                    }
2749                }
2750                SequenceItem::I2cTransfer { resource, write_data, read_length } => {
2751                    match self.i2c_transfer_internal(
2752                        policy,
2753                        audit,
2754                        session,
2755                        *resource,
2756                        write_data,
2757                        *read_length,
2758                        Some(idx),
2759                    ) {
2760                        Ok(outcome) => {
2761                            results.push(SequenceItemResult {
2762                                index: idx,
2763                                ok: true,
2764                                outcome: SequenceItemOutcome::I2cTransfer(outcome),
2765                            });
2766                        }
2767                        Err(I2cTransferError::Denied { denial, .. }) => {
2768                            results.push(SequenceItemResult {
2769                                index: idx,
2770                                ok: false,
2771                                outcome: SequenceItemOutcome::Error(denial),
2772                            });
2773                            complete = false;
2774                            break;
2775                        }
2776                        Err(I2cTransferError::Backend { error, .. }) => {
2777                            results.push(SequenceItemResult {
2778                                index: idx,
2779                                ok: false,
2780                                outcome: SequenceItemOutcome::Backend(error),
2781                            });
2782                            complete = false;
2783                            break;
2784                        }
2785                    }
2786                }
2787                SequenceItem::SpiTransmit { resource, tx_data } => {
2788                    match self.spi_transmit_internal(
2789                        policy,
2790                        audit,
2791                        session,
2792                        *resource,
2793                        tx_data,
2794                        Some(idx),
2795                    ) {
2796                        Ok(outcome) => {
2797                            results.push(SequenceItemResult {
2798                                index: idx,
2799                                ok: true,
2800                                outcome: SequenceItemOutcome::SpiTransmit(outcome),
2801                            });
2802                        }
2803                        Err(SpiTransmitError::Denied { denial, .. }) => {
2804                            results.push(SequenceItemResult {
2805                                index: idx,
2806                                ok: false,
2807                                outcome: SequenceItemOutcome::Error(denial),
2808                            });
2809                            complete = false;
2810                            break;
2811                        }
2812                        Err(SpiTransmitError::Backend { error, .. }) => {
2813                            results.push(SequenceItemResult {
2814                                index: idx,
2815                                ok: false,
2816                                outcome: SequenceItemOutcome::Backend(error),
2817                            });
2818                            complete = false;
2819                            break;
2820                        }
2821                    }
2822                }
2823            }
2824        }
2825        Ok(SequenceOutcome { results, complete })
2826    }
2827}
2828
2829fn op_record(
2830    session: u64,
2831    resource: ResourceId,
2832    operation: &'static str,
2833    offset: u64,
2834    decision: Decision,
2835    status: OpStatus,
2836    value: Option<u32>,
2837    timestamp_ns: i64,
2838) -> AuditRecord {
2839    AuditRecord {
2840        session: Some(session),
2841        resource: Some(resource),
2842        operation,
2843        offset: Some(offset),
2844        decision,
2845        status,
2846        value,
2847        timestamp_ns,
2848        run_id: None,
2849        item_index: None,
2850    }
2851}
2852
2853#[cfg(test)]
2854mod tests {
2855    use super::*;
2856    use crate::access_policy::{
2857        AccessRule, MmioResource, ResourceCeiling, SessionMode, WIDTH32, WritableRegister,
2858    };
2859    use crate::hardware_backend::{FakeClock, FakeMmio};
2860
2861    const CTRL: ResourceId = 1;
2862    const SESSION: u64 = 7;
2863
2864    fn rule(offset: u64, class: AccessClass) -> AccessRule {
2865        AccessRule { resource: CTRL, offset, width: WIDTH32, class }
2866    }
2867
2868    fn make_executor(
2869        rules: &[AccessRule],
2870    ) -> (Executor<FakeMmio, FakeClock>, AccessPolicy, AuditRing) {
2871        let resources =
2872            BTreeMap::from([(CTRL, MmioResource::mmio("ctrl".to_string(), 0x100, 0x100))]);
2873        let ceiling = BTreeMap::from([(
2874            CTRL,
2875            ResourceCeiling {
2876                hard_denied: vec![],
2877                allow_unknown_reads: true,
2878                allow_poll: false,
2879                writable_registers: vec![],
2880                protocol: None,
2881                allow_interrupt: false,
2882            },
2883        )]);
2884        let policy =
2885            AccessPolicy::new(SessionMode::ReadOnly, resources, ceiling, rules.iter().copied())
2886                .unwrap();
2887        let mut mmio = FakeMmio::new();
2888        mmio.set(0x3c, 0xdead_beef);
2889        mmio.set(0x40, 0x1234_5678);
2890        mmio.fail_at(0x44);
2891        let backends = BTreeMap::from([(CTRL, mmio)]);
2892        let clock = FakeClock::new(1000, 10);
2893        let executor = Executor::new(
2894            backends,
2895            clock,
2896            ExecLimits {
2897                max_snapshot_items: 4,
2898                max_sequence_items: 4,
2899                max_delay_ns: 5_000_000_000,
2900                max_sequence_duration_ns: 1_000_000_000,
2901            },
2902        );
2903        (executor, policy, AuditRing::new(32))
2904    }
2905
2906    fn make_mutating_executor(
2907        writable_registers: Vec<WritableRegister>,
2908        rules: &[AccessRule],
2909    ) -> (Executor<FakeMmio, FakeClock>, AccessPolicy, AuditRing) {
2910        let resources =
2911            BTreeMap::from([(CTRL, MmioResource::mmio("ctrl".to_string(), 0x100, 0x100))]);
2912        let ceiling = BTreeMap::from([(
2913            CTRL,
2914            ResourceCeiling {
2915                hard_denied: vec![],
2916                allow_unknown_reads: true,
2917                allow_poll: true,
2918                writable_registers,
2919                protocol: None,
2920                allow_interrupt: false,
2921            },
2922        )]);
2923        let policy =
2924            AccessPolicy::new(SessionMode::Mutating, resources, ceiling, rules.iter().copied())
2925                .unwrap();
2926        let mut mmio = FakeMmio::new();
2927        mmio.set(0x3c, 0xdead_beef);
2928        mmio.set(0x40, 0x1234_5678);
2929        mmio.fail_at(0x44);
2930        let backends = BTreeMap::from([(CTRL, mmio)]);
2931        let clock = FakeClock::new(1000, 10);
2932        let executor = Executor::new(
2933            backends,
2934            clock,
2935            ExecLimits {
2936                max_snapshot_items: 4,
2937                max_sequence_items: 8,
2938                max_delay_ns: 5_000_000_000,
2939                max_sequence_duration_ns: 1_000_000_000,
2940            },
2941        );
2942        (executor, policy, AuditRing::new(32))
2943    }
2944
2945    fn accesses(executor: &Executor<FakeMmio, FakeClock>) -> &[u64] {
2946        &executor.backend(CTRL).unwrap().accesses
2947    }
2948
2949    fn write_accesses(executor: &Executor<FakeMmio, FakeClock>) -> &[(u64, u32)] {
2950        &executor.backend(CTRL).unwrap().write_accesses
2951    }
2952
2953    fn barriers(executor: &Executor<FakeMmio, FakeClock>) -> usize {
2954        executor.backend(CTRL).unwrap().barriers
2955    }
2956
2957    #[test]
2958    fn read_allowed_returns_value_and_audits() {
2959        let (mut executor, policy, mut audit) = make_executor(&[rule(0x3c, AccessClass::ReadOnce)]);
2960        let outcome = executor.read32(&policy, &mut audit, SESSION, CTRL, 0x3c).unwrap();
2961        assert_eq!(outcome.value, 0xdead_beef);
2962        assert_eq!(outcome.timestamp_ns, 1000);
2963        assert_eq!(accesses(&executor), &[0x3c]);
2964
2965        let entry = &audit.read(outcome.audit_seq, 1).entries[0];
2966        assert_eq!(entry.record.decision, Decision::Allowed);
2967        assert_eq!(entry.record.status, OpStatus::Ok);
2968        assert_eq!(entry.record.value, Some(0xdead_beef));
2969        assert_eq!(entry.record.session, Some(SESSION));
2970    }
2971
2972    #[test]
2973    fn denied_read_touches_no_hardware_but_is_audited() {
2974        let (mut executor, policy, mut audit) = make_executor(&[rule(0x3c, AccessClass::ReadOnce)]);
2975        let error = executor.read32(&policy, &mut audit, SESSION, CTRL, 0x40).unwrap_err();
2976        let ReadError::Denied { denial, audit_seq } = error else {
2977            panic!("expected denial, got {error:?}");
2978        };
2979        assert_eq!(denial, Denial::NotInAllowlist);
2980        assert!(accesses(&executor).is_empty());
2981
2982        let entry = &audit.read(audit_seq, 1).entries[0];
2983        assert_eq!(entry.record.decision, Decision::Denied(Denial::NotInAllowlist));
2984        assert_eq!(entry.record.status, OpStatus::Rejected);
2985        assert_eq!(entry.record.value, None);
2986    }
2987
2988    #[test]
2989    fn read_backend_fault_is_audited() {
2990        let (mut executor, policy, mut audit) = make_executor(&[rule(0x44, AccessClass::ReadOnce)]);
2991        let error = executor.read32(&policy, &mut audit, SESSION, CTRL, 0x44).unwrap_err();
2992        let ReadError::Backend { error, audit_seq } = error else {
2993            panic!("expected backend error, got {error:?}");
2994        };
2995        assert_eq!(error, BackendError::Fault);
2996
2997        let entry = &audit.read(audit_seq, 1).entries[0];
2998        assert_eq!(entry.record.decision, Decision::Allowed);
2999        assert_eq!(entry.record.status, OpStatus::BackendFault);
3000    }
3001
3002    #[test]
3003    fn snapshot_reads_in_order_with_distinct_timestamps() {
3004        let (mut executor, policy, mut audit) =
3005            make_executor(&[rule(0x3c, AccessClass::Snapshot), rule(0x40, AccessClass::Snapshot)]);
3006        let items = [
3007            SnapshotItem { resource: CTRL, offset: 0x3c },
3008            SnapshotItem { resource: CTRL, offset: 0x40 },
3009        ];
3010        let outcome = executor.snapshot(&policy, &mut audit, SESSION, &items).unwrap();
3011        assert!(outcome.complete);
3012        assert_eq!(outcome.results.len(), 2);
3013        assert_eq!(outcome.results[0].value, Ok(0xdead_beef));
3014        assert_eq!(outcome.results[1].value, Ok(0x1234_5678));
3015        assert!(outcome.results[0].timestamp_ns < outcome.results[1].timestamp_ns);
3016        assert!(outcome.results[0].audit_seq < outcome.results[1].audit_seq);
3017        assert_eq!(accesses(&executor), &[0x3c, 0x40]);
3018    }
3019
3020    #[test]
3021    fn snapshot_prevalidation_blocks_every_read() {
3022        let (mut executor, policy, mut audit) = make_executor(&[rule(0x3c, AccessClass::Snapshot)]);
3023        let items = [
3024            SnapshotItem { resource: CTRL, offset: 0x3c },
3025            SnapshotItem { resource: CTRL, offset: 0x48 },
3026        ];
3027        let error = executor.snapshot(&policy, &mut audit, SESSION, &items).unwrap_err();
3028        let SnapshotError::Rejected { index, denial, .. } = error else {
3029            panic!("expected rejection, got {error:?}");
3030        };
3031        assert_eq!(index, 1);
3032        assert_eq!(denial, Denial::NotInAllowlist);
3033        assert!(accesses(&executor).is_empty());
3034    }
3035
3036    #[test]
3037    fn read_once_grant_does_not_authorize_snapshot() {
3038        let (mut executor, policy, mut audit) = make_executor(&[rule(0x3c, AccessClass::ReadOnce)]);
3039        let items = [SnapshotItem { resource: CTRL, offset: 0x3c }];
3040        let error = executor.snapshot(&policy, &mut audit, SESSION, &items).unwrap_err();
3041        let SnapshotError::Rejected { index, denial, .. } = error else {
3042            panic!("expected rejection, got {error:?}");
3043        };
3044        assert_eq!(index, 0);
3045        assert_eq!(denial, Denial::NotInAllowlist);
3046        assert!(accesses(&executor).is_empty());
3047    }
3048
3049    #[test]
3050    fn snapshot_stops_at_first_backend_fault() {
3051        let (mut executor, policy, mut audit) = make_executor(&[
3052            rule(0x3c, AccessClass::Snapshot),
3053            rule(0x44, AccessClass::Snapshot),
3054            rule(0x40, AccessClass::Snapshot),
3055        ]);
3056        let items = [
3057            SnapshotItem { resource: CTRL, offset: 0x3c },
3058            SnapshotItem { resource: CTRL, offset: 0x44 },
3059            SnapshotItem { resource: CTRL, offset: 0x40 },
3060        ];
3061        let outcome = executor.snapshot(&policy, &mut audit, SESSION, &items).unwrap();
3062        assert!(!outcome.complete);
3063        assert_eq!(outcome.results.len(), 2);
3064        assert_eq!(outcome.results[0].value, Ok(0xdead_beef));
3065        assert_eq!(outcome.results[1].value, Err(BackendError::Fault));
3066        // The third item never began.
3067        assert_eq!(accesses(&executor), &[0x3c, 0x44]);
3068
3069        let entry = &audit.read(outcome.results[1].audit_seq, 1).entries[0];
3070        assert_eq!(entry.record.status, OpStatus::BackendFault);
3071    }
3072
3073    #[test]
3074    fn snapshot_item_limit_enforced() {
3075        let (mut executor, policy, mut audit) = make_executor(&[rule(0x3c, AccessClass::Snapshot)]);
3076        let items = [SnapshotItem { resource: CTRL, offset: 0x3c }; 5];
3077        let error = executor.snapshot(&policy, &mut audit, SESSION, &items).unwrap_err();
3078        let SnapshotError::TooManyItems { max, audit_seq } = error else {
3079            panic!("expected limit rejection, got {error:?}");
3080        };
3081        assert_eq!(max, 4);
3082        assert!(accesses(&executor).is_empty());
3083
3084        let entry = &audit.read(audit_seq, 1).entries[0];
3085        assert_eq!(entry.record.decision, Decision::Denied(Denial::LimitExceeded));
3086    }
3087
3088    #[test]
3089    fn missing_backend_is_an_audited_fault() {
3090        let resources =
3091            BTreeMap::from([(CTRL, MmioResource::mmio("ctrl".to_string(), 0x100, 0x100))]);
3092        let ceiling = BTreeMap::from([(
3093            CTRL,
3094            ResourceCeiling {
3095                hard_denied: vec![],
3096                allow_unknown_reads: true,
3097                allow_poll: false,
3098                writable_registers: vec![],
3099                protocol: None,
3100                allow_interrupt: false,
3101            },
3102        )]);
3103        let policy = AccessPolicy::new(
3104            SessionMode::ReadOnly,
3105            resources,
3106            ceiling,
3107            [rule(0x3c, AccessClass::ReadOnce)],
3108        )
3109        .unwrap();
3110        let clock = FakeClock::new(0, 1);
3111        let mut executor = Executor::<FakeMmio, _>::new(
3112            BTreeMap::new(),
3113            clock,
3114            ExecLimits {
3115                max_snapshot_items: 4,
3116                max_sequence_items: 4,
3117                max_delay_ns: 5_000_000_000,
3118                max_sequence_duration_ns: 1_000_000_000,
3119            },
3120        );
3121        let mut audit = AuditRing::new(4);
3122        let error = executor.read32(&policy, &mut audit, SESSION, CTRL, 0x3c).unwrap_err();
3123        let ReadError::Backend { error, audit_seq } = error else {
3124            panic!("expected backend error, got {error:?}");
3125        };
3126        assert_eq!(error, BackendError::Fault);
3127        let entry = &audit.read(audit_seq, 1).entries[0];
3128        assert_eq!(entry.record.status, OpStatus::BackendFault);
3129    }
3130
3131    #[test]
3132    fn write_full_mask_writes_directly_and_applies_barrier() {
3133        let reg = WritableRegister {
3134            offset: 0x40,
3135            width: WIDTH32,
3136            allow_mask: 0xFFFF_FFFF,
3137            allow_rmw: false,
3138            require_precondition: false,
3139            precondition_mask: 0,
3140            readback: false,
3141        };
3142        let (mut executor, policy, mut audit) =
3143            make_mutating_executor(vec![reg], &[rule(0x40, AccessClass::Write)]);
3144        let outcome = executor
3145            .write32(
3146                &policy,
3147                &mut audit,
3148                SESSION,
3149                CTRL,
3150                0x40,
3151                0xcafe_babe,
3152                0xFFFF_FFFF,
3153                None,
3154                false,
3155            )
3156            .unwrap();
3157        assert_eq!(outcome.readback_value, 0);
3158        assert_eq!(write_accesses(&executor), &[(0x40, 0xcafe_babe)]);
3159        assert_eq!(barriers(&executor), 1);
3160        assert!(accesses(&executor).is_empty()); // No before-read since full-mask & no precondition
3161
3162        let entry = &audit.read(outcome.audit_seq, 1).entries[0];
3163        assert_eq!(entry.record.decision, Decision::Allowed);
3164        assert_eq!(entry.record.status, OpStatus::Ok);
3165        assert_eq!(entry.record.value, Some(0xcafe_babe));
3166    }
3167
3168    #[test]
3169    fn write_rmw_reads_before_and_modifies_bits() {
3170        let reg = WritableRegister {
3171            offset: 0x40,
3172            width: WIDTH32,
3173            allow_mask: 0x00FF_0000,
3174            allow_rmw: true,
3175            require_precondition: false,
3176            precondition_mask: 0,
3177            readback: false,
3178        };
3179        let (mut executor, policy, mut audit) =
3180            make_mutating_executor(vec![reg], &[rule(0x40, AccessClass::Write)]);
3181        // Original 0x40 is 0x1234_5678. We modify byte 2 to 0xab. Expected: 0x12ab_5678.
3182        let outcome = executor
3183            .write32(
3184                &policy,
3185                &mut audit,
3186                SESSION,
3187                CTRL,
3188                0x40,
3189                0x00ab_0000,
3190                0x00FF_0000,
3191                None,
3192                false,
3193            )
3194            .unwrap();
3195        assert_eq!(outcome.readback_value, 0);
3196        assert_eq!(accesses(&executor), &[0x40]); // Audited before-read
3197        assert_eq!(write_accesses(&executor), &[(0x40, 0x12ab_5678)]);
3198        assert_eq!(barriers(&executor), 1);
3199
3200        let entries = &audit.read(0, 5).entries;
3201        assert_eq!(entries.len(), 2);
3202        assert_eq!(entries[0].record.operation, "write32_before_read");
3203        assert_eq!(entries[0].record.value, Some(0x1234_5678));
3204        assert_eq!(entries[1].record.operation, "write32");
3205        assert_eq!(entries[1].record.value, Some(0x12ab_5678));
3206    }
3207
3208    #[test]
3209    fn write_precondition_satisfied_allows_write() {
3210        let reg = WritableRegister {
3211            offset: 0x40,
3212            width: WIDTH32,
3213            allow_mask: 0xFFFF_FFFF,
3214            allow_rmw: true,
3215            require_precondition: true,
3216            precondition_mask: 0xFFFF_0000,
3217            readback: false,
3218        };
3219        let pre = WritePrecondition { expected: 0x1234_0000, mask: 0xFFFF_0000 };
3220        let (mut executor, policy, mut audit) =
3221            make_mutating_executor(vec![reg], &[rule(0x40, AccessClass::Write)]);
3222        let outcome = executor
3223            .write32(
3224                &policy,
3225                &mut audit,
3226                SESSION,
3227                CTRL,
3228                0x40,
3229                0xcafe_babe,
3230                0xFFFF_FFFF,
3231                Some(pre),
3232                false,
3233            )
3234            .unwrap();
3235        assert_eq!(write_accesses(&executor), &[(0x40, 0xcafe_babe)]);
3236        assert_eq!(outcome.readback_value, 0);
3237    }
3238
3239    #[test]
3240    fn write_precondition_failed_rejects_and_prevents_write() {
3241        let reg = WritableRegister {
3242            offset: 0x40,
3243            width: WIDTH32,
3244            allow_mask: 0xFFFF_FFFF,
3245            allow_rmw: true,
3246            require_precondition: true,
3247            precondition_mask: 0xFFFF_0000,
3248            readback: false,
3249        };
3250        let pre = WritePrecondition { expected: 0x9999_0000, mask: 0xFFFF_0000 };
3251        let (mut executor, policy, mut audit) =
3252            make_mutating_executor(vec![reg], &[rule(0x40, AccessClass::Write)]);
3253        let error = executor
3254            .write32(
3255                &policy,
3256                &mut audit,
3257                SESSION,
3258                CTRL,
3259                0x40,
3260                0xcafe_babe,
3261                0xFFFF_FFFF,
3262                Some(pre),
3263                false,
3264            )
3265            .unwrap_err();
3266        let WriteError::Denied { denial, audit_seq } = error else {
3267            panic!("expected denial, got {error:?}");
3268        };
3269        assert_eq!(denial, Denial::PreconditionFailed);
3270        assert!(write_accesses(&executor).is_empty()); // Hardware was not written!
3271
3272        let entry = &audit.read(audit_seq, 1).entries[0];
3273        assert_eq!(entry.record.decision, Decision::Denied(Denial::PreconditionFailed));
3274        assert_eq!(entry.record.status, OpStatus::Rejected);
3275        assert_eq!(entry.record.value, Some(0x1234_5678)); // Audited before-value
3276    }
3277
3278    #[test]
3279    fn write_readback_audited_and_returned() {
3280        let reg = WritableRegister {
3281            offset: 0x40,
3282            width: WIDTH32,
3283            allow_mask: 0xFFFF_FFFF,
3284            allow_rmw: false,
3285            require_precondition: false,
3286            precondition_mask: 0,
3287            readback: true,
3288        };
3289        let (mut executor, policy, mut audit) =
3290            make_mutating_executor(vec![reg], &[rule(0x40, AccessClass::Write)]);
3291        let outcome = executor
3292            .write32(
3293                &policy,
3294                &mut audit,
3295                SESSION,
3296                CTRL,
3297                0x40,
3298                0xcafe_babe,
3299                0xFFFF_FFFF,
3300                None,
3301                false,
3302            )
3303            .unwrap();
3304        assert_eq!(outcome.readback_value, 0xcafe_babe);
3305
3306        let entries = &audit.read(0, 5).entries;
3307        assert_eq!(entries.len(), 2);
3308        assert_eq!(entries[0].record.operation, "write32");
3309        assert_eq!(entries[1].record.operation, "write32_readback");
3310        assert_eq!(entries[1].record.value, Some(0xcafe_babe));
3311    }
3312
3313    #[test]
3314    fn write_denied_in_read_only_session() {
3315        let (mut executor, policy, mut audit) = make_executor(&[rule(0x40, AccessClass::ReadOnce)]);
3316        let error = executor
3317            .write32(
3318                &policy,
3319                &mut audit,
3320                SESSION,
3321                CTRL,
3322                0x40,
3323                0xcafe_babe,
3324                0xFFFF_FFFF,
3325                None,
3326                false,
3327            )
3328            .unwrap_err();
3329        let WriteError::Denied { denial, .. } = error else {
3330            panic!("expected denial, got {error:?}");
3331        };
3332        assert_eq!(denial, Denial::ReadOnlySession);
3333        assert!(write_accesses(&executor).is_empty());
3334    }
3335
3336    #[test]
3337    fn poll32_matches_immediately_without_sleep() {
3338        let (mut executor, policy, mut audit) =
3339            make_mutating_executor(vec![], &[rule(0x3c, AccessClass::Poll)]);
3340        let outcome = futures::executor::block_on(executor.poll32(
3341            &policy,
3342            &mut audit,
3343            SESSION,
3344            CTRL,
3345            0x3c,
3346            0xdead_beef,
3347            0xFFFF_FFFF,
3348            100,
3349            1000,
3350        ))
3351        .unwrap();
3352        assert_eq!(outcome.value, 0xdead_beef);
3353        assert_eq!(executor.clock.sleeps.len(), 0);
3354        assert_eq!(accesses(&executor), &[0x3c]);
3355    }
3356
3357    #[test]
3358    fn poll32_timeout_audited_and_records_sleeps() {
3359        let (mut executor, policy, mut audit) =
3360            make_mutating_executor(vec![], &[rule(0x3c, AccessClass::Poll)]);
3361        let error = futures::executor::block_on(executor.poll32(
3362            &policy,
3363            &mut audit,
3364            SESSION,
3365            CTRL,
3366            0x3c,
3367            0x9999_9999,
3368            0xFFFF_FFFF,
3369            100,
3370            300,
3371        ))
3372        .unwrap_err();
3373        let PollError::Denied { denial, audit_seq } = error else {
3374            panic!("expected timeout denial, got {error:?}");
3375        };
3376        assert_eq!(denial, Denial::Timeout);
3377        assert!(executor.clock.sleeps.len() > 0);
3378
3379        let entry = &audit.read(audit_seq, 1).entries[0];
3380        assert_eq!(entry.record.operation, "poll32");
3381        assert_eq!(entry.record.decision, Decision::Denied(Denial::Timeout));
3382        assert_eq!(entry.record.status, OpStatus::Rejected);
3383        assert_eq!(entry.record.value, Some(0xdead_beef));
3384    }
3385
3386    #[test]
3387    fn sequence_prevalidation_blocks_all_hardware_access() {
3388        let reg = WritableRegister {
3389            offset: 0x40,
3390            width: WIDTH32,
3391            allow_mask: 0xFFFF_FFFF,
3392            allow_rmw: false,
3393            require_precondition: false,
3394            precondition_mask: 0,
3395            readback: false,
3396        };
3397        let (mut executor, policy, mut audit) = make_mutating_executor(
3398            vec![reg],
3399            &[rule(0x3c, AccessClass::Sequence), rule(0x40, AccessClass::Sequence)],
3400        );
3401        let items = [
3402            SequenceItem::Read32 { resource: CTRL, offset: 0x3c },
3403            SequenceItem::Write32 {
3404                resource: CTRL,
3405                offset: 0x50, // Not permitted by ceiling or allowlist!
3406                value: 123,
3407                write_mask: 0xFFFF_FFFF,
3408                precondition: None,
3409                readback: false,
3410            },
3411        ];
3412        let error = futures::executor::block_on(
3413            executor.execute_sequence(&policy, &mut audit, SESSION, &items),
3414        )
3415        .unwrap_err();
3416        let SequenceError::Rejected { index, denial, .. } = error else {
3417            panic!("expected rejection, got {error:?}");
3418        };
3419        assert_eq!(index, 1);
3420        assert_eq!(denial, Denial::WriteNotPermitted);
3421        // Zero hardware reads or writes!
3422        assert!(accesses(&executor).is_empty());
3423        assert!(write_accesses(&executor).is_empty());
3424    }
3425
3426    #[test]
3427    fn sequence_full_execution_succeeds_in_order() {
3428        let reg = WritableRegister {
3429            offset: 0x40,
3430            width: WIDTH32,
3431            allow_mask: 0xFFFF_FFFF,
3432            allow_rmw: false,
3433            require_precondition: false,
3434            precondition_mask: 0,
3435            readback: false,
3436        };
3437        let (mut executor, policy, mut audit) = make_mutating_executor(
3438            vec![reg],
3439            &[rule(0x3c, AccessClass::Sequence), rule(0x40, AccessClass::Sequence)],
3440        );
3441        let items = [
3442            SequenceItem::Read32 { resource: CTRL, offset: 0x3c },
3443            SequenceItem::Write32 {
3444                resource: CTRL,
3445                offset: 0x40,
3446                value: 0x9999_8888,
3447                write_mask: 0xFFFF_FFFF,
3448                precondition: None,
3449                readback: false,
3450            },
3451            SequenceItem::DelayNs(200),
3452            SequenceItem::Barrier,
3453            SequenceItem::Poll32 {
3454                resource: CTRL,
3455                offset: 0x40,
3456                expected: 0x9999_8888,
3457                mask: 0xFFFF_FFFF,
3458                interval_ns: 50,
3459                timeout_ns: 200,
3460            },
3461        ];
3462        let outcome = futures::executor::block_on(
3463            executor.execute_sequence(&policy, &mut audit, SESSION, &items),
3464        )
3465        .unwrap();
3466        assert!(outcome.complete);
3467        assert_eq!(outcome.results.len(), 5);
3468        assert!(outcome.results.iter().all(|r| r.ok));
3469        assert_eq!(write_accesses(&executor), &[(0x40, 0x9999_8888)]);
3470        assert_eq!(barriers(&executor), 2); // 1 from write, 1 explicit barrier item
3471
3472        let entries = &audit.read(0, 10).entries;
3473        assert_eq!(entries.len(), 5);
3474        for (i, entry) in entries.iter().enumerate() {
3475            assert_eq!(entry.record.item_index, Some(i as u32));
3476        }
3477    }
3478
3479    #[test]
3480    fn sequence_stops_at_first_runtime_failure() {
3481        let reg = WritableRegister {
3482            offset: 0x40,
3483            width: WIDTH32,
3484            allow_mask: 0xFFFF_FFFF,
3485            allow_rmw: true,
3486            require_precondition: true,
3487            precondition_mask: 0xFFFF_FFFF,
3488            readback: false,
3489        };
3490        let (mut executor, policy, mut audit) = make_mutating_executor(
3491            vec![reg],
3492            &[rule(0x3c, AccessClass::Sequence), rule(0x40, AccessClass::Sequence)],
3493        );
3494        let items = [
3495            SequenceItem::Read32 { resource: CTRL, offset: 0x3c },
3496            SequenceItem::Write32 {
3497                resource: CTRL,
3498                offset: 0x40,
3499                value: 0x9999_8888,
3500                write_mask: 0xFFFF_FFFF,
3501                precondition: Some(WritePrecondition { expected: 0x0000_0000, mask: 0xFFFF_FFFF }), // Will fail
3502                readback: false,
3503            },
3504            SequenceItem::Read32 { resource: CTRL, offset: 0x3c }, // Should never execute!
3505        ];
3506        let outcome = futures::executor::block_on(
3507            executor.execute_sequence(&policy, &mut audit, SESSION, &items),
3508        )
3509        .unwrap();
3510        assert!(!outcome.complete);
3511        assert_eq!(outcome.results.len(), 2);
3512        assert!(outcome.results[0].ok);
3513        assert!(!outcome.results[1].ok);
3514        // Third item never touched hardware
3515        assert_eq!(accesses(&executor), &[0x3c, 0x40]); // 0x3c from item 0, 0x40 from before-read in item 1
3516        assert!(write_accesses(&executor).is_empty());
3517    }
3518
3519    #[test]
3520    fn sequence_stops_on_duration_timeout() {
3521        let (mut executor, policy, mut audit) =
3522            make_mutating_executor(vec![], &[rule(0x3c, AccessClass::Sequence)]);
3523        executor.limits.max_sequence_duration_ns = 500;
3524        let items = [
3525            SequenceItem::Read32 { resource: CTRL, offset: 0x3c },
3526            SequenceItem::DelayNs(600),
3527            SequenceItem::Read32 { resource: CTRL, offset: 0x3c },
3528        ];
3529        let outcome = futures::executor::block_on(
3530            executor.execute_sequence(&policy, &mut audit, SESSION, &items),
3531        )
3532        .unwrap();
3533        assert!(!outcome.complete);
3534        assert_eq!(outcome.results.len(), 3);
3535        assert!(outcome.results[0].ok);
3536        assert!(outcome.results[1].ok);
3537        assert!(!outcome.results[2].ok);
3538        assert_eq!(outcome.results[2].outcome, SequenceItemOutcome::Error(Denial::Timeout));
3539    }
3540
3541    #[test]
3542    fn sequence_stops_on_abort() {
3543        let (mut executor, policy, mut audit) =
3544            make_mutating_executor(vec![], &[rule(0x3c, AccessClass::Sequence)]);
3545        let abort = std::sync::atomic::AtomicBool::new(true);
3546        let items = [SequenceItem::Read32 { resource: CTRL, offset: 0x3c }];
3547        let error = futures::executor::block_on(executor.execute_sequence_with_abort(
3548            &policy,
3549            &mut audit,
3550            SESSION,
3551            &items,
3552            Some(&abort),
3553        ))
3554        .unwrap_err();
3555        let SequenceError::Rejected { denial, .. } = error else {
3556            panic!("expected rejected error, got {error:?}");
3557        };
3558        assert_eq!(denial, Denial::NotAccepting);
3559    }
3560
3561    #[test]
3562    fn poll32_aborts_when_token_set() {
3563        let (mut executor, policy, mut audit) =
3564            make_mutating_executor(vec![], &[rule(0x3c, AccessClass::Poll)]);
3565        let abort = std::sync::atomic::AtomicBool::new(true);
3566        let error = futures::executor::block_on(executor.poll32_with_abort(
3567            &policy,
3568            &mut audit,
3569            SESSION,
3570            CTRL,
3571            0x3c,
3572            0xdead_beef,
3573            0xFFFF_FFFF,
3574            100,
3575            1000,
3576            Some(&abort),
3577        ))
3578        .unwrap_err();
3579        let PollError::Denied { denial, .. } = error else {
3580            panic!("expected denial, got {error:?}");
3581        };
3582        assert_eq!(denial, Denial::NotAccepting);
3583    }
3584
3585    #[test]
3586    fn protocol_gpio_standalone_read_and_write() {
3587        use crate::access_policy::ProtocolCeiling;
3588        let gpio_id = 2;
3589        let resources = BTreeMap::from([(gpio_id, MmioResource::gpio("gpio_pin".to_string()))]);
3590        let ceiling = BTreeMap::from([(
3591            gpio_id,
3592            ResourceCeiling {
3593                hard_denied: vec![],
3594                allow_unknown_reads: true,
3595                allow_poll: false,
3596                writable_registers: vec![],
3597                protocol: Some(ProtocolCeiling::default_for(
3598                    crate::access_policy::ResourceKind::Gpio,
3599                )),
3600                allow_interrupt: false,
3601            },
3602        )]);
3603        let policy_ro = AccessPolicy::new(
3604            SessionMode::ReadOnly,
3605            resources.clone(),
3606            ceiling.clone(),
3607            [AccessRule { resource: gpio_id, offset: 0, width: 0, class: AccessClass::Protocol }],
3608        )
3609        .unwrap();
3610        let policy_rw = AccessPolicy::new(
3611            SessionMode::Mutating,
3612            resources,
3613            ceiling,
3614            [AccessRule { resource: gpio_id, offset: 0, width: 0, class: AccessClass::Protocol }],
3615        )
3616        .unwrap();
3617
3618        let fake_gpio = crate::protocol_resource_adapter::FakeGpio::new(true);
3619        let backends = BTreeMap::from([(gpio_id, fake_gpio)]);
3620        let clock = FakeClock::new(1000, 10);
3621        let mut executor = Executor::new(
3622            backends,
3623            clock,
3624            ExecLimits {
3625                max_snapshot_items: 4,
3626                max_sequence_items: 4,
3627                max_delay_ns: 5_000_000_000,
3628                max_sequence_duration_ns: 1_000_000_000,
3629            },
3630        );
3631        let mut audit = AuditRing::new(32);
3632
3633        // Read in ReadOnly mode succeeds
3634        let read_outcome = executor.gpio_read(&policy_ro, &mut audit, SESSION, gpio_id).unwrap();
3635        assert_eq!(read_outcome.value, true);
3636
3637        // Write in ReadOnly mode is denied (requires mutation)
3638        let write_err =
3639            executor.gpio_write(&policy_ro, &mut audit, SESSION, gpio_id, false).unwrap_err();
3640        assert_eq!(
3641            write_err,
3642            GpioWriteError::Denied { denial: Denial::ReadOnlySession, audit_seq: 1 }
3643        );
3644
3645        // Write in Mutating mode succeeds
3646        let write_outcome =
3647            executor.gpio_write(&policy_rw, &mut audit, SESSION, gpio_id, false).unwrap();
3648        assert_eq!(write_outcome.audit_seq, 2);
3649
3650        // Read back new value
3651        let read_outcome2 = executor.gpio_read(&policy_rw, &mut audit, SESSION, gpio_id).unwrap();
3652        assert_eq!(read_outcome2.value, false);
3653    }
3654
3655    #[test]
3656    fn heterogeneous_sequence_executes_mmio_and_protocols() {
3657        use crate::access_policy::ProtocolCeiling;
3658        let mmio_id = 1;
3659        let gpio_id = 2;
3660        let i2c_id = 3;
3661        let spi_id = 4;
3662
3663        let resources = BTreeMap::from([
3664            (mmio_id, MmioResource::mmio("mmio".to_string(), 0x100, 0x100)),
3665            (gpio_id, MmioResource::gpio("gpio".to_string())),
3666            (i2c_id, MmioResource::i2c("i2c".to_string())),
3667            (spi_id, MmioResource::spi("spi".to_string())),
3668        ]);
3669        let ceiling = BTreeMap::from([
3670            (
3671                mmio_id,
3672                ResourceCeiling {
3673                    hard_denied: vec![],
3674                    allow_unknown_reads: true,
3675                    allow_poll: false,
3676                    writable_registers: vec![],
3677                    protocol: None,
3678                    allow_interrupt: false,
3679                },
3680            ),
3681            (
3682                gpio_id,
3683                ResourceCeiling {
3684                    hard_denied: vec![],
3685                    allow_unknown_reads: false,
3686                    allow_poll: false,
3687                    writable_registers: vec![],
3688                    protocol: Some(ProtocolCeiling::default_for(
3689                        crate::access_policy::ResourceKind::Gpio,
3690                    )),
3691                    allow_interrupt: false,
3692                },
3693            ),
3694            (
3695                i2c_id,
3696                ResourceCeiling {
3697                    hard_denied: vec![],
3698                    allow_unknown_reads: false,
3699                    allow_poll: false,
3700                    writable_registers: vec![],
3701                    protocol: Some(ProtocolCeiling::default_for(
3702                        crate::access_policy::ResourceKind::I2c,
3703                    )),
3704                    allow_interrupt: false,
3705                },
3706            ),
3707            (
3708                spi_id,
3709                ResourceCeiling {
3710                    hard_denied: vec![],
3711                    allow_unknown_reads: false,
3712                    allow_poll: false,
3713                    writable_registers: vec![],
3714                    protocol: Some(ProtocolCeiling::default_for(
3715                        crate::access_policy::ResourceKind::Spi,
3716                    )),
3717                    allow_interrupt: false,
3718                },
3719            ),
3720        ]);
3721        let rules = [
3722            AccessRule {
3723                resource: mmio_id,
3724                offset: 0x10,
3725                width: WIDTH32,
3726                class: AccessClass::Sequence,
3727            },
3728            AccessRule { resource: gpio_id, offset: 0, width: 0, class: AccessClass::Sequence },
3729            AccessRule { resource: i2c_id, offset: 0, width: 0, class: AccessClass::Sequence },
3730            AccessRule { resource: spi_id, offset: 0, width: 0, class: AccessClass::Sequence },
3731        ];
3732        let policy = AccessPolicy::new(SessionMode::Mutating, resources, ceiling, rules).unwrap();
3733
3734        let mut mmio = FakeMmio::new();
3735        mmio.set(0x10, 0xaabb_ccdd);
3736
3737        let gpio = crate::protocol_resource_adapter::FakeGpio::new(false);
3738
3739        let mut i2c = crate::protocol_resource_adapter::FakeI2c::new();
3740        i2c.set_read_response(vec![0x11, 0x22]);
3741
3742        let mut spi = crate::protocol_resource_adapter::FakeSpi::new();
3743        spi.set_rx_response(vec![0x33, 0x44]);
3744
3745        use crate::protocol_resource_adapter::DeviceBackend;
3746        let backends: BTreeMap<
3747            ResourceId,
3748            DeviceBackend<
3749                FakeMmio,
3750                crate::protocol_resource_adapter::FakeGpio,
3751                crate::protocol_resource_adapter::FakeI2c,
3752                crate::protocol_resource_adapter::FakeSpi,
3753            >,
3754        > = BTreeMap::from([
3755            (mmio_id, DeviceBackend::new_mmio(mmio)),
3756            (gpio_id, DeviceBackend::new_gpio(gpio)),
3757            (i2c_id, DeviceBackend::new_i2c(i2c)),
3758            (spi_id, DeviceBackend::new_spi(spi)),
3759        ]);
3760
3761        let clock = FakeClock::new(1000, 10);
3762        let mut executor = Executor::new(
3763            backends,
3764            clock,
3765            ExecLimits {
3766                max_snapshot_items: 4,
3767                max_sequence_items: 10,
3768                max_delay_ns: 5_000_000_000,
3769                max_sequence_duration_ns: 1_000_000_000,
3770            },
3771        );
3772        let mut audit = AuditRing::new(32);
3773
3774        let sequence = [
3775            SequenceItem::Read32 { resource: mmio_id, offset: 0x10 },
3776            SequenceItem::GpioWrite { resource: gpio_id, value: true },
3777            SequenceItem::DelayNs(100),
3778            SequenceItem::I2cTransfer { resource: i2c_id, write_data: vec![0x55], read_length: 2 },
3779            SequenceItem::SpiTransmit { resource: spi_id, tx_data: vec![0xaa, 0xbb] },
3780            SequenceItem::GpioRead { resource: gpio_id },
3781            SequenceItem::Barrier,
3782        ];
3783
3784        let outcome = futures::executor::block_on(
3785            executor.execute_sequence(&policy, &mut audit, SESSION, &sequence),
3786        )
3787        .unwrap();
3788
3789        assert!(outcome.complete);
3790        assert_eq!(outcome.results.len(), 7);
3791        assert!(outcome.results.iter().all(|r| r.ok));
3792
3793        match &outcome.results[0].outcome {
3794            SequenceItemOutcome::Read32(r) => assert_eq!(r.value, 0xaabb_ccdd),
3795            other => panic!("expected Read32, got {other:?}"),
3796        }
3797        assert!(matches!(outcome.results[1].outcome, SequenceItemOutcome::GpioWrite(_)));
3798        assert_eq!(outcome.results[2].outcome, SequenceItemOutcome::DelayNs);
3799        match &outcome.results[3].outcome {
3800            SequenceItemOutcome::I2cTransfer(t) => assert_eq!(t.read_data, vec![0x11, 0x22]),
3801            other => panic!("expected I2cTransfer, got {other:?}"),
3802        }
3803        match &outcome.results[4].outcome {
3804            SequenceItemOutcome::SpiTransmit(t) => assert_eq!(t.rx_data, vec![0x33, 0x44]),
3805            other => panic!("expected SpiTransmit, got {other:?}"),
3806        }
3807        match &outcome.results[5].outcome {
3808            SequenceItemOutcome::GpioRead(r) => assert_eq!(r.value, true),
3809            other => panic!("expected GpioRead, got {other:?}"),
3810        }
3811        assert_eq!(outcome.results[6].outcome, SequenceItemOutcome::Barrier);
3812
3813        let entries = &audit.read(0, 10).entries;
3814        assert_eq!(entries.len(), 7);
3815        for (i, entry) in entries.iter().enumerate() {
3816            assert_eq!(entry.record.item_index, Some(i as u32));
3817        }
3818    }
3819}