Two boot looped, I had to enter recovery and the other just powered off [0].
The demo modifies the wallpaper on supported Pixel devices.
[0] IonStack https://rootme.nebusec.ai
____
Tip: Install a Chromium flavor browser (Chromite) separate from the main browser.
Disable Javascript and hardware accelerated video decoder (commonly exploited) from the flags page and enable reader mode to fix broken JS-dependent websites when browsing blogs and random sites on your personal devices, else dedicate a tablet.
There is a lot of cool work that went into making memory allocation work well; the different arenas, fast bins, chunk headers, etc. are super cool.
It was even enumerated in the first pass of CWE as CWE-416 in 2006.
2025: https://redis.io/blog/security-advisory-cve-2025-49844/ 2023: https://seclists.org/oss-sec/2023/q2/133 2022: https://www.zerodayinitiative.com/advisories/ZDI-22-1690/ 2014: https://ftp.openbsd.org/pub/OpenBSD/patches/5.4/common/008_o...
It's an issue as old as time, or thereabouts.
It's not so widely used and it's not explained in the first couple screenfuls of TFA (which by itself is weirdly structured, taking entire paragraphs to explain when it was introduced, when it was discovered, etc. before even explaining what it actually is).
Of course the title was chosen when the article was first published on a site dedicated to security, where probably everyone knows it. This suggests that insisting on unmodified titles when republishing in HN is a poor rule.
I would have hoped that only a few of us are so misinformed as to do that.
Also can we talk about how bad Linux security is? At this point it's becoming a real liability to run anything on Linux that needs to be secure. OpenBSD has been around for ages, is written in C, and is really, really secure. Do they support containers yet (or microVMs)? Cuz if they do, I'm moving my workloads to obsd.
The reason I re-upped this post, btw, is that it was at the top of a list called "underwater" that we try to look at every day, which lists the most-upvoted posts that for whatever reason didn't happen to make the frontpage. This was at the top of that list.
Any app that can run native code execution on any version of Linux in the past fifteen years can get root until kernel updates arrive on your devices.
Backporting an old kernel should be possible, but the only indicator is the system update changelog that explicitly mentions it, I rarely see CVEs mentioned in changelogs on any smartphone. A tool to test the vulnerability is the only way.
Any compromised app on the Play store or external can get root access instantly, but we can still rely on trust and audits when installing apps which should always be the rule.
I suspect that this will be added to all Google Play integrity levels, limiting many apps from being installed on unpatched phones in the future.
That's not the case with browsers with random sites and ads which is hardly avoidable, having any sandbox escape is now more severe considering that it bypasses the app container. It's similar to JailbreakMe on iOS [0]
... until its getting popular usage and gets targeted for vulnerability research
Why are you not making easy money hand over fist from these rewards? A couple of weekends of work and you can retire early.
Maybe that's exactly what these infosec companies are. And maybe you need more than "a skid with claude over a weekend" to get anything worthwhile.
Messing with the displayed times people sent their comments is honestly akin to rewriting their answers.
You do realize that a full kernel vulnerability like this allows you to feed falsified information to SafetyNet? Just like DRM, it gives the developer the illusion of control, but doesn't do anything to actually improve "safety" or "integrity".
It's silly that whenever I see a vulnerability like this, all I can think about is "finally, a way to get control over my own devices back". Once again, Stallman was right.
https://www.gnu.org/philosophy/right-to-read.en.html
Personally, I'll use this to root my Android TV and Chromecast devices and remove the shitty ads in the launcher (which Google added after I bought the devices!).
How the cluster f*k of the Android update situation Google has allowed this to happen really needs a regulator to step in.
Planned obsolescence is supposed to be illegal in Europe.
There are many issues with the formulation of containers on Linux (though I think people overstate it whenever bugs like this happen) but ultimately this bug was a UAF that gave you arbitrary code execution in the kernel. Zone IDs are also just numbers in kernel memory... right?
Are you sure that's true? The whole reason why modern Safetynet/Play Integrity uses HSM data where possible is that you can't spoof that with root (without a microcode bug). It does not trust the running OS by design
I just tried GrapheneOS's https://attestation.app/ on a stock Pixel, and all of the OS version info shows in the "hardware verified" section
Good news for reusing old phones and taking control.
*as in replace
Maybe just Algol68 and Rust can withstand this, among ADA. And PL/1 under Multics.
[0]https://www.usenix.org/legacy/event/lisa04/tech/full_papers/...
[1]Tape different things together and see if it holds.
First there is Android's attestation framework. That does actual hardware attestation, as used by GrapheneOS, and supported by literally no app whatsoever.
Then there is SafetyNet, now Play Integrity. Depending on what level of integrity checking is being done, this will do a combination of cursory surface-level software checks, delegation to the aforementioned hardware attestation framework, and several other checks.
Importantly, SafetyNet/Play Integrity rejects some devices that pass hardware attestation (e.g., Graphene OS), and accepts some devices that fail hardware attestation (fairphone, many cheaper devices with broken ROMs, etc).
e.g., fairphone leaked the private key for their attestation, but many of their devices still pass SafetyNet, while some other devices that pass attestation but have known bootloader flaws are blocked by SafetyNet.
Because this isn't strict cryptographic verification, but a mess of heuristics and guesswork, it's a constant cat and mouse game.
What Google really achieved here is to make it expensive enough that no casual user can bypass it to e.g. cheat in Pokemon Go, but only a determined attacker has a chance.
And with "determined attacker" I'm not just talking about states, but even e.g. movie pirates breaking DRM to rip Netflix movies.
Of course, even full cryptographic attestation isn't perfect, and can be bypassed with enough effort. As shown by the famous iPhone hardware jailbreak, where you drill into the SoC and solder directly to the CPU's internal wiring.
The problem being that there are many millions of people who can't afford to replace a phone they only recently bought just because the vendor never updates it, which means those banks and things can't in practice demand that people do that. Indeed, it creates the opposite problem, because installing a custom ROM on that device would give it a patched kernel but cause it to fail attestation, so what the attestation is actually doing is requiring those people to continue to use the vulnerable OS.
The proper solution is one we had with desktop computing for decades. If you keep the key material on your eID or bank card, you don't need a locked down operating system. Which then allows devices to live for much longer.
We're slowly losing the war on General Purpose Computing.
https://media.ccc.de/v/28c3-4848-en-the_coming_war_on_genera...
A lot of phones don't receive any upgrades after 1 or 2 years...
I wish that Google would have forced vendors to implement a proper hardware abstraction (uefi or similar) so that a single kernel could run on any smartphone, just like it's the case for PCs...
Just like Microsoft with Windows.
(I've been one of the maintainers of runc -- the most widely use used container runtime on Linux -- for more than a decade, so I'm at least somewhat well-informed on the topic.)
The duct tape criticisms are fair when talking about other vulnerabilities (such as when container runtimes have misconfiguration or other inatomicity bugs) but not really here in the context of a kernel arbitrary code execution gadget. It also seems quite unlikely that the illumos kernel doesn't contain any of these kinds of bugs.
I don't see any ports dependency issues as there's been binary packages available forever. Even the install process is a lot faster than any linux I have to use at $work, not to mention easier to automate with autoinstall[0] if needed.
So you want a bank card/ID card to be required each time you use Google Pay? What's the point of Google Pay then.
https://source.android.com/docs/core/architecture/kernel/gen...
Maybe I could even duct-tape it to my phone if I really want to do that.
From then your Google Pay account is authorized to initiate charges until you tell your bank otherwise and you don't need the card again unless you want to sign up for Venmo etc.
And it makes things easy if someone steals your phone, because you just sign into the payment processor and deauthorize the device or, if they've already changed your password etc., sign into (or go to) the bank and deauthorize the payment processor.
That then allows you to do secure NFC credit card payments even on a rooted phone with custom ROM.
The alternative would be for the hardware market to be less consolidated (the government keeps allowing Qualcomm to buy up competitors) so that the chip companies would have to compete on things like this. But that's no excuse for Google to be sitting on their hands when they could fix it too.
Not a great solution.
Which hasn't been an issue since Chip & PIN became required, 22 years ago (at least over here).
GhostLock (CVE-2026-43499) is a Linux kernel vulnerability found by VEGA that exists in every major distribution since 2011. Triggering the bug does not require any special kernel config or privilege. By turning it into a 97% stable privilege escalation and container escape, Google has rewarded us $92,337 in kernelCTF. This writeup covers the technical details of the exploit.
GhostLock (CVE-2026-43499) lets an unprivileged local attacker:
GhostLock was introduced in Linux 2.6.39 and fixed in Linux 7.1. It has existed in the Linux kernel for more than 15 years. Every Linux distribution without the patch is affected and should consider upgrading to the latest LTS version.
Your browser does not support the video tag.
GhostLock was introduced with the rtmutex rework in 8161239a8bcc (“rtmutex: Simplify PI algorithm and make highest prio task get lock”), and sat untouched for about fifteen years until the April 2026 fix in 3bfdc63936dd (“rtmutex: Use waiter::task instead of current in remove_waiter()”). The affected range is v2.6.39-rc1 to v7.1-rc1, with CONFIG_FUTEX_PI=y the only requirement and no capabilities or user namespaces needed.
remove_waiter() in kernel/locking/rtmutex.c clears current->pi_blocked_on. That is correct on the normal slow path, where current is the task that owns the waiter. It is wrong on the proxy path. rt_mutex_start_proxy_lock() enqueues, and on error rolls back, an rt_mutex_waiter on behalf of another task, so current is the requeuer rather than the waiter.
The waiter object lives on the stack of a task sleeping in FUTEX_WAIT_REQUEUE_PI. A FUTEX_CMP_REQUEUE_PI then proxies that waiter onto the target PI futex. When the rtmutex chain walk reports a deadlock, the rollback dequeues the waiter from the lock but clears pi_blocked_on on the requeuer. The waiter task keeps pi_blocked_on pointing at its own stack frame, which is popped the moment the waiter returns to userspace. Any later PI chain walk through that task follows the dangling pointer.
Like other lifecycle bugs, this occurs when a function is used by a caller it was never designed to support.
The helper function remove_waiter() was originally written for exactly one scenario: a thread blocks on its own, then cleans up after itself. So it has always assumed that current (whichever thread happens to be running) is the waiter it needs to clean up, and clears current->pi_blocked_on accordingly.
However, Requeue-PI breaks that assumption. Through rt_mutex_start_proxy_lock(), this helper is now used to clean up on behalf of a different, sleeping thread. In that path, current is the thread that issued FUTEX_CMP_REQUEUE_PI rather than the actual waiter.
When __rt_mutex_start_proxy_lock() returns -EDEADLK, it rolls back via remove_waiter(), the misused helper.
int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter, struct task_struct *task){ int ret; raw_spin_lock_irq(&lock->wait_lock); ret = __rt_mutex_start_proxy_lock(lock, waiter, task); if (unlikely(ret)) remove_waiter(lock, waiter); // ret == -EDEADLK raw_spin_unlock_irq(&lock->wait_lock); return ret;}
remove_waiter() then scrubs the wrong task.
static void __sched remove_waiter(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter){ ... raw_spin_lock(¤t->pi_lock); rt_mutex_dequeue(lock, waiter); current->pi_blocked_on = NULL; // should be waiter->task raw_spin_unlock(¤t->pi_lock); ...}
waiter is the object that lives on the sleeping thread’s own stack, while current here is the thread that requested the requeue. The fix locks waiter->task->pi_lock and clears waiter->task->pi_blocked_on instead. This issue slips past lockdep, which only checks that a pi_lock is held but not whose it is.
Triggering the -EDEADLK Path. Reaching the -EDEADLK rollback needs a PI dependency cycle built from three futex words and three threads.
f_pi_chain, a PI futex, locked first by the waiter thread.f_pi_target, a PI futex, locked first by the owner thread. This is the requeue target.f_wait, the plain futex the waiter blocks on with FUTEX_WAIT_REQUEUE_PI.The sequence is:
f_pi_chain, then blocks in FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target). Its rt_mutex_waiter is now on its stack.f_pi_target, then blocks on f_pi_chain, which the waiter holds.FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target).The requeue tries to proxy the waiter onto f_pi_target. The owner of f_pi_target is already blocked behind the waiter through f_pi_chain, so the chain walk closes the loop waiter -> f_pi_target -> owner -> f_pi_chain -> waiter. It returns -EDEADLK and takes the buggy rollback. The waiter wakes with a dangling pi_blocked_on.
Here the only ordering that matters is the requeuer rolling back the waiter while the waiter still owns the soon-to-be-freed object, and once the cycle is staged that happens on its own. After it resolves there is no time pressure at all. The waiter sits in userspace with a dangling pi_blocked_on, and the follow-up sched_setattr() that walks the chain can fire whenever it likes. The UAF window is wide open.
The catch is where the freed object lives on the kernel stack (stack-UAF if we call ret out of the futex syscall a “free”). To reclaim it, we need to find a syscall that can land controlled bytes back on the same stack at the same depth (offset).
Staging the three-futex cycle leaves the waiter task in userspace with pi_blocked_on dangling into its old FUTEX_WAIT_REQUEUE_PI frame. Everything below rides on that one pointer.
Note that three threads is for better understanding. To win the race and trigger UAF, you only need one CPU core.
By now we hold a pointer into freed kernel stack, and we can trigger, at will, a kernel access that dereferences it as an rt_mutex_waiter. We can spray controlled bytes onto that stack and forge the rt_mutex_waiter outright. Depending on the layout we forge, this one access yields several primitives, two main ones:
Several pointer dereferences and integrity checks run before the primitive fires, and after it fires the kernel returns normally, no crash.
So our main questions, each answered in a section below:
rt_mutex_waiter past its built-in structural checks, and forge pointers that read as valid? -> From fake waiter to a writert_mutex_waiter in the waiter task’s pi_blocked_on.PR_SET_MM_MAP to reclaim the waiter’s own kernel stack and forge a fake rt_mutex_waiter over the freed frame.inet6_protos[IPPROTO_UDP] = <CEA pointer>.inet6_protocol, pivot slots, ROP stack} all together at a known direct-map address.core_pattern’s mode bits, then the rest LPE is pure userspace.What about Android?
This part we are focusing on basic exploit steps of generic x86 Linux systems, our next blog will discuss how to exploit GhostLock on Android, reclaiming stack frame, bypassing both ASLR and CFI.
A prefetch on a given address runs in a different number of cycles depending on whether that address is mapped in the current page tables, so an unprivileged process can time prefetch across the kernel range and read off which addresses are mapped (the prefetch paper has the details).
It works here as Linux barely randomizes the base of its default kernel image (~9 bits of entropy for text base), so a little averaging can recover the KASLR base with near 100% reliability.
In theory any CPU with prefetch and without proper Kernel Page-Table Isolation is affected. But in practice it is more of an x86 technique (unless the ARM target runs KPTI off). kernelCTF images keep KPTI disabled.
kernelCTF images keep KPTI disabled, but even with KPTI on,
prefetchpaired with EntryBleed can still recover the kernel image base through the trampoline.
The CEA (CPU entry area) is a per-CPU x86 structure holding the stacks and register context used for entry and exception handling: on an exception, interrupt, or syscall the CPU switches to a stack that lives in the CEA, and the entry code spills the register frame (pt_regs) there. An unprivileged userspace program can trigger a software exception and write its own register context into the pt_regs saved on a CEA exception stack. Before 6.2 the CEA sat at a completely fixed address, so we can place about 120 bytes of contiguous controlled memory at a known kernel address, which is very handy for forging structures, for absorbing the side effects of the pointer dereferences along the way, and for staging a ROP stack.
After Project Zero’s Bringing back the stack attack writeup, the kernel started strongly randomizing the CEA’s virtual address (since 6.2). But the virtual address of the CPU entry area is never needed, as the CEA’s physical offset is fixed, so its direct-map alias follows from the physmap base (same observation @kqx used).
That direct-map address is easy to leak with prefetch, plus candidate-edge normalization and a check against the predicted CEA page to reject neighbouring aliases. (The direct-map leak is noisier than the text one and may need a little more tuning, but it lands at very high accuracy on the target in the end.) So we can always compute the CEA’s other virtual-address mapping:
cea_direct = physmap_base + CPU1_CEA_BASE
Note that each CPU’s CEA virtual address is randomized to a different place. Their physical addresses are all fixed, though, and this offset depends mainly on the target’s kernel version and boot memory size. In the kernelCTF LTS 6.12.80 3.5G-boot environment, it is 0x11c517000(+0x1f58).
PR_SET_MM_MAPThe dangling object is the waiter’s own stack rt_mutex_waiter.
struct rt_mutex_waiter { struct rt_waiter_node tree; // rb node, lives in lock->waiters struct rt_waiter_node pi_tree; struct task_struct *task; struct rt_mutex_base *lock; unsigned int wake_state; struct ww_acquire_ctx *ww_ctx;};
Controlled bytes have to land back over that exact frame, on the waiter thread’s own stack, and stay there long enough to be read. The waiter thread returns from the futex syscall and immediately calls prctl(PR_SET_MM, PR_SET_MM_MAP, ...). Inside, prctl_set_mm_map() copies a user-supplied auxv into a fixed-size unsigned long user_auxv[AT_VECTOR_SIZE] stack buffer. That buffer sits at roughly the same stack depth as the freed waiter, so it is a large, naturally-aligned, namespace-free block of controlled qwords landing right on top of the old object.
The auxv is laid out so the overlapping qwords become:
tree, an rb node crafted so erasing it promotes one chosen child pointer (W0_BASE, below) into the tree root.task, set to &init_task, a valid task_struct so the chain walk’s task derefs are safe.lock, set to &inet6_protos[IPPROTO_UDP] - 8, the write target.wake_state, set to 0.The auxv is backed by a memfd and positioned so the copy straddles a page boundary. A sibling thread races fallocate(PUNCH_HOLE) on the trailing page during the prctl, which stretches the copy_from_user window. The forged waiter stays live on the stack while, on another CPU, a consumer thread fires sched_setattr() on the waiter to walk the PI chain.
The race window is wide and we believe GhostLock is also exploitable on a single-core CPU.
clone/setsockopt/pselect/keyctland other syscalls with large controlled stack locals work the same way.prctlis just convenient here. The buffer is large, aligned, and needs no namespace.
Here’s more useful syscalls that can reclaim the stack frame in our open-sourced PoC code.
Controlling the waiter does not give an arbitrary write. The chain walk only does:
task->pi_blocked_on -> fake waiterfake waiter->lock -> fake rt_mutex_basert_mutex_dequeue(lock, waiter) // rb_erase on lock->waiters
rt_mutex_dequeue() is an rb-tree erase, and erasing a single-child root writes that child into the root slot. Pointing lock at target - 8 lines the rt_mutex_base fields up over the data around the target pointer.
target - 8 -> raw_spinlock_t wait_lock (must read as "unlocked")target -> waiters.rb_root.rb_node (this slot gets written)target + 8 -> waiters.rb_leftmosttarget + 16 -> owner
The fake waiter’s rb node is crafted so the erase writes exactly one child pointer into rb_root.rb_node. The write primitive itself is a single constrained store: *(uint64_t *)target = W0_BASE.
The constraints are also highly strict: The qword before the target must read as an unlocked spinlock, meaning zero in the low 4 bytes, or the trylock fails and the walk exits without writing. The qwords after it (rb_leftmost, owner) must not steer the walk into an uncontrolled top waiter or owner. An unmapped value there faults and panics the box. The equivalent target address constraint is roughly as follows (*target will be written to a pointer):
*(u32 *)(target - 0x08) == 0*(u64 *)(target + 0x08) == 0 // simplified((*(u64 *)(target + 0x10)) & ~1ULL) == 0// Then we can do:*(u64 *)target = &W0->tree.entry // W0_BASE
Here the W0_BASE has to point at something that stays valid through the comparisons and the no-owner wakeup later in the same rt_mutex_adjust_prio_chain(). We point it at the direct-map alias of the CPU entry area, which pays off twice:
W0 that survives the walk.W0 no longer has to look like a waiter at all, so we can re-spray the CEA with whatever the kernel expects the target to point at (if we overwritten a function table pointer with W0, we can now fake function pointer in CEA to get Control Flow Hijack).Why the CEA?
There’s several ways to spray controlled memory at a fixed (knowned) kernel address. The CEA is one of the more efficient, and its main limit is the ~120-byte small size. NPerm, kernelsnitch and other tricks can do the same job with more room.
Before the trigger, W0 is spraied as that fake waiter and lock pair: task = &init_task, a legit prio, and a lock whose wait_lock reads unlocked and whose owner is benign, so the dequeue, re-enqueue, priority update and wakeup all survive.
The following figure shows how CPU entry area is used to first hold fake rt_mutex_waiter and lock structures, then serve inet6 (next section), ROP stack and JOP gadgets for stack pivoting at the same time, and eventually use a very short ROP to perform the DirtyMode and safely halt the core.
inet6_protos[IPPROTO_UDP] to helpStart from now the exploit path would differ from targets, as of regular x86_64 Linux kernel, we can pick a shorter path by just overwriting some function table (or any object that contains one), as we already have KASLR leaked and ready to get a CFH.
A scan of writable data turns up many pointer tables whose neighbours satisfy the layout above. inet6_protos[IPPROTO_UDP] is a nice one. The neighbours fall out for free, and the trigger is a trivial unprivileged loopback packet.
inet6_protos[16] == NULL // fake wait_lock -> unlockedinet6_protos[17] == &udpv6_protocol // <- target (IPPROTO_UDP)inet6_protos[18] == NULL // fake rb_leftmostinet6_protos[19] == NULL // fake owner
After the write, inet6_protos[IPPROTO_UDP] points into the CEA page, where the kernel expects an inet6_protocol.
struct inet6_protocol { int (*handler)(struct sk_buff *skb); int (*err_handler)(...); unsigned int flags;};
So W0 is re-spraied as a fake inet6_protocol. handler is the first pivot gadget, err_handler is unused, and flags is INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL. Once we send a loopback IPv6 UDP (connect then write to ::1), the kernel will dereference the handler and give us a PC control.
We use the same compact CEA window to holds multiple objects: {the fake inet6_protocol, a few JOP/pivot slots, the final ROP stack}. On Google’s lts-6.12.80 kernel target we are not lucky enough to find a nice single stack pivot target, so the chain takes one extra load/call to land the CEA address in rbp, then pivots with mov rsp, rbp; pop rbp; ret.
A ret2usr or a full /proc/%P/fd/x overwrite would run to around ten gadget qwords, which is too long. So we use DirtyMode as the final exploit stage: a single write, with an almost-garbage value, that flips a permission bit. After it, LPE can be done purely in userspace.
Here we target at the core_pattern sysctl’s mode flags:
static struct ctl_table coredump_sysctls[] = { ... { .procname = "core_pattern", .data = core_pattern, .maxlen = CORENAME_MAX_SIZE, .mode = 0644, .proc_handler = proc_dostring_coredump }, ...};
coredump_sysctls lives in writable kernel data (share same KASLR slide with kernel image). The ROP writes a permissive value to coredump_sysctls[1].mode. Any value with the write bit (2nd LSB) set is enough.
Here we uses a short pop reg; mov [reg], reg; ret plus an msleep to park the hijacked thread safely. And now /proc/sys/kernel/core_pattern is now world-writable, so an unprivileged process opens it, writes |/proc/%P/fd/666 %P, and crashes a helper to trick kernel runs our binary as root.
The initial write primitive (the rb-tree write) cannot reach
coredump_sysctls[1].modedirectly because of where it lands, so the mode flip is done from the short ROP stage.
The full exploit code can be found in our open source security research project, CyberMeowfia.
kernelCTF is a race, and the shortest reliable chain wins. NPerm-backed memory makes a fine large fake stack after the hijack, and there are heavier routes that would also work, including Lukas Maar’s heap-KASLR leak. Each adds another stage and increases time cost. CEA plus DirtyMode is the shortest path to a one-write win, and on the remote it win us the flag in about 5 seconds.
diff --git a/kernel/locking/rtmutex.c b/kernel/locking/rtmutex.c--- a/kernel/locking/rtmutex.c+++ b/kernel/locking/rtmutex.c@@ -1544,6 +1544,8 @@ static bool rtmutex_spin_on_owner(struct rt_mutex_base *lock, * * Must be called with lock->wait_lock held and interrupts disabled. It must * have just failed to try_to_take_rt_mutex().+ *+ * When invoked from rt_mutex_start_proxy_lock() waiter::task != current ! */ static void __sched remove_waiter(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter)@@ -1551,14 +1553,15 @@ static void __sched remove_waiter(struct rt_mutex_base *lock, { bool is_top_waiter = (waiter == rt_mutex_top_waiter(lock)); struct task_struct *owner = rt_mutex_owner(lock);+ struct task_struct *waiter_task = waiter->task; struct rt_mutex_base *next_lock; lockdep_assert_held(&lock->wait_lock);- raw_spin_lock(¤t->pi_lock);- rt_mutex_dequeue(lock, waiter);- current->pi_blocked_on = NULL;- raw_spin_unlock(¤t->pi_lock);+ scoped_guard(raw_spinlock, &waiter_task->pi_lock) {+ rt_mutex_dequeue(lock, waiter);+ waiter_task->pi_blocked_on = NULL;+ } /* * Only update priority if the waiter was the highest priority@@ -1594,7 +1597,7 @@ static void __sched remove_waiter(struct rt_mutex_base *lock, raw_spin_unlock_irq(&lock->wait_lock); rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock,- next_lock, NULL, current);+ next_lock, NULL, waiter_task);
We had also sent a fix to security@kernel.org before v1 landed. Its core:
static void __sched remove_waiter(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter) struct rt_mutex_waiter *waiter, struct task_struct *task){ ... raw_spin_lock(¤t->pi_lock); raw_spin_lock(&task->pi_lock); rt_mutex_dequeue(lock, waiter); current->pi_blocked_on = NULL; raw_spin_unlock(¤t->pi_lock); if (task->pi_blocked_on == waiter) task->pi_blocked_on = NULL; raw_spin_unlock(&task->pi_lock); ... rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock, next_lock, NULL, current); next_lock, NULL, task);}
Instead of reading the task out of waiter->task, the callers pass in the owning task (current on the self-blocking path, the proxied task on the rt_mutex_start_proxy_lock() rollback), and pi_blocked_on is cleared only when it still points at this waiter. task is always a valid task and the clear is guarded.
The stack-reuse step relies on the freed waiter frame and the later user_auxv frame overlapping deterministically. With RANDOMIZE_KSTACK_OFFSET on they no longer do, and the step becomes a roughly 1/32 (5-bit) stack-offset guess. Both submitted targets leave it off by default. The mitigation target turns it on, so this path was not used there.
STATIC_USERMODE_HELPER would close this particular DirtyMode path. But the same idea can be generalized to any /proc/sys knob whose ctl_table::mode gates access and whose table sits in predictable writable kernel data.
For all bugs found by VEGA, we follow our standard 90+30 days disclosure policy as described on our About page.
[
Previous Post Longinus: 2 Boundaries in One Bug, Piercing Chrome’s Renderer and V8 Sandbox with a Single Vulnerability, CVE-2026-6307
](https://nebusec.ai/research/v8-cve-2026-6307-writeup#post-title)[
Next Post You're at the newest post!
](#)