blob: 1a1faf58a8d43d67314e66d5ee8b2b3eddddef57 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![recursion_limit = "256"]
pub(crate) mod device;
pub(crate) mod sysfs;
use crate::device::{
USB_SPEED_FULL, USB_SPEED_HIGH, USB_SPEED_LOW, USB_SPEED_SUPER, USB_SPEED_SUPER_PLUS,
USB_SPEED_UNKNOWN, UsbDevice, UsbDeviceFile,
};
use crate::sysfs::build_usb_device_directory;
use fidl::endpoints::Proxy;
use fidl_fuchsia_hardware_usb as fhardware_usb;
use fidl_fuchsia_hardware_usb_descriptor as fusb_descriptor;
use fuchsia_component::client::Service;
use futures::TryStreamExt;
use starnix_core::device::DeviceMode;
use starnix_core::device::kobject::{Bus, Device, DeviceMetadata};
use starnix_core::task::dynamic_thread_spawner::SpawnRequestBuilder;
use starnix_core::task::{CurrentTask, Kernel};
use starnix_core::vfs::NamespaceNode;
use starnix_logging::{log_error, log_info};
use starnix_sync::{
LockDepMutex, UsbFsDevicesLock, UsbfsActiveHwIdsLock, UsbfsDevNumAllocatorLock,
};
use starnix_uapi::device_id::{DeviceId, USB_DEVICE_MAJOR};
use starnix_uapi::errors::Errno;
use starnix_uapi::open_flags::OpenFlags;
use starnix_uapi::{errno, usb_device_descriptor};
use std::collections::{BTreeMap, HashSet};
use std::sync::Arc;
pub(crate) fn fuchsia_speed_to_linux_speed(speed: fusb_descriptor::UsbSpeed) -> u32 {
match speed {
fusb_descriptor::UsbSpeed::Full => USB_SPEED_FULL,
fusb_descriptor::UsbSpeed::Low => USB_SPEED_LOW,
fusb_descriptor::UsbSpeed::High => USB_SPEED_HIGH,
fusb_descriptor::UsbSpeed::Super => USB_SPEED_SUPER,
fusb_descriptor::UsbSpeed::EnhancedSuper => USB_SPEED_SUPER_PLUS,
fusb_descriptor::UsbSpeed::Undefined => USB_SPEED_UNKNOWN,
}
}
pub(crate) fn fidl_device_descriptor_to_linux(
desc: &fusb_descriptor::UsbDeviceDescriptor,
) -> usb_device_descriptor {
usb_device_descriptor {
bLength: desc.b_length,
bDescriptorType: desc.b_descriptor_type,
bcdUSB: desc.bcd_usb.to_le(),
bDeviceClass: desc.b_device_class,
bDeviceSubClass: desc.b_device_sub_class,
bDeviceProtocol: desc.b_device_protocol,
bMaxPacketSize0: desc.b_max_packet_size0,
idVendor: desc.id_vendor.to_le(),
idProduct: desc.id_product.to_le(),
bcdDevice: desc.bcd_device.to_le(),
iManufacturer: desc.i_manufacturer,
iProduct: desc.i_product,
iSerialNumber: desc.i_serial_number,
bNumConfigurations: desc.b_num_configurations,
}
}
struct DevNumAllocator {
allocated: HashSet<u16>,
next: u16,
}
impl DevNumAllocator {
fn new() -> Self {
Self { allocated: HashSet::new(), next: 2 }
}
fn allocate(&mut self) -> Option<u16> {
let start = self.next;
for i in 0..126 {
let num = ((start - 2 + i) % 126) + 2;
if self.allocated.insert(num) {
self.next = if num >= 127 { 2 } else { num + 1 };
return Some(num);
}
}
None
}
fn release(&mut self, dev_num: u16) {
self.allocated.remove(&dev_num);
}
}
#[derive(Clone)]
pub struct UsbDeviceManager {
pub(crate) bus: Bus,
pub(crate) devices: Arc<LockDepMutex<BTreeMap<DeviceId, Arc<UsbDevice>>, UsbFsDevicesLock>>,
pub(crate) allocator: Arc<LockDepMutex<DevNumAllocator, UsbfsDevNumAllocatorLock>>,
}
impl UsbDeviceManager {
pub(crate) fn new(bus: Bus) -> Self {
Self {
bus,
devices: Arc::new(LockDepMutex::new(BTreeMap::new())),
allocator: Arc::new(LockDepMutex::new(DevNumAllocator::new())),
}
}
pub(crate) fn allocate_dev_num(&self) -> Option<u16> {
self.allocator.lock().allocate()
}
pub(crate) fn release_dev_num(&self, dev_num: u16) {
self.allocator.lock().release(dev_num);
}
pub(crate) fn get_device(&self, id: DeviceId) -> Option<Arc<UsbDevice>> {
self.devices.lock().get(&id).cloned()
}
pub(crate) fn add_device(
&self,
kernel: &Kernel,
device: Arc<UsbDevice>,
) -> Result<Device, Errno> {
let bus_str = format!("{:03}", device.bus_num);
let dev_str = format!("{:03}", device.dev_num);
let devname = format!("bus/usb/{}/{}", bus_str, dev_str);
let sysname = format!("{}-{}", device.bus_num, device.dev_num);
let devt = DeviceId::new(USB_DEVICE_MAJOR, device.minor());
let desc = &device.device_descriptor;
let id_vendor = u16::from_le(desc.idVendor);
let id_product = u16::from_le(desc.idProduct);
let bcd_device = u16::from_le(desc.bcdDevice);
let b_device_class = desc.bDeviceClass;
let b_device_sub_class = desc.bDeviceSubClass;
let b_device_protocol = desc.bDeviceProtocol;
let metadata =
DeviceMetadata::new(devname.into(), devt, DeviceMode::Char).with_devtype("usb_device");
metadata.properties.insert(
b"PRODUCT".into(),
format!("{:x}/{:x}/{:x}", id_vendor, id_product, bcd_device).into(),
);
metadata.properties.insert(
b"TYPE".into(),
format!("{}/{}/{}", b_device_class, b_device_sub_class, b_device_protocol).into(),
);
metadata.properties.insert(b"BUSNUM".into(), format!("{:03}", device.bus_num).into());
metadata.properties.insert(b"DEVNUM".into(), format!("{:03}", device.dev_num).into());
self.devices.lock().insert(devt, device.clone());
let device_clone = device.clone();
let registered_device = kernel.device_registry.add_major_device(
kernel,
sysname.as_bytes().into(),
metadata,
None,
self.bus.clone(),
move |device_obj, dir| {
build_usb_device_directory(device_obj, dir, &device_clone);
},
);
Ok(registered_device)
}
pub(crate) fn remove_device(
&self,
current_task: &CurrentTask,
registered_device: Device,
) -> Result<(), Errno> {
if let Some(metadata) = registered_device.metadata() {
let removed = self.devices.lock().remove(&metadata.devt);
if let Some(device) = removed {
self.release_dev_num(device.dev_num);
device.on_disconnect();
}
}
current_task.kernel().device_registry.remove_device(current_task, registered_device);
Ok(())
}
}
async fn run_in_kernel_task_async<F, R>(
kernel: &Kernel,
debug_name: &'static str,
f: F,
) -> Result<R, Errno>
where
F: FnOnce(&CurrentTask) -> R + Send + 'static,
R: Send + 'static,
{
let closure = move |current_task: &CurrentTask| {
current_task.override_creds(
starnix_core::security::creds_start_internal_operation(current_task),
|| f(current_task),
)
};
let (result_fut, req) = SpawnRequestBuilder::new()
.with_debug_name(debug_name)
.with_sync_closure(closure)
.build_with_async_result();
kernel.kthreads.spawner().spawn_from_request(req);
result_fut.await
}
pub fn usbfs_init(kernel: &Arc<Kernel>) -> Result<UsbDeviceManager, Errno> {
let registry = &kernel.device_registry;
let usb_bus = registry.objects.get_or_create_bus("usb".into());
let manager = UsbDeviceManager::new(usb_bus);
let manager_clone = manager.clone();
registry.register_major(
"usb".into(),
DeviceMode::Char,
USB_DEVICE_MAJOR,
move |_current_task: &CurrentTask,
id: DeviceId,
_node: &NamespaceNode,
_flags: OpenFlags|
-> Result<Box<dyn starnix_core::vfs::FileOps>, Errno> {
let device = manager_clone.get_device(id).ok_or_else(|| errno!(ENODEV))?;
Ok(Box::new(UsbDeviceFile::new(device)))
},
)?;
let kernel_clone = Arc::clone(kernel);
let manager_for_watcher = manager.clone();
kernel.kthreads.spawn_future(
move || async move {
watch_usb_devices(kernel_clone, manager_for_watcher).await;
},
"usbfs_watcher",
);
Ok(manager)
}
async fn watch_usb_devices(kernel: Arc<Kernel>, manager: UsbDeviceManager) {
log_info!("Starting Starnix usbfs USB device watcher.");
let service = match Service::open(fhardware_usb::UsbServiceMarker) {
Ok(service) => service,
Err(e) => {
log_info!("Physical USB service not available: {:?}", e);
return;
}
};
watch_usb_devices_from_service(kernel, manager, service).await;
}
async fn watch_usb_devices_from_service(
kernel: Arc<Kernel>,
manager: UsbDeviceManager,
service: Service<fhardware_usb::UsbServiceMarker>,
) {
let mut watcher = match service.watch().await {
Ok(watcher) => watcher,
Err(e) => {
log_info!("Failed to create USB service watcher: {:?}", e);
return;
}
};
let active_hw_ids =
Arc::new(LockDepMutex::<HashSet<u32>, UsbfsActiveHwIdsLock>::new(HashSet::new()));
while let Ok(Some(instance)) = watcher.try_next().await {
let device_proxy = match instance.connect_to_device() {
Ok(proxy) => proxy,
Err(e) => {
log_error!("Failed to connect to USB device: {:?}", e);
continue;
}
};
let first_config_desc = match device_proxy.get_configuration_descriptor(1).await {
Err(fidl::Error::ClientChannelClosed { .. }) => continue,
Ok(Err(zx::sys::ZX_ERR_NOT_SUPPORTED)) => {
// Child interface node from usb-composite, not parent UsbDevice; skip it.
continue;
}
Ok(Ok(desc)) if !desc.is_empty() => Some(desc),
_ => None,
};
let hw_device_id = match device_proxy.get_device_id().await {
Ok(id) => id,
Err(e) => {
log_error!("Failed to get device ID: {:?}", e);
continue;
}
};
if !active_hw_ids.lock().insert(hw_device_id) {
continue;
}
let fidl_desc = match device_proxy.get_device_descriptor().await {
Ok(desc) => desc,
Err(e) => {
active_hw_ids.lock().remove(&hw_device_id);
log_error!("Failed to get device descriptor: {:?}", e);
continue;
}
};
let device_descriptor = fidl_device_descriptor_to_linux(&fidl_desc);
let mut config_descriptors = Vec::new();
if let Some(desc) = first_config_desc {
config_descriptors.extend_from_slice(&desc);
}
for cfg in 2..=device_descriptor.bNumConfigurations {
match device_proxy.get_configuration_descriptor(cfg).await {
Ok(Ok(desc)) if !desc.is_empty() => {
config_descriptors.extend_from_slice(&desc);
}
_ => {}
}
}
let speed = match device_proxy.get_device_speed().await {
Ok(s) => fuchsia_speed_to_linux_speed(s),
Err(_) => USB_SPEED_FULL,
};
let active_config = device_proxy.get_configuration().await.unwrap_or(1);
let assigned_dev_num = match manager.allocate_dev_num() {
Some(n) => n,
None => {
active_hw_ids.lock().remove(&hw_device_id);
log_error!("Exhausted USB device numbers (2..127) for incoming device.");
continue;
}
};
let sync_proxy = match instance.connect_to_device() {
Ok(client_proxy) => {
let client_end = client_proxy.into_client_end().ok();
client_end
.map(|c| Arc::new(fhardware_usb::UsbSynchronousProxy::new(c.into_channel())))
}
Err(_) => None,
};
let mut usb_device =
UsbDevice::new(1, assigned_dev_num, speed, device_descriptor, config_descriptors);
usb_device.state.get_mut().active_config = active_config;
usb_device.sync_proxy = sync_proxy;
let usb_device = Arc::new(usb_device);
let add_res = manager.add_device(&kernel, Arc::clone(&usb_device));
match add_res {
Ok(registered_device) => {
log_info!(
"Registered USB device in Starnix usbfs (/dev/bus/usb/001/{:03}) with speed {}",
assigned_dev_num,
speed
);
let kernel_for_close = Arc::clone(&kernel);
let manager_for_close = manager.clone();
let active_hw_ids_for_close = Arc::clone(&active_hw_ids);
let proxy_for_close = device_proxy.clone();
kernel.kthreads.spawn_future(
move || async move {
let mut event_stream = proxy_for_close.take_event_stream();
let _ = event_stream.try_next().await;
active_hw_ids_for_close.lock().remove(&hw_device_id);
let _ = run_in_kernel_task_async(
&kernel_for_close,
"usbfs-device-disconnect",
move |current_task| {
if manager_for_close
.remove_device(current_task, registered_device)
.is_ok()
{
log_info!(
"Removed disconnected USB device from Starnix usbfs (/dev/bus/usb/001/{:03})",
assigned_dev_num
);
}
},
)
.await;
},
"usbfs-device-lifecycle",
);
}
Err(e) => {
manager.release_dev_num(assigned_dev_num);
active_hw_ids.lock().remove(&hw_device_id);
log_error!("Failed to register USB device in Starnix usbfs: {:?}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{
DEFAULT_CANCEL_DRAIN_TIMEOUT, INVALID_USBDEVFS_BULK_SIZE, MAX_RETIRED_VMOS,
MAX_USBDEVFS_BULK_SIZE, MAX_USBDEVFS_CONTROL_SIZE, canonicalize_ioctl_request,
};
use fidl::endpoints::{Responder, ServerEnd, create_endpoints};
use fidl_fuchsia_hardware_usb_endpoint as fusb_endpoint;
use fidl_fuchsia_hardware_usb_request as fusb_request;
use starnix_core::device::DeviceListener;
use starnix_core::device::kobject::{UEventAction, UEventContext};
use starnix_core::fs::devtmpfs::DevTmpFs;
use starnix_core::mm::syscalls::do_mmap;
use starnix_core::mm::{MemoryAccessor, MemoryAccessorExt, PAGE_SIZE};
use starnix_core::signals::{SignalInfo, send_standard_signal};
use starnix_core::task::{KernelFeatures, TaskFlags};
use starnix_core::testing::{spawn_kernel_and_run, spawn_kernel_with_features_and_run};
use starnix_core::vfs::buffers::VecOutputBuffer;
use starnix_core::vfs::{FdNumber, FileHandle, FsStr, FsString, LookupContext};
use starnix_sync::Mutex;
use starnix_syscalls::{SUCCESS, SyscallResult};
use starnix_uapi::errors::Errno;
use starnix_uapi::signals::{SIGKILL, SIGUSR1};
#[cfg(target_arch = "aarch64")]
use starnix_uapi::uaddr32;
#[cfg(target_arch = "aarch64")]
use starnix_uapi::uapi::arch32;
use starnix_uapi::user_address::{UserAddress, UserRef};
use starnix_uapi::{
MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE, USB_CLASS_HID, USB_CLASS_PRINTER,
USB_CLASS_VENDOR_SPEC, USB_DIR_IN, USB_DIR_OUT, USB_DT_CONFIG, USB_DT_CONFIG_SIZE,
USB_DT_DEVICE, USB_DT_DEVICE_SIZE, USB_DT_ENDPOINT, USB_DT_ENDPOINT_SIZE, USB_DT_INTERFACE,
USB_DT_INTERFACE_SIZE, USB_DT_SS_ENDPOINT_COMP, USB_DT_SS_EP_COMP_SIZE,
USB_ENDPOINT_XFER_BULK, USB_ENDPOINT_XFER_INT, USB_ENDPOINT_XFER_ISOC, USB_RECIP_ENDPOINT,
USB_RECIP_INTERFACE, USB_REQ_CLEAR_FEATURE, USB_REQ_GET_DESCRIPTOR, USB_REQ_SET_INTERFACE,
USB_TYPE_CLASS, USB_TYPE_STANDARD, USB_TYPE_VENDOR, USBDEVFS_BULK, USBDEVFS_CLAIMINTERFACE,
USBDEVFS_CLEAR_HALT, USBDEVFS_CONNECT, USBDEVFS_CONNECTINFO, USBDEVFS_CONTROL,
USBDEVFS_DISCONNECT, USBDEVFS_DISCONNECT_CLAIM, USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER,
USBDEVFS_DISCONNECT_CLAIM_IF_DRIVER, USBDEVFS_GET_CAPABILITIES, USBDEVFS_GET_SPEED,
USBDEVFS_GETDRIVER, USBDEVFS_IOCTL, USBDEVFS_RELEASEINTERFACE, USBDEVFS_RESET,
USBDEVFS_SETCONFIGURATION, USBDEVFS_SETINTERFACE, error, sigaction_t, uaddr,
usb_config_descriptor, usb_endpoint_descriptor, usb_interface_descriptor,
usbdevfs_bulktransfer, usbdevfs_connectinfo, usbdevfs_ctrltransfer,
usbdevfs_disconnect_claim, usbdevfs_getdriver, usbdevfs_ioctl, usbdevfs_setinterface,
};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicUsize, Ordering};
use zerocopy::{FromZeros, Immutable, IntoBytes};
const EP1_IN: u8 = 0x81;
const EP1_OUT: u8 = 0x01;
const EP2_IN: u8 = 0x82;
const EP2_OUT: u8 = 0x02;
const EP3_IN: u8 = 0x83;
const XFER_ISO: u8 = USB_ENDPOINT_XFER_ISOC as u8;
const XFER_BULK: u8 = USB_ENDPOINT_XFER_BULK as u8;
const XFER_INT: u8 = USB_ENDPOINT_XFER_INT as u8;
const CLASS_PRINTER: u8 = USB_CLASS_PRINTER as u8;
const CLASS_VENDOR: u8 = USB_CLASS_VENDOR_SPEC as u8;
/// Request answered by the mock device without any side effect.
const TEST_REQUEST: u8 = 0x42;
const INTERFACE_OUT: u32 = USB_DIR_OUT | USB_TYPE_STANDARD | USB_RECIP_INTERFACE;
const ENDPOINT_OUT: u32 = USB_DIR_OUT | USB_TYPE_STANDARD | USB_RECIP_ENDPOINT;
const PRINTER_GET_DEVICE_ID: u32 = USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
/// Data returned by the mock device for an IN request when no reply is scripted.
const DEFAULT_IN_DATA: [u8; 4] = [0xaa, 0xbb, 0xcc, 0xdd];
/// Offset of the data buffer from the ioctl argument in the buffers of `map_test_buffers`.
const TEST_DATA_OFFSET: usize = 256;
/// Device number of the device registered by the `run_with_*` helpers.
const DEV_NUM: u16 = 2;
fn create_mock_hid_descriptors() -> (usb_device_descriptor, Vec<u8>) {
let device_desc = usb_device_descriptor {
bLength: USB_DT_DEVICE_SIZE as u8,
bDescriptorType: USB_DT_DEVICE as u8,
bcdUSB: 0x0210u16.to_le(),
bDeviceClass: 0,
bDeviceSubClass: 0,
bDeviceProtocol: 0,
bMaxPacketSize0: 64,
idVendor: 0x1234u16.to_le(),
idProduct: 0x5678u16.to_le(),
bcdDevice: 0x0543u16.to_le(),
iManufacturer: 1,
iProduct: 2,
iSerialNumber: 3,
bNumConfigurations: 1,
};
let config_desc = usb_config_descriptor {
bLength: USB_DT_CONFIG_SIZE as u8,
bDescriptorType: USB_DT_CONFIG as u8,
wTotalLength: ((9 + 9 + 7 + 7) as u16).to_le(),
bNumInterfaces: 1,
bConfigurationValue: 1,
iConfiguration: 0,
bmAttributes: 0x80, // Bus powered
bMaxPower: 50, // 100mA
};
let if_desc = usb_interface_descriptor {
bLength: USB_DT_INTERFACE_SIZE as u8,
bDescriptorType: USB_DT_INTERFACE as u8,
bInterfaceNumber: 0,
bAlternateSetting: 0,
bNumEndpoints: 2,
bInterfaceClass: USB_CLASS_HID as u8,
bInterfaceSubClass: 0,
bInterfaceProtocol: 0,
iInterface: 0,
};
let ep_in_desc = usb_endpoint_descriptor {
bLength: USB_DT_ENDPOINT_SIZE as u8,
bDescriptorType: USB_DT_ENDPOINT as u8,
bEndpointAddress: EP1_IN,
bmAttributes: XFER_INT,
wMaxPacketSize: 64u16.to_le(),
bInterval: 5,
bRefresh: 0,
bSynchAddress: 0,
};
let ep_out_desc = usb_endpoint_descriptor { bEndpointAddress: EP1_OUT, ..ep_in_desc };
let endpoint_size = USB_DT_ENDPOINT_SIZE as usize;
let mut config_bytes = Vec::new();
config_bytes.extend_from_slice(config_desc.as_bytes());
config_bytes.extend_from_slice(if_desc.as_bytes());
config_bytes.extend_from_slice(&ep_in_desc.as_bytes()[..endpoint_size]);
config_bytes.extend_from_slice(&ep_out_desc.as_bytes()[..endpoint_size]);
(device_desc, config_bytes)
}
fn hid_config() -> Vec<u8> {
create_mock_hid_descriptors().1
}
/// Interface alternate setting used to build test configuration descriptors.
struct TestAlt {
interface: u8,
alt: u8,
class: u8,
/// `(bEndpointAddress, bmAttributes, wMaxPacketSize)` of each endpoint.
endpoints: Vec<(u8, u8, u16)>,
}
fn alt(interface: u8, alt: u8, endpoints: Vec<(u8, u8, u16)>) -> TestAlt {
TestAlt { interface, alt, class: CLASS_VENDOR, endpoints }
}
fn endpoint_descriptor(address: u8, attributes: u8, max_packet_size: u16) -> Vec<u8> {
let [lo, hi] = max_packet_size.to_le_bytes();
vec![USB_DT_ENDPOINT_SIZE as u8, USB_DT_ENDPOINT as u8, address, attributes, lo, hi, 0]
}
/// Builds a configuration descriptor with value `config_value` containing `alts`.
fn build_config(config_value: u8, alts: &[TestAlt]) -> Vec<u8> {
let num_interfaces = alts.iter().map(|a| a.interface).collect::<HashSet<_>>().len();
let mut bytes = usb_config_descriptor {
bLength: USB_DT_CONFIG_SIZE as u8,
bDescriptorType: USB_DT_CONFIG as u8,
wTotalLength: 0,
bNumInterfaces: num_interfaces as u8,
bConfigurationValue: config_value,
iConfiguration: 0,
bmAttributes: 0x80,
bMaxPower: 50,
}
.as_bytes()
.to_vec();
for a in alts {
let interface = usb_interface_descriptor {
bLength: USB_DT_INTERFACE_SIZE as u8,
bDescriptorType: USB_DT_INTERFACE as u8,
bInterfaceNumber: a.interface,
bAlternateSetting: a.alt,
bNumEndpoints: a.endpoints.len() as u8,
bInterfaceClass: a.class,
bInterfaceSubClass: 0,
bInterfaceProtocol: 0,
iInterface: 0,
};
bytes.extend_from_slice(interface.as_bytes());
for &(address, attributes, max_packet_size) in &a.endpoints {
bytes.extend(endpoint_descriptor(address, attributes, max_packet_size));
}
}
let [lo, hi] = (bytes.len() as u16).to_le_bytes();
bytes[2] = lo;
bytes[3] = hi;
bytes
}
fn open_dev_node(
current_task: &CurrentTask,
path: &FsStr,
flags: OpenFlags,
) -> Result<FileHandle, Errno> {
let dev_root = NamespaceNode::new_anonymous(
DevTmpFs::from_kernel(current_task.kernel()).root().clone(),
);
let mut context = LookupContext::default();
let node = current_task.lookup_path(&mut context, dev_root, path)?;
node.open(current_task, flags)
}
fn open_device(current_task: &CurrentTask, dev_num: u16) -> FileHandle {
let path = format!("bus/usb/001/{:03}", dev_num);
open_dev_node(current_task, path.as_bytes().into(), OpenFlags::RDWR).expect("open device")
}
fn map_test_buffer(current_task: &CurrentTask, length: usize) -> UserAddress {
do_mmap(
current_task,
UserAddress::NULL,
length,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
FdNumber::from_raw(-1),
0,
)
.unwrap()
}
/// Maps a page and returns the address of an ioctl argument and of a data buffer in it.
fn map_test_buffers(current_task: &CurrentTask) -> (UserAddress, UserAddress) {
let arg = map_test_buffer(current_task, *PAGE_SIZE as usize);
(arg, (arg + TEST_DATA_OFFSET).unwrap())
}
/// Writes `value` at `addr` and issues ioctl `request` with `addr` as argument.
fn ioctl_with<T: IntoBytes + Immutable>(
current_task: &CurrentTask,
file: &FileHandle,
request: u32,
addr: UserAddress,
value: &T,
) -> Result<SyscallResult, Errno> {
current_task.write_object(UserRef::new(addr), value).unwrap();
file.ops().ioctl(file, current_task, request, addr.into())
}
/// Issues ioctl `request` without argument.
fn ioctl_no_arg(
current_task: &CurrentTask,
file: &FileHandle,
request: u32,
) -> Result<SyscallResult, Errno> {
file.ops().ioctl(file, current_task, request, 0u64.into())
}
/// Issues `USBDEVFS_BULK` on raw endpoint value `ep`, using `arg` for the ioctl argument and
/// `data` as buffer.
fn bulk_raw(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
ep: u32,
len: u32,
timeout: u32,
data: UserAddress,
) -> Result<SyscallResult, Errno> {
let transfer = usbdevfs_bulktransfer {
ep,
len,
timeout,
data: uaddr { addr: data.ptr() as u64 },
..usbdevfs_bulktransfer::new_zeroed()
};
ioctl_with(current_task, file, USBDEVFS_BULK, arg, &transfer)
}
/// Issues `USBDEVFS_BULK` on endpoint `ep`.
fn bulk(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
ep: u8,
len: u32,
timeout: u32,
data: UserAddress,
) -> Result<SyscallResult, Errno> {
bulk_raw(current_task, file, arg, u32::from(ep), len, timeout, data)
}
/// Reads `N` bytes at `addr`.
fn read_bytes<const N: usize>(current_task: &CurrentTask, addr: UserAddress) -> [u8; N] {
current_task.read_object(UserRef::new(addr)).unwrap()
}
fn ctrl_transfer(
request_type: u32,
request: u8,
value: u16,
index: u16,
length: u16,
timeout: u32,
data: UserAddress,
) -> usbdevfs_ctrltransfer {
usbdevfs_ctrltransfer {
bRequestType: request_type as u8,
bRequest: request,
wValue: value,
wIndex: index,
wLength: length,
timeout,
data: uaddr { addr: data.ptr() as u64 },
..usbdevfs_ctrltransfer::new_zeroed()
}
}
/// Issues `USBDEVFS_CONTROL` without data stage.
fn control(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
request_type: u32,
request: u8,
index: u16,
timeout: u32,
) -> Result<SyscallResult, Errno> {
let ctrl = ctrl_transfer(request_type, request, 0, index, 0, timeout, UserAddress::NULL);
ioctl_with(current_task, file, USBDEVFS_CONTROL, arg, &ctrl)
}
fn claim(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
ifno: u32,
) -> Result<SyscallResult, Errno> {
ioctl_with(current_task, file, USBDEVFS_CLAIMINTERFACE, arg, &ifno)
}
fn release(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
ifno: u32,
) -> Result<SyscallResult, Errno> {
ioctl_with(current_task, file, USBDEVFS_RELEASEINTERFACE, arg, &ifno)
}
fn set_interface(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
interface: u32,
altsetting: u32,
) -> Result<SyscallResult, Errno> {
let set = usbdevfs_setinterface { interface, altsetting };
ioctl_with(current_task, file, USBDEVFS_SETINTERFACE, arg, &set)
}
fn set_configuration(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
config: i32,
) -> Result<SyscallResult, Errno> {
ioctl_with(current_task, file, USBDEVFS_SETCONFIGURATION, arg, &config)
}
fn clear_halt(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
ep: u32,
) -> Result<SyscallResult, Errno> {
ioctl_with(current_task, file, USBDEVFS_CLEAR_HALT, arg, &ep)
}
fn disconnect_claim(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
interface: u32,
flags: u32,
driver: &[u8],
) -> Result<SyscallResult, Errno> {
let mut dc = usbdevfs_disconnect_claim::default();
dc.interface = interface;
dc.flags = flags;
for (dst, src) in dc.driver.iter_mut().zip(driver) {
*dst = *src as _;
}
ioctl_with(current_task, file, USBDEVFS_DISCONNECT_CLAIM, arg, &dc)
}
fn driver_ioctl(
current_task: &CurrentTask,
file: &FileHandle,
arg: UserAddress,
ifno: i32,
ioctl_code: u32,
) -> Result<SyscallResult, Errno> {
let ctl = usbdevfs_ioctl { ifno, ioctl_code: ioctl_code as i32, data: uaddr { addr: 0 } };
ioctl_with(current_task, file, USBDEVFS_IOCTL, arg, &ctl)
}
/// Queues (or removes) a SIGKILL for `current_task` without making its thread group exit, so
/// the test can observe the interrupted wait.
fn set_pending_sigkill(current_task: &CurrentTask, pending: bool) {
let mut task_state = current_task.write();
if pending {
task_state.enqueue_signal(SignalInfo::kernel(SIGKILL));
task_state.set_flags(TaskFlags::SIGNALS_AVAILABLE, true);
} else {
task_state.take_specific_signal(SignalInfo::kernel(SIGKILL));
task_state.set_flags(TaskFlags::SIGNALS_AVAILABLE, false);
}
}
/// Runs `test` against device `DEV_NUM` built by `build`.
async fn run_with_device<B, F>(build: B, test: F)
where
B: FnOnce(&CurrentTask) -> UsbDevice + Send + Sync + 'static,
F: FnOnce(&CurrentTask, &Arc<UsbDevice>) + Send + Sync + 'static,
{
spawn_kernel_with_features_and_run(
async move |current_task| {
let kernel = current_task.kernel();
let manager = usbfs_init(kernel).expect("usbfs_init failed");
let device = Arc::new(build(current_task));
let registered =
manager.add_device(kernel, Arc::clone(&device)).expect("add_device failed");
test(current_task, &device);
manager.remove_device(current_task, registered).expect("remove_device failed");
},
KernelFeatures { devtmpfs: true, ..Default::default() },
)
.await;
}
/// Runs `test` against device `DEV_NUM` with configuration descriptors `config_bytes` and
/// without connection to a USB device.
async fn run_with_unconnected_device<F>(config_bytes: Vec<u8>, test: F)
where
F: FnOnce(&CurrentTask) + Send + Sync + 'static,
{
run_with_device(
move |_| {
let (device_desc, _) = create_mock_hid_descriptors();
UsbDevice::new(1, DEV_NUM, USB_SPEED_HIGH, device_desc, config_bytes)
},
|current_task, _| test(current_task),
)
.await;
}
/// What the mock endpoint server does with the held IN requests when it receives `CancelAll`.
#[derive(Clone, Copy, PartialEq)]
enum CancelBehavior {
/// Replies to `CancelAll`, then completes the held requests with `ZX_ERR_CANCELED`.
CompleteAfterReply,
/// Replies to `CancelAll`, then completes the held requests successfully, as if they
/// completed just before being cancelled.
CompleteSuccessfullyAfterReply,
/// Never completes the held requests.
Drop,
}
/// Event signaled with `USER_0` by the mock server.
struct MockEvent(zx::Event);
impl Default for MockEvent {
fn default() -> Self {
Self(zx::Event::create())
}
}
impl MockEvent {
fn signal(&self) {
let _ = self.0.signal(zx::Signals::NONE, zx::Signals::USER_0);
}
fn is_signaled(&self) -> bool {
self.0
.wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
.to_result()
.is_ok()
}
fn wait_signaled(&self) {
self.0
.wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE)
.to_result()
.unwrap();
}
}
#[derive(Default)]
struct MockUsbServerState {
/// Configuration values of the device, accepted by `SetConfiguration`.
config_values: Mutex<HashSet<u8>>,
last_ctrl_out: Mutex<Vec<u8>>,
ctrl_transfer_error: AtomicI32,
ctrl_timeouts: Mutex<Vec<i64>>,
/// `(bmRequestType, bRequest, wValue, wIndex)` of each control transfer.
ctrl_setups: Mutex<Vec<(u8, u8, u16, u16)>>,
hold_ctrl_transfers: AtomicBool,
held_ctrl_responders: Mutex<Vec<fhardware_usb::UsbControlTransferResponder>>,
/// Signaled with `USER_0` when a control transfer responder is held.
ctrl_held: MockEvent,
set_interface_status: AtomicI32,
last_set_iface: Mutex<Option<(u8, u8)>>,
set_configuration_status: AtomicI32,
/// Configuration value of each `SetConfiguration` call.
set_config_log: Mutex<Vec<u8>>,
/// Endpoint address of the last `ResetEndpoint` call.
last_reset_endpoint: AtomicU8,
reset_endpoint_count: AtomicUsize,
reset_endpoint_status: AtomicI32,
/// Endpoints enabled on the mock host controller.
enabled_endpoints: Mutex<HashSet<u8>>,
/// `(address, enable, wMaxPacketSize)` of each successful `EnableEndpoint` call.
enable_log: Mutex<Vec<(u8, bool, u16)>>,
/// `(address, bMaxBurst)` of the SuperSpeed companion of each enabled endpoint.
ss_companion_log: Mutex<Vec<(u8, u8)>>,
connect_count: AtomicUsize,
connect_error_once: AtomicI32,
/// Closes the next endpoint channel before serving `RegisterVmos`.
close_endpoint_before_register: AtomicBool,
/// Signaled with `USER_0` when usbfs closes an endpoint channel.
endpoint_unbound: MockEvent,
/// `(address, VMO ID)` of each registered and unregistered VMO.
registered_vmos: Mutex<Vec<(u8, u64)>>,
unregistered_vmos: Mutex<Vec<(u8, u64)>>,
/// `(address, transfer type)` of each queued request.
request_types: Mutex<Vec<(u8, &'static str)>>,
/// `(address, size)` of each queued request.
request_sizes: Mutex<Vec<(u8, u64)>>,
received_bulk_out: Mutex<Vec<Vec<u8>>>,
bulk_out_max_transfer_size: AtomicUsize,
bulk_in_queue_count: AtomicUsize,
bulk_in_scripted_packets: Mutex<VecDeque<(Vec<u8>, i32)>>,
/// Reports the full scripted reply as transferred, even if it exceeds the request size.
overreport_in_transfer_size: AtomicBool,
hold_in_requests: AtomicBool,
cancel_behavior: Mutex<Option<CancelBehavior>>,
cancel_all_status: AtomicI32,
cancel_count: AtomicUsize,
send_stale_completion_on_cancel: AtomicBool,
/// Completes the held requests successfully when the next requests are queued.
complete_held_on_next_queue: AtomicBool,
drop_endpoint_on_next_in: AtomicBool,
/// Number of `QueueRequests` calls received while `ep_addr` was not in `enabled_endpoints`.
invalid_queue_while_disabled: AtomicUsize,
/// Optional hook invoked after sending an IN completion in `QueueRequests`.
on_in_completion: Mutex<Option<Box<dyn FnOnce() + Send>>>,
/// Optional hook invoked when serving `ConnectToEndpoint`.
on_connect_to_endpoint: Mutex<Option<Box<dyn FnOnce() + Send>>>,
}
fn request_type_name(request: &fusb_request::Request) -> &'static str {
match request.information {
Some(fusb_request::RequestInfo::Bulk(_)) => "bulk",
Some(fusb_request::RequestInfo::Interrupt(_)) => "interrupt",
Some(fusb_request::RequestInfo::Isochronous(_)) => "isochronous",
Some(fusb_request::RequestInfo::Control(_)) => "control",
_ => "unknown",
}
}
/// Returns the `(VMO ID, offset, size)` of the buffer of `request`.
fn request_region(request: &fusb_request::Request) -> (u64, u64, u64) {
let region = request.data.as_ref().and_then(|d| d.first()).unwrap();
let id = match region.buffer {
Some(fusb_request::Buffer::VmoId(id)) => id,
_ => panic!("unexpected buffer"),
};
(id, region.offset.unwrap_or(0), region.size.unwrap_or(0))
}
/// Completes `request` successfully, filling its buffer with the next scripted reply.
fn complete_in_request(
state: &MockUsbServerState,
vmos: &HashMap<u64, zx::Vmo>,
request: fusb_request::Request,
) -> fusb_endpoint::Completion {
let (id, offset, size) = request_region(&request);
let (reply, status) =
state.bulk_in_scripted_packets.lock().pop_front().unwrap_or_else(|| {
let len = std::cmp::min(size as usize, DEFAULT_IN_DATA.len());
(DEFAULT_IN_DATA[..len].to_vec(), zx::sys::ZX_OK)
});
// Like a host controller, never write beyond the request buffer.
let written = std::cmp::min(reply.len(), size as usize);
if written > 0 {
vmos[&id].write(&reply[..written], offset).unwrap();
}
let reported = if state.overreport_in_transfer_size.load(Ordering::SeqCst) {
reply.len()
} else {
written
};
fusb_endpoint::Completion {
request: Some(request),
status: Some(status),
transfer_size: Some(reported as u64),
..Default::default()
}
}
fn cancelled_completion(request: fusb_request::Request) -> fusb_endpoint::Completion {
fusb_endpoint::Completion {
request: Some(request),
status: Some(zx::sys::ZX_ERR_CANCELED),
transfer_size: Some(0),
..Default::default()
}
}
/// Serves `ep` like the xHCI endpoint server.
///
/// When the server closes the channel, it disables the endpoint first. When usbfs closes the
/// channel, the endpoint stays enabled, which models the asynchronous disable of the xHCI
/// driver losing the race with a later enable.
async fn serve_mock_endpoint(
ep_addr: u8,
ep: ServerEnd<fusb_endpoint::EndpointMarker>,
state: Arc<MockUsbServerState>,
) {
let mut ep_stream = ep.into_stream();
let mut vmos: HashMap<u64, zx::Vmo> = HashMap::new();
let mut held: Vec<fusb_request::Request> = Vec::new();
let is_in = (ep_addr & (USB_DIR_IN as u8)) != 0;
while let Ok(Some(ep_req)) = ep_stream.try_next().await {
match ep_req {
fusb_endpoint::EndpointRequest::RegisterVmos { vmo_ids, responder } => {
let mut registered = Vec::new();
for info in vmo_ids {
if let (Some(id), Some(size)) = (info.id, info.size) {
let vmo = zx::Vmo::create(size).unwrap();
let dup = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
vmos.insert(id, vmo);
state.registered_vmos.lock().push((ep_addr, id));
registered.push(fusb_endpoint::VmoHandle {
id: Some(id),
vmo: Some(dup),
..Default::default()
});
}
}
let _ = responder.send(registered);
}
fusb_endpoint::EndpointRequest::UnregisterVmos { vmo_ids, responder } => {
for id in vmo_ids {
vmos.remove(&id);
state.unregistered_vmos.lock().push((ep_addr, id));
}
let _ = responder.send(&[], &[]);
}
fusb_endpoint::EndpointRequest::QueueRequests { req: reqs, control_handle } => {
if !state.enabled_endpoints.lock().contains(&ep_addr) {
state.invalid_queue_while_disabled.fetch_add(1, Ordering::SeqCst);
}
if is_in && state.drop_endpoint_on_next_in.swap(false, Ordering::SeqCst) {
// Like xHCI, disable the endpoint before closing the channel.
state.enabled_endpoints.lock().remove(&ep_addr);
return;
}
if state.complete_held_on_next_queue.swap(false, Ordering::SeqCst) {
let completions: Vec<_> = held
.drain(..)
.map(|r| {
let (id, offset, size) = request_region(&r);
vmos[&id].write(&vec![0x99; size as usize], offset).unwrap();
fusb_endpoint::Completion {
request: Some(r),
status: Some(zx::sys::ZX_OK),
transfer_size: Some(size),
..Default::default()
}
})
.collect();
let _ = control_handle.send_on_completion(completions);
}
let mut completions = Vec::new();
for r in reqs {
state.request_types.lock().push((ep_addr, request_type_name(&r)));
let (id, offset, size) = request_region(&r);
state.request_sizes.lock().push((ep_addr, size));
if is_in {
state.bulk_in_queue_count.fetch_add(1, Ordering::SeqCst);
if state.hold_in_requests.load(Ordering::SeqCst) {
held.push(r);
continue;
}
completions.push(complete_in_request(&state, &vmos, r));
} else {
let max_out = state.bulk_out_max_transfer_size.load(Ordering::Relaxed);
let actual_size = std::cmp::min(size as usize, max_out);
let mut buf = vec![0u8; actual_size];
if actual_size > 0 {
vmos[&id].read(&mut buf, offset).unwrap();
}
state.received_bulk_out.lock().push(buf);
completions.push(fusb_endpoint::Completion {
request: Some(r),
status: Some(zx::sys::ZX_OK),
transfer_size: Some(actual_size as u64),
..Default::default()
});
}
}
if !completions.is_empty() {
let _ = control_handle.send_on_completion(completions);
if is_in {
if let Some(hook) = state.on_in_completion.lock().take() {
hook();
}
}
}
}
fusb_endpoint::EndpointRequest::CancelAll { responder } => {
state.cancel_count.fetch_add(1, Ordering::SeqCst);
let status = state.cancel_all_status.load(Ordering::SeqCst);
if status != zx::sys::ZX_OK {
let _ = responder.send(Err(status));
continue;
}
let control_handle = responder.control_handle().clone();
if state.send_stale_completion_on_cancel.swap(false, Ordering::SeqCst) {
let _ =
control_handle.send_on_completion(vec![fusb_endpoint::Completion {
status: Some(zx::sys::ZX_ERR_CANCELED),
transfer_size: Some(0),
..Default::default()
}]);
}
let _ = responder.send(Ok(()));
if held.is_empty() {
continue;
}
let behavior =
state.cancel_behavior.lock().unwrap_or(CancelBehavior::CompleteAfterReply);
let completions: Vec<_> = match behavior {
CancelBehavior::CompleteAfterReply => {
held.drain(..).map(cancelled_completion).collect()
}
CancelBehavior::CompleteSuccessfullyAfterReply => {
held.drain(..).map(|r| complete_in_request(&state, &vmos, r)).collect()
}
CancelBehavior::Drop => continue,
};
let _ = control_handle.send_on_completion(completions);
}
_ => {}
}
}
state.endpoint_unbound.signal();
}
fn status_result(status: i32) -> Result<(), i32> {
if status == zx::sys::ZX_OK { Ok(()) } else { Err(status) }
}
fn spawn_mock_usb_server(
kernel: &Arc<Kernel>,
config_values: HashSet<u8>,
) -> (Arc<fhardware_usb::UsbSynchronousProxy>, Arc<MockUsbServerState>) {
let (client_end, server_end) = create_endpoints::<fhardware_usb::UsbMarker>();
let sync_proxy =
Arc::new(fhardware_usb::UsbSynchronousProxy::new(client_end.into_channel()));
let state = Arc::new(MockUsbServerState {
config_values: Mutex::new(config_values),
bulk_out_max_transfer_size: AtomicUsize::new(usize::MAX),
..Default::default()
});
let state_srv = Arc::clone(&state);
let kernel_for_srv = Arc::clone(kernel);
kernel.kthreads.spawn_future(
move || async move {
let mut stream = server_end.into_stream();
while let Ok(Some(req)) = stream.try_next().await {
match req {
fhardware_usb::UsbRequest::ControlTransfer {
setup,
timeout_ns,
write_data,
responder,
} => {
state_srv.ctrl_timeouts.lock().push(timeout_ns);
state_srv.ctrl_setups.lock().push((
setup.bm_request_type,
setup.b_request,
setup.w_value,
setup.w_index,
));
let is_in = (setup.bm_request_type & (USB_DIR_IN as u8)) != 0;
// Like usb-bus, the OUT data must match wLength.
let expected_len = if is_in { 0 } else { usize::from(setup.w_length) };
if write_data.len() != expected_len {
let _ = responder.send(Err(zx::sys::ZX_ERR_INVALID_ARGS));
continue;
}
if state_srv.hold_ctrl_transfers.load(Ordering::SeqCst) {
state_srv.held_ctrl_responders.lock().push(responder);
state_srv.ctrl_held.signal();
continue;
}
let err = state_srv.ctrl_transfer_error.load(Ordering::Relaxed);
if err != zx::sys::ZX_OK {
let _ = responder.send(Err(err));
continue;
}
if is_in {
let reply =
vec![setup.b_request, (setup.w_value & 0xff) as u8, 0xca, 0xfe];
let _ = responder.send(Ok(&reply));
} else {
*state_srv.last_ctrl_out.lock() = write_data;
let _ = responder.send(Ok(&[]));
}
}
fhardware_usb::UsbRequest::SetInterface {
interface_number,
alt_setting,
responder,
} => {
let status = state_srv.set_interface_status.load(Ordering::Relaxed);
if status == zx::sys::ZX_OK {
*state_srv.last_set_iface.lock() =
Some((interface_number, alt_setting));
}
let _ = responder.send(status_result(status));
}
fhardware_usb::UsbRequest::SetConfiguration {
configuration,
responder,
} => {
state_srv.set_config_log.lock().push(configuration);
// Like usb-bus, only configurations of the device are accepted.
let status = if state_srv.config_values.lock().contains(&configuration)
{
state_srv.set_configuration_status.load(Ordering::Relaxed)
} else {
zx::sys::ZX_ERR_INVALID_ARGS
};
let _ = responder.send(status_result(status));
}
fhardware_usb::UsbRequest::EnableEndpoint {
ep_desc,
ss_com_desc,
enable,
responder,
} => {
// Like the xHCI driver, enabling an enabled endpoint or disabling a
// disabled one fails.
let address = ep_desc.b_endpoint_address;
let mut enabled = state_srv.enabled_endpoints.lock();
let ok = if enable {
enabled.insert(address)
} else {
enabled.remove(&address)
};
if ok {
state_srv.enable_log.lock().push((
address,
enable,
ep_desc.w_max_packet_size,
));
if enable {
state_srv
.ss_companion_log
.lock()
.push((address, ss_com_desc.b_max_burst));
}
let _ = responder.send(Ok(()));
} else {
let _ = responder.send(Err(zx::sys::ZX_ERR_BAD_STATE));
}
}
fhardware_usb::UsbRequest::ResetEndpoint { ep_address, responder } => {
state_srv.last_reset_endpoint.store(ep_address, Ordering::Relaxed);
state_srv.reset_endpoint_count.fetch_add(1, Ordering::Relaxed);
let status = state_srv.reset_endpoint_status.load(Ordering::Relaxed);
let _ = responder.send(status_result(status));
}
fhardware_usb::UsbRequest::ConnectToEndpoint { ep_addr, ep, responder } => {
if let Some(hook) = state_srv.on_connect_to_endpoint.lock().take() {
hook();
}
let err =
state_srv.connect_error_once.swap(zx::sys::ZX_OK, Ordering::SeqCst);
if err != zx::sys::ZX_OK {
let _ = responder.send(Err(err));
continue;
}
state_srv.connect_count.fetch_add(1, Ordering::SeqCst);
let _ = responder.send(Ok(()));
if state_srv
.close_endpoint_before_register
.swap(false, Ordering::SeqCst)
{
drop(ep);
continue;
}
let ep_state = Arc::clone(&state_srv);
kernel_for_srv.kthreads.spawn_future(
move || serve_mock_endpoint(ep_addr, ep, ep_state),
"mock_usb_ep_server",
);
}
_ => {}
}
}
},
"mock_usb_server",
);
(sync_proxy, state)
}
/// Returns the configuration values of the configuration descriptors in `config_bytes`.
fn config_values(config_bytes: &[u8]) -> HashSet<u8> {
let mut values = HashSet::new();
let mut rest = config_bytes;
while rest.len() >= 2 && usize::from(rest[0]) >= 2 && usize::from(rest[0]) <= rest.len() {
if u32::from(rest[1]) == USB_DT_CONFIG && rest.len() >= USB_DT_CONFIG_SIZE as usize {
values.insert(rest[5]);
}
rest = &rest[usize::from(rest[0])..];
}
values
}
/// Device served by a mock USB server.
struct MockDeviceOptions {
config_bytes: Vec<u8>,
speed: u32,
active_config: u8,
cancel_drain_timeout: zx::MonotonicDuration,
}
impl MockDeviceOptions {
fn new(config_bytes: Vec<u8>) -> Self {
Self {
config_bytes,
speed: USB_SPEED_HIGH,
active_config: 1,
cancel_drain_timeout: DEFAULT_CANCEL_DRAIN_TIMEOUT,
}
}
}
/// Runs `test` against device `DEV_NUM` served by a mock USB server.
async fn run_with_mock_device_options<F>(options: MockDeviceOptions, test: F)
where
F: FnOnce(&CurrentTask, &Arc<UsbDevice>, &Arc<MockUsbServerState>) + Send + Sync + 'static,
{
let mock_slot: Arc<Mutex<Option<Arc<MockUsbServerState>>>> = Default::default();
let mock_for_build = Arc::clone(&mock_slot);
run_with_device(
move |current_task| {
let (sync_proxy, mock) = spawn_mock_usb_server(
current_task.kernel(),
config_values(&options.config_bytes),
);
*mock_for_build.lock() = Some(mock);
let (device_desc, _) = create_mock_hid_descriptors();
let mut device =
UsbDevice::new(1, DEV_NUM, options.speed, device_desc, options.config_bytes);
device.sync_proxy = Some(sync_proxy);
device.cancel_drain_timeout = options.cancel_drain_timeout;
device.state.get_mut().active_config = options.active_config;
device
},
move |current_task, device| {
let mock = mock_slot.lock().take().unwrap();
test(current_task, device, &mock);
},
)
.await;
}
/// Runs `test` against device `DEV_NUM` served by a mock USB server with configuration
/// descriptors `config_bytes`.
async fn run_with_mock_device<F>(config_bytes: Vec<u8>, test: F)
where
F: FnOnce(&CurrentTask, &Arc<UsbDevice>, &Arc<MockUsbServerState>) + Send + Sync + 'static,
{
run_with_mock_device_options(MockDeviceOptions::new(config_bytes), test).await;
}
#[::fuchsia::test]
async fn test_usb_device_descriptor_layout() {
assert_eq!(std::mem::size_of::<usb_device_descriptor>(), 18);
assert_eq!(std::mem::size_of::<usb_config_descriptor>(), 9);
assert_eq!(std::mem::size_of::<usb_interface_descriptor>(), 9);
assert_eq!(std::mem::size_of::<usb_endpoint_descriptor>(), 9);
let (device_desc, config_bytes) = create_mock_hid_descriptors();
let device = UsbDevice::new(1, 2, USB_SPEED_HIGH, device_desc, config_bytes);
assert_eq!(device.bus_num, 1);
assert_eq!(device.dev_num, 2);
assert_eq!(device.minor(), 1);
assert_eq!(device.speed, USB_SPEED_HIGH);
assert_eq!(device.raw_descriptors.len(), 18 + 9 + 9 + 7 + 7);
}
#[::fuchsia::test]
async fn test_usbfs_registration_and_devtmpfs() {
spawn_kernel_with_features_and_run(
async |current_task| {
let kernel = current_task.kernel();
let manager = usbfs_init(kernel).expect("usbfs_init failed");
let (device_desc, config_bytes) = create_mock_hid_descriptors();
let usb_dev =
Arc::new(UsbDevice::new(1, 2, USB_SPEED_HIGH, device_desc, config_bytes));
let registered = manager.add_device(kernel, usb_dev).expect("add_device failed");
let metadata = registered.metadata().expect("metadata present");
assert_eq!(metadata.devname.as_slice(), b"bus/usb/001/002");
assert_eq!(metadata.devt.major(), USB_DEVICE_MAJOR);
assert_eq!(metadata.devt.minor(), 1);
let node = open_dev_node(current_task, b"bus/usb/001/002".into(), OpenFlags::RDWR)
.expect("open_dev_node failed");
assert!(node.ops().is_seekable());
// Verify device appears under /sys/bus/usb/devices/1-2.
assert!(
kernel
.device_registry
.objects
.root
.lookup("bus/usb/devices/1-2".into())
.is_some()
);
manager.remove_device(current_task, registered).expect("remove_device failed");
// Verify device is removed from /sys/bus/usb/devices/1-2.
assert!(
kernel
.device_registry
.objects
.root
.lookup("bus/usb/devices/1-2".into())
.is_none()
);
},
KernelFeatures { devtmpfs: true, ..Default::default() },
)
.await;
}
#[::fuchsia::test]
async fn test_usbfs_read_descriptors() {
spawn_kernel_with_features_and_run(
async |current_task| {
let kernel = current_task.kernel();
let manager = usbfs_init(kernel).expect("usbfs_init failed");
let (device_desc, config_bytes) = create_mock_hid_descriptors();
let mut expected_raw = Vec::new();
expected_raw.extend_from_slice(device_desc.as_bytes());
expected_raw.extend_from_slice(&config_bytes);
let usb_dev = Arc::new(UsbDevice::new(
1,
3,
USB_SPEED_FULL,
device_desc,
config_bytes.clone(),
));
let registered = manager.add_device(kernel, usb_dev).unwrap();
let file =
open_dev_node(current_task, b"bus/usb/001/003".into(), OpenFlags::RDONLY)
.expect("open_dev_node failed");
let mut output_buf = VecOutputBuffer::new(128);
let bytes_read = file.ops().read(&file, current_task, 0, &mut output_buf).unwrap();
assert_eq!(bytes_read, expected_raw.len());
assert_eq!(Vec::from(output_buf), expected_raw);
manager.remove_device(current_task, registered).unwrap();
},
KernelFeatures { devtmpfs: true, ..Default::default() },
)
.await;
}
struct MockUEventListener {
add_count: Arc<AtomicUsize>,
remove_count: Arc<AtomicUsize>,
}
impl DeviceListener for MockUEventListener {
fn on_device_event(&self, action: UEventAction, device: Device, _context: UEventContext) {
if let Some(metadata) = device.metadata() {
if metadata.devname.as_slice() == b"bus/usb/001/005" {
match action {
UEventAction::Add => {
self.add_count.fetch_add(1, Ordering::SeqCst);
}
UEventAction::Remove => {
self.remove_count.fetch_add(1, Ordering::SeqCst);
}
_ => {}
}
}
}
}
}
#[::fuchsia::test]
async fn test_usbfs_uevent_emission() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let manager = usbfs_init(kernel).expect("usbfs_init failed");
let add_count = Arc::new(AtomicUsize::new(0));
let remove_count = Arc::new(AtomicUsize::new(0));
let listener = MockUEventListener {
add_count: add_count.clone(),
remove_count: remove_count.clone(),
};
let _listener_key = kernel.device_registry.register_listener(listener);
let (device_desc, config_bytes) = create_mock_hid_descriptors();
let usb_dev = Arc::new(UsbDevice::new(1, 5, USB_SPEED_HIGH, device_desc, config_bytes));
let registered = manager.add_device(kernel, usb_dev).unwrap();
let props: HashMap<FsString, FsString> =
registered.get_uevent_properties_list().into_iter().collect();
assert_eq!(props.get(&FsString::from(b"SUBSYSTEM")).unwrap().as_slice(), b"usb");
assert_eq!(props.get(&FsString::from(b"DEVTYPE")).unwrap().as_slice(), b"usb_device");
assert_eq!(props.get(&FsString::from(b"BUSNUM")).unwrap().as_slice(), b"001");
assert_eq!(props.get(&FsString::from(b"DEVNUM")).unwrap().as_slice(), b"005");
assert_eq!(
props.get(&FsString::from(b"PRODUCT")).unwrap().as_slice(),
b"1234/5678/543"
);
assert_eq!(add_count.load(Ordering::SeqCst), 1);
assert_eq!(remove_count.load(Ordering::SeqCst), 0);
manager.remove_device(current_task, registered).unwrap();
assert_eq!(remove_count.load(Ordering::SeqCst), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_multidevice_minor_allocation_and_cleanup() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let manager = usbfs_init(kernel).expect("usbfs_init failed");
let dev1 = manager.allocate_dev_num().expect("alloc 1");
let dev2 = manager.allocate_dev_num().expect("alloc 2");
assert_eq!(dev1, 2);
assert_eq!(dev2, 3);
manager.release_dev_num(dev1);
let dev3 = manager.allocate_dev_num().expect("alloc 3");
assert_eq!(dev3, 4, "Monotonic device number allocation must increment");
manager.release_dev_num(dev3);
manager.release_dev_num(dev2);
})
.await;
}
#[::fuchsia::test]
async fn test_watch_usb_devices_missing_service_handled_gracefully() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let manager = usbfs_init(kernel).expect("usbfs_init failed");
assert!(manager.allocate_dev_num().is_some());
})
.await;
}
#[::fuchsia::test]
async fn test_usbfs_interface_claim_release_lifecycle() {
run_with_unconnected_device(hid_config(), |current_task| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(claim(current_task, &file1, arg, 0), Ok(SUCCESS));
assert_eq!(claim(current_task, &file1, arg, 0), Ok(SUCCESS));
// Claiming non-existent interface < 64 returns ENOENT; >= 64 returns EINVAL.
assert_eq!(claim(current_task, &file1, arg, 5), error!(ENOENT));
assert_eq!(claim(current_task, &file1, arg, 99), error!(EINVAL));
assert_eq!(claim(current_task, &file2, arg, 0), error!(EBUSY));
// Releasing non-existent interface < 64 returns ENOENT; >= 64 returns EINVAL;
// releasing an interface not claimed by this FD returns EINVAL.
assert_eq!(release(current_task, &file1, arg, 5), error!(ENOENT));
assert_eq!(release(current_task, &file1, arg, 99), error!(EINVAL));
assert_eq!(release(current_task, &file2, arg, 0), error!(EINVAL));
assert_eq!(release(current_task, &file1, arg, 0), Ok(SUCCESS));
assert_eq!(claim(current_task, &file2, arg, 0), Ok(SUCCESS));
})
.await;
}
#[::fuchsia::test]
async fn test_usbfs_interface_claim_release_on_file_close() {
run_with_unconnected_device(hid_config(), |current_task| {
let (arg, _) = map_test_buffers(current_task);
{
let file1 = open_device(current_task, DEV_NUM);
assert_eq!(claim(current_task, &file1, arg, 0), Ok(SUCCESS));
}
current_task.kernel().delayed_releaser.apply(current_task);
let file2 = open_device(current_task, DEV_NUM);
assert_eq!(claim(current_task, &file2, arg, 0), Ok(SUCCESS));
})
.await;
}
#[::fuchsia::test]
async fn test_usbfs_empty_descriptors_have_no_interfaces_or_endpoints() {
run_with_unconnected_device(vec![], |current_task| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(claim(current_task, &file, arg, 0), error!(ENOENT));
// Without a configuration, endpoints cannot be looked up.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 0, data), error!(ESRCH));
})
.await;
}
#[::fuchsia::test]
async fn test_usbfs_control_transfer_size_limit() {
run_with_unconnected_device(hid_config(), |current_task| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let ctrl = ctrl_transfer(
USB_DIR_IN,
USB_REQ_GET_DESCRIPTOR as u8,
(USB_DT_DEVICE as u16) << 8,
0,
(MAX_USBDEVFS_CONTROL_SIZE + 1) as u16,
1000,
UserAddress::NULL,
);
assert_eq!(
ioctl_with(current_task, &file, USBDEVFS_CONTROL, arg, &ctrl),
error!(EINVAL)
);
})
.await;
}
#[::fuchsia::test]
fn test_compat32_control_and_bulk_struct_layout() {
#[cfg(target_arch = "aarch64")]
{
assert_eq!(std::mem::size_of::<arch32::usbdevfs_ctrltransfer>(), 16);
assert_eq!(std::mem::size_of::<arch32::usbdevfs_bulktransfer>(), 16);
assert_eq!(std::mem::size_of::<arch32::usbdevfs_ioctl>(), 12);
for (arch32_request, expected) in [
(arch32::USBDEVFS_CONTROL, USBDEVFS_CONTROL),
(arch32::USBDEVFS_BULK, USBDEVFS_BULK),
(arch32::USBDEVFS_SETINTERFACE, USBDEVFS_SETINTERFACE),
(arch32::USBDEVFS_SETCONFIGURATION, USBDEVFS_SETCONFIGURATION),
(arch32::USBDEVFS_GETDRIVER, USBDEVFS_GETDRIVER),
(arch32::USBDEVFS_CONNECTINFO, USBDEVFS_CONNECTINFO),
(arch32::USBDEVFS_RESET, USBDEVFS_RESET),
(arch32::USBDEVFS_CLEAR_HALT, USBDEVFS_CLEAR_HALT),
(arch32::USBDEVFS_RELEASEINTERFACE, USBDEVFS_RELEASEINTERFACE),
(arch32::USBDEVFS_CLAIMINTERFACE, USBDEVFS_CLAIMINTERFACE),
(arch32::USBDEVFS_GET_CAPABILITIES, USBDEVFS_GET_CAPABILITIES),
(arch32::USBDEVFS_DISCONNECT_CLAIM, USBDEVFS_DISCONNECT_CLAIM),
(arch32::USBDEVFS_GET_SPEED, USBDEVFS_GET_SPEED),
(arch32::USBDEVFS_IOCTL, USBDEVFS_IOCTL),
] {
assert_eq!(
canonicalize_ioctl_request(true, arch32_request),
expected,
"request {arch32_request:#x}"
);
}
assert_eq!(
canonicalize_ioctl_request(false, arch32::USBDEVFS_CONTROL),
arch32::USBDEVFS_CONTROL
);
let ctrl32 = arch32::usbdevfs_ctrltransfer {
bRequestType: USB_DIR_IN as u8,
bRequest: USB_REQ_GET_DESCRIPTOR as u8,
wValue: (USB_DT_DEVICE as u16) << 8,
wIndex: 0,
wLength: 18,
timeout: 500,
data: uaddr32 { addr: 0x1234_5678 },
};
let converted_ctrl = usbdevfs_ctrltransfer::from(ctrl32);
assert_eq!(converted_ctrl.bRequestType, USB_DIR_IN as u8);
assert_eq!(converted_ctrl.wLength, 18);
assert_eq!(converted_ctrl.data.addr, 0x1234_5678);
let bulk32 = arch32::usbdevfs_bulktransfer {
ep: u32::from(EP1_IN),
len: 64,
timeout: 250,
data: uaddr32 { addr: 0x8765_4321 },
};
let converted_bulk = usbdevfs_bulktransfer::from(bulk32);
assert_eq!(converted_bulk.ep, u32::from(EP1_IN));
assert_eq!(converted_bulk.len, 64);
assert_eq!(converted_bulk.data.addr, 0x8765_4321);
let ioctl32 = arch32::usbdevfs_ioctl {
ifno: 1,
ioctl_code: USBDEVFS_DISCONNECT as i32,
data: uaddr32 { addr: 0xdead_beef },
};
let converted_ioctl = usbdevfs_ioctl::from(ioctl32);
assert_eq!(converted_ioctl.ifno, 1);
assert_eq!(converted_ioctl.ioctl_code, USBDEVFS_DISCONNECT as i32);
assert_eq!(converted_ioctl.data.addr, 0xdead_beef);
}
assert_eq!(std::mem::size_of::<usbdevfs_ctrltransfer>(), 24);
assert_eq!(std::mem::size_of::<usbdevfs_bulktransfer>(), 24);
assert_eq!(std::mem::size_of::<usbdevfs_ioctl>(), 16);
assert_eq!(canonicalize_ioctl_request(false, USBDEVFS_CONTROL), USBDEVFS_CONTROL);
}
#[::fuchsia::test]
async fn test_disconnected_device_rejects_all_ioctls_with_enodev() {
run_with_mock_device(hid_config(), |current_task, device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
device.on_disconnect();
for request in [
USBDEVFS_CONTROL,
USBDEVFS_BULK,
USBDEVFS_CLAIMINTERFACE,
USBDEVFS_RELEASEINTERFACE,
USBDEVFS_SETINTERFACE,
USBDEVFS_SETCONFIGURATION,
USBDEVFS_CLEAR_HALT,
USBDEVFS_RESET,
USBDEVFS_GETDRIVER,
USBDEVFS_GET_SPEED,
USBDEVFS_GET_CAPABILITIES,
USBDEVFS_CONNECTINFO,
USBDEVFS_DISCONNECT_CLAIM,
USBDEVFS_IOCTL,
] {
assert_eq!(
file.ops().ioctl(&file, current_task, request, arg.into()),
error!(ENODEV),
"request {request:#x}"
);
}
let mut output_buf = VecOutputBuffer::new(16);
let res_read = file.ops().read(&file, current_task, 0, &mut output_buf);
assert_eq!(res_read, error!(ENODEV));
})
.await;
}
#[::fuchsia::test]
async fn test_closed_usb_channel_returns_enodev_for_control_and_bulk() {
run_with_device(
|_| {
let (client_end, server_end) = create_endpoints::<fhardware_usb::UsbMarker>();
drop(server_end);
let (device_desc, config_bytes) = create_mock_hid_descriptors();
let mut device =
UsbDevice::new(1, DEV_NUM, USB_SPEED_HIGH, device_desc, config_bytes);
device.sync_proxy = Some(Arc::new(fhardware_usb::UsbSynchronousProxy::new(
client_end.into_channel(),
)));
device
},
|current_task, _device| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 1000),
error!(ENODEV)
);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ENODEV));
},
)
.await;
}
#[::fuchsia::test]
async fn test_usbfs_invalid_and_unknown_ioctls() {
run_with_unconnected_device(hid_config(), |current_task| {
let file = open_device(current_task, DEV_NUM);
assert_eq!(ioctl_no_arg(current_task, &file, 0xdeadbeef), error!(ENOTTY));
assert_eq!(ioctl_no_arg(current_task, &file, USBDEVFS_GETDRIVER), error!(EFAULT));
assert_eq!(
ioctl_no_arg(current_task, &file, USBDEVFS_GET_SPEED),
Ok((USB_SPEED_HIGH as usize).into())
);
// Opening with O_RDONLY rejects all USBDEVFS_* ioctls with EPERM.
let ro_file = open_dev_node(current_task, b"bus/usb/001/002".into(), OpenFlags::RDONLY)
.expect("open ro_file");
assert_eq!(ioctl_no_arg(current_task, &ro_file, USBDEVFS_GET_SPEED), error!(EPERM));
})
.await;
}
#[::fuchsia::test]
async fn test_usbfs_fidl_synchronous_control_transfers() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
current_task.write_object(UserRef::new(data), &[0u8; 4]).unwrap();
let ctrl_in = ctrl_transfer(USB_DIR_IN, TEST_REQUEST, 0x0102, 0, 4, 1000, data);
assert_eq!(
ioctl_with(current_task, &file, USBDEVFS_CONTROL, arg, &ctrl_in),
Ok(4.into())
);
assert_eq!(read_bytes::<4>(current_task, data), [TEST_REQUEST, 0x02, 0xca, 0xfe]);
let out_payload: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
current_task.write_object(UserRef::new(data), &out_payload).unwrap();
let ctrl_out = ctrl_transfer(USB_DIR_OUT, TEST_REQUEST, 0, 0, 4, 1000, data);
assert_eq!(
ioctl_with(current_task, &file, USBDEVFS_CONTROL, arg, &ctrl_out),
Ok(4.into())
);
assert_eq!(*mock.last_ctrl_out.lock(), out_payload.to_vec());
})
.await;
}
#[::fuchsia::test]
async fn test_control_timeout_zero_means_no_timeout() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
for timeout in [0, 1000] {
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, timeout),
Ok(0.into())
);
}
assert_eq!(
*mock.ctrl_timeouts.lock(),
vec![zx::MonotonicDuration::INFINITE.into_nanos(), 1_000_000_000]
);
})
.await;
}
#[::fuchsia::test]
async fn test_control_device_errors_map_to_errno() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
for (status, expected) in [
(zx::sys::ZX_ERR_IO_REFUSED, error!(EPIPE)),
(zx::sys::ZX_ERR_TIMED_OUT, error!(ETIMEDOUT)),
(zx::sys::ZX_ERR_NOT_FOUND, error!(ENOENT)),
(zx::sys::ZX_ERR_ALREADY_BOUND, error!(EBUSY)),
(zx::sys::ZX_ERR_IO_NOT_PRESENT, error!(ENODEV)),
(zx::sys::ZX_ERR_PEER_CLOSED, error!(ENODEV)),
(zx::sys::ZX_ERR_INVALID_ARGS, error!(EINVAL)),
(zx::sys::ZX_ERR_BAD_STATE, error!(EHOSTUNREACH)),
(zx::sys::ZX_ERR_CANCELED, error!(ECONNRESET)),
(zx::sys::ZX_ERR_NO_MEMORY, error!(ENOMEM)),
(zx::sys::ZX_ERR_INTERNAL, error!(EIO)),
] {
mock.ctrl_transfer_error.store(status, Ordering::Relaxed);
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 1000),
expected,
"status {status}"
);
}
})
.await;
}
#[::fuchsia::test]
async fn test_control_wait_ignores_maskable_signals() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
current_task.thread_group.signal_actions.set(
SIGUSR1,
sigaction_t { sa_handler: uaddr { addr: 0x1000 }, ..sigaction_t::default() },
);
send_standard_signal(current_task, SignalInfo::kernel(SIGUSR1));
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 0),
Ok(0.into()),
"A maskable signal must not interrupt a control transfer"
);
assert_eq!(mock.ctrl_timeouts.lock().len(), 1);
assert!(current_task.read().has_signal_pending(SIGUSR1));
})
.await;
}
#[::fuchsia::test]
async fn test_control_wait_is_killable() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
mock.hold_ctrl_transfers.store(true, Ordering::SeqCst);
set_pending_sigkill(current_task, true);
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 0),
error!(EINTR),
"SIGKILL must end a control transfer without timeout"
);
set_pending_sigkill(current_task, false);
// Complete the abandoned transfer so the kernel thread running it exits.
mock.ctrl_held.wait_signaled();
for responder in mock.held_ctrl_responders.lock().drain(..) {
let _ = responder.send(Err(zx::sys::ZX_ERR_CANCELED));
}
})
.await;
}
#[::fuchsia::test]
async fn test_control_waits_for_abandoned_control_transfer() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
mock.hold_ctrl_transfers.store(true, Ordering::SeqCst);
set_pending_sigkill(current_task, true);
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 0),
error!(EINTR)
);
mock.ctrl_held.wait_signaled();
// The abandoned transfer is still running, so the next one waits for it instead of
// starting another one, and that wait is killable too.
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 0),
error!(EINTR)
);
assert_eq!(mock.ctrl_timeouts.lock().len(), 1);
set_pending_sigkill(current_task, false);
// Once the abandoned transfer completes, the next transfer runs.
mock.hold_ctrl_transfers.store(false, Ordering::SeqCst);
for responder in mock.held_ctrl_responders.lock().drain(..) {
let _ = responder.send(Ok(&[]));
}
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 0),
Ok(0.into())
);
assert_eq!(mock.ctrl_timeouts.lock().len(), 2);
})
.await;
}
#[::fuchsia::test]
async fn test_control_recipient_requires_interface_claim() {
let config = build_config(
1,
&[
alt(0, 0, vec![(EP1_IN, XFER_BULK, 64)]),
TestAlt { interface: 1, alt: 0, class: CLASS_PRINTER, endpoints: vec![] },
],
);
run_with_mock_device(config, |current_task, _device, _mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let set_interface_request = USB_REQ_SET_INTERFACE as u8;
let clear_feature = USB_REQ_CLEAR_FEATURE as u8;
assert_eq!(claim(current_task, &file1, arg, 0), Ok(SUCCESS));
assert_eq!(claim(current_task, &file1, arg, 1), Ok(SUCCESS));
assert_eq!(
control(current_task, &file2, arg, INTERFACE_OUT, set_interface_request, 0, 1000),
error!(EBUSY)
);
let ep1_in = u16::from(EP1_IN);
assert_eq!(
control(current_task, &file2, arg, ENDPOINT_OUT, clear_feature, ep1_in, 1000),
error!(EBUSY)
);
// The endpoint number without its direction bit also resolves to the endpoint.
let ep1_number = u16::from(EP1_IN & !(USB_DIR_IN as u8));
assert_eq!(
control(current_task, &file2, arg, ENDPOINT_OUT, clear_feature, ep1_number, 1000),
error!(EBUSY)
);
assert_eq!(
control(current_task, &file2, arg, INTERFACE_OUT, set_interface_request, 64, 1000),
error!(EINVAL)
);
// Endpoint 0, device, and vendor requests need no claim.
let ep0_in = USB_DIR_IN as u16;
assert_eq!(
control(current_task, &file2, arg, ENDPOINT_OUT, clear_feature, ep0_in, 1000),
Ok(0.into())
);
assert_eq!(
control(current_task, &file2, arg, USB_DIR_OUT, TEST_REQUEST, 0, 1000),
Ok(0.into())
);
let vendor_interface = USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_INTERFACE;
assert_eq!(
control(current_task, &file2, arg, vendor_interface, TEST_REQUEST, 0, 1000),
Ok(0.into())
);
// The printer class GET_DEVICE_ID request is allowed on a claimed interface.
assert_eq!(
control(current_task, &file2, arg, PRINTER_GET_DEVICE_ID, 0, 1 << 8, 1000),
Ok(0.into())
);
// None of these requests took the interfaces away from the first FD.
assert_eq!(release(current_task, &file1, arg, 1), Ok(SUCCESS));
// An interface-recipient request auto-claims the unclaimed interface.
assert_eq!(release(current_task, &file1, arg, 0), Ok(SUCCESS));
assert_eq!(
control(current_task, &file2, arg, INTERFACE_OUT, set_interface_request, 0, 1000),
Ok(0.into())
);
assert_eq!(claim(current_task, &file1, arg, 0), error!(EBUSY));
})
.await;
}
#[::fuchsia::test]
async fn test_control_interface_recipient_on_unconfigured_device() {
let options =
MockDeviceOptions { active_config: 0, ..MockDeviceOptions::new(hid_config()) };
run_with_mock_device_options(options, |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let request = USB_REQ_SET_INTERFACE as u8;
// The configuration is checked before the interface number.
for index in [0, 99] {
assert_eq!(
control(current_task, &file, arg, INTERFACE_OUT, request, index, 1000),
error!(EHOSTUNREACH),
"interface {index}"
);
}
// Requests to the device are allowed.
assert_eq!(
control(current_task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 1000),
Ok(0.into())
);
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_only_disables_endpoints_of_interface() {
// Interface 0 alt 0 has EP1, alt 1 has no endpoints, interface 1 has EP2.
let config = build_config(
1,
&[
alt(0, 0, vec![(EP1_IN, XFER_INT, 64), (EP1_OUT, XFER_INT, 64)]),
alt(0, 1, vec![]),
alt(1, 0, vec![(EP2_IN, XFER_BULK, 64)]),
],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP2_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
// Switching interface 0 to alt 1 disables EP1 IN but leaves EP2 IN alone.
assert_eq!(set_interface(current_task, &file, arg, 0, 1), Ok(SUCCESS));
assert_eq!(*mock.last_set_iface.lock(), Some((0, 1)));
assert_eq!(*mock.enabled_endpoints.lock(), HashSet::from([EP2_IN]));
// EP2 IN keeps working without being enabled again.
assert_eq!(bulk(current_task, &file, arg, EP2_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(
*mock.enable_log.lock(),
vec![(EP2_IN, true, 64), (EP1_IN, true, 64), (EP1_IN, false, 64)]
);
// EP1 IN is not in the active alternate setting anymore.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 2, 1000, data), error!(EINVAL));
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_reenables_endpoints_after_switching_back() {
let config = build_config(
1,
&[
alt(0, 0, vec![]),
alt(0, 1, vec![(EP1_IN, XFER_BULK, 64)]),
alt(0, 2, vec![(EP1_IN, XFER_BULK, 512)]),
alt(1, 0, vec![(EP2_IN, XFER_BULK, 64)]),
],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP2_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(set_interface(current_task, &file, arg, 0, 1), Ok(SUCCESS));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(set_interface(current_task, &file, arg, 0, 0), Ok(SUCCESS));
assert_eq!(set_interface(current_task, &file, arg, 0, 1), Ok(SUCCESS));
// Fails if EP1 IN was not disabled when it left the active alternate setting, since
// enabling an enabled endpoint fails.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
// EP1 IN changes descriptors in alt 2, so it is enabled again with them.
assert_eq!(set_interface(current_task, &file, arg, 0, 2), Ok(SUCCESS));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(
*mock.enable_log.lock(),
vec![
(EP2_IN, true, 64),
(EP1_IN, true, 64),
(EP1_IN, false, 64),
(EP1_IN, true, 64),
(EP1_IN, false, 64),
(EP1_IN, true, 512),
]
);
// The endpoint channel is reused across disable and enable.
assert_eq!(mock.connect_count.load(Ordering::SeqCst), 2);
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_invalid_alt_does_not_claim() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(set_interface(current_task, &file1, arg, 0, 0x100), error!(EINVAL));
assert_eq!(set_interface(current_task, &file1, arg, 0, 7), error!(EINVAL));
// The interface is not claimed by the first FD.
assert_eq!(claim(current_task, &file2, arg, 0), Ok(SUCCESS));
assert_eq!(set_interface(current_task, &file1, arg, 0, 0), error!(EBUSY));
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_errno_order() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(set_interface(current_task, &file, arg, 99, 0x100), error!(EINVAL));
// A missing interface is reported before an invalid alternate setting.
assert_eq!(set_interface(current_task, &file, arg, 5, 0x100), error!(ENOENT));
})
.await;
let options =
MockDeviceOptions { active_config: 0, ..MockDeviceOptions::new(hid_config()) };
run_with_mock_device_options(options, |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
// The configuration is checked first.
assert_eq!(set_interface(current_task, &file, arg, 99, 0x100), error!(EHOSTUNREACH));
assert_eq!(set_interface(current_task, &file, arg, 0, 0), error!(EHOSTUNREACH));
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_stall_on_single_alt_setting_succeeds() {
let config =
build_config(1, &[alt(0, 0, vec![(EP1_IN, XFER_BULK, 64), (EP2_OUT, XFER_BULK, 64)])]);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
mock.set_interface_status.store(zx::sys::ZX_ERR_IO_REFUSED, Ordering::Relaxed);
assert_eq!(set_interface(current_task, &file, arg, 0, 0), Ok(SUCCESS));
// The halt of each endpoint of the interface is cleared instead.
let clear_halt =
|ep: u8| (ENDPOINT_OUT as u8, USB_REQ_CLEAR_FEATURE as u8, 0, u16::from(ep));
assert_eq!(*mock.ctrl_setups.lock(), vec![clear_halt(EP2_OUT), clear_halt(EP1_IN)]);
// The endpoints were disabled, and are enabled again on next use.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(
*mock.enable_log.lock(),
vec![(EP1_IN, true, 64), (EP1_IN, false, 64), (EP1_IN, true, 64)]
);
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_stall_on_multiple_alt_settings_fails() {
let config = build_config(
1,
&[alt(0, 0, vec![(EP1_IN, XFER_BULK, 64)]), alt(0, 1, vec![(EP1_IN, XFER_BULK, 64)])],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
mock.set_interface_status.store(zx::sys::ZX_ERR_IO_REFUSED, Ordering::Relaxed);
assert_eq!(set_interface(current_task, &file, arg, 0, 1), error!(EPIPE));
assert!(mock.ctrl_setups.lock().is_empty());
})
.await;
}
#[::fuchsia::test]
async fn test_setinterface_failure_keeps_alt_setting() {
let config = build_config(
1,
&[alt(0, 0, vec![(EP1_IN, XFER_BULK, 64)]), alt(0, 1, vec![(EP2_IN, XFER_BULK, 64)])],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.set_interface_status.store(zx::sys::ZX_ERR_INTERNAL, Ordering::Relaxed);
assert_eq!(set_interface(current_task, &file, arg, 0, 1), error!(EIO));
// Alternate setting 0 is still active.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(bulk(current_task, &file, arg, EP2_IN, 4, 1000, data), error!(EINVAL));
})
.await;
}
#[::fuchsia::test]
async fn test_setconfiguration_busy_with_any_claim() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(claim(current_task, &file1, arg, 0), Ok(SUCCESS));
// A claim by another FD or by the caller prevents the change.
assert_eq!(set_configuration(current_task, &file2, arg, 1), error!(EBUSY));
assert_eq!(set_configuration(current_task, &file1, arg, 1), error!(EBUSY));
// The claims are checked before the configuration value.
assert_eq!(set_configuration(current_task, &file1, arg, 0x100), error!(EBUSY));
assert!(mock.set_config_log.lock().is_empty());
})
.await;
}
#[::fuchsia::test]
async fn test_setconfiguration_disables_endpoints() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(release(current_task, &file, arg, 0), Ok(SUCCESS));
assert_eq!(set_configuration(current_task, &file, arg, 1), Ok(SUCCESS));
assert_eq!(*mock.set_config_log.lock(), vec![1]);
assert!(mock.enabled_endpoints.lock().is_empty());
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(
*mock.enable_log.lock(),
vec![(EP1_IN, true, 64), (EP1_IN, false, 64), (EP1_IN, true, 64)]
);
})
.await;
}
#[::fuchsia::test]
async fn test_setconfiguration_rejects_invalid_values() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
for value in [0x100, -2, 5] {
assert_eq!(
set_configuration(current_task, &file, arg, value),
error!(EINVAL),
"configuration {value}"
);
}
assert!(mock.set_config_log.lock().is_empty());
})
.await;
}
#[::fuchsia::test]
async fn test_setconfiguration_unconfigure_rejected_by_driver() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(release(current_task, &file, arg, 0), Ok(SUCCESS));
// -1 unconfigures the device, which the USB driver does not support.
assert_eq!(set_configuration(current_task, &file, arg, -1), error!(EINVAL));
assert_eq!(*mock.set_config_log.lock(), vec![0]);
// The configuration is unchanged, and the disabled endpoint is enabled again.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(*mock.enabled_endpoints.lock(), HashSet::from([EP1_IN]));
})
.await;
}
#[::fuchsia::test]
async fn test_setconfiguration_failure_keeps_configuration() {
let mut config = hid_config();
config.extend(build_config(2, &[alt(3, 0, vec![])]));
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.set_configuration_status.store(zx::sys::ZX_ERR_IO_REFUSED, Ordering::Relaxed);
assert_eq!(set_configuration(current_task, &file, arg, 2), error!(EPIPE));
// Configuration 1 is still active.
assert_eq!(claim(current_task, &file, arg, 3), error!(ENOENT));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
})
.await;
}
#[::fuchsia::test]
async fn test_unconfigured_device() {
let options =
MockDeviceOptions { active_config: 0, ..MockDeviceOptions::new(hid_config()) };
run_with_mock_device_options(options, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(claim(current_task, &file, arg, 0), error!(ENOENT));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ESRCH));
assert_eq!(clear_halt(current_task, &file, arg, u32::from(EP1_IN)), error!(ESRCH));
assert_eq!(set_configuration(current_task, &file, arg, 1), Ok(SUCCESS));
assert_eq!(*mock.set_config_log.lock(), vec![1]);
assert_eq!(claim(current_task, &file, arg, 0), Ok(SUCCESS));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
})
.await;
}
#[::fuchsia::test]
async fn test_clear_halt_rejects_invalid_endpoints() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(clear_halt(current_task, &file, arg, 0x10), error!(EINVAL));
assert_eq!(clear_halt(current_task, &file, arg, 0x181), error!(EINVAL));
assert_eq!(clear_halt(current_task, &file, arg, 0x85), error!(ENOENT));
assert!(mock.ctrl_setups.lock().is_empty());
})
.await;
}
#[::fuchsia::test]
async fn test_clear_halt_skips_reset_endpoint_when_clear_feature_fails() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
mock.ctrl_transfer_error.store(zx::sys::ZX_ERR_IO_REFUSED, Ordering::Relaxed);
assert_eq!(clear_halt(current_task, &file, arg, u32::from(EP1_IN)), error!(EPIPE));
assert_eq!(mock.reset_endpoint_count.load(Ordering::Relaxed), 0);
})
.await;
}
#[::fuchsia::test]
async fn test_clear_halt_resets_halted_endpoint() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(clear_halt(current_task, &file, arg, u32::from(EP1_IN)), Ok(SUCCESS));
assert_eq!(
*mock.ctrl_setups.lock(),
vec![(ENDPOINT_OUT as u8, USB_REQ_CLEAR_FEATURE as u8, 0, u16::from(EP1_IN))]
);
assert_eq!(mock.last_reset_endpoint.load(Ordering::Relaxed), EP1_IN);
assert_eq!(mock.reset_endpoint_count.load(Ordering::Relaxed), 1);
// The endpoint stays enabled.
assert_eq!(*mock.enable_log.lock(), vec![(EP1_IN, true, 64)]);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
})
.await;
}
#[::fuchsia::test]
async fn test_clear_halt_reenables_endpoint_that_is_not_halted() {
for status in [zx::sys::ZX_ERR_INVALID_ARGS, zx::sys::ZX_ERR_NOT_SUPPORTED] {
run_with_mock_device(hid_config(), move |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
// The host controller does not reset an endpoint that is not halted, so the
// endpoint is disabled to reset the host data toggle when it is enabled again.
mock.reset_endpoint_status.store(status, Ordering::Relaxed);
assert_eq!(clear_halt(current_task, &file, arg, u32::from(EP1_IN)), Ok(SUCCESS));
assert!(mock.enabled_endpoints.lock().is_empty());
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(
*mock.enable_log.lock(),
vec![(EP1_IN, true, 64), (EP1_IN, false, 64), (EP1_IN, true, 64)]
);
// The endpoint channel stays open.
assert_eq!(mock.connect_count.load(Ordering::SeqCst), 1);
assert!(!mock.endpoint_unbound.is_signaled());
})
.await;
}
}
#[::fuchsia::test]
async fn test_clear_halt_fails_on_reset_endpoint_error() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
mock.reset_endpoint_status.store(zx::sys::ZX_ERR_INTERNAL, Ordering::Relaxed);
assert_eq!(clear_halt(current_task, &file, arg, u32::from(EP1_IN)), error!(EIO));
})
.await;
}
#[::fuchsia::test]
async fn test_clear_halt_claims_interface() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(clear_halt(current_task, &file1, arg, u32::from(EP1_IN)), Ok(SUCCESS));
assert_eq!(clear_halt(current_task, &file2, arg, u32::from(EP1_IN)), error!(EBUSY));
})
.await;
}
#[::fuchsia::test]
async fn test_endpoint_lookup_searches_all_alt_settings() {
let config =
build_config(1, &[alt(0, 0, vec![]), alt(0, 1, vec![(EP1_IN, XFER_BULK, 64)])]);
run_with_mock_device(config, |current_task, _device, _mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
// EP1 IN belongs to inactive alt 1 of interface 0, which is claimed.
assert_eq!(clear_halt(current_task, &file1, arg, u32::from(EP1_IN)), Ok(SUCCESS));
assert_eq!(claim(current_task, &file2, arg, 0), error!(EBUSY));
assert_eq!(bulk(current_task, &file1, arg, EP1_IN, 4, 1000, data), error!(EINVAL));
})
.await;
}
#[::fuchsia::test]
async fn test_usbfs_null_buffer_returns_efault() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let ctrl = ctrl_transfer(USB_DIR_OUT, TEST_REQUEST, 0, 0, 4, 1000, UserAddress::NULL);
assert_eq!(
ioctl_with(current_task, &file, USBDEVFS_CONTROL, arg, &ctrl),
error!(EFAULT)
);
assert_eq!(
bulk(current_task, &file, arg, EP1_OUT, 4, 1000, UserAddress::NULL),
error!(EFAULT)
);
})
.await;
}
/// Issues `USBDEVFS_CONNECTINFO` on a buffer filled with a non-zero pattern.
fn connect_info(current_task: &CurrentTask, file: &FileHandle) -> usbdevfs_connectinfo {
let (arg, _) = map_test_buffers(current_task);
let pattern = [0xffu8; std::mem::size_of::<usbdevfs_connectinfo>()];
assert_eq!(
ioctl_with(current_task, file, USBDEVFS_CONNECTINFO, arg, &pattern),
Ok(SUCCESS)
);
current_task.read_object(UserRef::new(arg)).unwrap()
}
#[::fuchsia::test]
async fn test_capabilities_and_connectinfo() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
// None of the optional capabilities is implemented.
assert_eq!(
ioctl_with(current_task, &file, USBDEVFS_GET_CAPABILITIES, arg, &0xffff_ffffu32),
Ok(SUCCESS)
);
let caps: u32 = current_task.read_object(UserRef::new(arg)).unwrap();
assert_eq!(caps, 0);
let conn_info = connect_info(current_task, &file);
assert_eq!(conn_info.devnum, u32::from(DEV_NUM));
assert_eq!(conn_info.slow, 0);
})
.await;
}
#[::fuchsia::test]
async fn test_connectinfo_reports_low_speed_device_as_slow() {
let options =
MockDeviceOptions { speed: USB_SPEED_LOW, ..MockDeviceOptions::new(hid_config()) };
run_with_mock_device_options(options, |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let conn_info = connect_info(current_task, &file);
assert_eq!(conn_info.devnum, u32::from(DEV_NUM));
assert_eq!(conn_info.slow, 1);
})
.await;
}
#[::fuchsia::test]
async fn test_getdriver_reports_usbfs_for_claimed_interfaces() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
let getdriver = |interface: u32| {
let mut gd = usbdevfs_getdriver::new_zeroed();
gd.interface = interface;
gd.driver.fill(0x7f);
ioctl_with(current_task, &file, USBDEVFS_GETDRIVER, arg, &gd)
};
assert_eq!(getdriver(0), error!(ENODATA));
assert_eq!(getdriver(5), error!(ENODATA));
assert_eq!(getdriver(99), error!(ENODATA));
assert_eq!(claim(current_task, &file, data, 0), Ok(SUCCESS));
assert_eq!(getdriver(0), Ok(SUCCESS));
let gd: usbdevfs_getdriver = current_task.read_object(UserRef::new(arg)).unwrap();
assert_eq!(gd.interface, 0);
let name: Vec<u8> =
gd.driver.iter().map(|&c| c as u8).take_while(|&c| c != 0).collect();
assert_eq!(name, b"usbfs");
assert!(gd.driver[name.len()..].iter().all(|&c| c == 0));
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_claim_rejects_missing_interfaces() {
// Interface 3 only exists in inactive configuration 2.
let mut config = hid_config();
config.extend(build_config(2, &[alt(3, 0, vec![])]));
run_with_mock_device(config, |current_task, _device, _mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(disconnect_claim(current_task, &file, arg, 99, 0, b""), error!(EINVAL));
assert_eq!(disconnect_claim(current_task, &file, arg, 3, 0, b""), error!(EINVAL));
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_claim_claims_free_interface() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(disconnect_claim(current_task, &file1, arg, 0, 0, b""), Ok(SUCCESS));
assert_eq!(disconnect_claim(current_task, &file1, arg, 0, 0, b""), Ok(SUCCESS));
assert_eq!(claim(current_task, &file2, arg, 0), error!(EBUSY));
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_claim_driver_filters() {
run_with_mock_device(hid_config(), |current_task, _device, _mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let if_driver = USBDEVFS_DISCONNECT_CLAIM_IF_DRIVER;
let except_driver = USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER;
assert_eq!(claim(current_task, &file1, arg, 0), Ok(SUCCESS));
// The interface is bound to the "usbfs" driver.
assert_eq!(
disconnect_claim(current_task, &file2, arg, 0, if_driver, b"usb-storage"),
error!(EBUSY)
);
assert_eq!(
disconnect_claim(current_task, &file2, arg, 0, except_driver, b"usbfs"),
error!(EBUSY)
);
assert_eq!(
disconnect_claim(current_task, &file2, arg, 0, if_driver, b"usbfs"),
Ok(SUCCESS)
);
assert_eq!(
disconnect_claim(current_task, &file1, arg, 0, except_driver, b"usb-storage"),
Ok(SUCCESS)
);
assert_eq!(claim(current_task, &file2, arg, 0), error!(EBUSY));
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_claim_takes_interface_from_other_fd() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file1, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(disconnect_claim(current_task, &file2, arg, 0, 0, b""), Ok(SUCCESS));
// The requests of the previous claimant are cancelled.
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
assert_eq!(release(current_task, &file1, arg, 0), error!(EINVAL));
assert_eq!(claim(current_task, &file1, arg, 0), error!(EBUSY));
assert_eq!(bulk(current_task, &file2, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
})
.await;
}
#[::fuchsia::test]
async fn test_driver_ioctl_disconnect_and_connect() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(
file1.ioctl(current_task, USBDEVFS_IOCTL, UserAddress::NULL.into()),
error!(EFAULT)
);
assert_eq!(
driver_ioctl(current_task, &file1, arg, 99, USBDEVFS_DISCONNECT),
error!(EINVAL)
);
assert_eq!(
driver_ioctl(current_task, &file1, arg, -1, USBDEVFS_DISCONNECT),
error!(EINVAL)
);
assert_eq!(
driver_ioctl(current_task, &file1, arg, 0, USBDEVFS_DISCONNECT),
error!(ENODATA)
);
assert_eq!(driver_ioctl(current_task, &file1, arg, 0, USBDEVFS_CONNECT), Ok(SUCCESS));
assert_eq!(driver_ioctl(current_task, &file1, arg, 0, 0x1234), error!(ENOTTY));
// Claiming interface 0 on file1 makes CONNECT return EBUSY and lets file2 detach file1.
assert_eq!(bulk(current_task, &file1, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(driver_ioctl(current_task, &file2, arg, 0, USBDEVFS_CONNECT), error!(EBUSY));
assert_eq!(
driver_ioctl(current_task, &file2, arg, 0, USBDEVFS_DISCONNECT),
Ok(SUCCESS)
);
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
assert_eq!(release(current_task, &file1, arg, 0), error!(EINVAL));
assert_eq!(claim(current_task, &file2, arg, 0), Ok(SUCCESS));
assert_eq!(release(current_task, &file2, arg, 0), Ok(SUCCESS));
assert_eq!(driver_ioctl(current_task, &file2, arg, 0, USBDEVFS_CONNECT), Ok(SUCCESS));
})
.await;
}
#[::fuchsia::test]
async fn test_reset_preserves_configuration_and_claims() {
let config = build_config(
1,
&[alt(0, 0, vec![(EP1_IN, XFER_BULK, 64)]), alt(0, 1, vec![(EP2_IN, XFER_BULK, 64)])],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(set_interface(current_task, &file1, arg, 0, 1), Ok(SUCCESS));
assert_eq!(ioctl_no_arg(current_task, &file1, USBDEVFS_RESET), Ok(SUCCESS));
assert!(mock.set_config_log.lock().is_empty());
assert_eq!(claim(current_task, &file2, arg, 0), error!(EBUSY));
// Alternate setting 1 is still active.
assert_eq!(bulk(current_task, &file1, arg, EP2_IN, 4, 1000, data), Ok(4.into()));
})
.await;
}
#[::fuchsia::test]
async fn test_reset_cancels_endpoint_requests() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(ioctl_no_arg(current_task, &file, USBDEVFS_RESET), Ok(SUCCESS));
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_reset_stray_completion_is_not_mistaken_for_next_one() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
// A stray completion sent while no request is in flight must not be mistaken for
// the completion of a later request.
mock.send_stale_completion_on_cancel.store(true, Ordering::SeqCst);
assert_eq!(ioctl_no_arg(current_task, &file, USBDEVFS_RESET), Ok(SUCCESS));
mock.bulk_in_scripted_packets.lock().push_back((vec![1, 2, 3, 4], zx::sys::ZX_OK));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), [1, 2, 3, 4]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_rejects_invalid_endpoints() {
let config = build_config(
1,
&[alt(
0,
0,
vec![(EP1_IN, XFER_ISO, 64), (EP2_IN, XFER_BULK, 0), (EP3_IN, XFER_BULK, 64)],
)],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, 0x10, 0, 1000, data), error!(EINVAL));
assert_eq!(bulk_raw(current_task, &file, arg, 0x101, 0, 1000, data), error!(EINVAL));
assert_eq!(bulk(current_task, &file, arg, 0x85, 0, 1000, data), error!(ENOENT));
// Isochronous endpoints and endpoints with a zero wMaxPacketSize are rejected.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(EINVAL));
assert_eq!(bulk(current_task, &file, arg, EP2_IN, 4, 1000, data), error!(EINVAL));
assert!(mock.request_types.lock().is_empty());
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_length_limits() {
let config =
build_config(1, &[alt(0, 0, vec![(EP1_IN, XFER_ISO, 64), (EP3_IN, XFER_BULK, 64)])]);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
let too_long = (MAX_USBDEVFS_BULK_SIZE + 1) as u32;
let invalid = INVALID_USBDEVFS_BULK_SIZE as u32;
// usbfs limits the memory used by a transfer, including its URB.
assert_eq!(
bulk(current_task, &file, arg, EP3_IN, too_long, 1000, data),
error!(ENOMEM)
);
// Very large lengths are invalid, which is checked before the endpoint.
assert_eq!(bulk(current_task, &file, arg, EP3_IN, invalid, 1000, data), error!(EINVAL));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, invalid, 1000, data), error!(EINVAL));
// The endpoint is checked before the memory limit.
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, too_long, 1000, data),
error!(EINVAL)
);
assert!(mock.request_types.lock().is_empty());
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_uses_endpoint_transfer_type() {
let config =
build_config(1, &[alt(0, 0, vec![(EP1_IN, XFER_INT, 64), (EP2_OUT, XFER_BULK, 512)])]);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(bulk(current_task, &file, arg, EP2_OUT, 4, 1000, data), Ok(4.into()));
assert_eq!(*mock.request_types.lock(), vec![(EP1_IN, "interrupt"), (EP2_OUT, "bulk")]);
})
.await;
}
#[::fuchsia::test]
async fn test_enable_endpoint_passes_ss_companion() {
let mut config = build_config(1, &[alt(0, 0, vec![])]);
config.extend(endpoint_descriptor(EP1_IN, XFER_BULK, 1024));
config.extend([USB_DT_SS_EP_COMP_SIZE as u8, USB_DT_SS_ENDPOINT_COMP as u8, 3, 0, 0, 0]);
config[4 + USB_DT_CONFIG_SIZE as usize] = 1; // bNumEndpoints
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(*mock.ss_companion_log.lock(), vec![(EP1_IN, 3)]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_out_claims_interface() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
let out_payload = [0x11u8, 0x22, 0x33, 0x44, 0x55];
current_task.write_memory(data, &out_payload).unwrap();
assert_eq!(bulk(current_task, &file1, arg, EP1_OUT, 5, 0, data), Ok(5.into()));
assert_eq!(bulk(current_task, &file2, arg, EP1_OUT, 5, 0, data), error!(EBUSY));
assert_eq!(*mock.received_bulk_out.lock(), vec![out_payload.to_vec()]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_zero_length_transfers_queue_zero_length_requests() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(
bulk(current_task, &file, arg, EP1_OUT, 0, 1000, UserAddress::NULL),
Ok(0.into())
);
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, 0, 1000, UserAddress::NULL),
Ok(0.into())
);
assert_eq!(*mock.request_sizes.lock(), vec![(EP1_OUT, 0), (EP1_IN, 0)]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_out_short_write_returns_partial_count() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
// If the device accepts only 3 of 5 bytes, 3 is returned.
mock.bulk_out_max_transfer_size.store(3, Ordering::Relaxed);
assert_eq!(bulk(current_task, &file, arg, EP1_OUT, 5, 1000, data), Ok(3.into()));
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_in_returns_eoverflow_when_device_sends_too_much() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
current_task.write_memory(data, &[0u8; 6]).unwrap();
mock.overreport_in_transfer_size.store(true, Ordering::SeqCst);
mock.bulk_in_scripted_packets.lock().push_back((vec![1, 2, 3, 4, 5, 6], 0));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 2, 1000, data), error!(EOVERFLOW));
assert_eq!(read_bytes::<6>(current_task, data), [0; 6]);
// Nothing is kept for the next transfer.
mock.overreport_in_transfer_size.store(false, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(mock.bulk_in_queue_count.load(Ordering::SeqCst), 2);
})
.await;
}
#[::fuchsia::test]
async fn test_release_cancels_endpoint_requests() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(release(current_task, &file, arg, 0), Ok(SUCCESS));
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_release_resets_nonzero_alternate_setting_to_zero() {
let config = build_config(
1,
&[alt(0, 0, vec![(EP1_IN, XFER_BULK, 64)]), alt(0, 1, vec![(EP2_IN, XFER_BULK, 64)])],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
assert_eq!(set_interface(current_task, &file, arg, 0, 1), Ok(SUCCESS));
assert_eq!(*mock.last_set_iface.lock(), Some((0, 1)));
assert_eq!(release(current_task, &file, arg, 0), Ok(SUCCESS));
assert_eq!(*mock.last_set_iface.lock(), Some((0, 0)));
})
.await;
}
#[::fuchsia::test]
async fn test_close_cancels_endpoint_requests() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let (arg, data) = map_test_buffers(current_task);
let file = open_device(current_task, DEV_NUM);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
drop(file);
current_task.kernel().delayed_releaser.apply(current_task);
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
})
.await;
}
/// Runs `f` on a kernel thread once `count` IN requests have been queued to the mock, and
/// returns a receiver for its result.
fn run_when_in_requests_queued<R: Send + 'static>(
current_task: &CurrentTask,
mock: &Arc<MockUsbServerState>,
count: usize,
f: impl FnOnce(&CurrentTask) -> R + Send + 'static,
) -> std::sync::mpsc::Receiver<R> {
let mock = Arc::clone(mock);
let (sender, receiver) = std::sync::mpsc::channel();
let closure = move |task: &CurrentTask| {
while mock.bulk_in_queue_count.load(Ordering::SeqCst) < count {
std::thread::sleep(std::time::Duration::from_millis(1));
}
let _ = sender.send(f(task));
};
let request = SpawnRequestBuilder::new()
.with_debug_name("usbfs-test")
.with_sync_closure(closure)
.build();
current_task.kernel().kthreads.spawner().spawn_from_request(request);
receiver
}
#[::fuchsia::test]
async fn test_reset_from_other_fd_shuts_down_in_flight_bulk() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
// Another file descriptor resets the device while a transfer without timeout waits
// for its request.
mock.hold_in_requests.store(true, Ordering::SeqCst);
let reset = run_when_in_requests_queued(current_task, mock, 1, move |task| {
ioctl_no_arg(task, &file2, USBDEVFS_RESET)
});
assert_eq!(bulk(current_task, &file1, arg, EP1_IN, 4, 0, data), error!(ESHUTDOWN));
assert_eq!(reset.recv().unwrap(), Ok(SUCCESS));
// The cancelled request was drained: the next transfer gets its own completion on
// the same VMO.
mock.hold_in_requests.store(false, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file1, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(mock.registered_vmos.lock().len(), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_fails_in_flight_bulk_with_enodev() {
run_with_mock_device(hid_config(), |current_task, device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.hold_in_requests.store(true, Ordering::SeqCst);
let device = Arc::clone(device);
let disconnected =
run_when_in_requests_queued(current_task, mock, 1, move |_| device.on_disconnect());
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 0, data), error!(ENODEV));
disconnected.recv().unwrap();
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_timeout_drains_late_cancelled_completion() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
// The request never completes, and its cancelled completion arrives after the
// CancelAll reply.
mock.hold_in_requests.store(true, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), error!(ETIMEDOUT));
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
// The next transfer gets its own completion, not the cancelled one.
mock.hold_in_requests.store(false, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(mock.connect_count.load(Ordering::SeqCst), 1);
assert_eq!(mock.registered_vmos.lock().len(), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_timeout_returns_data_completed_before_cancel() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
// The request completes successfully as it is cancelled after the timeout.
mock.hold_in_requests.store(true, Ordering::SeqCst);
*mock.cancel_behavior.lock() = Some(CancelBehavior::CompleteSuccessfullyAfterReply);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_timeout_retires_vmo_when_cancel_does_not_complete() {
let options = MockDeviceOptions {
cancel_drain_timeout: zx::MonotonicDuration::from_millis(50),
..MockDeviceOptions::new(hid_config())
};
run_with_mock_device_options(options, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.hold_in_requests.store(true, Ordering::SeqCst);
*mock.cancel_behavior.lock() = Some(CancelBehavior::Drop);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), error!(ETIMEDOUT));
// The next transfer uses a new VMO on the same channel, without disabling the
// endpoint.
mock.hold_in_requests.store(false, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(*mock.registered_vmos.lock(), vec![(EP1_IN, 1), (EP1_IN, 2)]);
assert_eq!(mock.connect_count.load(Ordering::SeqCst), 1);
assert_eq!(*mock.enable_log.lock(), vec![(EP1_IN, true, 64)]);
assert!(!mock.endpoint_unbound.is_signaled());
// The retired request eventually completes successfully. Its completion is not
// mistaken for the one of the next transfer, and the retired VMO is unregistered.
mock.complete_held_on_next_queue.store(true, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(*mock.unregistered_vmos.lock(), vec![(EP1_IN, 1)]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_fails_when_too_many_vmos_are_retired() {
let options = MockDeviceOptions {
cancel_drain_timeout: zx::MonotonicDuration::from_millis(50),
..MockDeviceOptions::new(hid_config())
};
run_with_mock_device_options(options, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.hold_in_requests.store(true, Ordering::SeqCst);
*mock.cancel_behavior.lock() = Some(CancelBehavior::Drop);
for _ in 0..=MAX_RETIRED_VMOS {
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), error!(ETIMEDOUT));
}
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), error!(EIO));
assert_eq!(mock.registered_vmos.lock().len(), MAX_RETIRED_VMOS + 1);
assert_eq!(mock.bulk_in_queue_count.load(Ordering::SeqCst), MAX_RETIRED_VMOS + 1);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_wait_ignores_maskable_signals() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
current_task.thread_group.signal_actions.set(
SIGUSR1,
sigaction_t { sa_handler: uaddr { addr: 0x1000 }, ..sigaction_t::default() },
);
send_standard_signal(current_task, SignalInfo::kernel(SIGUSR1));
// A pending maskable signal neither interrupts a transfer that completes nor one that
// times out.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 0, data), Ok(4.into()));
mock.hold_in_requests.store(true, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), error!(ETIMEDOUT));
assert!(current_task.read().has_signal_pending(SIGUSR1));
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_wait_is_killable() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.hold_in_requests.store(true, Ordering::SeqCst);
set_pending_sigkill(current_task, true);
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, 4, 0, data),
error!(EINTR),
"SIGKILL must end a bulk transfer without timeout"
);
set_pending_sigkill(current_task, false);
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
// The cancelled request was drained: the next transfer gets its own completion on
// the same VMO.
mock.hold_in_requests.store(false, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
assert_eq!(mock.connect_count.load(Ordering::SeqCst), 1);
assert_eq!(mock.registered_vmos.lock().len(), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_without_usb_connection_returns_enodev() {
run_with_unconnected_device(hid_config(), |current_task| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
// ENODEV is returned before reading user memory.
let unmapped = UserAddress::from(0x1000u64);
assert_eq!(bulk(current_task, &file, arg, EP1_OUT, 64, 1000, unmapped), error!(ENODEV));
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_cancels_endpoint_transfers() {
run_with_mock_device(hid_config(), |current_task, device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
device.on_disconnect();
assert_eq!(mock.cancel_count.load(Ordering::SeqCst), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_endpoint_closed_before_register_vmos_disables_endpoint() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.close_endpoint_before_register.store(true, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ENODEV));
assert_eq!(*mock.enable_log.lock(), vec![(EP1_IN, true, 64), (EP1_IN, false, 64)]);
})
.await;
}
#[::fuchsia::test]
async fn test_endpoint_connect_failure_disables_endpoint() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
// The endpoint is disabled again, so a later attempt can enable it.
mock.connect_error_once.store(zx::sys::ZX_ERR_NOT_FOUND, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ENOENT));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(
*mock.enable_log.lock(),
vec![(EP1_IN, true, 64), (EP1_IN, false, 64), (EP1_IN, true, 64)]
);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_reconnects_after_driver_closes_endpoint() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.drop_endpoint_on_next_in.store(true, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ENODEV));
// The driver disabled the endpoint before closing the channel, so it is enabled and
// connected again.
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(mock.connect_count.load(Ordering::SeqCst), 2);
assert_eq!(*mock.enable_log.lock(), vec![(EP1_IN, true, 64), (EP1_IN, true, 64)]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_out_chunks_transfers_larger_than_vmo() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let large_payload: Vec<u8> = (0..70_000usize).map(|i| (i % 251) as u8).collect();
let (arg, _) = map_test_buffers(current_task);
let buf_addr = map_test_buffer(current_task, 73_728);
current_task.write_memory(buf_addr, &large_payload).unwrap();
assert_eq!(
bulk(current_task, &file, arg, EP1_OUT, large_payload.len() as u32, 2000, buf_addr),
Ok(large_payload.len().into())
);
assert_eq!(mock.received_bulk_out.lock().concat(), large_payload);
assert_eq!(*mock.request_sizes.lock(), vec![(EP1_OUT, 65_536), (EP1_OUT, 4_464)]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_in_chunks_transfers_larger_than_vmo() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let buf_addr = map_test_buffer(current_task, 73_728);
// 70,000 bytes are received as a 65,536-byte chunk and a 4,464-byte chunk.
let expected_in: Vec<u8> = (0..70_000usize).map(|i| (i % 239) as u8).collect();
{
let mut scripted = mock.bulk_in_scripted_packets.lock();
scripted.push_back((expected_in[..65_536].to_vec(), zx::sys::ZX_OK));
scripted.push_back((expected_in[65_536..].to_vec(), zx::sys::ZX_OK));
}
let len = expected_in.len() as u32;
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, len, 2000, buf_addr),
Ok(expected_in.len().into())
);
let actual_in = current_task.read_memory_to_vec(buf_addr, expected_in.len()).unwrap();
assert_eq!(actual_in, expected_in);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_in_error_in_second_chunk_discards_first_chunk() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let buf_addr = map_test_buffer(current_task, 73_728);
current_task.write_memory(buf_addr, &[0u8; 16]).unwrap();
{
let mut scripted = mock.bulk_in_scripted_packets.lock();
scripted.push_back((vec![0x5au8; 65_536], zx::sys::ZX_OK));
scripted.push_back((Vec::new(), zx::sys::ZX_ERR_IO_REFUSED));
}
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, 70_000, 2000, buf_addr),
error!(EPIPE)
);
let untouched = current_task.read_memory_to_vec(buf_addr, 16).unwrap();
assert_eq!(untouched, vec![0u8; 16]);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_in_short_chunk_ends_transfer() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let buf_addr = map_test_buffer(current_task, 73_728);
mock.bulk_in_scripted_packets.lock().push_back((vec![0x11; 100], zx::sys::ZX_OK));
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, 70_000, 2000, buf_addr),
Ok(100.into())
);
assert_eq!(mock.bulk_in_queue_count.load(Ordering::SeqCst), 1);
})
.await;
}
#[::fuchsia::test]
async fn test_bulk_chunks_are_multiples_of_max_packet_size() {
// Interrupt endpoints can have a wMaxPacketSize that does not divide the VMO size.
let config = build_config(
1,
&[alt(0, 0, vec![(EP1_OUT, XFER_INT, 1000), (EP1_IN, XFER_INT, 1000)])],
);
run_with_mock_device(config, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let buf_addr = map_test_buffer(current_task, 73_728);
assert_eq!(
bulk(current_task, &file, arg, EP1_OUT, 70_000, 2000, buf_addr),
Ok(70_000.into())
);
{
let mut scripted = mock.bulk_in_scripted_packets.lock();
scripted.push_back((vec![1; 65_000], zx::sys::ZX_OK));
scripted.push_back((vec![2; 5_000], zx::sys::ZX_OK));
}
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, 70_000, 2000, buf_addr),
Ok(70_000.into())
);
assert_eq!(
*mock.request_sizes.lock(),
vec![(EP1_OUT, 65_000), (EP1_OUT, 5_000), (EP1_IN, 65_000), (EP1_IN, 5_000)]
);
})
.await;
}
#[::fuchsia::test]
async fn test_reset_between_bulk_chunks_returns_eshutdown() {
run_with_mock_device(hid_config(), |current_task, device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, _) = map_test_buffers(current_task);
let buf_addr = map_test_buffer(current_task, 73_728);
// Complete the first 65,536-byte chunk normally, but reset the endpoint before the
// second chunk is submitted. The transfer must fail with ESHUTDOWN (not ENOENT).
mock.bulk_in_scripted_packets.lock().push_back((vec![0x42u8; 65_536], zx::sys::ZX_OK));
let device_for_hook = Arc::clone(device);
*mock.on_in_completion.lock() = Some(Box::new(move || {
device_for_hook.reset_endpoint_state(EP1_IN);
}));
assert_eq!(
bulk(current_task, &file, arg, EP1_IN, 70_000, 2000, buf_addr),
error!(ESHUTDOWN)
);
})
.await;
}
#[::fuchsia::test]
async fn test_cancel_all_domain_error_retires_vmo_without_waiting_for_drain_timeout() {
let options = MockDeviceOptions {
// Large drain timeout so that ignoring an inner CancelAll error would block for
// multiple seconds instead of returning immediately.
cancel_drain_timeout: zx::MonotonicDuration::from_seconds(5),
..MockDeviceOptions::new(hid_config())
};
run_with_mock_device_options(options, |current_task, _device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
mock.hold_in_requests.store(true, Ordering::SeqCst);
mock.cancel_all_status.store(zx::sys::ZX_ERR_IO, Ordering::SeqCst);
let start = zx::MonotonicInstant::get();
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1, data), error!(ETIMEDOUT));
let elapsed = zx::MonotonicInstant::get() - start;
assert!(
elapsed < zx::MonotonicDuration::from_seconds(1),
"CancelAll domain error must abort draining immediately, took {elapsed:?}"
);
// Because CancelAll failed, the next transfer retires VMO 1 and registers VMO 2.
mock.hold_in_requests.store(false, Ordering::SeqCst);
mock.cancel_all_status.store(zx::sys::ZX_OK, Ordering::SeqCst);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(*mock.registered_vmos.lock(), vec![(EP1_IN, 1), (EP1_IN, 2)]);
})
.await;
}
#[::fuchsia::test]
async fn test_disconnect_during_endpoint_connect_returns_enodev_and_leaves_no_endpoint() {
run_with_mock_device(hid_config(), |current_task, device, mock| {
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
let device_for_hook = Arc::clone(device);
*mock.on_connect_to_endpoint.lock() = Some(Box::new(move || {
device_for_hook.on_disconnect();
}));
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ENODEV));
assert!(
mock.enabled_endpoints.lock().is_empty(),
"endpoint enabled concurrently with on_disconnect must be rolled back"
);
})
.await;
}
fn spawn_kernel_thread<R: Send + Sync + 'static>(
current_task: &CurrentTask,
f: impl FnOnce(&CurrentTask) -> R + Send + Sync + 'static,
) -> std::sync::mpsc::Receiver<R> {
let task_builder = current_task
.clone_task(
u64::from(
starnix_uapi::CLONE_VM
| starnix_uapi::CLONE_FS
| starnix_uapi::CLONE_FILES
| starnix_uapi::CLONE_THREAD
| starnix_uapi::CLONE_SIGHAND,
),
None,
UserRef::default(),
UserRef::default(),
UserRef::default(),
)
.expect("failed to clone task in test");
let (sender, receiver) = std::sync::mpsc::channel();
starnix_core::execution::execute_task(
task_builder,
move |worker_task| {
let result = f(worker_task);
worker_task
.write()
.set_exit_status_if_not_already(starnix_core::task::ExitStatus::Exit(0));
let _ = sender.send(result);
error!(EHWPOISON)
},
|_| {},
)
.expect("failed to execute worker task");
receiver
}
#[::fuchsia::test]
async fn test_reset_before_begin_transfer_returns_eshutdown() {
run_with_mock_device(hid_config(), |current_task, _device, mock| {
let file1 = open_device(current_task, DEV_NUM);
let file2 = open_device(current_task, DEV_NUM);
// Thread 1 holds the endpoint transfer lock while waiting on an in-flight request.
mock.hold_in_requests.store(true, Ordering::SeqCst);
let file1_first = Arc::clone(&file1);
let first = spawn_kernel_thread(current_task, move |task| {
let (arg1, data1) = map_test_buffers(task);
bulk(task, &file1_first, arg1, EP1_IN, 4, 0, data1)
});
while mock.bulk_in_queue_count.load(Ordering::SeqCst) < 1 {
std::thread::sleep(std::time::Duration::from_millis(1));
}
// Thread 2 snapshots `expected_reset` in `bulk` and blocks on the endpoint transfer
// lock before entering `begin_transfer`.
let entered = Arc::new(AtomicBool::new(false));
let entered_clone = Arc::clone(&entered);
let file1_second = Arc::clone(&file1);
let second = spawn_kernel_thread(current_task, move |task| {
let (arg2, data2) = map_test_buffers(task);
entered_clone.store(true, Ordering::SeqCst);
bulk(task, &file1_second, arg2, EP1_IN, 4, 1000, data2)
});
while !entered.load(Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(1));
}
// Give Thread 2 a brief moment to reach `self.transfer.lock()`.
std::thread::sleep(std::time::Duration::from_millis(10));
// Resetting the device wakes Thread 1 and increments `reset_count`, so when Thread 2
// acquires `self.transfer.lock()`, `begin_transfer` sees the reset and returns
// `ESHUTDOWN`.
assert_eq!(ioctl_no_arg(current_task, &file2, USBDEVFS_RESET), Ok(SUCCESS));
assert_eq!(first.recv().unwrap(), error!(ESHUTDOWN));
assert_eq!(second.recv().unwrap(), error!(ESHUTDOWN));
})
.await;
}
#[::fuchsia::test]
async fn test_concurrent_bulk_transfers_resets_alt_switches_and_errors_stress() {
let config = build_config(
1,
&[
alt(0, 0, vec![(EP1_IN, XFER_BULK, 512), (EP1_OUT, XFER_BULK, 512)]),
alt(0, 1, vec![(EP1_IN, XFER_BULK, 512), (EP1_OUT, XFER_BULK, 512)]),
],
);
let options = MockDeviceOptions {
cancel_drain_timeout: zx::MonotonicDuration::from_millis(20),
..MockDeviceOptions::new(config)
};
run_with_mock_device_options(options, |current_task, _device, mock| {
const ITERS: usize = 25;
let file_in = open_device(current_task, DEV_NUM);
let file_out = open_device(current_task, DEV_NUM);
let file_ctrl = open_device(current_task, DEV_NUM);
let file_mgmt = open_device(current_task, DEV_NUM);
// Worker 1: single-chunk and multi-chunk IN transfers with short timeouts and errors.
let mock_in = Arc::clone(mock);
let t_in = spawn_kernel_thread(current_task, move |task| {
let (arg_in, _) = map_test_buffers(task);
let buf_in = map_test_buffer(task, 73_728);
for i in 0..ITERS {
if i % 5 == 0 {
mock_in
.bulk_in_scripted_packets
.lock()
.push_back((Vec::new(), zx::sys::ZX_ERR_IO_REFUSED));
}
let len = if i % 3 == 0 { 70_000 } else { 64 };
let _ = bulk(task, &file_in, arg_in, EP1_IN, len, 5, buf_in);
}
});
// Worker 2: single-chunk and multi-chunk OUT transfers with periodic short writes.
let mock_out = Arc::clone(mock);
let t_out = spawn_kernel_thread(current_task, move |task| {
let (arg_out, _) = map_test_buffers(task);
let buf_out = map_test_buffer(task, 73_728);
task.write_memory(buf_out, &[0x5au8; 70_000]).unwrap();
for i in 0..ITERS {
if i % 5 == 0 {
mock_out.bulk_out_max_transfer_size.store(16, Ordering::Relaxed);
} else {
mock_out.bulk_out_max_transfer_size.store(usize::MAX, Ordering::Relaxed);
}
let len = if i % 4 == 0 { 70_000 } else { 64 };
let _ = bulk(task, &file_out, arg_out, EP1_OUT, len, 5, buf_out);
}
mock_out.bulk_out_max_transfer_size.store(usize::MAX, Ordering::Relaxed);
});
// Worker 3: concurrent control transfers exercising ControlTransferSlot and errors.
let mock_ctrl = Arc::clone(mock);
let t_ctrl = spawn_kernel_thread(current_task, move |task| {
let (arg_ctrl, _) = map_test_buffers(task);
for i in 0..ITERS {
let status =
if i % 6 == 0 { zx::sys::ZX_ERR_IO_REFUSED } else { zx::sys::ZX_OK };
mock_ctrl.ctrl_transfer_error.store(status, Ordering::Relaxed);
let _ = control(task, &file_ctrl, arg_ctrl, USB_DIR_OUT, TEST_REQUEST, 0, 10);
}
mock_ctrl.ctrl_transfer_error.store(zx::sys::ZX_OK, Ordering::Relaxed);
});
// Worker 4: concurrent resets, clear_halt, alternate setting switches, and
// disconnect_claim, plus toggling hold_in_requests to exercise cancel_and_drain.
let mock_mgmt = Arc::clone(mock);
let t_mgmt = spawn_kernel_thread(current_task, move |task| {
let (arg_mgmt, _) = map_test_buffers(task);
for i in 0..ITERS {
mock_mgmt.hold_in_requests.store(i % 4 == 0, Ordering::SeqCst);
let _ = disconnect_claim(task, &file_mgmt, arg_mgmt, 0, 0, b"");
let _ = set_interface(task, &file_mgmt, arg_mgmt, 0, (i % 2) as u32);
let _ = clear_halt(task, &file_mgmt, arg_mgmt, u32::from(EP1_IN));
let _ = ioctl_no_arg(task, &file_mgmt, USBDEVFS_RESET);
}
mock_mgmt.hold_in_requests.store(false, Ordering::SeqCst);
});
t_in.recv().unwrap();
t_out.recv().unwrap();
t_ctrl.recv().unwrap();
t_mgmt.recv().unwrap();
mock.bulk_in_scripted_packets.lock().clear();
assert_eq!(
mock.invalid_queue_while_disabled.load(Ordering::SeqCst),
0,
"no request may be queued onto a disabled endpoint"
);
// After the stress run, verify the device and endpoints recover cleanly.
let file_check = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(disconnect_claim(current_task, &file_check, arg, 0, 0, b""), Ok(SUCCESS));
assert_eq!(bulk(current_task, &file_check, arg, EP1_IN, 4, 1000, data), Ok(4.into()));
assert_eq!(read_bytes::<4>(current_task, data), DEFAULT_IN_DATA);
})
.await;
}
#[::fuchsia::test]
async fn test_concurrent_transfers_and_device_disconnect_stress() {
run_with_mock_device(hid_config(), |current_task, device, mock| {
let mut workers = Vec::new();
for i in 0..4 {
let file = open_device(current_task, DEV_NUM);
workers.push(spawn_kernel_thread(current_task, move |task| {
let (arg, data) = map_test_buffers(task);
for step in 0..20 {
let ep = if (i + step) % 2 == 0 { EP1_IN } else { EP1_OUT };
let _ = bulk(task, &file, arg, ep, 4, 10, data);
let _ = control(task, &file, arg, USB_DIR_OUT, TEST_REQUEST, 0, 10);
let _ = ioctl_no_arg(task, &file, USBDEVFS_RESET);
}
}));
}
std::thread::sleep(std::time::Duration::from_millis(5));
device.on_disconnect();
for worker in workers {
worker.recv().unwrap();
}
assert!(device.endpoints.lock().is_empty());
assert_eq!(mock.invalid_queue_while_disabled.load(Ordering::SeqCst), 0);
let file = open_device(current_task, DEV_NUM);
let (arg, data) = map_test_buffers(current_task);
assert_eq!(bulk(current_task, &file, arg, EP1_IN, 4, 1000, data), error!(ENODEV));
})
.await;
}
}