[RFC PATCH 1/9] rust: usb: add synchronous bulk transfer support
From: Mike Lothian
Date: Wed Jun 17 2026 - 11:10:27 EST
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@xxxxxxxxxxxxxx>
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