* [RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 16:07 ` Danilo Krummrich
2026-06-17 14:59 ` [RFC PATCH 2/9] rust: usb: add synchronous control " Mike Lothian
` (7 subsequent siblings)
8 siblings, 1 reply; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
The USB abstractions currently let a Rust driver bind a device
(probe/disconnect) but provide no way to move data, so any real driver
still has to drop to C. Add synchronous bulk IN/OUT transfers, the most
common need, as safe methods on `usb::Device`.
`Device` is made public and gains `bulk_send()` and `bulk_recv()`,
wrappers over `usb_bulk_msg()`. The endpoint pipe is built with the
`usb_sndbulkpipe()` / `usb_rcvbulkpipe()` macros, exposed to Rust via new
`rust_helper_*` shims (they are function-like macros that bindgen cannot
bind directly). Both methods take the endpoint's `bEndpointAddress` as it
appears in the descriptor (e.g. 0x84) -- matching the later `clear_halt()`
-- and use its low four bits as the endpoint number; the direction is
fixed by the method. The transfer length and timeout are range-checked
into the C `int` types, and the timeout is a `Delta` (a zero `Delta`, or
any sub-millisecond value once truncated to whole ms, waits indefinitely,
matching `usb_bulk_msg()`).
`usb_bulk_msg()` DMAs directly to/from the transfer buffer, which must
therefore be kmalloc'd (DMA-capable) memory -- not the stack, `.rodata`
or vmalloc. Rather than push that onto callers as an unenforced
precondition (which would make these safe methods unsound for an
arbitrary `&[u8]`), the payload is copied through a kmalloc'd bounce
buffer internally, so any slice is accepted. A future zero-copy fast path
could take a dedicated DMA-buffer type once one exists; feedback on that
API shape is a main reason this is posted as an RFC.
Both methods block and sleep, so they must be called from process context
only.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/helpers/usb.c | 12 ++++++
rust/kernel/usb.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 108 insertions(+), 1 deletion(-)
diff --git a/rust/helpers/usb.c b/rust/helpers/usb.c
index eff1cf7be3c2..d398eb2f6669 100644
--- a/rust/helpers/usb.c
+++ b/rust/helpers/usb.c
@@ -7,3 +7,15 @@ 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);
+}
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 7aff0c82d0af..c9acadb2deaf 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -19,6 +19,7 @@
},
prelude::*,
sync::aref::AlwaysRefCounted,
+ time::Delta,
types::Opaque,
ThisModule, //
};
@@ -426,7 +427,7 @@ unsafe impl Sync for Interface {}
///
/// [`struct usb_device`]: https://www.kernel.org/doc/html/latest/driver-api/usb/usb.html#c.usb_device
#[repr(transparent)]
-struct Device<Ctx: device::DeviceContext = device::Normal>(
+pub struct Device<Ctx: device::DeviceContext = device::Normal>(
Opaque<bindings::usb_device>,
PhantomData<Ctx>,
);
@@ -435,6 +436,100 @@ impl<Ctx: device::DeviceContext> Device<Ctx> {
fn as_raw(&self) -> *mut bindings::usb_device {
self.0.get()
}
+
+ /// Issues a synchronous bulk OUT transfer of `data` to bulk endpoint
+ /// `endpoint` and returns the number of bytes actually transferred.
+ ///
+ /// This is a blocking, sleeping call and therefore must only be invoked from
+ /// a process (sleepable) context, never from atomic or interrupt context.
+ ///
+ /// `endpoint` is the endpoint's `bEndpointAddress` as it appears in the
+ /// descriptor (e.g. `0x02`); the transfer direction is fixed by the method
+ /// (OUT), so only the low four bits — the endpoint number — are used.
+ /// `timeout` is the maximum time to wait, rounded down to whole
+ /// milliseconds; a [`Delta`] of zero — or any non-zero value below 1 ms —
+ /// waits indefinitely (the `usb_bulk_msg()` `timeout == 0` contract).
+ ///
+ /// `usb_bulk_msg()` DMAs directly from the transfer buffer, so the buffer
+ /// must reside in DMA-capable (kmalloc'd) memory. `data` may be any slice —
+ /// it is copied into a kmalloc'd bounce buffer internally — so callers need
+ /// not arrange DMA-capable storage themselves.
+ ///
+ /// [`usb_bulk_msg()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_bulk_msg
+ pub fn bulk_send(&self, endpoint: u8, data: &[u8], timeout: Delta) -> Result<usize> {
+ let mut actual: kernel::ffi::c_int = 0;
+
+ // `usb_bulk_msg()` requires a DMA-capable buffer; `data` may live on the
+ // stack or in `.rodata`, so copy it into a kmalloc'd bounce buffer.
+ let mut buf = KVec::with_capacity(data.len(), GFP_KERNEL)?;
+ buf.extend_from_slice(data, GFP_KERNEL)?;
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ let pipe = unsafe { bindings::usb_sndbulkpipe(self.as_raw(), endpoint.into()) };
+
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `buf` is a kmalloc'd
+ // buffer valid for reads of `buf.len()` bytes; `actual` is a valid out-pointer.
+ to_result(unsafe {
+ bindings::usb_bulk_msg(
+ self.as_raw(),
+ pipe,
+ buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+ buf.len().try_into()?,
+ &mut actual,
+ timeout.as_millis().try_into()?,
+ )
+ })?;
+
+ Ok(actual as usize)
+ }
+
+ /// Issues a synchronous bulk IN transfer of up to `data.len()` bytes from bulk
+ /// endpoint `endpoint` into `data` and returns the number of bytes received.
+ ///
+ /// This is a blocking, sleeping call and therefore must only be invoked from a
+ /// process (sleepable) context, never from atomic or interrupt context.
+ ///
+ /// `endpoint` is the endpoint's `bEndpointAddress` as it appears in the
+ /// descriptor (e.g. `0x84`); the transfer direction is fixed by the method
+ /// (IN), so only the low four bits — the endpoint number — are used.
+ /// `timeout` is the maximum time to wait, rounded down to whole
+ /// milliseconds; a [`Delta`] of zero — or any non-zero value below 1 ms —
+ /// waits indefinitely (the `usb_bulk_msg()` `timeout == 0` contract).
+ ///
+ /// `usb_bulk_msg()` DMAs directly into the transfer buffer, so the buffer
+ /// must reside in DMA-capable (kmalloc'd) memory. The data is received into
+ /// a kmalloc'd bounce buffer internally and then copied into `data`, so
+ /// `data` may be any slice.
+ ///
+ /// [`usb_bulk_msg()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_bulk_msg
+ pub fn bulk_recv(&self, endpoint: u8, data: &mut [u8], timeout: Delta) -> Result<usize> {
+ let mut actual: kernel::ffi::c_int = 0;
+
+ // `usb_bulk_msg()` requires a DMA-capable buffer; receive into a kmalloc'd
+ // bounce buffer and copy out, so `data` need not be DMA-capable itself.
+ let mut buf = KVec::from_elem(0u8, data.len(), GFP_KERNEL)?;
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ let pipe = unsafe { bindings::usb_rcvbulkpipe(self.as_raw(), endpoint.into()) };
+
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `buf` is a kmalloc'd
+ // buffer valid for writes of `buf.len()` bytes; `actual` is a valid out-pointer.
+ to_result(unsafe {
+ bindings::usb_bulk_msg(
+ self.as_raw(),
+ pipe,
+ buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+ buf.len().try_into()?,
+ &mut actual,
+ timeout.as_millis().try_into()?,
+ )
+ })?;
+
+ // `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)
+ }
}
// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support
2026-06-17 14:59 ` [RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support Mike Lothian
@ 2026-06-17 16:07 ` Danilo Krummrich
0 siblings, 0 replies; 11+ messages in thread
From: Danilo Krummrich @ 2026-06-17 16:07 UTC (permalink / raw)
To: Mike Lothian
Cc: rust-for-linux, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Daniel Almeida, Greg Kroah-Hartman,
Alexandre Courbot, Shankari Anand, linux-kernel
On Wed Jun 17, 2026 at 4:59 PM CEST, Mike Lothian wrote:
> @@ -426,7 +427,7 @@ unsafe impl Sync for Interface {}
> ///
> /// [`struct usb_device`]: https://www.kernel.org/doc/html/latest/driver-api/usb/usb.html#c.usb_device
> #[repr(transparent)]
> -struct Device<Ctx: device::DeviceContext = device::Normal>(
> +pub struct Device<Ctx: device::DeviceContext = device::Normal>(
Please see commit 22d693e45d4a ("rust: usb: keep usb::Device private for now"),
this shouldn't be needed for writing a usb::Interface driver; please also see
[1].
[1] https://lore.kernel.org/all/DD08HWM0M68R.2R74OSODBIWSZ@kernel.org/
> + pub fn bulk_send(&self, endpoint: u8, data: &[u8], timeout: Delta) -> Result<usize> {
This should only be exposed for a usb::Interface<Bound>, i.e. note the Bound
device context.
^ permalink raw reply [flat|nested] 11+ messages in thread
* [RFC PATCH 2/9] rust: usb: add synchronous control transfer support
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 3/9] rust: usb: add usb::Device::set_interface() Mike Lothian
` (6 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
Building on the bulk transfer support, add synchronous control IN/OUT
transfers on the default control endpoint (pipe 0) as safe methods on
`usb::Device`.
`control_send()` and `control_recv()` wrap `usb_control_msg_send()` and
`usb_control_msg_recv()`. The `bRequest`, `bmRequestType`, `wValue` and
`wIndex` setup fields are taken as arguments; `wLength` is the buffer
length. Both copy the buffer internally, so unlike the bulk path the
caller's buffer need not be DMA-capable. They block and sleep, so must be
called from process context; the timeout is a `Delta` (zero waits
indefinitely).
This is what a driver needs for the standard control-request preamble at
device bring-up before bulk traffic begins.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/usb.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index c9acadb2deaf..e1fb50fd6997 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -530,6 +530,81 @@ pub fn bulk_recv(&self, endpoint: u8, data: &mut [u8], timeout: Delta) -> Result
data[..n].copy_from_slice(&buf[..n]);
Ok(n)
}
+
+ /// Issues a synchronous control OUT transfer on the default control endpoint.
+ ///
+ /// Wraps [`usb_control_msg_send()`]; `request`, `request_type`, `value` and
+ /// `index` are the `bRequest`, `bmRequestType`, `wValue` and `wIndex` setup
+ /// fields. `data` is the payload (`wLength` is its length).
+ ///
+ /// This is a blocking, sleeping call and must only be invoked from process
+ /// context. Unlike the bulk path, the buffer is copied internally, so `data`
+ /// need not reside in DMA-capable memory. `timeout` is the maximum time to
+ /// wait, rounded down to whole milliseconds; a [`Delta`] of zero — or any
+ /// non-zero value below 1 ms — waits indefinitely.
+ ///
+ /// [`usb_control_msg_send()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_control_msg_send
+ pub fn control_send(
+ &self,
+ request: u8,
+ request_type: u8,
+ value: u16,
+ index: u16,
+ data: &[u8],
+ timeout: Delta,
+ ) -> Result {
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `data` is valid for
+ // reads of `data.len()` bytes; `usb_control_msg_send()` copies the buffer.
+ to_result(unsafe {
+ bindings::usb_control_msg_send(
+ self.as_raw(),
+ 0,
+ request,
+ request_type,
+ value,
+ index,
+ data.as_ptr().cast::<kernel::ffi::c_void>(),
+ data.len().try_into()?,
+ timeout.as_millis().try_into()?,
+ bindings::GFP_KERNEL,
+ )
+ })
+ }
+
+ /// Issues a synchronous control IN transfer on the default control endpoint,
+ /// filling `data` with exactly `data.len()` bytes.
+ ///
+ /// Wraps [`usb_control_msg_recv()`], which fails the transfer if the device
+ /// returns fewer than `data.len()` bytes. The setup fields and context/buffer
+ /// rules are as for [`Device::control_send`].
+ ///
+ /// [`usb_control_msg_recv()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_control_msg_recv
+ pub fn control_recv(
+ &self,
+ request: u8,
+ request_type: u8,
+ value: u16,
+ index: u16,
+ data: &mut [u8],
+ timeout: Delta,
+ ) -> Result {
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `data` is valid for
+ // writes of `data.len()` bytes; `usb_control_msg_recv()` copies into the buffer.
+ to_result(unsafe {
+ bindings::usb_control_msg_recv(
+ self.as_raw(),
+ 0,
+ request,
+ request_type,
+ value,
+ index,
+ data.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+ data.len().try_into()?,
+ timeout.as_millis().try_into()?,
+ bindings::GFP_KERNEL,
+ )
+ })
+ }
}
// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 3/9] rust: usb: add usb::Device::set_interface()
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 2/9] rust: usb: add synchronous control " Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 4/9] rust: usb: add usb::Interface::number() Mike Lothian
` (5 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
Add a safe wrapper over `usb_set_interface()` (the standard SET_INTERFACE
request) so a Rust driver can select an interface's alternate setting
during bring-up. It blocks and sleeps, so it must be called from process
context only.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/usb.rs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index e1fb50fd6997..73637e3467f1 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -605,6 +605,19 @@ pub fn control_recv(
)
})
}
+
+ /// Selects alternate setting `alternate` of interface `interface`.
+ ///
+ /// Wraps [`usb_set_interface()`] (the standard `SET_INTERFACE` request). This is
+ /// a blocking, sleeping call and must only be invoked from process context.
+ ///
+ /// [`usb_set_interface()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_set_interface
+ pub fn set_interface(&self, interface: u8, alternate: u8) -> Result {
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ to_result(unsafe {
+ bindings::usb_set_interface(self.as_raw(), interface.into(), alternate.into())
+ })
+ }
}
// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 4/9] rust: usb: add usb::Interface::number()
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
` (2 preceding siblings ...)
2026-06-17 14:59 ` [RFC PATCH 3/9] rust: usb: add usb::Device::set_interface() Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 5/9] rust: usb: add usb::Device::clear_halt() Mike Lothian
` (4 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
Add a safe accessor returning an interface's bInterfaceNumber (from its
current alternate setting). A USB function driver's probe() is called once
per matching interface; a multi-interface device therefore needs to know
which interface it was handed so it can drive its protocol on the right one
and stay idle on the others. This exposes that number to Rust drivers.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/usb.rs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 73637e3467f1..683e8b52d6d1 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -357,6 +357,17 @@ impl<Ctx: device::DeviceContext> Interface<Ctx> {
fn as_raw(&self) -> *mut bindings::usb_interface {
self.0.get()
}
+
+ /// Returns the interface number (`bInterfaceNumber`) of this interface's
+ /// current alternate setting.
+ pub fn number(&self) -> u8 {
+ // SAFETY: `self.as_raw()` is a valid `struct usb_interface` pointer by the
+ // type invariant, and a bound interface always has a `cur_altsetting`.
+ unsafe {
+ let altsetting = (*self.as_raw()).cur_altsetting;
+ (*altsetting).desc.bInterfaceNumber
+ }
+ }
}
// SAFETY: `usb::Interface` is a transparent wrapper of `struct usb_interface`.
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 5/9] rust: usb: add usb::Device::clear_halt()
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
` (3 preceding siblings ...)
2026-06-17 14:59 ` [RFC PATCH 4/9] rust: usb: add usb::Interface::number() Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 6/9] rust: usb: add usb::Device::interrupt_recv() Mike Lothian
` (3 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
Add a safe wrapper over usb_clear_halt() so a Rust driver can recover
a bulk endpoint left in a halt/stall condition (for example a streaming
endpoint that a previous driver left stalled). It clears both the
device-side stall (CLEAR_FEATURE ENDPOINT_HALT) and the host-side data
toggle, and it sleeps, so it must be called from process context.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/usb.rs | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 683e8b52d6d1..a151a120e82e 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -448,6 +448,30 @@ fn as_raw(&self) -> *mut bindings::usb_device {
self.0.get()
}
+ /// Clears a halt/stall on bulk `endpoint`, resetting both the device-side stall
+ /// (CLEAR_FEATURE ENDPOINT_HALT) and the host-side data toggle via
+ /// `usb_clear_halt()`. The transfer direction is taken from bit 7 of the endpoint
+ /// address (`USB_DIR_IN`), so this works for both bulk IN and bulk OUT endpoints.
+ /// Must be called from process context (it sleeps). Needed e.g. when another
+ /// driver left a streaming endpoint stalled.
+ pub fn clear_halt(&self, endpoint: u8) -> Result {
+ // `usb_clear_halt()` must be given a pipe whose direction matches the endpoint:
+ // it issues CLEAR_FEATURE to that endpoint address and resets the matching data
+ // toggle. Build an IN or OUT pipe from the `USB_DIR_IN` bit of the address;
+ // using `usb_sndbulkpipe()` unconditionally would force OUT and silently fail
+ // to clear a stalled IN endpoint.
+ let pipe = if endpoint & (bindings::USB_DIR_IN as u8) != 0 {
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ unsafe { bindings::usb_rcvbulkpipe(self.as_raw(), endpoint.into()) }
+ } else {
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ unsafe { bindings::usb_sndbulkpipe(self.as_raw(), endpoint.into()) }
+ };
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `usb_clear_halt()`
+ // only issues a control request and updates host-side endpoint state.
+ to_result(unsafe { bindings::usb_clear_halt(self.as_raw(), pipe as kernel::ffi::c_int) })
+ }
+
/// Issues a synchronous bulk OUT transfer of `data` to bulk endpoint
/// `endpoint` and returns the number of bytes actually transferred.
///
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 6/9] rust: usb: add usb::Device::interrupt_recv()
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
` (4 preceding siblings ...)
2026-06-17 14:59 ` [RFC PATCH 5/9] rust: usb: add usb::Device::clear_halt() Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 7/9] rust: usb: add usb::Device::reset_configuration() Mike Lothian
` (2 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
Add a safe wrapper for synchronous interrupt IN transfers, mirroring
bulk_recv() but using usb_interrupt_msg() over an interrupt pipe so the
URB's transfer type matches the endpoint. This is the correctly-typed API
for reading a device's interrupt-IN status endpoints; usb_bulk_msg() would
also work (it detects an interrupt endpoint and rewrites the pipe to
PIPE_INTERRUPT), but interrupt_recv() exists precisely so callers do not
have to rely on that legacy fixup.
The interrupt pipe is built with a new rust_helper_usb_rcvintpipe() shim,
matching the existing bulk-pipe helpers (usb_rcvintpipe() is a
function-like macro). It sleeps, so it must be called from process
context, and like bulk_recv() the buffer must be DMA-capable.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/helpers/usb.c | 6 ++++++
rust/kernel/usb.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 50 insertions(+)
diff --git a/rust/helpers/usb.c b/rust/helpers/usb.c
index d398eb2f6669..ac7b30334882 100644
--- a/rust/helpers/usb.c
+++ b/rust/helpers/usb.c
@@ -19,3 +19,9 @@ 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 a151a120e82e..c7bf4637ee90 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -566,6 +566,50 @@ pub fn bulk_recv(&self, endpoint: u8, data: &mut [u8], timeout: Delta) -> Result
Ok(n)
}
+ /// Issues a synchronous interrupt IN transfer on `endpoint`, returning the number of bytes
+ /// received. Uses `usb_interrupt_msg()` over an interrupt pipe (built with `usb_rcvintpipe()`)
+ /// so the URB's transfer type matches the endpoint — the correctly-typed counterpart to
+ /// [`bulk_recv`] for interrupt-IN status endpoints.
+ ///
+ /// This is a blocking, sleeping call and must only be invoked from process context. `endpoint`
+ /// is the endpoint's `bEndpointAddress` (e.g. `0x83`); only its low four bits — the endpoint
+ /// number — are used and the direction is fixed by the method (IN). As for [`bulk_recv`], the
+ /// data is received into a kmalloc'd bounce buffer internally and copied into `data`, so `data`
+ /// need not be DMA-capable. `timeout` is the maximum time to wait, rounded down to whole
+ /// milliseconds; a [`Delta`] of zero, or any non-zero value below 1 ms, waits indefinitely.
+ ///
+ /// [`bulk_recv`]: Self::bulk_recv
+ pub fn interrupt_recv(&self, endpoint: u8, data: &mut [u8], timeout: Delta) -> Result<usize> {
+ let mut actual: kernel::ffi::c_int = 0;
+
+ // `usb_interrupt_msg()` requires a DMA-capable buffer; receive into a kmalloc'd
+ // bounce buffer and copy out, so `data` need not be DMA-capable itself.
+ let mut buf = KVec::from_elem(0u8, data.len(), GFP_KERNEL)?;
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant.
+ let pipe = unsafe { bindings::usb_rcvintpipe(self.as_raw(), endpoint.into()) };
+
+ // SAFETY: `self.as_raw()` is valid by the type invariant; `buf` is a kmalloc'd buffer
+ // valid for writes of `buf.len()` bytes; `actual` is a valid out-pointer.
+ // `usb_interrupt_msg()` fills an interrupt URB for `pipe` and blocks until it completes
+ // or `timeout` elapses.
+ to_result(unsafe {
+ bindings::usb_interrupt_msg(
+ self.as_raw(),
+ pipe,
+ buf.as_mut_ptr().cast::<kernel::ffi::c_void>(),
+ buf.len().try_into()?,
+ &mut actual,
+ timeout.as_millis().try_into()?,
+ )
+ })?;
+
+ // `usb_interrupt_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 control OUT transfer on the default control endpoint.
///
/// Wraps [`usb_control_msg_send()`]; `request`, `request_type`, `value` and
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 7/9] rust: usb: add usb::Device::reset_configuration()
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
` (5 preceding siblings ...)
2026-06-17 14:59 ` [RFC PATCH 6/9] rust: usb: add usb::Device::interrupt_recv() Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 8/9] rust: usb: add an asynchronous persistently-queued bulk IN reader Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 9/9] rust: usb: add an asynchronous pipelined bulk OUT queue Mike Lothian
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
Add a safe wrapper over usb_reset_configuration() so a Rust driver can
re-issue SET_CONFIGURATION for the device's current configuration,
resetting every endpoint's data toggle and returning each interface to
alternate setting 0 (clearing stalls) without re-enumerating the device.
It sleeps, so it must be called from process context.
This is the in-kernel-coordinated way to recover a device whose endpoint
toggles or stall state have become inconsistent with the host -- e.g.
after another driver was forcibly unbound, after error recovery, or to
return a multi-function device to a known state before re-driving it.
Drivers must not open-code it by sending a raw SET_CONFIGURATION control
request: usb_reset_configuration() also re-installs the host-side
endpoint state and is serialised against the USB core, whereas a bare
control transfer is not and is unsafe on a composite device.
Because it re-issues SET_CONFIGURATION it resets the toggles of all the
device's endpoints, so on a composite device it disturbs sibling
functions; callers should treat it as a heavy reset, not a per-endpoint
operation (for a single stalled endpoint, prefer clear_halt()).
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/usb.rs | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index c7bf4637ee90..5dc5b496b970 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -697,6 +697,23 @@ pub fn set_interface(&self, interface: u8, alternate: u8) -> Result {
bindings::usb_set_interface(self.as_raw(), interface.into(), alternate.into())
})
}
+
+ /// Re-issues `SET_CONFIGURATION` for the device's current configuration, resetting
+ /// per-endpoint data toggles and returning every interface to alternate setting 0
+ /// (clearing any stalls) WITHOUT re-enumerating the device.
+ ///
+ /// Wraps [`usb_reset_configuration()`]. This is a blocking, sleeping call and must
+ /// only be invoked from process context. Note: it resets the toggles of ALL of the
+ /// device's endpoints, so on a composite device it can disturb sibling-interface
+ /// drivers that are mid-transfer.
+ ///
+ /// [`usb_reset_configuration()`]: https://docs.kernel.org/driver-api/usb/usb.html#c.usb_reset_configuration
+ pub fn reset_configuration(&self) -> Result {
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariant;
+ // `usb_reset_configuration()` only re-issues SET_CONFIGURATION and updates the
+ // host-side endpoint state for this device.
+ to_result(unsafe { bindings::usb_reset_configuration(self.as_raw()) })
+ }
}
// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 8/9] rust: usb: add an asynchronous persistently-queued bulk IN reader
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
` (6 preceding siblings ...)
2026-06-17 14:59 ` [RFC PATCH 7/9] rust: usb: add usb::Device::reset_configuration() Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
2026-06-17 14:59 ` [RFC PATCH 9/9] rust: usb: add an asynchronous pipelined bulk OUT queue Mike Lothian
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
`usb::Device::bulk_recv()` posts a single URB for the duration of the
call, leaving the IN endpoint un-posted between calls. A device that
pushes a large reply while the host is blocked on an OUT transfer can
then stall the bus in a FIFO deadlock.
Add `usb::Device::bulk_in_queue()`, which allocates `depth` URBs (each
with its own kmalloc'd DMA buffer) and submits them all up front, keeping
`depth` IN transfers continuously posted to the device -- the
always-pending-IN behaviour of a libusb async event loop. The returned
`BulkInQueue::recv()` waits for the next completion, copies it out, and
immediately re-submits that URB so the endpoint stays posted. Completion
callbacks run in interrupt context and do nothing but signal a per-URB
completion; all data handling and re-submission happen in process
context, so no IRQ-context locking is required. `Drop` cancels every URB
(`usb_kill_urb`) and only then frees them and releases the device
reference.
The URBs are filled with `usb_fill_bulk_urb()` and re-armed with
`reinit_completion()`, both reached through `rust_helper_` shims since
they are static inlines; the queue owns its URBs for its whole lifetime
in a fixed slot pool, so teardown kills them directly rather than via a
`usb_anchor` (whose group-wait primitive does not fit the per-completion
delivery this reader needs).
This is consumed by the Vino DisplayLink DL3 driver, whose EP84
control-plane endpoint must stay drained concurrently with the
OUT-direction handshake bursts.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/helpers/usb.c | 17 ++++
rust/kernel/usb.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 210 insertions(+)
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 5dc5b496b970..d77cda9ac15d 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -714,6 +714,199 @@ pub fn reset_configuration(&self) -> Result {
// host-side endpoint state for this device.
to_result(unsafe { bindings::usb_reset_configuration(self.as_raw()) })
}
+
+ /// 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. The host controller then keeps `depth` IN transfers posted to
+ /// the device continuously — matching the always-pending-IN behaviour of a libusb
+ /// async event loop, and unlike the synchronous [`bulk_recv`] which posts a single
+ /// URB only for the duration of each call (leaving the IN endpoint un-posted between
+ /// calls — a window in which a device that pushes a large reply while the host is
+ /// blocked sending an OUT can stall the bus in a FIFO deadlock).
+ ///
+ /// Completion callbacks run in interrupt context but do nothing except signal a
+ /// per-URB completion; all data handling and re-submission happen in process context
+ /// inside [`BulkInQueue::recv`], so no IRQ-context locking is required.
+ ///
+ /// Must be called from process (sleepable) context. `endpoint` is the bulk endpoint
+ /// number (low nibble of the address, e.g. `4` for `0x84`).
+ ///
+ /// [`bulk_recv`]: Self::bulk_recv
+ /// [`BulkInQueue::recv`]: BulkInQueue::recv
+ pub fn bulk_in_queue(&self, endpoint: u8, depth: usize, buf_len: usize) -> Result<BulkInQueue> {
+ let dev = self.as_raw();
+
+ // SAFETY: `dev` is a valid `struct usb_device` by the type invariant; take a
+ // refcount so the device outlives the queue (released in `BulkInQueue::drop`).
+ unsafe { bindings::usb_get_dev(dev) };
+
+ // SAFETY: `dev` is valid by the type invariant.
+ let pipe = unsafe { bindings::usb_rcvbulkpipe(dev, endpoint.into()) };
+
+ // Build the queue first so that on any early error its `Drop` cleans up whatever
+ // has been allocated/submitted so far (and releases the device refcount).
+ let mut q = BulkInQueue {
+ dev: NonNull::new(dev).ok_or(ENODEV)?,
+ slots: KVec::with_capacity(depth, GFP_KERNEL)?,
+ cursor: 0,
+ };
+
+ for _ in 0..depth {
+ // DMA-capable IN buffer (kmalloc-backed `KVec`; contents are overwritten by
+ // the device, so zero-fill is just to establish `len == buf_len`).
+ let mut buf = KVec::from_elem(0u8, buf_len, GFP_KERNEL)?;
+
+ // Per-URB completion, signaled by the IRQ-context callback. Heap-allocated so
+ // its address is stable for the lifetime the URB references it via `context`.
+ let done: KBox<Opaque<bindings::completion>> =
+ KBox::new(Opaque::uninit(), GFP_KERNEL)?;
+ // SAFETY: `done.get()` is a valid, uninitialized `struct completion`.
+ unsafe { bindings::init_completion(done.get()) };
+
+ // SAFETY: standard URB allocation; returns NULL on OOM.
+ let urb = unsafe { bindings::usb_alloc_urb(0, bindings::GFP_KERNEL) };
+ let urb = NonNull::new(urb).ok_or(ENOMEM)?;
+
+ // Fill the URB as a bulk IN transfer. `done` is the per-URB completion the
+ // callback signals; `buf`/`done` outlive the URB (owned by the slot below).
+ // SAFETY: `urb` is a freshly-allocated URB; `dev`, `buf` and `done` are valid
+ // and outlive it.
+ unsafe {
+ bindings::usb_fill_bulk_urb(
+ urb.as_ptr(),
+ dev,
+ pipe,
+ buf.as_mut_ptr().cast(),
+ buf_len.try_into()?,
+ Some(bulk_in_complete),
+ done.get().cast(),
+ );
+ }
+
+ q.slots.push(UrbSlot { urb, buf, done }, GFP_KERNEL)?;
+ }
+
+ // Submit every URB. On failure, `q`'s Drop kills/frees the already-submitted ones.
+ for slot in q.slots.iter() {
+ // SAFETY: `slot.urb` is a valid, filled URB; submitting transfers ownership of
+ // the buffer to the controller until completion.
+ to_result(unsafe {
+ bindings::usb_submit_urb(slot.urb.as_ptr(), bindings::GFP_KERNEL)
+ })?;
+ }
+
+ Ok(q)
+ }
+}
+
+/// One slot of a [`BulkInQueue`]: a URB, its DMA buffer, and the completion the URB's
+/// callback signals. The buffer and completion outlive the URB's submission; both are
+/// freed only after the URB is killed in [`BulkInQueue::drop`].
+struct UrbSlot {
+ urb: NonNull<bindings::urb>,
+ buf: KVec<u8>,
+ done: KBox<Opaque<bindings::completion>>,
+}
+
+/// A persistently-queued asynchronous bulk IN reader. See [`Device::bulk_in_queue`].
+///
+/// Keeps `depth` IN URBs posted to the device at all times: [`recv`] waits for the next
+/// one to complete, copies its data out, and immediately re-submits it, so at least
+/// `depth - 1` transfers stay outstanding while one is being serviced.
+///
+/// [`recv`]: BulkInQueue::recv
+pub struct BulkInQueue {
+ dev: NonNull<bindings::usb_device>,
+ slots: KVec<UrbSlot>,
+ cursor: usize,
+}
+
+impl BulkInQueue {
+ /// 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 (`n` bytes copied; the URB is
+ /// re-submitted before returning so the endpoint stays posted), `Ok(None)` on timeout
+ /// (the URB is left outstanding — call again to keep waiting on it), or `Err` if the
+ /// completed transfer carried an error status (the URB is still re-submitted).
+ ///
+ /// Must be called from process (sleepable) context.
+ pub fn recv(&mut self, out: &mut [u8], timeout: Delta) -> Result<Option<usize>> {
+ let i = self.cursor;
+ let jiffies = unsafe {
+ bindings::__msecs_to_jiffies(timeout.as_millis().try_into().unwrap_or(u32::MAX))
+ };
+
+ let slot = &self.slots[i];
+
+ // SAFETY: `slot.done` is a valid, initialized completion.
+ let remaining = unsafe { bindings::wait_for_completion_timeout(slot.done.get(), jiffies) };
+ if remaining == 0 {
+ // Timed out: the URB is still outstanding (posted). Leave it; the caller can
+ // retry and we keep waiting on the same slot.
+ return Ok(None);
+ }
+
+ let urb = slot.urb.as_ptr();
+ // SAFETY: the completion fired, so the controller is done with this URB; reading
+ // its result fields and buffer is now race-free until we re-submit below.
+ let status = unsafe { (*urb).status };
+ let len = unsafe { (*urb).actual_length } as usize;
+ let n = core::cmp::min(len, out.len());
+ out[..n].copy_from_slice(&slot.buf[..n]);
+
+ // Re-arm: reset the completion (the URB is not outstanding right now, so nothing
+ // races this) and re-submit to keep the endpoint posted.
+ // SAFETY: `slot.done` is a valid, initialized completion.
+ unsafe { bindings::reinit_completion(slot.done.get()) };
+ // SAFETY: `urb` is valid and not currently submitted.
+ let rc = unsafe { bindings::usb_submit_urb(urb, bindings::GFP_KERNEL) };
+
+ self.cursor = (i + 1) % self.slots.len();
+
+ to_result(rc)?;
+ if status != 0 {
+ return Err(Error::from_errno(status));
+ }
+ Ok(Some(n))
+ }
+}
+
+impl Drop for BulkInQueue {
+ fn drop(&mut self) {
+ // Cancel every URB first (waits for any in-flight completion callback to finish),
+ // THEN free them. Only after this do the slots' buffers/completions get dropped
+ // (struct fields drop after this body), so nothing the controller could touch is
+ // freed while a URB is still live.
+ for slot in self.slots.iter() {
+ // SAFETY: `slot.urb` is a valid URB allocated by `usb_alloc_urb`.
+ unsafe { bindings::usb_kill_urb(slot.urb.as_ptr()) };
+ }
+ for slot in self.slots.iter() {
+ // SAFETY: `slot.urb` is a valid, now-cancelled URB; freeing drops the
+ // allocation's reference.
+ unsafe { bindings::usb_free_urb(slot.urb.as_ptr()) };
+ }
+ // SAFETY: balances the `usb_get_dev` taken in `bulk_in_queue`.
+ unsafe { bindings::usb_put_dev(self.dev.as_ptr()) };
+ }
+}
+
+/// URB completion callback (interrupt context). Does nothing but signal the per-URB
+/// completion whose pointer was stored in `urb->context`; all data handling and
+/// re-submission happen in process context in [`BulkInQueue::recv`].
+///
+/// # Safety
+///
+/// `urb` must be a valid URB whose `context` is a live `struct completion` (guaranteed by
+/// construction in [`Device::bulk_in_queue`]).
+unsafe extern "C" fn bulk_in_complete(urb: *mut bindings::urb) {
+ // SAFETY: by construction `context` points to a live, initialized `struct completion`
+ // that outlives the URB.
+ let done = unsafe { (*urb).context } as *mut bindings::completion;
+ // SAFETY: `done` is a valid completion; `complete()` is safe from interrupt context.
+ unsafe { bindings::complete(done) };
}
// SAFETY: `Device` is a transparent wrapper of a type that doesn't depend on `Device`'s generic
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH 9/9] rust: usb: add an asynchronous pipelined bulk OUT queue
2026-06-17 14:59 [RFC PATCH 0/9] rust: usb: synchronous bulk/control transfers + helpers Mike Lothian
` (7 preceding siblings ...)
2026-06-17 14:59 ` [RFC PATCH 8/9] rust: usb: add an asynchronous persistently-queued bulk IN reader Mike Lothian
@ 2026-06-17 14:59 ` Mike Lothian
8 siblings, 0 replies; 11+ messages in thread
From: Mike Lothian @ 2026-06-17 14:59 UTC (permalink / raw)
To: rust-for-linux
Cc: Mike Lothian, linux-usb, Miguel Ojeda, Boqun Feng, Gary Guo,
Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
Trevor Gross, Danilo Krummrich, Daniel Almeida,
Greg Kroah-Hartman, Alexandre Courbot, Shankari Anand,
linux-kernel
`usb::Device::bulk_send()` issues a synchronous `usb_bulk_msg()` and
blocks until each transfer completes (a device-ACK round-trip) before
the next can be submitted. On a request/response channel that round-trip
is invisible, but on a back-to-back OUT burst it serialises transfers
that a libusb async event loop would pipeline -- inserting host-side gaps
that are not present in the daemon this driver replaces.
Add `usb::Device::bulk_out_queue()`, the OUT counterpart to
`bulk_in_queue()`. It pre-allocates `depth` URBs (each with its own
kmalloc'd DMA buffer, filled with `usb_fill_bulk_urb()`) but submits none
up front, since an OUT URB carries caller data. `BulkOutQueue::send()`
fills and submits the slot at the round-robin cursor without waiting,
reaping that slot's previous transfer only when the cursor laps back to
it -- so up to `depth - 1` OUT transfers stay in flight, the
submit-and-reap model of a libusb async event loop. `flush()` drains all
outstanding transfers and surfaces the first error status; `Drop` cancels
and frees every URB.
The per-URB completion callback only signals a `struct completion` and is
direction-agnostic, so it is shared with `bulk_in_queue()` and renamed
`urb_signal_complete()`; slots are re-armed with `reinit_completion()`.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/usb.rs | 216 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 212 insertions(+), 4 deletions(-)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index d77cda9ac15d..6c49de422b30 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -779,7 +779,7 @@ pub fn bulk_in_queue(&self, endpoint: u8, depth: usize, buf_len: usize) -> Resul
pipe,
buf.as_mut_ptr().cast(),
buf_len.try_into()?,
- Some(bulk_in_complete),
+ Some(urb_signal_complete),
done.get().cast(),
);
}
@@ -798,6 +798,88 @@ pub fn bulk_in_queue(&self, endpoint: u8, depth: usize, buf_len: usize) -> Resul
Ok(q)
}
+
+ /// Opens an asynchronous, pipelined bulk OUT writer on `endpoint`.
+ ///
+ /// Pre-allocates `depth` URBs, each with its own `buf_len`-byte DMA buffer, but submits
+ /// none up front (unlike [`bulk_in_queue`], an OUT URB carries caller data so it is
+ /// filled and submitted per [`BulkOutQueue::send`]). This lets the host keep up to
+ /// `depth` OUT transfers in flight at once — the submit-and-reap execution model of a
+ /// libusb async event loop — instead of the synchronous [`bulk_send`], which blocks for
+ /// each transfer's completion (a device-ACK round-trip) before the next can be submitted.
+ /// On a request/response channel that round-trip is invisible, but on a back-to-back OUT
+ /// burst (e.g. the pre-arm capability sequence) it serialises transfers the daemon
+ /// pipelines.
+ ///
+ /// Must be called from process (sleepable) context. `endpoint` is the bulk endpoint
+ /// number (low nibble of the address, e.g. `2` for `0x02`). `buf_len` is the largest
+ /// single transfer the caller will [`send`]; longer payloads are rejected with `EMSGSIZE`.
+ ///
+ /// [`bulk_in_queue`]: Self::bulk_in_queue
+ /// [`bulk_send`]: Self::bulk_send
+ /// [`send`]: BulkOutQueue::send
+ pub fn bulk_out_queue(
+ &self,
+ endpoint: u8,
+ depth: usize,
+ buf_len: usize,
+ ) -> Result<BulkOutQueue> {
+ let dev = self.as_raw();
+
+ // SAFETY: `dev` is valid by the type invariant; take a refcount so the device
+ // outlives the queue (released in `BulkOutQueue::drop`).
+ unsafe { bindings::usb_get_dev(dev) };
+
+ // SAFETY: `dev` is valid by the type invariant.
+ let pipe = unsafe { bindings::usb_sndbulkpipe(dev, endpoint.into()) };
+
+ let mut q = BulkOutQueue {
+ dev: NonNull::new(dev).ok_or(ENODEV)?,
+ slots: KVec::with_capacity(depth, GFP_KERNEL)?,
+ cursor: 0,
+ };
+
+ for _ in 0..depth {
+ // DMA-capable OUT buffer; filled per `send`.
+ let buf = KVec::from_elem(0u8, buf_len, GFP_KERNEL)?;
+
+ let done: KBox<Opaque<bindings::completion>> =
+ KBox::new(Opaque::uninit(), GFP_KERNEL)?;
+ // SAFETY: `done.get()` is a valid, uninitialized `struct completion`.
+ unsafe { bindings::init_completion(done.get()) };
+
+ // SAFETY: standard URB allocation; returns NULL on OOM.
+ let urb = unsafe { bindings::usb_alloc_urb(0, bindings::GFP_KERNEL) };
+ let urb = NonNull::new(urb).ok_or(ENOMEM)?;
+
+ // Fill the fixed URB fields; the transfer buffer and length are set per `send`,
+ // so pass a null buffer here.
+ // SAFETY: `urb` is freshly allocated; `dev`/`done` outlive the URB.
+ unsafe {
+ bindings::usb_fill_bulk_urb(
+ urb.as_ptr(),
+ dev,
+ pipe,
+ core::ptr::null_mut(),
+ 0,
+ Some(urb_signal_complete),
+ done.get().cast(),
+ );
+ }
+
+ q.slots.push(
+ OutSlot {
+ urb,
+ buf,
+ done,
+ inflight: false,
+ },
+ GFP_KERNEL,
+ )?;
+ }
+
+ Ok(q)
+ }
}
/// One slot of a [`BulkInQueue`]: a URB, its DMA buffer, and the completion the URB's
@@ -893,15 +975,141 @@ fn drop(&mut self) {
}
}
+/// One slot of a [`BulkOutQueue`]: a URB, its DMA buffer, the completion its callback
+/// signals, and whether a transfer using it is currently outstanding. Like [`UrbSlot`] but
+/// the `inflight` flag lets [`BulkOutQueue::send`] know whether the slot must be reaped
+/// (its previous transfer awaited) before the buffer can be reused.
+struct OutSlot {
+ urb: NonNull<bindings::urb>,
+ buf: KVec<u8>,
+ done: KBox<Opaque<bindings::completion>>,
+ inflight: bool,
+}
+
+/// An asynchronous, pipelined bulk OUT writer. See [`Device::bulk_out_queue`].
+///
+/// Round-robins over `depth` slots: [`send`] reuses the slot at the cursor, first waiting
+/// for that slot's previous transfer to complete (so at most `depth` transfers are ever
+/// outstanding), then fills and submits it without waiting for *its* completion. This keeps
+/// up to `depth - 1` OUT transfers in flight while the next is prepared — the libusb
+/// submit-and-reap model. [`flush`] drains all outstanding transfers and surfaces the first
+/// error status.
+///
+/// [`send`]: BulkOutQueue::send
+/// [`flush`]: BulkOutQueue::flush
+pub struct BulkOutQueue {
+ dev: NonNull<bindings::usb_device>,
+ slots: KVec<OutSlot>,
+ cursor: usize,
+}
+
+impl BulkOutQueue {
+ /// Reaps the slot at index `i` if it has an outstanding transfer: waits up to `timeout`
+ /// for completion, clears `inflight`, and returns the transfer's error status (if any).
+ /// `Ok(false)` means nothing was outstanding; `Ok(true)` means a transfer was reaped OK.
+ fn reap(&mut self, i: usize, timeout: Delta) -> Result<bool> {
+ if !self.slots[i].inflight {
+ return Ok(false);
+ }
+ let jiffies = unsafe {
+ bindings::__msecs_to_jiffies(timeout.as_millis().try_into().unwrap_or(u32::MAX))
+ };
+ // SAFETY: `done` is a valid, initialized completion.
+ let remaining =
+ unsafe { bindings::wait_for_completion_timeout(self.slots[i].done.get(), jiffies) };
+ if remaining == 0 {
+ // Still outstanding; leave `inflight` set so a later call keeps waiting on it.
+ return Err(ETIMEDOUT);
+ }
+ self.slots[i].inflight = false;
+ // SAFETY: the completion fired, so the controller is done with this URB.
+ let status = unsafe { (*self.slots[i].urb.as_ptr()).status };
+ if status != 0 {
+ return Err(Error::from_errno(status));
+ }
+ Ok(true)
+ }
+
+ /// Submits `data` as a bulk OUT transfer without waiting for its completion, returning as
+ /// soon as the URB is queued to the controller.
+ ///
+ /// If every slot is busy this blocks up to `timeout` reaping the oldest in-flight
+ /// transfer before reusing it; a reap error (timeout or a failed prior transfer) is
+ /// surfaced. `data` must be no longer than the queue's `buf_len` (else `EMSGSIZE`).
+ ///
+ /// Must be called from process (sleepable) context.
+ pub fn send(&mut self, data: &[u8], timeout: Delta) -> Result {
+ let i = self.cursor;
+ if data.len() > self.slots[i].buf.len() {
+ return Err(EMSGSIZE);
+ }
+
+ // Free the slot if its previous transfer is still outstanding.
+ let _ = self.reap(i, timeout)?;
+
+ // Fill the buffer and length for this transfer.
+ self.slots[i].buf[..data.len()].copy_from_slice(data);
+ // SAFETY: `done` is a valid, initialized completion (the URB is not outstanding
+ // right now, so nothing races this).
+ unsafe { bindings::reinit_completion(self.slots[i].done.get()) };
+ let buf_ptr = self.slots[i].buf.as_mut_ptr();
+ let urb = self.slots[i].urb.as_ptr();
+ // SAFETY: `urb` is valid and not currently submitted; `buf_ptr` is a DMA-capable
+ // buffer valid for `data.len()` bytes for the transfer's duration.
+ unsafe {
+ (*urb).transfer_buffer = buf_ptr.cast();
+ (*urb).transfer_buffer_length = data.len().try_into()?;
+ }
+ // SAFETY: `urb` is a valid, filled OUT URB; submitting transfers buffer ownership to
+ // the controller until completion.
+ to_result(unsafe { bindings::usb_submit_urb(urb, bindings::GFP_KERNEL) })?;
+ self.slots[i].inflight = true;
+ self.cursor = (i + 1) % self.slots.len();
+ Ok(())
+ }
+
+ /// Waits up to `timeout` for every outstanding transfer to complete, returning the first
+ /// error status encountered (all slots are still reaped regardless).
+ pub fn flush(&mut self, timeout: Delta) -> Result {
+ 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
+ }
+}
+
+impl Drop for BulkOutQueue {
+ fn drop(&mut self) {
+ // Cancel every URB first (waits for any in-flight completion callback), THEN free.
+ for slot in self.slots.iter() {
+ // SAFETY: `slot.urb` is a valid URB allocated by `usb_alloc_urb`.
+ unsafe { bindings::usb_kill_urb(slot.urb.as_ptr()) };
+ }
+ for slot in self.slots.iter() {
+ // SAFETY: `slot.urb` is a valid, now-cancelled URB.
+ unsafe { bindings::usb_free_urb(slot.urb.as_ptr()) };
+ }
+ // SAFETY: balances the `usb_get_dev` taken in `bulk_out_queue`.
+ unsafe { bindings::usb_put_dev(self.dev.as_ptr()) };
+ }
+}
+
/// URB completion callback (interrupt context). Does nothing but signal the per-URB
/// completion whose pointer was stored in `urb->context`; all data handling and
-/// re-submission happen in process context in [`BulkInQueue::recv`].
+/// re-submission happen in process context (in [`BulkInQueue::recv`] for IN, or in
+/// [`BulkOutQueue::send`]/[`BulkOutQueue::flush`] for OUT). Shared by both queues since
+/// it only fires the completion and is direction-agnostic.
///
/// # Safety
///
/// `urb` must be a valid URB whose `context` is a live `struct completion` (guaranteed by
-/// construction in [`Device::bulk_in_queue`]).
-unsafe extern "C" fn bulk_in_complete(urb: *mut bindings::urb) {
+/// construction in [`Device::bulk_in_queue`] / [`Device::bulk_out_queue`]).
+unsafe extern "C" fn urb_signal_complete(urb: *mut bindings::urb) {
// SAFETY: by construction `context` points to a live, initialized `struct completion`
// that outlives the URB.
let done = unsafe { (*urb).context } as *mut bindings::completion;
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread