Rust for Linux List
 help / color / mirror / Atom feed
* [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver
@ 2026-08-26 16:30 Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 1/5] rust: usb: add revocable typed interface I/O Mike Lothian
                   ` (5 more replies)
  0 siblings, 6 replies; 8+ messages in thread
From: Mike Lothian @ 2026-08-26 16:30 UTC (permalink / raw)
  To: linux-usb
  Cc: Mike Lothian, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
	Alexandre Courbot, Onur Özkan, Nathan Chancellor,
	Nick Desaulniers, Bill Wendling, Justin Stitt, rust-for-linux,
	llvm

Host-side USB abstractions for a driver whose device is a bulk-endpoint pipe
rather than a class device

  Revocable typed interface I/O, so an interface cannot be used after the core
    has taken it back
  Reusable URBs and persistent bulk queues, which is what keeps a video stream
    in flight without allocating per transfer
  The device descriptor fields a driver needs to identify hardware before it
    decides to drive it, and a queue-readiness check
  A device id constructor matching on vendor together with interface class,
    subclass and protocol, for a composite device whose function is not
    identified by the product id alone
  Letting a driver keep its interface usable while unbinding, so teardown can
    still talk to the device it is releasing

Changes since v2:

  v2 10/11, "keep usb::Device private and gate ...", is dropped entirely.
    Oliver Neukum was right that it was conceptually wrong: USB does device
    level operations, and hiding that behind an interface is a layering
    violation. Device stays public
  as_bound() is gone from both the binding and the driver. Danilo Krummrich's
    point stands: needing an unsafe as_bound() means the design or the
    infrastructure is wrong, not that the escape hatch is needed
  reset_configuration() is gone, and set_interface() now exists in two
    correctly scoped forms, one on Interface<Bound> taking an altsetting and
    one on Device taking interface plus altsetting
  What replaces the concealment is lifecycle gating: an adapter-owned,
    revocable I/O window that is valid across probe, suspend, reset, resume and
    disconnect and invalid outside them. That is the interval in which I/O is
    legal, which is narrower than "the interface is bound"
  There is no private URB implementation. Colin Braun's URB RFC is carried
    unchanged as the foundation and this builds on it
  A topology walk and a device-removal notifier were written after v2 and are
    not here. They existed for a second consumer that is not part of this
    posting, so nothing in what is sent would call them

Alan Stern's lifecycle point is what makes device access from an interface
sound, and is worth restating because the whole shape depends on it: an
unconfigured device has no interfaces, so an interface that exists implies a
configured device

v2: https://lore.kernel.org/r/20260703030020.2694-1-mike@fireburn.co.uk

The rest of the posting, which is one series per subsystem:

  rust-core, 9 patches, rust-for-linux and linux-kernel
  https://lore.kernel.org/r/20260826162851.2497-1-mike@fireburn.co.uk
  rust-crypto, 2 patches, linux-crypto and rust-for-linux
  https://lore.kernel.org/r/20260826163004.3365-1-mike@fireburn.co.uk
  rust-usb, 5 patches, this one
  rust-drm, 23 patches, to dri-devel and rust-for-linux, not sent yet
  rust-firmware, 1 patch, to linux-kernel and rust-for-linux, not sent yet
  drm-vino, 13 patches, to dri-devel, not sent yet

Vino is the user for all of them. The abstractions themselves are generic and
carry no knowledge of DisplayLink

The whole thing is one branch, base and prerequisites included, which is the
quickest way to read it:

  git clone -b vino-v3 https://github.com/FireBurn/linux
  cd linux
  make LLVM=1 rustavailable
  make LLVM=1 -j$(nproc)
  make LLVM=1 -j$(nproc) modules

CONFIG_RUST=y and CONFIG_DRM_VINO=m are the two to set; DRM_VINO selects the
rest of what it needs

It is the exact tree these patches were generated from, at 4c9ba407018e, the
drm-rust-next tip of 2026-08-06. drm-next has moved on since, and this follows
drm-rust-next deliberately: the KMS layer underneath this work lives only there,
and that tree picks up drm-next on its own schedule

Two commits on the branch are not in any of the series above, because they
enable no part of Vino: a scheduler call site that stops compiling under the
locking-guard series, and the Kms associated type Tyr needs once the KMS
registration trait requires one

It applies to the base above plus this, and nothing else:

  Colin Braun, rust: usb: add usb request block abstractions
  https://lore.kernel.org/r/20260712-urb-abstraction-v1-v1-0-9fa011634ead@gmail.com

The reference branch also carries Boqun Feng's counted interrupt disabling
series, which SpinLockIrq needs. One patch of it is already in tip locking/core
as e901c1510e24

These patches were written with the assistance of Claude (Anthropic), used
through Claude Code as an interactive coding assistant, across the design, the
implementation and the tests. Every patch it contributed to carries an
Assisted-by trailer. The Signed-off-by is mine: I have reviewed and tested what
is here and I stand behind it

Mike Lothian (5):
  rust: usb: add revocable typed interface I/O
  rust: usb: add reusable URBs and persistent bulk queues
  rust: usb: expose device descriptor fields and queue readiness
  rust: usb: add a vendor-and-interface-info device id constructor
  rust: usb: let a driver keep its interface usable while unbinding

 3 files changed, 1553 insertions(+), 26 deletions(-)

base-commit: 4c9ba407018e8deb06dbc643112bac8f40404f95
prerequisite-message-id: <20260712-urb-abstraction-v1-v1-0-9fa011634ead@gmail.com>

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 1/5] rust: usb: add revocable typed interface I/O
  2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
@ 2026-08-26 16:30 ` Mike Lothian
  2026-08-26 18:59   ` Danilo Krummrich
  2026-08-26 16:30 ` [PATCH v3 2/5] rust: usb: add reusable URBs and persistent bulk queues Mike Lothian
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 8+ messages in thread
From: Mike Lothian @ 2026-08-26 16:30 UTC (permalink / raw)
  To: linux-usb
  Cc: Mike Lothian, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
	Alexandre Courbot, Onur Özkan, Greg Kroah-Hartman,
	Colin Braun, rust-for-linux, linux-kernel

Make the USB driver adapter own its bound data and a per-interface
I/O window. Close the window around disconnect, suspend, and reset
callbacks, wait for outstanding users, and release bound data after
driver teardown.

Represent endpoint direction and transfer type in sealed marker types
validated from the active interface descriptor. Issue bulk, interrupt,
and control transfers through a borrowed I/O token, use DMA-capable
bounce buffers where required, and prevent a driver from retargeting
a sibling interface.

Add the standard power-management callbacks to the Rust USB driver trait
and update the sample driver for the I/O capability passed at probe.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
 rust/helpers/usb.c              |  18 +
 rust/kernel/usb.rs              | 767 +++++++++++++++++++++++++++++++-
 samples/rust/rust_driver_usb.rs |   6 +-
 3 files changed, 772 insertions(+), 19 deletions(-)

diff --git a/rust/helpers/usb.c b/rust/helpers/usb.c
index eff1cf7be3c2..ac7b30334882 100644
--- a/rust/helpers/usb.c
+++ b/rust/helpers/usb.c
@@ -7,3 +7,21 @@ rust_helper_interface_to_usbdev(struct usb_interface *intf)
 {
 	return interface_to_usbdev(intf);
 }
+
+__rust_helper unsigned int
+rust_helper_usb_sndbulkpipe(struct usb_device *dev, unsigned int endpoint)
+{
+	return usb_sndbulkpipe(dev, endpoint);
+}
+
+__rust_helper unsigned int
+rust_helper_usb_rcvbulkpipe(struct usb_device *dev, unsigned int endpoint)
+{
+	return usb_rcvbulkpipe(dev, endpoint);
+}
+
+__rust_helper unsigned int
+rust_helper_usb_rcvintpipe(struct usb_device *dev, unsigned int endpoint)
+{
+	return usb_rcvintpipe(dev, endpoint);
+}
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index f4a3d7c0ef6c..c3d0cc36d425 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -6,6 +6,7 @@
 //! C header: [`include/linux/usb.h`](srctree/include/linux/usb.h)
 
 use crate::{
+    alloc::Flags,
     bindings,
     device,
     device_id::{
@@ -19,9 +20,16 @@
     },
     prelude::*,
     sync::{
-        aref::AlwaysRefCounted,
+        aref::{
+            ARef,
+            AlwaysRefCounted, //
+        },
+        new_condvar,
+        new_mutex,
         Arc,
-        ArcBorrow, //
+        ArcBorrow,
+        CondVar,
+        Mutex, //
     },
     time::Delta,
     types::Opaque,
@@ -53,14 +61,29 @@
 /// An adapter for the registration of USB drivers.
 pub struct Adapter<T: Driver>(T);
 
+#[pin_data]
+#[doc(hidden)]
+pub struct BoundData<'bound, T: Driver> {
+    #[pin]
+    driver_data: T::Data<'bound>,
+    io: Arc<IoWindow>,
+}
+
+impl<'bound, T: Driver> BoundData<'bound, T> {
+    fn driver_data<'a>(self: Pin<&'a Self>) -> Pin<&'a T::Data<'bound>> {
+        // SAFETY: `driver_data` is structurally pinned with `Self`.
+        unsafe { self.map_unchecked(|this| &this.driver_data) }
+    }
+}
+
 // SAFETY:
 // - `bindings::usb_driver` is a C type declared as `repr(C)`.
-// - `T::Data` is the type of the driver's device private data.
+// - `BoundData<T>` is the type of the driver's device private data.
 // - `struct usb_driver` embeds a `struct device_driver`.
 // - `DEVICE_DRIVER_OFFSET` is the correct byte offset to the embedded `struct device_driver`.
 unsafe impl<T: Driver> driver::DriverLayout for Adapter<T> {
     type DriverType = bindings::usb_driver;
-    type DriverData<'bound> = T::Data<'bound>;
+    type DriverData<'bound> = BoundData<'bound, T>;
     const DEVICE_DRIVER_OFFSET: usize = core::mem::offset_of!(Self::DriverType, driver);
 }
 
@@ -77,6 +100,11 @@ unsafe fn register(
             (*udrv.get()).name = name.as_char_ptr();
             (*udrv.get()).probe = Some(Self::probe_callback);
             (*udrv.get()).disconnect = Some(Self::disconnect_callback);
+            (*udrv.get()).suspend = Some(Self::suspend_callback);
+            (*udrv.get()).resume = Some(Self::resume_callback);
+            (*udrv.get()).reset_resume = Some(Self::reset_resume_callback);
+            (*udrv.get()).pre_reset = Some(Self::pre_reset_callback);
+            (*udrv.get()).post_reset = Some(Self::post_reset_callback);
             (*udrv.get()).id_table = T::ID_TABLE.as_ptr();
         }
 
