blob: 1071d7795149a40d62b30483dde95fb420cb9035 [file]
// Copyright 2026 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
use super::dispatcher::{
Dispatcher, DispatcherOps, PeerHolder, PeerHolderMuClass, PeeredState,
impl_peered_dispatcher_facade_with_state,
};
use super::handle::{HandleValue, KernelHandle};
use super::io_buffer_dispatcher_ffi::{
cpp_io_buffer_dispatcher_as_child_observer, cpp_io_buffer_dispatcher_create,
};
use super::io_buffer_shared_region_dispatcher::IoBufferSharedRegionDispatcher;
use super::vm_object_dispatcher::VmObjectDispatcher;
use crate::kernel::koid;
use crate::kernel::types::VAddr;
use crate::user_copy::UserInPtr;
use crate::vm::arch_vm_aspace::{ARCH_MMU_FLAG_PERM_READ, ARCH_MMU_FLAG_PERM_WRITE};
use crate::vm::pinned_vm_object::PinnedVmObject;
use crate::vm::pmm;
use crate::vm::vm_address_region::flag::DEBUG_DYNAMIC_KERNEL_MAPPING;
use crate::vm::vm_aspace::VmAspace;
use crate::vm::vm_mapping::VmMapping;
use crate::vm::vm_object::{Resizability, VmObject, VmObjectChildObserver};
use crate::vm::vm_object_paged::VmObjectPaged;
use core::convert::Infallible;
use core::pin::Pin;
use core::{ptr, slice};
use counters_rs::define_kcounter;
use fbl::{Array, Canary, Recyclable, RefPtr, pin_make_ref_counted, ref_counted};
use iob::{BlobIdAllocator, ZeroFill};
use kalloc::{AllocError, Box};
use ksync::{KMutex, LockToken, PhantomMutex, guarded};
use object_constants_rs::{
kIoBufferDispatcherStateAlign, kIoBufferDispatcherStateOffset, kIoBufferDispatcherStateSize,
};
use page;
use pin_init::{PinInit, pin_data, pin_init, pinned_drop};
use zx_status::Status;
use zx_types::{
ZX_IOB_ACCESS_EP0_CAN_MAP_READ, ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE,
ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ, ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE,
ZX_IOB_ACCESS_EP1_CAN_MAP_READ, ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE,
ZX_IOB_ACCESS_EP1_CAN_MEDIATED_READ, ZX_IOB_ACCESS_EP1_CAN_MEDIATED_WRITE,
ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR, ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER,
ZX_IOB_DISCIPLINE_TYPE_NONE, ZX_IOB_MAX_REGIONS, ZX_IOB_PEER_CLOSED,
ZX_IOB_REGION_TYPE_PRIVATE, ZX_IOB_REGION_TYPE_SHARED, ZX_MAX_NAME_LEN, ZX_OBJ_TYPE_IOB,
ZX_RIGHT_DUPLICATE, ZX_RIGHT_GET_PROPERTY, ZX_RIGHT_INSPECT, ZX_RIGHT_MAP, ZX_RIGHT_NONE,
ZX_RIGHT_READ, ZX_RIGHT_SET_PROPERTY, ZX_RIGHT_SIGNAL, ZX_RIGHT_SIGNAL_PEER, ZX_RIGHT_TRANSFER,
ZX_RIGHT_WAIT, ZX_RIGHT_WRITE, ZX_USER_SIGNAL_ALL, zx_info_iob_t, zx_iob_region_info_t,
zx_iob_region_t, zx_iovec_t, zx_koid_t, zx_rights_t,
};
const DEFAULT_RIGHTS: zx_rights_t = ZX_RIGHT_TRANSFER
| ZX_RIGHT_DUPLICATE
| ZX_RIGHT_INSPECT
| ZX_RIGHT_WAIT
| ZX_RIGHT_READ
| ZX_RIGHT_WRITE
| ZX_RIGHT_GET_PROPERTY
| ZX_RIGHT_SET_PROPERTY
| ZX_RIGHT_MAP
| ZX_RIGHT_SIGNAL
| ZX_RIGHT_SIGNAL_PEER;
const ALLOWED_SIGNALS: u32 = ZX_USER_SIGNAL_ALL;
zr::static_assert_size_and_align!(
IoBufferDispatcherState,
kIoBufferDispatcherStateSize,
kIoBufferDispatcherStateAlign,
);
define_kcounter!(DISPATCHER_IOB_CREATE_COUNT, "dispatcher.iob.create", Sum);
define_kcounter!(DISPATCHER_IOB_DESTROY_COUNT, "dispatcher.iob.destroy", Sum);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(usize)]
enum IobEndpointId {
Ep0 = 0,
Ep1 = 1,
}
impl IobEndpointId {
fn other(self) -> Self {
match self {
Self::Ep0 => Self::Ep1,
Self::Ep1 => Self::Ep0,
}
}
}
/// Represents an IOBuffer memory region and its endpoint-specific child VMOs and kernel mapping.
struct IobRegion {
// A child VMO reference for each endpoint so that they can individually be notified of maps
// and unmaps.
ep_vmos: [RefPtr<VmObject>; 2],
// A mapping of the region into kernel space, or None if the region does not permit mediated
// access.
mapping: Option<RefPtr<VmMapping>>,
// The base address of the mapping.
base: VAddr,
region: zx_iob_region_t,
vmo_user_id: zx_koid_t,
}
impl IobRegion {
fn new(
ep0_vmo: RefPtr<VmObject>,
ep1_vmo: RefPtr<VmObject>,
mapping: Option<RefPtr<VmMapping>>,
base: VAddr,
region: &zx_iob_region_t,
vmo_user_id: zx_koid_t,
) -> Self {
Self { ep_vmos: [ep0_vmo, ep1_vmo], mapping, base, region: *region, vmo_user_id }
}
/// Calculates the map rights for a given endpoint based on region access flags.
fn get_map_rights(&self, id: IobEndpointId) -> zx_rights_t {
let (read_flag, write_flag) = match id {
IobEndpointId::Ep0 => (ZX_IOB_ACCESS_EP0_CAN_MAP_READ, ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE),
IobEndpointId::Ep1 => (ZX_IOB_ACCESS_EP1_CAN_MAP_READ, ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE),
};
let mut rights = 0;
if (self.region.access & write_flag) != 0 {
rights |= ZX_RIGHT_WRITE | ZX_RIGHT_MAP;
}
if (self.region.access & read_flag) != 0 {
rights |= ZX_RIGHT_READ | ZX_RIGHT_MAP;
}
rights
}
/// Calculates the mediated access rights for a given endpoint based on region access flags.
fn get_mediated_rights(&self, id: IobEndpointId) -> zx_rights_t {
let (read_flag, write_flag) = match id {
IobEndpointId::Ep0 => {
(ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ, ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE)
}
IobEndpointId::Ep1 => {
(ZX_IOB_ACCESS_EP1_CAN_MEDIATED_READ, ZX_IOB_ACCESS_EP1_CAN_MEDIATED_WRITE)
}
};
let mut rights = 0;
if (self.region.access & write_flag) != 0 {
rights |= ZX_RIGHT_WRITE;
}
if (self.region.access & read_flag) != 0 {
rights |= ZX_RIGHT_READ;
}
rights
}
/// Returns the child VMO reference for the specified endpoint.
fn get_vmo(&self, id: IobEndpointId) -> &RefPtr<VmObject> {
&self.ep_vmos[id as usize]
}
/// Returns the `zx_iob_region_info_t` for this region, swapping endpoint access flags if
/// querying from endpoint 1.
fn get_region_info(&self, swap_endpoints: bool) -> zx_iob_region_info_t {
let mut info = zx_iob_region_info_t { region: self.region, koid: self.vmo_user_id };
if swap_endpoints {
let ep0_access = info.region.access
& (ZX_IOB_ACCESS_EP0_CAN_MAP_READ
| ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE
| ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ
| ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE);
let ep1_access = info.region.access
& (ZX_IOB_ACCESS_EP1_CAN_MAP_READ
| ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE
| ZX_IOB_ACCESS_EP1_CAN_MEDIATED_READ
| ZX_IOB_ACCESS_EP1_CAN_MEDIATED_WRITE);
info.region.access = (ep0_access << 4) | (ep1_access >> 4);
}
info
}
/// Debug asserts that the discipline is of the expected type, intended as a guardrail for use
/// in discipline-specific subclasses.
#[inline(always)]
fn assert_discipline(&self, discipline_type: u64) {
debug_assert_eq!(self.region.discipline.r#type, discipline_type);
}
}
impl Drop for IobRegion {
fn drop(&mut self) {
if let Some(mapping) = &self.mapping {
let _ = mapping.destroy();
}
}
}
/// Represents `ZX_IOB_DISCIPLINE_TYPE_NONE`.
type IobRegionNone = IobRegion;
/// Represents `ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR`.
struct IobRegionIdAllocator {
region: IobRegion,
_pin: Option<PinnedVmObject>,
}
impl IobRegionIdAllocator {
/// When mediated access is requested, this initializes the ID allocator container and pins and
/// maps the whole region mapping for reliable future access.
fn new(
ep0_vmo: RefPtr<VmObject>,
ep1_vmo: RefPtr<VmObject>,
mapping: Option<RefPtr<VmMapping>>,
base: VAddr,
region: &zx_iob_region_t,
vmo_user_id: zx_koid_t,
) -> Result<Self, Status> {
let pin = if let Some(mapping) = &mapping {
let vmo = mapping.vmo().expect("mapping must have a VMO");
let pin = PinnedVmObject::create(vmo, 0, region.size, true)?;
mapping.map_range(0, region.size as usize, true, false)?;
// A fresh VMO, so already zero-filled.
let slice =
unsafe { slice::from_raw_parts_mut(base.0 as *mut u8, region.size as usize) };
BlobIdAllocator::init_from_slice(slice, ZeroFill::No);
Some(pin)
} else {
None
};
let this = Self {
region: IobRegion::new(ep0_vmo, ep1_vmo, mapping, base, region, vmo_user_id),
_pin: pin,
};
this.region.assert_discipline(ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR);
Ok(this)
}
/// Returns a view into the whole kernel-mapped region.
fn mediated_bytes(&self) -> &'static [u8] {
if self.region.base.0 == 0 {
return &[];
}
// SAFETY: The region is mapped and pinned in the kernel address space for the
// lifetime of the allocator.
unsafe {
slice::from_raw_parts(self.region.base.0 as *const u8, self.region.region.size as usize)
}
}
/// Allocates a blob ID within the ID allocator region.
fn allocate_id(
&self,
id: IobEndpointId,
blob: UserInPtr<u8>,
blob_size: usize,
) -> Result<u32, Status> {
self.region.assert_discipline(ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR);
let mediated_rights = self.region.get_mediated_rights(id);
if (mediated_rights & ZX_RIGHT_WRITE) == 0 {
return Err(Status::ACCESS_DENIED);
}
let allocator = BlobIdAllocator::from_slice(self.mediated_bytes());
allocator
.allocate_with(blob_size, |dest| blob.copy_slice_from_user(dest).map(|_| ()))
.map_err(Into::into)
}
}
/// Represents `ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER`.
struct IobRegionMediatedWriteRingBuffer {
region: IobRegion,
shared_region: RefPtr<IoBufferSharedRegionDispatcher>,
tag: u64,
}
impl IobRegionMediatedWriteRingBuffer {
fn new(
ep0_vmo: RefPtr<VmObject>,
ep1_vmo: RefPtr<VmObject>,
region: &zx_iob_region_t,
vmo_user_id: zx_koid_t,
shared_region: RefPtr<IoBufferSharedRegionDispatcher>,
tag: u64,
) -> Self {
let this = Self {
region: IobRegion::new(ep0_vmo, ep1_vmo, None, VAddr(0), region, vmo_user_id),
shared_region,
tag,
};
this.region.assert_discipline(ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER);
this
}
/// Performs a mediated write to the shared ring buffer region.
fn write(
&self,
id: IobEndpointId,
vector: UserInPtr<zx_iovec_t>,
vector_count: usize,
) -> Result<(), Status> {
self.region.assert_discipline(ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER);
let mediated_rights = self.region.get_mediated_rights(id);
if (mediated_rights & ZX_RIGHT_WRITE) == 0 {
return Err(Status::ACCESS_DENIED);
}
self.shared_region.write(self.tag, vector, vector_count)
}
}
/// Enum representing the supported IOBuffer region disciplines.
enum IobRegionVariant {
None(IobRegionNone),
IdAllocator(IobRegionIdAllocator),
MediatedWriteRingBuffer(IobRegionMediatedWriteRingBuffer),
}
impl IobRegionVariant {
fn base_region(&self) -> &IobRegion {
match self {
Self::None(r) => r,
Self::IdAllocator(r) => &r.region,
Self::MediatedWriteRingBuffer(r) => &r.region,
}
}
fn get_map_rights(&self, id: IobEndpointId) -> zx_rights_t {
self.base_region().get_map_rights(id)
}
fn get_mediated_rights(&self, id: IobEndpointId) -> zx_rights_t {
self.base_region().get_mediated_rights(id)
}
fn get_vmo(&self, id: IobEndpointId) -> &RefPtr<VmObject> {
self.base_region().get_vmo(id)
}
fn get_region_info(&self, swap_endpoints: bool) -> zx_iob_region_info_t {
self.base_region().get_region_info(swap_endpoints)
}
fn region(&self) -> zx_iob_region_t {
self.base_region().region
}
fn allocate_id(
&self,
id: IobEndpointId,
blob: UserInPtr<u8>,
blob_size: usize,
) -> Result<u32, Status> {
match self {
Self::IdAllocator(allocator) => allocator.allocate_id(id, blob, blob_size),
_ => Err(Status::WRONG_TYPE),
}
}
fn write(
&self,
id: IobEndpointId,
vector: UserInPtr<zx_iovec_t>,
vector_count: usize,
) -> Result<(), Status> {
match self {
Self::MediatedWriteRingBuffer(rb) => rb.write(id, vector, vector_count),
_ => Err(Status::WRONG_TYPE),
}
}
}
/// Wrapper struct to allow both peers to hold a reference to the regions.
#[ref_counted]
#[pin_data]
#[derive(Recyclable)]
#[repr(C)]
struct SharedIobState {
regions: Array<IobRegionVariant>,
}
impl SharedIobState {
fn create(regions: Array<IobRegionVariant>) -> Result<RefPtr<Self>, AllocError> {
pin_make_ref_counted!(Self { regions })
}
fn get_region(&self, index: usize) -> Result<&IobRegionVariant, Status> {
self.regions.get(index).ok_or(Status::OUT_OF_RANGE)
}
}
#[guarded]
#[pin_data(PinnedDrop)]
#[repr(C)]
pub struct IoBufferDispatcherState {
canary: Canary<{ fbl::magic(b"IOBD") }>,
#[pin]
pub peered: PeeredState<IoBufferDispatcher>,
shared_state: RefPtr<SharedIobState>,
endpoint_id: IobEndpointId,
// Number of regions currently mapped by the peer endpoint.
#[guarded_by(mu)]
peer_mapped_regions: usize,
// Whether the peer endpoint has closed all of its handles.
#[guarded_by(mu)]
peer_zero_handles: bool,
#[mutex(PeerHolderMuClass<IoBufferDispatcher>)]
pub mu: KMutex<PhantomMutex>,
}
impl IoBufferDispatcherState {
pub fn init(
holder: RefPtr<PeerHolder<IoBufferDispatcher>>,
endpoint_id: usize,
shared_state: *mut (),
) -> impl PinInit<Self, Infallible> {
DISPATCHER_IOB_CREATE_COUNT.add(1);
let endpoint_id = if endpoint_id == 0 { IobEndpointId::Ep0 } else { IobEndpointId::Ep1 };
// SAFETY: `shared_state` is a valid `SharedIobState` pointer passed via `RefPtr::into_raw`.
let shared_state = unsafe { RefPtr::from_raw(shared_state as *mut SharedIobState) };
pin_init!(Self {
canary: Canary::new(),
peered <- PeeredState::init(holder),
shared_state,
endpoint_id,
peer_mapped_regions: 0.into(),
peer_zero_handles: false.into(),
mu: KMutex::new(PhantomMutex),
})
}
}
#[pinned_drop]
impl PinnedDrop for IoBufferDispatcherState {
fn drop(self: Pin<&mut Self>) {
DISPATCHER_IOB_DESTROY_COUNT.add(1);
let this = self.project();
let other_id = this.endpoint_id.other();
// The other endpoint's VMOs are set up to notify this endpoint when they map regions via a
// raw pointer (`VmObjectChildObserver*`). Since we're about to be destroyed, we need to
// unregister.
//
// `VmObjectDispatcher` unregisters when the last handle is closed (`on_zero_handles`),
// but we do it differently here in the destructor.
//
// If we were to unregister in `on_peer_zero_handles_locked`, updating the child observer
// would establish the lock ordering `dispatcher.lock -> child_observer_lock`. However,
// when `OnZeroChild` callback is invoked from the VMO subsystem, it first acquires
// `child_observer_lock` and then acquires `dispatcher.lock`, creating an ABBA lock cycle
// (`child_observer_lock -> dispatcher.lock`).
//
// Cleaning up in the destructor avoids this lock inversion, but means that a child
// notification could potentially occur while we are being destroyed. This is safe because:
// - Each VMO's child observer is protected by its internal `child_observer_lock`.
// - While we are in this destructor, a concurrent notification may attempt to acquire
// the VMO's `child_observer_lock`.
// - If we have already cleared the observer, the notifier will observe `nullptr` and
// will not access our state.
// - If we have not yet cleared the observer, it will notify us while continuing to hold
// the `child_observer_lock`.
// - Calling `set_child_observer` will block until any in-flight notification on that VMO
// completes and releases the lock.
// - Thus, destruction cannot proceed past this loop until all in-progress notifications
// have finished, and no subsequent observer callback can access our state.
for region in this.shared_state.regions.iter() {
let vmo = region.get_vmo(other_id);
// SAFETY: `vmo` is a valid `RefPtr<VmObject>` owned by `SharedIobState`. Passing
// `null_mut()` unregisters the observer safely while synchronizing with the VMO's
// internal `child_observer_lock`, ensuring no further callbacks dereference this
// dispatcher.
unsafe {
vmo.set_child_observer(ptr::null_mut());
}
}
}
}
// An IOBuffer is a memory container for interacting with drivers and other hardware-adjacent
// software.
//
// IOBuffer memory is divided into one or more regions, each of which represents a single VMO
// that is either private to the IOBuffer or shared with another.
//
// Like channels and sockets, IOBuffers are peered objects; each endpoint represents one side
// of the memory conversation.
impl_peered_dispatcher_facade_with_state!(
pub struct IoBufferDispatcher,
IoBufferDispatcherState,
ZX_OBJ_TYPE_IOB,
kIoBufferDispatcherStateOffset,
allowed_signals: ALLOWED_SIGNALS,
);
impl IoBufferDispatcher {
pub fn default_rights() -> zx_rights_t {
DEFAULT_RIGHTS
}
/// Performs the C++ multiple-inheritance upcast from `IoBufferDispatcher*` to
/// `VmObjectChildObserver*`.
pub fn as_child_observer(&self) -> *mut VmObjectChildObserver {
unsafe { cpp_io_buffer_dispatcher_as_child_observer(self) }
}
/// Creates an endpoint pair of `IoBufferDispatcher` objects.
pub fn create(
options: u64,
region_configs: &[zx_iob_region_t],
) -> Result<(KernelHandle<Self>, KernelHandle<Self>, zx_rights_t), Status> {
if options != 0 || region_configs.is_empty() || region_configs.len() > ZX_IOB_MAX_REGIONS {
return Err(Status::INVALID_ARGS);
}
let holder0 = PeerHolder::<Self>::create().map_err(|_| Status::NO_MEMORY)?;
let holder1 = holder0.clone();
let regions = Self::create_regions(region_configs)?;
let shared_state = SharedIobState::create(regions).map_err(|_| Status::NO_MEMORY)?;
let create_single = |holder: RefPtr<PeerHolder<Self>>,
endpoint_id: IobEndpointId,
shared_state: RefPtr<SharedIobState>|
-> Result<KernelHandle<Self>, Status> {
// SAFETY: `holder` and `shared_state` transfer acquired refcounts, and
// `cpp_io_buffer_dispatcher_create` initializes `handle` on success.
unsafe {
KernelHandle::create(|out| {
cpp_io_buffer_dispatcher_create(
RefPtr::into_raw(holder) as *mut _,
endpoint_id as usize,
RefPtr::into_raw(shared_state) as *mut _,
out,
)
})
}
};
let handle0 = create_single(holder0, IobEndpointId::Ep0, shared_state.clone())?;
let handle1 = create_single(holder1, IobEndpointId::Ep1, shared_state.clone())?;
// Now each endpoint can observe the mappings created by the other.
for region in shared_state.regions.iter() {
// SAFETY: `handle0` and `handle1` are valid, initialized dispatchers whose
// `as_child_observer` upcasts yield valid `VmObjectChildObserver` pointers that
// remain valid until unregistered during destruction.
unsafe {
region
.get_vmo(IobEndpointId::Ep0)
.set_child_observer(handle1.dispatcher().as_child_observer());
region
.get_vmo(IobEndpointId::Ep1)
.set_child_observer(handle0.dispatcher().as_child_observer());
}
}
handle0.dispatcher().init_peer(handle1.dispatcher().clone());
handle1.dispatcher().init_peer(handle0.dispatcher().clone());
Ok((handle0, handle1, DEFAULT_RIGHTS))
}
fn create_regions(
region_configs: &[zx_iob_region_t],
) -> Result<Array<IobRegionVariant>, Status> {
let mut regions = Box::<[IobRegionVariant]>::try_new_uninit_slice(region_configs.len())
.map_err(|_| Status::NO_MEMORY)?;
for (i, config) in region_configs.iter().enumerate() {
let mut region_config = *config;
let (vmo, vmo_user_id, dispatcher) = match region_config.r#type {
ZX_IOB_REGION_TYPE_PRIVATE => {
let options = unsafe { region_config.extension.private_region.options };
let stats = VmObjectDispatcher::parse_create_syscall_flags(
options,
region_config.size,
)?;
let created_vmo = VmObjectPaged::create(
pmm::ALLOC_FLAG_ANY | pmm::ALLOC_FLAG_CAN_WAIT,
stats.flags,
stats.size,
)?;
// parse_create_syscall_flags will round up the size to the nearest page, or set
// the size to the maximum possible VMO size if ZX_VMO_UNBOUNDED is used. We
// need to know the actual size of the VMO to later return if asked.
region_config.size = stats.size;
let vmo_user_id = koid::generate();
created_vmo.set_user_id(vmo_user_id);
let vmo = VmObjectPaged::into_vm_object(created_vmo);
(vmo, vmo_user_id, None)
}
ZX_IOB_REGION_TYPE_SHARED => {
// TODO(https://fxbug.dev/319500512): Remove the cast when we move it out of
// vdso next.
let shared_region = unsafe { region_config.extension.shared_region };
if shared_region.options != 0 || region_config.size != 0 {
return Err(Status::INVALID_ARGS);
}
let sr = Dispatcher::get_with_rights::<IoBufferSharedRegionDispatcher>(
HandleValue::new(shared_region.shared_region),
ZX_RIGHT_NONE,
)?;
let vmo = sr.vmo();
let paged_vmo =
VmObject::downcast_paged(vmo.clone()).ok_or(Status::INVALID_ARGS)?;
if paged_vmo.size() < (page::SIZE as u64) * 2 {
return Err(Status::INVALID_ARGS);
}
// For memory attribution to work correctly, we need to use the same KOID for
// all IOBuffers that use this shared region.
let vmo_user_id = sr.get_koid();
(vmo, vmo_user_id, Some(sr))
}
_ => return Err(Status::INVALID_ARGS),
};
// A note on resource management:
//
// Everything we allocate in this loop ultimately gets owned by the SharedIobState and
// will be cleaned up when both PeeredDispatchers are destroyed and drop their reference
// to the SharedIobState.
//
// However, there is a complication. The VmoChildObservers have a reference to the
// dispatchers which creates a cycle: IoBufferDispatcher -> SharedIobState -> IobRegion
// -> VmObject -> VmoChildObserver -> IoBufferDispatcher.
//
// Since the VmoChildObservers keep raw pointers to the dispatchers, we need to be sure
// to reset the corresponding pointers when we destroy an IoBufferDispatcher. Otherwise
// when an IoBufferDispatcher maps a region, it could try to update a destroyed peer.
//
// See: PinnedDrop for IoBufferDispatcherState.
// We effectively duplicate the logic from sys_vmo_create here, but instead of creating
// a kernel handle and dispatcher, we keep ownership of it and assign it to a region.
// In order to track mappings and unmappings separately for each endpoint, we give each
// endpoint a child reference instead of the created VMO.
let resizability = if vmo.is_resizable() {
Resizability::Resizable
} else {
Resizability::NonResizable
};
let (ep0_reference, _) = vmo.create_child_reference(resizability, 0, 0, true)?;
let (ep1_reference, _) = vmo.create_child_reference(resizability, 0, 0, true)?;
ep0_reference.set_user_id(vmo_user_id);
ep1_reference.set_user_id(vmo_user_id);
let variant = Self::create_iob_region_variant(
ep0_reference,
ep1_reference,
vmo,
&region_config,
vmo_user_id,
dispatcher,
)?;
regions[i].write(variant);
}
Ok(Array::from_box(unsafe { regions.assume_init() }))
}
fn create_iob_region_variant(
ep0_vmo: RefPtr<VmObject>,
ep1_vmo: RefPtr<VmObject>,
kernel_vmo: RefPtr<VmObject>,
region: &zx_iob_region_t,
vmo_user_id: zx_koid_t,
shared_region: Option<RefPtr<IoBufferSharedRegionDispatcher>>,
) -> Result<IobRegionVariant, Status> {
let mediated0 = region.access
& (ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ | ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE);
let mediated1 = region.access
& (ZX_IOB_ACCESS_EP1_CAN_MEDIATED_READ | ZX_IOB_ACCESS_EP1_CAN_MEDIATED_WRITE);
let mediated = mediated0 != 0 || mediated1 != 0;
let map_writable = (region.access & ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE) != 0
|| (region.access & ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE) != 0;
// The region memory must be directly accessible by userspace or the kernel.
if !map_writable && !mediated {
return Err(Status::INVALID_ARGS);
}
match region.discipline.r#type {
ZX_IOB_DISCIPLINE_TYPE_NONE => {
// For now, disallow shared regions. This can be relaxed as and when need arises.
if shared_region.is_some() {
return Err(Status::INVALID_ARGS);
}
if mediated {
// NONE type discipline does not support mediated access.
return Err(Status::INVALID_ARGS);
}
Ok(IobRegionVariant::None(IobRegion::new(
ep0_vmo,
ep1_vmo,
None,
VAddr(0),
region,
vmo_user_id,
)))
}
ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR => {
// For now, disallow shared regions. This can be relaxed as and when need arises.
if shared_region.is_some() {
return Err(Status::INVALID_ARGS);
}
// If an ID allocator endpoint requests mediated access, that access must at
// least admit mediated writes. Equivalently, no ID allocator endpoint can
// be read-only in terms of mediated access.
if mediated0 == ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ
|| mediated1 == ZX_IOB_ACCESS_EP1_CAN_MEDIATED_READ
{
return Err(Status::INVALID_ARGS);
}
// Create a mapping of this VMO in the kernel aspace. It is convenient to
// create the mapping object first before any discipline-specific pinning
// and mapping is actually performed, allowing that to be done in
// subclasses, so we pass `DEBUG_DYNAMIC_KERNEL_MAPPING`.
let kernel_vmar = VmAspace::kernel_aspace()
.root_vmar()
.expect("kernel aspace root VMAR must be initialized");
let map_result = kernel_vmar.create_vm_mapping(
0,
region.size as usize,
0,
DEBUG_DYNAMIC_KERNEL_MAPPING,
kernel_vmo,
0,
ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE,
c"IOBuffer region (ID allocator)",
)?;
let allocator = IobRegionIdAllocator::new(
ep0_vmo,
ep1_vmo,
Some(map_result.mapping),
VAddr(map_result.base),
region,
vmo_user_id,
)?;
Ok(IobRegionVariant::IdAllocator(allocator))
}
ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER => {
// For now, insist upon a shared region. This can be relaxed as and when need
// arises.
let shared_region = shared_region.ok_or(Status::INVALID_ARGS)?;
// TODO(https://fxbug.dev/319500512): Remove the cast when we move it out of vdso
// next.
let tag = unsafe { region.discipline.extension.ring_buffer.tag };
Ok(IobRegionVariant::MediatedWriteRingBuffer(
IobRegionMediatedWriteRingBuffer::new(
ep0_vmo,
ep1_vmo,
region,
vmo_user_id,
shared_region,
tag,
),
))
}
_ => {
// Unknown discipline.
Err(Status::INVALID_ARGS)
}
}
}
/// Returns the number of regions configured in this IOBuffer.
pub fn region_count(&self) -> usize {
self.state().canary.assert();
self.state().shared_state.regions.len()
}
/// Returns a `zx_info_iob_t` struct containing information about the IOBuffer.
pub fn get_info(&self) -> zx_info_iob_t {
self.state().canary.assert();
zx_info_iob_t { options: 0, region_count: self.region_count() as u32, ..Default::default() }
}
/// Returns a `zx_iob_region_info_t` struct for the region at the given index.
pub fn get_region_info(&self, index: usize) -> zx_iob_region_info_t {
self.state().canary.assert();
debug_assert!(index < self.state().shared_state.regions.len());
self.state().shared_state.regions[index]
.get_region_info(self.state().endpoint_id == IobEndpointId::Ep1)
}
/// Returns the region configuration at the given index.
pub fn get_region(&self, region_index: usize) -> Result<zx_iob_region_t, Status> {
self.state().canary.assert();
self.state().shared_state.get_region(region_index).map(|r| r.region())
}
/// Returns the child VMO reference for the specified region and this endpoint.
pub fn get_vmo(&self, region_index: usize) -> Result<RefPtr<VmObject>, Status> {
self.state().canary.assert();
self.state()
.shared_state
.get_region(region_index)
.map(|r| r.get_vmo(self.state().endpoint_id).clone())
}
/// Returns the map rights available for the given region, masked by the caller's IOBuffer
/// rights.
pub fn get_map_rights(&self, iob_rights: zx_rights_t, region_index: usize) -> zx_rights_t {
self.state().canary.assert();
self.state()
.shared_state
.get_region(region_index)
.map_or(0, |r| iob_rights & r.get_map_rights(self.state().endpoint_id))
}
/// Returns the mediated access rights for the given region and this endpoint.
pub fn get_mediated_rights(&self, region_index: usize) -> zx_rights_t {
self.state().canary.assert();
self.state()
.shared_state
.get_region(region_index)
.map_or(0, |r| r.get_mediated_rights(self.state().endpoint_id))
}
/// Creates a child reference to the region's VMO that can be mapped by userspace.
pub fn create_mappable_vmo_for_region(
&self,
region_index: usize,
) -> Result<RefPtr<VmObject>, Status> {
self.state().canary.assert();
let vmo = self.get_vmo(region_index)?;
let resizability =
if vmo.is_resizable() { Resizability::Resizable } else { Resizability::NonResizable };
let (child_reference, first_child) =
vmo.create_child_reference(resizability, 0, 0, true)?;
if first_child {
// Need to record 0->1 transition. As we are holding a RefPtr to the child we know that
// OnZeroChildren cannot get called to decrement before we can increment. In this case
// the lock is not attempting to synchronize anything beyond the raw access of
// peer_mapped_regions.
ksync::lock!(let mut guard = self.state().peered.lock());
if let Some(peer) = guard.peer().clone() {
*peer.state().guard_mu_mut(guard.as_mut().token_mut()).peer_mapped_regions_mut() +=
1;
}
}
child_reference.set_user_id(vmo.user_id());
Ok(child_reference)
}
/// Attempts to allocate a sequential ID for a blob within an ID allocator region.
pub fn allocate_id(
&self,
region_index: u32,
blob: UserInPtr<u8>,
blob_size: usize,
) -> Result<u32, Status> {
self.state().canary.assert();
self.state().shared_state.get_region(region_index as usize)?.allocate_id(
self.state().endpoint_id,
blob,
blob_size,
)
}
/// Performs a mediated write to the region.
pub fn write(
&self,
region_index: u32,
vector: UserInPtr<zx_iovec_t>,
vector_count: usize,
) -> Result<(), Status> {
self.state().canary.assert();
self.state().shared_state.get_region(region_index as usize)?.write(
self.state().endpoint_id,
vector,
vector_count,
)
}
/// Called by `VmObjectChildObserver` when child VMO references drop to zero.
pub fn on_zero_child(&self) {
self.state().canary.assert();
ksync::lock!(let mut guard = self.state().peered.lock());
let (is_zero, peer_zero_handles) = {
let mut state = self.state().guard_mu_mut(guard.as_mut().token_mut());
let peer_mapped_regions = state.peer_mapped_regions_mut();
// OnZeroChildren gets called every time the number of children is equal to zero. Due to
// races, by the time this method is called we could already have children again. This
// is fine, since all we need to do is ensure that every increment of
// peer_mapped_regions_ is paired with a decrement.
debug_assert!(*peer_mapped_regions > 0);
*peer_mapped_regions -= 1;
(*peer_mapped_regions == 0, *state.peer_zero_handles())
};
// If there are no more mapped regions, and the peer has closed its handles, then we
// can set the peer closed signal.
if is_zero && peer_zero_handles {
self.update_state_locked(guard.token(), 0, ZX_IOB_PEER_CLOSED);
}
}
/// Called when the peer endpoint closes its last handle.
pub fn on_peer_zero_handles_locked(
&self,
token: &mut LockToken<'_, PeerHolderMuClass<IoBufferDispatcher>>,
) {
self.state().canary.assert();
let is_zero = {
let mut state = self.state().guard_mu_mut(token);
*state.peer_zero_handles_mut() = true;
*state.peer_mapped_regions() == 0
};
// If there are no mapped regions we are already closed and need to update state, otherwise
// we need to wait for on_zero_child.
if is_zero {
self.update_state_locked(token, 0, ZX_IOB_PEER_CLOSED);
}
}
pub fn on_zero_handles_locked(
&self,
_token: &LockToken<'_, PeerHolderMuClass<IoBufferDispatcher>>,
) {
self.state().canary.assert();
}
/// Gets the name of the underlying VMO in the first region.
pub fn get_name(&self, out_name: &mut [u8; ZX_MAX_NAME_LEN]) -> Result<(), Status> {
self.state().canary.assert();
let region =
self.state().shared_state.regions.first().expect("IoBuffer has at least one region");
let vmo = region.get_vmo(IobEndpointId::Ep0);
vmo.get_name(out_name);
Ok(())
}
/// Sets the name on all private region VMOs.
pub fn set_name(&self, name: &[u8]) -> Result<(), Status> {
self.state().canary.assert();
for region in self.state().shared_state.regions.iter() {
match region {
IobRegionVariant::None(r)
| IobRegionVariant::IdAllocator(IobRegionIdAllocator { region: r, .. }) => {
if r.region.r#type == ZX_IOB_REGION_TYPE_PRIVATE {
r.get_vmo(IobEndpointId::Ep0).set_name(name)?;
r.get_vmo(IobEndpointId::Ep1).set_name(name)?;
}
}
_ => continue,
}
}
Ok(())
}
}
/// In-tree kernel unit tests for `IoBufferDispatcher`.
#[cfg(ktest)]
#[unittest::suite(name = "io_buffer_dispatcher_rust_tests")]
mod tests {
use super::{IoBufferDispatcher, IoBufferSharedRegionDispatcher, IobEndpointId};
use crate::user_copy::UserInPtr;
use crate::user_memory::UserMemory;
use crate::vm::pmm::ALLOC_FLAG_ANY;
use crate::vm::vm_object_paged::VmObjectPaged;
use core::{mem, ptr, slice};
use iob::{BlobIdAllocator, Header, Index, ZeroFill};
use kalloc::Box;
use page;
use unittest::{expect_eq, expect_nonnull, expect_ok, expect_true};
use zx_status::Status;
use zx_types::{
ZX_IOB_ACCESS_EP0_CAN_MAP_READ, ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE,
ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ, ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE,
ZX_IOB_ACCESS_EP1_CAN_MAP_READ, ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE,
ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR, ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER,
ZX_IOB_DISCIPLINE_TYPE_NONE, ZX_IOB_PEER_CLOSED, ZX_IOB_REGION_TYPE_PRIVATE,
ZX_IOB_REGION_TYPE_SHARED, ZX_MAX_NAME_LEN, ZX_RIGHT_MAP, ZX_RIGHT_READ, ZX_RIGHT_WRITE,
zx_iob_region_t, zx_iovec_t,
};
fn make_user_in(data: &[u8]) -> Option<(UserMemory, UserInPtr<u8>)> {
let alloc_size = if data.is_empty() { 1 } else { data.len() };
let mem = UserMemory::create(alloc_size)?;
mem.commit_and_map(0..alloc_size).ok()?;
if !data.is_empty() {
mem.vmo_write(data, 0).ok()?;
}
let ptr = UserInPtr::new(mem.base() as *const u8);
Some((mem, ptr))
}
fn make_user_in_iovec(iovecs: &[zx_iovec_t]) -> Option<(UserMemory, UserInPtr<zx_iovec_t>)> {
let size = mem::size_of_val(iovecs);
let alloc_size = if size == 0 { 1 } else { size };
let mem = UserMemory::create(alloc_size)?;
mem.commit_and_map(0..alloc_size).ok()?;
if !iovecs.is_empty() {
let bytes = unsafe { slice::from_raw_parts(iovecs.as_ptr().cast(), size) };
mem.vmo_write(bytes, 0).ok()?;
}
let ptr = UserInPtr::new(mem.base() as *const zx_iovec_t);
Some((mem, ptr))
}
/// Allocate/destroy many iobuffers. Ad hoc resource leak check.
#[test]
fn test_create_destroy_many_io_buffers() {
const MANY: usize = 10_000;
for region_count in [1, 7, 64] {
let mut regions = Box::<[zx_iob_region_t]>::try_new_uninit_slice(region_count)
.expect("allocation failed");
for uninit_region in regions.iter_mut() {
let mut region = unsafe { mem::zeroed::<zx_iob_region_t>() };
region.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
region.access = ZX_IOB_ACCESS_EP0_CAN_MAP_READ | ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE;
region.size = page::SIZE as u64;
region.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_NONE;
uninit_region.write(region);
}
let regions = unsafe { regions.assume_init() };
for _ in 0..MANY {
expect_ok!(IoBufferDispatcher::create(0, &regions).map(|_| ()));
}
}
}
/// Tests Header and Index conversions and bounds checking.
#[test]
fn test_blob_id_allocator_header_and_index() {
// Test Header conversion
let hdr = Header { next_id: 42, blob_head: 4096 };
let raw = hdr.to_u64();
let decoded = Header::from_u64(raw);
expect_true!(hdr == decoded);
// Test index_end
expect_true!(Header { next_id: 0, blob_head: 4096 }.index_end() == Some(8));
expect_true!(Header { next_id: 1, blob_head: 4096 }.index_end() == Some(16));
expect_true!(Header { next_id: 5, blob_head: 4096 }.index_end() == Some(48));
const MAX_NEXT_ID: u32 = (u32::MAX - 8) / 8;
expect_true!(
Header { next_id: MAX_NEXT_ID, blob_head: 4096 }.index_end()
== Some(8 + MAX_NEXT_ID * 8)
);
expect_true!(Header { next_id: MAX_NEXT_ID + 1, blob_head: 4096 }.index_end().is_none());
// Test remaining_bytes
expect_true!(
Header { next_id: 0, blob_head: 1024 }.remaining_bytes(1024) == Some(1024 - 8)
);
expect_true!(
Header { next_id: 1, blob_head: 1000 }.remaining_bytes(1024) == Some(1000 - 16)
);
// Boundary where blob_head == index_end (0 remaining bytes)
expect_true!(Header { next_id: 0, blob_head: 8 }.remaining_bytes(8) == Some(0));
// Overlapping index and blobs
expect_true!(Header { next_id: 1, blob_head: 15 }.remaining_bytes(1024).is_none());
// Blob head past length
expect_true!(Header { next_id: 0, blob_head: 2000 }.remaining_bytes(1024).is_none());
// Test Index conversion
let idx = Index { size: 100, offset: 500 };
let raw_idx = idx.to_u64();
expect_eq!(raw_idx, 100 | (500u64 << 32));
}
/// Tests BlobIdAllocator allocation sequence and index table layout.
#[test]
fn test_blob_id_allocator_allocation_and_index_layout() {
const BUF_SIZE: usize = 1024;
let mut buf = Box::<[u64]>::try_new_zeroed_slice(BUF_SIZE / 8).expect("allocation failed");
let base = buf.as_mut_ptr() as usize;
let slice = unsafe { slice::from_raw_parts_mut(base as *mut u8, BUF_SIZE) };
let alloc = BlobIdAllocator::init_from_slice(slice, ZeroFill::Yes);
let blob0_data = [0xAAu8; 16];
let (_mem0, ptr0) = make_user_in(&blob0_data).expect("user mem 0");
let id0 = alloc
.allocate_with(blob0_data.len(), |dest| ptr0.copy_slice_from_user(dest).map(|_| ()))
.expect("allocate blob 0");
expect_eq!(id0, 0);
let blob1_data = [0xBBu8; 32];
let (_mem1, ptr1) = make_user_in(&blob1_data).expect("user mem 1");
let id1 = alloc
.allocate_with(blob1_data.len(), |dest| ptr1.copy_slice_from_user(dest).map(|_| ()))
.expect("allocate blob 1");
expect_eq!(id1, 1);
// Verify written data in buffer
let offset0 = BUF_SIZE - 16;
let offset1 = offset0 - 32;
unsafe {
let slice0 = slice::from_raw_parts((base + offset0) as *const u8, 16);
expect_true!(slice0 == &blob0_data[..]);
let slice1 = slice::from_raw_parts((base + offset1) as *const u8, 32);
expect_true!(slice1 == &blob1_data[..]);
// Verify index entries
let idx0_raw = ptr::read_volatile((base + 8) as *const u64);
expect_eq!(idx0_raw, 16u64 | ((offset0 as u64) << 32));
let idx1_raw = ptr::read_volatile((base + 16) as *const u64);
expect_eq!(idx1_raw, 32u64 | ((offset1 as u64) << 32));
}
}
/// Tests BlobIdAllocator out of memory conditions.
#[test]
fn test_blob_id_allocator_out_of_memory() {
const BUF_SIZE: usize = 64;
let mut buf = Box::<[u64]>::try_new_zeroed_slice(BUF_SIZE / 8).expect("allocation failed");
let base = buf.as_mut_ptr() as usize;
let slice = unsafe { slice::from_raw_parts_mut(base as *mut u8, BUF_SIZE) };
let alloc = BlobIdAllocator::init_from_slice(slice, ZeroFill::No);
// Buffer is 64 bytes. Header is 8 bytes. Index 0 is 8 bytes.
// That leaves 48 bytes for data.
let blob_data = [1u8; 48];
let (_mem, ptr) = make_user_in(&blob_data).expect("user mem");
let id = alloc
.allocate_with(48, |dest| ptr.copy_slice_from_user(dest).map(|_| ()))
.expect("allocate 48 bytes");
expect_eq!(id, 0);
// Now buffer has 0 remaining bytes. Allocating 1 byte fails with NO_MEMORY.
let blob1_data = [2u8; 1];
let (_mem1, ptr1) = make_user_in(&blob1_data).expect("user mem 1");
let res: Result<u32, Status> = alloc
.allocate_with(1, |dest| ptr1.copy_slice_from_user(dest).map(|_| ()))
.map_err(Into::into);
expect_true!(res == Err(Status::NO_MEMORY));
}
/// Tests BlobIdAllocator data integrity and corruption handling.
#[test]
fn test_blob_id_allocator_corruption_detection() {
const BUF_SIZE: usize = 128;
let mut buf = Box::<[u64]>::try_new_zeroed_slice(BUF_SIZE / 8).expect("allocation failed");
let base = buf.as_mut_ptr() as usize;
let slice = unsafe { slice::from_raw_parts_mut(base as *mut u8, BUF_SIZE) };
let alloc = BlobIdAllocator::init_from_slice(slice, ZeroFill::No);
// Corrupt header by setting blob_head < index_end
let corrupted_hdr = Header { next_id: 1, blob_head: 8 };
unsafe {
ptr::write_volatile(base as *mut u64, corrupted_hdr.to_u64());
}
let blob_data = [0u8; 4];
let (_mem, ptr) = make_user_in(&blob_data).expect("user mem");
let res: Result<u32, Status> = alloc
.allocate_with(4, |dest| ptr.copy_slice_from_user(dest).map(|_| ()))
.map_err(Into::into);
expect_true!(res == Err(Status::IO_DATA_INTEGRITY));
// Re-initialize and corrupt index slot to cause CAS failure
let slice = unsafe { slice::from_raw_parts_mut(base as *mut u8, BUF_SIZE) };
let alloc = BlobIdAllocator::init_from_slice(slice, ZeroFill::No);
unsafe {
// Slot for index 0 is at base + 8. Write non-zero value to it.
ptr::write_volatile((base + 8) as *mut u64, 0x1234);
}
let res2: Result<u32, Status> = alloc
.allocate_with(4, |dest| ptr.copy_slice_from_user(dest).map(|_| ()))
.map_err(Into::into);
expect_true!(res2 == Err(Status::IO_DATA_INTEGRITY));
}
/// Tests IobEndpointId enum methods.
#[test]
fn test_iob_endpoint_id() {
expect_true!(IobEndpointId::Ep0.other() == IobEndpointId::Ep1);
expect_true!(IobEndpointId::Ep1.other() == IobEndpointId::Ep0);
}
/// Tests IoBufferDispatcher::create argument validation.
#[test]
fn test_create_validation() {
let mut region = unsafe { mem::zeroed::<zx_iob_region_t>() };
region.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
region.access = ZX_IOB_ACCESS_EP0_CAN_MAP_READ | ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE;
region.size = page::SIZE as u64;
region.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_NONE;
// Invalid options
expect_true!(
IoBufferDispatcher::create(1, &[region]).map(|_| ()) == Err(Status::INVALID_ARGS)
);
// Empty regions
expect_true!(IoBufferDispatcher::create(0, &[]).map(|_| ()) == Err(Status::INVALID_ARGS));
// No access permissions
let mut no_access_region = region;
no_access_region.access = 0;
expect_true!(
IoBufferDispatcher::create(0, &[no_access_region]).map(|_| ())
== Err(Status::INVALID_ARGS)
);
// Invalid region type
let mut invalid_type_region = region;
invalid_type_region.r#type = 999;
expect_true!(
IoBufferDispatcher::create(0, &[invalid_type_region]).map(|_| ())
== Err(Status::INVALID_ARGS)
);
// Invalid discipline: NONE with mediated access
let mut invalid_disc = region;
invalid_disc.access |= ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ;
expect_true!(
IoBufferDispatcher::create(0, &[invalid_disc]).map(|_| ()) == Err(Status::INVALID_ARGS)
);
// Invalid discipline: ID_ALLOCATOR with mediated read only
let mut id_alloc_ro = region;
id_alloc_ro.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR;
id_alloc_ro.access = ZX_IOB_ACCESS_EP0_CAN_MEDIATED_READ | ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE;
expect_true!(
IoBufferDispatcher::create(0, &[id_alloc_ro]).map(|_| ()) == Err(Status::INVALID_ARGS)
);
// Unknown discipline type
let mut unknown_disc = region;
unknown_disc.discipline.r#type = 999;
expect_true!(
IoBufferDispatcher::create(0, &[unknown_disc]).map(|_| ()) == Err(Status::INVALID_ARGS)
);
}
/// Tests dispatcher methods, region info, rights, and child observers.
#[test]
fn test_dispatcher_methods_and_region_info() {
let mut r0 = unsafe { mem::zeroed::<zx_iob_region_t>() };
r0.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
r0.access = ZX_IOB_ACCESS_EP0_CAN_MAP_READ
| ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE
| ZX_IOB_ACCESS_EP1_CAN_MAP_READ;
r0.size = page::SIZE as u64;
r0.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_NONE;
let mut r1 = unsafe { mem::zeroed::<zx_iob_region_t>() };
r1.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
r1.access = ZX_IOB_ACCESS_EP0_CAN_MAP_READ
| ZX_IOB_ACCESS_EP1_CAN_MAP_READ
| ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE;
r1.size = page::SIZE as u64;
r1.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_NONE;
let (h0, h1, rights) =
IoBufferDispatcher::create(0, &[r0, r1]).expect("failed to create iob");
expect_eq!(rights, super::DEFAULT_RIGHTS);
expect_eq!(IoBufferDispatcher::default_rights(), super::DEFAULT_RIGHTS);
let d0 = h0.dispatcher();
let d1 = h1.dispatcher();
expect_eq!(d0.region_count(), 2);
expect_eq!(d1.region_count(), 2);
let info0 = d0.get_info();
expect_eq!(info0.region_count, 2);
expect_eq!(info0.options, 0);
// Query regions
expect_ok!(d0.get_region(0).map(|_| ()));
expect_ok!(d0.get_region(1).map(|_| ()));
expect_true!(d0.get_region(2).map(|_| ()) == Err(Status::OUT_OF_RANGE));
// Query VMOs
let vmo0 = d0.get_vmo(0).expect("get_vmo 0");
let vmo1 = d0.get_vmo(1).expect("get_vmo 1");
expect_true!(vmo0.user_id() != 0);
expect_true!(vmo1.user_id() != 0);
expect_true!(d0.get_vmo(2).map(|_| ()) == Err(Status::OUT_OF_RANGE));
// Map rights
expect_eq!(
d0.get_map_rights(super::DEFAULT_RIGHTS, 0),
ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP
);
expect_eq!(d1.get_map_rights(super::DEFAULT_RIGHTS, 0), ZX_RIGHT_READ | ZX_RIGHT_MAP);
expect_eq!(d0.get_map_rights(super::DEFAULT_RIGHTS, 1), ZX_RIGHT_READ | ZX_RIGHT_MAP);
expect_eq!(
d1.get_map_rights(super::DEFAULT_RIGHTS, 1),
ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP
);
// Masking with limited rights
expect_eq!(d0.get_map_rights(ZX_RIGHT_READ, 0), ZX_RIGHT_READ);
expect_eq!(d0.get_map_rights(super::DEFAULT_RIGHTS, 2), 0);
// Region info swapping
let rinfo0_ep0 = d0.get_region_info(0);
let rinfo0_ep1 = d1.get_region_info(0);
expect_eq!(rinfo0_ep0.region.access, r0.access);
expect_eq!(
rinfo0_ep1.region.access,
ZX_IOB_ACCESS_EP1_CAN_MAP_READ
| ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE
| ZX_IOB_ACCESS_EP0_CAN_MAP_READ
);
// as_child_observer pointer is non-null
expect_nonnull!(d0.as_child_observer());
expect_nonnull!(d1.as_child_observer());
// create_mappable_vmo_for_region
let mappable = d0.create_mappable_vmo_for_region(0).expect("create mappable vmo");
expect_eq!(mappable.user_id(), vmo0.user_id());
expect_true!(d0.create_mappable_vmo_for_region(2).map(|_| ()) == Err(Status::OUT_OF_RANGE));
// set_name and get_name
expect_ok!(d0.set_name(b"test_iob_name"));
let mut name_buf = [0u8; ZX_MAX_NAME_LEN];
expect_ok!(d0.get_name(&mut name_buf));
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(ZX_MAX_NAME_LEN);
expect_true!(&name_buf[..name_len] == b"test_iob_name");
}
/// Tests ID allocator discipline operations and type safety.
#[test]
fn test_id_allocator_and_discipline_type_safety() {
let mut region = unsafe { mem::zeroed::<zx_iob_region_t>() };
region.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
region.access = ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE | ZX_IOB_ACCESS_EP1_CAN_MAP_READ;
region.size = page::SIZE as u64;
region.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR;
let (h0, h1, _) =
IoBufferDispatcher::create(0, &[region]).expect("create id allocator iob");
let d0 = h0.dispatcher();
let d1 = h1.dispatcher();
let blob_data = [42u8; 16];
let (_mem, ptr) = make_user_in(&blob_data).expect("user mem");
// EP0 has mediated write rights -> allocate_id succeeds
let id = d0.allocate_id(0, ptr, blob_data.len()).expect("allocate_id ep0");
expect_eq!(id, 0);
// EP1 lacks mediated write rights -> ACCESS_DENIED
let res_denied = d1.allocate_id(0, ptr, blob_data.len());
expect_true!(res_denied == Err(Status::ACCESS_DENIED));
// Out of range region index -> OUT_OF_RANGE
let res_oor = d0.allocate_id(1, ptr, blob_data.len());
expect_true!(res_oor == Err(Status::OUT_OF_RANGE));
// Calling write on ID allocator discipline -> WRONG_TYPE
let dummy_iovec = UserInPtr::<zx_iovec_t>::new(core::ptr::null());
let res_wt = d0.write(0, dummy_iovec, 0);
expect_true!(res_wt == Err(Status::WRONG_TYPE));
}
/// Tests ZX_IOB_PEER_CLOSED signal propagation with mapped child VMO lifecycle.
#[test]
fn test_peer_closed_signal_with_child_vmo() {
let mut region = unsafe { mem::zeroed::<zx_iob_region_t>() };
region.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
region.access = ZX_IOB_ACCESS_EP0_CAN_MAP_READ
| ZX_IOB_ACCESS_EP0_CAN_MAP_WRITE
| ZX_IOB_ACCESS_EP1_CAN_MAP_READ
| ZX_IOB_ACCESS_EP1_CAN_MAP_WRITE;
region.size = page::SIZE as u64;
region.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_NONE;
let (h0, h1, _) = IoBufferDispatcher::create(0, &[region]).expect("create iob");
let d0 = h0.dispatcher();
let d1 = h1.dispatcher();
// Create a child VMO on endpoint 1
let child_vmo = d1.create_mappable_vmo_for_region(0).expect("create child vmo");
// Drop endpoint 1 handle
drop(h1);
// d0 should NOT have PEER_CLOSED yet because child VMO is still alive
{
ksync::lock!(let guard = d0.state().peered.lock());
let sigs = d0.signals_state_locked(guard.token());
expect_eq!(sigs & ZX_IOB_PEER_CLOSED, 0);
}
// Dropping child VMO triggers on_zero_child
drop(child_vmo);
// Now d0 should observe PEER_CLOSED
{
ksync::lock!(let guard = d0.state().peered.lock());
let sigs = d0.signals_state_locked(guard.token());
expect_true!((sigs & ZX_IOB_PEER_CLOSED) != 0);
}
}
/// Tests ID allocator discipline variant and allocator extraction.
#[test]
fn test_id_allocator_variant() {
let page_size = page::SIZE as u64;
let vmo0 = VmObjectPaged::create(ALLOC_FLAG_ANY, 0, page_size).expect("create vmo0");
let vmo1 = VmObjectPaged::create(ALLOC_FLAG_ANY, 0, page_size).expect("create vmo1");
let kernel_vmo =
VmObjectPaged::create(ALLOC_FLAG_ANY, 0, page_size).expect("create kernel_vmo");
let vmo0 = VmObjectPaged::into_vm_object(vmo0);
let vmo1 = VmObjectPaged::into_vm_object(vmo1);
let kernel_vmo = VmObjectPaged::into_vm_object(kernel_vmo);
let mut region = unsafe { mem::zeroed::<zx_iob_region_t>() };
region.r#type = ZX_IOB_REGION_TYPE_PRIVATE;
region.access = ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE | ZX_IOB_ACCESS_EP1_CAN_MAP_READ;
region.size = page_size;
region.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_ID_ALLOCATOR;
let variant = IoBufferDispatcher::create_iob_region_variant(
vmo0.clone(),
vmo1.clone(),
kernel_vmo,
&region,
vmo0.user_id(),
None,
)
.expect("create id allocator variant");
// EP0 has mediated write rights -> allocate_id succeeds
let blob_data = [42u8; 16];
let (_mem, ptr) = make_user_in(&blob_data).expect("user mem");
let id =
variant.allocate_id(IobEndpointId::Ep0, ptr, blob_data.len()).expect("allocate_id ep0");
expect_eq!(id, 0);
let id = variant
.allocate_id(IobEndpointId::Ep0, ptr, blob_data.len())
.expect("allocate_id ep0 second");
expect_eq!(id, 1);
// EP1 lacks mediated write rights -> ACCESS_DENIED
expect_true!(matches!(
variant.allocate_id(IobEndpointId::Ep1, ptr, blob_data.len()),
Err(Status::ACCESS_DENIED)
));
// Calling write on ID allocator discipline -> WRONG_TYPE
let iovec = zx_iovec_t { buffer: ptr.as_ptr().cast_mut(), capacity: blob_data.len() };
let (_vec_mem, vec_ptr) = make_user_in_iovec(&[iovec]).expect("user vec mem");
expect_true!(matches!(
variant.write(IobEndpointId::Ep0, vec_ptr, 1),
Err(Status::WRONG_TYPE)
));
}
/// Tests mediated write ring buffer discipline without lock violation.
#[test]
fn test_mediated_write_ring_buffer_variant() {
let page_size = page::SIZE as u64;
let (sr_handle, _) =
IoBufferSharedRegionDispatcher::create(2 * page_size).expect("create shared region");
let sr_disp = sr_handle.dispatcher().clone();
let vmo0 = VmObjectPaged::create(ALLOC_FLAG_ANY, 0, page_size).expect("create vmo0");
let vmo1 = VmObjectPaged::create(ALLOC_FLAG_ANY, 0, page_size).expect("create vmo1");
let vmo0 = VmObjectPaged::into_vm_object(vmo0);
let vmo1 = VmObjectPaged::into_vm_object(vmo1);
let mut region = unsafe { mem::zeroed::<zx_iob_region_t>() };
region.r#type = ZX_IOB_REGION_TYPE_SHARED;
region.access = ZX_IOB_ACCESS_EP0_CAN_MEDIATED_WRITE | ZX_IOB_ACCESS_EP1_CAN_MAP_READ;
region.size = 0;
region.discipline.r#type = ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITE_RING_BUFFER;
region.discipline.extension.ring_buffer.tag = 0x1234_5678;
let variant = IoBufferDispatcher::create_iob_region_variant(
vmo0.clone(),
vmo1.clone(),
vmo0.clone(),
&region,
sr_disp.get_koid(),
Some(sr_disp.clone()),
)
.expect("create mediated write ring buffer variant");
// Perform write through variant (assert_no_locks_held runs inside shared_region.write)
let msg_data = [42u8; 8];
let (_msg_mem, msg_ptr) = make_user_in(&msg_data).expect("user msg mem");
let iovec = zx_iovec_t { buffer: msg_ptr.as_ptr().cast_mut(), capacity: msg_data.len() };
let (_vec_mem, vec_ptr) = make_user_in_iovec(&[iovec]).expect("user vec mem");
expect_ok!(variant.write(IobEndpointId::Ep0, vec_ptr, 1));
// EP1 lacks mediated write rights -> ACCESS_DENIED
expect_true!(matches!(
variant.write(IobEndpointId::Ep1, vec_ptr, 1),
Err(Status::ACCESS_DENIED)
));
// Calling allocate_id on mediated write ring buffer discipline -> WRONG_TYPE
expect_true!(matches!(
variant.allocate_id(IobEndpointId::Ep0, msg_ptr, msg_data.len()),
Err(Status::WRONG_TYPE)
));
}
}