Skip to main content

lab_proxy_core/
audit_ring.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//! Bounded target-side audit ring.
6//!
7//! Sequence numbers increase monotonically for the life of the proxy
8//! instance. Wrap overwrites the oldest entries but never reuses or resets
9//! sequence numbers, so a host reading from a stale cursor can detect the
10//! gap. The ring is in-memory: undrained entries are lost if the driver
11//! host crashes, which is why hosts drain incrementally during mutating
12//! plans.
13
14use crate::access_policy::{Denial, ResourceId};
15use std::collections::VecDeque;
16use std::fmt;
17
18/// The policy outcome recorded for an attempted operation.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum Decision {
21    /// The operation passed every policy layer.
22    Allowed,
23    /// The operation was rejected without touching hardware.
24    Denied(Denial),
25}
26
27/// Execution outcome recorded alongside the policy decision.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum OpStatus {
30    /// The operation completed.
31    Ok,
32    /// Policy rejected the operation; hardware was not touched.
33    Rejected,
34    /// The backend failed after policy allowed the operation.
35    BackendFault,
36}
37
38/// One audit record. Callers supply bounded, normalized values; the ring
39/// never stores pointers, physical addresses, handles, or unbounded caller
40/// strings.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct AuditRecord {
43    /// Session the operation belonged to; `None` for lifecycle events.
44    pub session: Option<u64>,
45    /// Resource the operation targeted, when applicable.
46    pub resource: Option<ResourceId>,
47    /// Normalized operation name from a fixed vocabulary, e.g. `"read32"`
48    /// or `"driver_start"`.
49    pub operation: &'static str,
50    /// Byte offset for MMIO operations.
51    pub offset: Option<u64>,
52    /// Policy decision.
53    pub decision: Decision,
54    /// Execution outcome.
55    pub status: OpStatus,
56    /// Value read or written, when applicable.
57    pub value: Option<u32>,
58    /// Target monotonic timestamp in nanoseconds, supplied by the driver
59    /// layer so this crate stays host-testable.
60    pub timestamp_ns: i64,
61    /// Host run identifier, recorded on session-open records (success and
62    /// rejection) where the session-to-run association is established.
63    pub run_id: Option<String>,
64    /// Index of the item within a multi-item operation (for example a
65    /// snapshot), when applicable. Plan-level operation indexes are host
66    /// knowledge; the target records only what it can observe.
67    pub item_index: Option<u32>,
68}
69
70impl AuditRecord {
71    /// A session-independent lifecycle event such as driver start or stop.
72    pub fn lifecycle(operation: &'static str, timestamp_ns: i64) -> Self {
73        Self {
74            session: None,
75            resource: None,
76            operation,
77            offset: None,
78            decision: Decision::Allowed,
79            status: OpStatus::Ok,
80            value: None,
81            timestamp_ns,
82            run_id: None,
83            item_index: None,
84        }
85    }
86}
87
88/// An audit record together with its assigned sequence number.
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct AuditEntry {
91    /// Monotonically increasing sequence number.
92    pub seq: u64,
93    /// The record.
94    pub record: AuditRecord,
95}
96
97/// One bounded page returned from [`AuditRing::read`].
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct AuditPage {
100    /// Entries with `seq >= cursor`, oldest first, at most `limit` of them.
101    pub entries: Vec<AuditEntry>,
102    /// Oldest sequence still retained, or `None` if the ring is empty. A
103    /// cursor older than this indicates a gap: those entries were
104    /// overwritten before they were drained.
105    pub oldest_retained: Option<u64>,
106    /// Cursor to pass to the next read.
107    pub next_cursor: u64,
108}
109
110/// Callback invoked with each record and its assigned sequence number as
111/// the record is appended.
112///
113/// Observers run synchronously on the appending thread, typically while
114/// the caller holds the proxy state lock, so they must be cheap and must
115/// not call back into the ring. The driver layer uses this to mirror audit
116/// records into system tracing while keeping this crate free of Fuchsia
117/// dependencies.
118pub type AuditObserver = Box<dyn Fn(u64, &AuditRecord) + Send + Sync>;
119
120/// Fixed-capacity audit ring.
121pub struct AuditRing {
122    capacity: usize,
123    next_seq: u64,
124    entries: VecDeque<AuditEntry>,
125    observer: Option<AuditObserver>,
126}
127
128impl fmt::Debug for AuditRing {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.debug_struct("AuditRing")
131            .field("capacity", &self.capacity)
132            .field("next_seq", &self.next_seq)
133            .field("entries", &self.entries)
134            .field("has_observer", &self.observer.is_some())
135            .finish()
136    }
137}
138
139impl AuditRing {
140    /// Creates a ring holding at most `capacity` entries.
141    ///
142    /// # Panics
143    ///
144    /// Panics if `capacity` is zero.
145    pub fn new(capacity: usize) -> Self {
146        assert!(capacity > 0, "audit ring capacity must be nonzero");
147        Self { capacity, next_seq: 0, entries: VecDeque::with_capacity(capacity), observer: None }
148    }
149
150    /// Installs `observer`, replacing any previous one. It sees every
151    /// record appended from this point on, including records that are
152    /// later overwritten before a host drains them.
153    pub fn set_observer(&mut self, observer: AuditObserver) {
154        self.observer = Some(observer);
155    }
156
157    /// Whether an observer is installed.
158    pub fn has_observer(&self) -> bool {
159        self.observer.is_some()
160    }
161
162    /// Appends a record, overwriting the oldest entry when full, and
163    /// returns the assigned sequence number.
164    pub fn append(&mut self, record: AuditRecord) -> u64 {
165        let seq = self.next_seq;
166        self.next_seq += 1;
167        if let Some(observer) = &self.observer {
168            observer(seq, &record);
169        }
170        if self.entries.len() == self.capacity {
171            self.entries.pop_front();
172        }
173        self.entries.push_back(AuditEntry { seq, record });
174        seq
175    }
176
177    /// The oldest sequence number still retained.
178    pub fn oldest_retained(&self) -> Option<u64> {
179        self.entries.front().map(|entry| entry.seq)
180    }
181
182    /// The sequence number the next appended record will receive.
183    pub fn next_sequence(&self) -> u64 {
184        self.next_seq
185    }
186
187    /// The fixed entry capacity, for capability reporting.
188    pub fn capacity(&self) -> usize {
189        self.capacity
190    }
191
192    /// Reads at most `limit` entries with `seq >= cursor`, oldest first.
193    pub fn read(&self, cursor: u64, limit: usize) -> AuditPage {
194        let entries: Vec<AuditEntry> =
195            self.entries.iter().filter(|entry| entry.seq >= cursor).take(limit).cloned().collect();
196        let next_cursor = entries.last().map_or(cursor, |entry| entry.seq + 1);
197        AuditPage { entries, oldest_retained: self.oldest_retained(), next_cursor }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn record(op: &'static str) -> AuditRecord {
206        AuditRecord::lifecycle(op, 0)
207    }
208
209    #[test]
210    fn sequences_are_monotonic() {
211        let mut ring = AuditRing::new(4);
212        for expected in 0..10 {
213            assert_eq!(ring.append(record("op")), expected);
214        }
215        assert_eq!(ring.next_sequence(), 10);
216    }
217
218    #[test]
219    fn wrap_discards_oldest_but_keeps_sequence() {
220        let mut ring = AuditRing::new(3);
221        for _ in 0..5 {
222            ring.append(record("op"));
223        }
224        // Entries 0 and 1 were overwritten; 2, 3, 4 remain.
225        assert_eq!(ring.oldest_retained(), Some(2));
226        let page = ring.read(0, 10);
227        let seqs: Vec<u64> = page.entries.iter().map(|entry| entry.seq).collect();
228        assert_eq!(seqs, vec![2, 3, 4]);
229        // A cursor of 0 against oldest_retained of 2 is a detectable gap.
230        assert_eq!(page.oldest_retained, Some(2));
231    }
232
233    #[test]
234    fn pagination_via_next_cursor() {
235        let mut ring = AuditRing::new(10);
236        for _ in 0..6 {
237            ring.append(record("op"));
238        }
239        let first = ring.read(0, 4);
240        assert_eq!(first.entries.len(), 4);
241        assert_eq!(first.next_cursor, 4);
242        let second = ring.read(first.next_cursor, 4);
243        let seqs: Vec<u64> = second.entries.iter().map(|entry| entry.seq).collect();
244        assert_eq!(seqs, vec![4, 5]);
245        assert_eq!(second.next_cursor, 6);
246        // Reading past the end returns nothing and does not advance.
247        let done = ring.read(second.next_cursor, 4);
248        assert!(done.entries.is_empty());
249        assert_eq!(done.next_cursor, 6);
250    }
251
252    #[test]
253    fn empty_ring_reads_empty() {
254        let ring = AuditRing::new(4);
255        let page = ring.read(0, 4);
256        assert!(page.entries.is_empty());
257        assert_eq!(page.oldest_retained, None);
258        assert_eq!(page.next_cursor, 0);
259    }
260
261    #[test]
262    fn observer_sees_every_append_with_its_sequence() {
263        use std::sync::{Arc, Mutex};
264
265        let seen = Arc::new(Mutex::new(Vec::new()));
266        let mut ring = AuditRing::new(2);
267        ring.append(record("before_observer"));
268        let seen_by_observer = Arc::clone(&seen);
269        ring.set_observer(Box::new(move |seq, record| {
270            seen_by_observer.lock().unwrap().push((seq, record.operation));
271        }));
272        for op in ["a", "b", "c"] {
273            ring.append(record(op));
274        }
275        // The observer sees records appended after installation, including
276        // "a", which wrap overwrote before any host could drain it.
277        assert_eq!(*seen.lock().unwrap(), vec![(1, "a"), (2, "b"), (3, "c")]);
278        let page = ring.read(0, 10);
279        let ops: Vec<&str> = page.entries.iter().map(|entry| entry.record.operation).collect();
280        assert_eq!(ops, vec!["b", "c"]);
281    }
282
283    #[test]
284    fn debug_reports_observer_presence() {
285        let mut ring = AuditRing::new(1);
286        assert!(!ring.has_observer());
287        ring.set_observer(Box::new(|_, _| {}));
288        assert!(ring.has_observer());
289        assert!(format!("{ring:?}").contains("has_observer: true"));
290    }
291
292    #[test]
293    #[should_panic(expected = "capacity must be nonzero")]
294    fn zero_capacity_panics() {
295        let _ = AuditRing::new(0);
296    }
297}