From: sashiko-bot@kernel.org
To: "Markus Probst" <markus.probst@posteo.de>
Cc: linux-leds@vger.kernel.org, lee@kernel.org,
linux-pci@vger.kernel.org, ojeda@kernel.org
Subject: Re: [PATCH v20 1/3] rust: leds: add basic led classdev abstractions
Date: Mon, 29 Jun 2026 10:31:51 +0000 [thread overview]
Message-ID: <20260629103152.024461F000E9@smtp.kernel.org> (raw)
In-Reply-To: <20260629-rust_leds-v20-1-f493ab053b4f@posteo.de>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] FwNode reference count leak and underflow (Use-After-Free) due to retrieving fwnode from C device in drop
- [High] FwNode reference count is decremented before unregistering the LED device, leading to a Use-After-Free during unregistration.
- [High] Lifetime erasure in `DeviceBuilder::build` allows `Device` to outlive the `name` string, causing a dangling pointer in `led_classdev.name`.
- [High] Creating `&mut usize` from shared C-provided pointers in `blink_set_callback` violates Rust's exclusive mutability guarantee (Undefined Behavior).
--
commit 5790f9c92f07b2ed588fa76855369d33b813c58c
Author: Markus Probst <markus.probst@posteo.de>
rust: leds: add basic led classdev abstractions
Implement the core abstractions needed for led class devices, including:
* `led::LedOps` - the trait for handling leds, including
`brightness_set`, `brightness_get` and `blink_set`
* `led::DeviceBuilder` - the builder for the led class device
* `led::Device` - a safe wrapper around `led_classdev`
> diff --git a/rust/kernel/led/normal.rs b/rust/kernel/led/normal.rs
> new file mode 100644
> index 0000000000000..a294c96b34dc2
> --- /dev/null
> +++ b/rust/kernel/led/normal.rs
[ ... ]
> +impl<'a, S: DeviceBuilderState> DeviceBuilder<'a, S> {
> + /// Registers a new [`Device`].
> + pub fn build<'bound: 'a, T: LedOps + 'bound>(
> + self,
> + parent: &'bound T::Bus,
> + ops: impl PinInit<T, Error> + 'a,
> + ) -> impl PinInit<Device<'bound, T>, Error> + 'a {
> + const_assert!(T::MAX_BRIGHTNESS <= i32::MAX.unsigned_abs() || !T::HAS_BRIGHTNESS_GET);
> +
> + try_pin_init!(Device {
> + ops <- ops,
> + classdev <- Opaque::try_ffi_init(|ptr: *mut bindings::led_classdev| {
> + // SAFETY: `try_ffi_init` guarantees that `ptr` is valid for write.
> + // `led_classdev` gets fully initialized in-place by
> + // `led_classdev_register_ext` including `mutex` and `list_head`.
> + unsafe {
> + ptr.write(bindings::led_classdev {
> + brightness_set: (!T::BLOCKING)
> + .then_some(Adapter::<T>::brightness_set_callback),
> + brightness_set_blocking: T::BLOCKING
> + .then_some(Adapter::<T>::brightness_set_blocking_callback),
> + brightness_get: T::HAS_BRIGHTNESS_GET
> + .then_some(Adapter::<T>::brightness_get_callback),
> + blink_set: T::HAS_BLINK_SET.then_some(Adapter::<T>::blink_set_callback),
> + max_brightness: T::MAX_BRIGHTNESS,
> + brightness: self.initial_brightness,
> + color: self.color as u32,
> + name: self.name.map_or(core::ptr::null(), CStrExt::as_char_ptr),
^^^^
[Severity: High]
Does this allow `Device` to outlive the `name` string, leaving a dangling
pointer in `led_classdev.name`?
The `DeviceBuilder` accepts a short-lived `name: &'a CStr`, and the pointer
is copied into the C `led_classdev` struct. The returned `Device<'bound, T>`
only enforces `'bound: 'a` (meaning `'bound` outlives `'a`). Because the
resulting `Device` type itself does not capture the `'a` lifetime, the Rust
borrow checker allows the `Device` to outlive the `name` string.
If a driver uses a dynamically allocated local string or a short-lived format
buffer for the device name, will this leave a dangling pointer that gets
triggered upon system suspend/resume?
> + ..bindings::led_classdev::default()
> + })
> + };
[ ... ]
> + unsafe extern "C" fn blink_set_callback(
> + led_cdev: *mut bindings::led_classdev,
> + delay_on: *mut usize,
> + delay_off: *mut usize,
> + ) -> i32 {
> + from_result(|| {
> + // SAFETY: The function's contract guarantees that `led_cdev` is a valid pointer to a
> + // `led_classdev` embedded within a `led::Device`.
> + let classdev = unsafe { Device::<T>::from_raw(led_cdev) };
> + // SAFETY: `classdev.parent()` is guaranteed to be contained in `T::Bus`.
> + let parent = unsafe { T::Bus::from_device(classdev.parent()) };
> +
> + classdev.ops.blink_set(
> + parent,
> + classdev,
> + // SAFETY: The function's contract guarantees that `delay_on` points to a `usize`
> + // and is exclusive for the period of this function.
> + unsafe { &mut *delay_on },
^^^^
[Severity: High]
Does creating `&mut usize` from these shared C-provided pointers violate
Rust's exclusive mutability guarantees?
In `Adapter::blink_set_callback`, the raw pointers `delay_on` and `delay_off`
are unconditionally cast to exclusive Rust references (`&mut usize`). These
pointers are passed from the C LED core, where they directly point to the
`led_cdev->blink_delay_on` and `blink_delay_off` fields.
Since these same fields are concurrently readable by lockless sysfs functions
like `led_delay_on_show()`, they can be read concurrently while the `&mut`
reference is alive. Does this result in undefined behavior since they are not
exclusively owned by the callback?
> + // SAFETY: The function's contract guarantees that `delay_off` points to a `usize`
> + // and is exclusive for the period of this function.
> + unsafe { &mut *delay_off },
> + )?;
> + Ok(0)
> + })
> + }
[ ... ]
> +#[pinned_drop]
> +impl<'bound, T: LedOps + 'bound> PinnedDrop for Device<'bound, T> {
> + fn drop(self: Pin<&mut Self>) {
> + let raw = self.classdev.get();
> + // SAFETY: The existence of `self` guarantees that `self.classdev.get()` is a pointer to a
> + // valid `led_classdev`.
> + let dev: &device::Device = unsafe { device::Device::from_raw((*raw).dev) };
> +
> + if let Some(fwnode) = dev.fwnode() {
> + // SAFETY: the reference count of `fwnode` has previously been incremented in
> + // `led::DeviceBuilder::build`.
> + unsafe { FwNode::dec_ref(NonNull::from(fwnode)) };
^^^^
[Severity: High]
Can this cause a reference count underflow and use-after-free for the fwnode?
If `DeviceBuilder::build()` is called without an explicitly provided `fwnode`,
it does not leak a reference. However, during registration, the C core
`device_add()` automatically inherits the parent's `fwnode` if the device
has none.
Later, in `PinnedDrop::drop()`, this code blindly calls `dev.fwnode()` and
decrements its reference count. Doesn't this erroneously decrement the parent
device's `fwnode` reference count, which it never owned?
> + }
> +
> + // SAFETY: The existence of `self` guarantees that `self.classdev` has previously been
> + // successfully registered with `led_classdev_register_ext`.
> + unsafe { bindings::led_classdev_unregister(raw) };
^^^^
[Severity: High]
Is it safe to decrement the fwnode reference count before unregistering the
LED device?
The `FwNode::dec_ref()` call happens before
`bindings::led_classdev_unregister()`. If this was the last reference, the
fwnode is immediately freed. Then, `led_classdev_unregister()` calls
`device_unregister()` and subsequently `device_del()`. Inside `device_del()`,
the core driver framework accesses `dev->fwnode`.
Could this result in a use-after-free when the C core attempts to access the
fwnode to clean up software nodes and links?
> + }
> +}
Note: The issues regarding `DeviceBuilder::build`, `blink_set_callback`, and
`PinnedDrop::drop` appear to also apply to `MultiColorDevice` in
`rust/kernel/led/multicolor.rs`.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260629-rust_leds-v20-0-f493ab053b4f@posteo.de?part=1
next prev parent reply other threads:[~2026-06-29 10:31 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-29 10:15 [PATCH v20 0/3] rust: leds: add led classdev abstractions Markus Probst
2026-06-29 10:15 ` [PATCH v20 1/3] rust: leds: add basic " Markus Probst
2026-06-29 10:31 ` sashiko-bot [this message]
2026-06-29 10:15 ` [PATCH v20 2/3] rust: leds: add Mode trait Markus Probst
2026-06-29 10:20 ` sashiko-bot
2026-06-29 10:15 ` [PATCH v20 3/3] rust: leds: add multicolor classdev abstractions Markus Probst
2026-06-29 10:34 ` sashiko-bot
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=20260629103152.024461F000E9@smtp.kernel.org \
--to=sashiko-bot@kernel.org \
--cc=lee@kernel.org \
--cc=linux-leds@vger.kernel.org \
--cc=linux-pci@vger.kernel.org \
--cc=markus.probst@posteo.de \
--cc=ojeda@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/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