From nobody Tue Sep 29 06:59:03 2026 Received: from mail-wr1-f43.google.com (mail-wr1-f43.google.com [209.85.221.43]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B811142A16C for ; Tue, 11 Aug 2026 09:42:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.43 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441366; cv=none; b=J3KRp3a3gAZHsvy4qCPabUAX7FqTSW0pEOgqhDDun+tNJmfZzoN9bmedD9FnVrYlMoDDjzWDFCnkNWRnPd17MgSLaCeThhPzOOWrwf9tRF9UeC80153wxQIMw8SuLNNLfFyIMBcDD8xzKe7FG7p0lvPft4NITAOn6bkY+QfI5IM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441366; c=relaxed/simple; bh=893Hh4a3h/32n5wAisX8BIxVtF4lLpIUJFYvCdz41wM=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=q6zDi9LZEwJrhBnUl46V91ZfpYsi61KfEb0DaEvYg0e4kdZhjtaPVxnMH2xqIr6hWiORn8vOE0ePHZdQao+wV1caXDOaXn7DpxNHZtrExElZGHTVWmUtwExpkBh0Kyhi2+zWgmZc6VxqJGCWFI6exS0eHkZsFcvE28yEwyN71JM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com; spf=pass smtp.mailfrom=wyliodrin.com; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b=Xa8kq3V3; arc=none smtp.client-ip=209.85.221.43 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b="Xa8kq3V3" Received: by mail-wr1-f43.google.com with SMTP id ffacd0b85a97d-47f7027ca11so1836052f8f.3 for ; Tue, 11 Aug 2026 02:42:42 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=wyliodrin.com; s=google; t=1786441361; x=1787046161; darn=vger.kernel.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=9+S0yv4vQXBKIZeGxLC3P5VbQOMPo3F4lWAXocbmzYo=; b=Xa8kq3V3eLMGWyWA76PSPNZ89W1kM2zsf7q65Ppv13burBErba4t+9cGFNEere/jQp 9uO1oaLmFz7dwKisnlZGgVicNSkOTLFXJN2nAmkWtz0LZZWvjvQR9NR4kOrBDcF+GvK3 LjaJjD91ARIbindc5aa7Vi54XwkMA9K3XCXlI= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786441361; x=1787046161; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=9+S0yv4vQXBKIZeGxLC3P5VbQOMPo3F4lWAXocbmzYo=; b=lm8RcdllEV9FguOn31wL+p036jZekpCWopjsstU1IkOYTv+cjhJZv/B4TitUXyNH/o ogrxwnztEcuUp6gu0v7XLhIeT2Bm6r6z0Uzhva+yv+D1ccYZyVaFyeaeUzFqWTI3maCQ P9EIiUb+y9rr8xmJOOm7JCfsNw1rwk3J2Ad2uR5mNuEsl9QL9g6ahz09RXbZD+cEAfOO 1Xv1xzpuxDKgP7cdEq592MaYoC+DvJ9LkSC9OtAgTO4UphfLt1CWebZfsZ4U7sQJ9igJ gTxoWXZuOOhY7jfvRgR2FVQxpPAsS9nPDVhUNsug4eDTz0JxBm+N+dgwr8J8iZL1Lw+G g8IQ== X-Gm-Message-State: AOJu0Yw95b65YRXX7efBYPVk7khbVsInhOKmTIwH7mWf1fBhtS8y7aOO 9etjLrwnkorF7O1Ubpuyj9Y8nMG4f34tu8KLTRD6m2wabG7ilZ23ZY01lakzCEjNmPU= X-Gm-Gg: AR+sD11I3dmRLiDxGwS3Xg346k0PuLn97TNEoMilZJm82ViUZQM4pGKLgFxJwhri/7j 2kFftQDn8ofA6/AoS4eXY9C7Mri1xcfGXEoebz3RCDnl9eXOR5ZI7pFZJqRG8Flf7l8QZvn633c VPGxN3DPuM7M4P/bPJf82Ofr/DRShxb9OSO5QMn6Ao9oZ9+1huFAjdwkGrpu3Fj/D21umKr1qnS LY6cxqNe86vtgydi8GACBk/oTblCRrjUFdk4+zk44Kn4PaEa8WgP0rYNDJZcL/SStQdG0HuDD8H k1M9iHMzr9jv/oe+ziPSDrVNzbt49OVSHU/wAQwcXEQKSbLCkfUs+nzOCVKFk3wQ0k2vUvO9B+y 2/8K0ajpD778fidUJBdunzOTfMXQE2z23lKgr0BRKEuMmMA86uF9WbhpZMh7+mxwpZxR+9Ertaq 9Cv3l0WzopUsVdUU5jn7c7fBG2GRd5ZkUM7hRrX0BixdnGkpqaj+muB/eJYQoGxzQJRK4hhIBYJ MWCnNbxzrCF01Yl8eTk02SIxAPaaBS41LkFPQ== X-Received: by 2002:a05:6000:4711:b0:47f:e377:8d61 with SMTP id ffacd0b85a97d-4814ad86764mr3170979f8f.11.1786441360521; Tue, 11 Aug 2026 02:42:40 -0700 (PDT) Received: from [10.0.36.57] ([217.73.170.83]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-4814a709624sm3388917f8f.22.2026.08.11.02.42.38 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 11 Aug 2026 02:42:39 -0700 (PDT) From: Alexandru Radovici Date: Tue, 11 Aug 2026 12:42:28 +0300 Subject: [PATCH RFC v2 1/4] rust: usb: add endpoint abstraction Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260811-rust-usb_control_msg-v2-1-ef79c92bd898@wyliodrin.com> References: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> In-Reply-To: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> To: Greg Kroah-Hartman , Miguel Ojeda , Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Danilo Krummrich , Daniel Almeida , Tamir Duberstein , Alexandre Courbot , =?utf-8?q?Onur_=C3=96zkan?= , "Rafael J. Wysocki" Cc: linux-kernel@vger.kernel.org, linux-usb@vger.kernel.org, rust-for-linux@vger.kernel.org, driver-core@lists.linux.dev, Alexandru Radovici X-Mailer: b4 0.14.3 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` 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 --- 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, // }; =20 +pub mod endpoint; + /// An adapter for the registration of USB drivers. pub struct Adapter(T); =20 @@ -334,6 +338,78 @@ fn disconnect<'bound>( ); } =20 +/// 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 U= VC camera exposing one +/// setting per bandwidth tier, plus a zero-bandwidth setting used while i= dle. 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 `alts= etting` array of a +/// `struct usb_interface` owned by the C side, which guarantees that `des= c` is initialised and +/// that the setting outlives the borrow. +#[repr(transparent)] +pub struct AlternateSetting(Opaque); + +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 in= itialised 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; settin= g 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 val= id + // `struct usb_host_interface` with an initialised `desc`. + unsafe { (*self.as_raw()).desc.bAlternateSetting } + } + + /// Returns the `bInterfaceNumber` of the interface this setting belon= gs to. + /// + /// Every alternate setting of a given interface reports the same numb= er, 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 val= id + // `struct usb_host_interface` with an initialised `desc`. + unsafe { (*self.as_raw()).desc.bInterfaceNumber } + } + + /// Returns the endpoints declared by this alternate setting, in descr= iptor order. + /// + /// The endpoints come back untyped, as `Endpoint`; = refine them with + /// [`Endpoint::as_in()`], [`Endpoint::as_out()`] and [`Endpoint::as_c= ontrol()`]. + pub fn endpoints(&self) -> &[HostEndpoint] { + // SAFETY: By the type invariants, `self.as_raw()` points at a val= id + // `struct usb_host_interface` with an initialised `desc`. + let (ptr, len) =3D (unsafe { (*self.as_raw()).endpoint }, unsafe { + (*self.as_raw()).desc.bNumEndpoints + }); + + if len =3D=3D 0 { + &[] + } else { + // SAFETY: When `bNumEndpoints` is non-zero the C side has all= ocated an array of that + // many initialised `struct usb_host_endpoint` at `ptr`, livin= g as long as the + // interface. `Endpoint` is a `#[repr(transparent)]` wrapper a= round + // `Opaque`, which is itself layo= ut-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_in= terface`]. @@ -356,6 +432,54 @@ impl Interface { fn as_raw(&self) -> *mut bindings::usb_interface { self.0.get() } + + /// Returns all alternate settings of this interface, in `bAlternateSe= tting` 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 val= id `struct usb_interface`, + // so both fields are initialised. Reading them requires nothing o= f the device context: + // `altsetting` is filled in when the interface is created and hol= ds until it is released. + let (ptr, len) =3D (unsafe { (*self.as_raw()).altsetting }, unsafe= { + (*self.as_raw()).num_altsetting + }); + + if len =3D=3D 0 { + &[] + } else { + // SAFETY: When `num_altsetting` is non-zero the C side has al= located 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 arou= nd + // `Opaque`, which is itself lay= out-compatible with + // `struct usb_host_interface`, so the cast preserves both siz= e 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 int= erface. + /// + /// This is the setting whose endpoints the device is actually prepare= d to service, so it is + /// the one a driver should read endpoint descriptors from. After conf= iguration it is + /// setting 0. + /// + /// The result is a snapshot. `usb_set_interface()` can repoint the in= terface at a different + /// setting, which does not invalidate the returned reference - both p= oint into the same live + /// array - but does stop it being the current one. A driver that cach= es 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 val= id `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` and so layout-compatible= with it, and the borrow + // lasts no longer than `&self`. + unsafe { &*(*self.as_raw()).cur_altsetting.cast() } + } } =20 // SAFETY: `usb::Interface` is a transparent wrapper of `struct usb_interf= ace`. @@ -426,7 +550,7 @@ unsafe impl Sync for Interface {} /// /// [`struct usb_device`]: https://www.kernel.org/doc/html/latest/driver-a= pi/usb/usb.html#c.usb_device #[repr(transparent)] -struct Device( +pub struct Device( Opaque, PhantomData, ); 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 endpo= int descriptor the device +//! reported: [`number()`](HostEndpoint::number) and [`address()`](HostEnd= point::address), +//! [`direction()`](HostEndpoint::direction) and [`transfer_type()`](HostE= ndpoint::transfer_type), +//! and the packet and interval geometry needed to size and schedule trans= fers. +//! +//! # Where endpoints come from +//! +//! Endpoints declared by an interface are borrowed from the alternate set= ting that describes them, +//! via [`AlternateSetting::endpoints()`](crate::usb::AlternateSetting::en= dpoints). The +//! default control endpoint - endpoint 0 - is not among them: no interfac= e descriptor lists +//! it, because the specification excludes it from `bNumEndpoints` and dev= ices 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 +//! | | | +//! as_in() <-- v as_control() --> as_ou= t() +//! Endpoint Endpoint Endpoi= nt +//! ``` +//! +//! A function taking `&Endpoint` therefore cannot be handed any= thing but a bulk IN +//! endpoint, and needs no run-time check of its own. The markers are [`Ph= antomData`], 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 US= B 2.0 +//! section 9.6.6 correspondingly defines bit 7 of `bEndpointAddress` as i= gnored +//! 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 erro= r 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>, Option<&End= point>) { +//! let eps =3D 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::AlternateSett= ing). +/// +/// `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 `Endp= oint`, +/// 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 `&Endpoin= t` may then +/// rely on the endpoint really being a bulk IN endpoint without re-checki= ng it. +/// +/// Direction and transfer type are not independent: a control endpoint ha= s no direction, so +/// [`as_control()`] yields [`Bidirectional`] rather than preserving or di= scovering 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 sam= e layout as +/// an untyped one and the refinement is free at run time. +/// +/// # Invariants +/// +/// - The wrapped [`Opaque`] holds an initialised `struct usb_host_endpoin= t`. Instances are never +/// constructed by Rust code; they are only ever borrowed out of a `stru= ct 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( + Opaque, + PhantomData, + PhantomData, +); + +/// 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 wit= h 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 m= arker 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 de= fines bit 7 +/// of `bEndpointAddress` as ignored for control endpoints - so "is this e= ndpoint IN or OUT" +/// has no answer for one. +/// +/// Since [`HostEndpoint::as_in()`] and [`HostEndpoint::as_out()`] are def= ined 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 b= eing implemented + /// outside of this module, so that the type invariants of [`HostEndpo= int`] 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 t= he 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 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 =3D=3D 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 err= or retry, for + /// time-sensitive streams such as audio and video. + Isochronous, + /// Transfers with guaranteed delivery but no bandwidth or latency gua= rantee, for bulk data + /// such as mass storage. + Bulk, + /// Small, periodically polled transfers with bounded latency and guar= anteed delivery, for + /// devices such as keyboards and mice. + Interrupt, +} + +impl From for TransferType { + /// Extracts the transfer type from a raw `bmAttributes`. + /// + /// Bits 1:0 of the endpoint attributes hold the transfer type (`USB_E= NDPOINT_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 =3D> TransferType::Control, + 1 =3D> TransferType::Isochronous, + 2 =3D> TransferType::Bulk, + _ =3D> TransferType::Interrupt, + } + } +} + +impl HostEndpoint { + /// Returns a raw pointer to the underlying `struct usb_host_endpoint`. + /// + /// By the type invariants the pointer is non-null and points at an in= itialised 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 e= ndpoint of the same + /// interface may share one, so pair it with [`direction()`](Self::dir= ection), 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 val= id + // `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 i= n bit 7 (set for IN, + /// clear for OUT); bits 6:4 are reserved and zero. Taken together tho= se 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 val= id + // `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 i= t reports for one is + /// whatever bit 7 of the address byte happens to hold, which the spec= ification leaves + /// undefined. + pub fn direction(&self) -> Direction { + // SAFETY: By the type invariants, `self.as_raw()` points at a val= id + // `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 val= id + // `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 isochrono= us and interrupt + /// endpoints the total payload per microframe is this value multiplie= d 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 val= id + // `struct usb_host_endpoint` with an initialised `desc`. + unsafe { u16::from_le((*self.as_raw()).desc.wMaxPacketSize) & 0x07= ff } + } + + /// Returns the number of *additional* transaction opportunities per m= icroframe. + /// + /// This is bits 12:11 of `wMaxPacketSize`, so the total number of tra= nsactions 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..=3D2`; `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 val= id + // `struct usb_host_endpoint` with an initialised `desc`. + unsafe { (u16::from_le((*self.as_raw()).desc.wMaxPacketSize >> 11)= & 0b11) as u8 + 1 } + } +} + +impl HostEndpoint { + /// 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 endpoin= t - refining direction + /// first and transfer type second would otherwise produce an `Endpoin= t`, 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 lis= ts; reach that one + /// /// through `Device::control_endpoint()` instead. + /// fn extra_control(alt: &AlternateSetting) -> Option<&Endpoint> { + /// alt.endpoints().iter().find_map(|ep| ep.as_control()) + /// } + /// ``` + pub fn as_control(&self) -> Option<&HostEndpoint> { + matches!(self.transfer_type(), TransferType::Control).then(|| { + // SAFETY: The transfer type was just checked, so the [`Contro= l`] invariant holds, and + // [`Bidirectional`] is exactly what a control endpoint warran= ts. + // `Endpoint` is a `#[repr(transparent= )]` wrapper around the + // same `struct usb_host_endpoint` and differs only in its `Ph= antomData` 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 HostEndpoint { + /// Refines this endpoint into a device-to-host endpoint, if it is one. + /// + /// Returns [`None`] if [`direction()`](Self::direction) is not [`Dire= ction::In`]. The known + /// transfer type, if any, is preserved. + pub fn as_in(&self) -> Option<&HostEndpoint> { + if self.direction() =3D=3D Direction::In { + // SAFETY: The direction was just checked, so the [`In`] invar= iant holds. + // `Endpoint` is a `#[repr(transparent)]` wrapper ar= ound the same + // `struct usb_host_endpoint` and differs only in its `Phantom= Data` 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 [`Dire= ction::Out`]. The known + /// transfer type, if any, is preserved. + pub fn as_out(&self) -> Option<&HostEndpoint> { + if self.direction() =3D=3D Direction::Out { + // SAFETY: The direction was just checked, so the [`Out`] inva= riant holds. + // `Endpoint` is a `#[repr(transparent)]` wrapper a= round the same + // `struct usb_host_endpoint` and differs only in its `Phantom= Data` 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 HostEndpoint { + /// Returns the raw `bInterval` value of the endpoint descriptor. + /// + /// How this encodes a service interval depends on the transfer type a= nd the device's + /// operating speed: + /// + /// - Full-/low-speed interrupt endpoints: the interval in frames (1 m= s), `1..=3D255`. + /// - High-speed interrupt endpoints and all isochronous endpoints: an= exponent, giving an + /// interval of `2^(bInterval - 1)` microframes (125 micros), with `= bInterval` in `1..=3D16`. + pub fn interval(&self) -> u8 { + // SAFETY: By the type invariants, `self.as_raw()` points at a val= id + // `struct usb_host_endpoint` with an initialised `desc`. + unsafe { (*self.as_raw()).desc.bInterval } + } +} + +impl HostEndpoint { + /// 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 val= id + // `struct usb_host_endpoint` with an initialised `desc`. + unsafe { (*self.as_raw()).desc.bInterval } + } +} + +impl HostEndpoint { + /// 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 val= id + // `struct usb_host_endpoint` with an initialised `desc`. + unsafe { (*self.as_raw()).desc.bInterval } + } +} + +impl Device { + /// Returns the default control endpoint (endpoint 0) of this device. + /// + /// Every USB device has exactly one, and it is the endpoint all enume= ration and standard + /// requests travel over. Unlike the endpoints of an + /// [`AlternateSetting`](crate::usb::AlternateSetting), it is not desc= ribed by any + /// descriptor the device sends: the USB core synthesizes its descript= or + /// in `usb_alloc_dev()` and fills in the packet size from `bMaxPacket= Size0` of the device + /// descriptor during enumeration. It therefore cannot be found by sea= rching + /// [`AlternateSetting::endpoints()`](crate::usb::AlternateSetting::en= dpoints), and + /// this accessor is the only way to reach it. + /// + /// Neither marker needs a run-time check. [`Control`] holds because t= he core sets + /// `bmAttributes` to `USB_ENDPOINT_XFER_CONTROL` itself and the speci= fication fixes endpoint 0 + /// as a control endpoint; [`Bidirectional`] holds because control end= points have no direction. + /// Endpoint 0's address byte is `0x00`, so [`direction()`](HostEndpoi= nt::direction) + /// would report [`Direction::Out`] - a meaningless answer, which is w= hy 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 val= id `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 { + // SAFETY: By the type invariants, `self.as_raw()` points at a val= id `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 =3D unsafe { core::ptr::addr_of!((*self.as_raw()).ep0) }; + + // SAFETY: `HostEndpoint` is a `#[repr(transparent)]` wrapper arou= nd + // `Opaque`, which is layout-compatib= le with + // `struct usb_host_endpoint`, so the cast preserves size and alig= nment. The [`Control`] + // invariant holds because the core fixes `ep0.desc.bmAttributes` = to + // `USB_ENDPOINT_XFER_CONTROL`, and [`Bidirectional`] holds for an= y control endpoint. The + // `Opaque` accounts for the C side mutating the endpoint behind t= his shared reference, and + // the borrow lasts no longer than `&self`. + unsafe { &*ep0.cast() } + } +} --=20 2.55.0 From nobody Tue Sep 29 06:59:03 2026 Received: from mail-wr1-f49.google.com (mail-wr1-f49.google.com [209.85.221.49]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 4C99B42BE8A for ; Tue, 11 Aug 2026 09:42:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.49 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441367; cv=none; b=izw5BpgbQfXUU1+hUmD/6RJUyYDCX66FW2khG9Sn4rutbPMJ7LCXxdhLPAPwcAf8Z55SmDhLgqWVC5biP0Lui2VjOmVbgQlOGlMdktuSQum+wccCxS1c0bBhbe6kTMOh8LmXnEIoKJoBOXIQR2L+dpajLQ1yrHXYHJ932iZqF9o= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441367; c=relaxed/simple; bh=95yY9cjA4oVPmXAFuCH3xualr4XOSa23Dn+LBb2Yodg=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=XdKwLgQZj8wZvlDzgS0lOKgQOXsfOhZ7oQ3MDXdLAMuFkwv0oK8iBA+7yguL+94a93fwX0d/yhJT53UMSh9DIKU8DF6Lrt2cAmRxgZZxLSoDHDu67fcwSw5ULnhUCiP3hNTrFzBfsC37wAOFl6jh8+4VqpqKHr3Iml8JkRNL8+k= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com; spf=pass smtp.mailfrom=wyliodrin.com; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b=fhEcEHYv; arc=none smtp.client-ip=209.85.221.49 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b="fhEcEHYv" Received: by mail-wr1-f49.google.com with SMTP id ffacd0b85a97d-481412f1828so645265f8f.1 for ; Tue, 11 Aug 2026 02:42:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=wyliodrin.com; s=google; t=1786441362; x=1787046162; darn=vger.kernel.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=o2iRWi2TN8f6wY1UpWFyKTBNPNL1D5PfKvHtZR9X5kU=; b=fhEcEHYvGLu7cO0Ar/GsoaX8cK2hu7l15Dh6l6awpMQilJ+pdsWOVgSt001vdHLT0S DtGdFNPWP4lE68kVdlFRCwRwFjUrBs81TOCRt0RAjLqMdIz2zV1l2eKFRgB2NtjC+QK0 YkjOkZw+YgFuO5jpOaDNh8gHJcR+D1Myv+iZ4= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786441362; x=1787046162; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=o2iRWi2TN8f6wY1UpWFyKTBNPNL1D5PfKvHtZR9X5kU=; b=IBCjYFjud/Qzz33g9OBkHGNswmsaNUfQgY9dUNHrWmHYJ3/UH4k7Zkg9o6gmAZbFic JYX5A8KQxruqfC9DhjUjcvXJVuXvgDwzgfBtye6EtmLV9/pgiAmMGwEV0Ill/hcxIBmq tCKNFdrdqVWH2kV515Z2euMCf+wZ+a5ToenCjsjrOUTtzjSFi68N2gVs3YSbKlihg9pY NSjCe/Rt8yGiGNbx95ZvnyMhCxGv9zPo2cXMVveKsKEXpAPqkxjtyagXb7Bjpk6Gy9et F1QkQ70hvcvkjUgM4tgkqX/s0oG+jDPBS33oWJWCk4oNv1HDMi18Ig/Uj1REk6eYj3jS wcrw== X-Gm-Message-State: AOJu0YwLL9oXP/CxGqVg1FDKysRQI1Ja0rnmFD012OmaQww1GUlQdakp qe+A3lVcbZXNapBMyQbWohnU90xiVP2Q1XlswRS/n+CQ0OrksDDaFZLynx/SnNua7xI= X-Gm-Gg: AR+sD12/HTPdmFNYKCEzl0nzoPId7IX11kZYAza72L3WvydABVFgTJQL7ZK6nTDwtod oXrp/c7FraHPQHJGyGF0URVopfxY72FI85chb5yUfpY/jc2u93me4ish7ptJ/EPU2je74Sf/dw5 1A8w74dSES/6J8Vb3tE0MWBuBgbau4PYpZczipsTbalogf+cGEMpfS+3pTHBSQ2ypGxD89yuY01 iEM9GJJ/TEx4P/g3Rh4FfqjRbGSqPpOU6KTKdZDOHjwyMVzqgD3AzHgLZ+ChaEuyQ2aGY2ow1u/ k83JGec+m/6koK2Qs8auk3tzrwAuAWNeWfhOCK4NOx/csszUiDg12A3e39ctXEuClhHIsEfUXMh nGzn29RO8QTIZ8qSfeO18ku7/GKv9X6eExwTdQu5dhEECNBTCLRt+irChoIxxjGtdzer+T7LUiG FhHgyKVfwTBa0uOt1wyVjRYuysOaB++tK++O+GFo3wJIRkoh0AhCcq9kKyuYVDyjA0iuQ/SmAtO EM9qQzEIDiqokXbcEewwgkQxMpYx25MRachcg== X-Received: by 2002:a05:6000:240c:b0:47f:6dcb:3737 with SMTP id ffacd0b85a97d-4814aee3ef2mr2770871f8f.29.1786441362473; Tue, 11 Aug 2026 02:42:42 -0700 (PDT) Received: from [10.0.36.57] ([217.73.170.83]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-4814a709624sm3388917f8f.22.2026.08.11.02.42.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 11 Aug 2026 02:42:41 -0700 (PDT) From: Alexandru Radovici Date: Tue, 11 Aug 2026 12:42:29 +0300 Subject: [PATCH RFC v2 2/4] rust: usb: add control message send and receive Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260811-rust-usb_control_msg-v2-2-ef79c92bd898@wyliodrin.com> References: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> In-Reply-To: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> To: Greg Kroah-Hartman , Miguel Ojeda , Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Danilo Krummrich , Daniel Almeida , Tamir Duberstein , Alexandre Courbot , =?utf-8?q?Onur_=C3=96zkan?= , "Rafael J. Wysocki" Cc: linux-kernel@vger.kernel.org, linux-usb@vger.kernel.org, rust-for-linux@vger.kernel.org, driver-core@lists.linux.dev, Alexandru Radovici X-Mailer: b4 0.14.3 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`, so a non-control endpoint cannot be passed by construction. Signed-off-by: Alexandru Radovici --- 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, // }; =20 +pub mod control; pub mod endpoint; =20 /// 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 stat= us 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 bidirect= ional, the endpoint does +//! not determine the direction: the `Direction` bit of `bmRequestType` do= es, 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 =3D 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 interprete= d 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 it= self and interpreted the + /// same way by every device. Usually handled by the USB core rather t= han by a driver; see + /// [`Device`]. + Standard =3D 0, + /// Request is intended for a USB class. + Class =3D 1, + /// Request is vendor-specific. + Vendor =3D 2, + /// Reserved. + Reserved =3D 3, +} + +/// The target a [`Request`] is addressed to. +/// +/// This is the `Recipient` field of `bmRequestType`, occupying bits 4:0 o= f 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 define= d - hence +/// the bound on the wrapped [`Bounded`]. +#[derive(Copy, Clone, PartialEq, PartialOrd)] +#[repr(transparent)] +pub struct Recipient(Bounded); + +impl Recipient { + /// The device as a whole. + pub const DEVICE: Self =3D Self(Bounded::::new::<0u8>()); + /// A specific interface, named by [`Request::index`]. + pub const INTERFACE: Self =3D Self(Bounded::::new::<1u8>()); + /// A specific endpoint, named by [`Request::index`]. + pub const ENDPOINT: Self =3D Self(Bounded::::new::<2u8>()); + /// Some other target, defined by the request itself. + pub const OTHER: Self =3D Self(Bounded::::new::<3u8>()); + /// A port. Wireless USB only. + pub const PORT: Self =3D Self(Bounded::::new::<4u8>()); + /// An RPipe. Wireless USB only. + pub const RPIPE: Self =3D Self(Bounded::::new::<5u8>()); + + /// Builds a recipient from a raw field value. + /// + /// Prefer the associated constants; this exists for values a future s= pecification revision may + /// define. The [`Bounded`] parameter rules out values the field canno= t hold, but does not + /// guarantee the device understands the one you pass. + pub fn new(val: Bounded) -> 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 `bmRequestTyp= e` 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 th= em 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 fie= lds. + pub request: u8, + /// Request value. The meaning of the value depends on the previous fi= elds. + pub value: u16, + /// Request index. The meaning of the value depends on the previous fi= elds. + 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 endpoin= t it runs over: bit 7 of + /// `bmRequestType` is what the host controller obeys for a control tr= ansfer, and bit 7 of the + /// endpoint address is defined to be ignored. + fn bm_request_type(&self, direction: Direction) -> u8 { + let dir =3D match direction { + // `USB_DIR_IN` is `0x80`; `USB_DIR_OUT` is zero. + Direction::In =3D> bindings::USB_DIR_IN as u8, + Direction::Out =3D> 0, + }; + + dir | ((self.request_type as u8) << REQUEST_TYPE_SHIFT) | self.rec= ipient.0.get() + } +} + +/// Converts a buffer length into a `wLength` value. +/// +/// The field is 16 bits, so anything larger cannot be expressed in a sing= le control transfer. +#[inline] +fn transfer_length(len: usize) -> Result { + 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 `wLengt= h` 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 defau= lt control endpoint - + /// almost always the right choice - pass [`control_endpoint()`](Devic= e::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, + request: Request, + data: Option<&[u8]>, + timeout: Duration, + memflags: Flags, + ) -> Result { + let (data, size) =3D match data { + Some(bytes) =3D> ( + bytes.as_ptr().cast::(), + transfer_length(bytes.len())?, + ), + None =3D> (ptr::null(), 0), + }; + + // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the t= ype 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 `s= ize` initialised bytes + // that outlive the call - the callee only reads from it, and copi= es before submitting, so + // no DMA is performed on the caller's buffer. + let ret =3D 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. devic= e 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 `wLengt= h` to zero - note that + /// this makes the direction bit meaningless, so such a request is ind= istinguishable from the + /// [`control_message_send()`](Device::control_message_send) equivalen= t. + /// + /// `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 defau= lt control endpoint - + /// almost always the right choice - pass [`control_endpoint()`](Devic= e::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 { + /// let mut buf =3D [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, + request: Request, + data: &mut [u8], + timeout: Duration, + memflags: Flags, + ) -> Result { + let (data, size) =3D { + let size =3D transfer_length(data.len())?; + (data.as_mut_ptr().cast::(), size) + }; + + // SAFETY: `self.as_raw()` is a valid `struct usb_device` by the t= ype 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 `s= ize` bytes of a buffer + // uniquely borrowed for the duration of the call, which the calle= e may only write to. + let ret =3D 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(()) + } + } +} --=20 2.55.0 From nobody Tue Sep 29 06:59:03 2026 Received: from mail-wr1-f43.google.com (mail-wr1-f43.google.com [209.85.221.43]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EB73E427A03 for ; Tue, 11 Aug 2026 09:42:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.43 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441370; cv=none; b=mM9RXWJYLJ1HMyZY795WFdASsjdHrXspnfoIyoDgQCRp2SshD3Ela6edtRjnyqwQD8yxvC9sqKkM9tQM5U8KZuBg7Lq5pJ4+rQ/OV7F1eow9/bIsdQQHbkpriapmCotcxQWue6puvGo1frgwBmvgKG7bMWAiDJvM/DhyVDirWwY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441370; c=relaxed/simple; bh=IwiFOUbxvRNjK7crHGsCI07ULImGE0Hqz0O4LJsekdw=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=fIffagGSGGEu7JRju1SRfLoLiDmIhCm+ZYm2ChQVi0EouWmmDwJTrg601B2Ignx/1XTpVfjsr7KMI+Rrwsm0lf4T4F9C8b3EjTBRLHFtlGVeAqaS29zzD9jtWNKFH752qN7zeKLVMddNNTLiiJc9/szHK7juzEWYNQEYr6G1QOk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com; spf=pass smtp.mailfrom=wyliodrin.com; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b=jk67Rto2; arc=none smtp.client-ip=209.85.221.43 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b="jk67Rto2" Received: by mail-wr1-f43.google.com with SMTP id ffacd0b85a97d-47362928f65so2344251f8f.2 for ; Tue, 11 Aug 2026 02:42:46 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=wyliodrin.com; s=google; t=1786441365; x=1787046165; darn=vger.kernel.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=E+oomZkoZ295rtQrxCnuL4AG5gCiLGi20tvBGKVA+/0=; b=jk67Rto2EASElWane17b+I06rkdFq9dJQkyGRd21w5kWJ19YwcoizNX1xs2Mv+edyO q1wK2exBeHWvmkrfI7MCLwpvDN7tbFqSRkv8aNkz8IeDWaaZwt80iTZA52sEDiW+miCN ggxtYTcowZ0X2vtzrsL+cscm2iPpm0bZzB7xw= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786441365; x=1787046165; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=E+oomZkoZ295rtQrxCnuL4AG5gCiLGi20tvBGKVA+/0=; b=nlXAdQFYSKLk012vUTjt8VFg60MiDe1lKElEEB0kx2TrLSSs+Hjt9Qw0UymU029x19 xyfc/0QbEU4zBzHmKh04OBmI4Ym54dmf7LA1+3sSfQlmp7nUxNemykCs+FXcT/9wcwC1 tZKu9VJSGApWS15GoP8ajcmrTZtQPPWK0T93oJvr2MfUN3jaHVoskKrlohZ/5JbNPugS 1o7uv9W45wYUnmAdaScoXrC5z6h4gRD5THZIbiEUIMF/cdtVAitY3/891eXCMSWKR0nD YMtxOBw+CBUC98+16eg8M5PO94nTToWYknui8VrUzfKmsTJsah8tLsU+JAk5NImlKnxa Unmg== X-Gm-Message-State: AOJu0Ywo8yEJaKOpNkigeZ5s/NUhAvPhpjs1js+wKK478+ePANnLy7MF Bps3zpw+CD46W/rNV3WHAxQomRCp5xlyig5/i82N0+G4ebQFuH5xHuVkzBqkUAzm8HA= X-Gm-Gg: AR+sD107HNfHh/qzQkUW3YLSTQX85EVDao+5aH/58nRS2VnsE7ev6vzguqg057xsf0c xiGF6ccrF/koY0nKF+kdoq7dWO1PNIM2m8CTT3m+WL0E6/3ROoqVdr0jBphE6AoMNaAEDbotGWa hJmYyRScQFyRKXQ62SpAizlR2xW/wpsldKb+EKh2vqMxPZfFVS1FncqR40uzLR6RXUi1E7BYz+G Fo3zp3W+vQHIsjgcL0xlfj4TghdECL34aTg3eZbcIIpEprl/ckJEMYCbCEmoqRyED+VTuIttl/S S6t+yvLKBiNCA/5zpqzjpUroLCBXKP3ktUmCPiZhEUU1oMq/AwirzmFVCENL7qHPMd/CRI75KFp wgu+nZo4E2/x5TfSfd48hv9qW3hKMIINHFxAZYD1lJT0Zh86dI5YwoblL1du+T0Krr8+3NNt6gZ sOsvT2p0ZRdId+JVbxfXqngNNM+FUlNXnsJ76qtRAjGvcEwwkx6sf6JPM5QWz2nqrrEuvNxLz+J mzfrUYJrZr814Y33jvPiRZhv692Y0VTAyB5pyTPNFHpkd0N X-Received: by 2002:a05:6000:250a:b0:47f:eca7:ad1a with SMTP id ffacd0b85a97d-4814ad7fcd8mr3082069f8f.1.1786441364643; Tue, 11 Aug 2026 02:42:44 -0700 (PDT) Received: from [10.0.36.57] ([217.73.170.83]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-4814a709624sm3388917f8f.22.2026.08.11.02.42.42 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 11 Aug 2026 02:42:44 -0700 (PDT) From: Alexandru Radovici Date: Tue, 11 Aug 2026 12:42:30 +0300 Subject: [PATCH RFC v2 3/4] rust: sysfs: add abstractions for device attributes Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260811-rust-usb_control_msg-v2-3-ef79c92bd898@wyliodrin.com> References: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> In-Reply-To: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> To: Greg Kroah-Hartman , Miguel Ojeda , Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Danilo Krummrich , Daniel Almeida , Tamir Duberstein , Alexandre Courbot , =?utf-8?q?Onur_=C3=96zkan?= , "Rafael J. Wysocki" Cc: linux-kernel@vger.kernel.org, linux-usb@vger.kernel.org, rust-for-linux@vger.kernel.org, driver-core@lists.linux.dev, Alexandru Radovici X-Mailer: b4 0.14.3 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 --- 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 { /// the device is fully unbound. /// - The type `T` must match the type of the `ForeignOwnable` previou= sly stored by /// [`Device::set_drvdata`]. - unsafe fn drvdata_unchecked(&self) -> Pin<&T> { + pub(crate) unsafe fn drvdata_unchecked(&self) -> Pin<&T> { // SAFETY: By the type invariants, `self.as_raw()` is a valid poin= ter to a `struct device`. let ptr =3D unsafe { bindings::dev_get_drvdata(self.as_raw()) }; =20 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 muta= ted +//! from Rust afterwards. That is what makes the [`Send`]/[`Sync`] impls b= elow +//! 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_group= s`. +//! +//! The [`attribute_list!`] macro builds all four layers. +//! +//! # Registration window +//! +//! Both trampolines recover the driver-private data from `dev->driver_dat= a`, +//! which is only meaningful while a driver is bound. The groups built her= e must +//! therefore be installed through `driver->dev_groups`, which the driver = core +//! creates after a successful `probe` and removes before `remove` runs. H= anging +//! them off `device_type::groups` or `dev->groups` instead would expose t= he +//! files from `device_add` onwards, i.e. before any driver has set the pr= ivate +//! 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 sy= sfs +/// directory. +/// +/// # Type parameters +/// +/// * `ID` distinguishes attributes backed by the same operations type. Be= cause +/// 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 fro= m the +/// device, tied to `O` through `AttributeOperations::Data`. +/// * `O` supplies the `show`/`store` implementations. +/// +/// # Invariants +/// +/// `attribute` contains an initialised [`bindings::device_attribute`] who= se +/// `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> { + attribute: Opaque, + _phantom: PhantomData, +} + +impl DeviceAttribute +where + O: AttributeOperations, +{ + /// 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 err= no. + /// + /// # Safety + /// + /// * `dev` must point to a valid `struct device` which stays valid fo= r the + /// duration of this call. + /// * `dev` must have a driver bound for the duration of this call, an= d 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 lea= st + /// [`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 allocate= d page + /// that it hands out to exactly one reader at a time, and kernfs hold= s an + /// active reference across the call so the group cannot be torn down + /// underneath it. The second is upheld by registering the group throu= gh + /// `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 w= hole + // period, which is what the `Bound` context asserts. + let dev =3D unsafe { Device::::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 pr= ivate + // data until after these files are gone, so the borrow cannot out= live + // the allocation. + let data =3D unsafe { dev.drvdata_unchecked::() }; + + // 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_ch= ar` + // and `u8` have the same size and alignment (1), so the cast to + // `[u8; PAGE_SIZE]` preserves layout and cannot introduce a misal= igned + // access. + let page =3D 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) =3D> size.min(PAGE_SIZE) as isize, + Err(err) =3D> 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. Returni= ng + /// 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 fo= r the + /// duration of this call. + /// * `dev` must have a driver bound for the duration of this call, an= d 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 ca= ll. + /// * `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` c= ontext + // asserts. + let dev =3D unsafe { Device::::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 =3D unsafe { dev.drvdata_unchecked::() }; + + match O::store( + data, + dev, + // SAFETY: By the function safety requirements, `page` is vali= d for + // reads of `size` bytes and is not mutated while this call ru= ns, 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) =3D> size.min(PAGE_SIZE) as isize, + Err(err) =3D> 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::sh= ow`] + /// 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 attribut= e 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` + // field, so initialising that one field initialises the w= hole + // 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 buil= ding an + /// [`AttributeList`]. + /// + /// The returned pointer inherits the lifetime of `&self`; callers mus= t 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 fi= eld 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 th= ere is +// no Rust-side state that needs synchronisation and `&Self` may be shared +// across threads. +unsafe impl> Sync + for DeviceAttribute +{ +} + +// 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> Send + for DeviceAttribute +{ +} + +/// Operations backing a single sysfs attribute. +/// +/// Implemented on an operations type, once per attribute; the `ID` parame= ter 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 { + /// The driver-private data type this attribute belongs to. + /// + /// Recovered from `dev->driver_data` by the trampolines, so it must b= e 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 retu= rns 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, + _buf: &mut [u8; PAGE_SIZE], + ) -> Result { + // Unreachable: `HAS_SHOW` is `false` for this impl, so + // `DeviceAttribute::new` leaves the C `show` pointer NULL. Reachi= ng + // here means the vtable was built inconsistently, hence a build e= rror + // 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 terminat= ed. + /// 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, _buf: &[u8]) -= > Result { + // Unreachable: `HAS_STORE` is `false` for this impl, so + // `DeviceAttribute::new` leaves the C `store` pointer NULL. Reach= ing + // here means the vtable was built inconsistently, hence a build e= rror + // 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 attribu= te 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 reac= hable +/// from the C side, and belongs to a `DeviceAttribute` whose `Data` is `D= `. +#[repr(transparent)] +pub struct AttributeList([*const bindings::attribute; N= ], PhantomData); + +impl AttributeList { + /// 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 mu= st + /// 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 priva= te 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 `DeviceAttrib= ute`s, +// which are themselves `Sync`. +unsafe impl Sync for AttributeList {} + +// SAFETY: An `AttributeList` owns no thread-affine state, so ownership ca= n be +// transferred between threads. +unsafe impl Send for AttributeList {} + +/// A group of sysfs attributes, i.e. `struct attribute_group`. +/// +/// A group with no name (as built below) places its attributes directly i= n the +/// device's sysfs directory; a named group would place them in a subdirec= tory. +/// `D` is carried over from the list so the private data type stays visib= le 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 f= ields +/// are zero. +#[repr(transparent)] +pub struct AttributeGroup(Opaque, PhantomDat= a); + +impl AttributeGroup { + /// Creates an unnamed group containing every attribute in `attributes= `. + pub const fn from_attribute_list( + attributes: &'static AttributeList, + ) -> 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 i= s 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`, a `umode_t` or a union of tho= se, and + // the all-zero bit pattern is valid for each of them: NUL= L name, + // no `is_visible`/`is_bin_visible` callbacks, no bin attr= ibutes. + // 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 constructi= on 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 Sync for AttributeGroup {} + +// SAFETY: An `AttributeGroup` owns no thread-affine state, so ownership c= an be +// transferred between threads. +unsafe impl Send for AttributeGroup {} + +/// 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, t= he +/// group and the terminator. +/// +/// # Invariants +/// +/// Element 0 points to a valid [`bindings::attribute_group`] that outlive= s this +/// value, and element 1 is NULL. +#[repr(transparent)] +pub struct AttributeGroups([*const bindings::attribute_group; 2], Phant= omData); + +impl AttributeGroups { + /// Creates a group array holding the single group `attribute_group`. + pub const fn new(attribute_group: &'static AttributeGroup) -> Self { + // INVARIANT: `attribute_group` is a `'static` reference, so the p= ointer + // stays valid forever, and the second element is the NULL termina= tor. + // + // The cast is layout-preserving: `AttributeGroup` is + // `repr(transparent)` over `Opaque` 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 d= ocs 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 `*m= ut` + // derived from `&self` is fine as long as callers never write thr= ough + // 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 th= reads. +// The groups it points to are themselves `Sync`. +unsafe impl Sync for AttributeGroups {} + +// SAFETY: An `AttributeGroups` owns no thread-affine state, so ownership = can be +// transferred between threads. +unsafe impl Send for AttributeGroups {} + +/// 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 t= ype the +/// trampolines will recover from the device, `ops:` is the type implement= ing the +/// attribute operations, and `attributes:` is a comma separated list of +/// identifiers, one per sysfs file. Each identifier must name a `u64` con= stant, +/// which is used as the `ID` type parameter, and `ops:` must implement +/// `AttributeOperations` 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 =3D 0; +/// const MODE: u64 =3D 1; +/// +/// #[vtable] +/// impl AttributeOperations for SampleDriver { +/// type Data =3D Self; +/// +/// fn show( +/// _data: Pin<&SampleDriver>, +/// _dev: &Device, +/// buf: &mut [u8; PAGE_SIZE], +/// ) -> Result { +/// buf[0] =3D b'h'; +/// Ok(1) +/// } +/// +/// fn store( +/// _data: Pin<&SampleDriver>, +/// _dev: &Device, +/// buf: &[u8; PAGE_SIZE], +/// ) -> Result { +/// // use the data stored in buf +/// Ok(buf.len()) +/// } +/// } +/// +/// #[vtable] +/// impl AttributeOperations for SampleDriver { +/// type Data =3D Self; +/// +/// fn show( +/// _data: Pin<&SampleDriver>, +/// _dev: &Device, +/// buf: &mut [u8; PAGE_SIZE], +/// ) -> Result { +/// buf[0] =3D b'1'; +/// Ok(1) +/// } +/// } +/// +/// let groups =3D 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),+ + $(,)? + ) =3D> { + { + use $crate::{ + c_str, + sysfs::{ + AttributeGroup, + AttributeGroups, + AttributeList, + DeviceAttribute, + } + }; + + use core::ptr; + + // One `static` per attribute, named `_ATTR`. It must b= e a + // `static` (not a `let`) so that the pointer taken below outl= ives + // this block and can be handed to the C side. + $( + $crate::macros::paste!{ + static [< $attributes:upper _ATTR >]: DeviceAttribute<= $attributes, $data, $ops> + =3D DeviceAttribute::new(c_str!(stringify!([< $att= ributes:lower >]))); + } + )+ + + // One slot per attribute plus one for the NULL terminator. The + // `let _ =3D $attributes;` is just a way to expand each repet= ition to + // `+ 1` while keeping the metavariable in the expansion. + const LEN: usize =3D 1 $( + { let _ =3D $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:: =3D 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 termina= ted + // group array that `dev_groups` expects. + static ATTRIBUTE_GROUP: AttributeGroup<$data> + =3D AttributeGroup::from_attribute_list(&ATTRIBUTE_LIST); + static ATTRIBUTE_GROUPS: AttributeGroups<$data> + =3D AttributeGroups::new(&ATTRIBUTE_GROUP); + + // Evaluates to a `&'static AttributeGroups`; `ATTRIBUTE_GROUP= S` is + // a `static`, so the borrow outlives the block. + &ATTRIBUTE_GROUPS + } + }; +} --=20 2.55.0 From nobody Tue Sep 29 06:59:03 2026 Received: from mail-wm1-f54.google.com (mail-wm1-f54.google.com [209.85.128.54]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 829E942B320 for ; Tue, 11 Aug 2026 09:42:48 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.54 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441371; cv=none; b=Ls6RaOgwzA/CHeBaPECqdzWrl+LcbaDlkJbjpktQy9DywsFA+1o6zyPnWJtmmiNZ9JY+yGspjrg11cUSWZkCMb64XxNwUiKvqKEare6qeU4ja3L9bnZjeB76VlRE4bzJfyxhCjfXotP0ZCKlDOmi7K1/LmR15aT8NNIhshF3YFM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786441371; c=relaxed/simple; bh=8zshD9o1aEgDQAPfsQQefG9ZjD9sl+/fnH6CGT3YDTU=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=V2uPAhHAJTd2zo4ix4/YN9chsBjVr9sPO5qIIaFEfcN6emOHqc57XjUGoEA5fWwtvsGQ479U+B58AXZ8XUSF1SAGzO/DDjTEK0ZYYQWiUDrtBrj/UFsQ9r5MTyu9Lu5ut99gu1YIof7qVO4Pmpm4tlfjym8/5/A7IWF41ad2X9g= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com; spf=pass smtp.mailfrom=wyliodrin.com; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b=KXO4NWum; arc=none smtp.client-ip=209.85.128.54 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=wyliodrin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=wyliodrin.com header.i=@wyliodrin.com header.b="KXO4NWum" Received: by mail-wm1-f54.google.com with SMTP id 5b1f17b1804b1-4954dff6536so22531315e9.0 for ; Tue, 11 Aug 2026 02:42:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=wyliodrin.com; s=google; t=1786441367; x=1787046167; darn=vger.kernel.org; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=bXGxdGnTIFSXmiJ8Kpd6ItKriKpKtoQz+U+0jIIi7d8=; b=KXO4NWum5kQi9baOQ3AemgdXjMuQgSiGQ7BnKC3YGMzVQBBjfuWeHpXbh2eqRAVH6z fpa5FsTmt01ptmDQ9MK7toCrT2kLZRDDilQih2bSuro9vB4xQiDyC1yIs4nIMZtwRl8Z Ab3IMLVitTRzfI8dZOgSqLMM70EOgCzCk+4bw= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786441367; x=1787046167; h=cc:to:in-reply-to:references:message-id:content-transfer-encoding :content-type:mime-version:subject:date:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=bXGxdGnTIFSXmiJ8Kpd6ItKriKpKtoQz+U+0jIIi7d8=; b=XsIVYXQhiNTm1W1WOofcng5VWhKqTrnq994ZgkF8jPctIicNDwj1pVSxERvxYM0ffJ yFtOFVyv1dqugpEKirUKTM/s2uYOV9oARcFXQDaeqT28mExYbUYCHo5SC+SEADJ+k/6t Ati50awRdILv7X0aMHxdvHPN+YXIueZyCpaU2XewEo9gNINc8Mql+B6W8X/y0NXLlTrJ iRQHsWoAuJ6St/3AsdflZH4rbbUiVvVvw6XTDZmKbTslrrhEva1wyJxEPbqIirSH8jP8 rZc605OG6owEAvmKwqoyBUnVcYmp7BaXJzbo62T7HxFZ6ml6b+5WCtoDnMkD/bzoq3SX ZrYg== X-Gm-Message-State: AOJu0Yyx75TioFtAuR6d+zI2zKYP4jv8eRQJpNGjHETeotYAXrXGTiFm lZ5IYwNZnmCZTJOL62zz7kZprVQDqfAxwXy3L9gfcrrwAehfdZfF4xJgCdcbcHRrhc8= X-Gm-Gg: AR+sD13Bf/MxKHtHcQPL2NP7XAcEQSHo8XV2faPntd1cETpUsmdII+rJh/yyNQBShhP XctMWZPhw3463SvmuK5BQKJ2asEuUNShFNs+ze8dHwUYVlZ4O6UUqeyA5AKwv2cNnTV8NASSRYS VAcx2rVhCETa/+dFav/dAejNX6CFtetUt+7ufGF+MgQzrL122qIEkoQqDe7V9+6iOGxVzwcnMD6 IlqHfsk4tmwQr469qc8su6GhgIzay/tbSSx0TZWKTijypnJNbV9mcOVEskodxvd2i2JW/Povv3j gMjLzKq+CgW1UDVizk7GMx0jy9bZDaWetN6GbW8Car8puzY9ESc00YxAjGBD2bWwase4zjexcYN usALlQYH2mzZIxt7zg43Afudk0KthEq32M8A6jazgOqpmDmPY6834888dunWZlwu7KmqnUX7JSj 0zfyVfhsPcir3wo6iItSmEATZexMrkiZyMoBJ68GeB113YMYipu91fHY7YEolVESYEONcoFwXqK 5f03QObjoZI9CkH058Vz/E1aWErHU7Wo1n4EQ== X-Received: by 2002:a05:6000:4802:b0:473:6e8d:7f3 with SMTP id ffacd0b85a97d-4814ad77f51mr3692843f8f.1.1786441366646; Tue, 11 Aug 2026 02:42:46 -0700 (PDT) Received: from [10.0.36.57] ([217.73.170.83]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-4814a709624sm3388917f8f.22.2026.08.11.02.42.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 11 Aug 2026 02:42:46 -0700 (PDT) From: Alexandru Radovici Date: Tue, 11 Aug 2026 12:42:31 +0300 Subject: [PATCH RFC v2 4/4] rust: usb: allow drivers to expose sysfs attributes Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260811-rust-usb_control_msg-v2-4-ef79c92bd898@wyliodrin.com> References: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> In-Reply-To: <20260811-rust-usb_control_msg-v2-0-ef79c92bd898@wyliodrin.com> To: Greg Kroah-Hartman , Miguel Ojeda , Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , Danilo Krummrich , Daniel Almeida , Tamir Duberstein , Alexandre Courbot , =?utf-8?q?Onur_=C3=96zkan?= , "Rafael J. Wysocki" Cc: linux-kernel@vger.kernel.org, linux-usb@vger.kernel.org, rust-for-linux@vger.kernel.org, driver-core@lists.linux.dev, Alexandru Radovici X-Mailer: b4 0.14.3 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> 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 --- 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, // }; =20 @@ -64,6 +68,11 @@ unsafe fn register( (*udrv.get()).probe =3D Some(Self::probe_callback); (*udrv.get()).disconnect =3D Some(Self::disconnect_callback); (*udrv.get()).id_table =3D T::ID_TABLE.as_ptr(); + (*udrv.get()).dev_groups =3D if let Some(dev_group) =3D T::DEV= ICE_GROUPS { + dev_group.as_ptr() + } else { + ptr::null_mut() + }; } =20 // 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; =20 + /// The sysfs attribute groups to create for interfaces bound to this = driver. + /// + /// Defaults to `None`, i.e. the driver exposes no attributes of its o= wn. + /// Build the value with [`attribute_list!`](crate::attribute_list), w= hich + /// 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 *interfa= ce*, not + /// of the USB device, for instance `/sys/bus/usb/devices/1-1:1.0/`. B= ecause + /// `dev_groups` belongs to the driver rather than to one device, every + /// interface this driver binds to gets the same set of files, and the= re is no + /// way to hide an individual attribute for some interfaces. + /// + /// # Registration window + /// + /// The array is stored in `struct usb_driver::dev_groups`, which usbc= ore + /// forwards to the embedded `struct device_driver`. The driver core c= reates + /// the files only after [`Driver::probe`] has returned successfully a= nd + /// removes them before [`Driver::disconnect`] runs, so an attribute c= allback + /// always finds live private data on the interface. That is what make= s it + /// sound for the callbacks to recover it at all. Groups installed any= where + /// that is populated earlier, such as a `device_type`, would expose t= he 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` fo= r the + /// same interface at once, so [`Self::Data`] has to be `Sync` for a d= river + /// that sets this to `Some`. The bound is deliberately not stated her= e: it + /// comes from `AttributeOperations::Data`, so it is checked at the + /// `attribute_list!` call site rather than being imposed on every dri= ver, + /// 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 curre= nt + /// binding, so the tag names a different instantiation of the GAT tha= n 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` ho= lding + /// `&'bound` references can leak them out of an attribute callback wi= th a + /// longer lifetime than they have, and nothing here catches it. + /// + /// # Examples + /// + /// ```ignore + /// const BLINK: u64 =3D 0; + /// + /// // No `'bound` borrows, so `Data<'static>` is the type that exists. + /// struct MyData { blinking: AtomicBool } + /// + /// impl usb::Driver for MyDriver { + /// type Data<'bound> =3D MyData; + /// + /// const DEVICE_GROUPS: Option<&'static AttributeGroups>> =3D + /// Some(kernel::attribute_list!( + /// data: MyData, + /// ops: MyDriver, + /// attributes: BLINK, + /// )); + /// + /// // ... ID_TABLE, probe, disconnect + /// } + /// + /// impl kernel::sysfs::AttributeOperations for MyDriver { + /// type Data =3D MyData; + /// + /// fn show( + /// data: Pin<&MyData>, + /// _dev: &Device, + /// buf: &mut [u8; PAGE_SIZE], + /// ) -> Result { + /// // Format into `buf` and return the byte count. + /// Ok(0) + /// } + /// } + /// ``` + const DEVICE_GROUPS: Option<&'static AttributeGroups>> =3D 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 @@ =20 //! Rust USB driver sample. =20 +const ATTR1: u64 =3D 0; +const ATTR2: u64 =3D 1; + use kernel::{ device::{ self, @@ -10,6 +13,7 @@ }, prelude::*, sync::aref::ARef, + sysfs::AttributeOperations, usb, // }; =20 @@ -17,6 +21,16 @@ struct SampleDriver { _intf: ARef, } =20 +#[vtable] +impl AttributeOperations for SampleDriver { + type Data =3D Self; +} + +#[vtable] +impl AttributeOperations for SampleDriver { + type Data =3D Self; +} + kernel::usb_device_table!( USB_TABLE, MODULE_USB_TABLE, --=20 2.55.0