Skip to main content

driver_lab_rust/
embedded.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//! Embedded in-situ driver library (`driver_lab_rust::embedded`) for DFv2
6//! Rust drivers (Spec Phase 2 Sections 2 & 3.1).
7//!
8//! Enables an active, bound driver to register its mapped MMIO banks (via
9//! VMO duplication), protocol resources, and interrupt channels with an
10//! embedded `fuchsia.driver.lab` server published on the driver's outgoing
11//! [`ServiceFs`].
12
13use crate::fuchsia_backends::{
14    FuchsiaClock, FuchsiaGpio, FuchsiaI2c, FuchsiaReset, FuchsiaSerial, FuchsiaSpi, LiveBackend,
15};
16use crate::platform_provider::MappedMmio;
17use crate::server::{self, ProxyState, SharedState, SharedStateData, ZxClock};
18use crate::trace::AuditTracer;
19use fidl_fuchsia_driver_lab as flab;
20use fidl_fuchsia_hardware_clock as fclock;
21use fidl_fuchsia_hardware_gpio as fgpio;
22use fidl_fuchsia_hardware_i2c as fi2c;
23use fidl_fuchsia_hardware_reset as freset;
24use fidl_fuchsia_hardware_serial as fserial;
25use fidl_fuchsia_hardware_spi as fspi;
26use fuchsia_async as fasync;
27#[cfg(feature = "fdf_driver")]
28use fuchsia_component::server::{ServiceFs, ServiceObj, ServiceObjLocal};
29use lab_proxy_core::access_policy::{
30    ProtocolCeiling, ResourceCeiling, ResourceId, ResourceKind, WritableRegister,
31};
32use lab_proxy_core::audit_ring::{AuditRecord, AuditRing, Decision, OpStatus};
33use lab_proxy_core::config::{AccessLimitEnforcer, ProxyConfig};
34use lab_proxy_core::executor::{ExecLimits, Executor};
35use lab_proxy_core::interrupt::{InterruptEvent, InterruptManager};
36use lab_proxy_core::provider::{ProvidedResources, ProviderError};
37use lab_proxy_core::session::{ProxyIdentity, QuiesceHook, SessionManager};
38pub use lab_proxy_core::state_bank::{StateBank, StateEntry, StateSlotHandle};
39use lab_proxy_core::target_policy::TargetPolicyManifest;
40use std::collections::BTreeMap;
41use std::ops::Range;
42use std::sync::atomic::{AtomicBool, Ordering};
43use std::sync::{Arc, Mutex};
44
45fn now_ns() -> i64 {
46    zx::MonotonicInstant::get().into_nanos()
47}
48
49/// Builder for configuring and instantiating an [`EmbeddedLabServer`] inside
50/// an existing DFv2 driver.
51pub struct DriverLabBuilder {
52    bundle: ProvidedResources<LiveBackend>,
53    next_id: ResourceId,
54    allow_mutating_sessions: bool,
55    config: ProxyConfig,
56    quiesce_hook: Option<QuiesceHook>,
57}
58
59impl DriverLabBuilder {
60    /// Creates a new builder for the given stable node/driver identity.
61    pub fn new(node_identity: impl Into<String>) -> Self {
62        let mut config = ProxyConfig::default();
63        config.enabled = cfg!(any(feature = "driver_lab_enabled", test));
64        Self {
65            bundle: ProvidedResources::embedded(&node_identity.into()),
66            next_id: 0,
67            allow_mutating_sessions: true,
68            config,
69            quiesce_hook: None,
70        }
71    }
72
73    /// Creates a new builder extracting the node identity from `DriverContext`.
74    #[cfg(feature = "fdf_driver")]
75    pub fn from_context(context: &fdf_component::DriverContext) -> Self {
76        let identity = context.node_name().unwrap_or("driver-lab.embedded");
77        Self::new(identity)
78    }
79
80    /// Explicitly enables or disables the embedded debug server (in release
81    /// builds where `enable_driver_lab = false`, this defaults to `false` and
82    /// omits outgoing service handlers).
83    pub fn with_enabled(mut self, enabled: bool) -> Self {
84        self.config.enabled = enabled;
85        self
86    }
87
88    /// Sets whether mutating sessions are permitted on this embedded server.
89    pub fn with_allow_mutating_sessions(mut self, allow: bool) -> Self {
90        self.allow_mutating_sessions = allow;
91        self
92    }
93
94    /// Overrides the proxy configuration bounds (narrowing against baseline).
95    pub fn with_config(mut self, config: ProxyConfig) -> Self {
96        if let Ok(narrowed) = ProxyConfig::default().narrow_with(&config) {
97            self.config = narrowed;
98        }
99        self
100    }
101
102    /// Registers a synchronous quiesce callback (`quiesce_hook(bool paused)`)
103    /// invoked with `true` when a mutating session acquires the mutation lease
104    /// and `false` when the session closes or the driver stops (Spec Phase 2
105    /// Section 2.2).
106    pub fn with_quiesce_hook<F>(&mut self, hook: F) -> &mut Self
107    where
108        F: Fn(bool) + Send + Sync + 'static,
109    {
110        self.quiesce_hook = Some(Arc::new(hook));
111        self
112    }
113
114    /// Ergonomic helper to register a driver MMIO VMO by byte size.
115    pub fn with_mmio(
116        &mut self,
117        name: impl Into<String>,
118        vmo: &zx::Vmo,
119        offset: usize,
120        size: usize,
121    ) -> Result<ResourceId, zx::Status> {
122        self.add_mmio_vmo(name, vmo, offset, size as u64)
123    }
124
125    /// Configures 32-bit writable register offsets on a registered MMIO resource.
126    pub fn with_writable_registers(&mut self, id: ResourceId, offsets: Vec<u64>) -> &mut Self {
127        if let Some(ceiling) = self.bundle.ceiling.get_mut(&id) {
128            ceiling.writable_registers = offsets
129                .into_iter()
130                .map(|offset| lab_proxy_core::access_policy::WritableRegister {
131                    offset,
132                    width: 4,
133                    allow_mask: 0xFFFF_FFFF,
134                    allow_rmw: true,
135                    require_precondition: false,
136                    precondition_mask: 0,
137                    readback: true,
138                })
139                .collect();
140        }
141        self
142    }
143
144    /// Configures hard-denied byte offset ranges `(start, end)` on a registered MMIO resource.
145    pub fn with_hard_denied_ranges(
146        &mut self,
147        id: ResourceId,
148        ranges: Vec<(u64, u64)>,
149    ) -> &mut Self {
150        if let Some(ceiling) = self.bundle.ceiling.get_mut(&id) {
151            ceiling.hard_denied = ranges.into_iter().map(|(start, end)| start..end).collect();
152        }
153        self
154    }
155
156    /// Ergonomic alias for [`Self::add_interrupt`].
157    pub fn with_interrupt(&mut self, name: impl Into<String>) -> ResourceId {
158        self.add_interrupt(name)
159    }
160
161    /// Shares an existing driver MMIO VMO with the embedded server using a
162    /// default read/poll-permissive ceiling.
163    ///
164    /// Duplicates `vmo` (`zx::Rights::SAME_RIGHTS`) and creates an independent
165    /// local mapping in the driver host address space.
166    pub fn add_mmio_vmo(
167        &mut self,
168        name: impl Into<String>,
169        vmo: &zx::Vmo,
170        offset: usize,
171        logical_size: u64,
172    ) -> Result<ResourceId, zx::Status> {
173        let ceiling = ResourceCeiling {
174            hard_denied: vec![],
175            allow_unknown_reads: true,
176            allow_poll: true,
177            writable_registers: vec![],
178            protocol: None,
179            allow_interrupt: false,
180        };
181        self.add_mmio_vmo_with_ceiling(name, vmo, offset, logical_size, ceiling)
182    }
183
184    /// Shares an existing driver MMIO VMO with an explicit [`ResourceCeiling`]
185    /// (such as `hard_denied` ranges for clear-on-read/FIFO registers and
186    /// `writable_registers` for permitted mutations).
187    pub fn add_mmio_vmo_with_ceiling(
188        &mut self,
189        name: impl Into<String>,
190        vmo: &zx::Vmo,
191        offset: usize,
192        logical_size: u64,
193        ceiling: ResourceCeiling,
194    ) -> Result<ResourceId, zx::Status> {
195        let map_len = usize::try_from(logical_size).map_err(|_| zx::Status::OUT_OF_RANGE)?;
196        let mmio = MappedMmio::from_vmo(vmo, offset, map_len)?;
197        Ok(self.add_mapped_mmio(name, mmio, logical_size, ceiling))
198    }
199
200    /// Registers a pre-mapped [`MappedMmio`] region with an explicit [`ResourceCeiling`].
201    pub fn add_mapped_mmio(
202        &mut self,
203        name: impl Into<String>,
204        mmio: MappedMmio,
205        logical_size: u64,
206        ceiling: ResourceCeiling,
207    ) -> ResourceId {
208        let id = self.next_id;
209        self.next_id += 1;
210        let mapped_size = mmio.len() as u64;
211        self.bundle.add_mmio(id, name, logical_size, mapped_size, ceiling, LiveBackend::Mmio(mmio));
212        id
213    }
214
215    /// Registers an in-memory [`StateBank`] exposing software state slots,
216    /// runtime knobs, read probes, and diagnostic triggers with a
217    /// [`ResourceCeiling`] populated from `bank.writable_offsets()`.
218    pub fn with_state_bank(&mut self, name: impl Into<String>, bank: StateBank) -> ResourceId {
219        self.add_state_bank(name, bank)
220    }
221
222    /// Registers an in-memory [`StateBank`] using its configured writable
223    /// offsets ceiling.
224    pub fn add_state_bank(&mut self, name: impl Into<String>, bank: StateBank) -> ResourceId {
225        let ceiling = bank.ceiling();
226        self.add_state_bank_with_ceiling(name, bank, ceiling)
227    }
228
229    /// Registers an in-memory [`StateBank`] with an explicit [`ResourceCeiling`].
230    pub fn add_state_bank_with_ceiling(
231        &mut self,
232        name: impl Into<String>,
233        bank: StateBank,
234        ceiling: ResourceCeiling,
235    ) -> ResourceId {
236        let id = self.next_id;
237        self.next_id += 1;
238        let logical_size = bank.logical_size();
239        self.bundle.add_mmio(
240            id,
241            name,
242            logical_size,
243            logical_size,
244            ceiling,
245            LiveBackend::State(bank),
246        );
247        id
248    }
249
250    /// Configures the permitted 32-bit writable registers on an existing MMIO
251    /// resource's target ceiling.
252    pub fn set_writable_registers(
253        &mut self,
254        resource_id: ResourceId,
255        writable_registers: Vec<WritableRegister>,
256    ) {
257        if let Some(ceiling) = self.bundle.ceiling.get_mut(&resource_id) {
258            ceiling.writable_registers = writable_registers;
259        }
260    }
261
262    /// Configures the hard-denied byte offset ranges on an existing MMIO
263    /// resource's target ceiling.
264    pub fn set_hard_denied_ranges(&mut self, resource_id: ResourceId, ranges: Vec<Range<u64>>) {
265        if let Some(ceiling) = self.bundle.ceiling.get_mut(&resource_id) {
266            ceiling.hard_denied = ranges;
267        }
268    }
269
270    /// Registers a GPIO protocol resource.
271    pub fn add_gpio(
272        &mut self,
273        name: impl Into<String>,
274        proxy: fgpio::GpioSynchronousProxy,
275    ) -> ResourceId {
276        let id = self.next_id;
277        self.next_id += 1;
278        self.bundle.add_protocol(
279            id,
280            name,
281            ResourceKind::Gpio,
282            ProtocolCeiling::default_for(ResourceKind::Gpio),
283            LiveBackend::Gpio(FuchsiaGpio::new(proxy)),
284        );
285        id
286    }
287
288    /// Registers an I2C protocol resource.
289    pub fn add_i2c(
290        &mut self,
291        name: impl Into<String>,
292        proxy: fi2c::DeviceSynchronousProxy,
293    ) -> ResourceId {
294        let id = self.next_id;
295        self.next_id += 1;
296        self.bundle.add_protocol(
297            id,
298            name,
299            ResourceKind::I2c,
300            ProtocolCeiling::default_for(ResourceKind::I2c),
301            LiveBackend::I2c(FuchsiaI2c::new(proxy)),
302        );
303        id
304    }
305
306    /// Registers a SPI protocol resource.
307    pub fn add_spi(
308        &mut self,
309        name: impl Into<String>,
310        proxy: fspi::DeviceSynchronousProxy,
311    ) -> ResourceId {
312        let id = self.next_id;
313        self.next_id += 1;
314        self.bundle.add_protocol(
315            id,
316            name,
317            ResourceKind::Spi,
318            ProtocolCeiling::default_for(ResourceKind::Spi),
319            LiveBackend::Spi(FuchsiaSpi::new(proxy)),
320        );
321        id
322    }
323
324    /// Registers a Clock protocol resource.
325    pub fn add_clock(
326        &mut self,
327        name: impl Into<String>,
328        proxy: fclock::ClockSynchronousProxy,
329    ) -> ResourceId {
330        let id = self.next_id;
331        self.next_id += 1;
332        self.bundle.add_protocol(
333            id,
334            name,
335            ResourceKind::Clock,
336            ProtocolCeiling::default_for(ResourceKind::Clock),
337            LiveBackend::Clock(FuchsiaClock::new(proxy)),
338        );
339        id
340    }
341
342    /// Registers a Reset protocol resource.
343    pub fn add_reset(
344        &mut self,
345        name: impl Into<String>,
346        proxy: freset::ResetSynchronousProxy,
347    ) -> ResourceId {
348        let id = self.next_id;
349        self.next_id += 1;
350        self.bundle.add_protocol(
351            id,
352            name,
353            ResourceKind::Reset,
354            ProtocolCeiling::default_for(ResourceKind::Reset),
355            LiveBackend::Reset(FuchsiaReset::new(proxy)),
356        );
357        id
358    }
359
360    /// Registers a Serial protocol resource.
361    pub fn add_serial(
362        &mut self,
363        name: impl Into<String>,
364        proxy: fserial::DeviceSynchronousProxy,
365    ) -> ResourceId {
366        let id = self.next_id;
367        self.next_id += 1;
368        self.bundle.add_protocol(
369            id,
370            name,
371            ResourceKind::Serial,
372            ProtocolCeiling::default_for(ResourceKind::Serial),
373            LiveBackend::Serial(FuchsiaSerial::new(proxy)),
374        );
375        id
376    }
377
378    /// Registers an interrupt observation resource for event tapping.
379    pub fn add_interrupt(&mut self, name: impl Into<String>) -> ResourceId {
380        let id = self.next_id;
381        self.next_id += 1;
382        self.bundle.add_interrupt(id, name, LiveBackend::Interrupt);
383        id
384    }
385
386    /// Validates all registered resources and builds the [`EmbeddedLabServer`].
387    pub fn build(self) -> Result<EmbeddedLabServer, ProviderError> {
388        self.bundle.validate()?;
389
390        let mut boot_id_bytes = [0u8; 16];
391        zx::cprng_draw(&mut boot_id_bytes);
392        let boot_id: String = boot_id_bytes.iter().map(|byte| format!("{byte:02x}")).collect();
393        let proxy_generation = zx::BootInstant::get().into_nanos() as u64;
394
395        let manifest = TargetPolicyManifest::from_bundle_ceiling(
396            &self.bundle.resources,
397            &self.bundle.ceiling,
398            self.allow_mutating_sessions,
399        );
400        let policy_digest = manifest.policy_digest().to_string();
401        let digests = self.bundle.digests();
402        let identity = ProxyIdentity {
403            boot_id,
404            proxy_generation,
405            resource_digest: self.bundle.combined_digest().to_string(),
406            policy_digest,
407        };
408        let resource_digests: BTreeMap<u32, String> =
409            digests.iter().map(|(id, digest)| (*id, digest.to_string())).collect();
410
411        let mut interrupts = InterruptManager::new();
412        for (id, res) in &self.bundle.resources {
413            if res.kind == ResourceKind::Interrupt {
414                interrupts.register(*id);
415            }
416        }
417
418        // Disabled servers, which include every embedded server in builds
419        // without `enable_driver_lab`, emit no trace events.
420        let tracer = self.config.enabled.then(|| {
421            Arc::new(AuditTracer::new(&self.bundle.node_identity, &self.bundle.resources))
422        });
423
424        let limit_enforcer =
425            AccessLimitEnforcer::new(self.config.max_ops_per_second, self.config.max_deadline_ns);
426        let mut sessions = SessionManager::with_limits(
427            identity,
428            self.bundle.resources,
429            self.bundle.ceiling,
430            self.allow_mutating_sessions,
431            limit_enforcer,
432        );
433        if let Some(hook) = self.quiesce_hook {
434            sessions.set_quiesce_hook(hook);
435        }
436        let quiesce_flag = sessions.quiesce_flag();
437        if !self.config.enabled {
438            sessions.reject_new_sessions();
439        }
440
441        let executor = Executor::new(
442            self.bundle.backends,
443            ZxClock,
444            ExecLimits {
445                max_snapshot_items: self.config.max_snapshot_items as usize,
446                max_sequence_items: self.config.max_sequence_items as usize,
447                max_delay_ns: self.config.max_delay_ns as i64,
448                max_sequence_duration_ns: self.config.max_sequence_duration_ns as i64,
449            },
450        );
451        let mut audit = AuditRing::new(self.config.audit_capacity as usize);
452        if let Some(tracer) = &tracer {
453            audit.set_observer(tracer.observer());
454        }
455        audit.append(AuditRecord::lifecycle("embedded_driver_start", now_ns()));
456
457        let state: SharedState = Arc::new(SharedStateData {
458            inner: Mutex::new(ProxyState {
459                sessions,
460                executor,
461                audit,
462                resource_digests,
463                interrupts,
464                config: self.config,
465            }),
466            abort_token: AtomicBool::new(false),
467        });
468
469        Ok(EmbeddedLabServer { state, quiesce_flag, tracer })
470    }
471}
472
473/// Handle to the embedded `fuchsia.driver.lab` server running inside an
474/// existing DFv2 driver.
475#[derive(Clone)]
476pub struct EmbeddedLabServer {
477    state: SharedState,
478    quiesce_flag: Arc<AtomicBool>,
479    tracer: Option<Arc<AuditTracer>>,
480}
481
482impl EmbeddedLabServer {
483    /// Returns whether the embedded server is enabled.
484    pub fn is_enabled(&self) -> bool {
485        self.state.inner.lock().unwrap().config.enabled
486    }
487
488    /// Publishes `fuchsia.driver.lab.Service` (`default` instance) onto a
489    /// thread-safe driver [`ServiceFs`], spawning connection handlers onto
490    /// `scope_handle`.
491    ///
492    /// When disabled (`enable_driver_lab = false`), this is a no-op and omits
493    /// outgoing service handlers (Spec Phase 2 Section 4.1).
494    #[cfg(feature = "fdf_driver")]
495    pub fn publish(
496        &self,
497        outgoing: &mut ServiceFs<ServiceObj<'static, ()>>,
498        scope_handle: fasync::ScopeHandle,
499    ) {
500        if !self.is_enabled() {
501            return;
502        }
503        let state = self.state.clone();
504        outgoing.dir("svc").add_fidl_service_instance(
505            "default",
506            move |request: flab::ServiceRequest| {
507                let flab::ServiceRequest::Proxy(stream) = request;
508                scope_handle.spawn(server::serve_proxy(
509                    state.clone(),
510                    scope_handle.clone(),
511                    stream,
512                ));
513            },
514        );
515    }
516
517    /// Publishes `fuchsia.driver.lab.Service` (`default` instance) onto a
518    /// local driver [`ServiceFs`], spawning connection handlers onto
519    /// `scope_handle`.
520    ///
521    /// When disabled (`enable_driver_lab = false`), this is a no-op and omits
522    /// outgoing service handlers (Spec Phase 2 Section 4.1).
523    #[cfg(feature = "fdf_driver")]
524    pub fn publish_local(
525        &self,
526        outgoing: &mut ServiceFs<ServiceObjLocal<'static, ()>>,
527        scope_handle: fasync::ScopeHandle,
528    ) {
529        if !self.is_enabled() {
530            return;
531        }
532        let state = self.state.clone();
533        outgoing.dir("svc").add_fidl_service_instance(
534            "default",
535            move |request: flab::ServiceRequest| {
536                let flab::ServiceRequest::Proxy(stream) = request;
537                scope_handle.spawn(server::serve_proxy(
538                    state.clone(),
539                    scope_handle.clone(),
540                    stream,
541                ));
542            },
543        );
544    }
545
546    /// Spawns a single `fuchsia.driver.lab.Proxy` stream on `scope_handle`.
547    pub fn serve_proxy_stream(
548        &self,
549        scope_handle: fasync::ScopeHandle,
550        stream: flab::Proxy_RequestStream,
551    ) {
552        let state = self.state.clone();
553        scope_handle.spawn(server::serve_proxy(state, scope_handle.clone(), stream));
554    }
555
556    /// Spawns a single `fuchsia.driver.lab.Proxy` channel server-end on `scope_handle`.
557    pub fn serve_proxy_channel(&self, scope_handle: fasync::ScopeHandle, channel: zx::Channel) {
558        let stream = fidl::endpoints::ServerEnd::<flab::Proxy_Marker>::new(channel).into_stream();
559        self.serve_proxy_stream(scope_handle, stream);
560    }
561
562    /// Returns a reference to the underlying shared proxy state.
563    pub fn state(&self) -> &SharedState {
564        &self.state
565    }
566
567    /// Returns whether the driver is currently quiesced due to an active
568    /// mutating session (lock-free atomic read).
569    pub fn is_quiesced(&self) -> bool {
570        self.quiesce_flag.load(Ordering::SeqCst)
571    }
572
573    /// Returns a cloneable handle to the atomic quiesce flag so driver
574    /// background loops can check quiesce status without locking.
575    pub fn quiesce_flag(&self) -> Arc<AtomicBool> {
576        self.quiesce_flag.clone()
577    }
578
579    /// Samples the `driver_lab` trace counter for `offset` on
580    /// `resource_id` with `value`.
581    ///
582    /// Successful host writes are sampled automatically. Call this when
583    /// the driver itself changes a register or knob that a host may also
584    /// write, so both sources share one counter track. No-op unless the
585    /// server is enabled and a trace session has enabled `driver_lab`.
586    pub fn trace_counter(&self, resource_id: ResourceId, offset: u64, value: u32) {
587        if let Some(tracer) = &self.tracer {
588            tracer.counter(resource_id, offset, value);
589        }
590    }
591
592    /// Taps an interrupt event from the driver's own interrupt service routine
593    /// (`lab.notify_interrupt(resource_id)`) without stealing or binding the
594    /// raw `zx::Interrupt` handle (Spec Phase 2 Section 2.3).
595    ///
596    /// Increments the interrupt sequence and total count, appends to the audit
597    /// ring, and satisfies pending host `WaitForInterrupt` hanging gets.
598    pub fn notify_interrupt(&self, resource_id: ResourceId) -> Option<InterruptEvent> {
599        self.notify_interrupt_with_timestamp(resource_id, now_ns())
600    }
601
602    /// Taps an interrupt event with an explicit timestamp.
603    pub fn notify_interrupt_with_timestamp(
604        &self,
605        resource_id: ResourceId,
606        timestamp_ns: i64,
607    ) -> Option<InterruptEvent> {
608        let mut guard = self.state.inner.lock().unwrap();
609        let event = guard.interrupts.on_interrupt(resource_id, timestamp_ns, None).ok();
610        if let Some(ref ev) = event {
611            guard.audit.append(AuditRecord {
612                session: None,
613                resource: Some(resource_id),
614                operation: "interrupt_tap",
615                offset: None,
616                decision: Decision::Allowed,
617                status: OpStatus::Ok,
618                value: Some(ev.sequence as u32),
619                timestamp_ns,
620                run_id: None,
621                item_index: None,
622            });
623        }
624        event
625    }
626
627    /// Stops accepting new sessions, releases any active mutation lease and
628    /// quiesce hook, cancels pending interrupt waiters, and appends a stop
629    /// lifecycle audit record.
630    pub fn stop(&self) {
631        self.state.abort_token.store(true, Ordering::SeqCst);
632        let mut state = self.state.inner.lock().unwrap();
633        state.sessions.reject_new_sessions();
634        state.interrupts.cancel_waiters();
635        state.audit.append(AuditRecord::lifecycle("embedded_driver_stop", now_ns()));
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use lab_proxy_core::access_policy::WritableRegister;
643    use mmio::Mmio as _;
644    use mmio::vmo::VmoMapping;
645    use zx::CachePolicy;
646
647    #[fuchsia::test]
648    async fn embedded_server_describes_and_reads_live_mmio_over_fidl() {
649        const SIZE: usize = 4096;
650        let vmo = zx::Vmo::create(SIZE as u64).unwrap();
651        let driver_vmo = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
652
653        let mut driver_mmio =
654            VmoMapping::map_with_cache_policy(0, SIZE, driver_vmo, CachePolicy::Cached).unwrap();
655        // Simulated live registers:
656        // 0x00 = DEVICE_ID (0xD1A6_0001)
657        // 0x04 = STATUS (0x0000_0007)
658        // 0x40 = FIFO_DATA (clear-on-read, marked hard_denied)
659        driver_mmio.try_store32(0x00, 0xD1A6_0001).unwrap();
660        driver_mmio.try_store32(0x04, 0x0000_0007).unwrap();
661        driver_mmio.try_store32(0x40, 0xDEAD_BEEF).unwrap();
662
663        let mut builder = DriverLabBuilder::new("sample-driver");
664        let mmio_id = builder
665            .add_mmio_vmo_with_ceiling(
666                "regs",
667                &vmo,
668                0,
669                0x100,
670                ResourceCeiling {
671                    hard_denied: vec![0x40..0x44],
672                    allow_unknown_reads: true,
673                    allow_poll: true,
674                    writable_registers: vec![WritableRegister {
675                        offset: 0x08,
676                        width: 4,
677                        allow_mask: 0xFFFF_FFFF,
678                        allow_rmw: true,
679                        require_precondition: false,
680                        precondition_mask: 0,
681                        readback: true,
682                    }],
683                    protocol: None,
684                    allow_interrupt: false,
685                },
686            )
687            .unwrap();
688        assert_eq!(mmio_id, 0);
689
690        let server = builder.build().unwrap();
691        let scope = fasync::Scope::new_with_name("embedded-test");
692        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<flab::Proxy_Marker>();
693        server.serve_proxy_stream(scope.to_handle(), stream);
694
695        // 1. Describe the embedded server
696        let desc = proxy.describe().await.unwrap();
697        assert_eq!(desc.protocol_major, Some(1));
698        let resources = desc.resources.as_ref().unwrap();
699        assert_eq!(resources.len(), 1);
700        assert_eq!(resources[0].name.as_deref(), Some("regs"));
701        assert_eq!(resources[0].logical_size, Some(0x100));
702
703        // 2. Open a read-only session allowing 0x00 and 0x04
704        let (session_proxy, session_server) =
705            fidl::endpoints::create_proxy::<flab::SessionMarker>();
706        let open_res = proxy
707            .open_session(
708                &flab::RunContext {
709                    run_id: Some("run-cs25".to_string()),
710                    case_id: Some("case-cs25".to_string()),
711                    plan_digest: Some("sha256:test".to_string()),
712                    host_tool_version: Some("0.1.0".to_string()),
713                    ..Default::default()
714                },
715                flab::SessionMode::ReadOnly,
716                &flab::Expectations {
717                    boot_id: desc.boot_id.clone(),
718                    proxy_generation: desc.proxy_generation,
719                    resource_digest: desc.resource_digest.clone(),
720                    policy_digest: desc.policy_digest.clone(),
721                    ..Default::default()
722                },
723                &[
724                    flab::AccessRule {
725                        resource: 0,
726                        offset: 0x00,
727                        width: 4,
728                        class: flab::AccessClass::ReadOnce,
729                    },
730                    flab::AccessRule {
731                        resource: 0,
732                        offset: 0x04,
733                        width: 4,
734                        class: flab::AccessClass::Snapshot,
735                    },
736                ],
737                session_server,
738            )
739            .await
740            .unwrap();
741        assert!(open_res.is_ok());
742
743        // 3. Read32 at 0x00 reflects live driver state
744        let (read_val, _, _) = session_proxy.read32(0, 0x00).await.unwrap().unwrap();
745        assert_eq!(read_val, 0xD1A6_0001);
746
747        // 4. Snapshot at 0x04 reflects live driver state
748        let (snap_results, snap_complete) = session_proxy
749            .snapshot(&[flab::SnapshotItem { resource: 0, offset: 0x04 }])
750            .await
751            .unwrap()
752            .unwrap();
753        assert!(snap_complete);
754        assert_eq!(snap_results.len(), 1);
755        assert_eq!(snap_results[0].value, 0x0000_0007);
756
757        // 5. Hard-denied destructive FIFO register at 0x40 is rejected when opening a session
758        let (_bad_session_proxy, bad_session_server) =
759            fidl::endpoints::create_proxy::<flab::SessionMarker>();
760        let bad_open = proxy
761            .open_session(
762                &flab::RunContext::default(),
763                flab::SessionMode::ReadOnly,
764                &flab::Expectations {
765                    boot_id: desc.boot_id.clone(),
766                    proxy_generation: desc.proxy_generation,
767                    resource_digest: desc.resource_digest.clone(),
768                    policy_digest: desc.policy_digest.clone(),
769                    ..Default::default()
770                },
771                &[flab::AccessRule {
772                    resource: 0,
773                    offset: 0x40,
774                    width: 4,
775                    class: flab::AccessClass::ReadOnce,
776                }],
777                bad_session_server,
778            )
779            .await
780            .unwrap();
781        assert_eq!(bad_open, Err(flab::OpenSessionError::RejectedAllowlist));
782
783        // 6. Stop rejects new sessions
784        server.stop();
785    }
786
787    #[fuchsia::test]
788    async fn embedded_mutating_session_engages_quiesce_and_disconnect_releases_lease() {
789        const SIZE: usize = 4096;
790        let vmo = zx::Vmo::create(SIZE as u64).unwrap();
791        let driver_vmo = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
792        let mut driver_mmio =
793            VmoMapping::map_with_cache_policy(0, SIZE, driver_vmo, CachePolicy::Cached).unwrap();
794        driver_mmio.try_store32(0x08, 0x0000_0001).unwrap();
795
796        let hook_log = Arc::new(Mutex::new(Vec::new()));
797        let hook_log_clone = hook_log.clone();
798
799        let mut builder = DriverLabBuilder::new("sample-driver");
800        builder.with_quiesce_hook(move |paused| {
801            hook_log_clone.lock().unwrap().push(paused);
802        });
803        builder
804            .add_mmio_vmo_with_ceiling(
805                "regs",
806                &vmo,
807                0,
808                0x100,
809                ResourceCeiling {
810                    hard_denied: vec![],
811                    allow_unknown_reads: true,
812                    allow_poll: true,
813                    writable_registers: vec![WritableRegister {
814                        offset: 0x08,
815                        width: 4,
816                        allow_mask: 0xFFFF_FFFF,
817                        allow_rmw: true,
818                        require_precondition: false,
819                        precondition_mask: 0,
820                        readback: true,
821                    }],
822                    protocol: None,
823                    allow_interrupt: false,
824                },
825            )
826            .unwrap();
827        let irq_id = builder.add_interrupt("irq0");
828        assert_eq!(irq_id, 1);
829
830        let server = builder.build().unwrap();
831        let scope = fasync::Scope::new_with_name("embedded-quiesce-test");
832        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<flab::Proxy_Marker>();
833        server.serve_proxy_stream(scope.to_handle(), stream);
834
835        let desc = proxy.describe().await.unwrap();
836        assert!(!server.is_quiesced());
837
838        // Open mutating session with write + interrupt access
839        let (session_proxy, session_server) =
840            fidl::endpoints::create_proxy::<flab::SessionMarker>();
841        let open_res = proxy
842            .open_session(
843                &flab::RunContext { run_id: Some("run-cs26".to_string()), ..Default::default() },
844                flab::SessionMode::Mutating,
845                &flab::Expectations {
846                    boot_id: desc.boot_id.clone(),
847                    proxy_generation: desc.proxy_generation,
848                    resource_digest: desc.resource_digest.clone(),
849                    policy_digest: desc.policy_digest.clone(),
850                    ..Default::default()
851                },
852                &[
853                    flab::AccessRule {
854                        resource: 0,
855                        offset: 0x08,
856                        width: 4,
857                        class: flab::AccessClass::Write,
858                    },
859                    flab::AccessRule {
860                        resource: 1,
861                        offset: 0,
862                        width: 0,
863                        class: flab::AccessClass::Interrupt,
864                    },
865                ],
866                session_server,
867            )
868            .await
869            .unwrap();
870        assert!(open_res.is_ok());
871
872        // Driver is now quiesced
873        assert!(server.is_quiesced());
874        assert_eq!(*hook_log.lock().unwrap(), vec![true]);
875
876        // Perform Write32 while quiesced
877        let (readback_val, _, _) = session_proxy
878            .write32(0, 0x08, 0xABCD_EF01, 0xFFFF_FFFF, None, true)
879            .await
880            .unwrap()
881            .unwrap();
882        assert_eq!(readback_val, 0xABCD_EF01);
883        assert_eq!(driver_mmio.try_load32(0x08), Ok(0xABCD_EF01));
884
885        // Tap interrupt from driver ISR and verify WaitForInterrupt wakes up
886        let server_for_irq = server.clone();
887        scope.spawn(async move {
888            server_for_irq.notify_interrupt(1);
889        });
890        let (irq_res, irq_seq, _, _, _) =
891            session_proxy.wait_for_interrupt(1, 0, 500_000_000).await.unwrap().unwrap();
892        assert_eq!(irq_res, 1);
893        assert_eq!(irq_seq, 1);
894
895        // Simulate host disconnect by dropping session_proxy
896        drop(session_proxy);
897
898        // Open a second mutating session to prove lease was released and driver unquiesced
899        for _ in 0..50 {
900            if !server.is_quiesced() {
901                break;
902            }
903            fasync::Timer::new(zx::MonotonicInstant::after(zx::Duration::from_millis(5))).await;
904        }
905        assert!(!server.is_quiesced());
906        assert_eq!(*hook_log.lock().unwrap(), vec![true, false]);
907
908        let audit_page = server.state().inner.lock().unwrap().audit.read(0, 100);
909        assert!(audit_page.entries.iter().any(|e| e.record.operation == "quiesce_engaged"));
910        assert!(audit_page.entries.iter().any(|e| e.record.operation == "quiesce_released"));
911        assert!(audit_page.entries.iter().any(|e| e.record.operation == "interrupt_tap"));
912    }
913
914    #[fuchsia::test]
915    fn embedded_server_mirrors_audit_into_tracing_only_when_enabled() {
916        let enabled = DriverLabBuilder::new("sample-driver").build().unwrap();
917        assert!(enabled.state().inner.lock().unwrap().audit.has_observer());
918        // Without an active trace session, sampling is a no-op.
919        enabled.trace_counter(0, 0x1c, 1);
920
921        let disabled = DriverLabBuilder::new("sample-driver").with_enabled(false).build().unwrap();
922        assert!(!disabled.state().inner.lock().unwrap().audit.has_observer());
923        disabled.trace_counter(0, 0x1c, 1);
924    }
925
926    #[fuchsia::test]
927    async fn embedded_state_bank_supports_slots_knobs_and_triggers_over_fidl() {
928        let mut bank = StateBank::new(0x20);
929        let violations_slot = bank.define_state_slot(0x00, 0);
930        let delay_knob = bank.define_knob(0x04, 0);
931        let fix_knob = bank.define_knob(0x08, 0);
932
933        let violations_for_trigger = violations_slot.clone();
934        let delay_for_trigger = delay_knob.clone();
935        let fix_for_trigger = fix_knob.clone();
936        bank.define_trigger(0x0C, move |_arg| {
937            if delay_for_trigger.load() > 0 && fix_for_trigger.load() == 0 {
938                violations_for_trigger.fetch_add(1);
939                Ok(1)
940            } else {
941                Ok(0)
942            }
943        });
944
945        let mut builder = DriverLabBuilder::new("sample-driver");
946        let state_id = builder.with_state_bank("state0", bank);
947        assert_eq!(state_id, 0);
948
949        let server = builder.build().unwrap();
950        let scope = fasync::Scope::new_with_name("embedded-state-bank-test");
951        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<flab::Proxy_Marker>();
952        server.serve_proxy_stream(scope.to_handle(), stream);
953
954        let desc = proxy.describe().await.unwrap();
955        let resources = desc.resources.as_ref().unwrap();
956        assert_eq!(resources.len(), 1);
957        assert_eq!(resources[0].name.as_deref(), Some("state0"));
958        assert_eq!(resources[0].logical_size, Some(0x20));
959
960        // Attempting to open a session with a Write rule on read-only state slot 0x00 fails.
961        let (_bad_session, bad_server) = fidl::endpoints::create_proxy::<flab::SessionMarker>();
962        let bad_open = proxy
963            .open_session(
964                &flab::RunContext::default(),
965                flab::SessionMode::Mutating,
966                &flab::Expectations {
967                    boot_id: desc.boot_id.clone(),
968                    proxy_generation: desc.proxy_generation,
969                    resource_digest: desc.resource_digest.clone(),
970                    policy_digest: desc.policy_digest.clone(),
971                    ..Default::default()
972                },
973                &[flab::AccessRule {
974                    resource: 0,
975                    offset: 0x00,
976                    width: 4,
977                    class: flab::AccessClass::Write,
978                }],
979                bad_server,
980            )
981            .await
982            .unwrap();
983        assert_eq!(bad_open, Err(flab::OpenSessionError::RejectedAllowlist));
984
985        // Open mutating session with read access to 0x00 and write access to 0x04, 0x08, 0x0C.
986        let (session_proxy, session_server) =
987            fidl::endpoints::create_proxy::<flab::SessionMarker>();
988        let open_res = proxy
989            .open_session(
990                &flab::RunContext { run_id: Some("run-state".to_string()), ..Default::default() },
991                flab::SessionMode::Mutating,
992                &flab::Expectations {
993                    boot_id: desc.boot_id.clone(),
994                    proxy_generation: desc.proxy_generation,
995                    resource_digest: desc.resource_digest.clone(),
996                    policy_digest: desc.policy_digest.clone(),
997                    ..Default::default()
998                },
999                &[
1000                    flab::AccessRule {
1001                        resource: 0,
1002                        offset: 0x00,
1003                        width: 4,
1004                        class: flab::AccessClass::ReadOnce,
1005                    },
1006                    flab::AccessRule {
1007                        resource: 0,
1008                        offset: 0x04,
1009                        width: 4,
1010                        class: flab::AccessClass::Write,
1011                    },
1012                    flab::AccessRule {
1013                        resource: 0,
1014                        offset: 0x08,
1015                        width: 4,
1016                        class: flab::AccessClass::Write,
1017                    },
1018                    flab::AccessRule {
1019                        resource: 0,
1020                        offset: 0x0C,
1021                        width: 4,
1022                        class: flab::AccessClass::Write,
1023                    },
1024                ],
1025                session_server,
1026            )
1027            .await
1028            .unwrap();
1029        assert!(open_res.is_ok());
1030
1031        // Set delay knob = 50 us, fix knob = 0, then trigger -> violation observed.
1032        let (delay_rb, _, _) =
1033            session_proxy.write32(0, 0x04, 50, 0xFFFF_FFFF, None, true).await.unwrap().unwrap();
1034        assert_eq!(delay_rb, 50);
1035        let (trig_rb, _, _) =
1036            session_proxy.write32(0, 0x0C, 1, 0xFFFF_FFFF, None, true).await.unwrap().unwrap();
1037        assert_eq!(trig_rb, 1);
1038        let (violations, _, _) = session_proxy.read32(0, 0x00).await.unwrap().unwrap();
1039        assert_eq!(violations, 1);
1040
1041        // Set fix knob = 1, then trigger -> no violation (readback == 0, violations stays 1).
1042        let (fix_rb, _, _) =
1043            session_proxy.write32(0, 0x08, 1, 0xFFFF_FFFF, None, true).await.unwrap().unwrap();
1044        assert_eq!(fix_rb, 1);
1045        let (trig_fixed_rb, _, _) =
1046            session_proxy.write32(0, 0x0C, 1, 0xFFFF_FFFF, None, true).await.unwrap().unwrap();
1047        assert_eq!(trig_fixed_rb, 0);
1048        let (violations_after, _, _) = session_proxy.read32(0, 0x00).await.unwrap().unwrap();
1049        assert_eq!(violations_after, 1);
1050    }
1051}