* [PATCH RFC v2 1/4] rust: usb: add endpoint abstraction
2026-08-11 9:42 [PATCH RFC v2 0/4] rust: usb: abstractions towards the port of usbsevseg.c to Rust Alexandru Radovici
@ 2026-08-11 9:42 ` Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 2/4] rust: usb: add control message send and receive Alexandru Radovici
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Alexandru Radovici @ 2026-08-11 9:42 UTC (permalink / raw)
To: Greg Kroah-Hartman, 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, Rafael J. Wysocki
Cc: linux-kernel, linux-usb, rust-for-linux, driver-core,
Alexandru Radovici
Add an abstraction for `struct usb_host_endpoint`, together with the
accessors needed to reach one: `AlternateSetting` wrapping
`struct usb_host_interface`, `Interface::alternate_settings()` and
`Interface::current_alternate_setting()`, and `Device::control_endpoint()`
for the default control endpoint, which no interface descriptor lists.
`HostEndpoint` is generic over two sealed marker traits,
`EndpointDirection` and `EndpointTransferType`, whose implementors are
1-ZSTs held in `PhantomData`. An endpoint borrowed from an alternate
setting starts out generic in both; `as_in()`, `as_out()` and
`as_control()` check the descriptor once and return a reference
carrying the corresponding marker, so a function taking
`&HostEndpoint<In, Bulk>` needs no check of its own. The type is
`#[repr(transparent)]` over the C struct and the markers are
zero-sized, so the refinement costs nothing and a slice of endpoints
can be borrowed directly from the C array.
Control endpoints get a distinct `Bidirectional` marker rather than an
IN or OUT one. A control transfer takes its direction from bit 7 of the
setup packet's bmRequestType, and USB 2.0 section 9.6.6 defines the
corresponding bit of bEndpointAddress as ignored for control endpoints.
`as_in()` and `as_out()` are not implemented for `Bidirectional`, making
calling them a compile error rather than a misleading result.
Signed-off-by: Alexandru Radovici <alexandru.radovici@wyliodrin.com>
---
rust/kernel/usb.rs | 126 ++++++++++-
rust/kernel/usb/endpoint.rs | 516 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 641 insertions(+), 1 deletion(-)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 7aff0c82d0af..4f5f1db3e7ca 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -20,6 +20,7 @@
prelude::*,
sync::aref::AlwaysRefCounted,
types::Opaque,
+ usb::endpoint::HostEndpoint,
ThisModule, //
};
use core::{
@@ -29,8 +30,11 @@
MaybeUninit, //
},
ptr::NonNull,
+ slice, //
};
+pub mod endpoint;
+
/// An adapter for the registration of USB drivers.
pub struct Adapter<T: Driver>(T);
@@ -334,6 +338,78 @@ fn disconnect<'bound>(
);
}
+/// A single alternate setting of an [`Interface`].
+///
+/// A USB interface declares one or more alternate settings, each of which describes a different
+/// endpoint configuration for the same logical function - for example a UVC camera exposing one
+/// setting per bandwidth tier, plus a zero-bandwidth setting used while idle. Exactly one is
+/// active at a time; see [`Interface::current_alt_setting()`].
+///
+/// # Invariants
+///
+/// The wrapped [`Opaque`] holds an initialised `struct usb_host_interface`. Instances are never
+/// constructed by Rust code: they are only ever borrowed out of the `altsetting` array of a
+/// `struct usb_interface` owned by the C side, which guarantees that `desc` is initialised and
+/// that the setting outlives the borrow.
+#[repr(transparent)]
+pub struct AlternateSetting(Opaque<bindings::usb_host_interface>);
+
+impl AlternateSetting {
+ /// Returns a raw pointer to the underlying `struct usb_host_interface`.
+ ///
+ /// By the type invariants the pointer is non-null and points at an initialised alternate
+ /// setting for at least the lifetime of `&self`.
+ fn as_raw(&self) -> *mut bindings::usb_host_interface {
+ self.0.get()
+ }
+
+ /// Returns this setting's `bAlternateSetting` number.
+ ///
+ /// Alternate settings of one interface are numbered from zero; setting 0 always exists and is
+ /// the one the device defaults to after a configuration is selected. This is the value passed
+ /// to `usb_set_interface()` to activate the setting.
+ pub fn number(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_interface` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bAlternateSetting }
+ }
+
+ /// Returns the `bInterfaceNumber` of the interface this setting belongs to.
+ ///
+ /// Every alternate setting of a given interface reports the same number, so this identifies
+ /// the interface within its configuration rather than distinguishing settings from one
+ /// another, use [`number()`](Self::number) for that.
+ pub fn interface_number(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_interface` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bInterfaceNumber }
+ }
+
+ /// Returns the endpoints declared by this alternate setting, in descriptor order.
+ ///
+ /// The endpoints come back untyped, as `Endpoint<Generic, Generic>`; refine them with
+ /// [`Endpoint::as_in()`], [`Endpoint::as_out()`] and [`Endpoint::as_control()`].
+ pub fn endpoints(&self) -> &[HostEndpoint] {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_interface` with an initialised `desc`.
+ let (ptr, len) = (unsafe { (*self.as_raw()).endpoint }, unsafe {
+ (*self.as_raw()).desc.bNumEndpoints
+ });
+
+ if len == 0 {
+ &[]
+ } else {
+ // SAFETY: When `bNumEndpoints` is non-zero the C side has allocated an array of that
+ // many initialised `struct usb_host_endpoint` at `ptr`, living as long as the
+ // interface. `Endpoint` is a `#[repr(transparent)]` wrapper around
+ // `Opaque<bindings::usb_host_endpoint>`, which is itself layout-compatible with
+ // `struct usb_host_endpoint`, so the cast preserves both size and alignment and the
+ // resulting slice borrows for no longer than `&self`.
+ unsafe { slice::from_raw_parts(ptr.cast(), len as usize) }
+ }
+ }
+}
+
/// A USB interface.
///
/// This structure represents the Rust abstraction for a C [`struct usb_interface`].
@@ -356,6 +432,54 @@ impl<Ctx: device::DeviceContext> Interface<Ctx> {
fn as_raw(&self) -> *mut bindings::usb_interface {
self.0.get()
}
+
+ /// Returns all alternate settings of this interface, in `bAlternateSetting` order.
+ ///
+ /// The slice is never empty: every interface has at least setting 0.
+ pub fn alternate_settings(&self) -> &[AlternateSetting] {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid `struct usb_interface`,
+ // so both fields are initialised. Reading them requires nothing of the device context:
+ // `altsetting` is filled in when the interface is created and holds until it is released.
+ let (ptr, len) = (unsafe { (*self.as_raw()).altsetting }, unsafe {
+ (*self.as_raw()).num_altsetting
+ });
+
+ if len == 0 {
+ &[]
+ } else {
+ // SAFETY: When `num_altsetting` is non-zero the C side has allocated an array of that
+ // many initialised `struct usb_host_interface` at `ptr`, kept alive by the interface's
+ // reference for at least as long as `&self`.
+ //
+ // `AlternateSetting` is a `#[repr(transparent)]` wrapper around
+ // `Opaque<bindings::usb_host_interface>`, which is itself layout-compatible with
+ // `struct usb_host_interface`, so the cast preserves both size and alignment and
+ // the resulting slice borrows for no longer than `&self`.
+ unsafe { slice::from_raw_parts(ptr.cast(), len as usize) }
+ }
+ }
+
+ /// Returns the alternate setting that is currently active on this interface.
+ ///
+ /// This is the setting whose endpoints the device is actually prepared to service, so it is
+ /// the one a driver should read endpoint descriptors from. After configuration it is
+ /// setting 0.
+ ///
+ /// The result is a snapshot. `usb_set_interface()` can repoint the interface at a different
+ /// setting, which does not invalidate the returned reference - both point into the same live
+ /// array - but does stop it being the current one. A driver that caches endpoints across a
+ /// setting switch will go on using the previous setting's descriptors.
+ pub fn current_alternate_setting(&self) -> &AlternateSetting {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid `struct usb_interface`.
+ // `cur_altsetting` is set when the interface is created and only ever repointed within
+ // that same array by `usb_set_interface()`, so it is non-null and names an initialised
+ // `struct usb_host_interface` for at least the lifetime of `&self`.
+ //
+ // `AlternateSetting` is a `#[repr(transparent)]` wrapper around
+ // `Opaque<bindings::usb_host_interface>` and so layout-compatible with it, and the borrow
+ // lasts no longer than `&self`.
+ unsafe { &*(*self.as_raw()).cur_altsetting.cast() }
+ }
}
// SAFETY: `usb::Interface` is a transparent wrapper of `struct usb_interface`.
@@ -426,7 +550,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>,
);
diff --git a/rust/kernel/usb/endpoint.rs b/rust/kernel/usb/endpoint.rs
new file mode 100644
index 000000000000..e21554bee7f7
--- /dev/null
+++ b/rust/kernel/usb/endpoint.rs
@@ -0,0 +1,516 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (C) 2026 Wyliodrin SRL.
+
+//! USB endpoints.
+//!
+//! C header: [`include/linux/usb.h`](srctree/include/linux/usb.h)
+//!
+//! A [`HostEndpoint`] is a borrowed view of a `struct usb_host_endpoint` - one of the addressable
+//! sources or sinks of data on a USB device. Its accessors read the endpoint descriptor the device
+//! reported: [`number()`](HostEndpoint::number) and [`address()`](HostEndpoint::address),
+//! [`direction()`](HostEndpoint::direction) and [`transfer_type()`](HostEndpoint::transfer_type),
+//! and the packet and interval geometry needed to size and schedule transfers.
+//!
+//! # Where endpoints come from
+//!
+//! Endpoints declared by an interface are borrowed from the alternate setting that describes them,
+//! via [`AlternateSetting::endpoints()`](crate::usb::AlternateSetting::endpoints). The
+//! default control endpoint - endpoint 0 - is not among them: no interface descriptor lists
+//! it, because the specification excludes it from `bNumEndpoints` and devices never send a
+//! descriptor for it. It is reached through [`Device::control_endpoint()`] instead.
+//!
+//! # Type-state
+//!
+//! [`HostEndpoint`] carries two marker type parameters recording what is *statically* known
+//! about it. Endpoints start out fully generic, and the `as_*` accessors check the
+//! descriptor once and hand back a reference that remembers the answer:
+//!
+//! ```text
+//! Endpoint<Generic, Generic>
+//! | | |
+//! as_in() <-- v as_control() --> as_out()
+//! Endpoint<In, Generic> Endpoint<Bidirectional, Control> Endpoint<Out, Generic>
+//! ```
+//!
+//! A function taking `&Endpoint<In, Bulk>` therefore cannot be handed anything but a bulk IN
+//! endpoint, and needs no run-time check of its own. The markers are [`PhantomData`], so this
+//! costs nothing at run time and a typed endpoint has the same layout as an untyped one.
+//!
+//! # Direction and control endpoints
+//!
+//! The two axes are not independent. A control endpoint is bidirectional: the direction of a
+//! control transfer comes from the setup packet's `bmRequestType`, and USB 2.0
+//! section 9.6.6 correspondingly defines bit 7 of `bEndpointAddress` as ignored
+//! for control endpoints. So the direction of a control endpoint is
+//! not merely unknown - it does not exist.
+//!
+//! That is what [`Bidirectional`] marks. Because [`as_in()`](HostEndpoint::as_in) and
+//! [`as_out()`](HostEndpoint::as_out) are defined only for [`Generic`],
+//! asking a control endpoint which direction it runs in is a compile error rather than
+//! an answer that would mislead whichever way it came out. See [`control`](crate::usb::control)
+//! for issuing transfers on one.
+//!
+//! # Examples
+//!
+//! ```
+//! use kernel::usb::{
+//! endpoint::{HostEndpoint, In, Out},
+//! AlternateSetting,
+//! };
+//!
+//! /// Picks out the first IN and OUT endpoints of an alternate setting.
+//! fn pair(alt: &AlternateSetting) -> (Option<&Endpoint<In>>, Option<&Endpoint<Out>>) {
+//! let eps = alt.endpoints();
+//!
+//! (
+//! eps.iter().find_map(ep.as_in),
+//! eps.iter().find_map(ep.as_out),
+//! )
+//! }
+//! ```
+
+use core::marker::PhantomData;
+
+use crate::{
+ device,
+ types::Opaque,
+ usb::Device, //
+};
+
+/// A single endpoint of an [`AlternateSetting`](crate::usb::AlternateSetting).
+///
+/// `Dir` and `Type` are compile-time markers recording what is statically known about the
+/// endpoint's direction and transfer type. An endpoint obtained from an
+/// [`AlternateSetting`](crate::usb::AlternateSetting) starts out as `Endpoint<Generic, Generic>`,
+/// i.e. nothing is known about it yet. The [`as_in()`], [`as_out()`] and [`as_control()`]
+/// accessors inspect the endpoint descriptor at run time and, on success, hand back a reference
+/// carrying the corresponding marker. Code that accepts only an `&Endpoint<In, Bulk>` may then
+/// rely on the endpoint really being a bulk IN endpoint without re-checking it.
+///
+/// Direction and transfer type are not independent: a control endpoint has no direction, so
+/// [`as_control()`] yields [`Bidirectional`] rather than preserving or discovering an IN/OUT
+/// marker, and [`as_in()`]/[`as_out()`] are not defined on the result.
+///
+/// The markers are [`PhantomData`], so a typed `HostEndpoint` has the same layout as
+/// an untyped one and the refinement is free at run time.
+///
+/// # Invariants
+///
+/// - The wrapped [`Opaque`] holds an initialised `struct usb_host_endpoint`. Instances are never
+/// constructed by Rust code; they are only ever borrowed out of a `struct usb_host_interface`
+/// owned by the C side, which guarantees that `desc` is initialised and that the endpoint
+/// outlives the reference.
+/// - If `Type` is [`Control`], [`Isochronous`], [`Bulk`] or [`Interrupt`], then
+/// `desc.bmAttributes` really encodes that transfer type.
+/// - If `Dir` is [`In`] or [`Out`], then bit 7 of `desc.bEndpointAddress` really encodes that
+/// direction. If `Dir` is [`Bidirectional`], then `Type` is [`Control`] and the direction bit
+/// carries no meaning at all.
+/// - [`Generic`] asserts nothing in either position.
+///
+/// [`as_in()`]: HostEndpoint::as_in
+/// [`as_out()`]: HostEndpoint::as_out
+/// [`as_control()`]: HostEndpoint::as_control
+#[repr(transparent)]
+pub struct HostEndpoint<Dir: EndpointDirection = Generic, Type: EndpointTransferType = Generic>(
+ Opaque<bindings::usb_host_endpoint>,
+ PhantomData<Dir>,
+ PhantomData<Type>,
+);
+
+/// A marker usable in the `Dir` position of [`HostEndpoint`].
+///
+/// This trait is sealed: it is implemented by [`Generic`], [`In`], [`Out`] and [`Bidirectional`]
+/// only, and cannot be implemented outside of this module.
+pub trait EndpointDirection: private::Sealed {}
+
+/// A marker usable in the `Type` position of [`HostEndpoint`].
+///
+/// This trait is sealed: it is implemented by [`Generic`], [`Control`], [`Isochronous`],
+/// [`Bulk`] and [`Interrupt`] only, and cannot be implemented outside of this module.
+pub trait EndpointTransferType: private::Sealed {}
+
+/// Marker for an [`HostEndpoint`] whose direction or transfer type is not statically known.
+///
+/// This is the default in both marker positions. It carries no guarantee, so the property has
+/// to be queried at run time with [`HostEndpoint::direction()`] or
+/// [`HostEndpoint::transfer_type()`], or established once and for all with one of
+/// the `as_*` accessors.
+pub struct Generic;
+
+/// Marker for a device-to-host ("IN") [`HostEndpoint`].
+pub struct In;
+
+/// Marker for a host-to-device ("OUT") [`HostEndpoint`].
+pub struct Out;
+
+/// Marker for an [`HostEndpoint`] that carries data in both directions.
+///
+/// This is the direction marker of every control endpoint, and the only marker they get. A control
+/// transfer takes the direction of its data stage from bit 7 of the setup
+/// packet's `bmRequestType`, and USB 2.0 section 9.6.6 correspondingly defines bit 7
+/// of `bEndpointAddress` as ignored for control endpoints - so "is this endpoint IN or OUT"
+/// has no answer for one.
+///
+/// Since [`HostEndpoint::as_in()`] and [`HostEndpoint::as_out()`] are defined only
+/// for [`Generic`], asking that question of a [`Bidirectional`] endpoint fails to compile rather
+/// than returning an answer that would be misleading either way.
+pub struct Bidirectional;
+
+/// Marker for an [`HostEndpoint`] using control transfers.
+pub struct Control;
+
+/// Marker for an [`HostEndpoint`] using isochronous transfers.
+pub struct Isochronous;
+
+/// Marker for an [`HostEndpoint`] using bulk transfers.
+pub struct Bulk;
+
+/// Marker for an [`HostEndpoint`] using interrupt transfers.
+pub struct Interrupt;
+
+mod private {
+ /// Prevents [`EndpointDirection`] and [`EndpointTransferType`] from being implemented
+ /// outside of this module, so that the type invariants of [`HostEndpoint`] cannot be forged by
+ /// downstream code.
+ ///
+ /// [`EndpointDirection`]: super::EndpointDirection
+ /// [`EndpointTransferType`]: super::EndpointTransferType
+ /// [`HostEndpoint`]: super::Endpoint
+ pub trait Sealed {}
+
+ impl Sealed for super::Generic {}
+ impl Sealed for super::In {}
+ impl Sealed for super::Out {}
+ impl Sealed for super::Bidirectional {}
+ impl Sealed for super::Control {}
+ impl Sealed for super::Isochronous {}
+ impl Sealed for super::Bulk {}
+ impl Sealed for super::Interrupt {}
+}
+
+impl EndpointDirection for Generic {}
+impl EndpointTransferType for Generic {}
+impl EndpointDirection for In {}
+impl EndpointDirection for Out {}
+impl EndpointDirection for Bidirectional {}
+impl EndpointTransferType for Control {}
+impl EndpointTransferType for Isochronous {}
+impl EndpointTransferType for Bulk {}
+impl EndpointTransferType for Interrupt {}
+
+/// The direction in which data flows over an [`HostEndpoint`].
+///
+/// The direction is fixed by the endpoint descriptor and is stated from the host's point of
+/// view. Control endpoints are bidirectional; for those the descriptor's direction bit is
+/// meaningless and this enum should be ignored.
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum Direction {
+ /// Data flows from the device to the host (`USB_DIR_IN`).
+ In,
+ /// Data flows from the host to the device (`USB_DIR_OUT`).
+ Out,
+}
+
+impl From<u8> for Direction {
+ /// Extracts the direction from a raw `bEndpointAddress`.
+ ///
+ /// Bit 7 of the endpoint address is the direction bit: set means IN, clear means OUT. All
+ /// other bits are ignored, so any `u8` is a valid input.
+ fn from(value: u8) -> Self {
+ if (value >> 7) & 0b1 == 1 {
+ Direction::In
+ } else {
+ Direction::Out
+ }
+ }
+}
+
+/// The transfer type an [`HostEndpoint`] uses.
+///
+/// Every endpoint is fixed to exactly one of these by its descriptor; see USB 2.0 section 5.4
+/// for what each one guarantees.
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum TransferType {
+ /// Bidirectional, request/response transfers with guaranteed delivery. Used for device
+ /// configuration; endpoint 0 is always a control endpoint.
+ Control,
+ /// Transfers with guaranteed bandwidth and bounded latency but no error retry, for
+ /// time-sensitive streams such as audio and video.
+ Isochronous,
+ /// Transfers with guaranteed delivery but no bandwidth or latency guarantee, for bulk data
+ /// such as mass storage.
+ Bulk,
+ /// Small, periodically polled transfers with bounded latency and guaranteed delivery, for
+ /// devices such as keyboards and mice.
+ Interrupt,
+}
+
+impl From<u8> for TransferType {
+ /// Extracts the transfer type from a raw `bmAttributes`.
+ ///
+ /// Bits 1:0 of the endpoint attributes hold the transfer type (`USB_ENDPOINT_XFERTYPE_MASK`)
+ /// and all four encodings are defined, so any `u8` is a valid input. The remaining bits,
+ /// which further describe isochronous endpoints, are ignored.
+ fn from(value: u8) -> Self {
+ match value & 0b11 {
+ 0 => TransferType::Control,
+ 1 => TransferType::Isochronous,
+ 2 => TransferType::Bulk,
+ _ => TransferType::Interrupt,
+ }
+ }
+}
+
+impl<Dir: EndpointDirection, Type: EndpointTransferType> HostEndpoint<Dir, Type> {
+ /// Returns a raw pointer to the underlying `struct usb_host_endpoint`.
+ ///
+ /// By the type invariants the pointer is non-null and points at an initialised endpoint for
+ /// at least the lifetime of `&self`.
+ #[inline]
+ fn as_raw(&self) -> *mut bindings::usb_host_endpoint {
+ self.0.get()
+ }
+
+ /// Returns the endpoint number, i.e. bits 3:0 of `bEndpointAddress`.
+ ///
+ /// The number alone does not identify an endpoint: an IN and an OUT endpoint of the same
+ /// interface may share one, so pair it with [`direction()`](Self::direction), or use
+ /// [`address()`](Self::address), which combines the two.
+ pub fn number(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bEndpointAddress & 0x0f }
+ }
+
+ /// Returns the full `bEndpointAddress` of the endpoint descriptor.
+ ///
+ /// The byte packs the endpoint number in bits 3:0 and the direction in bit 7 (set for IN,
+ /// clear for OUT); bits 6:4 are reserved and zero. Taken together those fields uniquely
+ /// identify the endpoint within its configuration, which is why this is the value host-side
+ /// APIs use to name an endpoint - for example when constructing a URB pipe.
+ ///
+ /// For a control endpoint bit 7 is defined to be ignored, so the byte should not be read as a
+ /// direction there; see [`Bidirectional`].
+ ///
+ /// Use [`number()`](Self::number) or [`direction()`](Self::direction) to get the fields
+ /// individually.
+ pub fn address(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bEndpointAddress }
+ }
+
+ /// Returns the direction data flows in over this endpoint.
+ ///
+ /// Meaningless for control endpoints, which are bidirectional; what it reports for one is
+ /// whatever bit 7 of the address byte happens to hold, which the specification leaves
+ /// undefined.
+ pub fn direction(&self) -> Direction {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bEndpointAddress.into() }
+ }
+
+ /// Returns the transfer type this endpoint uses.
+ pub fn transfer_type(&self) -> TransferType {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bmAttributes.into() }
+ }
+
+ /// Returns the maximum payload size of a single transaction, in bytes.
+ ///
+ /// This is bits 10:0 of `wMaxPacketSize`. The permitted values depend on the transfer type
+ /// and the speed the device is operating at; for high-speed isochronous and interrupt
+ /// endpoints the total payload per microframe is this value multiplied by the number of
+ /// transactions per microframe, which [`max_packet_mult()`](Self::max_packet_mult)
+ /// describes.
+ pub fn max_packet_size(&self) -> u16 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { u16::from_le((*self.as_raw()).desc.wMaxPacketSize) & 0x07ff }
+ }
+
+ /// Returns the number of *additional* transaction opportunities per microframe.
+ ///
+ /// This is bits 12:11 of `wMaxPacketSize`, so the total number of transactions per microframe
+ /// is one more than the returned value. The field is only defined for high-speed isochronous
+ /// and interrupt endpoints and must not be used for anything else.
+ ///
+ /// The specification defines the encodings `0..=2`; `3` is reserved, so a conforming device
+ /// never reports it, but a malformed descriptor can and this returns it unchanged.
+ pub fn max_packet_multiplier(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (u16::from_le((*self.as_raw()).desc.wMaxPacketSize >> 11) & 0b11) as u8 + 1 }
+ }
+}
+
+impl HostEndpoint<Generic, Generic> {
+ /// Refines this endpoint into a control endpoint, if it is one.
+ ///
+ /// Returns [`None`] if [`transfer_type()`](Self::transfer_type) is not
+ /// [`TransferType::Control`].
+ ///
+ /// The result is [`Bidirectional`], not the direction the descriptor happens to record: bit 7
+ /// of a control endpoint's address is defined to be ignored, so there is nothing to preserve.
+ /// That is also why this is only available on a fully generic endpoint - refining direction
+ /// first and transfer type second would otherwise produce an `Endpoint<In, Control>`, a
+ /// combination that has no meaning.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use kernel::usb::{
+ /// Hostendpoint::{Bidirectional, Control, Endpoint},
+ /// AlternateSetting,
+ /// };
+ ///
+ /// /// Finds an interface's own control endpoint, if it declares one.
+ /// ///
+ /// /// This never finds endpoint 0, which no interface descriptor lists; reach that one
+ /// /// through `Device::control_endpoint()` instead.
+ /// fn extra_control(alt: &AlternateSetting) -> Option<&Endpoint<Bidirectional, Control>> {
+ /// alt.endpoints().iter().find_map(|ep| ep.as_control())
+ /// }
+ /// ```
+ pub fn as_control(&self) -> Option<&HostEndpoint<Bidirectional, Control>> {
+ matches!(self.transfer_type(), TransferType::Control).then(|| {
+ // SAFETY: The transfer type was just checked, so the [`Control`] invariant holds, and
+ // [`Bidirectional`] is exactly what a control endpoint warrants.
+ // `Endpoint<Bidirectional, Control>` is a `#[repr(transparent)]` wrapper around the
+ // same `struct usb_host_endpoint` and differs only in its `PhantomData` markers, which
+ // are 1-ZSTs, so the two types have identical layout and the reference stays valid for
+ // the same lifetime.
+ unsafe { &*core::ptr::from_ref(self).cast() }
+ })
+ }
+}
+
+impl<Type: EndpointTransferType> HostEndpoint<Generic, Type> {
+ /// Refines this endpoint into a device-to-host endpoint, if it is one.
+ ///
+ /// Returns [`None`] if [`direction()`](Self::direction) is not [`Direction::In`]. The known
+ /// transfer type, if any, is preserved.
+ pub fn as_in(&self) -> Option<&HostEndpoint<In, Type>> {
+ if self.direction() == Direction::In {
+ // SAFETY: The direction was just checked, so the [`In`] invariant holds.
+ // `Endpoint<In, Type>` is a `#[repr(transparent)]` wrapper around the same
+ // `struct usb_host_endpoint` and differs only in its `PhantomData` markers, which
+ // are 1-ZSTs, so the two types have identical layout and the reference stays valid
+ // for the same lifetime.
+ unsafe { Some(&*core::ptr::from_ref(self).cast()) }
+ } else {
+ None
+ }
+ }
+
+ /// Refines this endpoint into a host-to-device endpoint, if it is one.
+ ///
+ /// Returns [`None`] if [`direction()`](Self::direction) is not [`Direction::Out`]. The known
+ /// transfer type, if any, is preserved.
+ pub fn as_out(&self) -> Option<&HostEndpoint<Out, Type>> {
+ if self.direction() == Direction::Out {
+ // SAFETY: The direction was just checked, so the [`Out`] invariant holds.
+ // `Endpoint<Out, Type>` is a `#[repr(transparent)]` wrapper around the same
+ // `struct usb_host_endpoint` and differs only in its `PhantomData` markers, which
+ // are 1-ZSTs, so the two types have identical layout and the reference stays valid
+ // for the same lifetime.
+ unsafe { Some(&*core::ptr::from_ref(self).cast()) }
+ } else {
+ None
+ }
+ }
+}
+
+impl<Dir: EndpointDirection> HostEndpoint<Dir, Interrupt> {
+ /// Returns the raw `bInterval` value of the endpoint descriptor.
+ ///
+ /// How this encodes a service interval depends on the transfer type and the device's
+ /// operating speed:
+ ///
+ /// - Full-/low-speed interrupt endpoints: the interval in frames (1 ms), `1..=255`.
+ /// - High-speed interrupt endpoints and all isochronous endpoints: an exponent, giving an
+ /// interval of `2^(bInterval - 1)` microframes (125 micros), with `bInterval` in `1..=16`.
+ pub fn interval(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bInterval }
+ }
+}
+
+impl HostEndpoint<Bidirectional, Control> {
+ /// Returns the raw `bInterval` value of the endpoint descriptor.
+ ///
+ /// This applies only to high-speed bulk/control OUT endpoints and
+ /// represents the maximum NAK rate, or zero for no limit.
+ ///
+ /// Control endpoints do are Bidirectional, they can be used
+ /// for IN and OUT.
+ pub fn max_nak_rate(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bInterval }
+ }
+}
+
+impl HostEndpoint<Out, Bulk> {
+ /// Returns the raw `bInterval` value of the endpoint descriptor.
+ ///
+ /// This applies only to high-speed bulk/control OUT endpoints and
+ /// represents the maximum NAK rate, or zero for no limit.
+ pub fn max_nak_rate(&self) -> u8 {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid
+ // `struct usb_host_endpoint` with an initialised `desc`.
+ unsafe { (*self.as_raw()).desc.bInterval }
+ }
+}
+
+impl<Ctx: device::DeviceContext> Device<Ctx> {
+ /// Returns the default control endpoint (endpoint 0) of this device.
+ ///
+ /// Every USB device has exactly one, and it is the endpoint all enumeration and standard
+ /// requests travel over. Unlike the endpoints of an
+ /// [`AlternateSetting`](crate::usb::AlternateSetting), it is not described by any
+ /// descriptor the device sends: the USB core synthesizes its descriptor
+ /// in `usb_alloc_dev()` and fills in the packet size from `bMaxPacketSize0` of the device
+ /// descriptor during enumeration. It therefore cannot be found by searching
+ /// [`AlternateSetting::endpoints()`](crate::usb::AlternateSetting::endpoints), and
+ /// this accessor is the only way to reach it.
+ ///
+ /// Neither marker needs a run-time check. [`Control`] holds because the core sets
+ /// `bmAttributes` to `USB_ENDPOINT_XFER_CONTROL` itself and the specification fixes endpoint 0
+ /// as a control endpoint; [`Bidirectional`] holds because control endpoints have no direction.
+ /// Endpoint 0's address byte is `0x00`, so [`direction()`](HostEndpoint::direction)
+ /// would report [`Direction::Out`] - a meaningless answer, which is why the direction
+ /// refinements are not available on the returned type. Route control transfers
+ /// on `bmRequestType` instead.
+ ///
+ /// No device-state bound is required: a `struct usb_device` has a valid `ep0` from allocation
+ /// onwards, so this is available wherever a [`Device`] is.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use kernel::usb::Device;
+ ///
+ /// fn ep0_packet_size(dev: &Device) -> u16 {
+ /// dev.control_endpoint().max_packet_size()
+ /// }
+ /// ```
+ pub fn control_endpoint(&self) -> &HostEndpoint<Bidirectional, Control> {
+ // SAFETY: By the type invariants, `self.as_raw()` points at a valid `struct usb_device`.
+ // `ep0` is an embedded field rather than a pointer, so it is live for as long as the
+ // device is, and `usb_alloc_dev()` has initialised its descriptor.
+ let ep0 = unsafe { core::ptr::addr_of!((*self.as_raw()).ep0) };
+
+ // SAFETY: `HostEndpoint` is a `#[repr(transparent)]` wrapper around
+ // `Opaque<bindings::usb_host_endpoint>`, which is layout-compatible with
+ // `struct usb_host_endpoint`, so the cast preserves size and alignment. The [`Control`]
+ // invariant holds because the core fixes `ep0.desc.bmAttributes` to
+ // `USB_ENDPOINT_XFER_CONTROL`, and [`Bidirectional`] holds for any control endpoint. The
+ // `Opaque` accounts for the C side mutating the endpoint behind this shared reference, and
+ // the borrow lasts no longer than `&self`.
+ unsafe { &*ep0.cast() }
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH RFC v2 2/4] rust: usb: add control message send and receive
2026-08-11 9:42 [PATCH RFC v2 0/4] rust: usb: abstractions towards the port of usbsevseg.c to Rust Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 1/4] rust: usb: add endpoint abstraction Alexandru Radovici
@ 2026-08-11 9:42 ` Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 3/4] rust: sysfs: add abstractions for device attributes Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 4/4] rust: usb: allow drivers to expose sysfs attributes Alexandru Radovici
3 siblings, 0 replies; 5+ messages in thread
From: Alexandru Radovici @ 2026-08-11 9:42 UTC (permalink / raw)
To: Greg Kroah-Hartman, 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, Rafael J. Wysocki
Cc: linux-kernel, linux-usb, rust-for-linux, driver-core,
Alexandru Radovici
Add `Device::control_message_send()` and
`Device::control_message_receive()`, wrapping usb_control_msg_send()
and usb_control_msg_recv(), plus a `Request` type describing the
bmRequestType, bRequest, wValue and wIndex fields of a setup packet.
`Request` deliberately omits the two setup packet fields that are
properties of a submission rather than of the request itself: the
direction bit of bmRequestType and wLength. Both are derived from the
method called and the buffer passed, so a caller cannot describe an
inbound transfer while handing over a read-only buffer, nor set a
length that disagrees with one.
Both methods take a `&HostEndpoint<Bidirectional, Control>`, so a
non-control endpoint cannot be passed by construction.
Signed-off-by: Alexandru Radovici <alexandru.radovici@wyliodrin.com>
---
rust/kernel/usb.rs | 1 +
rust/kernel/usb/control.rs | 367 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 368 insertions(+)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 4f5f1db3e7ca..6670fa2ff377 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -33,6 +33,7 @@
slice, //
};
+pub mod control;
pub mod endpoint;
/// An adapter for the registration of USB drivers.
diff --git a/rust/kernel/usb/control.rs b/rust/kernel/usb/control.rs
new file mode 100644
index 000000000000..b9fafb31d479
--- /dev/null
+++ b/rust/kernel/usb/control.rs
@@ -0,0 +1,367 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (C) 2026 Wyliodrin SRL.
+
+//! USB control transfers.
+//!
+//! Control transfers are the request/response mechanism every USB device must support. A transfer
+//! consists of an 8-byte setup packet, an optional data stage, and a status stage;
+//! the setup packet is described by [`Request`], and the direction of the data stage is chosen by
+//! calling either [`Device::control_message_send()`] or [`Device::control_message_receive()`].
+//!
+//! Both travel over a control endpoint - usually the device's default one, from
+//! [`Device::control_endpoint()`]. Because control endpoints are bidirectional, the endpoint does
+//! not determine the direction: the `Direction` bit of `bmRequestType` does, and these two methods
+//! set it so it cannot disagree with the buffer you passed.
+
+use core::{ptr, time::Duration};
+
+use ffi::c_void;
+
+use crate::{
+ alloc::Flags,
+ bindings,
+ error::{
+ code::{
+ EINVAL,
+ EOVERFLOW, //
+ },
+ Error,
+ Result, //
+ },
+ num::Bounded,
+ usb::{
+ endpoint::{
+ Bidirectional,
+ Control,
+ Direction,
+ HostEndpoint, //
+ },
+ Device,
+ },
+};
+
+/// Position of the `Type` field within `bmRequestType` (bits 6:5).
+const REQUEST_TYPE_SHIFT: u32 = 5;
+
+/// Which part of the USB specification defines the meaning of a [`Request`].
+///
+/// This is the `Type` field of `bmRequestType`, occupying bits 6:5 of the setup packet's first
+/// byte. It selects the namespace that [`Request::request`] is interpreted in, so the same request
+/// code means different things under different types.
+#[derive(Copy, Clone, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum RequestType {
+ /// Request is a USB standard request, defined by the specification itself and interpreted the
+ /// same way by every device. Usually handled by the USB core rather than by a driver; see
+ /// [`Device`].
+ Standard = 0,
+ /// Request is intended for a USB class.
+ Class = 1,
+ /// Request is vendor-specific.
+ Vendor = 2,
+ /// Reserved.
+ Reserved = 3,
+}
+
+/// The target a [`Request`] is addressed to.
+///
+/// This is the `Recipient` field of `bmRequestType`, occupying bits 4:0 of the setup
+/// packet's first byte. For [`INTERFACE`](Self::INTERFACE) and [`ENDPOINT`](Self::ENDPOINT)
+/// the specific interface or endpoint is named by [`Request::index`]; the other recipients
+/// ignore it or give it a request-specific meaning.
+///
+/// Although the field is five bits wide, only the values below are defined - hence
+/// the bound on the wrapped [`Bounded`].
+#[derive(Copy, Clone, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct Recipient(Bounded<u8, 5>);
+
+impl Recipient {
+ /// The device as a whole.
+ pub const DEVICE: Self = Self(Bounded::<u8, 5>::new::<0u8>());
+ /// A specific interface, named by [`Request::index`].
+ pub const INTERFACE: Self = Self(Bounded::<u8, 5>::new::<1u8>());
+ /// A specific endpoint, named by [`Request::index`].
+ pub const ENDPOINT: Self = Self(Bounded::<u8, 5>::new::<2u8>());
+ /// Some other target, defined by the request itself.
+ pub const OTHER: Self = Self(Bounded::<u8, 5>::new::<3u8>());
+ /// A port. Wireless USB only.
+ pub const PORT: Self = Self(Bounded::<u8, 5>::new::<4u8>());
+ /// An RPipe. Wireless USB only.
+ pub const RPIPE: Self = Self(Bounded::<u8, 5>::new::<5u8>());
+
+ /// Builds a recipient from a raw field value.
+ ///
+ /// Prefer the associated constants; this exists for values a future specification revision may
+ /// define. The [`Bounded`] parameter rules out values the field cannot hold, but does not
+ /// guarantee the device understands the one you pass.
+ pub fn new(val: Bounded<u8, 5>) -> Recipient {
+ Recipient(val)
+ }
+}
+
+/// The setup packet of a control transfer, minus the direction and length.
+///
+/// These fields map onto the setup packet as `bmRequestType` (from
+/// [`request_type`](Self::request_type) and [`recipient`](Self::recipient)), `bRequest`, `wValue`
+/// and `wIndex`. The remaining two - the `Direction` bit of `bmRequestType` and `wLength` - are
+/// filled in by [`Device::control_message_send()`] and [`Device::control_message_receive()`] from
+/// the method you call and the buffer you hand it, which is what keeps them consistent with each
+/// other.
+pub struct Request {
+ /// Type of the request.
+ pub request_type: RequestType,
+ /// Recipient of the request.
+ pub recipient: Recipient,
+ /// Request code. The meaning of the value depends on the previous fields.
+ pub request: u8,
+ /// Request value. The meaning of the value depends on the previous fields.
+ pub value: u16,
+ /// Request index. The meaning of the value depends on the previous fields.
+ pub index: u16,
+}
+
+impl Request {
+ /// Encodes this request into a `bmRequestType` byte for a data stage flowing in `direction`.
+ ///
+ /// `direction` here is a property of the transfer, not of the endpoint it runs over: bit 7 of
+ /// `bmRequestType` is what the host controller obeys for a control transfer, and bit 7 of the
+ /// endpoint address is defined to be ignored.
+ fn bm_request_type(&self, direction: Direction) -> u8 {
+ let dir = match direction {
+ // `USB_DIR_IN` is `0x80`; `USB_DIR_OUT` is zero.
+ Direction::In => bindings::USB_DIR_IN as u8,
+ Direction::Out => 0,
+ };
+
+ dir | ((self.request_type as u8) << REQUEST_TYPE_SHIFT) | self.recipient.0.get()
+ }
+}
+
+/// Converts a buffer length into a `wLength` value.
+///
+/// The field is 16 bits, so anything larger cannot be expressed in a single control transfer.
+#[inline]
+fn transfer_length(len: usize) -> Result<u16> {
+ u16::try_from(len).map_err(|_| EOVERFLOW)
+}
+
+impl Device {
+ /// Performs a control transfer with an outbound data stage, i.e. host to device.
+ ///
+ /// The `Direction` bit of `bmRequestType` is set to `USB_DIR_OUT` for you. Pass [`None`] as
+ /// `data` for a request with no data stage at all, which sets `wLength` to zero.
+ ///
+ /// `endpoint` selects the control endpoint to use; it must belong to this device, since only
+ /// its [`number()`](Endpoint::number) is taken from it. For the default control endpoint -
+ /// almost always the right choice - pass [`control_endpoint()`](Device::control_endpoint).
+ ///
+ /// `timeout` with zero meaning `USB_MAX_SYNCHRONOUS_TIMEOUT` of 60s.
+ ///
+ /// `memflags` are used for an
+ /// internal copy of `data`, so `data` itself need not be DMA-capable.
+ ///
+ /// Returns `Ok(())` only if the whole request completed; unlike `usb_control_msg()` there
+ /// is no partial-success case to inspect.
+ ///
+ /// This sleeps, so it must not be called from atomic context.
+ ///
+ /// # Examples
+ ///
+ /// A vendor-specific request with no data stage:
+ ///
+ /// ```
+ /// use kernel::{
+ /// alloc::flags::GFP_KERNEL,
+ /// error::Result,
+ /// usb::{
+ /// transfer::{Recipient, Request, RequestType},
+ /// Device,
+ /// },
+ /// };
+ ///
+ /// fn set_led(dev: &Device, on: bool) -> Result {
+ /// dev.control_message_send(
+ /// dev.control_endpoint(),
+ /// Request {
+ /// request_type: RequestType::Vendor,
+ /// recipient: Recipient::DEVICE,
+ /// request: 0x01,
+ /// value: on as u16,
+ /// index: 0,
+ /// },
+ /// None,
+ /// 1000,
+ /// GFP_KERNEL,
+ /// )
+ /// }
+ /// ```
+ ///
+ /// A class request that carries a payload:
+ ///
+ /// ```
+ /// use kernel::{
+ /// alloc::flags::GFP_KERNEL,
+ /// error::Result,
+ /// usb::{
+ /// transfer::{Recipient, Request, RequestType},
+ /// Device,
+ /// },
+ /// };
+ ///
+ /// fn set_line_coding(dev: &Device, interface: u8, coding: &[u8; 7]) -> Result {
+ /// dev.control_message_send(
+ /// dev.control_endpoint(),
+ /// Request {
+ /// request_type: RequestType::Class,
+ /// recipient: Recipient::INTERFACE,
+ /// request: 0x20,
+ /// value: 0,
+ /// index: interface as u16,
+ /// },
+ /// Duration::from_millis(1000),
+ /// 1000,
+ /// GFP_KERNEL,
+ /// )
+ /// }
+ /// ```
+ pub fn control_message_send(
+ &self,
+ endpoint: &HostEndpoint<Bidirectional, Control>,
+ request: Request,
+ data: Option<&[u8]>,
+ timeout: Duration,
+ memflags: Flags,
+ ) -> Result {
+ let (data, size) = match data {
+ Some(bytes) => (
+ bytes.as_ptr().cast::<c_void>(),
+ transfer_length(bytes.len())?,
+ ),
+ None => (ptr::null(), 0),
+ };
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariants of
+ // [`Device`], and `endpoint` belongs to it, so its number names a control endpoint of this
+ // device. `data` is either null with `size` zero, or points at `size` initialised bytes
+ // that outlive the call - the callee only reads from it, and copies before submitting, so
+ // no DMA is performed on the caller's buffer.
+ let ret = unsafe {
+ bindings::usb_control_msg_send(
+ self.as_raw(),
+ endpoint.number(),
+ request.request,
+ request.bm_request_type(Direction::Out),
+ request.value,
+ request.index,
+ data,
+ size,
+ timeout.as_millis().try_into().map_err(|_| EINVAL)?,
+ memflags.as_raw(),
+ )
+ };
+
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Performs a control transfer with an inbound data stage, i.e. device to host.
+ ///
+ /// The `Direction` bit of `bmRequestType` is set to `USB_DIR_IN` for you. Pass [`None`] as
+ /// `data` for a request with no data stage at all, which sets `wLength` to zero - note that
+ /// this makes the direction bit meaningless, so such a request is indistinguishable from the
+ /// [`control_message_send()`](Device::control_message_send) equivalent.
+ ///
+ /// `endpoint` selects the control endpoint to use; it must belong to this device, since only
+ /// its [`number()`](Endpoint::number) is taken from it. For the default control endpoint -
+ /// almost always the right choice - pass [`control_endpoint()`](Device::control_endpoint).
+ ///
+ /// `timeout` with zero meaning `USB_MAX_SYNCHRONOUS_TIMEOUT` of 60s.
+ ///
+ /// `memflags` are used for an
+ /// internal DMA-capable buffer that is copied into `data` on success, so `data`
+ /// itself need not be DMA-capable.
+ ///
+ /// The transfer must fill `data` exactly. A device that returns fewer bytes than requested
+ /// fails with `EREMOTEIO` and leaves `data` untouched, so this is not the right method for
+ /// requests of variable-length descriptors - size the buffer from the device's own length
+ /// field, or use a lower-level transfer.
+ ///
+ /// This sleeps, so it must not be called from atomic context.
+ ///
+ /// # Examples
+ ///
+ /// Reading a fixed-size vendor-specific register:
+ ///
+ /// ```
+ /// use kernel::{
+ /// alloc::flags::GFP_KERNEL,
+ /// error::Result,
+ /// usb::{
+ /// transfer::{Recipient, Request, RequestType},
+ /// Device,
+ /// },
+ /// };
+ ///
+ /// fn firmware_version(dev: &Device) -> Result<u16> {
+ /// let mut buf = [0u8; 2];
+ ///
+ /// dev.control_message_receive(
+ /// dev.control_endpoint(),
+ /// Request {
+ /// request_type: RequestType::Vendor,
+ /// recipient: Recipient::DEVICE,
+ /// request: 0x02,
+ /// value: 0,
+ /// index: 0,
+ /// },
+ /// &mut buf,
+ /// Duration::from_millis(1000),
+ /// GFP_KERNEL,
+ /// )?;
+ ///
+ /// Ok(u16::from_le_bytes(buf))
+ /// }
+ /// ```
+ pub fn control_message_receive(
+ &self,
+ endpoint: &HostEndpoint<Bidirectional, Control>,
+ request: Request,
+ data: &mut [u8],
+ timeout: Duration,
+ memflags: Flags,
+ ) -> Result {
+ let (data, size) = {
+ let size = transfer_length(data.len())?;
+ (data.as_mut_ptr().cast::<c_void>(), size)
+ };
+
+ // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the type invariants of
+ // [`Device`], and `endpoint` belongs to it, so its number names a control endpoint of this
+ // device. `data` is either null with `size` zero, or points at `size` bytes of a buffer
+ // uniquely borrowed for the duration of the call, which the callee may only write to.
+ let ret = unsafe {
+ bindings::usb_control_msg_recv(
+ self.as_raw(),
+ endpoint.number(),
+ request.request,
+ request.bm_request_type(Direction::In),
+ request.value,
+ request.index,
+ data,
+ size,
+ timeout.as_millis().try_into().map_err(|_| EINVAL)?,
+ memflags.as_raw(),
+ )
+ };
+
+ if ret < 0 {
+ Err(Error::from_errno(ret))
+ } else {
+ Ok(())
+ }
+ }
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH RFC v2 3/4] rust: sysfs: add abstractions for device attributes
2026-08-11 9:42 [PATCH RFC v2 0/4] rust: usb: abstractions towards the port of usbsevseg.c to Rust Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 1/4] rust: usb: add endpoint abstraction Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 2/4] rust: usb: add control message send and receive Alexandru Radovici
@ 2026-08-11 9:42 ` Alexandru Radovici
2026-08-11 9:42 ` [PATCH RFC v2 4/4] rust: usb: allow drivers to expose sysfs attributes Alexandru Radovici
3 siblings, 0 replies; 5+ messages in thread
From: Alexandru Radovici @ 2026-08-11 9:42 UTC (permalink / raw)
To: Greg Kroah-Hartman, 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, Rafael J. Wysocki
Cc: linux-kernel, linux-usb, rust-for-linux, driver-core,
Alexandru Radovici
Add safe abstractions for exposing sysfs attributes on a device.
A driver implements AttributeOperations once per attribute, keyed by a
const ID so that a single type can back several files, and the
attribute_list!() macro assembles the statics the C side requires: one
struct device_attribute per file, a NULL terminated array of struct
attribute pointers, a struct attribute_group, and the NULL terminated
group array that a bus driver stores in its dev_groups field.
The show() and store() trampolines recover the driver private data from
dev->driver_data and pass it to the operations as Pin<&Data>, together
with a bound device. That is only meaningful while a driver is bound, so
the groups must be installed through the driver's dev_groups, which the
driver core creates after a successful probe and removes before remove
runs. Groups reachable earlier, for instance through a device_type,
would expose the files from device_add onwards, when no private data has
been stored yet.
The file mode is derived from which of the two operations the type
implements, so an attribute is never readable without a show() nor
writable without a store().
Only a single unnamed group is supported. Attribute visibility
callbacks and binary attributes are not implemented.
Signed-off-by: Alexandru Radovici <alexandru.radovici@wyliodrin.com>
---
rust/kernel/device.rs | 2 +-
rust/kernel/lib.rs | 2 +
rust/kernel/sysfs.rs | 602 ++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 605 insertions(+), 1 deletion(-)
diff --git a/rust/kernel/device.rs b/rust/kernel/device.rs
index 1a38b3bbdfb7..c42dcc6edc89 100644
--- a/rust/kernel/device.rs
+++ b/rust/kernel/device.rs
@@ -260,7 +260,7 @@ impl Device<Bound> {
/// the device is fully unbound.
/// - The type `T` must match the type of the `ForeignOwnable` previously stored by
/// [`Device::set_drvdata`].
- unsafe fn drvdata_unchecked<T>(&self) -> Pin<&T> {
+ pub(crate) unsafe fn drvdata_unchecked<T>(&self) -> Pin<&T> {
// SAFETY: By the type invariants, `self.as_raw()` is a valid pointer to a `struct device`.
let ptr = unsafe { bindings::dev_get_drvdata(self.as_raw()) };
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 9512af7156df..67f981bb7c70 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -126,6 +126,8 @@
pub mod std_vendor;
pub mod str;
pub mod sync;
+#[cfg(CONFIG_SYSFS)]
+pub mod sysfs;
pub mod task;
pub mod time;
pub mod tracepoint;
diff --git a/rust/kernel/sysfs.rs b/rust/kernel/sysfs.rs
new file mode 100644
index 000000000000..c30b5f6eb02d
--- /dev/null
+++ b/rust/kernel/sysfs.rs
@@ -0,0 +1,602 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Abstractions for sysfs device attributes.
+//!
+//! C header: [`include/linux/sysfs.h`](srctree/include/linux/sysfs.h)
+//! C header: [`include/linux/device.h`](srctree/include/linux/device.h)
+//!
+//! A sysfs attribute is a file in a device's sysfs directory. Reading the file
+//! invokes the attribute's `show` callback, writing it invokes `store`.
+//!
+//! The types here are built to live in `static`s: they are constructed in const
+//! context, handed to the C side as raw pointers, and never moved or mutated
+//! from Rust afterwards. That is what makes the [`Send`]/[`Sync`] impls below
+//! sound, and it is why every constructor is a `const fn`.
+//!
+//! The usual flow is:
+//!
+//! 1. One [`DeviceAttribute`] per file, each parameterised by a distinct `ID`
+//! so that a single type can implement [`AttributeOperations`] several times.
+//! 2. An [`AttributeList`] collecting the raw attribute pointers, NULL
+//! terminated as the C side expects.
+//! 3. An [`AttributeGroup`] wrapping that list, and an [`AttributeGroups`]
+//! wrapping the group, which is what gets stored in `driver->dev_groups`.
+//!
+//! The [`attribute_list!`] macro builds all four layers.
+//!
+//! # Registration window
+//!
+//! Both trampolines recover the driver-private data from `dev->driver_data`,
+//! which is only meaningful while a driver is bound. The groups built here must
+//! therefore be installed through `driver->dev_groups`, which the driver core
+//! creates after a successful `probe` and removes before `remove` runs. Hanging
+//! them off `device_type::groups` or `dev->groups` instead would expose the
+//! files from `device_add` onwards, i.e. before any driver has set the private
+//! data, and the first read would dereference NULL.
+
+use core::{
+ marker::PhantomData,
+ mem::MaybeUninit,
+ pin::Pin,
+ ptr, //
+};
+
+use crate::{
+ bindings,
+ device::{
+ Bound,
+ Device, //
+ },
+ error::Result,
+ ffi::CStr,
+ macros::vtable,
+ page::PAGE_SIZE,
+ types::Opaque, //
+};
+
+/// A single sysfs attribute of a device, i.e. one file in the device's sysfs
+/// directory.
+///
+/// # Type parameters
+///
+/// * `ID` distinguishes attributes backed by the same operations type. Because
+/// the operations are supplied by a trait implemented *on* that type, a device
+/// with several attributes needs several impls, and `ID` is what keeps them
+/// apart.
+/// * `D` is the driver-private data type the trampolines will recover from the
+/// device, tied to `O` through `AttributeOperations::Data`.
+/// * `O` supplies the `show`/`store` implementations.
+///
+/// # Invariants
+///
+/// `attribute` contains an initialised [`bindings::device_attribute`] whose
+/// `show`/`store` function pointers, when non-NULL, are the trampolines
+/// [`Self::show`] and [`Self::store`] of *this* type. It is never mutated after
+/// construction.
+#[repr(transparent)]
+pub struct DeviceAttribute<const ID: u64, D, O: AttributeOperations<ID, Data = D>> {
+ attribute: Opaque<bindings::device_attribute>,
+ _phantom: PhantomData<O>,
+}
+
+impl<const ID: u64, D, O> DeviceAttribute<ID, D, O>
+where
+ O: AttributeOperations<ID, Data = D>,
+{
+ /// Trampoline invoked by the sysfs core when userspace reads the file
+ /// backing this attribute.
+ ///
+ /// Returns the number of bytes written into `page`, or a negative errno.
+ ///
+ /// # Safety
+ ///
+ /// * `dev` must point to a valid `struct device` which stays valid for the
+ /// duration of this call.
+ /// * `dev` must have a driver bound for the duration of this call, and its
+ /// private data must be a live `O::Data` installed by that driver.
+ /// * `page` must point to a buffer that is valid for writes of at least
+ /// [`PAGE_SIZE`] bytes and that is not aliased for the duration of this
+ /// call.
+ /// * `item` must point to the `device_attribute` this trampoline was
+ /// installed on, i.e. the one owned by a `Self`.
+ ///
+ /// The sysfs core upholds the first and third requirements: it passes the
+ /// device the attribute was registered against and a freshly allocated page
+ /// that it hands out to exactly one reader at a time, and kernfs holds an
+ /// active reference across the call so the group cannot be torn down
+ /// underneath it. The second is upheld by registering the group through
+ /// `driver->dev_groups`; see the module docs.
+ unsafe extern "C" fn show(
+ dev: *mut bindings::device,
+ // Unused: the operations are selected statically through `O`, so we
+ // never need to look at the attribute we were called on.
+ _item: *mut bindings::device_attribute,
+ page: *mut kernel::ffi::c_char,
+ ) -> isize {
+ // SAFETY: By the function safety requirements, `dev` points to a valid
+ // `struct device` that outlives this call, and therefore outlives the
+ // reference derived from it, and it has a driver bound for that whole
+ // period, which is what the `Bound` context asserts.
+ let dev = unsafe { Device::<Bound>::from_raw(dev) };
+
+ // SAFETY: By the function safety requirements, the private data of
+ // `dev` is a live `O::Data` installed by the bound driver, so it is
+ // sound to view it as such. The driver core does not clear the private
+ // data until after these files are gone, so the borrow cannot outlive
+ // the allocation.
+ let data = unsafe { dev.drvdata_unchecked::<O::Data>() };
+
+ // SAFETY: By the function safety requirements, `page` is valid for
+ // writes of `PAGE_SIZE` bytes and is not aliased for the duration of
+ // this call, so creating a unique reference to it is sound. `c_char`
+ // and `u8` have the same size and alignment (1), so the cast to
+ // `[u8; PAGE_SIZE]` preserves layout and cannot introduce a misaligned
+ // access.
+ let page = unsafe { &mut *(page.cast::<[u8; PAGE_SIZE]>()) };
+
+ match O::show(data, dev, page) {
+ // Clamped so that a buggy implementation reporting more than a page
+ // cannot be reinterpreted as a negative value, i.e. silently turned
+ // into an errno, by the cast.
+ Ok(size) => size.min(PAGE_SIZE) as isize,
+ Err(err) => err.to_errno() as isize,
+ }
+ }
+
+ /// Trampoline invoked by the sysfs core when userspace writes to the file
+ /// backing this attribute.
+ ///
+ /// Returns the number of bytes consumed, or a negative errno. Returning
+ /// fewer bytes than `size` makes userspace retry with the remainder, so
+ /// implementations that consumed the whole buffer must return `size`.
+ ///
+ /// # Safety
+ ///
+ /// * `dev` must point to a valid `struct device` which stays valid for the
+ /// duration of this call.
+ /// * `dev` must have a driver bound for the duration of this call, and its
+ /// private data must be a live `O::Data` installed by that driver.
+ /// * `page` must point to a buffer that is valid for reads of at least
+ /// `size` bytes and that is not mutated for the duration of this call.
+ /// * `item` must point to the `device_attribute` this trampoline was
+ /// installed on, i.e. the one owned by a `Self`.
+ unsafe extern "C" fn store(
+ dev: *mut bindings::device,
+ // Unused, see `show` above.
+ _item: *mut bindings::device_attribute,
+ page: *const kernel::ffi::c_char,
+ size: usize,
+ ) -> isize {
+ // SAFETY: By the function safety requirements, `dev` points to a valid
+ // `struct device` that outlives the derived reference, and it has a
+ // driver bound for that whole period, which is what the `Bound` context
+ // asserts.
+ let dev = unsafe { Device::<Bound>::from_raw(dev) };
+
+ // SAFETY: By the function safety requirements, the private data of
+ // `dev` is a live `O::Data` installed by the bound driver, so it is
+ // sound to view it as such. See `show` above.
+ let data = unsafe { dev.drvdata_unchecked::<O::Data>() };
+
+ match O::store(
+ data,
+ dev,
+ // SAFETY: By the function safety requirements, `page` is valid for
+ // reads of `size` bytes and is not mutated while this call runs, so
+ // it may be viewed as a shared slice. `c_char` and `u8` share size
+ // and alignment, and `size` bytes cannot exceed `isize::MAX`
+ // because the buffer is a single kernel page.
+ unsafe { core::slice::from_raw_parts(page.cast(), size) },
+ ) {
+ // Clamped so that a buggy implementation reporting more than a page
+ // cannot be reinterpreted as a negative value, i.e. silently turned
+ // into an errno, by the cast.
+ Ok(size) => size.min(PAGE_SIZE) as isize,
+ Err(err) => err.to_errno() as isize,
+ }
+ }
+
+ /// Creates a new attribute, which will appear as a file named `name` in the
+ /// device's sysfs directory.
+ ///
+ /// The file's mode is derived from which of [`AttributeOperations::show`]
+ /// and [`AttributeOperations::store`] `O` actually implements, so an
+ /// attribute is never readable without a `show` nor writable without a
+ /// `store`.
+ ///
+ /// `name` is `'static` because the C side stores the pointer and
+ /// dereferences it for as long as the attribute is registered.
+ pub const fn new(name: &'static CStr) -> Self {
+ Self {
+ attribute: Opaque::new(bindings::device_attribute {
+ attr: bindings::attribute {
+ // The pointer stays valid for as long as the attribute is
+ // registered because `name` is `'static`.
+ name: crate::str::as_char_ptr_in_const_context(name),
+ // `S_IRUSR` (0o400) if readable, `S_IWUSR` (0o200) if
+ // writable. Only the owner gets access; drivers that want
+ // wider permissions need a different constructor.
+ mode: if O::HAS_SHOW { 0o400 } else { 0 }
+ | if O::HAS_STORE { 0o200 } else { 0 },
+ },
+ // Both bindgen anonymous unions hold a single `Option<fn>`
+ // field, so initialising that one field initialises the whole
+ // union; there is no padding to leave uninitialised.
+ __bindgen_anon_1: bindings::device_attribute__bindgen_ty_1 {
+ show: if O::HAS_SHOW { Some(Self::show) } else { None },
+ },
+ __bindgen_anon_2: bindings::device_attribute__bindgen_ty_2 {
+ store: if O::HAS_STORE {
+ Some(Self::store)
+ } else {
+ None
+ },
+ },
+ }),
+ _phantom: PhantomData,
+ }
+ }
+
+ /// Returns a raw pointer to the embedded `struct attribute`, for building an
+ /// [`AttributeList`].
+ ///
+ /// The returned pointer inherits the lifetime of `&self`; callers must not
+ /// hand it to the C side unless `self` lives in a `static`.
+ pub const fn as_raw_attribute(&self) -> *const bindings::attribute {
+ // SAFETY: `Opaque::get` returns a valid pointer to the initialised
+ // `device_attribute` (type invariant), and `attr` is the first field of
+ // that struct, so projecting to it is in bounds. No reference to the
+ // `device_attribute` is created, so concurrent writes from the C side
+ // cannot be aliasing violations.
+ unsafe { (&raw const (*self.attribute.get()).attr) }
+ }
+}
+
+// SAFETY: The only operation on a `DeviceAttribute` is `as_raw_attribute`,
+// which hands out a raw pointer and performs no access. The wrapped
+// `device_attribute` is never mutated from Rust after construction, so there is
+// no Rust-side state that needs synchronisation and `&Self` may be shared
+// across threads.
+unsafe impl<const ID: u64, D, O: AttributeOperations<ID, Data = D>> Sync
+ for DeviceAttribute<ID, D, O>
+{
+}
+
+// SAFETY: A `DeviceAttribute` owns no thread-affine state (it is a plain struct
+// of a name pointer, a mode and two function pointers), so ownership can be
+// transferred to another thread.
+unsafe impl<const ID: u64, D, O: AttributeOperations<ID, Data = D>> Send
+ for DeviceAttribute<ID, D, O>
+{
+}
+
+/// Operations backing a single sysfs attribute.
+///
+/// Implemented on an operations type, once per attribute; the `ID` parameter is
+/// what allows several impls on the same type. The driver's private data is
+/// named separately by [`Self::Data`], so the two may but need not be the same
+/// type. Both methods are optional: [`vtable`] generates the
+/// `HAS_SHOW`/`HAS_STORE` constants that [`DeviceAttribute::new`] uses to decide
+/// which C callbacks and which mode bits to install, so a method that is not
+/// implemented is never called.
+#[vtable]
+pub trait AttributeOperations<const ID: u64> {
+ /// The driver-private data type this attribute belongs to.
+ ///
+ /// Recovered from `dev->driver_data` by the trampolines, so it must be the
+ /// type the bound driver actually stored there.
+ ///
+ /// The `Driver` trait that uses this needs to enforce this data type restriction.
+ type Data: Sync;
+
+ /// Called when userspace reads the attribute's file.
+ ///
+ /// Writes the textual representation of the value into `buf` and returns the
+ /// number of bytes written, which must not exceed [`PAGE_SIZE`]. The
+ /// contents of `buf` on entry are unspecified.
+ fn show(
+ _data: Pin<&Self::Data>,
+ _dev: &Device<Bound>,
+ _buf: &mut [u8; PAGE_SIZE],
+ ) -> Result<usize> {
+ // Unreachable: `HAS_SHOW` is `false` for this impl, so
+ // `DeviceAttribute::new` leaves the C `show` pointer NULL. Reaching
+ // here means the vtable was built inconsistently, hence a build error
+ // rather than a runtime one.
+ kernel::build_error!(kernel::error::VTABLE_DEFAULT_ERROR)
+ }
+
+ /// Called when userspace writes to the attribute's file.
+ ///
+ /// `buf` holds the bytes written by userspace and is not NUL terminated.
+ /// Returns the number of bytes consumed; return `buf.len()` to signal that
+ /// the whole write was accepted.
+ fn store(_data: Pin<&Self::Data>, _dev: &Device<Bound>, _buf: &[u8]) -> Result<usize> {
+ // Unreachable: `HAS_STORE` is `false` for this impl, so
+ // `DeviceAttribute::new` leaves the C `store` pointer NULL. Reaching
+ // here means the vtable was built inconsistently, hence a build error
+ // rather than a runtime one.
+ kernel::build_error!(kernel::error::VTABLE_DEFAULT_ERROR)
+ }
+}
+
+/// A NULL terminated array of `N` attribute pointers, as consumed by
+/// `struct attribute_group::attrs`.
+///
+/// `N` counts the terminator, so a group of two attributes uses
+/// `AttributeList<3, _>`. `D` records the private data type every attribute in
+/// the list expects, which is what stops a list built for one driver from being
+/// wrapped in a group and attached to another.
+///
+/// # Invariants
+///
+/// The last element is NULL. Every other element points to a
+/// [`bindings::attribute`] that is valid for as long as this list is reachable
+/// from the C side, and belongs to a `DeviceAttribute` whose `Data` is `D`.
+#[repr(transparent)]
+pub struct AttributeList<const N: usize, D>([*const bindings::attribute; N], PhantomData<D>);
+
+impl<const N: usize, D> AttributeList<N, D> {
+ /// Creates a new attribute list from raw attribute pointers.
+ ///
+ /// # Safety
+ ///
+ /// * `list` must be NULL terminated, i.e. `list[N - 1]` must be NULL.
+ /// * Every other element must point to a [`bindings::attribute`] that stays
+ /// valid for as long as the list is registered. In practice each must
+ /// come from [`DeviceAttribute::as_raw_attribute`] on a `static`.
+ /// * Each of those attributes must belong to a `DeviceAttribute` whose
+ /// `Data` is `D`, since its trampoline will read the device's private data
+ /// as a `D`.
+ pub const unsafe fn new(list: [*const bindings::attribute; N]) -> Self {
+ // INVARIANT: The safety requirements guarantee NULL termination, the
+ // validity of every other element, and that each belongs to a
+ // `DeviceAttribute` over `D`.
+ Self(list, PhantomData)
+ }
+}
+
+// SAFETY: An `AttributeList` is an immutable array of raw pointers with no
+// interior mutability and no operations beyond construction, so sharing `&Self`
+// across threads cannot cause a data race. The pointees are `DeviceAttribute`s,
+// which are themselves `Sync`.
+unsafe impl<const N: usize, D> Sync for AttributeList<N, D> {}
+
+// SAFETY: An `AttributeList` owns no thread-affine state, so ownership can be
+// transferred between threads.
+unsafe impl<const N: usize, D> Send for AttributeList<N, D> {}
+
+/// A group of sysfs attributes, i.e. `struct attribute_group`.
+///
+/// A group with no name (as built below) places its attributes directly in the
+/// device's sysfs directory; a named group would place them in a subdirectory.
+/// `D` is carried over from the list so the private data type stays visible up
+/// to the point of registration.
+///
+/// # Invariants
+///
+/// The wrapped `attribute_group` is initialised, its `attrs_const` field points
+/// to a NULL terminated attribute array that outlives it, and all other fields
+/// are zero.
+#[repr(transparent)]
+pub struct AttributeGroup<D>(Opaque<bindings::attribute_group>, PhantomData<D>);
+
+impl<D> AttributeGroup<D> {
+ /// Creates an unnamed group containing every attribute in `attributes`.
+ pub const fn from_attribute_list<const N: usize>(
+ attributes: &'static AttributeList<N, D>,
+ ) -> Self {
+ // INVARIANT: `attributes` is `'static`, so the array outlives the
+ // group, and it is NULL terminated by `AttributeList`'s invariant.
+ AttributeGroup(
+ Opaque::new(bindings::attribute_group {
+ __bindgen_anon_2: bindings::attribute_group__bindgen_ty_2 {
+ // `AttributeList` is `repr(transparent)` over
+ // `[*const attribute; N]`, so a pointer to the list is a
+ // pointer to its first element, i.e. a
+ // `*const *const attribute`.
+ attrs_const: ptr::from_ref(attributes).cast(),
+ },
+ // SAFETY: Every remaining field of `attribute_group` is a
+ // pointer, an `Option<fn>`, a `umode_t` or a union of those, and
+ // the all-zero bit pattern is valid for each of them: NULL name,
+ // no `is_visible`/`is_bin_visible` callbacks, no bin attributes.
+ // This is also what the C side expects from a statically
+ // declared group.
+ ..unsafe { MaybeUninit::zeroed().assume_init() }
+ }),
+ PhantomData,
+ )
+ }
+}
+
+// SAFETY: An `AttributeGroup` is never mutated from Rust after construction and
+// exposes no operations, so there is no Rust-side state requiring
+// synchronisation; the C side does its own locking on the group.
+unsafe impl<D> Sync for AttributeGroup<D> {}
+
+// SAFETY: An `AttributeGroup` owns no thread-affine state, so ownership can be
+// transferred between threads.
+unsafe impl<D> Send for AttributeGroup<D> {}
+
+/// A NULL terminated array of attribute group pointers, as consumed by
+/// `struct driver::dev_groups`.
+///
+/// Only a single group is supported; the array is fixed at two entries, the
+/// group and the terminator.
+///
+/// # Invariants
+///
+/// Element 0 points to a valid [`bindings::attribute_group`] that outlives this
+/// value, and element 1 is NULL.
+#[repr(transparent)]
+pub struct AttributeGroups<D>([*const bindings::attribute_group; 2], PhantomData<D>);
+
+impl<D> AttributeGroups<D> {
+ /// Creates a group array holding the single group `attribute_group`.
+ pub const fn new(attribute_group: &'static AttributeGroup<D>) -> Self {
+ // INVARIANT: `attribute_group` is a `'static` reference, so the pointer
+ // stays valid forever, and the second element is the NULL terminator.
+ //
+ // The cast is layout-preserving: `AttributeGroup` is
+ // `repr(transparent)` over `Opaque<attribute_group>` plus a zero-sized
+ // `PhantomData`, and `Opaque` is itself `repr(transparent)` over the
+ // underlying `attribute_group`.
+ Self(
+ [ptr::from_ref(attribute_group).cast(), ptr::null()],
+ PhantomData,
+ )
+ }
+
+ /// Returns a pointer to the group array, for storing in `dev_groups`.
+ ///
+ /// Only `driver->dev_groups` is a valid destination; see the module docs for
+ /// why the earlier-created group fields are not.
+ pub(crate) fn as_ptr(&self) -> *mut *const bindings::attribute_group {
+ // This should be `*const *const attribute_group`, but the kernel's
+ // `dev_groups` field requires `*mut *const attribute_group`, not sure
+ // why.
+ //
+ // The C side only reads through the pointer, so handing out a `*mut`
+ // derived from `&self` is fine as long as callers never write through
+ // it. That is why this is `pub(crate)` rather than `pub`.
+ self.0.as_ptr().cast_mut()
+ }
+}
+
+// SAFETY: An `AttributeGroups` is an immutable array of raw pointers with no
+// operations that access the pointees, so `&Self` may be shared across threads.
+// The groups it points to are themselves `Sync`.
+unsafe impl<D> Sync for AttributeGroups<D> {}
+
+// SAFETY: An `AttributeGroups` owns no thread-affine state, so ownership can be
+// transferred between threads.
+unsafe impl<D> Send for AttributeGroups<D> {}
+
+/// Builds the full attribute plumbing for a driver and evaluates to a
+/// `&'static AttributeGroups` suitable for `driver->dev_groups`.
+///
+/// All three arguments are labelled. `data:` is the driver-private data type the
+/// trampolines will recover from the device, `ops:` is the type implementing the
+/// attribute operations, and `attributes:` is a comma separated list of
+/// identifiers, one per sysfs file. Each identifier must name a `u64` constant,
+/// which is used as the `ID` type parameter, and `ops:` must implement
+/// `AttributeOperations<ID, Data = $data>` for each of them. The sysfs file name
+/// is the lowercased identifier.
+///
+/// Everything the macro creates is a `static`, so the pointers handed to C
+/// remain valid for the lifetime of the module.
+///
+/// # Examples
+///
+/// ```ignore
+/// const POWER: u64 = 0;
+/// const MODE: u64 = 1;
+///
+/// #[vtable]
+/// impl AttributeOperations<POWER> for SampleDriver {
+/// type Data = Self;
+///
+/// fn show(
+/// _data: Pin<&SampleDriver>,
+/// _dev: &Device<Bound>,
+/// buf: &mut [u8; PAGE_SIZE],
+/// ) -> Result<usize> {
+/// buf[0] = b'h';
+/// Ok(1)
+/// }
+///
+/// fn store(
+/// _data: Pin<&SampleDriver>,
+/// _dev: &Device<Bound>,
+/// buf: &[u8; PAGE_SIZE],
+/// ) -> Result<usize> {
+/// // use the data stored in buf
+/// Ok(buf.len())
+/// }
+/// }
+///
+/// #[vtable]
+/// impl AttributeOperations<MODE> for SampleDriver {
+/// type Data = Self;
+///
+/// fn show(
+/// _data: Pin<&SampleDriver>,
+/// _dev: &Device<Bound>,
+/// buf: &mut [u8; PAGE_SIZE],
+/// ) -> Result<usize> {
+/// buf[0] = b'1';
+/// Ok(1)
+/// }
+/// }
+///
+/// let groups = kernel::attribute_list!(
+/// data: SampleDriver,
+/// ops: SampleDriver,
+/// attributes: POWER, MODE,
+/// );
+/// ```
+#[macro_export]
+macro_rules! attribute_list {
+ (
+ data: $data: ty,
+ ops: $ops: ty,
+ attributes: $($attributes: ident),+
+ $(,)?
+ ) => {
+ {
+ use $crate::{
+ c_str,
+ sysfs::{
+ AttributeGroup,
+ AttributeGroups,
+ AttributeList,
+ DeviceAttribute,
+ }
+ };
+
+ use core::ptr;
+
+ // One `static` per attribute, named `<IDENT>_ATTR`. It must be a
+ // `static` (not a `let`) so that the pointer taken below outlives
+ // this block and can be handed to the C side.
+ $(
+ $crate::macros::paste!{
+ static [< $attributes:upper _ATTR >]: DeviceAttribute<$attributes, $data, $ops>
+ = DeviceAttribute::new(c_str!(stringify!([< $attributes:lower >])));
+ }
+ )+
+
+ // One slot per attribute plus one for the NULL terminator. The
+ // `let _ = $attributes;` is just a way to expand each repetition to
+ // `+ 1` while keeping the metavariable in the expansion.
+ const LEN: usize = 1 $( + { let _ = $attributes; 1})+;
+
+ // SAFETY: The array holds exactly one pointer per attribute,
+ // obtained from `as_raw_attribute` on a `static` (so valid for
+ // 'static), followed by the required NULL terminator. Every
+ // attribute was declared above with `$ops` as its operations type,
+ // whose `Data` is `$data`, which matches the list's `D`.
+ static ATTRIBUTE_LIST: AttributeList::<LEN, $data> = unsafe { AttributeList::new([
+ $(
+ $crate::macros::paste!{
+ [< $attributes:upper _ATTR >].as_raw_attribute()
+ },
+ )*
+ ptr::null()
+ ]
+ ) };
+
+ // Wrap the list in a group, and the group in the NULL terminated
+ // group array that `dev_groups` expects.
+ static ATTRIBUTE_GROUP: AttributeGroup<$data>
+ = AttributeGroup::from_attribute_list(&ATTRIBUTE_LIST);
+ static ATTRIBUTE_GROUPS: AttributeGroups<$data>
+ = AttributeGroups::new(&ATTRIBUTE_GROUP);
+
+ // Evaluates to a `&'static AttributeGroups`; `ATTRIBUTE_GROUPS` is
+ // a `static`, so the borrow outlives the block.
+ &ATTRIBUTE_GROUPS
+ }
+ };
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH RFC v2 4/4] rust: usb: allow drivers to expose sysfs attributes
2026-08-11 9:42 [PATCH RFC v2 0/4] rust: usb: abstractions towards the port of usbsevseg.c to Rust Alexandru Radovici
` (2 preceding siblings ...)
2026-08-11 9:42 ` [PATCH RFC v2 3/4] rust: sysfs: add abstractions for device attributes Alexandru Radovici
@ 2026-08-11 9:42 ` Alexandru Radovici
3 siblings, 0 replies; 5+ messages in thread
From: Alexandru Radovici @ 2026-08-11 9:42 UTC (permalink / raw)
To: Greg Kroah-Hartman, 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, Rafael J. Wysocki
Cc: linux-kernel, linux-usb, rust-for-linux, driver-core,
Alexandru Radovici
Add an optional DEVICE_GROUPS constant to the USB driver trait and pass
it to struct usb_driver::dev_groups, so that a driver can expose sysfs
files on every interface it binds to. Drivers that leave it unset keep
the current behaviour, as the field stays NULL.
usbcore forwards dev_groups to the embedded struct device_driver, so the
files are created only after probe() has returned successfully and are
removed before disconnect() runs. An attribute callback therefore always
finds the private data that probe() stored.
The constant is typed AttributeGroups<Self::Data<'static>> because a
'static reference cannot name the 'bound lifetime that probe() works
with, while the value handed to a callback is a Self::Data<'bound>. A
driver whose private data borrows from 'bound must not set this
constant; only types that are the same for every instantiation are
sound here.
Signed-off-by: Alexandru Radovici <alexandru.radovici@wyliodrin.com>
---
rust/kernel/usb.rs | 100 ++++++++++++++++++++++++++++++++++++++--
samples/rust/rust_driver_usb.rs | 14 ++++++
2 files changed, 111 insertions(+), 3 deletions(-)
diff --git a/rust/kernel/usb.rs b/rust/kernel/usb.rs
index 6670fa2ff377..27fc5e28b45a 100644
--- a/rust/kernel/usb.rs
+++ b/rust/kernel/usb.rs
@@ -19,6 +19,7 @@
},
prelude::*,
sync::aref::AlwaysRefCounted,
+ sysfs::AttributeGroups,
types::Opaque,
usb::endpoint::HostEndpoint,
ThisModule, //
@@ -26,10 +27,13 @@
use core::{
marker::PhantomData,
mem::{
- offset_of,
- MaybeUninit, //
+ offset_of, //
+ MaybeUninit,
+ },
+ ptr::{
+ self,
+ NonNull, //
},
- ptr::NonNull,
slice, //
};
@@ -64,6 +68,11 @@ unsafe fn register(
(*udrv.get()).probe = Some(Self::probe_callback);
(*udrv.get()).disconnect = Some(Self::disconnect_callback);
(*udrv.get()).id_table = T::ID_TABLE.as_ptr();
+ (*udrv.get()).dev_groups = if let Some(dev_group) = T::DEVICE_GROUPS {
+ dev_group.as_ptr()
+ } else {
+ ptr::null_mut()
+ };
}
// SAFETY: `udrv` is guaranteed to be a valid `DriverType`.
@@ -320,6 +329,91 @@ pub trait Driver {
/// The table of device ids supported by the driver.
const ID_TABLE: IdTable<Self::IdInfo>;
+ /// The sysfs attribute groups to create for interfaces bound to this driver.
+ ///
+ /// Defaults to `None`, i.e. the driver exposes no attributes of its own.
+ /// Build the value with [`attribute_list!`](crate::attribute_list), which
+ /// declares the necessary `static`s and evaluates to a
+ /// `&'static AttributeGroups`. Only a single group is supported.
+ ///
+ /// The files appear in the sysfs directory of each bound USB *interface*, not
+ /// of the USB device, for instance `/sys/bus/usb/devices/1-1:1.0/`. Because
+ /// `dev_groups` belongs to the driver rather than to one device, every
+ /// interface this driver binds to gets the same set of files, and there is no
+ /// way to hide an individual attribute for some interfaces.
+ ///
+ /// # Registration window
+ ///
+ /// The array is stored in `struct usb_driver::dev_groups`, which usbcore
+ /// forwards to the embedded `struct device_driver`. The driver core creates
+ /// the files only after [`Driver::probe`] has returned successfully and
+ /// removes them before [`Driver::disconnect`] runs, so an attribute callback
+ /// always finds live private data on the interface. That is what makes it
+ /// sound for the callbacks to recover it at all. Groups installed anywhere
+ /// that is populated earlier, such as a `device_type`, would expose the files
+ /// from `device_add` onwards, before `probe` had stored anything.
+ ///
+ /// # `Sync`
+ ///
+ /// Attribute callbacks receive a shared reference to the private data, and
+ /// two readers on separate file descriptors can be inside a `show` for the
+ /// same interface at once, so [`Self::Data`] has to be `Sync` for a driver
+ /// that sets this to `Some`. The bound is deliberately not stated here: it
+ /// comes from `AttributeOperations::Data`, so it is checked at the
+ /// `attribute_list!` call site rather than being imposed on every driver,
+ /// including the ones that leave this as `None`.
+ ///
+ /// # The `'static` in `Self::Data<'static>`
+ ///
+ /// The reference is `'static`, so `'static` is the only lifetime this type
+ /// can name. The value a callback is handed at runtime is the
+ /// `Self::Data<'bound>` that [`Driver::probe`] returned for the current
+ /// binding, so the tag names a different instantiation of the GAT than the
+ /// one that exists, and the attribute code reads the private data as a
+ /// `Self::Data<'static>`. Variance turns `'static` into `'bound`, not the
+ /// reverse, so nothing recovers the difference.
+ ///
+ /// Only set this to `Some` when [`Self::Data`] does not borrow from `'bound`,
+ /// i.e. when every instantiation is the same owning type. A `Data` holding
+ /// `&'bound` references can leak them out of an attribute callback with a
+ /// longer lifetime than they have, and nothing here catches it.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// const BLINK: u64 = 0;
+ ///
+ /// // No `'bound` borrows, so `Data<'static>` is the type that exists.
+ /// struct MyData { blinking: AtomicBool }
+ ///
+ /// impl usb::Driver for MyDriver {
+ /// type Data<'bound> = MyData;
+ ///
+ /// const DEVICE_GROUPS: Option<&'static AttributeGroups<Self::Data<'static>>> =
+ /// Some(kernel::attribute_list!(
+ /// data: MyData,
+ /// ops: MyDriver,
+ /// attributes: BLINK,
+ /// ));
+ ///
+ /// // ... ID_TABLE, probe, disconnect
+ /// }
+ ///
+ /// impl kernel::sysfs::AttributeOperations<BLINK> for MyDriver {
+ /// type Data = MyData;
+ ///
+ /// fn show(
+ /// data: Pin<&MyData>,
+ /// _dev: &Device<Bound>,
+ /// buf: &mut [u8; PAGE_SIZE],
+ /// ) -> Result<usize> {
+ /// // Format into `buf` and return the byte count.
+ /// Ok(0)
+ /// }
+ /// }
+ /// ```
+ const DEVICE_GROUPS: Option<&'static AttributeGroups<Self::Data<'static>>> = None;
+
/// USB driver probe.
///
/// Called when a new USB interface is bound to this driver.
diff --git a/samples/rust/rust_driver_usb.rs b/samples/rust/rust_driver_usb.rs
index 02bd5085f9bc..055c46faf144 100644
--- a/samples/rust/rust_driver_usb.rs
+++ b/samples/rust/rust_driver_usb.rs
@@ -3,6 +3,9 @@
//! Rust USB driver sample.
+const ATTR1: u64 = 0;
+const ATTR2: u64 = 1;
+
use kernel::{
device::{
self,
@@ -10,6 +13,7 @@
},
prelude::*,
sync::aref::ARef,
+ sysfs::AttributeOperations,
usb, //
};
@@ -17,6 +21,16 @@ struct SampleDriver {
_intf: ARef<usb::Interface>,
}
+#[vtable]
+impl AttributeOperations<ATTR1> for SampleDriver {
+ type Data = Self;
+}
+
+#[vtable]
+impl AttributeOperations<ATTR2> for SampleDriver {
+ type Data = Self;
+}
+
kernel::usb_device_table!(
USB_TABLE,
MODULE_USB_TABLE,
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread