Linux LED subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Salah Ahmed" <haddad2inc@gmail.com>, linux-leds@vger.kernel.org
Cc: lee@kernel.org
Subject: Re: [PATCH] leds: trigger-netdev: add byte-rate threshold mode
Date: Fri, 30 Oct 2026 22:18:56 +0000	[thread overview]
Message-ID: <sashiko-outbox-191572@kernel.org> (raw)
In-Reply-To: <20261030215924.18060-1-haddad2inc@gmail.com>

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] leds: trigger-netdev: divide by zero in netdev_trig_work() when elapsed is 0
- [High] leds: trigger-netdev: set_baseline_state() skips stopping blink on hw_control
- [High] leds: trigger-netdev: race in rate_threshold_store() due to missing lock
- [Medium] leds: trigger-netdev: rate_threshold_store() polls while link is down
- [Medium] leds: trigger-netdev: rate_smooth decay in netdev_trig_work() clamps afterglow
- [Medium] leds: trigger-netdev: missing rate reset on link down in netdev_trig_notify()
- [Medium] leds: trigger-netdev: rate mode fails to stop hw blink in netdev_set_resting()
--

commit e9ed82389d653425379282a2ce97b71abb1b8c6e
Author: Salah Ahmed <haddad2inc@gmail.com>

leds: trigger-netdev: add byte-rate threshold mode

This patch adds a rate_threshold sysfs attribute to the netdev LED trigger so
that when non-zero, the LED blinks on a 50..150 ms ladder based on the
interface byte rate rather than per-packet counter updates, with smoothing
and a 20-second quadratic afterglow before returning to the baseline link
state.

> diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c
> index 5b0132484594c..941cb13cdf04e 100644
> --- a/drivers/leds/trigger/ledtrig-netdev.c
> +++ b/drivers/leds/trigger/ledtrig-netdev.c

[ ... ]

> @@ -79,97 +120,140 @@ struct led_netdev_data {
>  
>  static const struct attribute_group netdev_trig_link_speed_attrs_group;
>  
> -static void set_baseline_state(struct led_netdev_data *trigger_data)

[ ... ]

> -		if (test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &trigger_data->mode) &&
> -		    trigger_data->duplex == DUPLEX_HALF)
> -			blink_on = true;
> +static void netdev_set_resting(struct led_netdev_data *trigger_data)
> +{
> +	struct led_classdev *led_cdev = trigger_data->led_cdev;
>  
> -		if (test_bit(TRIGGER_NETDEV_FULL_DUPLEX, &trigger_data->mode) &&
> -		    trigger_data->duplex == DUPLEX_FULL)
> -			blink_on = true;
> +	if (trigger_data->carrier_link_up && netdev_baseline_on(trigger_data))
> +		led_set_brightness(led_cdev, led_cdev->blink_brightness);
> +	else
> +		led_set_brightness(led_cdev, LED_OFF);
> +}

[Severity: Medium]
When an LED driver implements the blink_set callback, will calling
led_stop_software_blink() followed by netdev_set_resting() actually stop
hardware blinking when the resting state is lit?

Unlike packet mode, which uses led_blink_set_oneshot() (which always uses
the software blink timer), rate mode in netdev_trig_work() calls
led_blink_set(), which invokes led_cdev->blink_set() and leaves LED_BLINK_SW
clear when hardware blinking is supported.

Per the blink_set contract in include/linux/leds.h, hardware blinking is
deactivated when the brightness is set to LED_OFF via brightness_set().
Because led_stop_software_blink() only stops the software timer and
netdev_set_resting() passes a non-zero led_cdev->blink_brightness directly to
led_set_brightness() when carrier_link_up and netdev_baseline_on() are true,
does hardware blinking remain active after traffic drops below threshold or
rate_threshold is cleared?

> -		if (blink_on)
> -			led_set_brightness(led_cdev,
> -					   led_cdev->blink_brightness);
> -		else
> -			led_set_brightness(led_cdev, LED_OFF);
> +static void set_baseline_state(struct led_netdev_data *trigger_data)
> +{
> +	int current_brightness;
> +	struct led_classdev *led_cdev = trigger_data->led_cdev;
>  
> -		/* If we are looking for RX/TX start periodically
> -		 * checking stats
> +	/* Already validated, hw control is possible with the requested mode */
> +	if (trigger_data->hw_control) {
> +		led_cdev->hw_control_set(led_cdev, trigger_data->mode);
> +
> +		return;
> +	}
> +
> +	/* While a rate-mode software blink is still armed, the solid
> +	 * brightness below only lands in blink_brightness (LED_BLINK_SW),
> +	 * and the work re-arms the blink only on a ladder step change: with
> +	 * rate_last_ms cleared, a below-threshold sample computes the same
> +	 * zero step and the old blink keeps running. Stop it here; the work
> +	 * re-arms it from the measured rate, as rate_threshold_store() does.
> +	 */
> +	if (trigger_data->rate_threshold_kbps)
> +		led_stop_software_blink(led_cdev);

[Severity: High]
What happens if rate mode has armed a continuous software blink via
led_blink_set() while trigger_data->hw_control is false, and the trigger
subsequently transitions to trigger_data->hw_control = true via
netdev_led_attr_store() or netdev_trig_notify(NETDEV_UP)?

Because can_hw_control() does not check trigger_data->rate_threshold_kbps,
and set_baseline_state() returns early in the trigger_data->hw_control branch
before calling led_stop_software_blink(), won't led_cdev->blink_timer keep
firing and calling brightness_set(LED_OFF), overriding hardware control while
rate_threshold_store() rejects writes (including 0) with -EINVAL?

[ ... ]

> @@ -509,7 +594,51 @@ static ssize_t interval_store(struct device *dev,
>  	return size;
>  }
>  
> +static ssize_t rate_threshold_show(struct device *dev,
> +				   struct device_attribute *attr, char *buf)
> +{
> +	struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev);
> +
> +	return sprintf(buf, "%lu\n", trigger_data->rate_threshold_kbps);
> +}
> +
> +static ssize_t rate_threshold_store(struct device *dev,
> +				    struct device_attribute *attr, const char *buf,
> +				    size_t size)
> +{
> +	struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev);
> +	unsigned long value;
> +	int ret;
> +
> +	if (trigger_data->hw_control)
> +		return -EINVAL;
> +
> +	ret = kstrtoul(buf, 0, &value);
> +	if (ret)
> +		return ret;
> +
> +	cancel_delayed_work_sync(&trigger_data->work);
> +
> +	trigger_data->rate_threshold_kbps = value;
> +	trigger_data->rate_last_ms = 0;
> +	netdev_trig_rate_reset(trigger_data);
> +
> +	/* Leaving rate mode has to undo the blink it armed: while a software
> +	 * blink is running led_set_brightness() only changes the blink
> +	 * brightness, and the packet mode below touches the LED only when the
> +	 * activity counter changes, so a link-only LED would keep blinking.
> +	 */
> +	led_stop_software_blink(trigger_data->led_cdev);
> +	set_baseline_state(trigger_data);	/* resets timer */
> +
> +	if (value)
> +		schedule_delayed_work(&trigger_data->work, 0);
> +
> +	return size;
> +}

[Severity: High]
Can rate_threshold_store() race with netdev_trig_notify() or
set_device_name() because it modifies rate state and schedules
trigger_data->work without holding trigger_data->lock?

In netdev_trig_notify() and set_device_name(), the caller cancels
trigger_data->work and then acquires trigger_data->lock before calling
dev_put(trigger_data->net_dev) and setting trigger_data->net_dev = NULL:

netdev_trig_notify()
  cancel_delayed_work_sync(&trigger_data->work);
  mutex_lock(&trigger_data->lock);
  ...
  case NETDEV_UNREGISTER:
    dev_put(trigger_data->net_dev);
    trigger_data->net_dev = NULL;

If rate_threshold_store() runs concurrently without trigger_data->lock and
schedules trigger_data->work, can netdev_trig_work() pass the initial
!trigger_data->net_dev check and then dereference a NULL or freed
trigger_data->net_dev in dev_get_stats(trigger_data->net_dev, &temp)?

Similarly, because netdev_trig_work() reads trigger_data->rate_threshold_kbps
multiple times without synchronization, if rate_threshold_store() writes 0
while netdev_trig_work() is already in the rate-mode block, could the later
unsigned comparison

  if (rate >= trigger_data->rate_threshold_kbps)

see rate_threshold_kbps == 0, evaluate to true for rate == 0, and arm a
150 ms blink after rate mode was disabled?

[Severity: Medium]
Also in rate_threshold_store(), set_baseline_state() already schedules
trigger_data->work when trigger_data->carrier_link_up is true and
trigger_data->rate_threshold_kbps is non-zero.

Why does rate_threshold_store() call schedule_delayed_work() a second time
unconditionally when value is non-zero? If carrier_link_up is false (while
trigger_data->net_dev is non-NULL), won't this start netdev_trig_work(),
which then re-arms itself every HZ / 2 and polls dev_get_stats() indefinitely
while the link is down?

[ ... ]

> @@ -639,12 +769,14 @@ static int netdev_trig_notify(struct notifier_block *nb,
>  		dev_put(trigger_data->net_dev);
>  		dev_hold(dev);
>  		trigger_data->net_dev = dev;
> +		netdev_trig_rate_reset(trigger_data);
>  		if (evt == NETDEV_CHANGENAME)
>  			get_device_state(trigger_data);
>  		break;
>  	case NETDEV_UNREGISTER:
>  		dev_put(trigger_data->net_dev);
>  		trigger_data->net_dev = NULL;
> +		netdev_trig_rate_reset(trigger_data);
>  		break;
>  	case NETDEV_UP:
>  		trigger_data->hw_control = can_hw_control(trigger_data);

[Severity: Medium]
Should netdev_trig_notify() also call netdev_trig_rate_reset() on NETDEV_DOWN
or when carrier_link_up becomes false on NETDEV_CHANGE (and should
set_baseline_state() reset rate_ag and rate_peak when zeroing rate_last_ms)?

Right now, if the carrier drops during active traffic or mid-afterglow, the
work is cancelled while rate_smooth, rate_peak, rate_ag, and rate_valid
remain frozen at their pre-down values. When the link comes back up later
with no traffic, won't netdev_trig_work() resume decaying those stale values
and blink the idle LED for up to 20 seconds?

> @@ -674,6 +806,8 @@ static void netdev_trig_work(struct work_struct *work)
>  	unsigned int new_activity;
>  	struct rtnl_link_stats64 temp;
>  	unsigned long interval;
> +	unsigned long now, ms, elapsed, stale;
> +	u64 bytes;
>  	int invert;
>  
>  	/* If we dont have a device, insure we are off */
> @@ -682,6 +816,153 @@ static void netdev_trig_work(struct work_struct *work)
>  		return;
>  	}
>  
> +	/* Rate mode: blink on a ladder driven by the interface byte rate instead
> +	 * of per packet. Independent of the packet-mode bits so that interfaces
> +	 * whose packet counters are blind to hardware-forwarded traffic still
> +	 * show activity, and so that a sub-threshold link rests in its baseline
> +	 * state (solid with a link bit, dark without one).
> +	 *
> +	 * The rate is measured over the interval between two observed counter
> +	 * changes, not over a fixed window. Counters that are refreshed in
> +	 * bursts - a cached switch MIB polled every few seconds, for instance -
> +	 * dump their whole accumulation into the first fixed window that follows
> +	 * the refresh, which reports a rate several times too high and then
> +	 * zero until the next refresh. Measuring between changes makes the
> +	 * trigger independent of how often the driver refreshes its counters,
> +	 * and keeps the blink running for as long as the traffic does.
> +	 */
> +	if (trigger_data->rate_threshold_kbps && !trigger_data->hw_control) {
> +		dev_stats = dev_get_stats(trigger_data->net_dev, &temp);
> +		bytes = dev_stats->rx_bytes + dev_stats->tx_bytes;
> +		now = jiffies;
> +
> +		if (!trigger_data->rate_valid) {
> +			trigger_data->rate_bytes = bytes;
> +			trigger_data->rate_jiffies = now;
> +			trigger_data->rate_interval = HZ;
> +			trigger_data->rate_valid = true;
> +		} else if (bytes != trigger_data->rate_bytes) {
> +			if (bytes > trigger_data->rate_bytes) {
> +				trigger_data->rate_interval =
> +					now - trigger_data->rate_jiffies;
> +				elapsed = jiffies_to_msecs(trigger_data->rate_interval);
> +				if (elapsed > NETDEV_RATE_MAX_ELAPSED_MS)
> +					elapsed = NETDEV_RATE_MAX_ELAPSED_MS;
> +				trigger_data->rate_kbps =
> +					div_u64((bytes - trigger_data->rate_bytes) * 1000,
> +						elapsed * 1024);
> +			} else {

[Severity: High]
Can elapsed be 0 here and cause a divide-by-zero in div_u64()?

If netdev_trig_work() runs twice within the same jiffy (now ==
trigger_data->rate_jiffies) while traffic is incrementing rx_bytes or
tx_bytes, trigger_data->rate_interval is 0 and jiffies_to_msecs(0) returns 0.

This can happen when set_baseline_state() reschedules trigger_data->work with
delay 0 (for example, on a mode or interval sysfs write, a NETDEV_CHANGE
notification, or in rate_threshold_store() where schedule_delayed_work() is
called twice with delay 0) without resetting rate_valid. Since elapsed is
only clamped from above against NETDEV_RATE_MAX_ELAPSED_MS, passing
elapsed * 1024 to div_u64() will divide by zero.

> +				/* Counters went backwards (reset or wrapped):
> +				 * re-anchor instead of reporting a huge rate.
> +				 */
> +				trigger_data->rate_kbps = 0;
> +			}
> +			trigger_data->rate_bytes = bytes;
> +			trigger_data->rate_jiffies = now;
> +		} else {
> +			/* No change: keep the last measured rate for as long as
> +			 * the counter keeps being refreshed, then fall back to
> +			 * the link state.
> +			 */
> +			stale = trigger_data->rate_interval * NETDEV_RATE_STALE_MULT;
> +			if (stale > NETDEV_RATE_STALE_MAX)
> +				stale = NETDEV_RATE_STALE_MAX;
> +			if (time_after(now, trigger_data->rate_jiffies + stale))
> +				trigger_data->rate_kbps = 0;
> +		}
> +
> +		/* Smoothing ported from the userspace daemon this replaces: attack
> +		 * immediately at or above the threshold so the first evaluation of
> +		 * a transfer already blinks, rise slowly and release quickly below
> +		 * it so the ladder step does not chatter when a bursty rate
> +		 * straddles a boundary.
> +		 */
> +		if (trigger_data->rate_kbps >= trigger_data->rate_threshold_kbps)
> +			trigger_data->rate_smooth = trigger_data->rate_kbps;
> +		else if (trigger_data->rate_kbps < trigger_data->rate_smooth)
> +			trigger_data->rate_smooth = (trigger_data->rate_smooth +
> +				3 * trigger_data->rate_kbps) / 4;
> +		else
> +			trigger_data->rate_smooth = (3 * trigger_data->rate_smooth +
> +				trigger_data->rate_kbps) / 4;
> +
> +		if (trigger_data->rate_smooth >= trigger_data->rate_threshold_kbps) {
> +			if (trigger_data->rate_smooth > trigger_data->rate_peak)
> +				trigger_data->rate_peak = trigger_data->rate_smooth;
> +			trigger_data->rate_ag = 0;
> +		}
> +
> +		ms = 0;
> +		if (trigger_data->carrier_link_up) {
> +			unsigned long rate = trigger_data->rate_smooth;
> +
> +			/* Quadratic afterglow from the last peak: the synthetic rate
> +			 * decays as peak * remaining^2 / window^2 over
> +			 * NETDEV_RATE_AFTERGLOW, then the LED returns to the link
> +			 * state. div_u64() keeps 32-bit builds linking.
> +			 */
> +			if (rate < trigger_data->rate_threshold_kbps &&
> +			    trigger_data->rate_peak >= trigger_data->rate_threshold_kbps &&
> +			    trigger_data->rate_ag < NETDEV_RATE_AFTERGLOW) {
> +				unsigned long rem;
> +
> +				trigger_data->rate_ag += HZ / 2;
> +				rem = NETDEV_RATE_AFTERGLOW - trigger_data->rate_ag;
> +				rate = div_u64((u64)trigger_data->rate_peak * rem * rem,
> +					       NETDEV_RATE_AFTERGLOW *
> +							NETDEV_RATE_AFTERGLOW);
> +			}
> +
> +			if (rate >= trigger_data->rate_threshold_kbps) {
> +				if (rate >= 4096)
> +					ms = 50;
> +				else if (rate >= 1024)
> +					ms = 60;
> +				else if (rate >= 256)
> +					ms = 80;
> +				else if (rate >= 32)
> +					ms = 100;
> +				else
> +					ms = 150;
> +			}
> +
> +			/* During the fade the step only ever gets longer: re-arming
> +			 * a faster blink mid-decay looks like stutter.
> +			 */
> +			if (ms && trigger_data->rate_ag &&
> +			    ms < trigger_data->rate_last_ms)
> +				ms = trigger_data->rate_last_ms;
> +		}

[Severity: Medium]
Does the quick-release decay of rate_smooth prevent the quadratic afterglow
from stepping down the ladder from rate_peak?

When traffic stops and rate_kbps drops to 0, rate_smooth is divided by 4 on
each tick while remaining >= rate_threshold_kbps for one or more ticks. On
those ticks, rate_ag stays 0 and trigger_data->rate_last_ms walks down to
slower ladder steps (for instance, 150 ms when rate_threshold_kbps <= 31).

Once rate_smooth finally falls below rate_threshold_kbps and the quadratic
afterglow computes a synthetic rate near rate_peak, won't the check

  if (ms && trigger_data->rate_ag &&
      ms < trigger_data->rate_last_ms)
          ms = trigger_data->rate_last_ms;

clamp ms to that slowest step for the entire 20-second afterglow window
instead of fading down from the peak blink rate?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261030215924.18060-1-haddad2inc@gmail.com?part=1

      reply	other threads:[~2026-10-30 22:18 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-30 21:59 [PATCH] leds: trigger-netdev: add byte-rate threshold mode Salah Ahmed
2026-10-30 22:18 ` sashiko-bot [this message]

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=sashiko-outbox-191572@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=haddad2inc@gmail.com \
    --cc=lee@kernel.org \
    --cc=linux-leds@vger.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