Skip to main content

driver_lab_rust/
platform_provider.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//! Platform-device and peripheral resource provider.
6//!
7//! Discovers and maps every MMIO region, IRQ, GPIO, I2C, and SPI resource
8//! the bound node offers, packaging them into a [`ProvidedResources`] bundle.
9//! A node that offers no resources yields an empty bundle: the control plane
10//! still serves, but nothing is reachable. A node that offers resources the
11//! driver cannot acquire fails acquisition -- a partially acquired device
12//! would misrepresent the resource identity the digests promise.
13
14#[cfg(feature = "fdf_driver")]
15use crate::fuchsia_backends::{
16    FuchsiaClock, FuchsiaGpio, FuchsiaI2c, FuchsiaReset, FuchsiaSerial, FuchsiaSpi, LiveBackend,
17};
18#[cfg(feature = "fdf_driver")]
19use fidl_fuchsia_hardware_clock as fclock;
20#[cfg(feature = "fdf_driver")]
21use fidl_fuchsia_hardware_gpio as fgpio;
22#[cfg(feature = "fdf_driver")]
23use fidl_fuchsia_hardware_i2c as fi2c;
24#[cfg(feature = "fdf_driver")]
25use fidl_fuchsia_hardware_reset as freset;
26#[cfg(feature = "fdf_driver")]
27use fidl_fuchsia_hardware_serial as fserial;
28#[cfg(feature = "fdf_driver")]
29use fidl_fuchsia_hardware_spi as fspi;
30#[cfg(feature = "fdf_driver")]
31use fidl_next_fuchsia_hardware_platform_device as fpdev;
32#[cfg(feature = "fdf_driver")]
33use lab_proxy_core::access_policy::{
34    MmioResource, ProtocolCeiling, ResourceCeiling, ResourceId, ResourceKind,
35};
36use lab_proxy_core::hardware_backend::{BackendError, MmioBackend};
37#[cfg(feature = "fdf_driver")]
38use lab_proxy_core::provider::ProvidedResources;
39#[cfg(feature = "fdf_driver")]
40use log::info;
41#[cfg(feature = "fdf_driver")]
42use mmio::Mmio as _;
43#[cfg(feature = "fdf_driver")]
44use mmio::region::MmioRegion;
45#[cfg(feature = "fdf_driver")]
46use mmio::vmo::{VmoMapping, VmoMemory};
47#[cfg(feature = "fdf_driver")]
48use pdev::PlatformDevice;
49
50/// Provider kind recorded in resource digests.
51pub const PROVIDER: &str = "platform";
52
53#[cfg(not(feature = "fdf_driver"))]
54struct RawVmoMapping {
55    map_addr: usize,
56    map_size: usize,
57    base_addr: usize,
58    len: usize,
59}
60
61#[cfg(not(feature = "fdf_driver"))]
62impl RawVmoMapping {
63    fn map(offset: usize, size: usize, vmo: zx::Vmo) -> Result<Self, zx::Status> {
64        if size == 0 || size > isize::MAX as usize {
65            return Err(zx::Status::OUT_OF_RANGE);
66        }
67        let page_size = zx::system_get_page_size() as usize;
68        let page_offset = offset % page_size;
69        let aligned_offset = (offset - page_offset) as u64;
70        let map_size = size
71            .checked_add(page_offset)
72            .ok_or(zx::Status::OUT_OF_RANGE)?
73            .next_multiple_of(page_size);
74
75        let info = vmo.info()?;
76        if aligned_offset.saturating_add(map_size as u64) > info.size_bytes {
77            return Err(zx::Status::OUT_OF_RANGE);
78        }
79
80        let root_self = fuchsia_runtime::vmar_root_self();
81        let map_addr = root_self.map(
82            0,
83            &vmo,
84            aligned_offset,
85            map_size,
86            zx::VmarFlags::PERM_READ | zx::VmarFlags::PERM_WRITE | zx::VmarFlags::MAP_RANGE,
87        )?;
88        Ok(Self { map_addr, map_size, base_addr: map_addr + page_offset, len: size })
89    }
90
91    fn len(&self) -> usize {
92        self.len
93    }
94
95    fn try_load32(&self, offset: usize) -> Result<u32, BackendError> {
96        if !offset.is_multiple_of(4) || offset.checked_add(4).is_none_or(|end| end > self.len) {
97            return Err(BackendError::Fault);
98        }
99        let ptr = std::ptr::with_exposed_provenance::<u32>(self.base_addr + offset);
100        // SAFETY: `offset + 4 <= self.len` and `offset` is 4-byte aligned within the VMAR mapping.
101        Ok(unsafe { std::ptr::read_volatile(ptr) })
102    }
103
104    fn try_store32(&mut self, offset: usize, value: u32) -> Result<(), BackendError> {
105        if !offset.is_multiple_of(4) || offset.checked_add(4).is_none_or(|end| end > self.len) {
106            return Err(BackendError::Fault);
107        }
108        let ptr = std::ptr::with_exposed_provenance_mut::<u32>(self.base_addr + offset);
109        // SAFETY: `offset + 4 <= self.len` and `offset` is 4-byte aligned within the VMAR mapping.
110        unsafe { std::ptr::write_volatile(ptr, value) };
111        Ok(())
112    }
113}
114
115#[cfg(not(feature = "fdf_driver"))]
116impl Drop for RawVmoMapping {
117    fn drop(&mut self) {
118        let root_self = fuchsia_runtime::vmar_root_self();
119        // SAFETY: `self.map_addr` and `self.map_size` were returned by `root_self.map` and are
120        // only accessed while `self` is alive.
121        let _ = unsafe { root_self.unmap(self.map_addr, self.map_size) };
122    }
123}
124
125/// Volatile access to one locally mapped MMIO region.
126pub struct MappedMmio {
127    #[cfg(feature = "fdf_driver")]
128    region: MmioRegion<VmoMemory>,
129    #[cfg(not(feature = "fdf_driver"))]
130    region: RawVmoMapping,
131}
132
133impl MappedMmio {
134    /// Wraps an already-mapped [`MmioRegion`].
135    #[cfg(feature = "fdf_driver")]
136    pub fn from_region(region: MmioRegion<VmoMemory>) -> Self {
137        Self { region }
138    }
139
140    /// Creates an independent local MMIO mapping by duplicating an existing
141    /// driver MMIO VMO handle (`zx::Rights::SAME_RIGHTS`) and mapping it into
142    /// the current driver host process address space (Spec Phase 2 Section 2.1).
143    ///
144    /// Preserves the VMO's existing cache policy so already-mapped VMOs can be
145    /// mapped a second time without failing on `zx_vmo_set_cache_policy`.
146    pub fn from_vmo(vmo: &zx::Vmo, offset: usize, size: usize) -> Result<Self, zx::Status> {
147        let dup_vmo = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
148        #[cfg(feature = "fdf_driver")]
149        {
150            let cache_policy = dup_vmo.info()?.cache_policy();
151            let region = VmoMapping::map_with_cache_policy(offset, size, dup_vmo, cache_policy)?;
152            Ok(Self { region })
153        }
154        #[cfg(not(feature = "fdf_driver"))]
155        {
156            let region = RawVmoMapping::map(offset, size, dup_vmo)?;
157            Ok(Self { region })
158        }
159    }
160
161    /// Returns the mapped byte length of the region.
162    pub fn len(&self) -> usize {
163        self.region.len()
164    }
165
166    /// Returns whether the mapped region has zero length.
167    pub fn is_empty(&self) -> bool {
168        self.region.len() == 0
169    }
170}
171
172impl std::fmt::Debug for MappedMmio {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.debug_struct("MappedMmio").field("len", &self.region.len()).finish()
175    }
176}
177
178impl MmioBackend for MappedMmio {
179    fn read32(&mut self, offset: u64) -> Result<u32, BackendError> {
180        let offset = usize::try_from(offset).map_err(|_| BackendError::Fault)?;
181        self.region.try_load32(offset).map_err(|_| BackendError::Fault)
182    }
183
184    fn write32(&mut self, offset: u64, value: u32) -> Result<(), BackendError> {
185        let offset = usize::try_from(offset).map_err(|_| BackendError::Fault)?;
186        self.region.try_store32(offset, value).map_err(|_| BackendError::Fault)
187    }
188
189    fn barrier(&mut self) {
190        std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
191    }
192}
193
194impl lab_proxy_core::protocol_resource_adapter::ResourceBackend for MappedMmio {
195    fn read32(&mut self, offset: u64) -> Result<u32, BackendError> {
196        MmioBackend::read32(self, offset)
197    }
198
199    fn write32(&mut self, offset: u64, value: u32) -> Result<(), BackendError> {
200        MmioBackend::write32(self, offset, value)
201    }
202
203    fn barrier(&mut self) {
204        MmioBackend::barrier(self);
205    }
206}
207
208/// Why acquisition failed. Driver start fails on any of these: serving
209/// a partially acquired device would break resource-identity promises.
210#[cfg(feature = "fdf_driver")]
211#[derive(Debug)]
212pub enum AcquireError {
213    /// The device reported an MMIO the driver could not fetch or map.
214    Mmio {
215        /// The MMIO index that failed.
216        id: u32,
217    },
218    /// The device reported an IRQ the driver could not fetch.
219    Irq {
220        /// The IRQ index that failed.
221        id: u32,
222    },
223}
224
225#[cfg(feature = "fdf_driver")]
226impl std::fmt::Display for AcquireError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        match self {
229            AcquireError::Mmio { id } => write!(f, "MMIO {id} could not be fetched or mapped"),
230            AcquireError::Irq { id } => write!(f, "IRQ {id} could not be fetched"),
231        }
232    }
233}
234
235#[cfg(feature = "fdf_driver")]
236fn connect_instance(
237    context: &fdf_component::DriverContext,
238    instance: &str,
239) -> Option<fidl_next::Client<fpdev::Device>> {
240    let service = context
241        .incoming
242        .service::<fdf_component::ServiceInstance<fpdev::Service>>()
243        .instance(instance)
244        .connect_next()
245        .ok()?;
246    let (client_end, server_end) = fidl_next::fuchsia::create_channel();
247    service.device(server_end).ok()?;
248    Some(client_end.spawn())
249}
250
251/// Acquires the bound node's platform-device and protocol resources.
252///
253/// Returns an empty bundle when the node offers no reachable resources.
254#[cfg(feature = "fdf_driver")]
255pub async fn acquire(
256    context: &fdf_component::DriverContext,
257    node_identity: &str,
258) -> Result<(ProvidedResources<LiveBackend>, Vec<(ResourceId, zx::Interrupt)>), AcquireError> {
259    let mut bundle = ProvidedResources::empty(PROVIDER, node_identity);
260    let mut acquired_irqs = Vec::new();
261    let mut next_id: ResourceId = 0;
262
263    // 1. Probe platform device for MMIOs and IRQs
264    let mut connected_pdev = None;
265    for instance in ["pdev", "default"] {
266        let Some(pdev) = connect_instance(context, instance) else { continue };
267        match pdev.get_node_device_info().await {
268            Ok(Ok(device_info)) => {
269                connected_pdev = Some((
270                    pdev,
271                    device_info.mmio_count.unwrap_or(0),
272                    device_info.irq_count.unwrap_or(0),
273                ));
274                break;
275            }
276            Ok(Err(status)) => {
277                info!("get_node_device_info on instance {instance} returned {status:?}");
278            }
279            Err(error) => {
280                info!("platform device instance {instance} not served ({error:?})");
281            }
282        }
283    }
284
285    if let Some((pdev, mmio_count, irq_count)) = connected_pdev {
286        for id in 0..mmio_count {
287            let region = pdev.map_mmio_by_id(id).await.map_err(|error| {
288                info!("mapping MMIO {id} failed: {error:?}");
289                AcquireError::Mmio { id }
290            })?;
291            let size = region.len() as u64;
292            let res_id = next_id;
293            next_id += 1;
294            bundle.resources.insert(res_id, MmioResource::mmio(format!("mmio{id}"), size, size));
295            bundle.ceiling.insert(
296                res_id,
297                ResourceCeiling {
298                    hard_denied: vec![],
299                    allow_unknown_reads: true,
300                    allow_poll: true,
301                    writable_registers: vec![],
302                    protocol: None,
303                    allow_interrupt: false,
304                },
305            );
306            bundle.backends.insert(res_id, LiveBackend::Mmio(MappedMmio { region }));
307        }
308
309        for id in 0..irq_count {
310            let irq = pdev
311                .get_interrupt_by_id(id, 0)
312                .await
313                .map_err(|error| {
314                    info!("getting IRQ {id} failed: {error:?}");
315                    AcquireError::Irq { id }
316                })?
317                .map_err(|status| {
318                    info!("getting IRQ {id} returned status {status:?}");
319                    AcquireError::Irq { id }
320                })?
321                .irq;
322            let res_id = next_id;
323            next_id += 1;
324            bundle.resources.insert(res_id, MmioResource::interrupt(format!("irq{id}")));
325            bundle.ceiling.insert(
326                res_id,
327                ResourceCeiling {
328                    hard_denied: vec![],
329                    allow_unknown_reads: false,
330                    allow_poll: false,
331                    writable_registers: vec![],
332                    protocol: Some(ProtocolCeiling::default_for(ResourceKind::Interrupt)),
333                    allow_interrupt: true,
334                },
335            );
336            bundle.backends.insert(res_id, LiveBackend::Interrupt);
337            acquired_irqs.push((res_id, irq));
338        }
339    }
340
341    // 2. Probe GPIO services
342    for instance in ["default"] {
343        if let Ok(service) =
344            context.incoming.service_marker(fgpio::ServiceMarker).instance(&instance).connect()
345        {
346            if let Ok(proxy) = service.connect_to_device_sync() {
347                match proxy.read(zx::MonotonicInstant::INFINITE) {
348                    Ok(_) => {
349                        let res_id = next_id;
350                        next_id += 1;
351                        let name = if instance == "default" {
352                            "gpio0".to_string()
353                        } else {
354                            format!("gpio-{instance}")
355                        };
356                        info!("acquired GPIO resource {name} (id {res_id})");
357                        bundle.resources.insert(res_id, MmioResource::gpio(name));
358                        bundle.ceiling.insert(
359                            res_id,
360                            ResourceCeiling {
361                                hard_denied: vec![],
362                                allow_unknown_reads: false,
363                                allow_poll: false,
364                                writable_registers: vec![],
365                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::Gpio)),
366                                allow_interrupt: false,
367                            },
368                        );
369                        bundle.backends.insert(res_id, LiveBackend::Gpio(FuchsiaGpio::new(proxy)));
370                    }
371                    Err(e) => {
372                        info!("GPIO instance {instance} probe failed: {e:?}");
373                    }
374                }
375            }
376        }
377    }
378
379    // 3. Probe I2C services
380    for instance in ["default"] {
381        if let Ok(service) =
382            context.incoming.service_marker(fi2c::ServiceMarker).instance(&instance).connect()
383        {
384            if let Ok(proxy) = service.connect_to_device_sync() {
385                // An empty transfer triggers no bus activity and succeeds with INVALID_ARGS if the server is active.
386                match proxy.transfer(&[], zx::MonotonicInstant::INFINITE) {
387                    Ok(Err(status)) if status == zx::Status::INVALID_ARGS.into_raw() => {
388                        let res_id = next_id;
389                        next_id += 1;
390                        let name = if instance == "default" {
391                            "i2c0".to_string()
392                        } else {
393                            format!("i2c-{instance}")
394                        };
395                        info!("acquired I2C resource {name} (id {res_id})");
396                        bundle.resources.insert(res_id, MmioResource::i2c(name));
397                        bundle.ceiling.insert(
398                            res_id,
399                            ResourceCeiling {
400                                hard_denied: vec![],
401                                allow_unknown_reads: false,
402                                allow_poll: false,
403                                writable_registers: vec![],
404                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::I2c)),
405                                allow_interrupt: false,
406                            },
407                        );
408                        bundle.backends.insert(res_id, LiveBackend::I2c(FuchsiaI2c::new(proxy)));
409                    }
410                    Ok(Ok(_)) => {
411                        let res_id = next_id;
412                        next_id += 1;
413                        let name = if instance == "default" {
414                            "i2c0".to_string()
415                        } else {
416                            format!("i2c-{instance}")
417                        };
418                        info!("acquired I2C resource {name} (id {res_id})");
419                        bundle.resources.insert(res_id, MmioResource::i2c(name));
420                        bundle.ceiling.insert(
421                            res_id,
422                            ResourceCeiling {
423                                hard_denied: vec![],
424                                allow_unknown_reads: false,
425                                allow_poll: false,
426                                writable_registers: vec![],
427                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::I2c)),
428                                allow_interrupt: false,
429                            },
430                        );
431                        bundle.backends.insert(res_id, LiveBackend::I2c(FuchsiaI2c::new(proxy)));
432                    }
433                    res => {
434                        info!("I2C instance {instance} probe returned: {res:?}");
435                    }
436                }
437            }
438        }
439    }
440
441    // 4. Probe SPI services
442    for instance in ["default"] {
443        if let Ok(service) =
444            context.incoming.service_marker(fspi::ServiceMarker).instance(&instance).connect()
445        {
446            if let Ok(proxy) = service.connect_to_device_sync() {
447                // CanAssertCs is a pure software query that verifies server liveness without bus transmission.
448                match proxy.can_assert_cs(zx::MonotonicInstant::INFINITE) {
449                    Ok(_) => {
450                        let res_id = next_id;
451                        next_id += 1;
452                        let name = if instance == "default" {
453                            "spi0".to_string()
454                        } else {
455                            format!("spi-{instance}")
456                        };
457                        info!("acquired SPI resource {name} (id {res_id})");
458                        bundle.resources.insert(res_id, MmioResource::spi(name));
459                        bundle.ceiling.insert(
460                            res_id,
461                            ResourceCeiling {
462                                hard_denied: vec![],
463                                allow_unknown_reads: false,
464                                allow_poll: false,
465                                writable_registers: vec![],
466                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::Spi)),
467                                allow_interrupt: false,
468                            },
469                        );
470                        bundle.backends.insert(res_id, LiveBackend::Spi(FuchsiaSpi::new(proxy)));
471                    }
472                    Err(e) => {
473                        info!("SPI instance {instance} probe returned: {e:?}");
474                    }
475                }
476            }
477        }
478    }
479
480    // 5. Probe Clock services
481    for instance in ["default"] {
482        if let Ok(service) =
483            context.incoming.service_marker(fclock::ServiceMarker).instance(&instance).connect()
484        {
485            if let Ok(proxy) = service.connect_to_clock_sync() {
486                match proxy.is_enabled(zx::MonotonicInstant::INFINITE) {
487                    Ok(_) => {
488                        let res_id = next_id;
489                        next_id += 1;
490                        let name = if instance == "default" {
491                            "clock0".to_string()
492                        } else {
493                            format!("clock-{instance}")
494                        };
495                        info!("acquired Clock resource {name} (id {res_id})");
496                        bundle.resources.insert(res_id, MmioResource::clock(name));
497                        bundle.ceiling.insert(
498                            res_id,
499                            ResourceCeiling {
500                                hard_denied: vec![],
501                                allow_unknown_reads: false,
502                                allow_poll: false,
503                                writable_registers: vec![],
504                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::Clock)),
505                                allow_interrupt: false,
506                            },
507                        );
508                        bundle
509                            .backends
510                            .insert(res_id, LiveBackend::Clock(FuchsiaClock::new(proxy)));
511                    }
512                    Err(e) => {
513                        info!("Clock instance {instance} probe returned: {e:?}");
514                    }
515                }
516            }
517        }
518    }
519
520    // 6. Probe Reset services
521    for instance in ["default"] {
522        if let Ok(service) =
523            context.incoming.service_marker(freset::ServiceMarker).instance(&instance).connect()
524        {
525            if let Ok(proxy) = service.connect_to_reset_sync() {
526                match proxy.status(zx::MonotonicInstant::INFINITE) {
527                    Ok(_) => {
528                        let res_id = next_id;
529                        next_id += 1;
530                        let name = if instance == "default" {
531                            "reset0".to_string()
532                        } else {
533                            format!("reset-{instance}")
534                        };
535                        info!("acquired Reset resource {name} (id {res_id})");
536                        bundle.resources.insert(res_id, MmioResource::reset(name));
537                        bundle.ceiling.insert(
538                            res_id,
539                            ResourceCeiling {
540                                hard_denied: vec![],
541                                allow_unknown_reads: false,
542                                allow_poll: false,
543                                writable_registers: vec![],
544                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::Reset)),
545                                allow_interrupt: false,
546                            },
547                        );
548                        bundle
549                            .backends
550                            .insert(res_id, LiveBackend::Reset(FuchsiaReset::new(proxy)));
551                    }
552                    Err(e) => {
553                        info!("Reset instance {instance} probe returned: {e:?}");
554                    }
555                }
556            }
557        }
558    }
559
560    // 7. Probe Serial services
561    for instance in ["default"] {
562        if let Ok(service) =
563            context.incoming.service_marker(fserial::ServiceMarker).instance(&instance).connect()
564        {
565            if let Ok(proxy) = service.connect_to_device_sync() {
566                match proxy.get_class(zx::MonotonicInstant::INFINITE) {
567                    Ok(_) => {
568                        let res_id = next_id;
569                        next_id += 1;
570                        let name = if instance == "default" {
571                            "serial0".to_string()
572                        } else {
573                            format!("serial-{instance}")
574                        };
575                        info!("acquired Serial resource {name} (id {res_id})");
576                        bundle.resources.insert(res_id, MmioResource::serial(name));
577                        bundle.ceiling.insert(
578                            res_id,
579                            ResourceCeiling {
580                                hard_denied: vec![],
581                                allow_unknown_reads: false,
582                                allow_poll: false,
583                                writable_registers: vec![],
584                                protocol: Some(ProtocolCeiling::default_for(ResourceKind::Serial)),
585                                allow_interrupt: false,
586                            },
587                        );
588                        bundle
589                            .backends
590                            .insert(res_id, LiveBackend::Serial(FuchsiaSerial::new(proxy)));
591                    }
592                    Err(e) => {
593                        info!("Serial instance {instance} probe returned: {e:?}");
594                    }
595                }
596            }
597        }
598    }
599
600    if bundle.resources.is_empty() {
601        info!("no hardware resources offered; serving zero resources");
602        return Ok((ProvidedResources::empty("none", node_identity), acquired_irqs));
603    }
604
605    info!("acquired {} total hardware resource(s)", bundle.resources.len());
606    Ok((bundle, acquired_irqs))
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612    use zx::CachePolicy;
613
614    #[test]
615    fn mapped_mmio_from_vmo_shares_memory_independently_of_driver_mapping() {
616        const SIZE: usize = 4096;
617        let vmo = zx::Vmo::create(SIZE as u64).unwrap();
618        let driver_vmo = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
619
620        // Simulate the active driver mapping its own MMIO region first.
621        let mut driver_region =
622            VmoMapping::map_with_cache_policy(0, SIZE, driver_vmo, CachePolicy::Cached).unwrap();
623        assert_eq!(vmo.info().unwrap().num_mappings, 1);
624
625        // Driver initializes a register at 0x10.
626        driver_region.try_store32(0x10, 0xCAFE_BABE).unwrap();
627
628        // Embedded library duplicates the VMO and creates an independent local mapping.
629        let mut lab_mmio = MappedMmio::from_vmo(&vmo, 0, SIZE).unwrap();
630        assert_eq!(lab_mmio.len(), SIZE);
631        assert!(!lab_mmio.is_empty());
632        assert_eq!(vmo.info().unwrap().num_mappings, 2);
633
634        // Library observes live register value written by the driver.
635        assert_eq!(MmioBackend::read32(&mut lab_mmio, 0x10), Ok(0xCAFE_BABE));
636
637        // Library writes a value at 0x20; driver observes it in its own mapping.
638        assert_eq!(MmioBackend::write32(&mut lab_mmio, 0x20, 0x1234_5678), Ok(()));
639        assert_eq!(driver_region.try_load32(0x20), Ok(0x1234_5678));
640
641        // Dropping the library mapping leaves the driver's mapping intact.
642        drop(lab_mmio);
643        assert_eq!(vmo.info().unwrap().num_mappings, 1);
644        assert_eq!(driver_region.try_load32(0x10), Ok(0xCAFE_BABE));
645        assert_eq!(driver_region.try_load32(0x20), Ok(0x1234_5678));
646    }
647
648    #[test]
649    fn mapped_mmio_from_vmo_rejects_out_of_range_size() {
650        let vmo = zx::Vmo::create(4096).unwrap();
651        let err = MappedMmio::from_vmo(&vmo, 0, 8192).unwrap_err();
652        assert_eq!(err, zx::Status::OUT_OF_RANGE);
653    }
654}