A use-after-free exists in the Zephyr second-generation work queue (kernel/work.c) in the handling of delayable work timeouts. When a delayable work item's timeout has been dequeued and its handler work_timeout() is in flight (blocked acquiring the work-queue spinlock), a concurrent cancellation does not wait for that handler to finish. In unschedule_locked() the pre-fix code called z_abort_timeout(), which for an already-announcing record returns -EINVAL without removing it; cancel_async_locked() then observes the work as idle, so even k_work_cancel_delayable_sync() and k_work_flush_delayable() return without blocking on the in-flight handler.
Because those are the APIs the kernel header documents as the safe way to cancel before freeing a k_work_delayable, a caller that frees the object immediately after a successful sync cancel can race the still-pending handler. work_timeout() subsequently dereferences the freed record: it reads to->dticks via z_is_timeout_handler_canceled() and, if the freed slot has been reused so the bail check fails, performs a read-modify-write of wp->flags (K_WORK_DELAYED_BIT) and submits work against a stale dw->queue pointer — a use-after-free read and write.
The k_work API is kernel-mode only (no __syscall entry point), so this is a kernel-internal concurrency defect rather than a userspace privilege escalation. Triggering it requires an SMP build and a subsystem that schedules and then frees (or reschedules) a delayable work item in the narrow window while its timeout is announcing; an attacker able to influence the timing of such teardown (for example via connection churn driving subsystem timers) has a plausible but probabilistic path. The impact is kernel memory corruption or crash (denial of service).
The fix makes unschedule_locked() wait, by spinning on z_try_abort_timeout() returning -EAGAIN while releasing and re-acquiring the work spinlock, until any in-flight handler completes before returning, and switches work_timeout() to atomic K_WORK_DELAYED_BIT ownership. This closes both the free-then-handler use-after-free and the related reschedule early-fire race.
Affected components
Affected versions
All releases through v4.4.0 (vulnerable code present in v4.4.0 kernel/work.c)
Fix
Fixed (merged) in 59cf34b
Projected fixed version: 4.5.0 (the fix is merged on main but not yet released; this forecast should be confirmed against the actual release).
Introduced by: Long-standing gen-2 delayable-work in-flight timeout handling (z_abort_timeout + z_is_timeout_handler_canceled); present through v4.4.0
Evidence
- kernel/work.c:1105-1132 — fixed unschedule_locked() now waits on z_try_abort_timeout()==-EAGAIN for the in-flight handler before returning true
- kernel/work.c:1000-1019 — work_timeout() relies on K_WORK_DELAYED_BIT ownership post-fix (the dereference site of dwork->work.flags)
- kernel/work.c:1239-1269 — k_work_cancel_delayable_sync(): when work is only delayed, cancel_sync_locked() returns false so the call does not wait for an in-flight timeout handler
- kernel/work.c:537-551 — cancel_sync_locked() only waits when K_WORK_CANCELING_BIT is set (running work), not for the timeout handler
- include/zephyr/kernel.h:4445-4463 — k_work_cancel_delayable doc: async may still be running; use the sync/flush variants to ensure not running (the documented safe-free path that still raced)
- v4.4.0:kernel/work.c unschedule_locked — released code used
ret = z_abort_timeout(&dwork->timeout) == 0, confirming the pre-fix defect shipped
- v4.4.0:kernel/timeout.c z_abort_timeout — returns -EINVAL for an ANNOUNCING (in-flight) record, only stamping dticks=ABORTED
- kernel/include/timeout_q.h z_is_timeout_handler_canceled — reads to->dticks, the freed-memory read in the in-flight handler
Patches
| Branch |
Pull request |
Status |
main |
#109977 |
merged |
v4.4-branch |
#112961 |
merged |
v4.3-branch |
— |
not applicable |
v3.7-branch |
— |
not applicable |
For more information
If you have any questions or comments about this advisory:
embargo: 2026-08-14
A use-after-free exists in the Zephyr second-generation work queue (
kernel/work.c) in the handling of delayable work timeouts. When a delayable work item's timeout has been dequeued and its handlerwork_timeout()is in flight (blocked acquiring the work-queue spinlock), a concurrent cancellation does not wait for that handler to finish. Inunschedule_locked()the pre-fix code calledz_abort_timeout(), which for an already-announcing record returns-EINVALwithout removing it;cancel_async_locked()then observes the work as idle, so evenk_work_cancel_delayable_sync()andk_work_flush_delayable()return without blocking on the in-flight handler.Because those are the APIs the kernel header documents as the safe way to cancel before freeing a
k_work_delayable, a caller that frees the object immediately after a successful sync cancel can race the still-pending handler.work_timeout()subsequently dereferences the freed record: it readsto->dticksviaz_is_timeout_handler_canceled()and, if the freed slot has been reused so the bail check fails, performs a read-modify-write ofwp->flags(K_WORK_DELAYED_BIT) and submits work against a staledw->queuepointer — a use-after-free read and write.The
k_workAPI is kernel-mode only (no__syscallentry point), so this is a kernel-internal concurrency defect rather than a userspace privilege escalation. Triggering it requires an SMP build and a subsystem that schedules and then frees (or reschedules) a delayable work item in the narrow window while its timeout is announcing; an attacker able to influence the timing of such teardown (for example via connection churn driving subsystem timers) has a plausible but probabilistic path. The impact is kernel memory corruption or crash (denial of service).The fix makes
unschedule_locked()wait, by spinning onz_try_abort_timeout()returning-EAGAINwhile releasing and re-acquiring the work spinlock, until any in-flight handler completes before returning, and switcheswork_timeout()to atomicK_WORK_DELAYED_BITownership. This closes both the free-then-handler use-after-free and the related reschedule early-fire race.Affected components
kernel/work.cAffected versions
All releases through v4.4.0 (vulnerable code present in v4.4.0 kernel/work.c)
Fix
Fixed (merged) in 59cf34b
Projected fixed version: 4.5.0 (the fix is merged on
mainbut not yet released; this forecast should be confirmed against the actual release).Introduced by: Long-standing gen-2 delayable-work in-flight timeout handling (z_abort_timeout + z_is_timeout_handler_canceled); present through v4.4.0
Evidence
ret = z_abort_timeout(&dwork->timeout) == 0, confirming the pre-fix defect shippedPatches
mainv4.4-branchv4.3-branchv3.7-branchFor more information
If you have any questions or comments about this advisory:
embargo: 2026-08-14