* [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse
@ 2026-04-27 16:47 Breno Leitao
2026-04-27 17:29 ` Tejun Heo
2026-04-27 17:51 ` Andy Shevchenko
0 siblings, 2 replies; 5+ messages in thread
From: Breno Leitao @ 2026-04-27 16:47 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan, Andy Shevchenko, Krzysztof Kozlowski
Cc: clm, linux-kernel, kernel-team, Breno Leitao
devm_alloc_workqueue() builds a va_list from its own variadic
arguments and then passes that va_list as a single positional
argument to the variadic alloc_workqueue() macro:
va_start(args, max_active);
wq = alloc_workqueue(fmt, flags, max_active, args);
va_end(args);
alloc_workqueue() expands to alloc_workqueue_noprof(), which
performs its own va_start() over its ... parameters. The inner
vsnprintf(wq->name, sizeof(wq->name), fmt, args) inside
__alloc_workqueue() therefore receives the outer va_list object
as the first variadic slot, not the caller's actual format
arguments. On x86-64 SysV a "%s" conversion dereferences the
pointer inside that va_list as a char * and copies arbitrary
stack bytes from devm_alloc_workqueue()'s register-save area
into wq->name. The write is bounded by WQ_NAME_LEN but the
read is not, so this is an out-of-bounds stack read whose result
also surfaces via show_all_workqueues(), print_worker_info(),
the WQ_NAME_LEN pr_warn_once(), and lockdep lock names.
C does not allow forwarding a va_list through a ... parameter;
a v-style entry point is required. __alloc_workqueue() is
exactly that and is already used by alloc_workqueue_noprof().
Split devm_alloc_workqueue() into devm_alloc_workqueue_noprof()
plus an alloc_hooks() macro wrapper, mirroring the
alloc_workqueue / alloc_workqueue_noprof split. The _noprof
function calls __alloc_workqueue() directly with the va_list we
already hold, and runs wq_init_lockdep(wq) afterwards to mirror
alloc_workqueue_noprof() (otherwise wq->lockdep_map stays NULL
and __flush_workqueue()'s on-stack
COMPLETION_INITIALIZER_ONSTACK_MAP would NULL-deref it).
Keeping the alloc_hooks() wrapper preserves memory allocation
profiling attribution under CONFIG_MEM_ALLOC_PROFILING=y: the
kzalloc_noprof() and alloc_workqueue_attrs_noprof() inside
__alloc_workqueue() are charged to the driver call site rather
than to a single line in kernel/workqueue.c.
No caller changes are required. Drivers continue to write
devm_alloc_workqueue() and devm_alloc_ordered_workqueue() as
before; the new macro forwards through alloc_hooks() into the
renamed _noprof function transparently. Out-of-tree modules
need only be recompiled against the updated header to pick up
the renamed export, mirroring the precedent set by the existing
alloc_workqueue / alloc_workqueue_noprof split.
devm_alloc_ordered_workqueue() in include/linux/workqueue.h is
a macro that forwards to devm_alloc_workqueue(), so it inherits
this fix automatically. Two in-tree callers actively trigger
the broken path on every probe:
drivers/power/supply/mt6370-charger.c:889
drivers/power/supply/max77705_charger.c:649
both of which use devm_alloc_ordered_workqueue(dev, "%s", 0,
dev_name(dev)).
A standalone reproducer module is available at[1]:
Link: https://github.com/leitao/debug/blob/main/workqueue/valist/wq_va_test.c [1]
Fixes: 1dfc9d60a69e ("workqueue: devres: Add device-managed allocate workqueue")
Signed-off-by: Breno Leitao <leitao@debian.org>
---
include/linux/workqueue.h | 6 ++++--
kernel/workqueue.c | 16 ++++++++++++----
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
index ab6cb70ca1a52..6177624539b3b 100644
--- a/include/linux/workqueue.h
+++ b/include/linux/workqueue.h
@@ -534,8 +534,10 @@ alloc_workqueue_noprof(const char *fmt, unsigned int flags, int max_active, ...)
* Pointer to the allocated workqueue on success, %NULL on failure.
*/
__printf(2, 5) struct workqueue_struct *
-devm_alloc_workqueue(struct device *dev, const char *fmt, unsigned int flags,
- int max_active, ...);
+devm_alloc_workqueue_noprof(struct device *dev, const char *fmt,
+ unsigned int flags, int max_active, ...);
+#define devm_alloc_workqueue(...) \
+ alloc_hooks(devm_alloc_workqueue_noprof(__VA_ARGS__))
#ifdef CONFIG_LOCKDEP
/**
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 5f747f241a5f1..ee5bd627bb52a 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -5932,26 +5932,34 @@ static void devm_workqueue_release(void *res)
}
__printf(2, 5) struct workqueue_struct *
-devm_alloc_workqueue(struct device *dev, const char *fmt, unsigned int flags,
- int max_active, ...)
+devm_alloc_workqueue_noprof(struct device *dev, const char *fmt,
+ unsigned int flags, int max_active, ...)
{
struct workqueue_struct *wq;
va_list args;
int ret;
va_start(args, max_active);
- wq = alloc_workqueue(fmt, flags, max_active, args);
+ wq = __alloc_workqueue(fmt, flags, max_active, args);
va_end(args);
if (!wq)
return NULL;
+ /*
+ * __alloc_workqueue() doesn't initialize wq->lockdep_map; only its
+ * sibling alloc_workqueue_noprof() does. Mirror that here, otherwise
+ * __flush_workqueue()'s COMPLETION_INITIALIZER_ONSTACK_MAP would
+ * dereference a NULL wq->lockdep_map.
+ */
+ wq_init_lockdep(wq);
+
ret = devm_add_action_or_reset(dev, devm_workqueue_release, wq);
if (ret)
return NULL;
return wq;
}
-EXPORT_SYMBOL_GPL(devm_alloc_workqueue);
+EXPORT_SYMBOL_GPL(devm_alloc_workqueue_noprof);
#ifdef CONFIG_LOCKDEP
__printf(1, 5)
---
base-commit: dd6c438c3e64a5ff0b5d7e78f7f9be547803ef1b
change-id: 20260427-wq_fix_chris-e23e3423c9a3
Best regards,
--
Breno Leitao <leitao@debian.org>
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse
2026-04-27 16:47 [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse Breno Leitao
@ 2026-04-27 17:29 ` Tejun Heo
2026-04-28 15:00 ` Breno Leitao
2026-04-27 17:51 ` Andy Shevchenko
1 sibling, 1 reply; 5+ messages in thread
From: Tejun Heo @ 2026-04-27 17:29 UTC (permalink / raw)
To: Breno Leitao
Cc: Lai Jiangshan, Andy Shevchenko, Krzysztof Kozlowski, clm,
linux-kernel, kernel-team
Hello, Breno.
Nice catch. Could you fold both noprof entries onto a shared helper?
static struct workqueue_struct *
alloc_workqueue_va(const char *fmt, unsigned int flags,
int max_active, va_list args)
{
struct workqueue_struct *wq;
wq = __alloc_workqueue(fmt, flags, max_active, args);
if (wq)
wq_init_lockdep(wq);
return wq;
}
alloc_workqueue_lockdep_map() can stay on bare __alloc_workqueue() since
it supplies its own lockdep_map.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse
2026-04-27 16:47 [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse Breno Leitao
2026-04-27 17:29 ` Tejun Heo
@ 2026-04-27 17:51 ` Andy Shevchenko
2026-04-28 14:59 ` Breno Leitao
1 sibling, 1 reply; 5+ messages in thread
From: Andy Shevchenko @ 2026-04-27 17:51 UTC (permalink / raw)
To: Breno Leitao
Cc: Tejun Heo, Lai Jiangshan, Krzysztof Kozlowski, clm, linux-kernel,
kernel-team
On Mon, Apr 27, 2026 at 09:47:44AM -0700, Breno Leitao wrote:
> devm_alloc_workqueue() builds a va_list from its own variadic
> arguments and then passes that va_list as a single positional
> argument to the variadic alloc_workqueue() macro:
>
> va_start(args, max_active);
> wq = alloc_workqueue(fmt, flags, max_active, args);
> va_end(args);
>
> alloc_workqueue() expands to alloc_workqueue_noprof(), which
> performs its own va_start() over its ... parameters. The inner
> vsnprintf(wq->name, sizeof(wq->name), fmt, args) inside
> __alloc_workqueue() therefore receives the outer va_list object
> as the first variadic slot, not the caller's actual format
> arguments. On x86-64 SysV a "%s" conversion dereferences the
> pointer inside that va_list as a char * and copies arbitrary
> stack bytes from devm_alloc_workqueue()'s register-save area
> into wq->name. The write is bounded by WQ_NAME_LEN but the
> read is not, so this is an out-of-bounds stack read whose result
> also surfaces via show_all_workqueues(), print_worker_info(),
> the WQ_NAME_LEN pr_warn_once(), and lockdep lock names.
>
> C does not allow forwarding a va_list through a ... parameter;
> a v-style entry point is required. __alloc_workqueue() is
> exactly that and is already used by alloc_workqueue_noprof().
> Split devm_alloc_workqueue() into devm_alloc_workqueue_noprof()
> plus an alloc_hooks() macro wrapper, mirroring the
> alloc_workqueue / alloc_workqueue_noprof split. The _noprof
> function calls __alloc_workqueue() directly with the va_list we
> already hold, and runs wq_init_lockdep(wq) afterwards to mirror
> alloc_workqueue_noprof() (otherwise wq->lockdep_map stays NULL
> and __flush_workqueue()'s on-stack
> COMPLETION_INITIALIZER_ONSTACK_MAP would NULL-deref it).
>
> Keeping the alloc_hooks() wrapper preserves memory allocation
> profiling attribution under CONFIG_MEM_ALLOC_PROFILING=y: the
> kzalloc_noprof() and alloc_workqueue_attrs_noprof() inside
> __alloc_workqueue() are charged to the driver call site rather
> than to a single line in kernel/workqueue.c.
>
> No caller changes are required. Drivers continue to write
> devm_alloc_workqueue() and devm_alloc_ordered_workqueue() as
> before; the new macro forwards through alloc_hooks() into the
> renamed _noprof function transparently. Out-of-tree modules
> need only be recompiled against the updated header to pick up
> the renamed export, mirroring the precedent set by the existing
> alloc_workqueue / alloc_workqueue_noprof split.
>
> devm_alloc_ordered_workqueue() in include/linux/workqueue.h is
> a macro that forwards to devm_alloc_workqueue(), so it inherits
> this fix automatically. Two in-tree callers actively trigger
> the broken path on every probe:
>
> drivers/power/supply/mt6370-charger.c:889
> drivers/power/supply/max77705_charger.c:649
>
> both of which use devm_alloc_ordered_workqueue(dev, "%s", 0,
> dev_name(dev)).
>
> A standalone reproducer module is available at[1]:
>
> Link: https://github.com/leitao/debug/blob/main/workqueue/valist/wq_va_test.c [1]
>
There shouldn't be blank line(s) in the tag block.
> Fixes: 1dfc9d60a69e ("workqueue: devres: Add device-managed allocate workqueue")
> Signed-off-by: Breno Leitao <leitao@debian.org>
Otherwise second Tejun's "nice catch"!
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse
2026-04-27 17:51 ` Andy Shevchenko
@ 2026-04-28 14:59 ` Breno Leitao
0 siblings, 0 replies; 5+ messages in thread
From: Breno Leitao @ 2026-04-28 14:59 UTC (permalink / raw)
To: Andy Shevchenko
Cc: Tejun Heo, Lai Jiangshan, Krzysztof Kozlowski, clm, linux-kernel,
kernel-team
On Mon, Apr 27, 2026 at 08:51:22PM +0300, Andy Shevchenko wrote:
> On Mon, Apr 27, 2026 at 09:47:44AM -0700, Breno Leitao wrote:
> > devm_alloc_workqueue() builds a va_list from its own variadic
> > arguments and then passes that va_list as a single positional
> > argument to the variadic alloc_workqueue() macro:
> >
> > va_start(args, max_active);
> > wq = alloc_workqueue(fmt, flags, max_active, args);
> > va_end(args);
> >
> > alloc_workqueue() expands to alloc_workqueue_noprof(), which
> > performs its own va_start() over its ... parameters. The inner
> > vsnprintf(wq->name, sizeof(wq->name), fmt, args) inside
> > __alloc_workqueue() therefore receives the outer va_list object
> > as the first variadic slot, not the caller's actual format
> > arguments. On x86-64 SysV a "%s" conversion dereferences the
> > pointer inside that va_list as a char * and copies arbitrary
> > stack bytes from devm_alloc_workqueue()'s register-save area
> > into wq->name. The write is bounded by WQ_NAME_LEN but the
> > read is not, so this is an out-of-bounds stack read whose result
> > also surfaces via show_all_workqueues(), print_worker_info(),
> > the WQ_NAME_LEN pr_warn_once(), and lockdep lock names.
> >
> > C does not allow forwarding a va_list through a ... parameter;
> > a v-style entry point is required. __alloc_workqueue() is
> > exactly that and is already used by alloc_workqueue_noprof().
> > Split devm_alloc_workqueue() into devm_alloc_workqueue_noprof()
> > plus an alloc_hooks() macro wrapper, mirroring the
> > alloc_workqueue / alloc_workqueue_noprof split. The _noprof
> > function calls __alloc_workqueue() directly with the va_list we
> > already hold, and runs wq_init_lockdep(wq) afterwards to mirror
> > alloc_workqueue_noprof() (otherwise wq->lockdep_map stays NULL
> > and __flush_workqueue()'s on-stack
> > COMPLETION_INITIALIZER_ONSTACK_MAP would NULL-deref it).
> >
> > Keeping the alloc_hooks() wrapper preserves memory allocation
> > profiling attribution under CONFIG_MEM_ALLOC_PROFILING=y: the
> > kzalloc_noprof() and alloc_workqueue_attrs_noprof() inside
> > __alloc_workqueue() are charged to the driver call site rather
> > than to a single line in kernel/workqueue.c.
> >
> > No caller changes are required. Drivers continue to write
> > devm_alloc_workqueue() and devm_alloc_ordered_workqueue() as
> > before; the new macro forwards through alloc_hooks() into the
> > renamed _noprof function transparently. Out-of-tree modules
> > need only be recompiled against the updated header to pick up
> > the renamed export, mirroring the precedent set by the existing
> > alloc_workqueue / alloc_workqueue_noprof split.
> >
> > devm_alloc_ordered_workqueue() in include/linux/workqueue.h is
> > a macro that forwards to devm_alloc_workqueue(), so it inherits
> > this fix automatically. Two in-tree callers actively trigger
> > the broken path on every probe:
> >
> > drivers/power/supply/mt6370-charger.c:889
> > drivers/power/supply/max77705_charger.c:649
> >
> > both of which use devm_alloc_ordered_workqueue(dev, "%s", 0,
> > dev_name(dev)).
> >
> > A standalone reproducer module is available at[1]:
> >
> > Link: https://github.com/leitao/debug/blob/main/workqueue/valist/wq_va_test.c [1]
>
> >
>
> There shouldn't be blank line(s) in the tag block.
Ack, I will remove it in the respin.
>
> > Fixes: 1dfc9d60a69e ("workqueue: devres: Add device-managed allocate workqueue")
> > Signed-off-by: Breno Leitao <leitao@debian.org>
>
> Otherwise second Tejun's "nice catch"!
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse
2026-04-27 17:29 ` Tejun Heo
@ 2026-04-28 15:00 ` Breno Leitao
0 siblings, 0 replies; 5+ messages in thread
From: Breno Leitao @ 2026-04-28 15:00 UTC (permalink / raw)
To: Tejun Heo
Cc: Lai Jiangshan, Andy Shevchenko, Krzysztof Kozlowski, clm,
linux-kernel, kernel-team
On Mon, Apr 27, 2026 at 07:29:31AM -1000, Tejun Heo wrote:
> Nice catch. Could you fold both noprof entries onto a shared helper?
>
> static struct workqueue_struct *
> alloc_workqueue_va(const char *fmt, unsigned int flags,
> int max_active, va_list args)
> {
> struct workqueue_struct *wq;
>
> wq = __alloc_workqueue(fmt, flags, max_active, args);
> if (wq)
> wq_init_lockdep(wq);
> return wq;
> }
>
> alloc_workqueue_lockdep_map() can stay on bare __alloc_workqueue() since
> it supplies its own lockdep_map.
Ack, I will update and respin.
Thanks for the review,
--breno
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-04-28 15:00 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-27 16:47 [PATCH] workqueue: fix devm_alloc_workqueue() va_list misuse Breno Leitao
2026-04-27 17:29 ` Tejun Heo
2026-04-28 15:00 ` Breno Leitao
2026-04-27 17:51 ` Andy Shevchenko
2026-04-28 14:59 ` Breno Leitao
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox