Skip to content

Use-after-free in Zephyr delayable work-queue cancellation under SMP timing race

Moderate
d3zd3z published GHSA-rhmh-r93p-6g99 Aug 14, 2026

Software

zephyr

Affected versions

>= 2.6.0, <= 4.4.2

Patched versions

4.5.0

Description

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

  • kernel/work.c

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

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H

CVE ID

CVE-2026-12365

Weaknesses

Use After Free

The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory belongs to the code that operates on the new pointer. Learn more on MITRE.