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