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 <[email protected]>
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

Reply via email to