@@ -109,7 +137,12 @@ extern "C" fn probe_callback(
             let id = unsafe { &*id.cast::<DeviceId>() };
 
             let info = T::ID_TABLE.info(id.index());
-            let data = T::probe(intf, id, info);
+            let interface: ARef<Interface> = intf.into();
+            let io = Arc::pin_init(IoWindow::new(interface), GFP_KERNEL)?;
+            let data = try_pin_init!(BoundData::<T> {
+                driver_data <- T::probe(intf, id, info, io.clone()),
+                io,
+            });
 
             let dev: &device::Device<device::CoreInternal<'_>> = intf.as_ref();
             dev.set_drvdata(data)?;
@@ -126,12 +159,94 @@ extern "C" fn disconnect_callback(intf: *mut bindings::usb_interface) {
 
         let dev: &device::Device<device::CoreInternal<'_>> = intf.as_ref();
 
+        // Take ownership of the driver data here rather than leaving it to the driver core's
+        // generic post-unbind teardown: `usb_unbind_interface()` calls `usb_set_intfdata(intf,
+        // NULL)` as soon as this callback returns, which is *before* `device_unbind_cleanup()`
+        // runs `post_unbind_rust`. The generic `drvdata_obtain()` therefore always finds NULL for
+        // USB and the driver data -- with everything it owns, such as a `drm::Registration` -- is
+        // leaked on every unbind.
+        //
         // SAFETY: `disconnect_callback` is only ever called after a successful call to
         // `probe_callback`, hence it's guaranteed that `Device::set_drvdata()` has been called
-        // and stored a `Pin<KBox<T::Data<'_>>>`.
-        let data = unsafe { dev.drvdata_borrow::<T::Data<'_>>() };
+        // and stored a `Pin<KBox<BoundData<'_, T>>>`.
+        let data = unsafe { dev.drvdata_obtain::<BoundData<'_, T>>() };
+
+        if let Some(data) = data {
+            T::quiesce(intf, data.as_ref().driver_data());
+            data.io.close();
+            T::disconnect(intf, data.as_ref().driver_data());
+
+            // Dropped only after `T::disconnect()` has returned, so a driver can rely on its
+            // owned resources still being alive for the whole of its disconnect handling.
+            drop(data);
+        }
+    }
+
+    /// Recovers the typed interface and driver data shared by every power-management and reset
+    /// callback, then dispatches to `f`.
+    ///
+    /// `f` is spelled as an explicitly higher-ranked `fn` pointer because the interface and the
+    /// driver data share the `'bound` lifetime; an `impl FnOnce` bound loses that relationship and
+    /// the trait methods no longer satisfy it.
+    fn pm_dispatch(
+        intf: *mut bindings::usb_interface,
+        f: for<'bound, 'a, 'b> fn(
+            &'bound Interface<device::Core<'a>>,
+            Pin<&'b T::Data<'bound>>,
+        ) -> Result,
+        resume: bool,
+    ) -> kernel::ffi::c_int {
+        // SAFETY: The USB core only ever calls these with a valid `struct usb_interface`.
+        //
+        // INVARIANT: `intf` is valid for the duration of the callback.
+        let intf = unsafe { &*intf.cast::<Interface<device::CoreInternal<'_>>>() };
+
+        let dev: &device::Device<device::CoreInternal<'_>> = intf.as_ref();
+
+        // SAFETY: These callbacks only ever run between a successful `probe_callback()` and
+        // `disconnect_callback()`, so the driver data is present.
+        let data = unsafe { dev.drvdata_borrow::<BoundData<'_, T>>() };
 
-        T::disconnect(intf, data);
+        from_result(|| {
+            if resume {
+                data.io.reopen();
+            }
+
+            if let Err(e) = f(intf, data.driver_data()) {
+                if resume {
+                    data.io.close();
+                }
+                return Err(e);
+            }
+
+            if !resume {
+                data.io.close();
+            }
+            Ok(0)
+        })
+    }
+
+    extern "C" fn suspend_callback(
+        intf: *mut bindings::usb_interface,
+        _message: bindings::pm_message_t,
+    ) -> kernel::ffi::c_int {
+        Self::pm_dispatch(intf, T::suspend, false)
+    }
+
+    extern "C" fn resume_callback(intf: *mut bindings::usb_interface) -> kernel::ffi::c_int {
+        Self::pm_dispatch(intf, T::resume, true)
+    }
+
+    extern "C" fn reset_resume_callback(intf: *mut bindings::usb_interface) -> kernel::ffi::c_int {
+        Self::pm_dispatch(intf, T::reset_resume, true)
+    }
+
+    extern "C" fn pre_reset_callback(intf: *mut bindings::usb_interface) -> kernel::ffi::c_int {
+        Self::pm_dispatch(intf, T::pre_reset, false)
+    }
+
+    extern "C" fn post_reset_callback(intf: *mut bindings::usb_interface) -> kernel::ffi::c_int {
+        Self::pm_dispatch(intf, T::post_reset, true)
     }
 }
 
@@ -289,7 +404,7 @@ macro_rules! usb_device_table {
 /// # Examples
 ///
 ///```
-/// # use kernel::{bindings, device::Core, usb};
+/// # use kernel::{bindings, device::Core, sync::Arc, usb};
 /// use kernel::prelude::*;
 ///
 /// struct MyDriver;
@@ -313,6 +428,7 @@ macro_rules! usb_device_table {
 ///         _interface: &'bound usb::Interface<Core<'_>>,
 ///         _id: &usb::DeviceId,
 ///         _info: &'bound Self::IdInfo,
+///         _io: Arc<usb::IoWindow>,
 ///     ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
 ///         Err(ENODEV)
 ///     }
@@ -342,15 +458,77 @@ fn probe<'bound>(
         interface: &'bound Interface<device::Core<'_>>,
         id: &DeviceId,
         id_info: &'bound Self::IdInfo,
+        io: Arc<IoWindow>,
     ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound;
 
+    /// Quiesces a USB driver before disconnect.
+    ///
+    /// The implementation must stop work which could start new transfers. Once this returns, the
+    /// adapter closes the interface's [`IoWindow`].
+    fn quiesce<'bound>(
+        _interface: &'bound Interface<device::Core<'_>>,
+        _data: Pin<&Self::Data<'bound>>,
+    ) {
+    }
+
     /// USB driver disconnect.
     ///
-    /// Called when the USB interface is about to be unbound from this driver.
+    /// Called after the interface's [`IoWindow`] has been closed and all I/O has completed. The
+    /// bound data is dropped after this returns.
     fn disconnect<'bound>(
         interface: &'bound Interface<device::Core<'_>>,
         data: Pin<&Self::Data<'bound>>,
     );
+
+    /// The interface is being suspended.
+    ///
+    /// The implementation must stop work which could start new transfers. If it returns success,
+    /// the adapter closes the interface's [`IoWindow`] before returning to the USB core. Returning
+    /// an error aborts the suspend and leaves the window open.
+    fn suspend<'bound>(
+        _interface: &'bound Interface<device::Core<'_>>,
+        _data: Pin<&Self::Data<'bound>>,
+    ) -> Result {
+        Ok(())
+    }
+
+    /// The interface has been resumed. The adapter reopens its [`IoWindow`] before this is called.
+    fn resume<'bound>(
+        _interface: &'bound Interface<device::Core<'_>>,
+        _data: Pin<&Self::Data<'bound>>,
+    ) -> Result {
+        Ok(())
+    }
+
+    /// The interface has been resumed after its device was reset while suspended.
+    ///
+    /// I/O is permitted again, but the device has lost the state configured before the suspend.
+    /// Defaults to [`resume`](Driver::resume).
+    fn reset_resume<'bound>(
+        interface: &'bound Interface<device::Core<'_>>,
+        data: Pin<&Self::Data<'bound>>,
+    ) -> Result {
+        Self::resume(interface, data)
+    }
+
+    /// The device is about to be reset.
+    ///
+    /// As for [`suspend`](Driver::suspend), the driver must stop work which could start new
+    /// transfers. The adapter closes the [`IoWindow`] after a successful return.
+    fn pre_reset<'bound>(
+        _interface: &'bound Interface<device::Core<'_>>,
+        _data: Pin<&Self::Data<'bound>>,
+    ) -> Result {
+        Ok(())
+    }
+
+    /// The device has been reset. The adapter reopens its [`IoWindow`] before this is called.
+    fn post_reset<'bound>(
+        _interface: &'bound Interface<device::Core<'_>>,
+        _data: Pin<&Self::Data<'bound>>,
+    ) -> Result {
+        Ok(())
+    }
 }
 
 /// A USB interface.
@@ -544,6 +722,541 @@ pub fn maxp_mult(&self) -> u16 {
     }
 }
 
+impl<Ctx: device::DeviceContext> Interface<Ctx> {
+    /// Returns this interface's `bInterfaceNumber`, or `None` if it currently has no active
+    /// alternate setting.
+    pub fn number(&self) -> Option<u8> {
+        // SAFETY: `self.as_raw()` is a valid `struct usb_interface` by the type invariant.
+        let alt = unsafe { (*self.as_raw()).cur_altsetting };
+        if alt.is_null() {
+            return None;
+        }
+        // SAFETY: `alt` is a valid `struct usb_host_interface` (checked non-null above).
+        Some(unsafe { (*alt).desc.bInterfaceNumber })
+    }
+
+    /// Asks the driver core to unbind whatever driver is currently bound to this interface.
+    ///
+    /// This is the narrow, reviewed replacement for handing out a raw `struct device` pointer: it
+    /// performs exactly one operation (`device_release_driver()`) on this interface's own device,
+    /// and cannot be used to reach the device-wide state of a composite peer.
+    ///
+    /// It is intended for a driver-provided "release my devices" control (e.g. a sysfs attribute),
+    /// and must not be called from the driver's own `probe()` or `disconnect()` callback: the
+    /// driver core already holds the device lock across those.
+    pub fn release_driver(&self) {
+        // SAFETY: `self.as_raw()` is a valid `struct usb_interface` by the type invariant, so the
+        // address of its embedded `dev` is a valid `struct device`. `device_release_driver()`
+        // takes the device lock itself and tolerates a device with no driver bound.
+        unsafe { bindings::device_release_driver(&raw mut (*self.as_raw()).dev) };
+    }
+
+    /// Asks the USB core to reset the device this interface belongs to, from a work item it owns.
+    ///
+    /// Unlike `usb_reset_device()`, this may be called from any context, including one holding the
+    /// device lock or one running inside a completion handler: the core defers the reset to its
+    /// own workqueue. The reset re-enumerates the device, so every driver bound to it is unbound
+    /// and re-probed; a caller must therefore treat its own state as gone from this point.
+    ///
+    /// A driver whose device has stopped responding uses this to recover in place, instead of
+    /// requiring the user to unplug it.
+    pub fn queue_reset_device(&self) {
+        // SAFETY: `self.as_raw()` is a valid `struct usb_interface` by the type invariant, which
+        // is all `usb_queue_reset_device()` requires; it performs no I/O itself and tolerates
+        // being called when a reset is already pending.
+        unsafe { bindings::usb_queue_reset_device(self.as_raw()) };
+    }
+}
+
+/// The transfer type and direction of a USB endpoint, used to tag an [`Endpoint`] so that a
+/// transfer method cannot be pointed at an endpoint of the wrong kind.
+///
+/// This trait is sealed: the set of endpoint kinds is fixed by this module and matches the USB
+/// endpoint types the abstraction supports.
+pub trait EndpointKind: private::Sealed {
+    /// The `bmAttributes` transfer type (`USB_ENDPOINT_XFER_*`) an endpoint must have.
+    const XFER_TYPE: u8;
+
+    /// Whether the endpoint must be an IN (device-to-host) endpoint.
+    const DIR_IN: bool;
+
+    /// Build the USB pipe corresponding to a validated endpoint.
+    fn pipe<Ctx: device::DeviceContext>(dev: &Device<Ctx>, endpoint: &HostEndpoint) -> Pipe;
+}
+
+mod private {
+    /// Seals [`EndpointKind`](super::EndpointKind) against external implementations.
+    pub trait Sealed {}
+}
+
+/// Marker for a bulk IN (device-to-host) endpoint.
+pub enum BulkIn {}
+/// Marker for a bulk OUT (host-to-device) endpoint.
+pub enum BulkOut {}
+/// Marker for an interrupt IN (device-to-host) endpoint.
+pub enum InterruptIn {}
+
+impl private::Sealed for BulkIn {}
+impl private::Sealed for BulkOut {}
+impl private::Sealed for InterruptIn {}
+
+impl EndpointKind for BulkIn {
+    const XFER_TYPE: u8 = bindings::USB_ENDPOINT_XFER_BULK as u8;
+    const DIR_IN: bool = true;
+
+    fn pipe<Ctx: device::DeviceContext>(dev: &Device<Ctx>, endpoint: &HostEndpoint) -> Pipe {
+        Pipe::new_receive_bulk_pipe(dev, endpoint)
+    }
+}
+
+impl EndpointKind for BulkOut {
+    const XFER_TYPE: u8 = bindings::USB_ENDPOINT_XFER_BULK as u8;
+    const DIR_IN: bool = false;
+
+    fn pipe<Ctx: device::DeviceContext>(dev: &Device<Ctx>, endpoint: &HostEndpoint) -> Pipe {
+        Pipe::new_send_bulk_pipe(dev, endpoint)
+    }
+}
+
+impl EndpointKind for InterruptIn {
+    const XFER_TYPE: u8 = bindings::USB_ENDPOINT_XFER_INT as u8;
+    const DIR_IN: bool = true;
+
+    fn pipe<Ctx: device::DeviceContext>(dev: &Device<Ctx>, endpoint: &HostEndpoint) -> Pipe {
+        Pipe::new_receive_int_pipe(dev, endpoint)
+    }
+}
+
+/// An endpoint of a USB interface, looked up in the interface's active alternate setting and
+/// checked to have the transfer type and direction named by `K`.
+///
+/// Because an [`Endpoint`] can only be produced by [`Interface::endpoint`], which validates it
+/// against the descriptor, a `&Endpoint<BulkOut>` is proof that the address really names a bulk
+/// OUT endpoint of that interface. Transfer methods take the correspondingly-typed endpoint, so
+/// the direction/type confusion possible with a bare `u8` address cannot occur.
+///
+/// # Invariants
+///
+/// `addr` is the `bEndpointAddress` of an endpoint that was present in the interface's active
+/// alternate setting, and whose direction and transfer type match `K`.
+pub struct Endpoint<K: EndpointKind> {
+    addr: u8,
+    max_packet: u16,
+    pipe: Pipe,
+    _kind: PhantomData<K>,
+}
+
+impl<K: EndpointKind> Endpoint<K> {
+    /// The endpoint's `bEndpointAddress`, including the direction bit.
+    pub fn address(&self) -> u8 {
+        self.addr
+    }
+
+    /// The endpoint's `wMaxPacketSize`.
+    pub fn max_packet_size(&self) -> u16 {
+        self.max_packet
+    }
+
+    fn pipe(&self) -> Pipe {
+        self.pipe
+    }
+}
+
+impl<K: EndpointKind> Clone for Endpoint<K> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+impl<K: EndpointKind> Copy for Endpoint<K> {}
+
+impl<Ctx: device::DeviceContext> Interface<Ctx> {
+    /// Looks `addr` up in this interface's active alternate setting and returns it as a typed
+    /// [`Endpoint`], provided its direction and transfer type match `K`.
+    ///
+    /// Returns [`ENODEV`] if the interface has no active alternate setting, [`ENOENT`] if no
+    /// endpoint with that address is present, and [`EINVAL`] if the endpoint exists but is of the
+    /// wrong direction or transfer type.
+    pub fn endpoint<K: EndpointKind>(&self, addr: u8) -> Result<Endpoint<K>> {
+        if self.number().is_none() {
+            return Err(ENODEV);
+        }
+        for endpoint in self.cur_altsetting().endpoints() {
+            let endpoint_addr = endpoint.endpoint_number()
+                | if endpoint.endpoint_dir() == Direction::In {
+                    bindings::USB_DIR_IN as u8
+                } else {
+                    0
+                };
+            if endpoint_addr != addr {
+                continue;
+            }
+
+            let is_in = endpoint.endpoint_dir() == Direction::In;
+            if is_in != K::DIR_IN || endpoint.endpoint_type() as u8 != K::XFER_TYPE {
+                return Err(EINVAL);
+            }
+
+            let dev: &Device<Ctx> = self.as_ref();
+            return Ok(Endpoint {
+                addr,
+                max_packet: endpoint.maxp(),
+                pipe: K::pipe(dev, endpoint),
+                _kind: PhantomData,
+            });
+        }
+
+        Err(ENOENT)
+    }
+}
+
+/// Converts a [`Delta`] into the whole-millisecond timeout the synchronous USB message helpers
+/// expect.
+///
+/// Those helpers treat `0` as "wait forever", so a caller who asks for a short-but-non-zero
+/// timeout must not have it silently truncated into an unbounded wait: any non-zero `timeout`
+/// below a millisecond is rounded *up* to 1 ms. Only an explicitly zero [`Delta`] means "wait
+/// indefinitely".
+fn timeout_millis(timeout: Delta) -> Result<kernel::ffi::c_int> {
+    let ms = timeout.as_millis();
+    if ms == 0 && !timeout.is_zero() {
+        return Ok(1);
+    }
+    Ok(ms.try_into()?)
+}
+
+/// A revocable window during which USB I/O is permitted on an interface.
+///
+/// A driver-`Bound` interface is *not* on its own proof that a transfer may be issued: the USB
+/// core forbids I/O outside the window that opens after a successful `probe()`/resume/reset-resume
+/// and must be closed again before `disconnect()`, `suspend()` or `pre_reset()` returns. This type
+/// represents exactly that narrower state.
+///
+/// The USB adapter owns one `IoWindow` for every successfully bound interface and passes a
+/// reference-counted handle to [`Driver::probe`]. Drivers take an [`Io`] token from it around every
+/// transfer. The adapter revokes the window and blocks until every outstanding token has been
+/// dropped before suspend, reset or disconnect completes.
+///
+/// Because [`Io`] borrows the window, and the transfer methods and queues live on [`Io`], a
+/// transfer cannot outlive the window that permitted it.
+///
+#[pin_data]
+pub struct IoWindow {
+    /// The interface I/O is permitted on. Holding a reference keeps the `struct usb_interface`
+    /// allocated; that it is still *bound* is what the open/closed state tracks.
+    interface: ARef<Interface>,
+    #[pin]
+    state: Mutex<IoState>,
+    #[pin]
+    idle: CondVar,
+}
+
+/// The mutable half of an [`IoWindow`].
+struct IoState {
+    /// Whether new [`Io`] tokens may still be handed out.
+    open: bool,
+    /// How many [`Io`] tokens are currently alive.
+    active: usize,
+}
+
+impl IoWindow {
+    /// Creates the open I/O window owned by the USB adapter.
+    fn new(interface: ARef<Interface>) -> impl PinInit<Self> {
+        pin_init!(Self {
+            interface,
+            state <- new_mutex!(IoState {
+                open: true,
+                active: 0,
+            }),
+            idle <- new_condvar!(),
+        })
+    }
+
+    /// Takes an [`Io`] token, proving that I/O is permitted for as long as the token is held.
+    ///
+    /// Returns [`ENODEV`] once the window has been closed.
+    pub fn enter(&self) -> Result<Io<'_>> {
+        let mut state = self.state.lock();
+        if !state.open {
+            return Err(ENODEV);
+        }
+        state.active = state.active.checked_add(1).ok_or(EOVERFLOW)?;
+        drop(state);
+
+        Ok(Io { window: self })
+    }
+
+    /// The interface this window permits I/O on.
+    pub fn interface(&self) -> &Interface {
+        &self.interface
+    }
+
+    /// Closes the window and waits until no I/O is in flight.
+    ///
+    /// New [`Io`] tokens are refused immediately, then the call blocks until the last outstanding
+    /// token has been dropped.
+    ///
+    /// This is idempotent and sleeps, so the adapter only calls it from process context.
+    fn close(&self) {
+        let mut state = self.state.lock();
+        state.open = false;
+
+        while state.active != 0 {
+            self.idle.wait(&mut state);
+        }
+    }
+
+    /// Reopens a window that was closed by a suspend or pre-reset.
+    ///
+    /// The adapter only calls this after the USB core has re-permitted I/O.
+    fn reopen(&self) {
+        self.state.lock().open = true;
+    }
+}
+
+/// Proof that USB I/O is currently permitted on an interface, and the handle through which every
+/// transfer is issued.
+///
+/// Obtained from [`IoWindow::enter`] and released when dropped; [`IoWindow::close`] blocks until
+/// every outstanding token is gone. Because the token borrows both the window and the interface,
+/// no transfer can outlive either.
+pub struct Io<'a> {
+    window: &'a IoWindow,
+}
+
+impl Drop for Io<'_> {
+    fn drop(&mut self) {
+        let mut state = self.window.state.lock();
+        state.active -= 1;
+        if state.active == 0 {
+            self.window.idle.notify_all();
+        }
+    }
+}
+
+impl<'a> Io<'a> {
+    /// The interface this token permits I/O on.
+    pub fn interface(&self) -> &Interface {
+        self.window.interface()
+    }
+
+    /// The `struct usb_device` that interface belongs to.
+    fn device(&self) -> *mut bindings::usb_device {
+        // SAFETY: the window holds a reference to a valid `struct usb_interface`, and
+        // `interface_to_usbdev()` returns its valid `struct usb_device`.
+        unsafe { bindings::interface_to_usbdev(self.window.interface.as_raw()) }
+    }
+
+    /// Clears a halt/stall on `endpoint`, resetting both the device-side stall and the host-side
+    /// data toggle. Sleeps.
+    pub fn clear_halt<K: EndpointKind>(&self, endpoint: &Endpoint<K>) -> Result {
+        let dev = self.device();
+
+        // SAFETY: `dev` is valid; `usb_clear_halt()` only issues a control request and updates
+        // host-side endpoint state.
+        to_result(unsafe { bindings::usb_clear_halt(dev, endpoint.pipe().0 as kernel::ffi::c_int) })
+    }
+
+    /// Issues a synchronous bulk OUT transfer of `data`, returning the number of bytes
+    /// transferred.
+    ///
+    /// `data` is copied into a kmalloc'd bounce buffer internally, so it need not be DMA-capable.
+    /// `gfp` selects that buffer's allocation flags: pass `GFP_KERNEL` normally, or `GFP_NOIO` on
+    /// a reset/resume or error-handling path. Sleeps.
+    pub fn bulk_send(
+        &self,
+        endpoint: &Endpoint<BulkOut>,
+        data: &[u8],
+        timeout: Delta,
+        gfp: Flags,
+    ) -> Result<usize> {
+        let mut actual: kernel::ffi::c_int = 0;
+        let millis = timeout_millis(timeout)?;
+
+        // `usb_bulk_msg()` DMAs straight from the buffer, and `data` may live on the stack or in
+        // `.rodata`, so bounce it through a kmalloc'd allocation.
+        let mut buf = KVec::with_capacity(data.len(), gfp)?;
+        buf.extend_from_slice(data, gfp)?;
+        let len = buf.len().try_into()?;
+
+        let dev = self.device();
+        // SAFETY: `dev` is valid; `buf` is a kmalloc'd buffer valid for reads of `len` bytes for
+        // the duration of the call; `actual` is a valid out-pointer.
+        to_result(unsafe {
+            bindings::usb_bulk_msg(
+                dev,
+                endpoint.pipe().0,
+                buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+                len,
+                &mut actual,
+                millis,
+            )
+        })?;
+
+        Ok(actual as usize)
+    }
+
+    /// Issues a synchronous bulk IN transfer into `data`, returning the number of bytes received.
+    ///
+    /// The data is received into a kmalloc'd bounce buffer and copied out, so `data` need not be
+    /// DMA-capable. Sleeps.
+    pub fn bulk_recv(
+        &self,
+        endpoint: &Endpoint<BulkIn>,
+        data: &mut [u8],
+        timeout: Delta,
+        gfp: Flags,
+    ) -> Result<usize> {
+        let mut actual: kernel::ffi::c_int = 0;
+        let millis = timeout_millis(timeout)?;
+
+        let mut buf = KVec::from_elem(0u8, data.len(), gfp)?;
+        let len = buf.len().try_into()?;
+
+        let dev = self.device();
+        // SAFETY: `dev` is valid; `buf` is a kmalloc'd buffer valid for writes of `len` bytes for
+        // the duration of the call; `actual` is a valid out-pointer.
+        to_result(unsafe {
+            bindings::usb_bulk_msg(
+                dev,
+                endpoint.pipe().0,
+                buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+                len,
+                &mut actual,
+                millis,
+            )
+        })?;
+
+        // `usb_bulk_msg()` never reports more than the requested length.
+        let n = (actual as usize).min(data.len());
+        data[..n].copy_from_slice(&buf[..n]);
+        Ok(n)
+    }
+
+    /// Issues a synchronous interrupt IN transfer into `data`, returning the number of bytes
+    /// received.
+    ///
+    /// As for [`bulk_recv`](Self::bulk_recv), the transfer is bounced through a kmalloc'd buffer.
+    /// Sleeps.
+    pub fn interrupt_recv(
+        &self,
+        endpoint: &Endpoint<InterruptIn>,
+        data: &mut [u8],
+        timeout: Delta,
+        gfp: Flags,
+    ) -> Result<usize> {
+        let mut actual: kernel::ffi::c_int = 0;
+        let millis = timeout_millis(timeout)?;
+
+        let mut buf = KVec::from_elem(0u8, data.len(), gfp)?;
+        let len = buf.len().try_into()?;
+
+        let dev = self.device();
+        // SAFETY: `dev` is valid; `buf` is a kmalloc'd buffer valid for writes of `len` bytes for
+        // the duration of the call; `actual` is a valid out-pointer.
+        to_result(unsafe {
+            bindings::usb_interrupt_msg(
+                dev,
+                endpoint.pipe().0,
+                buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+                len,
+                &mut actual,
+                millis,
+            )
+        })?;
+
+        let n = (actual as usize).min(data.len());
+        data[..n].copy_from_slice(&buf[..n]);
+        Ok(n)
+    }
+
+    /// Issues a synchronous control OUT transfer on the default control endpoint.
+    ///
+    /// `request`, `request_type`, `value` and `index` are the `bRequest`, `bmRequestType`,
+    /// `wValue` and `wIndex` setup fields. The buffer is copied internally, so `data` need not be
+    /// DMA-capable. Sleeps.
+    pub fn control_send(
+        &self,
+        request: u8,
+        request_type: u8,
+        value: u16,
+        index: u16,
+        data: &[u8],
+        timeout: Delta,
+        gfp: Flags,
+    ) -> Result {
+        let millis = timeout_millis(timeout)?;
+        let len = data.len().try_into()?;
+
+        // SAFETY: `self.device()` is valid; `data` is valid for reads of `len` bytes and
+        // `usb_control_msg_send()` copies out of it before returning.
+        to_result(unsafe {
+            bindings::usb_control_msg_send(
+                self.device(),
+                0,
+                request,
+                request_type,
+                value,
+                index,
+                data.as_ptr().cast::<kernel::ffi::c_void>(),
+                len,
+                millis,
+                gfp.as_raw(),
+            )
+        })
+    }
+
+    /// Issues a synchronous control IN transfer on the default control endpoint, filling `data`
+    /// with exactly `data.len()` bytes.
+    ///
+    /// The transfer fails if the device returns fewer bytes than requested. Sleeps.
+    pub fn control_recv(
+        &self,
+        request: u8,
+        request_type: u8,
+        value: u16,
+        index: u16,
+        data: &mut [u8],
+        timeout: Delta,
+        gfp: Flags,
+    ) -> Result {
+        let millis = timeout_millis(timeout)?;
+        let len = data.len().try_into()?;
+
+        // SAFETY: `self.device()` is valid; `data` is valid for writes of `len` bytes and
+        // `usb_control_msg_recv()` copies into it before returning.
+        to_result(unsafe {
+            bindings::usb_control_msg_recv(
+                self.device(),
+                0,
+                request,
+                request_type,
+                value,
+                index,
+                data.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+                len,
+                millis,
+                gfp.as_raw(),
+            )
+        })
+    }
+
+    /// Selects alternate setting `alternate` of *this* interface (`SET_INTERFACE`).
+    ///
+    /// Unlike a device-wide `set_interface()`, this can only ever retarget the interface the
+    /// driver is bound to: the interface number comes from the bound interface itself, not from
+    /// the caller, so a driver cannot disturb a sibling interface of a composite device. Sleeps.
+    pub fn set_alternate_setting(&self, alternate: u8) -> Result {
+        let number = self.window.interface.number().ok_or(ENODEV)?;
+
+        // SAFETY: `self.device()` is a valid `struct usb_device`, and `number` is the number of
+        // one of its interfaces -- the one this driver is bound to.
+        to_result(unsafe {
+            bindings::usb_set_interface(self.device(), number.into(), alternate.into())
+        })
+    }
+}
+
 // SAFETY: `usb::Interface` is a transparent wrapper of `struct usb_interface`.
 // The offset is guaranteed to point to a valid device field inside `usb::Interface`.
 unsafe impl<Ctx: device::DeviceContext> device::AsBusDevice<Ctx> for Interface<Ctx> {
@@ -634,17 +1347,20 @@ pub enum TransferFlag {
 
 impl Pipe {
     /// Create a host-to-device (OUT) control pipe (endpoint 0).
-    pub fn new_send_control_pipe(dev: &Device) -> Self {
+    pub fn new_send_control_pipe<Ctx: device::DeviceContext>(dev: &Device<Ctx>) -> Self {
         Self(bindings::PIPE_CONTROL << 30 | dev.devnum() << 8)
     }
 
     /// Create a device-to-host (IN) control pipe (endpoint 0).
-    pub fn new_receive_control_pipe(dev: &Device) -> Self {
+    pub fn new_receive_control_pipe<Ctx: device::DeviceContext>(dev: &Device<Ctx>) -> Self {
         Self(bindings::PIPE_CONTROL << 30 | dev.devnum() << 8 | bindings::USB_DIR_IN)
     }
 
     /// Create a device-to-host (IN) isochronous pipe.
-    pub fn new_receive_isoc_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
+    pub fn new_receive_isoc_pipe<Ctx: device::DeviceContext>(
+        dev: &Device<Ctx>,
+        endpoint: &HostEndpoint,
+    ) -> Self {
         Self(
             bindings::PIPE_ISOCHRONOUS << 30
                 | dev.devnum() << 8
@@ -654,7 +1370,10 @@ pub fn new_receive_isoc_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
     }
 
     /// Create a host-to-device (OUT) isochronous pipe.
-    pub fn new_send_isoc_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
+    pub fn new_send_isoc_pipe<Ctx: device::DeviceContext>(
+        dev: &Device<Ctx>,
+        endpoint: &HostEndpoint,
+    ) -> Self {
         Self(
             bindings::PIPE_ISOCHRONOUS << 30
                 | dev.devnum() << 8
@@ -663,7 +1382,10 @@ pub fn new_send_isoc_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
     }
 
     /// Create a host-to-device (OUT) bulk pipe.
-    pub fn new_send_bulk_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
+    pub fn new_send_bulk_pipe<Ctx: device::DeviceContext>(
+        dev: &Device<Ctx>,
+        endpoint: &HostEndpoint,
+    ) -> Self {
         Self(
             bindings::PIPE_BULK << 30
                 | dev.devnum() << 8
@@ -672,7 +1394,10 @@ pub fn new_send_bulk_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
     }
 
     /// Create a device-to-host (IN) bulk pipe.
-    pub fn new_receive_bulk_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
+    pub fn new_receive_bulk_pipe<Ctx: device::DeviceContext>(
+        dev: &Device<Ctx>,
+        endpoint: &HostEndpoint,
+    ) -> Self {
         Self(
             bindings::PIPE_BULK << 30
                 | dev.devnum() << 8
@@ -682,7 +1407,10 @@ pub fn new_receive_bulk_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
     }
 
     /// Create a host-to-device (OUT) interrupt pipe.
-    pub fn new_send_int_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
+    pub fn new_send_int_pipe<Ctx: device::DeviceContext>(
+        dev: &Device<Ctx>,
+        endpoint: &HostEndpoint,
+    ) -> Self {
         Self(
             bindings::PIPE_INTERRUPT << 30
                 | dev.devnum() << 8
@@ -691,7 +1419,10 @@ pub fn new_send_int_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
     }
 
     /// Create a device-to-host (IN) interrupt pipe.
-    pub fn new_receive_int_pipe(dev: &Device, endpoint: &HostEndpoint) -> Self {
+    pub fn new_receive_int_pipe<Ctx: device::DeviceContext>(
+        dev: &Device<Ctx>,
+        endpoint: &HostEndpoint,
+    ) -> Self {
         Self(
             bindings::PIPE_INTERRUPT << 30
                 | dev.devnum() << 8
diff --git a/samples/rust/rust_driver_usb.rs b/samples/rust/rust_driver_usb.rs
index 02bd5085f9bc..0f5fb8392149 100644
--- a/samples/rust/rust_driver_usb.rs
+++ b/samples/rust/rust_driver_usb.rs
@@ -9,7 +9,10 @@
         Core, //
     },
     prelude::*,
-    sync::aref::ARef,
+    sync::{
+        aref::ARef,
+        Arc, //
+    },
     usb, //
 };
 
@@ -33,6 +36,7 @@ fn probe<'bound>(
         intf: &'bound usb::Interface<Core<'_>>,
         _id: &usb::DeviceId,
         _info: &'bound Self::IdInfo,
+        _io: Arc<usb::IoWindow>,
     ) -> impl PinInit<Self, Error> + 'bound {
         let dev: &device::Device<Core<'_>> = intf.as_ref();
         dev_info!(dev, "Rust USB driver sample probed\n");

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 2/5] rust: usb: add reusable URBs and persistent bulk queues
  2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 1/5] rust: usb: add revocable typed interface I/O Mike Lothian
@ 2026-08-26 16:30 ` Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 3/5] rust: usb: expose device descriptor fields and queue readiness Mike Lothian
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Mike Lothian @ 2026-08-26 16:30 UTC (permalink / raw)
  To: linux-usb
  Cc: Mike Lothian, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
	Alexandre Courbot, Onur Özkan, Greg Kroah-Hartman,
	Colin Braun, rust-for-linux, linux-kernel

Allow an idle URB to retain its full transfer allocation, vary the
submitted length, recover the handle after completion or a failed
submission, and carry the narrow cancellation capability needed by
an owning I/O window.

Build bounded bulk-IN and bulk-OUT queues from those typed
URBs. Preallocate every slot and DMA buffer, require an I/O token
from the same interface for each operation, and register all queue
URBs so closing the window cancels blocked transfers before waiting
for users to drain.

This lets streaming drivers reuse the common USB abstraction instead
of carrying private URB allocation, completion, cancellation, and
teardown code.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
 rust/helpers/usb.c |  17 ++
 rust/kernel/usb.rs | 638 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 648 insertions(+), 7 deletions(-)

diff --git a/rust/helpers/usb.c b/rust/helpers/usb.c
index ac7b30334882..1501f5438e43 100644
--- a/rust/helpers/usb.c
+++ b/rust/helpers/usb.c
@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 
+#include <linux/completion.h>
 #include <linux/usb.h>
 
 __rust_helper struct usb_device *
@@ -8,6 +9,22 @@ rust_helper_interface_to_usbdev(struct usb_interface *intf)
 	return interface_to_usbdev(intf);
 }
 
+__rust_helper void
+rust_helper_usb_fill_bulk_urb(struct urb *urb, struct usb_device *dev,
+			      unsigned int pipe, void *transfer_buffer,
+			      int buffer_length, usb_complete_t complete_fn,
+			      void *context)
+{
+	usb_fill_bulk_urb(urb, dev, pipe, transfer_buffer, buffer_length,
+			  complete_fn, context);
+}
+
+__rust_helper void
+rust_helper_reinit_completion(struct completion *x)
+{
+	reinit_completion(x);
+}
+
 __rust_helper unsigned int
 rust_helper_usb_sndbulkpipe(struct usb_device *dev, unsigned int endpoint)
 {
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index c3d0cc36d425..ad40c814616a 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -28,6 +28,7 @@
         new_mutex,
         Arc,
         ArcBorrow,
+        Completion,
         CondVar,
         Mutex, //
     },
@@ -935,7 +936,8 @@ fn timeout_millis(timeout: Delta) -> Result<kernel::ffi::c_int> {
 /// The USB adapter owns one `IoWindow` for every successfully bound interface and passes a
 /// reference-counted handle to [`Driver::probe`]. Drivers take an [`Io`] token from it around every
 /// transfer. The adapter revokes the window and blocks until every outstanding token has been
-/// dropped before suspend, reset or disconnect completes.
+/// dropped and every queue URB registered against it has been killed before suspend, reset or
+/// disconnect completes.
 ///
 /// Because [`Io`] borrows the window, and the transfer methods and queues live on [`Io`], a
 /// transfer cannot outlive the window that permitted it.
@@ -957,6 +959,17 @@ struct IoState {
     open: bool,
     /// How many [`Io`] tokens are currently alive.
     active: usize,
+    /// URBs belonging to queues opened against this window, so [`IoWindow::close`] can cancel
+    /// them even though the queues themselves are owned by the driver.
+    urbs: KVec<RegisteredUrb>,
+    /// Source of the per-queue tokens used to deregister a queue's URBs on drop.
+    next_token: u64,
+}
+
+/// One queue-owned URB registered with an [`IoWindow`], tagged with its queue's token.
+struct RegisteredUrb {
+    token: u64,
+    canceller: UrbCanceller,
 }
 
 impl IoWindow {
@@ -967,6 +980,8 @@ fn new(interface: ARef<Interface>) -> impl PinInit<Self> {
             state <- new_mutex!(IoState {
                 open: true,
                 active: 0,
+                urbs: KVec::new(),
+                next_token: 0,
             }),
             idle <- new_condvar!(),
         })
@@ -993,17 +1008,32 @@ pub fn interface(&self) -> &Interface {
 
     /// Closes the window and waits until no I/O is in flight.
     ///
-    /// New [`Io`] tokens are refused immediately, then the call blocks until the last outstanding
-    /// token has been dropped.
+    /// New [`Io`] tokens are refused immediately. Every URB belonging to a queue opened against
+    /// this window is killed, which releases anything blocked waiting on one, and the call then
+    /// blocks until the last outstanding token has been dropped.
     ///
     /// This is idempotent and sleeps, so the adapter only calls it from process context.
     fn close(&self) {
         let mut state = self.state.lock();
         state.open = false;
 
+        // First wake token holders blocked in `recv()` or `flush()`. A token acquired before
+        // `open` was cleared may still resubmit while unwinding, so this is only the wakeup pass.
+        for reg in state.urbs.iter() {
+            reg.canceller.cancel();
+        }
+
         while state.active != 0 {
             self.idle.wait(&mut state);
         }
+
+        // No token can submit after this point. Cancel once more to catch a final resubmission
+        // made while an existing token was unwinding. Cancellation waits for the completion
+        // callback, which takes no window lock. Holding the lock also keeps queue deregistration
+        // from releasing a cancellation capability while it is used here.
+        for reg in state.urbs.iter() {
+            reg.canceller.cancel();
+        }
     }
 
     /// Reopens a window that was closed by a suspend or pre-reset.
@@ -1012,6 +1042,36 @@ fn close(&self) {
     fn reopen(&self) {
         self.state.lock().open = true;
     }
+
+    /// Registers `urb` as belonging to the queue identified by `token`, so [`close`] can cancel
+    /// it.
+    ///
+    /// [`close`]: IoWindow::close
+    fn register_urb(&self, token: u64, canceller: UrbCanceller) -> Result {
+        let mut state = self.state.lock();
+        if !state.open {
+            return Err(ENODEV);
+        }
+        Ok(state
+            .urbs
+            .push(RegisteredUrb { token, canceller }, GFP_KERNEL)?)
+    }
+
+    /// Allocates a fresh queue token.
+    fn new_token(&self) -> Result<u64> {
+        let mut state = self.state.lock();
+        if !state.open {
+            return Err(ENODEV);
+        }
+        let token = state.next_token;
+        state.next_token = state.next_token.checked_add(1).ok_or(EOVERFLOW)?;
+        Ok(token)
+    }
+
+    /// Drops every URB registration made under `token`.
+    fn deregister(&self, token: u64) {
+        self.state.lock().urbs.retain(|reg| reg.token != token);
+    }
 }
 
 /// Proof that USB I/O is currently permitted on an interface, and the handle through which every
@@ -1040,6 +1100,15 @@ pub fn interface(&self) -> &Interface {
         self.window.interface()
     }
 
+    /// Returns the interface in the bound context proven by this token.
+    fn bound_interface(&self) -> &Interface<device::Bound> {
+        // SAFETY: The adapter only opens an `IoWindow` after successful
+        // probe/resume/reset completion and closes it before the interface
+        // leaves the bound I/O state. Holding `Io` proves that window is
+        // still open for this borrow.
+        unsafe { &*(core::ptr::from_ref(self.window.interface()).cast()) }
+    }
+
     /// The `struct usb_device` that interface belongs to.
     fn device(&self) -> *mut bindings::usb_device {
         // SAFETY: the window holds a reference to a valid `struct usb_interface`, and
@@ -1257,6 +1326,432 @@ pub fn set_alternate_setting(&self, alternate: u8) -> Result {
     }
 }
 
+/// The I/O-window registration shared by both queue types.
+///
+/// Owning this as a separate field means a queue under construction already has a working `Drop`
+/// before any per-slot allocation is attempted, so a mid-construction failure
+/// cannot leave URBs registered with the window.
+///
+/// The window is held by [`Arc`] rather than borrowed because a queue normally lives in the
+/// driver's device data, which has no lifetime to borrow from. That does not weaken the guarantee
+/// that matters: every queue operation still requires an [`Io`] token, which [`IoWindow::close`]
+/// stops issuing, and `close()` cancels the queue's URBs through its registration.
+struct QueueRegistration {
+    window: Arc<IoWindow>,
+    token: u64,
+}
+
+impl QueueRegistration {
+    fn new(window: &Arc<IoWindow>) -> Result<Self> {
+        Ok(Self {
+            token: window.new_token()?,
+            window: window.clone(),
+        })
+    }
+
+    fn register(&self, canceller: UrbCanceller) -> Result {
+        self.window.register_urb(self.token, canceller)
+    }
+
+    /// Checks that `io` was taken from the same window this queue was opened against, so a queue
+    /// cannot be driven using a token that proves nothing about *its* device's I/O state.
+    fn check(&self, io: &Io<'_>) -> Result {
+        if !core::ptr::eq(&*self.window, io.window) {
+            return Err(EINVAL);
+        }
+        Ok(())
+    }
+}
+
+impl Drop for QueueRegistration {
+    fn drop(&mut self) {
+        // Drop the window's records of this queue's URBs before the queue frees them, so a
+        // concurrent `IoWindow::close()` can never see a freed URB.
+        self.window.deregister(self.token);
+    }
+}
+
+enum QueueUrb {
+    Idle(Pin<UrbHandle<Completion, Idle>>),
+    Active(UrbHandle<Completion, Active>),
+}
+
+/// One persistent queue slot built from the common typed URB abstraction.
+struct UrbSlot {
+    urb: Option<QueueUrb>,
+    done: Arc<Completion>,
+    capacity: usize,
+}
+
+impl UrbSlot {
+    fn new(io: &Io<'_>, pipe: Pipe, buf_len: usize) -> Result<Self> {
+        let buffer: Pin<KBox<[u8]>> = KBox::pin_slice(
+            |_| {
+                // SAFETY: The initializer writes one valid `u8` and cannot
+                // fail after partially initializing the element.
+                unsafe {
+                    pin_init::pin_init_from_closure(|slot: *mut u8| {
+                        slot.write(0);
+                        Ok::<(), Error>(())
+                    })
+                }
+            },
+            buf_len,
+            GFP_KERNEL,
+        )?;
+        let buffer = Pin::into_inner(buffer);
+        let done = Arc::pin_init(Completion::new(), GFP_KERNEL)?;
+        let urb = Urb::<Completion>::new_bulk(
+            GFP_KERNEL,
+            io.bound_interface(),
+            pipe,
+            buffer,
+            Some(done.clone()),
+            urb_signal_complete,
+            TransferFlags::default(),
+        )?;
+
+        Ok(Self {
+            urb: Some(QueueUrb::Idle(urb)),
+            done,
+            capacity: buf_len,
+        })
+    }
+
+    fn canceller(&self) -> Result<UrbCanceller> {
+        match self.urb.as_ref() {
+            Some(QueueUrb::Idle(urb)) => Ok(urb.canceller()),
+            Some(QueueUrb::Active(urb)) => Ok(urb.canceller()),
+            None => Err(EIO),
+        }
+    }
+
+    fn is_active(&self) -> bool {
+        matches!(self.urb, Some(QueueUrb::Active(_)))
+    }
+
+    fn wait(&self, timeout: Delta) -> bool {
+        let millis = timeout.as_millis();
+        let millis = if millis <= 0 {
+            0
+        } else {
+            millis.try_into().unwrap_or(u32::MAX)
+        };
+        self.done
+            .wait_for_completion_timeout(crate::time::msecs_to_jiffies(millis))
+    }
+
+    fn finish(&mut self) -> Result<(i32, usize)> {
+        let state = self.urb.take().ok_or(EIO)?;
+        let active = match state {
+            QueueUrb::Active(urb) => urb,
+            idle @ QueueUrb::Idle(_) => {
+                self.urb = Some(idle);
+                return Err(EINVAL);
+            }
+        };
+        let idle = active.into_idle();
+        let status = idle.status();
+        let actual = idle.inner().actual_length as usize;
+        self.urb = Some(QueueUrb::Idle(idle));
+        Ok((status, actual))
+    }
+
+    fn copy_from_buffer(&mut self, out: &mut [u8], len: usize) -> Result<usize> {
+        let state = self.urb.take().ok_or(EIO)?;
+        let idle = match state {
+            QueueUrb::Idle(urb) => unsafe { Pin::into_inner_unchecked(urb) },
+            active @ QueueUrb::Active(_) => {
+                self.urb = Some(active);
+                return Err(EBUSY);
+            }
+        };
+        let n = len.min(out.len()).min(self.capacity);
+        out[..n].copy_from_slice(&idle.transfer_buffer()[..n]);
+        // SAFETY: The C URB allocation is stable independently of this handle.
+        self.urb = Some(QueueUrb::Idle(unsafe { Pin::new_unchecked(idle) }));
+        Ok(n)
+    }
+
+    fn prepare_transfer(&mut self, data: &[u8]) -> Result {
+        let state = self.urb.take().ok_or(EIO)?;
+        let mut idle = match state {
+            QueueUrb::Idle(urb) => unsafe { Pin::into_inner_unchecked(urb) },
+            active @ QueueUrb::Active(_) => {
+                self.urb = Some(active);
+                return Err(EBUSY);
+            }
+        };
+        let result = if data.len() > self.capacity {
+            Err(EMSGSIZE)
+        } else {
+            idle.transfer_buffer_mut()[..data.len()].copy_from_slice(data);
+            idle.set_transfer_buffer_length(data.len())
+        };
+        // SAFETY: The C URB allocation is stable independently of this handle.
+        self.urb = Some(QueueUrb::Idle(unsafe { Pin::new_unchecked(idle) }));
+        result
+    }
+
+    fn submit(&mut self) -> Result {
+        let state = self.urb.take().ok_or(EIO)?;
+        let idle = match state {
+            QueueUrb::Idle(urb) => urb,
+            active @ QueueUrb::Active(_) => {
+                self.urb = Some(active);
+                return Err(EBUSY);
+            }
+        };
+
+        match idle.submit_recoverable(GFP_KERNEL) {
+            Ok(active) => {
+                self.urb = Some(QueueUrb::Active(active));
+                Ok(())
+            }
+            Err((error, idle)) => {
+                self.urb = Some(QueueUrb::Idle(idle));
+                Err(error)
+            }
+        }
+    }
+}
+
+/// A persistently-queued asynchronous bulk IN reader. See [`Io::bulk_in_queue`].
+///
+/// [`recv`](Self::recv) waits for the next queued transfer, copies its data out and immediately
+/// re-submits its URB, so the endpoint stays posted.
+pub struct BulkInQueue {
+    inner: QueueRegistration,
+    slots: KVec<UrbSlot>,
+    cursor: usize,
+}
+
+// SAFETY: The queue exclusively owns its device reference, URBs, buffers and completions. None is
+// tied to the creating thread, every operation that mutates it takes `&mut self`, and `Drop` kills
+// each URB before releasing the resources it refers to.
+unsafe impl Send for BulkInQueue {}
+
+impl BulkInQueue {
+    /// Opens a persistently-queued asynchronous bulk IN reader on `endpoint`.
+    ///
+    /// Allocates `depth` URBs, each with its own `buf_len`-byte DMA buffer, and submits them all
+    /// up front, so the controller keeps `depth` IN transfers posted to the device continuously.
+    /// This differs from [`Io::bulk_recv`], which posts a single URB only for the duration of the
+    /// call and so leaves the endpoint un-posted in between -- a window in which a device that
+    /// pushes a large reply while the host is blocked on an OUT can deadlock the bus.
+    ///
+    /// `io` must have been taken from `window`; it proves I/O is permitted right now. The queue's
+    /// URBs are registered with `window`, so [`IoWindow::close`] cancels them.
+    ///
+    /// Sleeps; must be called from process context.
+    pub fn new(
+        window: &Arc<IoWindow>,
+        io: &Io<'_>,
+        endpoint: &Endpoint<BulkIn>,
+        depth: usize,
+        buf_len: usize,
+    ) -> Result<Self> {
+        // A zero-depth queue has no slots, but `recv()` indexes slot zero and takes the cursor
+        // modulo the slot count; reject it rather than divide by zero later.
+        if depth == 0 || buf_len == 0 {
+            return Err(EINVAL);
+        }
+        if !core::ptr::eq(&**window, io.window) {
+            return Err(EINVAL);
+        }
+
+        let pipe = endpoint.pipe();
+
+        // Build the queue -- which owns the device reference and whose `Drop` releases everything
+        // allocated so far -- before any fallible per-slot work, so no early return can leak.
+        let mut queue = Self {
+            inner: QueueRegistration::new(window)?,
+            slots: KVec::with_capacity(depth, GFP_KERNEL)?,
+            cursor: 0,
+        };
+
+        for _ in 0..depth {
+            let slot = UrbSlot::new(io, pipe, buf_len)?;
+            queue.inner.register(slot.canceller()?)?;
+            queue.slots.push(slot, GFP_KERNEL)?;
+        }
+
+        // Post every URB. On failure the queue's `Drop` kills and frees the rest.
+        for slot in queue.slots.iter_mut() {
+            slot.submit()?;
+        }
+
+        Ok(queue)
+    }
+
+    /// Waits up to `timeout` for the next queued IN transfer and copies up to `out.len()` bytes of
+    /// it into `out`.
+    ///
+    /// Returns `Ok(Some(n))` when a transfer completed -- its URB is re-submitted before
+    /// returning, so the endpoint stays posted -- `Ok(None)` on timeout with the URB still
+    /// outstanding, or `Err` if the transfer or the re-submission failed.
+    ///
+    /// `io` must come from the same [`IoWindow`] this queue was opened against; it proves I/O is
+    /// still permitted. Sleeps.
+    pub fn recv(&mut self, io: &Io<'_>, out: &mut [u8], timeout: Delta) -> Result<Option<usize>> {
+        self.inner.check(io)?;
+
+        let i = self.cursor;
+
+        // A previous re-submission may have failed, leaving this slot un-posted. Waiting on it
+        // would block on a completion that can never fire, so re-post it first.
+        if !self.slots[i].is_active() {
+            self.slots[i].submit()?;
+        }
+
+        if !self.slots[i].wait(timeout) {
+            // Still outstanding; leave it posted so a later call keeps waiting on the same slot.
+            return Ok(None);
+        }
+
+        let (status, len) = self.slots[i].finish()?;
+        let n = self.slots[i].copy_from_buffer(out, len)?;
+
+        self.cursor = (i + 1) % self.slots.len();
+
+        // Keep the endpoint posted, then report the completed transfer's status.
+        let resubmit = self.slots[i].submit();
+        if status != 0 {
+            return Err(Error::from_errno(status));
+        }
+        resubmit?;
+
+        Ok(Some(n))
+    }
+}
+
+/// An asynchronous, pipelined bulk OUT writer. See [`Io::bulk_out_queue`].
+///
+/// [`send`](Self::send) round-robins over the slots, waiting only for the transfer that previously
+/// used the slot it is about to reuse, so up to `depth - 1` transfers stay in flight while the
+/// next is prepared. [`flush`](Self::flush) drains them all.
+pub struct BulkOutQueue {
+    inner: QueueRegistration,
+    slots: KVec<UrbSlot>,
+    cursor: usize,
+}
+
+// SAFETY: As for `BulkInQueue`, the queue exclusively owns everything it refers to and cancels
+// every URB before releasing it.
+unsafe impl Send for BulkOutQueue {}
+
+impl BulkOutQueue {
+    /// Opens an asynchronous, pipelined bulk OUT writer on `endpoint`.
+    ///
+    /// Pre-allocates `depth` URBs with `buf_len`-byte DMA buffers but submits none up front: an
+    /// OUT URB carries caller data, so it is filled and submitted per [`send`](Self::send). This
+    /// lets up to `depth` transfers be in flight at once, instead of [`Io::bulk_send`]'s
+    /// block-per-transfer round trip.
+    ///
+    /// `io` must have been taken from `window`. Sleeps; must be called from process context.
+    pub fn new(
+        window: &Arc<IoWindow>,
+        io: &Io<'_>,
+        endpoint: &Endpoint<BulkOut>,
+        depth: usize,
+        buf_len: usize,
+    ) -> Result<Self> {
+        if depth == 0 || buf_len == 0 {
+            return Err(EINVAL);
+        }
+        if !core::ptr::eq(&**window, io.window) {
+            return Err(EINVAL);
+        }
+
+        let pipe = endpoint.pipe();
+
+        let mut queue = Self {
+            inner: QueueRegistration::new(window)?,
+            slots: KVec::with_capacity(depth, GFP_KERNEL)?,
+            cursor: 0,
+        };
+
+        for _ in 0..depth {
+            let slot = UrbSlot::new(io, pipe, buf_len)?;
+            queue.inner.register(slot.canceller()?)?;
+            queue.slots.push(slot, GFP_KERNEL)?;
+        }
+
+        Ok(queue)
+    }
+
+    /// Reaps slot `i` if it has an outstanding transfer, returning that transfer's status.
+    ///
+    /// `Ok(false)` means nothing was outstanding.
+    fn reap(&mut self, i: usize, timeout: Delta) -> Result<bool> {
+        if !self.slots[i].is_active() {
+            return Ok(false);
+        }
+
+        if !self.slots[i].wait(timeout) {
+            // Leave it posted so a later call keeps waiting on it.
+            return Err(ETIMEDOUT);
+        }
+        let (status, _) = self.slots[i].finish()?;
+        if status != 0 {
+            return Err(Error::from_errno(status));
+        }
+
+        Ok(true)
+    }
+
+    /// Submits `data` as a bulk OUT transfer without waiting for it to complete.
+    ///
+    /// If the slot about to be reused still has a transfer outstanding, this blocks up to
+    /// `timeout` reaping it and surfaces its error. `data` must be no longer than the queue's
+    /// `buf_len`, else [`EMSGSIZE`].
+    ///
+    /// `io` must come from the same [`IoWindow`] this queue was opened against. Sleeps.
+    pub fn send(&mut self, io: &Io<'_>, data: &[u8], timeout: Delta) -> Result {
+        self.inner.check(io)?;
+
+        let i = self.cursor;
+        if data.len() > self.slots[i].capacity {
+            return Err(EMSGSIZE);
+        }
+
+        // Free the slot if its previous transfer is still outstanding.
+        self.reap(i, timeout)?;
+
+        self.slots[i].prepare_transfer(data)?;
+
+        self.slots[i].submit()?;
+        self.cursor = (i + 1) % self.slots.len();
+
+        Ok(())
+    }
+
+    /// Waits up to `timeout` for every outstanding transfer to complete, returning the first error
+    /// encountered. Every slot is reaped regardless.
+    ///
+    /// `io` must come from the same [`IoWindow`] this queue was opened against. Sleeps.
+    pub fn flush(&mut self, io: &Io<'_>, timeout: Delta) -> Result {
+        self.inner.check(io)?;
+
+        let mut first_err = Ok(());
+        for i in 0..self.slots.len() {
+            if let Err(e) = self.reap(i, timeout) {
+                if first_err.is_ok() {
+                    first_err = Err(e);
+                }
+            }
+        }
+        first_err
+    }
+}
+
+/// Wake the process-context owner of a completed queue URB.
+fn urb_signal_complete(result: UrbResult<'_, Completion>) {
+    if let Some(done) = result.context() {
+        done.complete();
+    }
+}
+
 // SAFETY: `usb::Interface` is a transparent wrapper of `struct usb_interface`.
 // The offset is guaranteed to point to a valid device field inside `usb::Interface`.
 unsafe impl<Ctx: device::DeviceContext> device::AsBusDevice<Ctx> for Interface<Ctx> {
@@ -1512,6 +2007,14 @@ fn status(&self) -> i32 {
         self.inner().status
     }
 
+    fn canceller(&self) -> UrbCanceller {
+        // SAFETY: `self` is a live URB. Take an additional USB-core
+        // reference for the cancellation capability.
+        let urb = unsafe { bindings::usb_get_urb(self.as_raw()) };
+        // `usb_get_urb()` returns its non-null argument.
+        UrbCanceller(unsafe { NonNull::new_unchecked(urb) })
+    }
+
     /// Returns a borrow of the driver-private context data, if any.
     pub fn context(&self) -> Option<ArcBorrow<'_, T>> {
         let context = self.inner().context;
@@ -1533,14 +2036,45 @@ pub fn context(&self) -> Option<ArcBorrow<'_, T>> {
 pub struct UrbHandle<T, S: UrbState = Idle> {
     /// Pointer to the underlying C `struct urb`.
     urb: NonNull<bindings::urb>,
+    /// Size of the allocation backing `transfer_buffer`.
+    transfer_buffer_capacity: usize,
     /// State marker.
     _state: PhantomData<S>,
     /// Type of driver-private context data.
     _ty: PhantomData<T>,
 }
 
-// SAFETY: The underlying urb is always reference-counted and can be released from any thread.
-unsafe impl<T> Send for UrbHandle<T, Active> {}
+// SAFETY: The underlying URB is reference-counted and may be released from
+// any thread. The context follows the same `Send + Sync` requirements as
+// `Arc<T>`.
+unsafe impl<T: Send + Sync, S: UrbState> Send for UrbHandle<T, S> {}
+
+/// A reference-counted capability which can only cancel an URB.
+///
+/// Queue registries keep this narrow handle so they can stop transfers during
+/// disconnect without gaining access to the URB or its transfer buffer.
+struct UrbCanceller(NonNull<bindings::urb>);
+
+// SAFETY: USB core reference-counts URBs and permits `usb_kill_urb()` from any
+// process context.
+unsafe impl Send for UrbCanceller {}
+// SAFETY: `cancel()` does not mutate Rust-owned state and USB core serializes
+// cancellation of an URB.
+unsafe impl Sync for UrbCanceller {}
+
+impl UrbCanceller {
+    fn cancel(&self) {
+        // SAFETY: This capability owns a reference to a live URB.
+        unsafe { bindings::usb_kill_urb(self.0.as_ptr()) };
+    }
+}
+
+impl Drop for UrbCanceller {
+    fn drop(&mut self) {
+        // SAFETY: Release the reference acquired by `Urb::canceller()`.
+        unsafe { bindings::usb_free_urb(self.0.as_ptr()) };
+    }
+}
 
 impl<T, S: UrbState> Deref for UrbHandle<T, S> {
     type Target = Urb<T>;
@@ -1551,6 +2085,76 @@ fn deref(&self) -> &Self::Target {
     }
 }
 
+impl<T> UrbHandle<T, Idle> {
+    /// Returns the entire transfer-buffer allocation for an idle URB.
+    ///
+    /// The idle state proves that USB core cannot access the buffer while the
+    /// shared slice exists.
+    pub fn transfer_buffer(&self) -> &[u8] {
+        if self.transfer_buffer_capacity == 0 {
+            return &[];
+        }
+        // SAFETY: The URB is idle, its transfer buffer was allocated for
+        // `transfer_buffer_capacity` bytes in `init_common()`.
+        unsafe {
+            slice::from_raw_parts(
+                (*self.urb.as_ptr()).transfer_buffer.cast(),
+                self.transfer_buffer_capacity,
+            )
+        }
+    }
+
+    /// Returns the entire transfer-buffer allocation for an idle URB.
+    ///
+    /// The idle state proves that USB core cannot access the buffer while the
+    /// mutable slice exists.
+    pub fn transfer_buffer_mut(&mut self) -> &mut [u8] {
+        if self.transfer_buffer_capacity == 0 {
+            return &mut [];
+        }
+        // SAFETY: The URB is idle, its transfer buffer was allocated for
+        // `transfer_buffer_capacity` bytes in `init_common()`, and `&mut self`
+        // grants exclusive access for the returned borrow.
+        unsafe {
+            slice::from_raw_parts_mut(
+                (*self.urb.as_ptr()).transfer_buffer.cast(),
+                self.transfer_buffer_capacity,
+            )
+        }
+    }
+
+    /// Sets the number of transfer-buffer bytes used by the next submission.
+    pub fn set_transfer_buffer_length(&mut self, len: usize) -> Result {
+        if len > self.transfer_buffer_capacity {
+            return Err(EMSGSIZE);
+        }
+        let len = len.try_into()?;
+        // SAFETY: The URB is idle and `len` is within its backing allocation.
+        unsafe { (*self.urb.as_ptr()).transfer_buffer_length = len };
+        Ok(())
+    }
+}
+
+impl<T> UrbHandle<T, Active> {
+    /// Cancel any outstanding transfer and recover an idle, reusable handle.
+    pub fn into_idle(self) -> Pin<UrbHandle<T, Idle>> {
+        let this = core::mem::ManuallyDrop::new(self);
+        // SAFETY: The active handle owns a live URB. `usb_kill_urb()` waits
+        // until its completion callback has returned.
+        unsafe { bindings::usb_kill_urb(this.urb.as_ptr()) };
+
+        let handle = UrbHandle {
+            urb: this.urb,
+            transfer_buffer_capacity: this.transfer_buffer_capacity,
+            _state: PhantomData,
+            _ty: PhantomData,
+        };
+        // SAFETY: The C URB allocation is stable independently of the Rust
+        // handle's address.
+        unsafe { Pin::new_unchecked(handle) }
+    }
+}
+
 impl<T, S: UrbState> Drop for UrbHandle<T, S> {
     fn drop(&mut self) {
         // SAFETY: `self.as_raw()` points to a valid, initialized C `struct urb`.
@@ -1579,7 +2183,7 @@ fn drop(&mut self) {
             unsafe {
                 drop(KBox::from_raw(ptr::slice_from_raw_parts_mut(
                     urb.transfer_buffer.cast::<u8>(),
-                    urb.transfer_buffer_length as usize,
+                    self.transfer_buffer_capacity,
                 )));
             }
         }
@@ -1767,6 +2371,8 @@ fn init_common(
         transfer_flags: TransferFlags,
         interval: i32,
     ) -> Result<Pin<UrbHandle<T, Idle>>> {
+        let transfer_buffer_capacity = transfer_buffer.as_ref().map_or(0, |buffer| buffer.len());
+
         // SAFETY: `usb_alloc_urb` allocates a `struct urb` + ISO frame.
         let urb_ptr =
             unsafe { bindings::usb_alloc_urb(number_of_packets as c_int, mem_flags.as_raw()) };
@@ -1819,6 +2425,7 @@ fn init_common(
         let urb_handle = UrbHandle {
             // SAFETY: `urb_ptr` is guaranteed non-null by the null check above.
             urb: unsafe { NonNull::new_unchecked(urb_ptr) },
+            transfer_buffer_capacity,
             _state: PhantomData,
             _ty: PhantomData,
         };
@@ -1836,6 +2443,18 @@ pub fn submit(
         self: Pin<UrbHandle<T, Idle>>,
         mem_flags: kernel::alloc::Flags,
     ) -> Result<UrbHandle<T, Active>> {
+        self.submit_recoverable(mem_flags)
+            .map_err(|(error, _handle)| error)
+    }
+
+    /// Submit the URB while returning the idle handle when submission fails.
+    ///
+    /// Queue implementations use this variant so a transient submission
+    /// error does not discard a preallocated URB and its transfer buffer.
+    pub fn submit_recoverable(
+        self: Pin<UrbHandle<T, Idle>>,
+        mem_flags: kernel::alloc::Flags,
+    ) -> core::result::Result<UrbHandle<T, Active>, (Error, Pin<UrbHandle<T, Idle>>)> {
         // SAFETY: The urb pointed to is not moved.
         let handle = unsafe { Pin::into_inner_unchecked(self) };
         // SAFETY: `handle.as_raw()` points to a valid, initialized `struct urb`.
@@ -1843,14 +2462,19 @@ pub fn submit(
 
         if result == 0 {
             let urb = handle.urb;
+            let transfer_buffer_capacity = handle.transfer_buffer_capacity;
             core::mem::forget(handle);
             Ok(UrbHandle {
                 urb,
+                transfer_buffer_capacity,
                 _state: PhantomData,
                 _ty: PhantomData,
             })
         } else {
-            Err(Error::from_errno(result))
+            // SAFETY: Submission failed, so USB core did not take ownership
+            // and the handle remains idle and reusable.
+            let handle = unsafe { Pin::new_unchecked(handle) };
+            Err((Error::from_errno(result), handle))
         }
     }
 

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 3/5] rust: usb: expose device descriptor fields and queue readiness
  2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 1/5] rust: usb: add revocable typed interface I/O Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 2/5] rust: usb: add reusable URBs and persistent bulk queues Mike Lothian
@ 2026-08-26 16:30 ` Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 4/5] rust: usb: add a vendor-and-interface-info device id constructor Mike Lothian
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Mike Lothian @ 2026-08-26 16:30 UTC (permalink / raw)
  To: linux-usb
  Cc: Mike Lothian, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
	Alexandre Courbot, Onur Özkan, Greg Kroah-Hartman,
	Colin Braun, rust-for-linux, linux-kernel

A driver that identifies hardware before it decides to drive it needs the
device descriptor: idVendor, idProduct, bcdDevice, bcdUSB, the enumerated
speed, and the cached iManufacturer, iProduct and iSerialNumber strings.
bcdDevice in particular is the only revision a driver can read without
already speaking the device's own protocol.

Add can_send_n() alongside them, which reports whether the next count
queue slots can be submitted without waiting and reaps completed slots on
the way. A protocol that must not block halfway through a multi-URB record
uses it to defer the whole record and service its control plane first.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
 rust/kernel/usb.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 96 insertions(+)

diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index ad40c814616a..782bae53584d 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -1430,6 +1430,7 @@ fn is_active(&self) -> bool {
         matches!(self.urb, Some(QueueUrb::Active(_)))
     }
 
+    #[inline]
     fn wait(&self, timeout: Delta) -> bool {
         let millis = timeout.as_millis();
         let millis = if millis <= 0 {
@@ -1441,6 +1442,7 @@ fn wait(&self, timeout: Delta) -> bool {
             .wait_for_completion_timeout(crate::time::msecs_to_jiffies(millis))
     }
 
+    #[inline]
     fn finish(&mut self) -> Result<(i32, usize)> {
         let state = self.urb.take().ok_or(EIO)?;
         let active = match state {
@@ -1700,6 +1702,34 @@ fn reap(&mut self, i: usize, timeout: Delta) -> Result<bool> {
         Ok(true)
     }
 
+    /// Reports whether the next `count` queue slots can be submitted without waiting.
+    ///
+    /// Completed slots are reaped and any transfer error is returned. This is useful when a
+    /// higher-level protocol must not block halfway through a multi-URB record while waiting for
+    /// endpoint progress; callers can defer the whole record and service its control plane first.
+    #[inline]
+    pub fn can_send_n(&mut self, io: &Io<'_>, count: usize) -> Result<bool> {
+        self.inner.check(io)?;
+        if count > self.slots.len() {
+            return Ok(false);
+        }
+        for off in 0..count {
+            let i = (self.cursor + off) % self.slots.len();
+            if self.slots[i].is_active() {
+                if !self.slots[i].wait(Delta::ZERO) {
+                    return Ok(false);
+                }
+                // `wait_for_completion_timeout()` consumes the completion signal. Reap the URB
+                // now rather than leaving `send()` to wait for the signal a second time.
+                let (status, _) = self.slots[i].finish()?;
+                if status != 0 {
+                    return Err(Error::from_errno(status));
+                }
+            }
+        }
+        Ok(true)
+    }
+
     /// Submits `data` as a bulk OUT transfer without waiting for it to complete.
     ///
     /// If the slot about to be reused still has a transfer outstanding, this blocks up to
@@ -2626,6 +2656,72 @@ fn inner(&self) -> &bindings::usb_device {
     fn devnum(&self) -> u32 {
         self.inner().devnum as u32
     }
+
+    /// Returns the `idVendor` of the device descriptor.
+    pub fn vendor_id(&self) -> u16 {
+        self.inner().descriptor.idVendor
+    }
+
+    /// Returns the `idProduct` of the device descriptor.
+    pub fn product_id(&self) -> u16 {
+        self.inner().descriptor.idProduct
+    }
+
+    /// Returns the `bcdDevice` of the device descriptor.
+    ///
+    /// Vendors conventionally use this as the device revision, and it is the only version a driver
+    /// can read without speaking the device's own protocol.
+    pub fn bcd_device(&self) -> u16 {
+        self.inner().descriptor.bcdDevice
+    }
+
+    /// Returns the `bcdUSB` of the device descriptor.
+    pub fn bcd_usb(&self) -> u16 {
+        self.inner().descriptor.bcdUSB
+    }
+
+    /// Returns the enumerated bus speed as a human-readable string.
+    pub fn speed_str(&self) -> &'static str {
+        match self.inner().speed {
+            bindings::usb_device_speed_USB_SPEED_LOW => "low (1.5 Mbps)",
+            bindings::usb_device_speed_USB_SPEED_FULL => "full (12 Mbps)",
+            bindings::usb_device_speed_USB_SPEED_HIGH => "high (480 Mbps)",
+            bindings::usb_device_speed_USB_SPEED_WIRELESS => "wireless",
+            bindings::usb_device_speed_USB_SPEED_SUPER => "super (5 Gbps)",
+            bindings::usb_device_speed_USB_SPEED_SUPER_PLUS => "super-plus (10+ Gbps)",
+            _ => "unknown",
+        }
+    }
+
+    /// Returns the device's `iManufacturer` string, if the core cached one.
+    pub fn manufacturer(&self) -> Option<&CStr> {
+        // SAFETY: `manufacturer` is either null or a NUL-terminated string owned by the USB core
+        // for as long as the device exists, which outlives the borrow of `self`.
+        unsafe { Self::opt_cstr(self.inner().manufacturer) }
+    }
+
+    /// Returns the device's `iProduct` string, if the core cached one.
+    pub fn product(&self) -> Option<&CStr> {
+        // SAFETY: As for `manufacturer`.
+        unsafe { Self::opt_cstr(self.inner().product) }
+    }
+
+    /// Returns the device's `iSerialNumber` string, if the core cached one.
+    pub fn serial(&self) -> Option<&CStr> {
+        // SAFETY: As for `manufacturer`.
+        unsafe { Self::opt_cstr(self.inner().serial) }
+    }
+
+    /// # Safety
+    ///
+    /// `p` must be null or point to a NUL-terminated string that outlives `'a`.
+    unsafe fn opt_cstr<'a>(p: *mut crate::ffi::c_char) -> Option<&'a CStr> {
+        if p.is_null() {
+            return None;
+        }
+        // SAFETY: The caller guarantees `p` is a NUL-terminated string valid for `'a`.
+        Some(unsafe { CStr::from_char_ptr(p) })
+    }
 }
 
 impl Device<device::Bound> {

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 4/5] rust: usb: add a vendor-and-interface-info device id constructor
  2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
                   ` (2 preceding siblings ...)
  2026-08-26 16:30 ` [PATCH v3 3/5] rust: usb: expose device descriptor fields and queue readiness Mike Lothian
@ 2026-08-26 16:30 ` Mike Lothian
  2026-08-26 16:30 ` [PATCH v3 5/5] rust: usb: let a driver keep its interface usable while unbinding Mike Lothian
  2026-08-26 18:25 ` [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Danilo Krummrich
  5 siblings, 0 replies; 8+ messages in thread
From: Mike Lothian @ 2026-08-26 16:30 UTC (permalink / raw)
  To: linux-usb
  Cc: Mike Lothian, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
	Alexandre Courbot, Onur Özkan, Greg Kroah-Hartman,
	Colin Braun, rust-for-linux, linux-kernel

`USB_VENDOR_AND_INTERFACE_INFO` is the C macro a driver uses to bind to
a function rather than to a list of product IDs: one vendor, plus an
interface class, subclass and protocol, matching every product that
exposes it.

The existing constructors cover `USB_DEVICE_INFO` and
`USB_INTERFACE_INFO`, which between them can express "any device of this
class" or "any interface of this class from any vendor", but not the
combination -- and the combination is what a vendor-specific function
needs, because class `0xff` means nothing without the vendor beside it.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
 rust/kernel/usb.rs | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 782bae53584d..b2a0fb104ddf 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -307,6 +307,29 @@ pub const fn from_interface_info(class: u8, subclass: u8, protocol: u8) -> Self
         })
     }
 
+    /// Equivalent to C's `USB_VENDOR_AND_INTERFACE_INFO` macro.
+    ///
+    /// Matches every device from one vendor that exposes an interface of the given class,
+    /// subclass and protocol, whatever its product ID. This is how a driver binds to a *function*
+    /// rather than to a list of the products someone happened to test.
+    pub const fn from_vendor_and_interface_info(
+        vendor: u16,
+        class: u8,
+        subclass: u8,
+        protocol: u8,
+    ) -> Self {
+        Self(bindings::usb_device_id {
+            match_flags: (bindings::USB_DEVICE_ID_MATCH_VENDOR
+                | bindings::USB_DEVICE_ID_MATCH_INT_INFO) as u16,
+            idVendor: vendor,
+            bInterfaceClass: class,
+            bInterfaceSubClass: subclass,
+            bInterfaceProtocol: protocol,
+            // SAFETY: It is safe to use all zeroes for the other fields of `usb_device_id`.
+            ..unsafe { MaybeUninit::zeroed().assume_init() }
+        })
+    }
+
     /// Equivalent to C's `USB_DEVICE_INTERFACE_CLASS` macro.
     pub const fn from_device_interface_class(vendor: u16, product: u16, class: u8) -> Self {
         Self(bindings::usb_device_id {

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v3 5/5] rust: usb: let a driver keep its interface usable while unbinding
  2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
                   ` (3 preceding siblings ...)
  2026-08-26 16:30 ` [PATCH v3 4/5] rust: usb: add a vendor-and-interface-info device id constructor Mike Lothian
@ 2026-08-26 16:30 ` Mike Lothian
  2026-08-26 18:25 ` [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Danilo Krummrich
  5 siblings, 0 replies; 8+ messages in thread
From: Mike Lothian @ 2026-08-26 16:30 UTC (permalink / raw)
  To: linux-usb
  Cc: Mike Lothian, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, Daniel Almeida, Tamir Duberstein,
	Alexandre Courbot, Onur Özkan, Greg Kroah-Hartman,
	Colin Braun, rust-for-linux, linux-kernel

The USB core kills every outstanding URB and disables an interface's
endpoints before it calls any driver callback. A driver with something
left to say to the device on the way out therefore cannot say it: the
transfer names an endpoint that no longer exists and is refused.

Expose the core's soft-unbind flag as a driver constant. Setting it
defers that teardown until the callbacks return, which makes cancelling
outstanding transfers the driver's own responsibility.

This matters to a driver that leaves the device in a state a user can
see. A display bridge that simply stops sending pixels leaves the
monitor lit on whatever it decoded last.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
---
 rust/kernel/usb.rs | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index b2a0fb104ddf..1d8f5bc8a545 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -107,6 +107,7 @@ unsafe fn register(
             (*udrv.get()).pre_reset = Some(Self::pre_reset_callback);
             (*udrv.get()).post_reset = Some(Self::post_reset_callback);
             (*udrv.get()).id_table = T::ID_TABLE.as_ptr();
+            (*udrv.get()).set_soft_unbind(T::SOFT_UNBIND as core::ffi::c_uint);
         }
 
         // SAFETY: `udrv` is guaranteed to be a valid `DriverType`.
@@ -474,6 +475,19 @@ pub trait Driver {
     /// The table of device ids supported by the driver.
     const ID_TABLE: IdTable<Self::IdInfo>;
 
+    /// Whether the USB core must leave this interface usable until the driver has let go of it.
+    ///
+    /// By default the core kills every outstanding URB and disables the interface's endpoints
+    /// *before* it calls any driver callback, so a driver that has something to say to the device
+    /// on the way out cannot say it: the transfer is refused with [`ENOENT`] because the endpoint
+    /// it names no longer exists. Setting this defers that teardown until after the callbacks
+    /// return, which makes cancelling outstanding transfers the driver's own responsibility.
+    ///
+    /// Only useful to a driver that leaves the device in a state a user can see -- a display that
+    /// otherwise goes on scanning out its last frame, an interface that must be told to power
+    /// down.
+    const SOFT_UNBIND: bool = false;
+
     /// USB driver probe.
     ///
     /// Called when a new USB interface is bound to this driver.

^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver
  2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
                   ` (4 preceding siblings ...)
  2026-08-26 16:30 ` [PATCH v3 5/5] rust: usb: let a driver keep its interface usable while unbinding Mike Lothian
@ 2026-08-26 18:25 ` Danilo Krummrich
  5 siblings, 0 replies; 8+ messages in thread
From: Danilo Krummrich @ 2026-08-26 18:25 UTC (permalink / raw)
  To: Mike Lothian
  Cc: linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
	Onur Özkan, Nathan Chancellor, Nick Desaulniers,
	Bill Wendling, Justin Stitt, rust-for-linux, llvm, gregkh,
	oneukum, stern

(Cc: Greg, Oliver, Alan)

On Wed Aug 26, 2026 at 6:30 PM CEST, Mike Lothian wrote:
>   v2 10/11, "keep usb::Device private and gate ...", is dropped entirely.
>     Oliver Neukum was right that it was conceptually wrong: USB does device
>     level operations, and hiding that behind an interface is a layering
>     violation. Device stays public

On an abstract level a USB interface is a device that is operated by some
driver, which is why from a driver core topology point of view, a struct
usb_interface *is* a struct device. And there is no layering violation in
shortening:

	intf.device().bulk_send();

to

	intf.bulk_send();

Now, I get that this might read a bit odd from a pure USB topology perspective,
but what this shortcut gives you is that it allows you to not create types with
incorrect type states intermediately.

If USB folks really want the API to have an indirection, so it matches the USB
topology reading a bit better, just create an abstract a new type, e.g.:

	struct IoDevice<'a> {
	    intf: &'a usb::Interface<Bound>,
	}

which only provides the corresponding I/O methods, but does not give you access
to a "real" usb::Device<Bound>. This way you can still write:

	intf.device().bulk_send();

if that's preferrable.

Interestingly, looking at your series, it already does something like this, but
it's called IoWindow and oddly reimplements the lifecycle constraints the Rust
driver core infrastructure already provides.

> Alan Stern's lifecycle point is what makes device access from an interface
> sound, and is worth restating because the whole shape depends on it: an
> unconfigured device has no interfaces, so an interface that exists implies a
> configured device

Well, Alan also said this:

"At first I thought that we ought to have such a guarantee.  But in fact we
don't, because the user can at any time write to a USB device's
bConfigurationValue sysfs attribute even if the device isn't bound to a driver.
This can create interfaces which may then be bound to drivers."

This was a reply to me asking:

"So, what you're saying is that, in the generic case, there is a guarantee that
if a usb_interface is bound to a usb_driver, then the usb_interface's parent
usb_device is also bound to a usb_device_driver."

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v3 1/5] rust: usb: add revocable typed interface I/O
  2026-08-26 16:30 ` [PATCH v3 1/5] rust: usb: add revocable typed interface I/O Mike Lothian
@ 2026-08-26 18:59   ` Danilo Krummrich
  0 siblings, 0 replies; 8+ messages in thread
From: Danilo Krummrich @ 2026-08-26 18:59 UTC (permalink / raw)
  To: Mike Lothian
  Cc: linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
	Onur Özkan, Greg Kroah-Hartman, Colin Braun, rust-for-linux,
	linux-kernel

On Wed Aug 26, 2026 at 6:30 PM CEST, Mike Lothian wrote:
> +    /// Asks the driver core to unbind whatever driver is currently bound to this interface.

What is this needed for? Where do you use it?

> +    /// This is the narrow, reviewed replacement for handing out a raw `struct device` pointer: it
> +    /// performs exactly one operation (`device_release_driver()`) on this interface's own device,
> +    /// and cannot be used to reach the device-wide state of a composite peer.
> +    ///
> +    /// It is intended for a driver-provided "release my devices" control (e.g. a sysfs attribute),
> +    /// and must not be called from the driver's own `probe()` or `disconnect()` callback: the
> +    /// driver core already holds the device lock across those.

IOW, it must not be available for Interface<Core>, which due to the deref chain
is not that trivial to model. So, if this is really needed I think this needs a
an abstraction where you get a different device newtype from the scope where
this *should* be called from that allows you to do this and can never leave the
scope.

> +    pub fn release_driver(&self) {
> +        // SAFETY: `self.as_raw()` is a valid `struct usb_interface` by the type invariant, so the
> +        // address of its embedded `dev` is a valid `struct device`. `device_release_driver()`
> +        // takes the device lock itself and tolerates a device with no driver bound.
> +        unsafe { bindings::device_release_driver(&raw mut (*self.as_raw()).dev) };
> +    }

[...]

> +/// A revocable window during which USB I/O is permitted on an interface.
> +///
> +/// A driver-`Bound` interface is *not* on its own proof that a transfer may be issued: the USB
> +/// core forbids I/O outside the window that opens after a successful `probe()`/resume/reset-resume
> +/// and must be closed again before `disconnect()`, `suspend()` or `pre_reset()` returns. This type
> +/// represents exactly that narrower state.

How is this different or narrower than the device's Bound type state represents?
Also, this seems to reinvent Devres, which we superseded with Rust native
lifetimes and higher-ranked types. Please use that instead.

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-08-26 18:59 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 16:30 [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Mike Lothian
2026-08-26 16:30 ` [PATCH v3 1/5] rust: usb: add revocable typed interface I/O Mike Lothian
2026-08-26 18:59   ` Danilo Krummrich
2026-08-26 16:30 ` [PATCH v3 2/5] rust: usb: add reusable URBs and persistent bulk queues Mike Lothian
2026-08-26 16:30 ` [PATCH v3 3/5] rust: usb: expose device descriptor fields and queue readiness Mike Lothian
2026-08-26 16:30 ` [PATCH v3 4/5] rust: usb: add a vendor-and-interface-info device id constructor Mike Lothian
2026-08-26 16:30 ` [PATCH v3 5/5] rust: usb: let a driver keep its interface usable while unbinding Mike Lothian
2026-08-26 18:25 ` [PATCH v3 0/5] rust: usb: host-side abstractions for a bulk-endpoint driver Danilo Krummrich

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox