Re: [PATCH v5] leds: rgb: leds-qcom-lpg: Fix LED color balancing in HW pattern mode

Lee Jones <[email protected]> Thu, 16 Jul 2026 14:07:21 +0100
Newsgroups org.kernel.vger.linux-leds,dev.linux.lists.sashiko-reviews
Message-ID <[email protected]>
On Wed, 08 Jul 2026, [email protected] wrote:

> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> 
> New issues:
> - [Medium] Mixing goto-based error handling with scope-based cleanup (__free and guard) in the same function.

Either provide a good explanation or fix please.

> Pre-existing issues:
> - [High] `lpg_lut_free()` fails to clear LUT slots for valid patterns of length 1, permanently leaking hardware LUT capacity.
> --
> 
> --- Patch [1]: [PATCH v5] leds: rgb: leds-qcom-lpg: Fix LED color balancing in HW pattern mode ---
> commit d02ed2e49b5e3fa47ddff854cf6f3e5653dde705
> Author: Fenglin Wu <[email protected]>
> Subject: leds: rgb: leds-qcom-lpg: Fix LED color balancing in HW pattern mode
> 
> This commit scales the pattern data according to the sub-led intensity 
> and programs the HW pattern separately for each LPG channel. This ensures 
> the LED device respects intensity settings instead of always operating in 
> a white-balanced mode.
> 
> Link: https://patch.msgid.link/20260629-lpg-rgb-color-balance-fix-v4-1-4db8592fb3c5@oss.qualcomm.com
> 
> > diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c
> > index d7d6518de30f2..7ba4765e7cfc2 100644
> > --- a/drivers/leds/rgb/leds-qcom-lpg.c
> > +++ b/drivers/leds/rgb/leds-qcom-lpg.c
> 
> [ ... ]
> 
> > @@ -1087,43 +1098,64 @@ static int lpg_pattern_set(struct lpg_led *led, struct led_pattern *led_pattern,
> >  	 * duration of all steps.
> >  	 */
> >  	if (lpg->lut_base || lpg->lut_sdam) {
> > -		lo_pause = pattern[0].delta_t;
> > -		hi_pause = pattern[actual_len - 1].delta_t;
> > +		prep->lo_pause = pattern[0].delta_t;
> > +		prep->hi_pause = pattern[actual_len - 1].delta_t;
> >  	} else {
> >  		if (delta_t != pattern[0].delta_t || delta_t != pattern[actual_len - 1].delta_t)
> > -			goto out_free_pattern;
> > +			return -EINVAL;
> > +		prep->lo_pause = 0;
> > +		prep->hi_pause = 0;
> >  	}
> >  
> > +	prep->data = no_free_ptr(pattern);
> > +	prep->len = actual_len;
> > +	prep->delta_t = delta_t;
> > +	prep->ping_pong = ping_pong;
> 
> [Severity: High]
> This is a pre-existing issue, but does lpg_lut_free() fail to clear LUT
> slots for valid patterns of length 1?
> 
> When a user writes a 4-element palindrome pattern (which translates to 
> len == 2 internal steps), lpg_prepare_pattern() calculates:
> 
>     if (ping_pong)
>         actual_len = (len + 1) / 2;
> 
> making actual_len = 1.
> 
> Later, when this pattern is cleared or an error path triggers, lpg_lut_free()
> checks:
> 
>     len = hi_idx - lo_idx + 1;
>     if (len == 1)
>         return;
> 
> This early return seems to silently skip clearing the allocated bitmap slot, 
> permanently leaking hardware LUT capacity over time.
> 
> > +	return 0;
> > +}
> > +
> > +static int lpg_pattern_set(struct lpg_led *led, struct led_pattern *led_pattern,
> > +			   u32 len, int repeat)
> > +{
> > +	struct lpg_channel *chan;
> > +	struct lpg *lpg = led->lpg;
> > +	struct lpg_pattern pattern;
> > +	unsigned int lo_idx;
> > +	unsigned int hi_idx;
> > +	unsigned int i;
> > +	int ret;
> > +
> > +	ret = lpg_prepare_pattern(lpg, led_pattern, len, repeat, &pattern);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	guard(mutex)(&lpg->lock);
> >  
> >  	if (lpg->lut_base)
> > -		ret = lpg_lut_store(lpg, pattern, actual_len, &lo_idx, &hi_idx);
> > +		ret = lpg_lut_store(lpg, pattern.data, pattern.len, &lo_idx, &hi_idx);
> >  	else
> > -		ret = lpg_lut_store_sdam(lpg, pattern, actual_len, &lo_idx, &hi_idx);
> > +		ret = lpg_lut_store_sdam(lpg, pattern.data, pattern.len, &lo_idx, &hi_idx);
> >  
> >  	if (ret < 0)
> > -		goto out_unlock;
> > +		goto free_pattern;
> >  
> >  	for (i = 0; i < led->num_channels; i++) {
> >  		chan = led->channels[i];
> >  
> > -		chan->ramp_tick_ms = delta_t;
> > -		chan->ramp_ping_pong = ping_pong;
> > +		chan->ramp_tick_ms = pattern.delta_t;
> > +		chan->ramp_ping_pong = pattern.ping_pong;
> >  		chan->ramp_oneshot = repeat != -1;
> >  
> > -		chan->ramp_lo_pause_ms = lo_pause;
> > -		chan->ramp_hi_pause_ms = hi_pause;
> > +		chan->ramp_lo_pause_ms = pattern.lo_pause;
> > +		chan->ramp_hi_pause_ms = pattern.hi_pause;
> >  
> >  		chan->pattern_lo_idx = lo_idx;
> >  		chan->pattern_hi_idx = hi_idx;
> >  	}
> >  
> > -out_unlock:
> > -	mutex_unlock(&lpg->lock);
> > -out_free_pattern:
> > -	kfree(pattern);
> > -
> > +free_pattern:
> > +	kfree(pattern.data);
> >  	return ret;
> >  }
> 
> [Severity: Medium]
> Does this code mix goto-based error handling with scope-based cleanup?
> 
> The cleanup subsystem guidelines suggest avoiding mixing goto labels 
> (like free_pattern) and guard() declarations within the same function. 
> Since pattern.data is manually managed here alongside the guarded mutex,
> this might make the code more prone to LIFO ordering mistakes or resource 
> leaks during future refactoring.
> 
> [ ... ]
> 
> > @@ -1144,23 +1176,86 @@ static int lpg_pattern_single_set(struct led_classdev *cdev,
> >  }
> >  
> >  static int lpg_pattern_mc_set(struct led_classdev *cdev,
> > -			      struct led_pattern *pattern, u32 len,
> > +			      struct led_pattern *led_pattern, u32 len,
> >  			      int repeat)
> >  {
> >  	struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
> >  	struct lpg_led *led = container_of(mc, struct lpg_led, mcdev);
> > +	struct lpg *lpg = led->lpg;
> > +	struct lpg_channel *chan;
> > +	struct lpg_pattern pattern;
> >  	unsigned int triled_mask = 0;
> > -	int ret, i;
> > +	unsigned int lo_idx;
> > +	unsigned int hi_idx;
> > +	unsigned int scale;
> > +	unsigned int i, j;
> > +	int ret;
> > +
> > +	ret = lpg_prepare_pattern(lpg, led_pattern, len, repeat, &pattern);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	/* Allocate buffer for the per-channel scaled pattern copy */
> > +	struct led_pattern *scaled __free(kfree) =
> > +			   kmalloc_array(pattern.len, sizeof(*scaled), GFP_KERNEL);
> > +	if (!scaled) {
> > +		kfree(pattern.data);
> > +		return -ENOMEM;
> > +	}
> >  
> >  	for (i = 0; i < led->num_channels; i++)
> >  		triled_mask |= led->channels[i]->triled_mask;
> > -	triled_set(led->lpg, triled_mask, 0);
> > +	triled_set(lpg, triled_mask, 0);
> >  
> > -	ret = lpg_pattern_set(led, pattern, len, repeat);
> > +	led_mc_calc_color_components(mc, LED_FULL);
> > +
> > +	/*
> > +	 * Each channel gets its own LUT block scaled by subled_info[i].brightness
> > +	 * so the pattern respects the configured colour balance.
> > +	 */
> > +	guard(mutex)(&lpg->lock);
> > +
> > +	for (i = 0; i < led->num_channels; i++) {
> > +		chan = led->channels[i];
> > +		scale = mc->subled_info[i].brightness;
> > +
> > +		for (j = 0; j < pattern.len; j++) {
> > +			scaled[j].brightness = DIV_ROUND_CLOSEST(
> > +				(u32)pattern.data[j].brightness * scale, LED_FULL);
> > +			scaled[j].delta_t = pattern.data[j].delta_t;
> > +		}
> > +
> > +		if (lpg->lut_base)
> > +			ret = lpg_lut_store(lpg, scaled, pattern.len, &lo_idx, &hi_idx);
> > +		else
> > +			ret = lpg_lut_store_sdam(lpg, scaled, pattern.len, &lo_idx, &hi_idx);
> > +
> > +		if (ret < 0) {
> > +			/* Free LUT slots already allocated for previous channels */
> > +			while (i-- > 0) {
> > +				chan = led->channels[i];
> > +				lpg_lut_free(lpg, chan->pattern_lo_idx, chan->pattern_hi_idx);
> > +				chan->pattern_lo_idx = 0;
> > +				chan->pattern_hi_idx = 0;
> > +			}
> > +
> > +			goto free_pattern;
> > +		}
> > +
> > +		chan->ramp_tick_ms = pattern.delta_t;
> > +		chan->ramp_ping_pong = pattern.ping_pong;
> > +		chan->ramp_oneshot = repeat != -1;
> > +		chan->ramp_lo_pause_ms = pattern.lo_pause;
> > +		chan->ramp_hi_pause_ms = pattern.hi_pause;
> > +		chan->pattern_lo_idx = lo_idx;
> > +		chan->pattern_hi_idx = hi_idx;
> > +	}
> > +
> > +free_pattern:
> > +	kfree(pattern.data);
> >  	if (ret < 0)
> >  		return ret;
> >  
> > -	led_mc_calc_color_components(mc, LED_FULL);
> >  	lpg_brightness_set(led, cdev, mc->subled_info);
> >  
> >  	return 0;
> 
> [Severity: Medium]
> Similarly here, does this function mix __free(), guard(), and a goto label?
> 
> The __free(kfree) and guard(mutex) helpers are used alongside the 
> free_pattern goto label to manually kfree pattern.data. The guidelines
> recommend converting all resources in a function to scope-based cleanup 
> or none of them, to ensure consistent ownership semantics.
> 
> -- 
> Sashiko AI review · https://sashiko.dev/#/patchset/20260707-lpg-rgb-color-balance-fix-v5-1-99e2d73084fc@oss.qualcomm.com?part=1

-- 
Lee Jones