Skip to main content

lab_proxy_core/
access_policy.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//! Layered access validation for proxy operations.
6//!
7//! Every hardware access must be permitted by every layer: actual resource
8//! bounds, the immutable target ceiling, the exact session allowlist, and
9//! per-operation structural validation (width, alignment, and overflow).
10//! Rejection at any layer denies the access without touching hardware.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::ops::Range;
14
15/// Identifies a resource within one proxy instance.
16pub type ResourceId = u32;
17
18/// The only MMIO access width supported by the V1 wire contract, in bytes.
19pub const WIDTH32: u32 = 4;
20
21/// Session mode. A read-only session can never issue a write; a mutating
22/// session additionally requires the exclusive mutation lease.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum SessionMode {
25    /// The session may only read.
26    ReadOnly,
27    /// The session holds the exclusive mutation lease.
28    Mutating,
29}
30
31/// Access classes distinguished by policy. A grant for one class never
32/// authorizes another: a one-shot read grant does not authorize polling.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
34pub enum AccessClass {
35    /// A single 32-bit read.
36    ReadOnce,
37    /// A read performed as part of a bounded snapshot.
38    Snapshot,
39    /// A repeated bounded read.
40    Poll,
41    /// A masked write.
42    Write,
43    /// An operation within a bounded sequence.
44    Sequence,
45    /// An operation on a protocol-backed resource (GPIO, I2C, SPI).
46    Protocol,
47    /// An interrupt observation.
48    Interrupt,
49}
50
51/// The kind of hardware resource offered.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
53pub enum ResourceKind {
54    Mmio,
55    Gpio,
56    I2c,
57    Spi,
58    Interrupt,
59    Clock,
60    Reset,
61    Serial,
62}
63
64/// Description of a resource offered by the parent node. Descriptions
65/// never contain physical addresses or raw handles.
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct MmioResource {
68    /// Stable logical name.
69    pub name: String,
70    /// Resource kind.
71    pub kind: ResourceKind,
72    /// Size of the logical resource in bytes.
73    pub logical_size: u64,
74    /// Size of the actual backing mapping in bytes. Accesses must fit both
75    /// the logical and the mapped size.
76    pub mapped_size: u64,
77}
78
79impl MmioResource {
80    pub fn mmio(name: impl Into<String>, logical_size: u64, mapped_size: u64) -> Self {
81        Self { name: name.into(), kind: ResourceKind::Mmio, logical_size, mapped_size }
82    }
83
84    pub fn gpio(name: impl Into<String>) -> Self {
85        Self { name: name.into(), kind: ResourceKind::Gpio, logical_size: 1, mapped_size: 1 }
86    }
87
88    pub fn i2c(name: impl Into<String>) -> Self {
89        Self { name: name.into(), kind: ResourceKind::I2c, logical_size: 0, mapped_size: 0 }
90    }
91
92    pub fn spi(name: impl Into<String>) -> Self {
93        Self { name: name.into(), kind: ResourceKind::Spi, logical_size: 0, mapped_size: 0 }
94    }
95
96    pub fn interrupt(name: impl Into<String>) -> Self {
97        Self { name: name.into(), kind: ResourceKind::Interrupt, logical_size: 0, mapped_size: 0 }
98    }
99
100    pub fn clock(name: impl Into<String>) -> Self {
101        Self { name: name.into(), kind: ResourceKind::Clock, logical_size: 0, mapped_size: 0 }
102    }
103
104    pub fn reset(name: impl Into<String>) -> Self {
105        Self { name: name.into(), kind: ResourceKind::Reset, logical_size: 0, mapped_size: 0 }
106    }
107
108    pub fn serial(name: impl Into<String>) -> Self {
109        Self { name: name.into(), kind: ResourceKind::Serial, logical_size: 0, mapped_size: 0 }
110    }
111}
112
113/// Target ceiling specification for one writable register.
114///
115/// Writes are denied unless the immutable target ceiling contains an exact
116/// writable-register entry (Spec 9.4). A host/session grant cannot invent a
117/// write entry.
118#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
119pub struct WritableRegister {
120    /// Byte offset from the start of the logical resource.
121    pub offset: u64,
122    /// Access width in bytes (must be 4 for V1).
123    pub width: u32,
124    /// Bitmask of writable bits permitted by policy.
125    pub allow_mask: u32,
126    /// Whether read-modify-write is permitted for partial mask writes.
127    pub allow_rmw: bool,
128    /// Whether a precondition is required for any write to this register.
129    pub require_precondition: bool,
130    /// Bitmask of valid precondition bits.
131    pub precondition_mask: u32,
132    /// Whether readback is configured/supported.
133    pub readback: bool,
134}
135
136/// A precondition required or checked before executing a write.
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub struct WritePrecondition {
139    /// Expected value of bits selected by `mask`.
140    pub expected: u32,
141    /// Bitmask of bits to check.
142    pub mask: u32,
143}
144
145/// Target ceiling policy for a protocol-backed resource (GPIO, I2C, SPI).
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct ProtocolCeiling {
148    /// Allowed method names (e.g. "read", "write", "transfer", "transmit").
149    pub allowed_methods: BTreeSet<String>,
150    /// Maximum transfer or transmit size in bytes.
151    pub max_transfer_size: u32,
152    /// Whether mutating operations are permitted.
153    pub allow_mutating: bool,
154}
155
156impl ProtocolCeiling {
157    pub fn default_for(kind: ResourceKind) -> Self {
158        let mut allowed_methods = BTreeSet::new();
159        match kind {
160            ResourceKind::Gpio => {
161                allowed_methods.insert("read".to_string());
162                allowed_methods.insert("write".to_string());
163                Self { allowed_methods, max_transfer_size: 1, allow_mutating: true }
164            }
165            ResourceKind::I2c => {
166                allowed_methods.insert("transfer".to_string());
167                Self { allowed_methods, max_transfer_size: 8192, allow_mutating: true }
168            }
169            ResourceKind::Spi => {
170                allowed_methods.insert("transmit".to_string());
171                Self { allowed_methods, max_transfer_size: 8192, allow_mutating: true }
172            }
173            ResourceKind::Interrupt => {
174                allowed_methods.insert("wait".to_string());
175                Self { allowed_methods, max_transfer_size: 0, allow_mutating: false }
176            }
177            ResourceKind::Clock => {
178                allowed_methods.insert("enable".to_string());
179                allowed_methods.insert("disable".to_string());
180                allowed_methods.insert("is_enabled".to_string());
181                allowed_methods.insert("set_rate".to_string());
182                allowed_methods.insert("query_rate".to_string());
183                allowed_methods.insert("get_rate".to_string());
184                Self { allowed_methods, max_transfer_size: 0, allow_mutating: true }
185            }
186            ResourceKind::Reset => {
187                allowed_methods.insert("assert".to_string());
188                allowed_methods.insert("deassert".to_string());
189                allowed_methods.insert("toggle".to_string());
190                allowed_methods.insert("status".to_string());
191                Self { allowed_methods, max_transfer_size: 0, allow_mutating: true }
192            }
193            ResourceKind::Serial => {
194                allowed_methods.insert("read".to_string());
195                allowed_methods.insert("write".to_string());
196                Self { allowed_methods, max_transfer_size: 8192, allow_mutating: true }
197            }
198            ResourceKind::Mmio => {
199                Self { allowed_methods, max_transfer_size: 0, allow_mutating: false }
200            }
201        }
202    }
203}
204
205/// Immutable per-resource read ceiling. A resource with no ceiling entry
206/// permits nothing.
207#[derive(Clone, Debug, Default)]
208pub struct ResourceCeiling {
209    /// Byte ranges that may never be read, even with operator consent.
210    pub hard_denied: Vec<Range<u64>>,
211    /// Whether otherwise-unclassified reads may be enabled by an exact
212    /// session allowlist rule.
213    pub allow_unknown_reads: bool,
214    /// Whether repeated polling reads may be authorized by an exact
215    /// session allowlist rule.
216    pub allow_poll: bool,
217    /// Exact writable registers permitted by immutable target policy.
218    pub writable_registers: Vec<WritableRegister>,
219    /// Protocol-specific ceiling when the resource is protocol-backed.
220    pub protocol: Option<ProtocolCeiling>,
221    /// Whether interrupt observation may be authorized by session allowlist.
222    pub allow_interrupt: bool,
223}
224
225impl ResourceCeiling {
226    /// Looks up a writable register definition at `offset`.
227    pub fn get_writable_register(&self, offset: u64) -> Option<&WritableRegister> {
228        self.writable_registers.iter().find(|r| r.offset == offset)
229    }
230}
231
232/// One exact session allowlist rule. Wildcards do not exist: a rule matches
233/// exactly one resource, offset, width, and access class.
234#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
235pub struct AccessRule {
236    /// Resource the rule applies to.
237    pub resource: ResourceId,
238    /// Byte offset from the start of the logical resource.
239    pub offset: u64,
240    /// Access width in bytes.
241    pub width: u32,
242    /// Access class the rule authorizes.
243    pub class: AccessClass,
244}
245
246/// Why an access or allowlist rule was rejected. Reasons stay
247/// machine-readable; they must never exist only in a human detail string.
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub enum Denial {
250    /// The resource ID is not offered by this proxy instance.
251    UnknownResource,
252    /// The requested width is not supported.
253    UnsupportedWidth,
254    /// The requested access class is not supported by the V1 executor.
255    UnsupportedAccessClass,
256    /// The offset is not aligned to the access width.
257    Misaligned,
258    /// `offset + width` overflows.
259    OffsetOverflow,
260    /// The access does not fit the logical resource size.
261    OutOfLogicalBounds,
262    /// The access does not fit the actual backing mapping.
263    OutOfMappedBounds,
264    /// The target ceiling has no entry for this resource.
265    NotPermittedByCeiling,
266    /// The offset lies in a hard-denied range; operator consent cannot
267    /// enable it.
268    HardDenied,
269    /// The ceiling does not permit operator-authorized unknown reads on
270    /// this resource.
271    UnknownReadsNotPermitted,
272    /// The ceiling does not permit polling on this resource.
273    PollNotPermitted,
274    /// No exact session allowlist rule matches the access.
275    NotInAllowlist,
276    /// A bounded-execution limit (for example maximum snapshot items) was
277    /// exceeded.
278    LimitExceeded,
279    /// A session-open expectation (boot, generation, or digest) was stale.
280    /// Used in audit records for rejected session opens.
281    StaleIdentity,
282    /// The exclusive mutation lease was already held.
283    MutationLeaseContention,
284    /// The proxy is stopping and accepts no new sessions.
285    NotAccepting,
286    /// The request set a field this phase does not implement (for
287    /// example a phase 2 takeover expectation). Fail closed rather than
288    /// silently ignoring the field.
289    UnsupportedExpectation,
290    /// A precondition specified for a write failed.
291    PreconditionFailed,
292    /// Target policy requires a precondition, but none was provided.
293    MissingPrecondition,
294    /// An asynchronous poll timed out without matching.
295    Timeout,
296    /// A write was attempted in a read-only session.
297    ReadOnlySession,
298    /// A write was attempted to an offset or bits not permitted by target policy.
299    WriteNotPermitted,
300    /// A requested protocol method is not permitted by target policy.
301    UnsupportedMethod,
302    /// A requested protocol transfer exceeds maximum allowed size.
303    TransferTooLarge,
304}
305
306/// The validated policy engine for one session.
307///
308/// Construction validates the entire allowlist against the resources and
309/// ceiling; a rejected allowlist creates no engine and therefore no
310/// session, and no hardware operation is performed.
311#[derive(Debug)]
312pub struct AccessPolicy {
313    mode: SessionMode,
314    resources: BTreeMap<ResourceId, MmioResource>,
315    ceiling: BTreeMap<ResourceId, ResourceCeiling>,
316    allowlist: BTreeSet<AccessRule>,
317}
318
319impl AccessPolicy {
320    /// Builds a policy engine, validating every allowlist rule structurally
321    /// and against the ceiling. Returns the first offending rule and the
322    /// reason on failure.
323    pub fn new(
324        mode: SessionMode,
325        resources: BTreeMap<ResourceId, MmioResource>,
326        ceiling: BTreeMap<ResourceId, ResourceCeiling>,
327        allowlist: impl IntoIterator<Item = AccessRule>,
328    ) -> Result<Self, (AccessRule, Denial)> {
329        let allowlist: BTreeSet<AccessRule> = allowlist.into_iter().collect();
330        let policy = Self { mode, resources, ceiling, allowlist };
331        for rule in &policy.allowlist {
332            policy.validate_rule(rule).map_err(|denial| (*rule, denial))?;
333        }
334        Ok(policy)
335    }
336
337    /// The session mode (read-only or mutating).
338    pub fn mode(&self) -> SessionMode {
339        self.mode
340    }
341
342    /// IDs of every resource this policy knows about.
343    pub fn resource_ids(&self) -> impl Iterator<Item = ResourceId> {
344        self.resources.keys().copied()
345    }
346
347    /// Returns the descriptor for `id`, if known.
348    pub fn resource(&self, id: ResourceId) -> Option<&MmioResource> {
349        self.resources.get(&id)
350    }
351
352    /// Immediately-before-access validation for a 32-bit read. This covers
353    /// resource lookup, width, alignment, overflow, logical and mapped
354    /// bounds, ceiling hard denies, unknown-read permission, and the exact
355    /// allowlist match. Session/generation staleness is the session
356    /// server's job and the volatile read itself is the executor's.
357    pub fn check_read32(
358        &self,
359        resource: ResourceId,
360        offset: u64,
361        class: AccessClass,
362    ) -> Result<(), Denial> {
363        if !matches!(class, AccessClass::ReadOnce | AccessClass::Snapshot | AccessClass::Sequence) {
364            return Err(Denial::UnsupportedAccessClass);
365        }
366        self.validate_read(resource, offset, WIDTH32)?;
367        let rule = AccessRule { resource, offset, width: WIDTH32, class };
368        let rule_seq =
369            AccessRule { resource, offset, width: WIDTH32, class: AccessClass::Sequence };
370        if !self.allowlist.contains(&rule) && !self.allowlist.contains(&rule_seq) {
371            return Err(Denial::NotInAllowlist);
372        }
373        Ok(())
374    }
375
376    /// Immediately-before-access validation for a 32-bit write.
377    pub fn check_write32(
378        &self,
379        resource: ResourceId,
380        offset: u64,
381        write_mask: u32,
382        precondition: Option<&WritePrecondition>,
383    ) -> Result<&WritableRegister, Denial> {
384        if self.mode != SessionMode::Mutating {
385            return Err(Denial::ReadOnlySession);
386        }
387        if write_mask == 0 {
388            return Err(Denial::WriteNotPermitted);
389        }
390        self.validate_write_rule(resource, offset, WIDTH32)?;
391        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
392        let reg = ceiling.get_writable_register(offset).ok_or(Denial::WriteNotPermitted)?;
393        if (write_mask & !reg.allow_mask) != 0 {
394            return Err(Denial::WriteNotPermitted);
395        }
396        if write_mask != 0xFFFF_FFFF && !reg.allow_rmw {
397            return Err(Denial::WriteNotPermitted);
398        }
399        if reg.require_precondition && precondition.is_none() {
400            return Err(Denial::MissingPrecondition);
401        }
402        if let Some(pre) = precondition {
403            if !reg.allow_rmw {
404                return Err(Denial::WriteNotPermitted);
405            }
406            if pre.mask == 0 || (pre.mask & !reg.precondition_mask) != 0 {
407                return Err(Denial::WriteNotPermitted);
408            }
409        }
410        let rule_write = AccessRule { resource, offset, width: WIDTH32, class: AccessClass::Write };
411        let rule_seq =
412            AccessRule { resource, offset, width: WIDTH32, class: AccessClass::Sequence };
413        if !self.allowlist.contains(&rule_write) && !self.allowlist.contains(&rule_seq) {
414            return Err(Denial::NotInAllowlist);
415        }
416        Ok(reg)
417    }
418
419    /// Immediately-before-access validation for repeated polling reads.
420    pub fn check_poll32(
421        &self,
422        resource: ResourceId,
423        offset: u64,
424        mask: u32,
425        interval_ns: i64,
426        timeout_ns: i64,
427    ) -> Result<(), Denial> {
428        if mask == 0 {
429            return Err(Denial::Misaligned);
430        }
431        if interval_ns < 0 || timeout_ns < 0 {
432            return Err(Denial::LimitExceeded);
433        }
434        self.validate_read(resource, offset, WIDTH32)?;
435        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
436        if !ceiling.allow_poll {
437            return Err(Denial::PollNotPermitted);
438        }
439        let rule_poll = AccessRule { resource, offset, width: WIDTH32, class: AccessClass::Poll };
440        let rule_seq =
441            AccessRule { resource, offset, width: WIDTH32, class: AccessClass::Sequence };
442        if !self.allowlist.contains(&rule_poll) && !self.allowlist.contains(&rule_seq) {
443            return Err(Denial::NotInAllowlist);
444        }
445        Ok(())
446    }
447
448    /// Pre-session validation of one allowlist rule.
449    fn validate_rule(&self, rule: &AccessRule) -> Result<(), Denial> {
450        match rule.class {
451            AccessClass::ReadOnce | AccessClass::Snapshot => {
452                self.validate_read(rule.resource, rule.offset, rule.width)
453            }
454            AccessClass::Poll => {
455                self.validate_read(rule.resource, rule.offset, rule.width)?;
456                let ceiling =
457                    self.ceiling.get(&rule.resource).ok_or(Denial::NotPermittedByCeiling)?;
458                if !ceiling.allow_poll {
459                    return Err(Denial::PollNotPermitted);
460                }
461                Ok(())
462            }
463            AccessClass::Write => {
464                if self.mode != SessionMode::Mutating {
465                    return Err(Denial::ReadOnlySession);
466                }
467                self.validate_write_rule(rule.resource, rule.offset, rule.width)?;
468                let ceiling =
469                    self.ceiling.get(&rule.resource).ok_or(Denial::NotPermittedByCeiling)?;
470                if ceiling.get_writable_register(rule.offset).is_none() {
471                    return Err(Denial::WriteNotPermitted);
472                }
473                Ok(())
474            }
475            AccessClass::Sequence => {
476                let desc = self.resources.get(&rule.resource).ok_or(Denial::UnknownResource)?;
477                if desc.kind != ResourceKind::Mmio {
478                    let ceiling =
479                        self.ceiling.get(&rule.resource).ok_or(Denial::NotPermittedByCeiling)?;
480                    if ceiling.protocol.is_none() {
481                        return Err(Denial::NotPermittedByCeiling);
482                    }
483                    return Ok(());
484                }
485                if rule.width != WIDTH32 {
486                    return Err(Denial::UnsupportedWidth);
487                }
488                if rule.offset % u64::from(rule.width) != 0 {
489                    return Err(Denial::Misaligned);
490                }
491                let end =
492                    rule.offset.checked_add(u64::from(rule.width)).ok_or(Denial::OffsetOverflow)?;
493                if end > desc.logical_size {
494                    return Err(Denial::OutOfLogicalBounds);
495                }
496                if end > desc.mapped_size {
497                    return Err(Denial::OutOfMappedBounds);
498                }
499                let ceiling =
500                    self.ceiling.get(&rule.resource).ok_or(Denial::NotPermittedByCeiling)?;
501                if ceiling
502                    .hard_denied
503                    .iter()
504                    .any(|range| range.start < end && rule.offset < range.end)
505                {
506                    return Err(Denial::HardDenied);
507                }
508                Ok(())
509            }
510            AccessClass::Protocol => {
511                let desc = self.resources.get(&rule.resource).ok_or(Denial::UnknownResource)?;
512                if desc.kind == ResourceKind::Mmio {
513                    return Err(Denial::UnsupportedAccessClass);
514                }
515                let ceiling =
516                    self.ceiling.get(&rule.resource).ok_or(Denial::NotPermittedByCeiling)?;
517                if ceiling.protocol.is_none() {
518                    return Err(Denial::NotPermittedByCeiling);
519                }
520                Ok(())
521            }
522            AccessClass::Interrupt => {
523                let desc = self.resources.get(&rule.resource).ok_or(Denial::UnknownResource)?;
524                if desc.kind != ResourceKind::Interrupt {
525                    return Err(Denial::UnsupportedAccessClass);
526                }
527                let ceiling =
528                    self.ceiling.get(&rule.resource).ok_or(Denial::NotPermittedByCeiling)?;
529                if !ceiling.allow_interrupt {
530                    return Err(Denial::NotPermittedByCeiling);
531                }
532                Ok(())
533            }
534        }
535    }
536
537    /// Immediately-before-access validation for an interrupt observation.
538    pub fn check_interrupt(&self, resource: ResourceId) -> Result<(), Denial> {
539        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
540        if desc.kind != ResourceKind::Interrupt {
541            return Err(Denial::UnknownResource);
542        }
543        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
544        if !ceiling.allow_interrupt {
545            return Err(Denial::NotPermittedByCeiling);
546        }
547        let rule = AccessRule { resource, offset: 0, width: 0, class: AccessClass::Interrupt };
548        if !self.allowlist.contains(&rule) {
549            return Err(Denial::NotInAllowlist);
550        }
551        Ok(())
552    }
553
554    /// Immediately-before-access validation for a GPIO read.
555    pub fn check_gpio_read(&self, resource: ResourceId) -> Result<(), Denial> {
556        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
557        if desc.kind != ResourceKind::Gpio {
558            return Err(Denial::UnknownResource);
559        }
560        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
561        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
562        if !proto.allowed_methods.contains("read") {
563            return Err(Denial::UnsupportedMethod);
564        }
565        if !self.is_protocol_allowed(resource, false) {
566            return Err(Denial::NotInAllowlist);
567        }
568        Ok(())
569    }
570
571    /// Immediately-before-access validation for a GPIO write. Requires mutation lease.
572    pub fn check_gpio_write(&self, resource: ResourceId) -> Result<(), Denial> {
573        if self.mode != SessionMode::Mutating {
574            return Err(Denial::ReadOnlySession);
575        }
576        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
577        if desc.kind != ResourceKind::Gpio {
578            return Err(Denial::UnknownResource);
579        }
580        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
581        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
582        if !proto.allow_mutating || !proto.allowed_methods.contains("write") {
583            return Err(Denial::WriteNotPermitted);
584        }
585        if !self.is_protocol_allowed(resource, true) {
586            return Err(Denial::NotInAllowlist);
587        }
588        Ok(())
589    }
590
591    /// Immediately-before-access validation for an I2C transfer.
592    pub fn check_i2c_transfer(
593        &self,
594        resource: ResourceId,
595        write_len: usize,
596        read_len: usize,
597    ) -> Result<(), Denial> {
598        let is_mutating = write_len > 0;
599        if is_mutating && self.mode != SessionMode::Mutating {
600            return Err(Denial::ReadOnlySession);
601        }
602        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
603        if desc.kind != ResourceKind::I2c {
604            return Err(Denial::UnknownResource);
605        }
606        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
607        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
608        if !proto.allowed_methods.contains("transfer") {
609            return Err(Denial::UnsupportedMethod);
610        }
611        if is_mutating && !proto.allow_mutating {
612            return Err(Denial::WriteNotPermitted);
613        }
614        let total_len = write_len.saturating_add(read_len);
615        if total_len as u32 > proto.max_transfer_size {
616            return Err(Denial::TransferTooLarge);
617        }
618        if !self.is_protocol_allowed(resource, is_mutating) {
619            return Err(Denial::NotInAllowlist);
620        }
621        Ok(())
622    }
623
624    /// Immediately-before-access validation for a SPI transmit. Requires mutation lease.
625    pub fn check_spi_transmit(&self, resource: ResourceId, tx_len: usize) -> Result<(), Denial> {
626        if self.mode != SessionMode::Mutating {
627            return Err(Denial::ReadOnlySession);
628        }
629        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
630        if desc.kind != ResourceKind::Spi {
631            return Err(Denial::UnknownResource);
632        }
633        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
634        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
635        if !proto.allowed_methods.contains("transmit") {
636            return Err(Denial::UnsupportedMethod);
637        }
638        if !proto.allow_mutating {
639            return Err(Denial::WriteNotPermitted);
640        }
641        if tx_len as u32 > proto.max_transfer_size {
642            return Err(Denial::TransferTooLarge);
643        }
644        if !self.is_protocol_allowed(resource, true) {
645            return Err(Denial::NotInAllowlist);
646        }
647        Ok(())
648    }
649
650    /// Immediately-before-access validation for Clock enable. Requires mutation lease.
651    pub fn check_clock_enable(&self, resource: ResourceId) -> Result<(), Denial> {
652        self.check_clock_op(resource, "enable", true)
653    }
654
655    /// Immediately-before-access validation for Clock disable. Requires mutation lease.
656    pub fn check_clock_disable(&self, resource: ResourceId) -> Result<(), Denial> {
657        self.check_clock_op(resource, "disable", true)
658    }
659
660    /// Immediately-before-access validation for Clock is_enabled query.
661    pub fn check_clock_is_enabled(&self, resource: ResourceId) -> Result<(), Denial> {
662        self.check_clock_op(resource, "is_enabled", false)
663    }
664
665    /// Immediately-before-access validation for Clock set_rate. Requires mutation lease.
666    pub fn check_clock_set_rate(&self, resource: ResourceId) -> Result<(), Denial> {
667        self.check_clock_op(resource, "set_rate", true)
668    }
669
670    /// Immediately-before-access validation for Clock query_rate.
671    pub fn check_clock_query_rate(&self, resource: ResourceId) -> Result<(), Denial> {
672        self.check_clock_op(resource, "query_rate", false)
673    }
674
675    /// Immediately-before-access validation for Clock get_rate.
676    pub fn check_clock_get_rate(&self, resource: ResourceId) -> Result<(), Denial> {
677        self.check_clock_op(resource, "get_rate", false)
678    }
679
680    fn check_clock_op(
681        &self,
682        resource: ResourceId,
683        method: &str,
684        is_mutating: bool,
685    ) -> Result<(), Denial> {
686        if is_mutating && self.mode != SessionMode::Mutating {
687            return Err(Denial::ReadOnlySession);
688        }
689        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
690        if desc.kind != ResourceKind::Clock {
691            return Err(Denial::UnknownResource);
692        }
693        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
694        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
695        if !proto.allowed_methods.contains(method) {
696            return Err(Denial::UnsupportedMethod);
697        }
698        if is_mutating && !proto.allow_mutating {
699            return Err(Denial::WriteNotPermitted);
700        }
701        if !self.is_protocol_allowed(resource, is_mutating) {
702            return Err(Denial::NotInAllowlist);
703        }
704        Ok(())
705    }
706
707    /// Immediately-before-access validation for Reset assert. Requires mutation lease.
708    pub fn check_reset_assert(&self, resource: ResourceId) -> Result<(), Denial> {
709        self.check_reset_op(resource, "assert", true)
710    }
711
712    /// Immediately-before-access validation for Reset deassert. Requires mutation lease.
713    pub fn check_reset_deassert(&self, resource: ResourceId) -> Result<(), Denial> {
714        self.check_reset_op(resource, "deassert", true)
715    }
716
717    /// Immediately-before-access validation for Reset toggle. Requires mutation lease.
718    pub fn check_reset_toggle(&self, resource: ResourceId) -> Result<(), Denial> {
719        self.check_reset_op(resource, "toggle", true)
720    }
721
722    /// Immediately-before-access validation for Reset status query.
723    pub fn check_reset_status(&self, resource: ResourceId) -> Result<(), Denial> {
724        self.check_reset_op(resource, "status", false)
725    }
726
727    fn check_reset_op(
728        &self,
729        resource: ResourceId,
730        method: &str,
731        is_mutating: bool,
732    ) -> Result<(), Denial> {
733        if is_mutating && self.mode != SessionMode::Mutating {
734            return Err(Denial::ReadOnlySession);
735        }
736        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
737        if desc.kind != ResourceKind::Reset {
738            return Err(Denial::UnknownResource);
739        }
740        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
741        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
742        if !proto.allowed_methods.contains(method) {
743            return Err(Denial::UnsupportedMethod);
744        }
745        if is_mutating && !proto.allow_mutating {
746            return Err(Denial::WriteNotPermitted);
747        }
748        if !self.is_protocol_allowed(resource, is_mutating) {
749            return Err(Denial::NotInAllowlist);
750        }
751        Ok(())
752    }
753
754    /// Immediately-before-access validation for Serial read.
755    pub fn check_serial_read(&self, resource: ResourceId) -> Result<(), Denial> {
756        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
757        if desc.kind != ResourceKind::Serial {
758            return Err(Denial::UnknownResource);
759        }
760        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
761        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
762        if !proto.allowed_methods.contains("read") {
763            return Err(Denial::UnsupportedMethod);
764        }
765        if !self.is_protocol_allowed(resource, false) {
766            return Err(Denial::NotInAllowlist);
767        }
768        Ok(())
769    }
770
771    /// Immediately-before-access validation for Serial write. Requires mutation lease.
772    pub fn check_serial_write(&self, resource: ResourceId, tx_len: usize) -> Result<(), Denial> {
773        if self.mode != SessionMode::Mutating {
774            return Err(Denial::ReadOnlySession);
775        }
776        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
777        if desc.kind != ResourceKind::Serial {
778            return Err(Denial::UnknownResource);
779        }
780        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
781        let proto = ceiling.protocol.as_ref().ok_or(Denial::NotPermittedByCeiling)?;
782        if !proto.allowed_methods.contains("write") {
783            return Err(Denial::UnsupportedMethod);
784        }
785        if !proto.allow_mutating {
786            return Err(Denial::WriteNotPermitted);
787        }
788        if tx_len as u32 > proto.max_transfer_size {
789            return Err(Denial::TransferTooLarge);
790        }
791        if !self.is_protocol_allowed(resource, true) {
792            return Err(Denial::NotInAllowlist);
793        }
794        Ok(())
795    }
796
797    fn is_protocol_allowed(&self, resource: ResourceId, is_mutating: bool) -> bool {
798        self.allowlist.iter().any(|r| {
799            r.resource == resource
800                && (r.class == AccessClass::Protocol
801                    || r.class == AccessClass::Sequence
802                    || (is_mutating && r.class == AccessClass::Write)
803                    || (!is_mutating
804                        && (r.class == AccessClass::ReadOnce || r.class == AccessClass::Snapshot)))
805        })
806    }
807
808    /// Structural and ceiling checks shared by rule prevalidation and
809    /// per-access validation for reads.
810    fn validate_read(&self, resource: ResourceId, offset: u64, width: u32) -> Result<(), Denial> {
811        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
812        if width != WIDTH32 {
813            return Err(Denial::UnsupportedWidth);
814        }
815        if offset % u64::from(width) != 0 {
816            return Err(Denial::Misaligned);
817        }
818        let end = offset.checked_add(u64::from(width)).ok_or(Denial::OffsetOverflow)?;
819        if end > desc.logical_size {
820            return Err(Denial::OutOfLogicalBounds);
821        }
822        if end > desc.mapped_size {
823            return Err(Denial::OutOfMappedBounds);
824        }
825        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
826        if ceiling.hard_denied.iter().any(|range| range.start < end && offset < range.end) {
827            return Err(Denial::HardDenied);
828        }
829        if !ceiling.allow_unknown_reads {
830            return Err(Denial::UnknownReadsNotPermitted);
831        }
832        Ok(())
833    }
834
835    /// Structural and ceiling checks for writes.
836    fn validate_write_rule(
837        &self,
838        resource: ResourceId,
839        offset: u64,
840        width: u32,
841    ) -> Result<(), Denial> {
842        let desc = self.resources.get(&resource).ok_or(Denial::UnknownResource)?;
843        if width != WIDTH32 {
844            return Err(Denial::UnsupportedWidth);
845        }
846        if offset % u64::from(width) != 0 {
847            return Err(Denial::Misaligned);
848        }
849        let end = offset.checked_add(u64::from(width)).ok_or(Denial::OffsetOverflow)?;
850        if end > desc.logical_size {
851            return Err(Denial::OutOfLogicalBounds);
852        }
853        if end > desc.mapped_size {
854            return Err(Denial::OutOfMappedBounds);
855        }
856        let ceiling = self.ceiling.get(&resource).ok_or(Denial::NotPermittedByCeiling)?;
857        if ceiling.hard_denied.iter().any(|range| range.start < end && offset < range.end) {
858            return Err(Denial::HardDenied);
859        }
860        let reg = ceiling.get_writable_register(offset).ok_or(Denial::WriteNotPermitted)?;
861        if reg.width != width {
862            return Err(Denial::UnsupportedWidth);
863        }
864        Ok(())
865    }
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871
872    const CTRL: ResourceId = 1;
873    const LOCKED: ResourceId = 2;
874    const NO_CEILING: ResourceId = 3;
875    const SHORT_MAP: ResourceId = 4;
876
877    fn resources() -> BTreeMap<ResourceId, MmioResource> {
878        BTreeMap::from([
879            (
880                CTRL,
881                MmioResource {
882                    name: "ctrl".to_string(),
883                    kind: ResourceKind::Mmio,
884                    logical_size: 0x100,
885                    mapped_size: 0x1000,
886                },
887            ),
888            (
889                LOCKED,
890                MmioResource {
891                    name: "locked".to_string(),
892                    kind: ResourceKind::Mmio,
893                    logical_size: 0x100,
894                    mapped_size: 0x100,
895                },
896            ),
897            (
898                NO_CEILING,
899                MmioResource {
900                    name: "no-ceiling".to_string(),
901                    kind: ResourceKind::Mmio,
902                    logical_size: 0x100,
903                    mapped_size: 0x100,
904                },
905            ),
906            (
907                SHORT_MAP,
908                MmioResource {
909                    name: "short-map".to_string(),
910                    kind: ResourceKind::Mmio,
911                    logical_size: 0x2000,
912                    mapped_size: 0x10,
913                },
914            ),
915        ])
916    }
917
918    fn ceiling() -> BTreeMap<ResourceId, ResourceCeiling> {
919        BTreeMap::from([
920            (
921                CTRL,
922                ResourceCeiling {
923                    hard_denied: vec![0x40..0x44],
924                    allow_unknown_reads: true,
925                    allow_poll: true,
926                    writable_registers: vec![
927                        WritableRegister {
928                            offset: 0x20,
929                            width: WIDTH32,
930                            allow_mask: 0x0000FFFF,
931                            allow_rmw: true,
932                            require_precondition: false,
933                            precondition_mask: 0,
934                            readback: false,
935                        },
936                        WritableRegister {
937                            offset: 0x24,
938                            width: WIDTH32,
939                            allow_mask: 0xFFFFFFFF,
940                            allow_rmw: true,
941                            require_precondition: true,
942                            precondition_mask: 0x00000001,
943                            readback: true,
944                        },
945                    ],
946                    protocol: None,
947                    allow_interrupt: false,
948                },
949            ),
950            (
951                LOCKED,
952                ResourceCeiling {
953                    hard_denied: vec![],
954                    allow_unknown_reads: false,
955                    allow_poll: false,
956                    writable_registers: vec![],
957                    protocol: None,
958                    allow_interrupt: false,
959                },
960            ),
961            (
962                SHORT_MAP,
963                ResourceCeiling {
964                    hard_denied: vec![],
965                    allow_unknown_reads: true,
966                    allow_poll: false,
967                    writable_registers: vec![],
968                    protocol: None,
969                    allow_interrupt: false,
970                },
971            ),
972        ])
973    }
974
975    fn read_rule(resource: ResourceId, offset: u64) -> AccessRule {
976        AccessRule { resource, offset, width: WIDTH32, class: AccessClass::ReadOnce }
977    }
978
979    fn policy(rules: &[AccessRule]) -> AccessPolicy {
980        AccessPolicy::new(SessionMode::ReadOnly, resources(), ceiling(), rules.iter().copied())
981            .unwrap()
982    }
983
984    #[test]
985    fn exact_read_allowed() {
986        let policy = policy(&[read_rule(CTRL, 0x3c)]);
987        assert_eq!(policy.check_read32(CTRL, 0x3c, AccessClass::ReadOnce), Ok(()));
988    }
989
990    #[test]
991    fn nearby_offset_not_authorized() {
992        let policy = policy(&[read_rule(CTRL, 0x3c)]);
993        assert_eq!(
994            policy.check_read32(CTRL, 0x38, AccessClass::ReadOnce),
995            Err(Denial::NotInAllowlist)
996        );
997    }
998
999    #[test]
1000    fn access_classes_are_distinct() {
1001        let policy = policy(&[read_rule(CTRL, 0x3c)]);
1002        assert_eq!(
1003            policy.check_read32(CTRL, 0x3c, AccessClass::Snapshot),
1004            Err(Denial::NotInAllowlist)
1005        );
1006        assert_eq!(
1007            policy.check_read32(CTRL, 0x3c, AccessClass::Poll),
1008            Err(Denial::UnsupportedAccessClass)
1009        );
1010        assert_eq!(
1011            policy.check_read32(CTRL, 0x3c, AccessClass::Write),
1012            Err(Denial::UnsupportedAccessClass)
1013        );
1014    }
1015
1016    #[test]
1017    fn unknown_resource_denied() {
1018        let policy = policy(&[]);
1019        assert_eq!(
1020            policy.check_read32(99, 0x0, AccessClass::ReadOnce),
1021            Err(Denial::UnknownResource)
1022        );
1023    }
1024
1025    #[test]
1026    fn misaligned_denied() {
1027        let policy = policy(&[read_rule(CTRL, 0x3c)]);
1028        assert_eq!(policy.check_read32(CTRL, 0x3e, AccessClass::ReadOnce), Err(Denial::Misaligned));
1029    }
1030
1031    #[test]
1032    fn offset_overflow_denied() {
1033        let policy = policy(&[]);
1034        // u64::MAX - 3 is 4-byte aligned, so this reaches the overflow
1035        // check rather than the alignment check.
1036        assert_eq!(
1037            policy.check_read32(CTRL, u64::MAX - 3, AccessClass::ReadOnce),
1038            Err(Denial::OffsetOverflow)
1039        );
1040    }
1041
1042    #[test]
1043    fn logical_bounds_enforced() {
1044        let policy = policy(&[read_rule(CTRL, 0xfc)]);
1045        assert_eq!(policy.check_read32(CTRL, 0xfc, AccessClass::ReadOnce), Ok(()));
1046        assert_eq!(
1047            policy.check_read32(CTRL, 0x100, AccessClass::ReadOnce),
1048            Err(Denial::OutOfLogicalBounds)
1049        );
1050    }
1051
1052    #[test]
1053    fn mapped_bounds_enforced() {
1054        let policy = policy(&[read_rule(SHORT_MAP, 0xc)]);
1055        assert_eq!(policy.check_read32(SHORT_MAP, 0xc, AccessClass::ReadOnce), Ok(()));
1056        assert_eq!(
1057            policy.check_read32(SHORT_MAP, 0x10, AccessClass::ReadOnce),
1058            Err(Denial::OutOfMappedBounds)
1059        );
1060    }
1061
1062    #[test]
1063    fn hard_denied_wins_over_allowlist() {
1064        // The rule cannot even enter the allowlist.
1065        let denied = AccessPolicy::new(
1066            SessionMode::ReadOnly,
1067            resources(),
1068            ceiling(),
1069            [read_rule(CTRL, 0x40)],
1070        );
1071        assert_eq!(denied.unwrap_err(), (read_rule(CTRL, 0x40), Denial::HardDenied));
1072        // And a direct check is denied before the allowlist is consulted.
1073        let policy = policy(&[]);
1074        assert_eq!(policy.check_read32(CTRL, 0x40, AccessClass::ReadOnce), Err(Denial::HardDenied));
1075    }
1076
1077    #[test]
1078    fn read_adjacent_to_hard_denied_range_allowed() {
1079        let policy = policy(&[read_rule(CTRL, 0x44)]);
1080        assert_eq!(policy.check_read32(CTRL, 0x44, AccessClass::ReadOnce), Ok(()));
1081    }
1082
1083    #[test]
1084    fn unknown_reads_disabled_by_ceiling() {
1085        let denied = AccessPolicy::new(
1086            SessionMode::ReadOnly,
1087            resources(),
1088            ceiling(),
1089            [read_rule(LOCKED, 0x0)],
1090        );
1091        assert_eq!(denied.unwrap_err(), (read_rule(LOCKED, 0x0), Denial::UnknownReadsNotPermitted));
1092    }
1093
1094    #[test]
1095    fn missing_ceiling_entry_permits_nothing() {
1096        let policy = policy(&[]);
1097        assert_eq!(
1098            policy.check_read32(NO_CEILING, 0x0, AccessClass::ReadOnce),
1099            Err(Denial::NotPermittedByCeiling)
1100        );
1101    }
1102
1103    #[test]
1104    fn allowlist_prevalidation_checks_poll_permission() {
1105        // CTRL has allow_poll: true
1106        let rule_poll_allowed =
1107            AccessRule { resource: CTRL, offset: 0x0, width: WIDTH32, class: AccessClass::Poll };
1108        assert!(
1109            AccessPolicy::new(SessionMode::ReadOnly, resources(), ceiling(), [rule_poll_allowed])
1110                .is_ok()
1111        );
1112
1113        // SHORT_MAP has allow_poll: false
1114        let rule_poll_denied = AccessRule {
1115            resource: SHORT_MAP,
1116            offset: 0x0,
1117            width: WIDTH32,
1118            class: AccessClass::Poll,
1119        };
1120        let denied =
1121            AccessPolicy::new(SessionMode::ReadOnly, resources(), ceiling(), [rule_poll_denied]);
1122        assert_eq!(denied.unwrap_err(), (rule_poll_denied, Denial::PollNotPermitted));
1123    }
1124
1125    #[test]
1126    fn allowlist_prevalidation_rejects_write_in_read_only_session() {
1127        let rule =
1128            AccessRule { resource: CTRL, offset: 0x20, width: WIDTH32, class: AccessClass::Write };
1129        let denied = AccessPolicy::new(SessionMode::ReadOnly, resources(), ceiling(), [rule]);
1130        assert_eq!(denied.unwrap_err(), (rule, Denial::ReadOnlySession));
1131    }
1132
1133    #[test]
1134    fn allowlist_prevalidation_accepts_valid_write_in_mutating_session() {
1135        let rule =
1136            AccessRule { resource: CTRL, offset: 0x20, width: WIDTH32, class: AccessClass::Write };
1137        let policy = AccessPolicy::new(SessionMode::Mutating, resources(), ceiling(), [rule]);
1138        assert!(policy.is_ok());
1139    }
1140
1141    #[test]
1142    fn write_checks_enforce_session_mode_mask_and_preconditions() {
1143        let write_rule =
1144            AccessRule { resource: CTRL, offset: 0x20, width: WIDTH32, class: AccessClass::Write };
1145        let write_rule_pre =
1146            AccessRule { resource: CTRL, offset: 0x24, width: WIDTH32, class: AccessClass::Write };
1147        let policy = AccessPolicy::new(
1148            SessionMode::Mutating,
1149            resources(),
1150            ceiling(),
1151            [write_rule, write_rule_pre],
1152        )
1153        .unwrap();
1154
1155        // Valid write on offset 0x20 (allow_mask 0x0000FFFF)
1156        let reg = policy.check_write32(CTRL, 0x20, 0x000000FF, None).unwrap();
1157        assert_eq!(reg.offset, 0x20);
1158
1159        // Disallowed bit in write_mask
1160        assert_eq!(
1161            policy.check_write32(CTRL, 0x20, 0x00010000, None),
1162            Err(Denial::WriteNotPermitted)
1163        );
1164
1165        // Zero mask rejected
1166        assert_eq!(policy.check_write32(CTRL, 0x20, 0, None), Err(Denial::WriteNotPermitted));
1167
1168        // Offset 0x24 requires precondition
1169        assert_eq!(
1170            policy.check_write32(CTRL, 0x24, 0xFFFFFFFF, None),
1171            Err(Denial::MissingPrecondition)
1172        );
1173
1174        // Invalid precondition mask (precondition_mask is 0x1)
1175        let bad_pre = WritePrecondition { expected: 1, mask: 0x2 };
1176        assert_eq!(
1177            policy.check_write32(CTRL, 0x24, 0xFFFFFFFF, Some(&bad_pre)),
1178            Err(Denial::WriteNotPermitted)
1179        );
1180
1181        // Valid precondition
1182        let good_pre = WritePrecondition { expected: 1, mask: 0x1 };
1183        assert!(policy.check_write32(CTRL, 0x24, 0xFFFFFFFF, Some(&good_pre)).is_ok());
1184    }
1185
1186    #[test]
1187    fn allowlist_prevalidation_rejects_bad_width() {
1188        let rule =
1189            AccessRule { resource: CTRL, offset: 0x0, width: 8, class: AccessClass::ReadOnce };
1190        let denied = AccessPolicy::new(SessionMode::ReadOnly, resources(), ceiling(), [rule]);
1191        assert_eq!(denied.unwrap_err(), (rule, Denial::UnsupportedWidth));
1192    }
1193
1194    #[test]
1195    fn allowlist_prevalidation_rejects_out_of_bounds() {
1196        let rule = read_rule(CTRL, 0x200);
1197        let denied = AccessPolicy::new(SessionMode::ReadOnly, resources(), ceiling(), [rule]);
1198        assert_eq!(denied.unwrap_err(), (rule, Denial::OutOfLogicalBounds));
1199    }
1200
1201    #[test]
1202    fn protocol_policy_gpio_and_i2c_and_spi() {
1203        const GPIO_ID: ResourceId = 10;
1204        const I2C_ID: ResourceId = 11;
1205        const SPI_ID: ResourceId = 12;
1206
1207        let mut res = BTreeMap::new();
1208        res.insert(GPIO_ID, MmioResource::gpio("gpio-a"));
1209        res.insert(I2C_ID, MmioResource::i2c("i2c-sensor"));
1210        res.insert(SPI_ID, MmioResource::spi("spi-flash"));
1211
1212        let mut ceil = BTreeMap::new();
1213        let mut gpio_ceil = ResourceCeiling::default();
1214        gpio_ceil.protocol = Some(ProtocolCeiling::default_for(ResourceKind::Gpio));
1215        ceil.insert(GPIO_ID, gpio_ceil);
1216
1217        let mut i2c_ceil = ResourceCeiling::default();
1218        i2c_ceil.protocol = Some(ProtocolCeiling::default_for(ResourceKind::I2c));
1219        ceil.insert(I2C_ID, i2c_ceil);
1220
1221        let mut spi_ceil = ResourceCeiling::default();
1222        spi_ceil.protocol = Some(ProtocolCeiling::default_for(ResourceKind::Spi));
1223        ceil.insert(SPI_ID, spi_ceil);
1224
1225        let proto_rule =
1226            |r| AccessRule { resource: r, offset: 0, width: 0, class: AccessClass::Protocol };
1227
1228        // Read-only session
1229        let ro_policy = AccessPolicy::new(
1230            SessionMode::ReadOnly,
1231            res.clone(),
1232            ceil.clone(),
1233            [proto_rule(GPIO_ID), proto_rule(I2C_ID), proto_rule(SPI_ID)],
1234        )
1235        .expect("policy creation ok");
1236
1237        // GPIO read ok in read-only
1238        assert!(ro_policy.check_gpio_read(GPIO_ID).is_ok());
1239        // GPIO write rejected in read-only session
1240        assert_eq!(ro_policy.check_gpio_write(GPIO_ID), Err(Denial::ReadOnlySession));
1241
1242        // I2C read-only transfer (write_len=0, read_len=4) ok
1243        assert!(ro_policy.check_i2c_transfer(I2C_ID, 0, 4).is_ok());
1244        // I2C write transfer (write_len=2) rejected in read-only
1245        assert_eq!(ro_policy.check_i2c_transfer(I2C_ID, 2, 4), Err(Denial::ReadOnlySession));
1246
1247        // SPI transmit (mutating) rejected in read-only
1248        assert_eq!(ro_policy.check_spi_transmit(SPI_ID, 4), Err(Denial::ReadOnlySession));
1249
1250        // Mutating session
1251        let mut_policy = AccessPolicy::new(
1252            SessionMode::Mutating,
1253            res,
1254            ceil,
1255            [proto_rule(GPIO_ID), proto_rule(I2C_ID), proto_rule(SPI_ID)],
1256        )
1257        .expect("policy creation ok");
1258
1259        // Now mutating operations succeed
1260        assert!(mut_policy.check_gpio_write(GPIO_ID).is_ok());
1261        assert!(mut_policy.check_i2c_transfer(I2C_ID, 2, 4).is_ok());
1262        assert!(mut_policy.check_spi_transmit(SPI_ID, 4).is_ok());
1263
1264        // Transfer exceeding max transfer size rejected
1265        assert_eq!(
1266            mut_policy.check_i2c_transfer(I2C_ID, 5000, 5000),
1267            Err(Denial::TransferTooLarge)
1268        );
1269        assert_eq!(mut_policy.check_spi_transmit(SPI_ID, 10000), Err(Denial::TransferTooLarge));
1270    }
1271}