* [PATCH v3 0/4] backlight: qcom-wled: Fix OVP IRQ imbalance and start from the hardware state
@ 2026-09-14 18:14 ` David Heidelberg
0 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
The qcom-wled OVP interrupt is enabled at probe whenever the module is
already on, but the driver still believes the backlight is off, so the
first brightness update re-enables the interrupt and trips "Unbalanced
enable for IRQ". sdm845-mainline has carried a workaround for years and
Joel posted a flag-based version last October [1], which Konrad and
Daniel asked to be replaced by something that does not track the IRQ
state by hand.
The first patch is unrelated and fell out of writing the readback: the
WLED3 brightness register stride has been one byte instead of two since
the 2019 restructuring, so multi-string pm8941 boards only program one
string correctly. Untested, I have no WLED3 board; it restores what
pm8941-wled.c did and matches the downstream leds-qpnp.c layout.
[1] https://lore.kernel.org/all/20251021-qcom-wled-fix-unbalanced-ovp-irq-enable-v2-1-7ff115b4ffe7@joelselvaraj.com/
Signed-off-by: David Heidelberg <david@ixit.cz>
---
Changes in v3:
- Fix typo 0x10 != 0b10. (Sashiko)
- Added fix for a unrelated issue, but since reported as critical, let's
squeeze it:
Missing platform_set_drvdata() in wled_probe() leads to a guaranteed NULL pointer dereference in wled_remove().
(Sashiko)
- Link to v2: https://patch.msgid.link/20260914-qcom-wled-backlight-v2-0-a908d9233e93@ixit.cz
Changes in v2:
- Add a fix for the WLED3 brightness register stride.
- Add patch reading the programmed brightness back via
backlight_ops.get_brightness. (Konrad)
- Note the brightness readback is done in the follow-up to the first
patch.
- Link to v1: https://patch.msgid.link/20260908-qcom-wled-backlight-v1-1-c4dd4eabda07@ixit.cz
---
David Heidelberg (4):
backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
backlight: qcom-wled: Fix WLED3 brightness register stride
backlight: qcom-wled: Fix unbalanced OVP IRQ enable at probe
backlight: qcom-wled: Read back the programmed brightness at probe
drivers/video/backlight/qcom-wled.c | 89 +++++++++++++++++++++++++++++++------
1 file changed, 76 insertions(+), 13 deletions(-)
---
base-commit: 944a035ecca915ae947905dcfb03f2b9dc6d032c
change-id: 20260908-qcom-wled-backlight-fd9574027353
Best regards,
--
David Heidelberg <david@ixit.cz>
^ permalink raw reply [flat|nested] 16+ messages in thread* [PATCH v3 1/4] backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
2026-09-14 18:14 ` David Heidelberg
@ 2026-09-14 18:14 ` David Heidelberg
-1 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg via B4 Relay @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
From: David Heidelberg <david@ixit.cz>
wled_probe() doesn't set the driver data for the platform device.
As a result, dev_get_drvdata() in wled_remove() will return NULL,
leading to a NULL pointer dereference afterward.
Set the platform device driver data in wled_probe().
Cc: stable@vger.kernel.org
Fixes: feeab87b3072 ("backlight: qcom-wled: Add support for short circuit handling")
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index 650dd95f06ef5..a76158a298335 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -1747,16 +1747,17 @@ static int wled_probe(struct platform_device *pdev)
memset(&props, 0, sizeof(struct backlight_properties));
props.type = BACKLIGHT_RAW;
props.brightness = val;
props.max_brightness = wled->max_brightness;
bl = devm_backlight_device_register(&pdev->dev, wled->name,
&pdev->dev, wled,
&wled_ops, &props);
+ platform_set_drvdata(pdev, bl);
return PTR_ERR_OR_ZERO(bl);
};
static void wled_remove(struct platform_device *pdev)
{
struct wled *wled = platform_get_drvdata(pdev);
mutex_destroy(&wled->lock);
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH v3 1/4] backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
@ 2026-09-14 18:14 ` David Heidelberg
0 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
wled_probe() doesn't set the driver data for the platform device.
As a result, dev_get_drvdata() in wled_remove() will return NULL,
leading to a NULL pointer dereference afterward.
Set the platform device driver data in wled_probe().
Cc: stable@vger.kernel.org
Fixes: feeab87b3072 ("backlight: qcom-wled: Add support for short circuit handling")
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index 650dd95f06ef5..a76158a298335 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -1747,16 +1747,17 @@ static int wled_probe(struct platform_device *pdev)
memset(&props, 0, sizeof(struct backlight_properties));
props.type = BACKLIGHT_RAW;
props.brightness = val;
props.max_brightness = wled->max_brightness;
bl = devm_backlight_device_register(&pdev->dev, wled->name,
&pdev->dev, wled,
&wled_ops, &props);
+ platform_set_drvdata(pdev, bl);
return PTR_ERR_OR_ZERO(bl);
};
static void wled_remove(struct platform_device *pdev)
{
struct wled *wled = platform_get_drvdata(pdev);
mutex_destroy(&wled->lock);
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH v3 1/4] backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
2026-09-14 18:14 ` David Heidelberg
(?)
@ 2026-09-14 18:26 ` sashiko-bot
-1 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-14 18:26 UTC (permalink / raw)
To: David Heidelberg; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Critical] Type confusion in wled_probe() and wled_remove() leading to memory corruption
Pre-existing issues:
- [High] Use-After-Free due to devres teardown order in wled_remove()
--
commit a2bfdd5e4b8eca63712840cdae737878170273a7
Author: David Heidelberg <david@ixit.cz>
Subject: backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
This patch attempts to fix a NULL pointer dereference during PM callbacks
by setting the platform device driver data in wled_probe().
> diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
> index 650dd95f06ef5..a76158a298335 100644
> --- a/drivers/video/backlight/qcom-wled.c
> +++ b/drivers/video/backlight/qcom-wled.c
> @@ -1752,6 +1752,7 @@ static int wled_probe(struct platform_device *pdev)
> bl = devm_backlight_device_register(&pdev->dev, wled->name,
> &pdev->dev, wled,
> &wled_ops, &props);
[Severity: High]
This is a pre-existing issue, but can using devres to register the backlight
device lead to a use-after-free regression during driver unbind?
Because the backlight device remains registered and accessible via sysfs
during and immediately after wled_remove() completes, could a concurrent
userspace write trigger wled_update_status() which calls
wled_module_enable()?
If this happens, it could re-schedule ovp_work after wled_remove() has
already canceled it:
wled_module_enable()
schedule_delayed_work(&wled->ovp_work, HZ / 100);
Will this pending timer later execute on freed memory once devres cleans up
the wled structure?
> + platform_set_drvdata(pdev, bl);
[Severity: Critical]
Does this code introduce a type mismatch regression?
wled_remove() expects the driver data to be a struct wled pointer:
wled_remove()
struct wled *wled = platform_get_drvdata(pdev);
If we set it to the backlight_device pointer here, will wled_remove()
operate on invalid memory offsets when it later calls
mutex_destroy(&wled->lock) and cancel_delayed_work_sync(&wled->ovp_work)?
> return PTR_ERR_OR_ZERO(bl);
> };
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914-qcom-wled-backlight-v3-0-d69fe9843841@ixit.cz?part=1
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH v3 1/4] backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
2026-09-14 18:14 ` David Heidelberg
(?)
(?)
@ 2026-09-14 18:34 ` David Heidelberg
-1 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg @ 2026-09-14 18:34 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, stable
On 14/09/2026 20:14, David Heidelberg via B4 Relay wrote:
> From: David Heidelberg <david@ixit.cz>
>
> wled_probe() doesn't set the driver data for the platform device.
> As a result, dev_get_drvdata() in wled_remove() will return NULL,
> leading to a NULL pointer dereference afterward.
>
> Set the platform device driver data in wled_probe().
>
> Cc: stable@vger.kernel.org
> Fixes: feeab87b3072 ("backlight: qcom-wled: Add support for short circuit handling")
> Signed-off-by: David Heidelberg <david@ixit.cz>
> ---
> drivers/video/backlight/qcom-wled.c | 1 +
> 1 file changed, 1 insertion(+)
>
Please ignore this [1/4] patch, so at least the rest get fixed.
In next series, I'll omit this one (I thought it's easy drop in fix for
critical, but seems not).
David
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH v3 1/4] backlight: qcom-wled: Fix NULL pointer dereference in PM callbacks
2026-09-14 18:14 ` David Heidelberg
` (2 preceding siblings ...)
(?)
@ 2026-09-21 9:27 ` Konrad Dybcio
-1 siblings, 0 replies; 16+ messages in thread
From: Konrad Dybcio @ 2026-09-21 9:27 UTC (permalink / raw)
To: david, Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller,
Kiran Gunda, Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Joel Selvaraj, stable
On 9/14/26 8:14 PM, David Heidelberg via B4 Relay wrote:
> From: David Heidelberg <david@ixit.cz>
>
> wled_probe() doesn't set the driver data for the platform device.
> As a result, dev_get_drvdata() in wled_remove() will return NULL,
> leading to a NULL pointer dereference afterward.
>
> Set the platform device driver data in wled_probe().
>
> Cc: stable@vger.kernel.org
> Fixes: feeab87b3072 ("backlight: qcom-wled: Add support for short circuit handling")
> Signed-off-by: David Heidelberg <david@ixit.cz>
> ---
> drivers/video/backlight/qcom-wled.c | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
> index 650dd95f06ef5..a76158a298335 100644
> --- a/drivers/video/backlight/qcom-wled.c
> +++ b/drivers/video/backlight/qcom-wled.c
> @@ -1747,16 +1747,17 @@ static int wled_probe(struct platform_device *pdev)
>
> memset(&props, 0, sizeof(struct backlight_properties));
> props.type = BACKLIGHT_RAW;
> props.brightness = val;
> props.max_brightness = wled->max_brightness;
> bl = devm_backlight_device_register(&pdev->dev, wled->name,
> &pdev->dev, wled,
> &wled_ops, &props);
> + platform_set_drvdata(pdev, bl);
This does make the pointer non-null, but it stores the wrong object
Konrad
> return PTR_ERR_OR_ZERO(bl);
> };
>
> static void wled_remove(struct platform_device *pdev)
> {
> struct wled *wled = platform_get_drvdata(pdev);
>
> mutex_destroy(&wled->lock);
>
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v3 2/4] backlight: qcom-wled: Fix WLED3 brightness register stride
2026-09-14 18:14 ` David Heidelberg
@ 2026-09-14 18:14 ` David Heidelberg
-1 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg via B4 Relay @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
From: David Heidelberg <david@ixit.cz>
WLED3 has a 16-bit brightness register pair per string, at 0x40 + 2*n,
which is what the pm8941-wled driver wrote to:
rc = regmap_bulk_write(wled->regmap,
wled->addr + WLED3_CTRL_REG_VAL_BASE + 2 * i,
v, 2);
The restructuring for WLED3 turned that into WLED3_SINK_REG_BRIGHT(n),
defined as 0x40 + n, so the two byte writes for consecutive strings
overlap: string 1 overwrites the MSB of string 0 with its own LSB, and
with the default three strings only string 1 ends up with the requested
value.
Use the 2 byte stride.
Fixes: 775d2ffb4af6 ("backlight: qcom-wled: Restructure the driver for WLED3")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index a76158a298335..e1962c2d90ed4 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -54,17 +54,17 @@
#define WLED3_SINK_REG_SYNC 0x47
#define WLED3_SINK_REG_SYNC_CLEAR 0x00
#define WLED3_SINK_REG_CURR_SINK 0x4f
#define WLED3_SINK_REG_CURR_SINK_MASK GENMASK(7, 5)
#define WLED3_SINK_REG_CURR_SINK_SHFT 5
/* WLED3 specific per-'string' registers below */
-#define WLED3_SINK_REG_BRIGHT(n) (0x40 + n)
+#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x10))
#define WLED3_SINK_REG_STR_MOD_EN(n) (0x60 + (n * 0x10))
#define WLED3_SINK_REG_STR_MOD_MASK BIT(7)
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR(n) (0x62 + (n * 0x10))
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR_MASK GENMASK(4, 0)
#define WLED3_SINK_REG_STR_MOD_SRC(n) (0x63 + (n * 0x10))
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH v3 2/4] backlight: qcom-wled: Fix WLED3 brightness register stride
@ 2026-09-14 18:14 ` David Heidelberg
0 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
WLED3 has a 16-bit brightness register pair per string, at 0x40 + 2*n,
which is what the pm8941-wled driver wrote to:
rc = regmap_bulk_write(wled->regmap,
wled->addr + WLED3_CTRL_REG_VAL_BASE + 2 * i,
v, 2);
The restructuring for WLED3 turned that into WLED3_SINK_REG_BRIGHT(n),
defined as 0x40 + n, so the two byte writes for consecutive strings
overlap: string 1 overwrites the MSB of string 0 with its own LSB, and
with the default three strings only string 1 ends up with the requested
value.
Use the 2 byte stride.
Fixes: 775d2ffb4af6 ("backlight: qcom-wled: Restructure the driver for WLED3")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index a76158a298335..e1962c2d90ed4 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -54,17 +54,17 @@
#define WLED3_SINK_REG_SYNC 0x47
#define WLED3_SINK_REG_SYNC_CLEAR 0x00
#define WLED3_SINK_REG_CURR_SINK 0x4f
#define WLED3_SINK_REG_CURR_SINK_MASK GENMASK(7, 5)
#define WLED3_SINK_REG_CURR_SINK_SHFT 5
/* WLED3 specific per-'string' registers below */
-#define WLED3_SINK_REG_BRIGHT(n) (0x40 + n)
+#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x10))
#define WLED3_SINK_REG_STR_MOD_EN(n) (0x60 + (n * 0x10))
#define WLED3_SINK_REG_STR_MOD_MASK BIT(7)
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR(n) (0x62 + (n * 0x10))
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR_MASK GENMASK(4, 0)
#define WLED3_SINK_REG_STR_MOD_SRC(n) (0x63 + (n * 0x10))
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH v3 2/4] backlight: qcom-wled: Fix WLED3 brightness register stride
2026-09-14 18:14 ` David Heidelberg
(?)
@ 2026-09-21 9:29 ` Konrad Dybcio
-1 siblings, 0 replies; 16+ messages in thread
From: Konrad Dybcio @ 2026-09-21 9:29 UTC (permalink / raw)
To: david, Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller,
Kiran Gunda, Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Joel Selvaraj, stable
On 9/14/26 8:14 PM, David Heidelberg via B4 Relay wrote:
> From: David Heidelberg <david@ixit.cz>
>
> WLED3 has a 16-bit brightness register pair per string, at 0x40 + 2*n,
> which is what the pm8941-wled driver wrote to:
>
> rc = regmap_bulk_write(wled->regmap,
> wled->addr + WLED3_CTRL_REG_VAL_BASE + 2 * i,
> v, 2);
>
> The restructuring for WLED3 turned that into WLED3_SINK_REG_BRIGHT(n),
> defined as 0x40 + n, so the two byte writes for consecutive strings
> overlap: string 1 overwrites the MSB of string 0 with its own LSB, and
> with the default three strings only string 1 ends up with the requested
> value.
>
> Use the 2 byte stride.
>
> Fixes: 775d2ffb4af6 ("backlight: qcom-wled: Restructure the driver for WLED3")
> Cc: stable@vger.kernel.org
> Assisted-by: LLM
> Signed-off-by: David Heidelberg <david@ixit.cz>
> ---
> drivers/video/backlight/qcom-wled.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
> index a76158a298335..e1962c2d90ed4 100644
> --- a/drivers/video/backlight/qcom-wled.c
> +++ b/drivers/video/backlight/qcom-wled.c
> @@ -54,17 +54,17 @@
> #define WLED3_SINK_REG_SYNC 0x47
> #define WLED3_SINK_REG_SYNC_CLEAR 0x00
>
> #define WLED3_SINK_REG_CURR_SINK 0x4f
> #define WLED3_SINK_REG_CURR_SINK_MASK GENMASK(7, 5)
> #define WLED3_SINK_REG_CURR_SINK_SHFT 5
>
> /* WLED3 specific per-'string' registers below */
> -#define WLED3_SINK_REG_BRIGHT(n) (0x40 + n)
> +#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x10))
You said 2 in the commit message and you set it to 2 in patch 4
Konrad
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v3 3/4] backlight: qcom-wled: Fix unbalanced OVP IRQ enable at probe
2026-09-14 18:14 ` David Heidelberg
@ 2026-09-14 18:14 ` David Heidelberg
-1 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg via B4 Relay @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
From: David Heidelberg <david@ixit.cz>
wled_configure_ovp_irq() derives the initial state of the OVP interrupt
from the hardware:
/* Keep OVP irq disabled until module is enabled */
if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
disable_irq(wled->ovp_irq);
but wled->brightness, which is what the rest of the driver uses to tell
whether the module is on, is left at zero.
On boards where the bootloader hands the kernel a lit backlight the two
disagree. MOD_EN is already set, so the interrupt is left enabled, while
wled_update_status() still believes the backlight is off and takes
if (!!brightness != !!wled->brightness)
rc = wled_module_enable(wled, !!brightness);
on the first backlight update. wled_module_enable() then schedules
wled_ovp_work(), which calls enable_irq() on the already enabled
interrupt:
Unbalanced enable for IRQ 176
WARNING: CPU: 0 PID: 160 at kernel/irq/manage.c:774 __enable_irq+0x50/0x80
Hardware name: Xiaomi Pocophone F1 (DT)
Workqueue: events wled_ovp_work
Call trace:
__enable_irq+0x50/0x80
enable_irq+0x48/0xa0
wled_ovp_work+0x18/0x24
process_one_work+0x1d0/0x350
worker_thread+0x13c/0x460
kthread+0x110/0x114
ret_from_fork+0x10/0x20
The bootloader is not the only way to get there. The readback runs after
wledN_setup(), and wled4_setup() sets MOD_EN itself on the path where the
sink configuration does not already match, as does the tail of
wled_auto_string_detection(), which all three setup paths can reach
through wled_auto_detection_at_init(). A cold-booted board with a dark
panel can therefore reach the same disagreement.
Move the MOD_EN readback into wled_probe() and use it to seed
wled->brightness, so the driver starts out agreeing with the hardware,
and key the OVP interrupt off wled->brightness instead. The first
backlight update then only reprograms the brightness registers and
leaves both the module and the interrupt alone. The OVP interrupt also
stays armed from probe whenever the module is already enabled, rather
than being disabled at probe and only enabled once something writes
brightness.
Note that a backlight update requesting brightness 0 before any non-zero
one now really does turn the module off wherever MOD_EN was already set,
where before it was silently ignored.
wled->brightness is seeded with default-brightness rather than the level
the bootloader actually programmed, so the first update can still step
the brightness. Reading that level back is version specific and is done
in a follow-up, to keep this fix small enough to backport.
Assisted-by: LLM
Fixes: 8663c188beea ("backlight: qcom-wled: Add auto string detection logic")
Cc: stable@vger.kernel.org
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 32 +++++++++++++++++++++-----------
1 file changed, 21 insertions(+), 11 deletions(-)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index e1962c2d90ed4..eb742a6598173 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -1622,54 +1622,49 @@ static int wled_configure_short_irq(struct wled *wled,
return rc;
}
static int wled_configure_ovp_irq(struct wled *wled,
struct platform_device *pdev)
{
int rc;
- u32 val;
wled->ovp_irq = platform_get_irq_byname(pdev, "ovp");
if (wled->ovp_irq < 0) {
dev_dbg(&pdev->dev, "OVP IRQ not found - disabling automatic string detection\n");
return 0;
}
rc = devm_request_threaded_irq(wled->dev, wled->ovp_irq, NULL,
wled_ovp_irq_handler, IRQF_ONESHOT,
"wled_ovp_irq", wled);
if (rc < 0) {
wled->ovp_irq = 0;
return 0;
}
- rc = regmap_read(wled->regmap, wled->ctrl_addr +
- WLED3_CTRL_REG_MOD_EN, &val);
- if (rc < 0)
- return rc;
-
- /* Keep OVP irq disabled until module is enabled */
- if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
+ /* Keep the OVP irq disabled until the module is enabled */
+ if (!wled->brightness)
disable_irq(wled->ovp_irq);
return 0;
}
static const struct backlight_ops wled_ops = {
.update_status = wled_update_status,
};
static int wled_probe(struct platform_device *pdev)
{
struct backlight_properties props;
struct backlight_device *bl;
struct wled *wled;
struct regmap *regmap;
+ u32 mod_en;
u32 val;
int rc;
regmap = dev_get_regmap(pdev->dev.parent, NULL);
if (!regmap) {
dev_err(&pdev->dev, "Unable to get regmap\n");
return -EINVAL;
}
@@ -1729,27 +1724,42 @@ static int wled_probe(struct platform_device *pdev)
default:
dev_err(wled->dev, "Invalid WLED version\n");
break;
}
INIT_DELAYED_WORK(&wled->ovp_work, wled_ovp_work);
+ val = WLED_DEFAULT_BRIGHTNESS;
+ of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
+
+ /*
+ * The module may already be enabled, either by a bootloader that left
+ * the backlight lit or by the setup above. Record that, so that the
+ * first brightness update does not enable an already enabled module,
+ * and so that the OVP irq is armed from probe rather than from that
+ * first update.
+ */
+ rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
+ &mod_en);
+ if (rc < 0)
+ return rc;
+
+ if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK)
+ wled->brightness = val;
+
rc = wled_configure_short_irq(wled, pdev);
if (rc < 0)
return rc;
rc = wled_configure_ovp_irq(wled, pdev);
if (rc < 0)
return rc;
- val = WLED_DEFAULT_BRIGHTNESS;
- of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
-
memset(&props, 0, sizeof(struct backlight_properties));
props.type = BACKLIGHT_RAW;
props.brightness = val;
props.max_brightness = wled->max_brightness;
bl = devm_backlight_device_register(&pdev->dev, wled->name,
&pdev->dev, wled,
&wled_ops, &props);
platform_set_drvdata(pdev, bl);
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH v3 3/4] backlight: qcom-wled: Fix unbalanced OVP IRQ enable at probe
@ 2026-09-14 18:14 ` David Heidelberg
0 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg, stable
wled_configure_ovp_irq() derives the initial state of the OVP interrupt
from the hardware:
/* Keep OVP irq disabled until module is enabled */
if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
disable_irq(wled->ovp_irq);
but wled->brightness, which is what the rest of the driver uses to tell
whether the module is on, is left at zero.
On boards where the bootloader hands the kernel a lit backlight the two
disagree. MOD_EN is already set, so the interrupt is left enabled, while
wled_update_status() still believes the backlight is off and takes
if (!!brightness != !!wled->brightness)
rc = wled_module_enable(wled, !!brightness);
on the first backlight update. wled_module_enable() then schedules
wled_ovp_work(), which calls enable_irq() on the already enabled
interrupt:
Unbalanced enable for IRQ 176
WARNING: CPU: 0 PID: 160 at kernel/irq/manage.c:774 __enable_irq+0x50/0x80
Hardware name: Xiaomi Pocophone F1 (DT)
Workqueue: events wled_ovp_work
Call trace:
__enable_irq+0x50/0x80
enable_irq+0x48/0xa0
wled_ovp_work+0x18/0x24
process_one_work+0x1d0/0x350
worker_thread+0x13c/0x460
kthread+0x110/0x114
ret_from_fork+0x10/0x20
The bootloader is not the only way to get there. The readback runs after
wledN_setup(), and wled4_setup() sets MOD_EN itself on the path where the
sink configuration does not already match, as does the tail of
wled_auto_string_detection(), which all three setup paths can reach
through wled_auto_detection_at_init(). A cold-booted board with a dark
panel can therefore reach the same disagreement.
Move the MOD_EN readback into wled_probe() and use it to seed
wled->brightness, so the driver starts out agreeing with the hardware,
and key the OVP interrupt off wled->brightness instead. The first
backlight update then only reprograms the brightness registers and
leaves both the module and the interrupt alone. The OVP interrupt also
stays armed from probe whenever the module is already enabled, rather
than being disabled at probe and only enabled once something writes
brightness.
Note that a backlight update requesting brightness 0 before any non-zero
one now really does turn the module off wherever MOD_EN was already set,
where before it was silently ignored.
wled->brightness is seeded with default-brightness rather than the level
the bootloader actually programmed, so the first update can still step
the brightness. Reading that level back is version specific and is done
in a follow-up, to keep this fix small enough to backport.
Assisted-by: LLM
Fixes: 8663c188beea ("backlight: qcom-wled: Add auto string detection logic")
Cc: stable@vger.kernel.org
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 32 +++++++++++++++++++++-----------
1 file changed, 21 insertions(+), 11 deletions(-)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index e1962c2d90ed4..eb742a6598173 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -1622,54 +1622,49 @@ static int wled_configure_short_irq(struct wled *wled,
return rc;
}
static int wled_configure_ovp_irq(struct wled *wled,
struct platform_device *pdev)
{
int rc;
- u32 val;
wled->ovp_irq = platform_get_irq_byname(pdev, "ovp");
if (wled->ovp_irq < 0) {
dev_dbg(&pdev->dev, "OVP IRQ not found - disabling automatic string detection\n");
return 0;
}
rc = devm_request_threaded_irq(wled->dev, wled->ovp_irq, NULL,
wled_ovp_irq_handler, IRQF_ONESHOT,
"wled_ovp_irq", wled);
if (rc < 0) {
wled->ovp_irq = 0;
return 0;
}
- rc = regmap_read(wled->regmap, wled->ctrl_addr +
- WLED3_CTRL_REG_MOD_EN, &val);
- if (rc < 0)
- return rc;
-
- /* Keep OVP irq disabled until module is enabled */
- if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
+ /* Keep the OVP irq disabled until the module is enabled */
+ if (!wled->brightness)
disable_irq(wled->ovp_irq);
return 0;
}
static const struct backlight_ops wled_ops = {
.update_status = wled_update_status,
};
static int wled_probe(struct platform_device *pdev)
{
struct backlight_properties props;
struct backlight_device *bl;
struct wled *wled;
struct regmap *regmap;
+ u32 mod_en;
u32 val;
int rc;
regmap = dev_get_regmap(pdev->dev.parent, NULL);
if (!regmap) {
dev_err(&pdev->dev, "Unable to get regmap\n");
return -EINVAL;
}
@@ -1729,27 +1724,42 @@ static int wled_probe(struct platform_device *pdev)
default:
dev_err(wled->dev, "Invalid WLED version\n");
break;
}
INIT_DELAYED_WORK(&wled->ovp_work, wled_ovp_work);
+ val = WLED_DEFAULT_BRIGHTNESS;
+ of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
+
+ /*
+ * The module may already be enabled, either by a bootloader that left
+ * the backlight lit or by the setup above. Record that, so that the
+ * first brightness update does not enable an already enabled module,
+ * and so that the OVP irq is armed from probe rather than from that
+ * first update.
+ */
+ rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
+ &mod_en);
+ if (rc < 0)
+ return rc;
+
+ if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK)
+ wled->brightness = val;
+
rc = wled_configure_short_irq(wled, pdev);
if (rc < 0)
return rc;
rc = wled_configure_ovp_irq(wled, pdev);
if (rc < 0)
return rc;
- val = WLED_DEFAULT_BRIGHTNESS;
- of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
-
memset(&props, 0, sizeof(struct backlight_properties));
props.type = BACKLIGHT_RAW;
props.brightness = val;
props.max_brightness = wled->max_brightness;
bl = devm_backlight_device_register(&pdev->dev, wled->name,
&pdev->dev, wled,
&wled_ops, &props);
platform_set_drvdata(pdev, bl);
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH v3 3/4] backlight: qcom-wled: Fix unbalanced OVP IRQ enable at probe
2026-09-14 18:14 ` David Heidelberg
(?)
@ 2026-09-14 18:32 ` sashiko-bot
-1 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-14 18:32 UTC (permalink / raw)
To: David Heidelberg; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
New issues:
- [High] Hardware over-voltage protection (OVP) interrupt is incorrectly disabled if the module is active but software evaluates brightness as 0, leaving the hardware unprotected.
Pre-existing issues:
- [Critical] Type mismatch in driver data leads to memory corruption and kernel panic in wled_remove().
- [High] AB-BA Deadlock between `wled_update_status` and `wled_ovp_irq_handler` via `disable_irq()`.
- [High] Unsafe `disable_irq()` calls in `wled_remove()` with potentially invalid or uninitialized IRQ numbers.
- [High] Destroying the driver mutex and cancelling work before unregistering the backlight device creates a use-after-destroy race condition via sysfs.
- [High] Delayed work `wled->ovp_work` can be scheduled during device probe but is not cancelled on probe error paths, causing a Use-After-Free.
--
commit fc9aa7b480cc714e361a97ee0c2edec0701360bf
Author: David Heidelberg <david@ixit.cz>
backlight: qcom-wled: Fix unbalanced OVP IRQ enable at probe
This commit fixes an issue where the OVP interrupt is disabled during probe
even if the backlight is already enabled by the bootloader. It does this by
checking the hardware MOD_EN register during probe and seeding
wled->brightness appropriately.
> diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
> index e1962c2d90ed4..eb742a6598173 100644
> --- a/drivers/video/backlight/qcom-wled.c
> +++ b/drivers/video/backlight/qcom-wled.c
> @@ -1643,13 +1642,8 @@ static int wled_configure_ovp_irq(struct wled *wled,
> return 0;
> }
>
> - rc = regmap_read(wled->regmap, wled->ctrl_addr +
> - WLED3_CTRL_REG_MOD_EN, &val);
> - if (rc < 0)
> - return rc;
> -
> - /* Keep OVP irq disabled until module is enabled */
> - if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
> + /* Keep the OVP irq disabled until the module is enabled */
> + if (!wled->brightness)
> disable_irq(wled->ovp_irq);
[Severity: High]
Does this leave the hardware unprotected if the module is active but software
evaluates brightness as 0?
If wled4_setup() explicitly sets MOD_EN to 1, and the initial brightness
evaluates to 0 (either via default-brightness or wled_read_brightness),
wled->brightness becomes 0. Could this cause disable_irq() to strip
protection from the active module?
Furthermore, if userspace later writes 0 to turn off the screen, could
wled_update_status() check (0 != 0), skip wled_module_enable(0), and
leave the unprotected hardware permanently on?
[ ... ]
> @@ -1734,6 +1729,24 @@ static int wled_probe(struct platform_device *pdev)
>
> INIT_DELAYED_WORK(&wled->ovp_work, wled_ovp_work);
>
> + val = WLED_DEFAULT_BRIGHTNESS;
> + of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
> +
> + /*
> + * The module may already be enabled, either by a bootloader that left
> + * the backlight lit or by the setup above. Record that, so that the
> + * first brightness update does not enable an already enabled module,
> + * and so that the OVP irq is armed from probe rather than from that
> + * first update.
> + */
> + rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
> + &mod_en);
> + if (rc < 0)
> + return rc;
> +
> + if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK)
> + wled->brightness = val;
^^^^^^^^^^^^^^^^^^^^^^
[Severity: High]
If default-brightness evaluates to 0, will wled->brightness become 0 here
despite MOD_EN being active?
[Severity: Critical]
This is a pre-existing issue, but does a type mismatch in the driver data lead
to memory corruption in wled_remove()?
wled_probe() incorrectly sets the platform drvdata to the backlight device:
drivers/video/backlight/qcom-wled.c:wled_probe() {
...
bl = devm_backlight_device_register(...);
platform_set_drvdata(pdev, bl);
return PTR_ERR_OR_ZERO(bl);
}
wled_remove() retrieves this pointer and incorrectly casts it to a
struct wled *:
drivers/video/backlight/qcom-wled.c:wled_remove() {
struct wled *wled = platform_get_drvdata(pdev);
mutex_destroy(&wled->lock);
...
}
Since struct backlight_device has a completely different memory layout than
struct wled, does this type confusion corrupt arbitrary memory when unbinding?
[Severity: High]
This is a pre-existing issue, but does destroying the mutex and cancelling
work before unregistering the backlight device create a use-after-destroy
race condition?
The backlight device is registered using devm_backlight_device_register(),
which defers unregistration until after wled_remove() completes.
However, wled_remove() manually performs cleanup while the backlight device
is still active and its sysfs interface is exposed:
drivers/video/backlight/qcom-wled.c:wled_remove() {
...
mutex_destroy(&wled->lock);
cancel_delayed_work_sync(&wled->ovp_work);
...
}
Could a concurrent sysfs write to the brightness attribute invoke
wled_update_status() and attempt to acquire the already-destroyed mutex?
drivers/video/backlight/qcom-wled.c:wled_update_status() {
...
mutex_lock(&wled->lock);
...
}
[Severity: High]
This is a pre-existing issue, but could a delayed work use-after-free occur
if probe fails?
If the hardware asserts a short circuit condition early, the threaded short
IRQ handler can execute and schedule wled->ovp_work:
drivers/video/backlight/qcom-wled.c:wled_module_enable() {
...
schedule_delayed_work(&wled->ovp_work, HZ / 100);
...
}
If probe subsequently fails during backlight device registration:
drivers/video/backlight/qcom-wled.c:wled_probe() {
...
bl = devm_backlight_device_register(...);
platform_set_drvdata(pdev, bl);
return PTR_ERR_OR_ZERO(bl);
}
Will devres silently free the wled structure without canceling the scheduled
delayed work, causing a crash when the workqueue later executes?
[Severity: High]
This is a pre-existing issue, but can this sequence lead to an AB-BA deadlock?
wled_update_status() acquires the mutex before attempting to disable
the module:
drivers/video/backlight/qcom-wled.c:wled_update_status() {
...
mutex_lock(&wled->lock);
...
rc = wled_module_enable(wled, !!brightness);
...
}
wled_module_enable() spin-waits for any currently executing OVP interrupt
handlers to finish while holding the mutex:
drivers/video/backlight/qcom-wled.c:wled_module_enable() {
...
disable_irq(wled->ovp_irq);
...
}
Concurrently, if the OVP interrupt fires on another CPU, the handler begins
executing and blocks attempting to acquire the same mutex:
drivers/video/backlight/qcom-wled.c:wled_ovp_irq_handler() {
...
mutex_lock(&wled->lock);
...
}
Would CPU 1 hold the lock and wait for the IRQ handler, while CPU 2 runs the
handler and waits for the lock?
[Severity: High]
This is a pre-existing issue, but does wled_remove() unconditionally disable
potentially invalid IRQs?
If devm_request_threaded_irq() fails, wled_configure_ovp_irq() explicitly
sets ovp_irq to 0:
drivers/video/backlight/qcom-wled.c:wled_configure_ovp_irq() {
...
if (rc < 0) {
wled->ovp_irq = 0;
return 0;
...
}
Additionally, platform_get_irq_byname() can return negative error codes.
However, wled_remove() appears to disable both IRQs without validation:
drivers/video/backlight/qcom-wled.c:wled_remove() {
...
disable_irq(wled->short_irq);
disable_irq(wled->ovp_irq);
...
}
Could passing a negative value cause out-of-bounds descriptor lookups, and
could passing 0 disable an essential system interrupt like the timer IRQ?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914-qcom-wled-backlight-v3-0-d69fe9843841@ixit.cz?part=3
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH v3 4/4] backlight: qcom-wled: Read back the programmed brightness at probe
2026-09-14 18:14 ` David Heidelberg
@ 2026-09-14 18:14 ` David Heidelberg
-1 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg via B4 Relay @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg
From: David Heidelberg <david@ixit.cz>
Since the previous change wled_probe() seeds wled->brightness from
MOD_EN, but with default-brightness rather than the level the hardware
is actually driving, so a backlight left lit by the bootloader still
visibly steps on the first update, and actual_brightness reports the
last value written rather than what the panel shows.
Add wled_read_brightness(), which returns 0 when MOD_EN is clear and
otherwise reads the brightness register the matching
wledN_set_brightness() writes: the first enabled string on WLED3 and
WLED4, the selected modulator on WLED5. Use it to seed both
wled->brightness and the initial backlight property, and expose it as
backlight_ops.get_brightness so that actual_brightness reflects the
hardware too.
A module that is enabled with the brightness registers at zero, which
wled4_setup() and wled_auto_string_detection() can leave behind on a
cold boot, now reads back as off. The OVP interrupt is then armed by the
first brightness update rather than from probe, which is what happens
on every other board where the module starts out disabled.
Suggested-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
Assisted-by: LLM
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 76 +++++++++++++++++++++++++++++++------
1 file changed, 64 insertions(+), 12 deletions(-)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index eb742a6598173..471a0cb2b81f6 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -54,17 +54,17 @@
#define WLED3_SINK_REG_SYNC 0x47
#define WLED3_SINK_REG_SYNC_CLEAR 0x00
#define WLED3_SINK_REG_CURR_SINK 0x4f
#define WLED3_SINK_REG_CURR_SINK_MASK GENMASK(7, 5)
#define WLED3_SINK_REG_CURR_SINK_SHFT 5
/* WLED3 specific per-'string' registers below */
-#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x10))
+#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x2))
#define WLED3_SINK_REG_STR_MOD_EN(n) (0x60 + (n * 0x10))
#define WLED3_SINK_REG_STR_MOD_MASK BIT(7)
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR(n) (0x62 + (n * 0x10))
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR_MASK GENMASK(4, 0)
#define WLED3_SINK_REG_STR_MOD_SRC(n) (0x63 + (n * 0x10))
@@ -285,16 +285,60 @@ static int wled5_set_brightness(struct wled *wled, u16 brightness)
WLED5_SINK_REG_MOD_A_BRIGHTNESS_LSB :
WLED5_SINK_REG_MOD_B_BRIGHTNESS_LSB;
rc = regmap_bulk_write(wled->regmap, wled->sink_addr + offset,
&v, sizeof(v));
return rc;
}
+static int wled_read_brightness(struct wled *wled)
+{
+ u16 addr, mask;
+ __le16 v;
+ u32 val;
+ int rc;
+
+ rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
+ &val);
+ if (rc < 0)
+ return rc;
+
+ if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
+ return 0;
+
+ switch (wled->version) {
+ case 3:
+ addr = wled->sink_addr +
+ WLED3_SINK_REG_BRIGHT(wled->cfg.enabled_strings[0]);
+ mask = WLED3_SINK_REG_BRIGHT_MAX;
+ break;
+ case 4:
+ addr = wled->sink_addr +
+ WLED4_SINK_REG_BRIGHT(wled->cfg.enabled_strings[0]);
+ mask = WLED3_SINK_REG_BRIGHT_MAX;
+ break;
+ case 5:
+ addr = wled->sink_addr + (wled->cfg.mod_sel == MOD_A ?
+ WLED5_SINK_REG_MOD_A_BRIGHTNESS_LSB :
+ WLED5_SINK_REG_MOD_B_BRIGHTNESS_LSB);
+ mask = WLED5_SINK_REG_BRIGHT_MAX_15B;
+ break;
+ default:
+ dev_err(wled->dev, "Invalid WLED version\n");
+ return -EINVAL;
+ }
+
+ rc = regmap_bulk_read(wled->regmap, addr, &v, sizeof(v));
+ if (rc < 0)
+ return rc;
+
+ return min_t(u32, le16_to_cpu(v) & mask, wled->max_brightness);
+}
+
static void wled_ovp_work(struct work_struct *work)
{
struct wled *wled = container_of(work,
struct wled, ovp_work.work);
enable_irq(wled->ovp_irq);
}
static int wled_module_enable(struct wled *wled, int val)
@@ -475,16 +519,28 @@ static int wled_update_status(struct backlight_device *bl)
wled->brightness = brightness;
unlock_mutex:
mutex_unlock(&wled->lock);
return rc;
}
+static int wled_get_brightness(struct backlight_device *bl)
+{
+ struct wled *wled = bl_get_data(bl);
+ int rc;
+
+ mutex_lock(&wled->lock);
+ rc = wled_read_brightness(wled);
+ mutex_unlock(&wled->lock);
+
+ return rc;
+}
+
static int wled4_cabc_config(struct wled *wled, bool enable)
{
int i, j, rc;
u8 val;
for (i = 0; i < wled->cfg.num_strings; i++) {
j = wled->cfg.enabled_strings[i];
@@ -1646,25 +1702,25 @@ static int wled_configure_ovp_irq(struct wled *wled,
if (!wled->brightness)
disable_irq(wled->ovp_irq);
return 0;
}
static const struct backlight_ops wled_ops = {
.update_status = wled_update_status,
+ .get_brightness = wled_get_brightness,
};
static int wled_probe(struct platform_device *pdev)
{
struct backlight_properties props;
struct backlight_device *bl;
struct wled *wled;
struct regmap *regmap;
- u32 mod_en;
u32 val;
int rc;
regmap = dev_get_regmap(pdev->dev.parent, NULL);
if (!regmap) {
dev_err(&pdev->dev, "Unable to get regmap\n");
return -EINVAL;
}
@@ -1728,41 +1784,37 @@ static int wled_probe(struct platform_device *pdev)
}
INIT_DELAYED_WORK(&wled->ovp_work, wled_ovp_work);
val = WLED_DEFAULT_BRIGHTNESS;
of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
/*
- * The module may already be enabled, either by a bootloader that left
- * the backlight lit or by the setup above. Record that, so that the
- * first brightness update does not enable an already enabled module,
- * and so that the OVP irq is armed from probe rather than from that
- * first update.
+ * The module may already be lit, either by the bootloader or by the
+ * setup above. Start from what the hardware is driving, so that the
+ * first brightness update does not step the brightness.
*/
- rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
- &mod_en);
+ rc = wled_read_brightness(wled);
if (rc < 0)
return rc;
- if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK)
- wled->brightness = val;
+ wled->brightness = rc;
rc = wled_configure_short_irq(wled, pdev);
if (rc < 0)
return rc;
rc = wled_configure_ovp_irq(wled, pdev);
if (rc < 0)
return rc;
memset(&props, 0, sizeof(struct backlight_properties));
props.type = BACKLIGHT_RAW;
- props.brightness = val;
+ props.brightness = wled->brightness ?: val;
props.max_brightness = wled->max_brightness;
bl = devm_backlight_device_register(&pdev->dev, wled->name,
&pdev->dev, wled,
&wled_ops, &props);
platform_set_drvdata(pdev, bl);
return PTR_ERR_OR_ZERO(bl);
};
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH v3 4/4] backlight: qcom-wled: Read back the programmed brightness at probe
@ 2026-09-14 18:14 ` David Heidelberg
0 siblings, 0 replies; 16+ messages in thread
From: David Heidelberg @ 2026-09-14 18:14 UTC (permalink / raw)
To: Lee Jones, Daniel Thompson, Jingoo Han, Helge Deller, Kiran Gunda,
Marco Mattiolo, Barnabás Czémán
Cc: linux-arm-msm, dri-devel, linux-fbdev, linux-kernel, phone-devel,
Konrad Dybcio, Joel Selvaraj, David Heidelberg
Since the previous change wled_probe() seeds wled->brightness from
MOD_EN, but with default-brightness rather than the level the hardware
is actually driving, so a backlight left lit by the bootloader still
visibly steps on the first update, and actual_brightness reports the
last value written rather than what the panel shows.
Add wled_read_brightness(), which returns 0 when MOD_EN is clear and
otherwise reads the brightness register the matching
wledN_set_brightness() writes: the first enabled string on WLED3 and
WLED4, the selected modulator on WLED5. Use it to seed both
wled->brightness and the initial backlight property, and expose it as
backlight_ops.get_brightness so that actual_brightness reflects the
hardware too.
A module that is enabled with the brightness registers at zero, which
wled4_setup() and wled_auto_string_detection() can leave behind on a
cold boot, now reads back as off. The OVP interrupt is then armed by the
first brightness update rather than from probe, which is what happens
on every other board where the module starts out disabled.
Suggested-by: Konrad Dybcio <konrad.dybcio@oss.qualcomm.com>
Assisted-by: LLM
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/video/backlight/qcom-wled.c | 76 +++++++++++++++++++++++++++++++------
1 file changed, 64 insertions(+), 12 deletions(-)
diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
index eb742a6598173..471a0cb2b81f6 100644
--- a/drivers/video/backlight/qcom-wled.c
+++ b/drivers/video/backlight/qcom-wled.c
@@ -54,17 +54,17 @@
#define WLED3_SINK_REG_SYNC 0x47
#define WLED3_SINK_REG_SYNC_CLEAR 0x00
#define WLED3_SINK_REG_CURR_SINK 0x4f
#define WLED3_SINK_REG_CURR_SINK_MASK GENMASK(7, 5)
#define WLED3_SINK_REG_CURR_SINK_SHFT 5
/* WLED3 specific per-'string' registers below */
-#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x10))
+#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x2))
#define WLED3_SINK_REG_STR_MOD_EN(n) (0x60 + (n * 0x10))
#define WLED3_SINK_REG_STR_MOD_MASK BIT(7)
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR(n) (0x62 + (n * 0x10))
#define WLED3_SINK_REG_STR_FULL_SCALE_CURR_MASK GENMASK(4, 0)
#define WLED3_SINK_REG_STR_MOD_SRC(n) (0x63 + (n * 0x10))
@@ -285,16 +285,60 @@ static int wled5_set_brightness(struct wled *wled, u16 brightness)
WLED5_SINK_REG_MOD_A_BRIGHTNESS_LSB :
WLED5_SINK_REG_MOD_B_BRIGHTNESS_LSB;
rc = regmap_bulk_write(wled->regmap, wled->sink_addr + offset,
&v, sizeof(v));
return rc;
}
+static int wled_read_brightness(struct wled *wled)
+{
+ u16 addr, mask;
+ __le16 v;
+ u32 val;
+ int rc;
+
+ rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
+ &val);
+ if (rc < 0)
+ return rc;
+
+ if (!(val & WLED3_CTRL_REG_MOD_EN_MASK))
+ return 0;
+
+ switch (wled->version) {
+ case 3:
+ addr = wled->sink_addr +
+ WLED3_SINK_REG_BRIGHT(wled->cfg.enabled_strings[0]);
+ mask = WLED3_SINK_REG_BRIGHT_MAX;
+ break;
+ case 4:
+ addr = wled->sink_addr +
+ WLED4_SINK_REG_BRIGHT(wled->cfg.enabled_strings[0]);
+ mask = WLED3_SINK_REG_BRIGHT_MAX;
+ break;
+ case 5:
+ addr = wled->sink_addr + (wled->cfg.mod_sel == MOD_A ?
+ WLED5_SINK_REG_MOD_A_BRIGHTNESS_LSB :
+ WLED5_SINK_REG_MOD_B_BRIGHTNESS_LSB);
+ mask = WLED5_SINK_REG_BRIGHT_MAX_15B;
+ break;
+ default:
+ dev_err(wled->dev, "Invalid WLED version\n");
+ return -EINVAL;
+ }
+
+ rc = regmap_bulk_read(wled->regmap, addr, &v, sizeof(v));
+ if (rc < 0)
+ return rc;
+
+ return min_t(u32, le16_to_cpu(v) & mask, wled->max_brightness);
+}
+
static void wled_ovp_work(struct work_struct *work)
{
struct wled *wled = container_of(work,
struct wled, ovp_work.work);
enable_irq(wled->ovp_irq);
}
static int wled_module_enable(struct wled *wled, int val)
@@ -475,16 +519,28 @@ static int wled_update_status(struct backlight_device *bl)
wled->brightness = brightness;
unlock_mutex:
mutex_unlock(&wled->lock);
return rc;
}
+static int wled_get_brightness(struct backlight_device *bl)
+{
+ struct wled *wled = bl_get_data(bl);
+ int rc;
+
+ mutex_lock(&wled->lock);
+ rc = wled_read_brightness(wled);
+ mutex_unlock(&wled->lock);
+
+ return rc;
+}
+
static int wled4_cabc_config(struct wled *wled, bool enable)
{
int i, j, rc;
u8 val;
for (i = 0; i < wled->cfg.num_strings; i++) {
j = wled->cfg.enabled_strings[i];
@@ -1646,25 +1702,25 @@ static int wled_configure_ovp_irq(struct wled *wled,
if (!wled->brightness)
disable_irq(wled->ovp_irq);
return 0;
}
static const struct backlight_ops wled_ops = {
.update_status = wled_update_status,
+ .get_brightness = wled_get_brightness,
};
static int wled_probe(struct platform_device *pdev)
{
struct backlight_properties props;
struct backlight_device *bl;
struct wled *wled;
struct regmap *regmap;
- u32 mod_en;
u32 val;
int rc;
regmap = dev_get_regmap(pdev->dev.parent, NULL);
if (!regmap) {
dev_err(&pdev->dev, "Unable to get regmap\n");
return -EINVAL;
}
@@ -1728,41 +1784,37 @@ static int wled_probe(struct platform_device *pdev)
}
INIT_DELAYED_WORK(&wled->ovp_work, wled_ovp_work);
val = WLED_DEFAULT_BRIGHTNESS;
of_property_read_u32(pdev->dev.of_node, "default-brightness", &val);
/*
- * The module may already be enabled, either by a bootloader that left
- * the backlight lit or by the setup above. Record that, so that the
- * first brightness update does not enable an already enabled module,
- * and so that the OVP irq is armed from probe rather than from that
- * first update.
+ * The module may already be lit, either by the bootloader or by the
+ * setup above. Start from what the hardware is driving, so that the
+ * first brightness update does not step the brightness.
*/
- rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN,
- &mod_en);
+ rc = wled_read_brightness(wled);
if (rc < 0)
return rc;
- if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK)
- wled->brightness = val;
+ wled->brightness = rc;
rc = wled_configure_short_irq(wled, pdev);
if (rc < 0)
return rc;
rc = wled_configure_ovp_irq(wled, pdev);
if (rc < 0)
return rc;
memset(&props, 0, sizeof(struct backlight_properties));
props.type = BACKLIGHT_RAW;
- props.brightness = val;
+ props.brightness = wled->brightness ?: val;
props.max_brightness = wled->max_brightness;
bl = devm_backlight_device_register(&pdev->dev, wled->name,
&pdev->dev, wled,
&wled_ops, &props);
platform_set_drvdata(pdev, bl);
return PTR_ERR_OR_ZERO(bl);
};
--
2.55.0
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH v3 4/4] backlight: qcom-wled: Read back the programmed brightness at probe
2026-09-14 18:14 ` David Heidelberg
(?)
@ 2026-09-14 18:30 ` sashiko-bot
-1 siblings, 0 replies; 16+ messages in thread
From: sashiko-bot @ 2026-09-14 18:30 UTC (permalink / raw)
To: David Heidelberg; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Critical] Type confusion and memory corruption in wled_remove() due to incorrect drvdata pointer.
Pre-existing issues:
- [High] Use-after-free on probe error path due to uncancelled delayed work.
--
commit 925c19cfe92bc196afb0b7a2001d793450d1b94f
Author: David Heidelberg <david@ixit.cz>
backlight: qcom-wled: Read back the programmed brightness at probe
Since the previous change wled_probe() seeds wled->brightness from
MOD_EN, but with default-brightness rather than the level the hardware
is actually driving, so a backlight left lit by the bootloader still
visibly steps on the first update. Add wled_read_brightness() to seed
both wled->brightness and the initial backlight property.
> diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c
> index eb742a6598173..471a0cb2b81f6 100644
> --- a/drivers/video/backlight/qcom-wled.c
> +++ b/drivers/video/backlight/qcom-wled.c
[ ... ]
> @@ -1757,7 +1809,7 @@ static int wled_probe(struct platform_device *pdev)
>
> memset(&props, 0, sizeof(struct backlight_properties));
> props.type = BACKLIGHT_RAW;
> - props.brightness = val;
> + props.brightness = wled->brightness ?: val;
> props.max_brightness = wled->max_brightness;
> bl = devm_backlight_device_register(&pdev->dev, wled->name,
> &pdev->dev, wled,
> &wled_ops, &props);
> platform_set_drvdata(pdev, bl);
[Severity: Critical]
Does this code incorrectly set the platform drvdata to a backlight_device
pointer instead of the expected wled structure pointer?
Looking at wled_remove(), it retrieves the platform drvdata but dereferences
it as a struct wled:
static void wled_remove(struct platform_device *pdev)
{
struct wled *wled = platform_get_drvdata(pdev);
mutex_destroy(&wled->lock);
cancel_delayed_work_sync(&wled->ovp_work);
...
}
Because the memory layout does not match, can this cause type confusion,
destruction of uninitialized or unrelated mutexes, and guaranteed memory
corruption or a kernel panic on removal?
> return PTR_ERR_OR_ZERO(bl);
[Severity: High]
This is a pre-existing issue, but if devm_backlight_device_register() fails,
this function returns an error and devm frees the wled structure without
canceling any potentially scheduled delayed work. Is it possible for a
spurious IRQ to trigger a use-after-free here?
wled_probe() configures and implicitly enables the short IRQ earlier via
devm_request_threaded_irq(). It also configures the OVP IRQ, which sets
wled->ovp_irq > 0. If the short IRQ triggers during or just before this
failure, wled_short_irq_handler() will execute and call
wled_module_enable(wled, true).
Because wled->ovp_irq > 0, wled_module_enable() schedules the delayed work:
schedule_delayed_work(&wled->ovp_work, HZ / 100);
Since the probe error path does not call cancel_delayed_work_sync(), this
leaves the work on the queue. When the delayed work executes, it calls:
static void wled_ovp_work(struct work_struct *work)
{
struct wled *wled = container_of(work,
struct wled, ovp_work.work);
enable_irq(wled->ovp_irq);
}
Can this access the already freed wled structure during the delayed work
execution, resulting in a use-after-free?
> };
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914-qcom-wled-backlight-v3-0-d69fe9843841@ixit.cz?part=4
^ permalink raw reply [flat|nested] 16+ messages in thread