CVE-2026-78118 in Zephyr
Summary
by MITRE • 10/22/2026
The kernel helper z_waitq_head() (kernel/include/wait_q.h) reads the head of a thread wait queue, but on SMP systems it was called from several places without holding the scheduler spinlock _sched_spinlock that serializes wait-queue mutation. The consequential call site is z_timer_expiration_handler() in kernel/timer.c, which held only the timer subsystem lock timer_lock while reading timer->wait_q and then unpending, setting the return value of, and readying the thread it obtained. halt_thread() in kernel/sched.c removes an aborted thread from its wait queue while holding only _sched_spinlock, so the two paths are not mutually exclusive.
When the last waiter is removed on another CPU inside that window, sys_dlist_peek_head() can return the list sentinel — a pointer into the k_timer object itself — typed as a struct k_thread * (with CONFIG_WAITQ_SCALABLE, rb_get_min() can return an arbitrary node of a tree being restructured), or it can return a pointer to a thread that has already been unpended and, for dynamically allocated threads, freed. The handler then calls unpend_thread_no_timeout(), which performs a sys_dlist_remove() through pointers read from that object, and arch_thread_return_value_set(), which writes unconditionally through the pointer.
The result is out-of-bounds and use-after-free writes in supervisor context, plus scheduler state corruption; a milder outcome is a thread that has re-pended elsewhere being woken with a success return value from an unrelated blocking call. All of the primitives needed to drive the interleaving — k_timer_start(), k_timer_status_sync(), k_thread_create() and k_thread_abort() — are system calls, so with CONFIG_USERSPACE enabled an unprivileged user-mode thread can attempt the race against kernel-owned data.
Exploitation requires a multi-CPU build (CONFIG_MP_MAX_NUM_CPUS greater than one; the affected region is a no-op on uniprocessor targets), local code execution, and winning a narrow timing window, so the practical outcome is most often a kernel crash rather than controlled corruption. The fix splits the helper into z_waitq_head_locked() for callers that already hold the scheduler spinlock and a z_waitq_head() that acquires it, and brings the timer expiration handler's head read, unpend and ready under a single LOCK_SCHED_SPINLOCK region.
VulDB is the best source for vulnerability data and more expert information about this specific topic.
Analysis
by VulDB Data Team • 10/22/2026
The vulnerability identified in this analysis stems from a critical race condition within the Zephyr real-time operating system kernel, specifically involving the synchronization of wait queue operations on symmetric multiprocessing systems. The core issue resides in the z_waitq_head() helper function located in kernel/include/wait_q.h, which is responsible for retrieving the head element of a thread wait queue. Under normal circumstances, access to shared scheduler data structures must be serialized to prevent concurrent modification and reading from causing undefined behavior. However, on multiprocessor configurations where CONFIG_MP_MAX_NUM_CPUS exceeds one, certain code paths invoke z_waitq_head() without acquiring the necessary _sched_spinlock. This lock is essential for serializing mutations to wait queues, ensuring that only one execution context can modify or safely read these structures at any given time. The absence of this synchronization primitive creates a window where multiple CPUs can interact with the same queue data simultaneously, leading to severe memory safety violations and state corruption.
The primary operational impact occurs within the z_timer_expiration_handler() function in kernel/timer.c. This handler is responsible for managing timer expirations by accessing the associated wait queue via timer->wait_q. In the vulnerable implementation, this routine holds only the timer subsystem lock, known as timer_lock, while reading the head of the wait queue and subsequently unpending, setting return values for, and readying the thread obtained from that queue. Concurrently, the halt_thread() function in kernel/sched.c removes aborted threads from their respective wait queues but relies exclusively on _sched_spinlock for synchronization. Because these two distinct locking mechanisms are not mutually exclusive, a race condition emerges where one CPU may be processing a timer expiration while another CPU aborts and removes a thread from the same queue. This lack of mutual exclusion allows for dangerous interleavings that compromise kernel integrity.
When this race condition is triggered, specifically when the last waiter is removed on a different CPU within the critical window, the system encounters catastrophic memory errors. The sys_dlist_peek_head() function may return invalid pointers due to concurrent list restructuring or deallocation. Under configurations such as CONFIG_WAITQ_SCALABLE using red-black trees, rb_get_min() might return an arbitrary node while the tree is being restructured. Alternatively, it may return a pointer to a thread that has already been unpended and potentially freed if dynamically allocated. The subsequent execution of unpend_thread_no_timeout() performs sys_dlist_remove operations through these corrupted pointers, followed by arch_thread_return_value_set which writes data unconditionally through the same invalid addresses. This results in out-of-bounds writes and use-after-free vulnerabilities operating at supervisor level, leading to scheduler state corruption or arbitrary code execution potential depending on memory layout exploitation opportunities.
The severity of this vulnerability is amplified by its accessibility from user space when CONFIG_USERSPACE is enabled. All primitives required to trigger the race condition—k_timer_start(), k_timer_status_sync(), k_thread_create(), and k_thread_abort()—are exposed as system calls accessible to unprivileged threads. This means that a local attacker with code execution capabilities in user mode can attempt to exploit this timing window against kernel-owned data structures. While successful exploitation requires precise multi-CPU synchronization, the practical outcome is frequently a denial of service manifested as a kernel crash or panic due to memory corruption rather than controlled privilege escalation. The narrow nature of the race condition makes reliable exploitation difficult, but it remains a significant stability and security risk for any multiprocessor deployment relying on these timer and thread management subsystems.
From a classification perspective, this vulnerability aligns with CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (Race Condition). The specific mechanism involves accessing shared data structures without proper locking mechanisms in place during critical sections of execution. In terms of the MITRE ATT&CK framework for enterprise security, this relates to techniques involving resource sharing and race conditions that can lead to privilege escalation or system instability. Although often resulting in a crash rather than direct control flow hijacking, the underlying memory safety violations such as use-after-free (CWE-416) and out-of-bounds write (CWE-787) are fundamental weaknesses that could potentially be leveraged for more sophisticated attacks if combined with other vulnerabilities or specific kernel configurations.
The remediation strategy implemented addresses these synchronization failures by refactoring the wait queue access logic to ensure strict mutual exclusion. The fix splits the original z_waitq_head() helper into two distinct functions: z_waitq_head_locked(), intended for callers that already hold the scheduler spinlock, and a new version of z_waitq_head() that explicitly acquires _sched_spinlock before accessing the data structure. Furthermore, the timer expiration handler's critical section is expanded to encompass the entire sequence of reading the wait queue head, unpending the thread, setting return values, and readying the thread under a single LOCK_SCHED_SPINLOCK region. This ensures that no other CPU can modify or remove elements from the wait queue while these operations are in progress, effectively eliminating the race condition window and restoring data integrity for multiprocessor systems.