From: Mike Lothian <mike@fireburn.co.uk>
To: rust-for-linux@vger.kernel.org
Cc: dri-devel@lists.freedesktop.org,
"David Airlie" <airlied@gmail.com>,
"Simona Vetter" <simona@ffwll.ch>,
"Danilo Krummrich" <dakr@kernel.org>,
"Lyude Paul" <lyude@redhat.com>,
"Miguel Ojeda" <ojeda@kernel.org>,
"Boqun Feng" <boqun@kernel.org>, "Gary Guo" <gary@garyguo.net>,
"Björn Roy Baron" <bjorn3_gh@protonmail.com>,
"Benno Lossin" <lossin@kernel.org>,
"Andreas Hindborg" <a.hindborg@kernel.org>,
"Alice Ryhl" <aliceryhl@google.com>,
"Trevor Gross" <tmgross@umich.edu>,
linux-kernel@vger.kernel.org,
"Mike Lothian" <mike@fireburn.co.uk>
Subject: [RFC PATCH v2 14/18] rust: add sysfs device attribute groups
Date: Fri, 3 Jul 2026 04:01:01 +0100 [thread overview]
Message-ID: <20260703030123.2814-15-mike@fireburn.co.uk> (raw)
In-Reply-To: <20260703030123.2814-1-mike@fireburn.co.uk>
A driver that exposes control files under sysfs -- for instance DisplayLink's
evdi, whose /sys/devices/evdi/{count,add,remove_all} files let the daemon create
and destroy virtual display cards -- had no safe way to do so from Rust.
Add a sysfs module: a driver implements DeviceAttributes (an ATTRS list plus
show()/store(), dispatched by attribute name) for the type it stores as a
device's driver data, and AttributeGroup::register_root() creates a standalone
root device (root_device_register(), under /sys/devices/<name>) hosting the
files. Reads and writes trampoline to show()/store() with the driver data
recovered from the device; the file mode (Attr::ro/wo/rw) gates access. The group
removes the files, reclaims the driver data and unregisters the device on drop.
Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
rust/kernel/lib.rs | 1 +
rust/kernel/sysfs.rs | 245 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 246 insertions(+)
create mode 100644 rust/kernel/sysfs.rs
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 8baa13079071..13729ec06333 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -128,6 +128,7 @@
pub mod std_vendor;
pub mod str;
pub mod sync;
+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..f6df17c5013d
--- /dev/null
+++ b/rust/kernel/sysfs.rs
@@ -0,0 +1,245 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Sysfs device attributes.
+//!
+//! A driver can expose a set of named sysfs files on a device by implementing [`DeviceAttributes`]
+//! for the type it stores as that device's driver data, and registering an [`AttributeGroup`].
+//! Reads and writes are dispatched by attribute name to the type's [`show`](DeviceAttributes::show)
+//! and [`store`](DeviceAttributes::store) methods; the file mode (see [`Attr`]) controls which are
+//! reachable from userspace.
+//!
+//! [`AttributeGroup::register_root`] additionally creates a standalone "root" device
+//! (`root_device_register()`, appearing under `/sys/devices/<name>`) to host the group -- the shape
+//! DisplayLink's evdi uses for its `add`/`remove`/`count` control files.
+//!
+//! C headers: [`include/linux/sysfs.h`](srctree/include/linux/sysfs.h),
+//! [`include/linux/device.h`](srctree/include/linux/device.h)
+
+use crate::{
+ bindings,
+ error::{from_err_ptr, to_result},
+ page::PAGE_SIZE,
+ prelude::*,
+ types::ForeignOwnable,
+ ThisModule,
+};
+
+/// Definition of one sysfs attribute file: its name and permission mode (e.g. `0o444` read-only,
+/// `0o200` write-only, `0o644` read-write).
+pub struct Attr {
+ /// The file name.
+ pub name: &'static CStr,
+ /// The permission bits (`umode_t`).
+ pub mode: u16,
+}
+
+impl Attr {
+ /// A read-only (`0o444`) attribute.
+ pub const fn ro(name: &'static CStr) -> Self {
+ Self { name, mode: 0o444 }
+ }
+ /// A write-only (`0o200`) attribute.
+ pub const fn wo(name: &'static CStr) -> Self {
+ Self { name, mode: 0o200 }
+ }
+ /// A read-write (`0o644`) attribute.
+ pub const fn rw(name: &'static CStr) -> Self {
+ Self { name, mode: 0o644 }
+ }
+}
+
+/// The show/store behaviour of a device's sysfs attribute group.
+///
+/// Implemented by the type a device stores as its driver data. A shared reference to it is handed
+/// to [`show`](Self::show)/[`store`](Self::store), dispatched by the attribute `name`.
+pub trait DeviceAttributes: Send + Sync + 'static {
+ /// The attributes exposed by this group.
+ const ATTRS: &'static [Attr];
+
+ /// Handle a read of attribute `name`, writing up to one page into `buf` and returning the
+ /// number of bytes written. Only called for readable (`ro`/`rw`) attributes.
+ fn show(&self, name: &CStr, buf: &mut [u8]) -> Result<usize> {
+ let _ = (name, buf);
+ Err(EINVAL)
+ }
+
+ /// Handle a write of `buf` to attribute `name`. Only called for writable (`wo`/`rw`)
+ /// attributes.
+ fn store(&self, name: &CStr, buf: &[u8]) -> Result {
+ let _ = (name, buf);
+ Err(EINVAL)
+ }
+}
+
+/// A registered sysfs attribute group hosted on a `root_device_register()` device.
+///
+/// Dropping it removes the files, reclaims the driver data, and unregisters the root device.
+pub struct AttributeGroup<T: DeviceAttributes> {
+ root: *mut bindings::device,
+ /// Backing storage for the `device_attribute`s handed to `device_create_file()`; must stay
+ /// alive (and not move) for as long as the files exist, so it lives in this heap allocation.
+ attrs: KVec<bindings::device_attribute>,
+ _p: core::marker::PhantomData<T>,
+}
+
+// SAFETY: the root device and its attributes are internally synchronized by the driver core; `T`
+// is `Send + Sync`.
+unsafe impl<T: DeviceAttributes> Send for AttributeGroup<T> {}
+// SAFETY: see `Send`.
+unsafe impl<T: DeviceAttributes> Sync for AttributeGroup<T> {}
+
+impl<T: DeviceAttributes> AttributeGroup<T> {
+ /// # Safety
+ /// `dev`'s driver data is a live `T` set via [`device::Device::set_drvdata`].
+ unsafe extern "C" fn show_trampoline(
+ dev: *mut bindings::device,
+ attr: *mut bindings::device_attribute,
+ buf: *mut crate::ffi::c_char,
+ ) -> isize {
+ // SAFETY: `dev` is a valid device with `T` driver data (invariant of `register_root`).
+ let ctx = unsafe { borrow_ctx::<T>(dev) };
+ // SAFETY: the attribute name is a valid C string for the callback's duration.
+ let name = unsafe { as_cstr((*attr).attr.name) };
+ // SAFETY: sysfs guarantees `buf` is a writable page.
+ let slice = unsafe { core::slice::from_raw_parts_mut(buf.cast::<u8>(), PAGE_SIZE) };
+ match T::show(ctx, name, slice) {
+ Ok(n) => core::cmp::min(n, PAGE_SIZE) as isize,
+ Err(e) => e.to_errno() as isize,
+ }
+ }
+
+ /// # Safety
+ /// `dev`'s driver data is a live `T` set via [`device::Device::set_drvdata`].
+ unsafe extern "C" fn store_trampoline(
+ dev: *mut bindings::device,
+ attr: *mut bindings::device_attribute,
+ buf: *const crate::ffi::c_char,
+ count: usize,
+ ) -> isize {
+ // SAFETY: as in `show_trampoline`.
+ let ctx = unsafe { borrow_ctx::<T>(dev) };
+ // SAFETY: the attribute name is a valid C string for the callback's duration.
+ let name = unsafe { as_cstr((*attr).attr.name) };
+ // SAFETY: sysfs guarantees `buf` holds `count` readable bytes.
+ let slice = unsafe { core::slice::from_raw_parts(buf.cast::<u8>(), count) };
+ match T::store(ctx, name, slice) {
+ Ok(()) => count as isize,
+ Err(e) => e.to_errno() as isize,
+ }
+ }
+
+ fn make_attr(a: &Attr) -> bindings::device_attribute {
+ let mut da: bindings::device_attribute = bindings::device_attribute::default();
+ da.attr.name = a.name.as_char_ptr();
+ da.attr.mode = a.mode;
+ da.__bindgen_anon_1.show = Some(Self::show_trampoline);
+ da.__bindgen_anon_2.store = Some(Self::store_trampoline);
+ da
+ }
+
+ /// Create a root device named `name` and expose `T`'s attributes on it.
+ ///
+ /// `T` becomes the device's driver data; the attribute callbacks recover it by name-dispatch.
+ pub fn register_root(
+ name: &CStr,
+ module: &'static ThisModule,
+ ctx: impl PinInit<T, Error>,
+ ) -> Result<KBox<Self>> {
+ // Build the attribute array up front (no external resources yet, so a failure here needs
+ // no cleanup).
+ let mut attrs: KVec<bindings::device_attribute> =
+ KVec::with_capacity(T::ATTRS.len(), GFP_KERNEL)?;
+ for a in T::ATTRS {
+ attrs.push(Self::make_attr(a), GFP_KERNEL)?;
+ }
+ // Box the context; its foreign pointer becomes the device's driver data.
+ let ctx_ptr = KBox::pin_init(ctx, GFP_KERNEL)?.into_foreign();
+
+ // SAFETY: `name` is a valid C string; `module` is this module.
+ let root = match from_err_ptr(unsafe {
+ bindings::__root_device_register(name.as_char_ptr(), module.as_ptr())
+ }) {
+ Ok(r) => r,
+ Err(e) => {
+ // Reclaim the context box we have not attached anywhere yet.
+ // SAFETY: `ctx_ptr` came from `into_foreign()` above and was not consumed.
+ drop(unsafe { <Pin<KBox<T>> as ForeignOwnable>::from_foreign(ctx_ptr) });
+ return Err(e);
+ }
+ };
+ // SAFETY: `root` is a freshly-registered, valid device; `ctx_ptr` is its driver data now.
+ unsafe { bindings::dev_set_drvdata(root, ctx_ptr) };
+
+ // Register each file against its stable slot in `attrs`.
+ for da in attrs.iter() {
+ if let Err(e) = to_result(unsafe { bindings::device_create_file(root, da) }) {
+ // `root_device_unregister` (device_del) removes any files created so far;
+ // reclaim the context box first.
+ Self::teardown(root);
+ return Err(e);
+ }
+ }
+
+ match KBox::new(
+ Self {
+ root,
+ attrs,
+ _p: core::marker::PhantomData,
+ },
+ GFP_KERNEL,
+ ) {
+ Ok(group) => Ok(group),
+ Err(e) => {
+ Self::teardown(root);
+ Err(e.into())
+ }
+ }
+ }
+
+ /// Reclaim the driver-data box and unregister the root device (which removes its files).
+ fn teardown(root: *mut bindings::device) {
+ // SAFETY: `root` is a valid registered device whose driver data is a `Pin<KBox<T>>` (or
+ // NULL); `dev_get_drvdata` returns the pointer stashed in `register_root`.
+ let ptr = unsafe { bindings::dev_get_drvdata(root) };
+ if !ptr.is_null() {
+ // SAFETY: `ptr` came from `Pin::<KBox<T>>::into_foreign`.
+ drop(unsafe { <Pin<KBox<T>> as ForeignOwnable>::from_foreign(ptr) });
+ }
+ // SAFETY: `root` was created by `__root_device_register` and not yet unregistered.
+ unsafe { bindings::root_device_unregister(root) };
+ }
+}
+
+impl<T: DeviceAttributes> Drop for AttributeGroup<T> {
+ fn drop(&mut self) {
+ for da in self.attrs.iter() {
+ // SAFETY: each `da` was passed to `device_create_file` on `self.root` and not removed
+ // since.
+ unsafe { bindings::device_remove_file(self.root, da) };
+ }
+ // Reclaim the driver-data box and unregister the root device.
+ Self::teardown(self.root);
+ }
+}
+
+/// Borrow the `T` driver data of the device `ptr`.
+///
+/// # Safety
+/// `ptr` is a valid `struct device` whose driver data is a `Pin<KBox<T>>` stashed by
+/// [`AttributeGroup::register_root`], valid for the returned reference's lifetime.
+unsafe fn borrow_ctx<'a, T>(ptr: *mut bindings::device) -> &'a T {
+ // SAFETY: `ptr` is a valid device by the contract.
+ let data = unsafe { bindings::dev_get_drvdata(ptr) };
+ // SAFETY: `data` is the pointer `Pin<KBox<T>>::into_foreign` returned, i.e. a valid `*mut T`
+ // live for the borrow.
+ unsafe { &*(data as *const T) }
+}
+
+/// # Safety
+/// `ptr` is a valid, NUL-terminated C string for the returned reference's lifetime.
+#[inline]
+unsafe fn as_cstr<'a>(ptr: *const crate::ffi::c_char) -> &'a CStr {
+ // SAFETY: by the contract, `ptr` is a valid NUL-terminated C string. `crate::ffi::c_char` is
+ // `u8` while `core::ffi::CStr::from_ptr` takes `*const i8`, so cast the pointer.
+ unsafe { CStr::from_ptr(ptr.cast()) }
+}
--
2.55.0
next prev parent reply other threads:[~2026-07-03 3:02 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-17 15:02 [RFC PATCH 0/5] rust: drm: minimal KMS bindings, EDID read, rotation, HDCP defs Mike Lothian
2026-06-17 15:02 ` [RFC PATCH 1/4] rust: drm: add minimal KMS bindings for simple-display-pipe drivers Mike Lothian
2026-06-17 15:02 ` [RFC PATCH 2/4] rust: drm: expose drm_edid.h for reading connector EDID Mike Lothian
2026-06-17 15:02 ` [RFC PATCH 3/4] rust: drm: expose drm::Device::as_raw() Mike Lothian
2026-06-17 15:02 ` [RFC PATCH 4/4] rust: drm: expose drm_blend.h and the atomic new-CRTC-state accessor Mike Lothian
2026-06-17 15:11 ` [RFC PATCH 0/5] rust: drm: minimal KMS bindings, EDID read, rotation, HDCP defs Miguel Ojeda
2026-06-17 15:29 ` Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 00/18] rust: drm: safe KMS mode-object layer + evdi bindings Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 01/18] rust: drm: kms: forward-port the safe mode-object layer onto the typestate device Mike Lothian
2026-07-07 21:46 ` lyude
2026-07-07 22:21 ` lyude
2026-07-03 3:00 ` [RFC PATCH v2 02/18] rust: drm: kms: adapt the port to current drm-next Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 03/18] rust: drm: kms: break the Driver* trait well-formedness cycle Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 04/18] rust: drm: kms: build the kernel crate clean under -Znext-solver Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 05/18] rust: drm: expose <drm/display/drm_hdcp.h> HDCP 2.2 message definitions Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 06/18] rust: drm: kms: add a Framebuffer::vmap() guard Mike Lothian
2026-07-07 21:51 ` lyude
2026-07-03 3:00 ` [RFC PATCH v2 07/18] rust: drm: kms: add safe accessors for common state and connector modes Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 08/18] rust: drm: tyr: add the Kms associated type Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 09/18] rust: drm: add drm_event delivery Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 10/18] rust: drm: allow drivers to declare ioctls from their own uAPI module Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 11/18] rust: platform: add runtime platform device creation Mike Lothian
2026-07-03 3:00 ` [RFC PATCH v2 12/18] rust: drm: framebuffer: add geometry accessors, refcounting and a byte-slice vmap Mike Lothian
2026-07-03 3:01 ` [RFC PATCH v2 13/18] rust: i2c: add adapter-provider (bus controller) registration Mike Lothian
2026-07-03 3:01 ` Mike Lothian [this message]
2026-07-03 3:01 ` [RFC PATCH v2 15/18] rust: drm: support hardware cursor planes with sleepable event delivery Mike Lothian
2026-07-03 3:01 ` [RFC PATCH v2 16/18] rust: drm: add CRTC gamma LUT and plane rotation property bindings Mike Lothian
2026-07-03 3:01 ` [RFC PATCH v2 17/18] rust: drm: kms: add connector detect() and mode_valid() hooks Mike Lothian
2026-07-03 3:01 ` [RFC PATCH v2 18/18] rust: drm: kms: add plane damage-clip accessors Mike Lothian
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260703030123.2814-15-mike@fireburn.co.uk \
--to=mike@fireburn.co.uk \
--cc=a.hindborg@kernel.org \
--cc=airlied@gmail.com \
--cc=aliceryhl@google.com \
--cc=bjorn3_gh@protonmail.com \
--cc=boqun@kernel.org \
--cc=dakr@kernel.org \
--cc=dri-devel@lists.freedesktop.org \
--cc=gary@garyguo.net \
--cc=linux-kernel@vger.kernel.org \
--cc=lossin@kernel.org \
--cc=lyude@redhat.com \
--cc=ojeda@kernel.org \
--cc=rust-for-linux@vger.kernel.org \
--cc=simona@ffwll.ch \
--cc=tmgross@umich.edu \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox